From f8ead215d875cda2f4860723810fc9e8924cb743 Mon Sep 17 00:00:00 2001 From: Mike Mueller Date: Thu, 7 May 2026 11:01:30 +0200 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20hello-agent=20=E2=80=94=20h?= =?UTF-8?q?eadless=20RustDesk-protocol-compatible=20Windows=20agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single-binary, Flutter-free remote-support agent that speaks the stock RustDesk wire protocol. Designed for one-line MDM deployment against a self-hosted rustdesk-server: a supporter using the unmodified rustdesk.exe client connects, the controlled-side user gets a native Win32 approval prompt, click Yes / No. CLI surface hello-agent.exe --install # register + start service hello-agent.exe --uninstall # stop, delete, clean up hello-agent.exe --config # admin-UI deploy string hello-agent.exe --install --config # MDM one-liner --config accepts both forms emitted by the rustdesk-server admin UI: the reversed-base64 deploy string and the host=,key=,api=,relay= filename form. Decoded via the upstream custom_server module, persisted via hbb_common::config::Config::set_option. Architecture --service runs as a Session 0 LocalSystem service. It polls WTSGetActiveConsoleSessionId and (re)spawns hello-agent.exe --server into the active console session via librustdesk::platform::run_as_user, handling the Session 0 → user-session token impersonation. --server is the worker. It boots three concurrent components: 1. cm_popup: an IPC listener on the rustdesk `_cm` named pipe 2. librustdesk::start_server(true, false): the upstream protocol stack — rendezvous mediator, NAT punch, IPC server, screen capture, login validation, hbbs_http heartbeat / sysinfo sync 3. (implicit) ApproveMode::Click is pinned in config, so every incoming connection routes through cm_popup The popup mechanism reuses an existing upstream contract without any patches to the protocol code: when a peer connects with no password, Connection::start in the upstream code calls try_start_cm_ipc, which ipc::connect-s the `_cm` pipe before falling back to spawning a Flutter CM child. Since cm_popup is up first, step 1 succeeds; we read the Data::Login{authorized:false} frame, show MessageBoxTimeoutW (Yes/No, 60s, top-most, system-modal), and reply Data::Authorize or Data::Close. Source tree src/main.rs CLI dispatcher + run_server() composition src/cli.rs hand-rolled argv parser + unit tests src/service.rs windows-service install/uninstall/dispatcher src/config_import.rs --config blob decoding + persistence src/cm_popup.rs _cm IPC listener + Win32 approval dialog Vendoring The upstream RustDesk crate is vendored under vendor/rustdesk/ — full workspace including libs/{hbb_common, scrap, enigo, clipboard, virtual_display, remote_printer}. This makes the build self-contained (no submodules, no sibling-repo checkout in CI) and gives us freedom to fork in a different direction later. Excluded from the vendor: .git, target/, flutter/, appimage/, flatpak/, fastlane/, docs/, examples/, ci/, build.py, Dockerfile, upstream README/CLAUDE/AGENTS/GEMINI. One local divergence vs. upstream: vendor/rustdesk/src/lib.rs flips `mod custom_server` → `pub mod custom_server` so config_import.rs can call get_custom_server_from_string without going through the ui_interface shim. Documented in README.md → "Re-syncing the vendored copy". CI .gitea/workflows/build-windows.yml builds on a self-hosted Windows runner with Rust 1.75, LLVM 15.0.6 (libclang for bindgen via libvpx-sys), and a vcpkg cache. The vendored vcpkg.json drives x64-windows-static deps. The workflow stages the resulting hello-agent.exe into SignOutput\, reports authenticode signing status (warns on unsigned), and uploads as artifact. ~15 min full build, faster on incremental. Out of scope for this commit: Linux/macOS builds, code signing, MSI packaging, coexistence with stock rustdesk on the same box (currently shares the RustDesk APP_NAME and config dir). Co-Authored-By: Claude Opus 4.7 (1M context) --- .cargo/config.toml | 21 + .gitea/workflows/build-windows.yml | 162 + .gitignore | 5 + Cargo.lock | 9821 +++++++++++++++++ Cargo.toml | 62 + README.md | 249 + build.rs | 40 + ci/runners/linux/provision.sh | 271 + resources/build_ico.py | 123 + resources/icon.ico | Bin 0 -> 123596 bytes resources/icon.png | Bin 0 -> 28901 bytes src/cli.rs | 190 + src/cm_popup.rs | 399 + src/config_import.rs | 93 + src/main.rs | 213 + src/service.rs | 919 ++ src/unattended_password.rs | 123 + vendor/rustdesk/.cargo/config.toml | 16 + vendor/rustdesk/.gitattributes | 1 + vendor/rustdesk/Cargo.toml | 258 + vendor/rustdesk/LICENCE | 661 ++ vendor/rustdesk/build.rs | 94 + vendor/rustdesk/libs/clipboard/Cargo.toml | 57 + vendor/rustdesk/libs/clipboard/build.rs | 35 + vendor/rustdesk/libs/clipboard/src/cliprdr.h | 247 + .../libs/clipboard/src/context_send.rs | 79 + vendor/rustdesk/libs/clipboard/src/lib.rs | 298 + .../libs/clipboard/src/platform/mod.rs | 26 + .../clipboard/src/platform/unix/filetype.rs | 188 + .../clipboard/src/platform/unix/fuse/cs.rs | 1010 ++ .../clipboard/src/platform/unix/fuse/mod.rs | 225 + .../clipboard/src/platform/unix/local_file.rs | 387 + .../platform/unix/macos/item_data_provider.rs | 77 + .../clipboard/src/platform/unix/macos/mod.rs | 14 + .../platform/unix/macos/paste-files-macos.png | Bin 0 -> 39355 bytes .../src/platform/unix/macos/paste_observer.rs | 179 + .../src/platform/unix/macos/paste_task.rs | 639 ++ .../platform/unix/macos/pasteboard_context.rs | 460 + .../libs/clipboard/src/platform/unix/mod.rs | 58 + .../clipboard/src/platform/unix/serv_files.rs | 271 + .../libs/clipboard/src/platform/windows.rs | 1327 +++ .../libs/clipboard/src/windows/wf_cliprdr.c | 3381 ++++++ vendor/rustdesk/libs/enigo/.gitattributes | 1 + vendor/rustdesk/libs/enigo/.gitignore | 14 + vendor/rustdesk/libs/enigo/.travis.yml | 15 + .../rustdesk/libs/enigo/.vscode/launch.json | 13 + vendor/rustdesk/libs/enigo/Cargo.toml | 44 + vendor/rustdesk/libs/enigo/LICENSE | 21 + vendor/rustdesk/libs/enigo/appveyor.yml | 121 + vendor/rustdesk/libs/enigo/build.rs | 61 + vendor/rustdesk/libs/enigo/rustfmt.toml | 1 + vendor/rustdesk/libs/enigo/src/dsl.rs | 184 + vendor/rustdesk/libs/enigo/src/lib.rs | 552 + vendor/rustdesk/libs/enigo/src/linux/mod.rs | 4 + .../rustdesk/libs/enigo/src/linux/nix_impl.rs | 392 + vendor/rustdesk/libs/enigo/src/linux/xdo.rs | 459 + .../rustdesk/libs/enigo/src/macos/keycodes.rs | 120 + .../libs/enigo/src/macos/macos_impl.rs | 864 ++ vendor/rustdesk/libs/enigo/src/macos/mod.rs | 4 + .../rustdesk/libs/enigo/src/win/keycodes.rs | 83 + vendor/rustdesk/libs/enigo/src/win/mod.rs | 4 + .../rustdesk/libs/enigo/src/win/win_impl.rs | 478 + vendor/rustdesk/libs/hbb_common/.gitignore | 3 + vendor/rustdesk/libs/hbb_common/Cargo.toml | 100 + vendor/rustdesk/libs/hbb_common/build.rs | 14 + .../libs/hbb_common/protos/message.proto | 984 ++ .../libs/hbb_common/protos/rendezvous.proto | 259 + .../libs/hbb_common/src/bytes_codec.rs | 280 + .../rustdesk/libs/hbb_common/src/compress.rs | 34 + vendor/rustdesk/libs/hbb_common/src/config.rs | 3491 ++++++ .../libs/hbb_common/src/fingerprint.rs | 381 + vendor/rustdesk/libs/hbb_common/src/fs.rs | 1806 +++ .../rustdesk/libs/hbb_common/src/keyboard.rs | 39 + vendor/rustdesk/libs/hbb_common/src/lib.rs | 633 ++ vendor/rustdesk/libs/hbb_common/src/mem.rs | 14 + .../libs/hbb_common/src/password_security.rs | 474 + .../libs/hbb_common/src/platform/linux.rs | 572 + .../libs/hbb_common/src/platform/macos.rs | 55 + .../libs/hbb_common/src/platform/mod.rs | 82 + .../libs/hbb_common/src/platform/windows.rs | 198 + .../libs/hbb_common/src/protos/mod.rs | 1 + vendor/rustdesk/libs/hbb_common/src/proxy.rs | 716 ++ .../libs/hbb_common/src/socket_client.rs | 348 + vendor/rustdesk/libs/hbb_common/src/stream.rs | 149 + vendor/rustdesk/libs/hbb_common/src/tcp.rs | 344 + vendor/rustdesk/libs/hbb_common/src/tls.rs | 121 + vendor/rustdesk/libs/hbb_common/src/udp.rs | 171 + .../rustdesk/libs/hbb_common/src/verifier.rs | 257 + vendor/rustdesk/libs/hbb_common/src/webrtc.rs | 770 ++ .../rustdesk/libs/hbb_common/src/websocket.rs | 531 + .../rustdesk/libs/libxdo-sys-stub/Cargo.toml | 9 + .../rustdesk/libs/libxdo-sys-stub/src/lib.rs | 505 + vendor/rustdesk/libs/portable/.gitignore | 3 + vendor/rustdesk/libs/portable/Cargo.toml | 39 + vendor/rustdesk/libs/portable/build.rs | 20 + vendor/rustdesk/libs/portable/generate.py | 108 + .../rustdesk/libs/portable/requirements.txt | 1 + .../rustdesk/libs/portable/src/bin_reader.rs | 139 + vendor/rustdesk/libs/portable/src/main.rs | 248 + .../rustdesk/libs/portable/src/res/label.png | Bin 0 -> 1234 bytes .../rustdesk/libs/portable/src/res/spin.gif | Bin 0 -> 59332 bytes vendor/rustdesk/libs/portable/src/ui.rs | 232 + .../rustdesk/libs/remote_printer/Cargo.toml | 11 + .../rustdesk/libs/remote_printer/src/lib.rs | 34 + .../libs/remote_printer/src/setup/driver.rs | 202 + .../libs/remote_printer/src/setup/mod.rs | 101 + .../libs/remote_printer/src/setup/port.rs | 128 + .../libs/remote_printer/src/setup/printer.rs | 161 + .../libs/remote_printer/src/setup/setup.rs | 94 + vendor/rustdesk/libs/scrap/.gitignore | 4 + vendor/rustdesk/libs/scrap/Cargo.toml | 68 + vendor/rustdesk/libs/scrap/build.rs | 267 + vendor/rustdesk/libs/scrap/src/android/ffi.rs | 511 + vendor/rustdesk/libs/scrap/src/android/mod.rs | 3 + .../libs/scrap/src/bindings/aom_ffi.h | 10 + .../libs/scrap/src/bindings/vpx_ffi.h | 9 + .../libs/scrap/src/bindings/yuv_ffi.h | 6 + .../rustdesk/libs/scrap/src/common/android.rs | 189 + vendor/rustdesk/libs/scrap/src/common/aom.rs | 581 + .../rustdesk/libs/scrap/src/common/camera.rs | 286 + .../rustdesk/libs/scrap/src/common/codec.rs | 1157 ++ .../rustdesk/libs/scrap/src/common/convert.rs | 236 + vendor/rustdesk/libs/scrap/src/common/dxgi.rs | 264 + .../rustdesk/libs/scrap/src/common/hwcodec.rs | 763 ++ .../rustdesk/libs/scrap/src/common/linux.rs | 139 + .../libs/scrap/src/common/mediacodec.rs | 171 + vendor/rustdesk/libs/scrap/src/common/mod.rs | 547 + .../rustdesk/libs/scrap/src/common/quartz.rs | 151 + .../rustdesk/libs/scrap/src/common/record.rs | 423 + vendor/rustdesk/libs/scrap/src/common/vpx.rs | 26 + .../libs/scrap/src/common/vpxcodec.rs | 597 + vendor/rustdesk/libs/scrap/src/common/vram.rs | 404 + .../rustdesk/libs/scrap/src/common/wayland.rs | 129 + vendor/rustdesk/libs/scrap/src/common/x11.rs | 139 + vendor/rustdesk/libs/scrap/src/dxgi/gdi.rs | 213 + vendor/rustdesk/libs/scrap/src/dxgi/mag.rs | 651 ++ vendor/rustdesk/libs/scrap/src/dxgi/mod.rs | 884 ++ vendor/rustdesk/libs/scrap/src/lib.rs | 26 + .../libs/scrap/src/quartz/capturer.rs | 111 + .../rustdesk/libs/scrap/src/quartz/config.rs | 75 + .../rustdesk/libs/scrap/src/quartz/display.rs | 87 + vendor/rustdesk/libs/scrap/src/quartz/ffi.rs | 241 + .../rustdesk/libs/scrap/src/quartz/frame.rs | 71 + vendor/rustdesk/libs/scrap/src/quartz/mod.rs | 17 + vendor/rustdesk/libs/scrap/src/wayland.rs | 6 + .../libs/scrap/src/wayland/capturable.rs | 60 + .../libs/scrap/src/wayland/display.rs | 256 + .../libs/scrap/src/wayland/pipewire.rs | 1528 +++ .../src/wayland/remote_desktop_portal.rs | 315 + .../libs/scrap/src/wayland/request_portal.rs | 45 + .../scrap/src/wayland/screencast_portal.rs | 106 + .../rustdesk/libs/scrap/src/x11/capturer.rs | 115 + vendor/rustdesk/libs/scrap/src/x11/display.rs | 70 + vendor/rustdesk/libs/scrap/src/x11/ffi.rs | 283 + vendor/rustdesk/libs/scrap/src/x11/iter.rs | 138 + vendor/rustdesk/libs/scrap/src/x11/mod.rs | 10 + vendor/rustdesk/libs/scrap/src/x11/server.rs | 146 + .../rustdesk/libs/virtual_display/Cargo.toml | 10 + .../libs/virtual_display/dylib/Cargo.toml | 19 + .../libs/virtual_display/dylib/build.rs | 35 + .../libs/virtual_display/dylib/src/lib.rs | 191 + .../dylib/src/win10/IddController.c | 1006 ++ .../dylib/src/win10/IddController.h | 161 + .../virtual_display/dylib/src/win10/Public.h | 54 + .../virtual_display/dylib/src/win10/idd.rs | 215 + .../virtual_display/dylib/src/win10/mod.rs | 9 + .../rustdesk/libs/virtual_display/src/lib.rs | 196 + vendor/rustdesk/res/128x128.png | Bin 0 -> 2978 bytes vendor/rustdesk/res/128x128@2x.png | Bin 0 -> 7689 bytes vendor/rustdesk/res/32x32.png | Bin 0 -> 938 bytes vendor/rustdesk/res/64x64.png | Bin 0 -> 1795 bytes vendor/rustdesk/res/DEBIAN/postinst | 27 + vendor/rustdesk/res/DEBIAN/postrm | 11 + vendor/rustdesk/res/DEBIAN/preinst | 16 + vendor/rustdesk/res/DEBIAN/prerm | 27 + vendor/rustdesk/res/PKGBUILD | 35 + vendor/rustdesk/res/ab.py | 791 ++ vendor/rustdesk/res/audits.py | 374 + vendor/rustdesk/res/bump.sh | 3 + vendor/rustdesk/res/design.svg | 374 + vendor/rustdesk/res/device-groups.py | 274 + vendor/rustdesk/res/devices.py | 205 + .../patches/0000-flutter-android-x86.patch | 16 + .../patches/0001-x86-no-debuggable.patch | 24 + vendor/rustdesk/res/gen_icon.sh | 8 + vendor/rustdesk/res/icon.ico | Bin 0 -> 99678 bytes vendor/rustdesk/res/icon.png | Bin 0 -> 40256 bytes vendor/rustdesk/res/inline-sciter.py | 82 + vendor/rustdesk/res/job.py | 321 + vendor/rustdesk/res/lang.py | 90 + vendor/rustdesk/res/logo-header.svg | 1 + vendor/rustdesk/res/logo.svg | 1 + vendor/rustdesk/res/mac-icon.png | Bin 0 -> 35842 bytes vendor/rustdesk/res/mac-tray-dark-x2.png | Bin 0 -> 612 bytes vendor/rustdesk/res/mac-tray-light-x2.png | Bin 0 -> 586 bytes vendor/rustdesk/res/manifest.xml | 36 + vendor/rustdesk/res/msi/.gitignore | 13 + .../rustdesk/res/msi/CustomActions/Common.h | 23 + .../res/msi/CustomActions/CustomActions.cpp | 1080 ++ .../res/msi/CustomActions/CustomActions.def | 16 + .../msi/CustomActions/CustomActions.vcxproj | 86 + .../res/msi/CustomActions/DeviceUtils.cpp | 84 + .../res/msi/CustomActions/FirewallRules.cpp | 413 + .../res/msi/CustomActions/ReadConfig.cpp | 36 + .../res/msi/CustomActions/RemotePrinter.cpp | 517 + .../res/msi/CustomActions/ServiceUtils.cpp | 175 + .../res/msi/CustomActions/dllmain.cpp | 26 + .../res/msi/CustomActions/framework.h | 10 + .../res/msi/CustomActions/packages.config | 5 + vendor/rustdesk/res/msi/CustomActions/pch.cpp | 5 + vendor/rustdesk/res/msi/CustomActions/pch.h | 13 + .../res/msi/Package/Components/Folders.wxs | 38 + .../res/msi/Package/Components/Regs.wxs | 56 + .../res/msi/Package/Components/RustDesk.wxs | 154 + .../Package/Fragments/AddRemoveProperties.wxs | 37 + .../msi/Package/Fragments/CustomActions.wxs | 23 + .../Package/Fragments/ShortcutProperties.wxs | 83 + .../res/msi/Package/Fragments/Upgrades.wxs | 10 + vendor/rustdesk/res/msi/Package/Includes.wxi | 7 + .../msi/Package/Language/Package.en-us.wxl | 56 + .../res/msi/Package/Language/WixExt_en-us.wxl | 32 + vendor/rustdesk/res/msi/Package/License.rtf | 303 + .../rustdesk/res/msi/Package/Package.wixproj | 22 + vendor/rustdesk/res/msi/Package/Package.wxs | 62 + .../res/msi/Package/UI/AnotherApp.wxs | 15 + .../res/msi/Package/UI/MyInstallDirDlg.wxs | 32 + .../res/msi/Package/UI/MyInstallDlg.wxs | 87 + vendor/rustdesk/res/msi/msi.sln | 26 + vendor/rustdesk/res/msi/preprocess.py | 560 + vendor/rustdesk/res/osx-dist.sh | 14 + vendor/rustdesk/res/pacman_install | 47 + vendor/rustdesk/res/pam.d/rustdesk.debian | 5 + vendor/rustdesk/res/pam.d/rustdesk.suse | 5 + vendor/rustdesk/res/rpm-flutter-suse.spec | 98 + vendor/rustdesk/res/rpm-flutter.spec | 98 + vendor/rustdesk/res/rpm-suse.spec | 93 + vendor/rustdesk/res/rpm.spec | 96 + vendor/rustdesk/res/rustdesk-banner.svg | 1 + vendor/rustdesk/res/rustdesk-link.desktop | 11 + vendor/rustdesk/res/rustdesk.desktop | 19 + vendor/rustdesk/res/rustdesk.service | 22 + vendor/rustdesk/res/scalable.svg | 88 + vendor/rustdesk/res/startwm.sh | 130 + vendor/rustdesk/res/strategies.py | 301 + vendor/rustdesk/res/tray-icon.ico | Bin 0 -> 4286 bytes vendor/rustdesk/res/user-groups.py | 302 + vendor/rustdesk/res/users.py | 292 + vendor/rustdesk/res/vcpkg/aom/aom-avx2.diff | 60 + .../rustdesk/res/vcpkg/aom/aom-install.diff | 75 + .../vcpkg/aom/aom-uninitialized-pointer.diff | 13 + vendor/rustdesk/res/vcpkg/aom/portfile.cmake | 79 + vendor/rustdesk/res/vcpkg/aom/vcpkg.json | 18 + .../ffmpeg/0001-create-lib-libraries.patch | 27 + .../res/vcpkg/ffmpeg/0002-fix-msvc-link.patch | 11 + .../ffmpeg/0003-fix-windowsinclude.patch | 13 + .../res/vcpkg/ffmpeg/0004-dependencies.patch | 65 + .../res/vcpkg/ffmpeg/0005-fix-nasm.patch | 78 + .../vcpkg/ffmpeg/0007-fix-lib-naming.patch | 12 + .../res/vcpkg/ffmpeg/0013-define-WINVER.patch | 15 + .../ffmpeg/0020-fix-aarch64-libswscale.patch | 28 + .../vcpkg/ffmpeg/0024-fix-osx-host-c11.patch | 15 + ...av_stream_get_first_dts-for-chromium.patch | 35 + ...0041-add-const-for-opengl-definition.patch | 13 + .../vcpkg/ffmpeg/0042-fix-arm64-linux.patch | 9 + .../res/vcpkg/ffmpeg/0043-fix-miss-head.patch | 12 + vendor/rustdesk/res/vcpkg/ffmpeg/build.sh.in | 152 + ...dd-query_timeout-option-for-h264-hev.patch | 71 + ...-amfenc-reconfig-when-bitrate-change.patch | 71 + .../0004-videotoolbox-changing-bitrate.patch | 85 + .../0005-mediacodec-changing-bitrate.patch | 246 + .../ffmpeg/patch/0006-dlopen-libva.patch | 1883 ++++ .../patch/0007-fix-linux-configure.patch | 30 + .../patch/0008-remove-amf-loop-query.patch | 26 + .../0009-fix-nvenc-reconfigure-blur.patch | 28 + ...10.disable-loading-DLLs-from-app-dir.patch | 31 + ...1-android-mediacodec-encode-align-64.patch | 42 + ...acos-big-sur-CVBufferCopyAttachments.patch | 60 + .../rustdesk/res/vcpkg/ffmpeg/portfile.cmake | 705 ++ .../vcpkg/ffmpeg/vcpkg-cmake-wrapper.cmake | 47 + vendor/rustdesk/res/vcpkg/ffmpeg/vcpkg.json | 44 + .../0003-add-uwp-v142-and-v143-support.patch | 168 + .../libvpx/0004-remove-library-suffixes.patch | 13 + .../rustdesk/res/vcpkg/libvpx/portfile.cmake | 316 + .../libvpx/unofficial-libvpx-config.cmake.in | 49 + vendor/rustdesk/res/vcpkg/libvpx/vcpkg.json | 26 + vendor/rustdesk/res/vcpkg/libvpx/vpx.pc.in | 12 + .../res/vcpkg/libyuv/fix-cmakelists.patch | 80 + .../res/vcpkg/libyuv/libyuv-config.cmake | 5 + .../rustdesk/res/vcpkg/libyuv/portfile.cmake | 81 + vendor/rustdesk/res/vcpkg/libyuv/usage | 4 + vendor/rustdesk/res/vcpkg/libyuv/usage-msvc | 9 + vendor/rustdesk/res/vcpkg/libyuv/vcpkg.json | 22 + .../0003-upgrade-cmake-3.14.patch | 10 + .../res/vcpkg/mfx-dispatch/fix-pkgconf.patch | 39 + .../mfx-dispatch/fix-unresolved-symbol.patch | 66 + .../res/vcpkg/mfx-dispatch/portfile.cmake | 40 + .../res/vcpkg/mfx-dispatch/vcpkg.json | 16 + .../vcpkg/opus/fix-pkgconfig-version.patch | 15 + vendor/rustdesk/res/vcpkg/opus/portfile.cmake | 61 + vendor/rustdesk/res/vcpkg/opus/vcpkg.json | 22 + vendor/rustdesk/res/xorg.conf | 30 + vendor/rustdesk/src/auth_2fa.rs | 204 + vendor/rustdesk/src/cli.rs | 199 + vendor/rustdesk/src/client.rs | 4199 +++++++ vendor/rustdesk/src/client/file_trait.rs | 193 + vendor/rustdesk/src/client/helper.rs | 37 + vendor/rustdesk/src/client/io_loop.rs | 2492 +++++ vendor/rustdesk/src/client/screenshot.rs | 99 + vendor/rustdesk/src/clipboard.rs | 885 ++ vendor/rustdesk/src/clipboard_file.rs | 427 + vendor/rustdesk/src/common.rs | 3007 +++++ vendor/rustdesk/src/core_main.rs | 850 ++ vendor/rustdesk/src/custom_server.rs | 219 + vendor/rustdesk/src/flutter.rs | 2363 ++++ vendor/rustdesk/src/flutter_ffi.rs | 3134 ++++++ vendor/rustdesk/src/hbbs_http.rs | 40 + vendor/rustdesk/src/hbbs_http/account.rs | 366 + vendor/rustdesk/src/hbbs_http/downloader.rs | 309 + vendor/rustdesk/src/hbbs_http/http_client.rs | 336 + .../rustdesk/src/hbbs_http/record_upload.rs | 211 + vendor/rustdesk/src/hbbs_http/sync.rs | 325 + vendor/rustdesk/src/ipc.rs | 1701 +++ vendor/rustdesk/src/kcp_stream.rs | 151 + vendor/rustdesk/src/keyboard.rs | 1598 +++ vendor/rustdesk/src/lan.rs | 344 + vendor/rustdesk/src/lang.rs | 278 + vendor/rustdesk/src/lang/ar.rs | 748 ++ vendor/rustdesk/src/lang/be.rs | 748 ++ vendor/rustdesk/src/lang/bg.rs | 748 ++ vendor/rustdesk/src/lang/ca.rs | 748 ++ vendor/rustdesk/src/lang/cn.rs | 748 ++ vendor/rustdesk/src/lang/cs.rs | 748 ++ vendor/rustdesk/src/lang/da.rs | 748 ++ vendor/rustdesk/src/lang/de.rs | 748 ++ vendor/rustdesk/src/lang/el.rs | 748 ++ vendor/rustdesk/src/lang/en.rs | 278 + vendor/rustdesk/src/lang/eo.rs | 748 ++ vendor/rustdesk/src/lang/es.rs | 748 ++ vendor/rustdesk/src/lang/et.rs | 748 ++ vendor/rustdesk/src/lang/eu.rs | 748 ++ vendor/rustdesk/src/lang/fa.rs | 748 ++ vendor/rustdesk/src/lang/fi.rs | 748 ++ vendor/rustdesk/src/lang/fr.rs | 748 ++ vendor/rustdesk/src/lang/ge.rs | 748 ++ vendor/rustdesk/src/lang/gu.rs | 747 ++ vendor/rustdesk/src/lang/he.rs | 748 ++ vendor/rustdesk/src/lang/hi.rs | 746 ++ vendor/rustdesk/src/lang/hr.rs | 748 ++ vendor/rustdesk/src/lang/hu.rs | 748 ++ vendor/rustdesk/src/lang/id.rs | 748 ++ vendor/rustdesk/src/lang/it.rs | 748 ++ vendor/rustdesk/src/lang/ja.rs | 748 ++ vendor/rustdesk/src/lang/ko.rs | 748 ++ vendor/rustdesk/src/lang/kz.rs | 748 ++ vendor/rustdesk/src/lang/lt.rs | 748 ++ vendor/rustdesk/src/lang/lv.rs | 748 ++ vendor/rustdesk/src/lang/ml.rs | 746 ++ vendor/rustdesk/src/lang/nb.rs | 748 ++ vendor/rustdesk/src/lang/nl.rs | 748 ++ vendor/rustdesk/src/lang/pl.rs | 748 ++ vendor/rustdesk/src/lang/pt_PT.rs | 748 ++ vendor/rustdesk/src/lang/ptbr.rs | 748 ++ vendor/rustdesk/src/lang/ro.rs | 748 ++ vendor/rustdesk/src/lang/ru.rs | 748 ++ vendor/rustdesk/src/lang/sc.rs | 748 ++ vendor/rustdesk/src/lang/sk.rs | 748 ++ vendor/rustdesk/src/lang/sl.rs | 748 ++ vendor/rustdesk/src/lang/sq.rs | 748 ++ vendor/rustdesk/src/lang/sr.rs | 748 ++ vendor/rustdesk/src/lang/sv.rs | 748 ++ vendor/rustdesk/src/lang/ta.rs | 748 ++ vendor/rustdesk/src/lang/template.rs | 748 ++ vendor/rustdesk/src/lang/th.rs | 748 ++ vendor/rustdesk/src/lang/tr.rs | 748 ++ vendor/rustdesk/src/lang/tw.rs | 748 ++ vendor/rustdesk/src/lang/uk.rs | 748 ++ vendor/rustdesk/src/lang/vi.rs | 748 ++ vendor/rustdesk/src/lib.rs | 79 + vendor/rustdesk/src/main.rs | 104 + vendor/rustdesk/src/naming.rs | 28 + vendor/rustdesk/src/platform/delegate.rs | 277 + vendor/rustdesk/src/platform/gtk_sudo.rs | 773 ++ vendor/rustdesk/src/platform/linux.rs | 2279 ++++ .../src/platform/linux_desktop_manager.rs | 744 ++ vendor/rustdesk/src/platform/macos.mm | 909 ++ vendor/rustdesk/src/platform/macos.rs | 1230 +++ vendor/rustdesk/src/platform/mod.rs | 248 + .../platform/privileges_scripts/agent.plist | 37 + .../platform/privileges_scripts/daemon.plist | 30 + .../platform/privileges_scripts/install.scpt | 16 + .../privileges_scripts/uninstall.scpt | 6 + .../platform/privileges_scripts/update.scpt | 26 + vendor/rustdesk/src/platform/win_device.rs | 459 + vendor/rustdesk/src/platform/windows.cc | 1058 ++ vendor/rustdesk/src/platform/windows.rs | 4384 ++++++++ .../src/platform/windows_delete_test_cert.cc | 406 + vendor/rustdesk/src/plugin/callback_ext.rs | 44 + vendor/rustdesk/src/plugin/callback_msg.rs | 411 + vendor/rustdesk/src/plugin/config.rs | 363 + vendor/rustdesk/src/plugin/desc.rs | 100 + vendor/rustdesk/src/plugin/errno.rs | 50 + vendor/rustdesk/src/plugin/ipc.rs | 230 + vendor/rustdesk/src/plugin/manager.rs | 600 + vendor/rustdesk/src/plugin/mod.rs | 188 + vendor/rustdesk/src/plugin/native.rs | 40 + .../src/plugin/native_handlers/macros.rs | 27 + .../src/plugin/native_handlers/mod.rs | 126 + .../src/plugin/native_handlers/session.rs | 219 + .../rustdesk/src/plugin/native_handlers/ui.rs | 143 + vendor/rustdesk/src/plugin/plog.rs | 34 + vendor/rustdesk/src/plugin/plugins.rs | 659 ++ vendor/rustdesk/src/port_forward.rs | 220 + vendor/rustdesk/src/privacy_mode.rs | 431 + vendor/rustdesk/src/privacy_mode/macos.rs | 81 + .../privacy_mode/win_exclude_from_capture.rs | 11 + vendor/rustdesk/src/privacy_mode/win_input.rs | 276 + vendor/rustdesk/src/privacy_mode/win_mag.rs | 57 + .../src/privacy_mode/win_topmost_window.rs | 383 + .../src/privacy_mode/win_virtual_display.rs | 586 + vendor/rustdesk/src/rendezvous_mediator.rs | 933 ++ vendor/rustdesk/src/server.rs | 834 ++ vendor/rustdesk/src/server/audio_service.rs | 527 + .../rustdesk/src/server/clipboard_service.rs | 274 + vendor/rustdesk/src/server/connection.rs | 5786 ++++++++++ vendor/rustdesk/src/server/dbus.rs | 92 + vendor/rustdesk/src/server/display_service.rs | 488 + vendor/rustdesk/src/server/input_service.rs | 2373 ++++ .../rustdesk/src/server/portable_service.rs | 992 ++ vendor/rustdesk/src/server/printer_service.rs | 163 + vendor/rustdesk/src/server/rdp_input.rs | 605 + vendor/rustdesk/src/server/service.rs | 358 + vendor/rustdesk/src/server/terminal_helper.rs | 1062 ++ .../rustdesk/src/server/terminal_service.rs | 1847 ++++ vendor/rustdesk/src/server/uinput.rs | 1307 +++ vendor/rustdesk/src/server/video_qos.rs | 595 + vendor/rustdesk/src/server/video_service.rs | 1419 +++ vendor/rustdesk/src/server/wayland.rs | 308 + vendor/rustdesk/src/service.rs | 11 + vendor/rustdesk/src/tray.rs | 281 + vendor/rustdesk/src/ui.rs | 878 ++ vendor/rustdesk/src/ui/ab.tis | 772 ++ vendor/rustdesk/src/ui/chatbox.html | 35 + vendor/rustdesk/src/ui/cm.css | 290 + vendor/rustdesk/src/ui/cm.html | 21 + vendor/rustdesk/src/ui/cm.rs | 198 + vendor/rustdesk/src/ui/cm.tis | 598 + vendor/rustdesk/src/ui/common.css | 492 + vendor/rustdesk/src/ui/common.tis | 482 + vendor/rustdesk/src/ui/file_transfer.css | 269 + vendor/rustdesk/src/ui/file_transfer.tis | 819 ++ vendor/rustdesk/src/ui/grid.tis | 258 + vendor/rustdesk/src/ui/header.css | 97 + vendor/rustdesk/src/ui/header.tis | 722 ++ vendor/rustdesk/src/ui/index.css | 441 + vendor/rustdesk/src/ui/index.html | 19 + vendor/rustdesk/src/ui/index.tis | 1681 +++ vendor/rustdesk/src/ui/install.html | 22 + vendor/rustdesk/src/ui/install.tis | 70 + vendor/rustdesk/src/ui/msgbox.tis | 390 + vendor/rustdesk/src/ui/port_forward.tis | 77 + vendor/rustdesk/src/ui/printer.tis | 41 + vendor/rustdesk/src/ui/remote.css | 46 + vendor/rustdesk/src/ui/remote.html | 44 + vendor/rustdesk/src/ui/remote.rs | 935 ++ vendor/rustdesk/src/ui/remote.tis | 601 + vendor/rustdesk/src/ui_cm_interface.rs | 1798 +++ vendor/rustdesk/src/ui_interface.rs | 1611 +++ vendor/rustdesk/src/ui_session_interface.rs | 2057 ++++ vendor/rustdesk/src/updater.rs | 290 + vendor/rustdesk/src/version.rs | 3 + .../rustdesk/src/virtual_display_manager.rs | 925 ++ vendor/rustdesk/src/whiteboard/client.rs | 258 + vendor/rustdesk/src/whiteboard/linux.rs | 463 + vendor/rustdesk/src/whiteboard/macos.rs | 323 + vendor/rustdesk/src/whiteboard/mod.rs | 41 + vendor/rustdesk/src/whiteboard/server.rs | 171 + vendor/rustdesk/src/whiteboard/win_linux.rs | 180 + vendor/rustdesk/src/whiteboard/windows.rs | 230 + vendor/rustdesk/vcpkg.json | 105 + 479 files changed, 188052 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitea/workflows/build-windows.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 build.rs create mode 100755 ci/runners/linux/provision.sh create mode 100644 resources/build_ico.py create mode 100644 resources/icon.ico create mode 100644 resources/icon.png create mode 100644 src/cli.rs create mode 100644 src/cm_popup.rs create mode 100644 src/config_import.rs create mode 100644 src/main.rs create mode 100644 src/service.rs create mode 100644 src/unattended_password.rs create mode 100644 vendor/rustdesk/.cargo/config.toml create mode 100644 vendor/rustdesk/.gitattributes create mode 100644 vendor/rustdesk/Cargo.toml create mode 100644 vendor/rustdesk/LICENCE create mode 100644 vendor/rustdesk/build.rs create mode 100644 vendor/rustdesk/libs/clipboard/Cargo.toml create mode 100644 vendor/rustdesk/libs/clipboard/build.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/cliprdr.h create mode 100644 vendor/rustdesk/libs/clipboard/src/context_send.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/lib.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/mod.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/filetype.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/fuse/cs.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/fuse/mod.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/local_file.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/macos/item_data_provider.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/macos/mod.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/macos/paste-files-macos.png create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/macos/paste_observer.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/macos/paste_task.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/mod.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/unix/serv_files.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/platform/windows.rs create mode 100644 vendor/rustdesk/libs/clipboard/src/windows/wf_cliprdr.c create mode 100644 vendor/rustdesk/libs/enigo/.gitattributes create mode 100644 vendor/rustdesk/libs/enigo/.gitignore create mode 100644 vendor/rustdesk/libs/enigo/.travis.yml create mode 100644 vendor/rustdesk/libs/enigo/.vscode/launch.json create mode 100644 vendor/rustdesk/libs/enigo/Cargo.toml create mode 100644 vendor/rustdesk/libs/enigo/LICENSE create mode 100644 vendor/rustdesk/libs/enigo/appveyor.yml create mode 100644 vendor/rustdesk/libs/enigo/build.rs create mode 100644 vendor/rustdesk/libs/enigo/rustfmt.toml create mode 100644 vendor/rustdesk/libs/enigo/src/dsl.rs create mode 100644 vendor/rustdesk/libs/enigo/src/lib.rs create mode 100644 vendor/rustdesk/libs/enigo/src/linux/mod.rs create mode 100644 vendor/rustdesk/libs/enigo/src/linux/nix_impl.rs create mode 100644 vendor/rustdesk/libs/enigo/src/linux/xdo.rs create mode 100644 vendor/rustdesk/libs/enigo/src/macos/keycodes.rs create mode 100644 vendor/rustdesk/libs/enigo/src/macos/macos_impl.rs create mode 100644 vendor/rustdesk/libs/enigo/src/macos/mod.rs create mode 100644 vendor/rustdesk/libs/enigo/src/win/keycodes.rs create mode 100644 vendor/rustdesk/libs/enigo/src/win/mod.rs create mode 100644 vendor/rustdesk/libs/enigo/src/win/win_impl.rs create mode 100644 vendor/rustdesk/libs/hbb_common/.gitignore create mode 100644 vendor/rustdesk/libs/hbb_common/Cargo.toml create mode 100644 vendor/rustdesk/libs/hbb_common/build.rs create mode 100644 vendor/rustdesk/libs/hbb_common/protos/message.proto create mode 100644 vendor/rustdesk/libs/hbb_common/protos/rendezvous.proto create mode 100644 vendor/rustdesk/libs/hbb_common/src/bytes_codec.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/compress.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/config.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/fingerprint.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/fs.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/keyboard.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/lib.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/mem.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/password_security.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/platform/linux.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/platform/macos.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/platform/mod.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/platform/windows.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/protos/mod.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/proxy.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/socket_client.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/stream.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/tcp.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/tls.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/udp.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/verifier.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/webrtc.rs create mode 100644 vendor/rustdesk/libs/hbb_common/src/websocket.rs create mode 100644 vendor/rustdesk/libs/libxdo-sys-stub/Cargo.toml create mode 100644 vendor/rustdesk/libs/libxdo-sys-stub/src/lib.rs create mode 100644 vendor/rustdesk/libs/portable/.gitignore create mode 100644 vendor/rustdesk/libs/portable/Cargo.toml create mode 100644 vendor/rustdesk/libs/portable/build.rs create mode 100755 vendor/rustdesk/libs/portable/generate.py create mode 100644 vendor/rustdesk/libs/portable/requirements.txt create mode 100644 vendor/rustdesk/libs/portable/src/bin_reader.rs create mode 100644 vendor/rustdesk/libs/portable/src/main.rs create mode 100644 vendor/rustdesk/libs/portable/src/res/label.png create mode 100644 vendor/rustdesk/libs/portable/src/res/spin.gif create mode 100644 vendor/rustdesk/libs/portable/src/ui.rs create mode 100644 vendor/rustdesk/libs/remote_printer/Cargo.toml create mode 100644 vendor/rustdesk/libs/remote_printer/src/lib.rs create mode 100644 vendor/rustdesk/libs/remote_printer/src/setup/driver.rs create mode 100644 vendor/rustdesk/libs/remote_printer/src/setup/mod.rs create mode 100644 vendor/rustdesk/libs/remote_printer/src/setup/port.rs create mode 100644 vendor/rustdesk/libs/remote_printer/src/setup/printer.rs create mode 100644 vendor/rustdesk/libs/remote_printer/src/setup/setup.rs create mode 100644 vendor/rustdesk/libs/scrap/.gitignore create mode 100644 vendor/rustdesk/libs/scrap/Cargo.toml create mode 100644 vendor/rustdesk/libs/scrap/build.rs create mode 100644 vendor/rustdesk/libs/scrap/src/android/ffi.rs create mode 100644 vendor/rustdesk/libs/scrap/src/android/mod.rs create mode 100644 vendor/rustdesk/libs/scrap/src/bindings/aom_ffi.h create mode 100644 vendor/rustdesk/libs/scrap/src/bindings/vpx_ffi.h create mode 100644 vendor/rustdesk/libs/scrap/src/bindings/yuv_ffi.h create mode 100644 vendor/rustdesk/libs/scrap/src/common/android.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/aom.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/camera.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/codec.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/convert.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/dxgi.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/hwcodec.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/linux.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/mediacodec.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/mod.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/quartz.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/record.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/vpx.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/vpxcodec.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/vram.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/wayland.rs create mode 100644 vendor/rustdesk/libs/scrap/src/common/x11.rs create mode 100644 vendor/rustdesk/libs/scrap/src/dxgi/gdi.rs create mode 100644 vendor/rustdesk/libs/scrap/src/dxgi/mag.rs create mode 100644 vendor/rustdesk/libs/scrap/src/dxgi/mod.rs create mode 100644 vendor/rustdesk/libs/scrap/src/lib.rs create mode 100644 vendor/rustdesk/libs/scrap/src/quartz/capturer.rs create mode 100644 vendor/rustdesk/libs/scrap/src/quartz/config.rs create mode 100644 vendor/rustdesk/libs/scrap/src/quartz/display.rs create mode 100644 vendor/rustdesk/libs/scrap/src/quartz/ffi.rs create mode 100644 vendor/rustdesk/libs/scrap/src/quartz/frame.rs create mode 100644 vendor/rustdesk/libs/scrap/src/quartz/mod.rs create mode 100644 vendor/rustdesk/libs/scrap/src/wayland.rs create mode 100644 vendor/rustdesk/libs/scrap/src/wayland/capturable.rs create mode 100644 vendor/rustdesk/libs/scrap/src/wayland/display.rs create mode 100644 vendor/rustdesk/libs/scrap/src/wayland/pipewire.rs create mode 100644 vendor/rustdesk/libs/scrap/src/wayland/remote_desktop_portal.rs create mode 100644 vendor/rustdesk/libs/scrap/src/wayland/request_portal.rs create mode 100644 vendor/rustdesk/libs/scrap/src/wayland/screencast_portal.rs create mode 100644 vendor/rustdesk/libs/scrap/src/x11/capturer.rs create mode 100644 vendor/rustdesk/libs/scrap/src/x11/display.rs create mode 100644 vendor/rustdesk/libs/scrap/src/x11/ffi.rs create mode 100644 vendor/rustdesk/libs/scrap/src/x11/iter.rs create mode 100644 vendor/rustdesk/libs/scrap/src/x11/mod.rs create mode 100644 vendor/rustdesk/libs/scrap/src/x11/server.rs create mode 100644 vendor/rustdesk/libs/virtual_display/Cargo.toml create mode 100644 vendor/rustdesk/libs/virtual_display/dylib/Cargo.toml create mode 100644 vendor/rustdesk/libs/virtual_display/dylib/build.rs create mode 100644 vendor/rustdesk/libs/virtual_display/dylib/src/lib.rs create mode 100644 vendor/rustdesk/libs/virtual_display/dylib/src/win10/IddController.c create mode 100644 vendor/rustdesk/libs/virtual_display/dylib/src/win10/IddController.h create mode 100644 vendor/rustdesk/libs/virtual_display/dylib/src/win10/Public.h create mode 100644 vendor/rustdesk/libs/virtual_display/dylib/src/win10/idd.rs create mode 100644 vendor/rustdesk/libs/virtual_display/dylib/src/win10/mod.rs create mode 100644 vendor/rustdesk/libs/virtual_display/src/lib.rs create mode 100644 vendor/rustdesk/res/128x128.png create mode 100644 vendor/rustdesk/res/128x128@2x.png create mode 100644 vendor/rustdesk/res/32x32.png create mode 100644 vendor/rustdesk/res/64x64.png create mode 100755 vendor/rustdesk/res/DEBIAN/postinst create mode 100755 vendor/rustdesk/res/DEBIAN/postrm create mode 100755 vendor/rustdesk/res/DEBIAN/preinst create mode 100755 vendor/rustdesk/res/DEBIAN/prerm create mode 100644 vendor/rustdesk/res/PKGBUILD create mode 100644 vendor/rustdesk/res/ab.py create mode 100755 vendor/rustdesk/res/audits.py create mode 100644 vendor/rustdesk/res/bump.sh create mode 100644 vendor/rustdesk/res/design.svg create mode 100755 vendor/rustdesk/res/device-groups.py create mode 100755 vendor/rustdesk/res/devices.py create mode 100644 vendor/rustdesk/res/fdroid/patches/0000-flutter-android-x86.patch create mode 100644 vendor/rustdesk/res/fdroid/patches/0001-x86-no-debuggable.patch create mode 100644 vendor/rustdesk/res/gen_icon.sh create mode 100644 vendor/rustdesk/res/icon.ico create mode 100644 vendor/rustdesk/res/icon.png create mode 100644 vendor/rustdesk/res/inline-sciter.py create mode 100755 vendor/rustdesk/res/job.py create mode 100644 vendor/rustdesk/res/lang.py create mode 100644 vendor/rustdesk/res/logo-header.svg create mode 100644 vendor/rustdesk/res/logo.svg create mode 100644 vendor/rustdesk/res/mac-icon.png create mode 100644 vendor/rustdesk/res/mac-tray-dark-x2.png create mode 100644 vendor/rustdesk/res/mac-tray-light-x2.png create mode 100644 vendor/rustdesk/res/manifest.xml create mode 100644 vendor/rustdesk/res/msi/.gitignore create mode 100644 vendor/rustdesk/res/msi/CustomActions/Common.h create mode 100644 vendor/rustdesk/res/msi/CustomActions/CustomActions.cpp create mode 100644 vendor/rustdesk/res/msi/CustomActions/CustomActions.def create mode 100644 vendor/rustdesk/res/msi/CustomActions/CustomActions.vcxproj create mode 100644 vendor/rustdesk/res/msi/CustomActions/DeviceUtils.cpp create mode 100644 vendor/rustdesk/res/msi/CustomActions/FirewallRules.cpp create mode 100644 vendor/rustdesk/res/msi/CustomActions/ReadConfig.cpp create mode 100644 vendor/rustdesk/res/msi/CustomActions/RemotePrinter.cpp create mode 100644 vendor/rustdesk/res/msi/CustomActions/ServiceUtils.cpp create mode 100644 vendor/rustdesk/res/msi/CustomActions/dllmain.cpp create mode 100644 vendor/rustdesk/res/msi/CustomActions/framework.h create mode 100644 vendor/rustdesk/res/msi/CustomActions/packages.config create mode 100644 vendor/rustdesk/res/msi/CustomActions/pch.cpp create mode 100644 vendor/rustdesk/res/msi/CustomActions/pch.h create mode 100644 vendor/rustdesk/res/msi/Package/Components/Folders.wxs create mode 100644 vendor/rustdesk/res/msi/Package/Components/Regs.wxs create mode 100644 vendor/rustdesk/res/msi/Package/Components/RustDesk.wxs create mode 100644 vendor/rustdesk/res/msi/Package/Fragments/AddRemoveProperties.wxs create mode 100644 vendor/rustdesk/res/msi/Package/Fragments/CustomActions.wxs create mode 100644 vendor/rustdesk/res/msi/Package/Fragments/ShortcutProperties.wxs create mode 100644 vendor/rustdesk/res/msi/Package/Fragments/Upgrades.wxs create mode 100644 vendor/rustdesk/res/msi/Package/Includes.wxi create mode 100644 vendor/rustdesk/res/msi/Package/Language/Package.en-us.wxl create mode 100644 vendor/rustdesk/res/msi/Package/Language/WixExt_en-us.wxl create mode 100644 vendor/rustdesk/res/msi/Package/License.rtf create mode 100644 vendor/rustdesk/res/msi/Package/Package.wixproj create mode 100644 vendor/rustdesk/res/msi/Package/Package.wxs create mode 100644 vendor/rustdesk/res/msi/Package/UI/AnotherApp.wxs create mode 100644 vendor/rustdesk/res/msi/Package/UI/MyInstallDirDlg.wxs create mode 100644 vendor/rustdesk/res/msi/Package/UI/MyInstallDlg.wxs create mode 100644 vendor/rustdesk/res/msi/msi.sln create mode 100644 vendor/rustdesk/res/msi/preprocess.py create mode 100755 vendor/rustdesk/res/osx-dist.sh create mode 100644 vendor/rustdesk/res/pacman_install create mode 100644 vendor/rustdesk/res/pam.d/rustdesk.debian create mode 100644 vendor/rustdesk/res/pam.d/rustdesk.suse create mode 100644 vendor/rustdesk/res/rpm-flutter-suse.spec create mode 100644 vendor/rustdesk/res/rpm-flutter.spec create mode 100644 vendor/rustdesk/res/rpm-suse.spec create mode 100644 vendor/rustdesk/res/rpm.spec create mode 100644 vendor/rustdesk/res/rustdesk-banner.svg create mode 100644 vendor/rustdesk/res/rustdesk-link.desktop create mode 100644 vendor/rustdesk/res/rustdesk.desktop create mode 100644 vendor/rustdesk/res/rustdesk.service create mode 100644 vendor/rustdesk/res/scalable.svg create mode 100755 vendor/rustdesk/res/startwm.sh create mode 100755 vendor/rustdesk/res/strategies.py create mode 100644 vendor/rustdesk/res/tray-icon.ico create mode 100755 vendor/rustdesk/res/user-groups.py create mode 100755 vendor/rustdesk/res/users.py create mode 100644 vendor/rustdesk/res/vcpkg/aom/aom-avx2.diff create mode 100644 vendor/rustdesk/res/vcpkg/aom/aom-install.diff create mode 100644 vendor/rustdesk/res/vcpkg/aom/aom-uninitialized-pointer.diff create mode 100644 vendor/rustdesk/res/vcpkg/aom/portfile.cmake create mode 100644 vendor/rustdesk/res/vcpkg/aom/vcpkg.json create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0001-create-lib-libraries.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0002-fix-msvc-link.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0003-fix-windowsinclude.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0004-dependencies.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0005-fix-nasm.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0007-fix-lib-naming.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0013-define-WINVER.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/0043-fix-miss-head.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/build.sh.in create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/patch/0011-android-mediacodec-encode-align-64.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/patch/0012-fix-macos-big-sur-CVBufferCopyAttachments.patch create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/portfile.cmake create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/vcpkg-cmake-wrapper.cmake create mode 100644 vendor/rustdesk/res/vcpkg/ffmpeg/vcpkg.json create mode 100644 vendor/rustdesk/res/vcpkg/libvpx/0003-add-uwp-v142-and-v143-support.patch create mode 100644 vendor/rustdesk/res/vcpkg/libvpx/0004-remove-library-suffixes.patch create mode 100644 vendor/rustdesk/res/vcpkg/libvpx/portfile.cmake create mode 100644 vendor/rustdesk/res/vcpkg/libvpx/unofficial-libvpx-config.cmake.in create mode 100644 vendor/rustdesk/res/vcpkg/libvpx/vcpkg.json create mode 100644 vendor/rustdesk/res/vcpkg/libvpx/vpx.pc.in create mode 100644 vendor/rustdesk/res/vcpkg/libyuv/fix-cmakelists.patch create mode 100644 vendor/rustdesk/res/vcpkg/libyuv/libyuv-config.cmake create mode 100644 vendor/rustdesk/res/vcpkg/libyuv/portfile.cmake create mode 100644 vendor/rustdesk/res/vcpkg/libyuv/usage create mode 100644 vendor/rustdesk/res/vcpkg/libyuv/usage-msvc create mode 100644 vendor/rustdesk/res/vcpkg/libyuv/vcpkg.json create mode 100644 vendor/rustdesk/res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch create mode 100644 vendor/rustdesk/res/vcpkg/mfx-dispatch/fix-pkgconf.patch create mode 100644 vendor/rustdesk/res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch create mode 100644 vendor/rustdesk/res/vcpkg/mfx-dispatch/portfile.cmake create mode 100644 vendor/rustdesk/res/vcpkg/mfx-dispatch/vcpkg.json create mode 100644 vendor/rustdesk/res/vcpkg/opus/fix-pkgconfig-version.patch create mode 100644 vendor/rustdesk/res/vcpkg/opus/portfile.cmake create mode 100644 vendor/rustdesk/res/vcpkg/opus/vcpkg.json create mode 100644 vendor/rustdesk/res/xorg.conf create mode 100644 vendor/rustdesk/src/auth_2fa.rs create mode 100644 vendor/rustdesk/src/cli.rs create mode 100644 vendor/rustdesk/src/client.rs create mode 100644 vendor/rustdesk/src/client/file_trait.rs create mode 100644 vendor/rustdesk/src/client/helper.rs create mode 100644 vendor/rustdesk/src/client/io_loop.rs create mode 100644 vendor/rustdesk/src/client/screenshot.rs create mode 100644 vendor/rustdesk/src/clipboard.rs create mode 100644 vendor/rustdesk/src/clipboard_file.rs create mode 100644 vendor/rustdesk/src/common.rs create mode 100644 vendor/rustdesk/src/core_main.rs create mode 100644 vendor/rustdesk/src/custom_server.rs create mode 100644 vendor/rustdesk/src/flutter.rs create mode 100644 vendor/rustdesk/src/flutter_ffi.rs create mode 100644 vendor/rustdesk/src/hbbs_http.rs create mode 100644 vendor/rustdesk/src/hbbs_http/account.rs create mode 100644 vendor/rustdesk/src/hbbs_http/downloader.rs create mode 100644 vendor/rustdesk/src/hbbs_http/http_client.rs create mode 100644 vendor/rustdesk/src/hbbs_http/record_upload.rs create mode 100644 vendor/rustdesk/src/hbbs_http/sync.rs create mode 100644 vendor/rustdesk/src/ipc.rs create mode 100644 vendor/rustdesk/src/kcp_stream.rs create mode 100644 vendor/rustdesk/src/keyboard.rs create mode 100644 vendor/rustdesk/src/lan.rs create mode 100644 vendor/rustdesk/src/lang.rs create mode 100644 vendor/rustdesk/src/lang/ar.rs create mode 100644 vendor/rustdesk/src/lang/be.rs create mode 100644 vendor/rustdesk/src/lang/bg.rs create mode 100644 vendor/rustdesk/src/lang/ca.rs create mode 100644 vendor/rustdesk/src/lang/cn.rs create mode 100644 vendor/rustdesk/src/lang/cs.rs create mode 100644 vendor/rustdesk/src/lang/da.rs create mode 100644 vendor/rustdesk/src/lang/de.rs create mode 100644 vendor/rustdesk/src/lang/el.rs create mode 100644 vendor/rustdesk/src/lang/en.rs create mode 100644 vendor/rustdesk/src/lang/eo.rs create mode 100644 vendor/rustdesk/src/lang/es.rs create mode 100644 vendor/rustdesk/src/lang/et.rs create mode 100644 vendor/rustdesk/src/lang/eu.rs create mode 100644 vendor/rustdesk/src/lang/fa.rs create mode 100644 vendor/rustdesk/src/lang/fi.rs create mode 100644 vendor/rustdesk/src/lang/fr.rs create mode 100644 vendor/rustdesk/src/lang/ge.rs create mode 100644 vendor/rustdesk/src/lang/gu.rs create mode 100644 vendor/rustdesk/src/lang/he.rs create mode 100644 vendor/rustdesk/src/lang/hi.rs create mode 100644 vendor/rustdesk/src/lang/hr.rs create mode 100644 vendor/rustdesk/src/lang/hu.rs create mode 100644 vendor/rustdesk/src/lang/id.rs create mode 100644 vendor/rustdesk/src/lang/it.rs create mode 100644 vendor/rustdesk/src/lang/ja.rs create mode 100644 vendor/rustdesk/src/lang/ko.rs create mode 100644 vendor/rustdesk/src/lang/kz.rs create mode 100644 vendor/rustdesk/src/lang/lt.rs create mode 100644 vendor/rustdesk/src/lang/lv.rs create mode 100644 vendor/rustdesk/src/lang/ml.rs create mode 100644 vendor/rustdesk/src/lang/nb.rs create mode 100644 vendor/rustdesk/src/lang/nl.rs create mode 100644 vendor/rustdesk/src/lang/pl.rs create mode 100644 vendor/rustdesk/src/lang/pt_PT.rs create mode 100644 vendor/rustdesk/src/lang/ptbr.rs create mode 100644 vendor/rustdesk/src/lang/ro.rs create mode 100644 vendor/rustdesk/src/lang/ru.rs create mode 100644 vendor/rustdesk/src/lang/sc.rs create mode 100644 vendor/rustdesk/src/lang/sk.rs create mode 100755 vendor/rustdesk/src/lang/sl.rs create mode 100644 vendor/rustdesk/src/lang/sq.rs create mode 100644 vendor/rustdesk/src/lang/sr.rs create mode 100644 vendor/rustdesk/src/lang/sv.rs create mode 100644 vendor/rustdesk/src/lang/ta.rs create mode 100644 vendor/rustdesk/src/lang/template.rs create mode 100644 vendor/rustdesk/src/lang/th.rs create mode 100644 vendor/rustdesk/src/lang/tr.rs create mode 100644 vendor/rustdesk/src/lang/tw.rs create mode 100644 vendor/rustdesk/src/lang/uk.rs create mode 100644 vendor/rustdesk/src/lang/vi.rs create mode 100644 vendor/rustdesk/src/lib.rs create mode 100644 vendor/rustdesk/src/main.rs create mode 100644 vendor/rustdesk/src/naming.rs create mode 100644 vendor/rustdesk/src/platform/delegate.rs create mode 100644 vendor/rustdesk/src/platform/gtk_sudo.rs create mode 100644 vendor/rustdesk/src/platform/linux.rs create mode 100644 vendor/rustdesk/src/platform/linux_desktop_manager.rs create mode 100644 vendor/rustdesk/src/platform/macos.mm create mode 100644 vendor/rustdesk/src/platform/macos.rs create mode 100644 vendor/rustdesk/src/platform/mod.rs create mode 100644 vendor/rustdesk/src/platform/privileges_scripts/agent.plist create mode 100644 vendor/rustdesk/src/platform/privileges_scripts/daemon.plist create mode 100644 vendor/rustdesk/src/platform/privileges_scripts/install.scpt create mode 100644 vendor/rustdesk/src/platform/privileges_scripts/uninstall.scpt create mode 100644 vendor/rustdesk/src/platform/privileges_scripts/update.scpt create mode 100644 vendor/rustdesk/src/platform/win_device.rs create mode 100644 vendor/rustdesk/src/platform/windows.cc create mode 100644 vendor/rustdesk/src/platform/windows.rs create mode 100644 vendor/rustdesk/src/platform/windows_delete_test_cert.cc create mode 100644 vendor/rustdesk/src/plugin/callback_ext.rs create mode 100644 vendor/rustdesk/src/plugin/callback_msg.rs create mode 100644 vendor/rustdesk/src/plugin/config.rs create mode 100644 vendor/rustdesk/src/plugin/desc.rs create mode 100644 vendor/rustdesk/src/plugin/errno.rs create mode 100644 vendor/rustdesk/src/plugin/ipc.rs create mode 100644 vendor/rustdesk/src/plugin/manager.rs create mode 100644 vendor/rustdesk/src/plugin/mod.rs create mode 100644 vendor/rustdesk/src/plugin/native.rs create mode 100644 vendor/rustdesk/src/plugin/native_handlers/macros.rs create mode 100644 vendor/rustdesk/src/plugin/native_handlers/mod.rs create mode 100644 vendor/rustdesk/src/plugin/native_handlers/session.rs create mode 100644 vendor/rustdesk/src/plugin/native_handlers/ui.rs create mode 100644 vendor/rustdesk/src/plugin/plog.rs create mode 100644 vendor/rustdesk/src/plugin/plugins.rs create mode 100644 vendor/rustdesk/src/port_forward.rs create mode 100644 vendor/rustdesk/src/privacy_mode.rs create mode 100644 vendor/rustdesk/src/privacy_mode/macos.rs create mode 100644 vendor/rustdesk/src/privacy_mode/win_exclude_from_capture.rs create mode 100644 vendor/rustdesk/src/privacy_mode/win_input.rs create mode 100644 vendor/rustdesk/src/privacy_mode/win_mag.rs create mode 100644 vendor/rustdesk/src/privacy_mode/win_topmost_window.rs create mode 100644 vendor/rustdesk/src/privacy_mode/win_virtual_display.rs create mode 100644 vendor/rustdesk/src/rendezvous_mediator.rs create mode 100644 vendor/rustdesk/src/server.rs create mode 100644 vendor/rustdesk/src/server/audio_service.rs create mode 100644 vendor/rustdesk/src/server/clipboard_service.rs create mode 100644 vendor/rustdesk/src/server/connection.rs create mode 100644 vendor/rustdesk/src/server/dbus.rs create mode 100644 vendor/rustdesk/src/server/display_service.rs create mode 100644 vendor/rustdesk/src/server/input_service.rs create mode 100644 vendor/rustdesk/src/server/portable_service.rs create mode 100644 vendor/rustdesk/src/server/printer_service.rs create mode 100644 vendor/rustdesk/src/server/rdp_input.rs create mode 100644 vendor/rustdesk/src/server/service.rs create mode 100644 vendor/rustdesk/src/server/terminal_helper.rs create mode 100644 vendor/rustdesk/src/server/terminal_service.rs create mode 100644 vendor/rustdesk/src/server/uinput.rs create mode 100644 vendor/rustdesk/src/server/video_qos.rs create mode 100644 vendor/rustdesk/src/server/video_service.rs create mode 100644 vendor/rustdesk/src/server/wayland.rs create mode 100644 vendor/rustdesk/src/service.rs create mode 100644 vendor/rustdesk/src/tray.rs create mode 100644 vendor/rustdesk/src/ui.rs create mode 100644 vendor/rustdesk/src/ui/ab.tis create mode 100644 vendor/rustdesk/src/ui/chatbox.html create mode 100644 vendor/rustdesk/src/ui/cm.css create mode 100644 vendor/rustdesk/src/ui/cm.html create mode 100644 vendor/rustdesk/src/ui/cm.rs create mode 100644 vendor/rustdesk/src/ui/cm.tis create mode 100644 vendor/rustdesk/src/ui/common.css create mode 100644 vendor/rustdesk/src/ui/common.tis create mode 100644 vendor/rustdesk/src/ui/file_transfer.css create mode 100644 vendor/rustdesk/src/ui/file_transfer.tis create mode 100644 vendor/rustdesk/src/ui/grid.tis create mode 100644 vendor/rustdesk/src/ui/header.css create mode 100644 vendor/rustdesk/src/ui/header.tis create mode 100644 vendor/rustdesk/src/ui/index.css create mode 100644 vendor/rustdesk/src/ui/index.html create mode 100644 vendor/rustdesk/src/ui/index.tis create mode 100644 vendor/rustdesk/src/ui/install.html create mode 100644 vendor/rustdesk/src/ui/install.tis create mode 100644 vendor/rustdesk/src/ui/msgbox.tis create mode 100644 vendor/rustdesk/src/ui/port_forward.tis create mode 100644 vendor/rustdesk/src/ui/printer.tis create mode 100644 vendor/rustdesk/src/ui/remote.css create mode 100644 vendor/rustdesk/src/ui/remote.html create mode 100644 vendor/rustdesk/src/ui/remote.rs create mode 100644 vendor/rustdesk/src/ui/remote.tis create mode 100644 vendor/rustdesk/src/ui_cm_interface.rs create mode 100644 vendor/rustdesk/src/ui_interface.rs create mode 100644 vendor/rustdesk/src/ui_session_interface.rs create mode 100644 vendor/rustdesk/src/updater.rs create mode 100644 vendor/rustdesk/src/version.rs create mode 100644 vendor/rustdesk/src/virtual_display_manager.rs create mode 100644 vendor/rustdesk/src/whiteboard/client.rs create mode 100644 vendor/rustdesk/src/whiteboard/linux.rs create mode 100644 vendor/rustdesk/src/whiteboard/macos.rs create mode 100644 vendor/rustdesk/src/whiteboard/mod.rs create mode 100644 vendor/rustdesk/src/whiteboard/server.rs create mode 100644 vendor/rustdesk/src/whiteboard/win_linux.rs create mode 100644 vendor/rustdesk/src/whiteboard/windows.rs create mode 100644 vendor/rustdesk/vcpkg.json diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..9834b90 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,21 @@ +# Cargo only honors `.cargo/config.toml` files in *ancestor* directories of +# the workspace root, so the one inside vendor/rustdesk/.cargo/ is invisible +# to the hello-agent build. We mirror the relevant target settings here. +# +# +crt-static: link the static MSVC C runtime (libcmt) to match vcpkg's +# x64-windows-static triplet. Without this, native deps installed by vcpkg +# pull in libcmt while Rust's default toolchain links msvcrt (the dynamic +# CRT), and the linker emits LNK1169 (multiply defined symbols) for every +# overlapping CRT entry. + +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] + +[target.i686-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"] + +[net] +# Use the system git binary for fetching git deps. Cargo's built-in libgit2 +# fetch path occasionally trips up on self-hosted runners with proxies or +# unusual auth setups; the system git is more forgiving. +git-fetch-with-cli = true diff --git a/.gitea/workflows/build-windows.yml b/.gitea/workflows/build-windows.yml new file mode 100644 index 0000000..35d061c --- /dev/null +++ b/.gitea/workflows/build-windows.yml @@ -0,0 +1,162 @@ +name: build-windows + +on: + push: + branches: [main, master] + workflow_dispatch: + inputs: + version_suffix: + description: "Version suffix (e.g. 'cst', 'beta1'). Empty = vanilla." + type: string + default: "" + +env: + RUST_VERSION: "1.75" + LLVM_VERSION: "15.0.6" + # bindgen (pulled in via scrap → libvpx-sys) reads LIBCLANG_PATH; the runner + # provisioner installs LLVM here. + LLVM_HOME: 'C:\tools\llvm-15.0.6' + VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" + +jobs: + build-x64: + name: build-hello-agent-x64 + runs-on: [self-hosted, windows-10] + timeout-minutes: 90 + env: + VCPKG_ROOT: C:\vcpkg + VCPKG_BINARY_SOURCES: "clear;files,C:\\vcpkg-cache,readwrite" + LIBCLANG_PATH: 'C:\tools\llvm-15.0.6\bin' + steps: + - name: Checkout hello-agent (with vendored rustdesk) + uses: actions/checkout@v4 + # We vendor the rustdesk source under vendor/rustdesk/ so this + # checkout is fully self-contained — no sibling repo, no submodules. + + - name: Verify host toolchain + shell: pwsh + run: | + $required = 'pwsh','git','bash','python','rustc','cargo','rustup','clang' + $missing = @() + foreach ($tool in $required) { + $cmd = Get-Command $tool -ErrorAction SilentlyContinue + if (-not $cmd) { $missing += $tool; continue } + $ver = & $tool --version 2>&1 | Select-Object -First 1 + Write-Host ("{0,-10} {1} ({2})" -f $tool, $cmd.Source, $ver) + } + if ($missing.Count -gt 0) { + Write-Error ("Missing tools on runner: {0}" -f ($missing -join ', ')) + exit 1 + } + if (-not $env:VCPKG_ROOT -or -not (Test-Path "$env:VCPKG_ROOT\vcpkg.exe")) { + Write-Error "VCPKG_ROOT not set or vcpkg.exe missing at $env:VCPKG_ROOT" + exit 1 + } + if (-not (Test-Path "$env:LIBCLANG_PATH\libclang.dll")) { + Write-Error "libclang.dll not found at $env:LIBCLANG_PATH" + exit 1 + } + + - name: Configure Rust toolchain + shell: pwsh + run: | + rustup toolchain install $env:RUST_VERSION --profile minimal --component rustfmt + if ($LASTEXITCODE -ne 0) { throw "rustup toolchain install failed ($LASTEXITCODE)" } + rustup default $env:RUST_VERSION + if ($LASTEXITCODE -ne 0) { throw "rustup default failed ($LASTEXITCODE)" } + rustup target add x86_64-pc-windows-msvc + rustc --version + cargo --version + + - name: Configure git safe.directory + shell: pwsh + run: git config --global --add safe.directory '*' + + - name: vcpkg install dependencies (x64-windows-static) + shell: bash + env: + VCPKG_DEFAULT_HOST_TRIPLET: x64-windows-static + # vcpkg.json sits at vendor/rustdesk/vcpkg.json (alongside the + # rustdesk Cargo.toml). Run from there so manifest mode picks it up. + run: | + mkdir -p /c/vcpkg-cache + cd vendor/rustdesk + if ! "$VCPKG_ROOT/vcpkg" install \ + --triplet x64-windows-static \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "$VCPKG_ROOT/" -name "*.log" -exec sh -c 'echo "===== {} ====="; cat "{}"' \; + exit 1 + fi + + # Build hello-agent. We do NOT pre-build vendor/rustdesk/libs/virtual_display/dylib + # the way the upstream rustdesk workflow does. That dylib produces a + # standalone `dylib_virtual_display.dll` runtime artifact that the + # rustdesk Flutter exe ships side-by-side; hello-agent doesn't bundle + # it (no virtual-display feature in v0), and the `virtual_display` + # crate that librustdesk *does* link against has no compile-time dep + # on the dylib — it loads it by name at runtime if present. + # + # Pre-building it would also force a second cargo invocation inside + # the vendor/rustdesk/ workspace, which has no Cargo.lock of its own + # and would re-resolve git deps from HEAD (breaking the tray-icon + # 0.21.3 pin we keep at the hello-agent root). + - name: Cargo build hello-agent + shell: pwsh + run: | + cargo build --release --bin hello-agent --locked + if ($LASTEXITCODE -ne 0) { throw "hello-agent build failed" } + if (-not (Test-Path target\release\hello-agent.exe)) { + throw "target\release\hello-agent.exe missing after cargo build" + } + + - name: Compute version suffix and stage artifact + shell: pwsh + run: | + $suffix = "${env:VERSION_SUFFIX}" + if ($suffix) { $tag = "0.1.0-$suffix" } else { $tag = "0.1.0" } + New-Item -ItemType Directory -Force -Path .\SignOutput | Out-Null + Copy-Item -Force ` + target\release\hello-agent.exe ` + ".\SignOutput\hello-agent-$tag-x86_64.exe" + Write-Host "staged: SignOutput\hello-agent-$tag-x86_64.exe" + env: + VERSION_SUFFIX: ${{ inputs.version_suffix }} + + - name: Report signing status of build artifacts + shell: pwsh + run: | + $artifacts = Get-ChildItem .\SignOutput -Include *.exe -File + if (-not $artifacts) { + Write-Warning "No artifacts found in SignOutput\" + return + } + $unsigned = @() + foreach ($f in $artifacts) { + $sig = Get-AuthenticodeSignature -FilePath $f.FullName + $size = '{0,8:N0}' -f $f.Length + switch ($sig.Status) { + 'Valid' { + Write-Host ("[ SIGNED ] {0} ({1} bytes) signed by: {2}" -f $f.Name, $size, $sig.SignerCertificate.Subject) + } + 'NotSigned' { + Write-Host ("[UNSIGNED] {0} ({1} bytes)" -f $f.Name, $size) + $unsigned += $f.Name + } + default { + Write-Host ("[ {0,-7} ] {1} ({2} bytes) -- {3}" -f $sig.Status, $f.Name, $size, $sig.StatusMessage) + $unsigned += $f.Name + } + } + } + if ($unsigned.Count -gt 0) { + $list = $unsigned -join ', ' + Write-Host "::warning title=Unsigned artifacts::$list -- SmartScreen will warn end users. Wire up signing before distributing." + } + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: hello-agent-windows-x64-${{ github.sha }} + path: SignOutput/hello-agent-*.exe + if-no-files-found: error + retention-days: 14 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8ad8db --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Build output (any nested target/ from cargo invocations under vendor/). +target/ +**/*.rs.bk +Cargo.lock.bak +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3eb3283 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,9821 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if 1.0.0", + "getrandom 0.3.2", + "once_cell", + "version_check", + "zerocopy 0.8.26", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" +dependencies = [ + "alsa-sys", + "bitflags 2.9.1", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.9.1", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk 0.9.0", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum 0.7.2", + "thiserror 1.0.61", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android-wakelock" +version = "0.1.0" +source = "git+https://github.com/rustdesk-org/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90" +dependencies = [ + "jni", + "log", + "ndk-context", +] + +[[package]] +name = "android_log-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" + +[[package]] +name = "android_logger" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f" +dependencies = [ + "android_log-sys", + "env_logger 0.10.2", + "log", + "once_cell", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arboard" +version = "3.4.0" +source = "git+https://github.com/rustdesk-org/arboard#85be1218668ff218a7b170c9d424fde73e069914" +dependencies = [ + "clipboard-win", + "core-graphics 0.23.2", + "image 0.25.1", + "log", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot", + "percent-encoding", + "serde 1.0.228", + "serde_derive", + "windows-sys 0.48.0", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "associative-cache" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46016233fc1bb55c23b856fe556b7db6ccd05119a0a392e04f0b3b7c79058f16" + +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-executor" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.1.0", + "futures-lite 2.3.0", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock 2.8.0", + "autocfg 1.3.0", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg 1.3.0", + "cfg-if 1.0.0", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" +dependencies = [ + "async-lock 3.4.0", + "cfg-if 1.0.0", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling 3.7.2", + "rustix 0.38.34", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", + "blocking", + "cfg-if 1.0.0", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.34", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "async-signal" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d" +dependencies = [ + "async-io 2.3.3", + "async-lock 3.4.0", + "atomic-waker", + "cfg-if 1.0.0", + "futures-core", + "futures-io", + "rustix 0.38.34", + "signal-hook-registry", + "slab", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "atk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" +dependencies = [ + "atk-sys", + "glib 0.18.5", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide 0.7.4", + "object", + "rustc-demangle", +] + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bindgen" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "clap 2.34.0", + "env_logger 0.9.3", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2 1.0.93", + "quote 1.0.36", + "regex", + "rustc-hash 1.1.0", + "shlex", + "which", +] + +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2 1.0.93", + "quote 1.0.36", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.98", + "which", +] + +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "proc-macro2 1.0.93", + "quote 1.0.36", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.98", +] + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "proc-macro2 1.0.93", + "quote 1.0.36", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.98", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite 2.3.0", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytecodec" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325" +dependencies = [ + "byteorder", + "trackable 0.2.24", +] + +[[package]] +name = "bytemuck" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.9.1", + "cairo-sys-rs", + "glib 0.18.5", + "libc", + "once_cell", + "thiserror 1.0.61", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys 0.18.1", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.9.1", + "log", + "polling 3.7.2", + "rustix 0.38.34", + "slab", + "thiserror 1.0.61", +] + +[[package]] +name = "calloop" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" +dependencies = [ + "bitflags 2.9.1", + "polling 3.7.2", + "rustix 1.1.2", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.34", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.3", + "rustix 1.1.2", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits 0.2.19", + "wasm-bindgen", + "windows-link 0.1.1", +] + +[[package]] +name = "cidr-utils" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2315f7119b7146d6a883de6acd63ddf96071b5f79d9d98d2adaa84d749f6abf1" +dependencies = [ + "debug-helper", + "num-bigint", + "num-traits 0.2.19", + "once_cell", + "regex", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.4", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "clipboard" +version = "0.1.0" +dependencies = [ + "cc", + "dirs 5.0.1", + "fsevent", + "hbb_common", + "lazy_static", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot", + "serde 1.0.228", + "serde_derive", + "thiserror 1.0.61", + "uuid", + "xattr", +] + +[[package]] +name = "clipboard-master" +version = "4.0.0-beta.6" +source = "git+https://github.com/rustdesk-org/clipboard-master#ddc39f00a6211959489ae683aa6ae6eedf03a809" +dependencies = [ + "objc", + "objc-foundation", + "objc_id", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", + "windows-win", + "wl-clipboard-rs", + "x11-clipboard", + "x11rb", +] + +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "cocoa" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c49e86fc36d5704151f5996b7b3795385f50ce09e3be0f47a0cfde869681cf8" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.7.0", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compression-codecs" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "confy" +version = "0.4.0-2" +source = "git+https://github.com/rustdesk-org/confy#83db9ec19a2f97e9718aef69e4fc5611bb382479" +dependencies = [ + "directories-next", + "serde 1.0.228", + "thiserror 1.0.61", + "toml 0.5.11", +] + +[[package]] +name = "const_fn" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d" + +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "unicode-xid 0.2.4", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "constant_time_eq" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.7.0", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-media-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273bf3fc5bf51fd06a7766a84788c1540b6527130a0bce39e00567d6ab9f31f1" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-text" +version = "19.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" +dependencies = [ + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-video-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "metal", + "objc", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys 0.8.7", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" +dependencies = [ + "bindgen 0.69.4", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#6b374bcaed076750ca8fce6da518ab39b882e14a" +dependencies = [ + "alsa", + "core-foundation-sys 0.8.7", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" + +[[package]] +name = "ctrlc" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" +dependencies = [ + "nix 0.28.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dasp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7381b67da416b639690ac77c73b86a7b5e64a29e31d1f75fb3b1102301ef355a" +dependencies = [ + "dasp_envelope", + "dasp_frame", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_signal", + "dasp_slice", + "dasp_window", +] + +[[package]] +name = "dasp_envelope" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec617ce7016f101a87fe85ed44180839744265fae73bb4aa43e7ece1b7668b6" +dependencies = [ + "dasp_frame", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", +] + +[[package]] +name = "dasp_frame" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" +dependencies = [ + "dasp_sample", +] + +[[package]] +name = "dasp_interpolate" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc975a6563bb7ca7ec0a6c784ead49983a21c24835b0bc96eea11ee407c7486" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + +[[package]] +name = "dasp_peak" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf88559d79c21f3d8523d91250c397f9a15b5fc72fbb3f87fdb0a37b79915bf" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_ring_buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d79e19b89618a543c4adec9c5a347fe378a19041699b3278e616e387511ea1" + +[[package]] +name = "dasp_rms" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c5dcb30b7e5014486e2822537ea2beae50b19722ffe2ed7549ab03774575aa" +dependencies = [ + "dasp_frame", + "dasp_ring_buffer", + "dasp_sample", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "dasp_signal" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1ab7d01689c6ed4eae3d38fe1cea08cba761573fbd2d592528d55b421077e7" +dependencies = [ + "dasp_envelope", + "dasp_frame", + "dasp_interpolate", + "dasp_peak", + "dasp_ring_buffer", + "dasp_rms", + "dasp_sample", + "dasp_window", +] + +[[package]] +name = "dasp_slice" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1c7335d58e7baedafa516cb361360ff38d6f4d3f9d9d5ee2a2fc8e27178fa1" +dependencies = [ + "dasp_frame", + "dasp_sample", +] + +[[package]] +name = "dasp_window" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ded7b88821d2ce4e8b842c9f1c86ac911891ab89443cc1de750cae764c5076" +dependencies = [ + "dasp_sample", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "dbus-crossroads" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4c83437187544ba5142427746835061b330446ca8902eabd70e4afb8f76de0" +dependencies = [ + "dbus", +] + +[[package]] +name = "debug-helper" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" + +[[package]] +name = "default-net" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4898b43aed56499fad6b294d15b3e76a51df68079bf492e5daae38ca084e003" +dependencies = [ + "dlopen2", + "libc", + "memalloc", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "once_cell", + "system-configuration", + "windows 0.32.0", +] + +[[package]] +name = "default_net" +version = "0.1.0" +source = "git+https://github.com/rustdesk-org/default_net#78f8f70cd85151a3a2c4a3230d80d5272703c02e" +dependencies = [ + "anyhow", + "regex", + "winapi", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.5", + "winapi", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.5", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.5", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.4", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.4", +] + +[[package]] +name = "dlopen" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937" +dependencies = [ + "dlopen_derive", + "lazy_static", + "libc", + "winapi", +] + +[[package]] +name = "dlopen2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b121caccfc363e4d9a4589528f3bef7c71b83c6ed01c8dc68cbeeb7fd29ec698" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a09ac8bb8c16a282264c379dffba707b9c998afc7506009137f3c6136888078" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "dlopen_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581" +dependencies = [ + "libc", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" + +[[package]] +name = "drm" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" +dependencies = [ + "bitflags 2.9.1", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "rustix 0.38.34", +] + +[[package]] +name = "drm-ffi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" +dependencies = [ + "drm-sys", + "rustix 0.38.34", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "enigo" +version = "0.0.14" +dependencies = [ + "core-graphics 0.22.3", + "hbb_common", + "libxdo-sys", + "log", + "objc", + "pkg-config", + "rdev", + "serde 1.0.228", + "serde_derive", + "tfc", + "unicode-segmentation", + "winapi", +] + +[[package]] +name = "enquote" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c36cb11dbde389f4096111698d8b567c0720e3452fd5ac3e6b4e47e1939932" +dependencies = [ + "thiserror 1.0.61", +] + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "enumflags2" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +dependencies = [ + "enumflags2_derive", + "serde 1.0.228", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "epoll" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74351c3392ea1ff6cd2628e0042d268ac2371cb613252ff383b6dfa50d22fa79" +dependencies = [ + "bitflags 2.9.1", + "libc", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + +[[package]] +name = "evdev" +version = "0.11.5" +source = "git+https://github.com/rustdesk-org/evdev#cec616e37790293d2cd2aa54a96601ed6b1b35a9" +dependencies = [ + "bitvec", + "libc", + "nix 0.23.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide 0.7.4", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset 0.9.1", + "rustc_version", +] + +[[package]] +name = "filedescriptor" +version = "0.8.2" +source = "git+https://github.com/rustdesk-org/wezterm?branch=rustdesk/pty_based_0.8.1#80174f8009f41565f0fa8c66dab90d4f9211ae16" +dependencies = [ + "libc", + "thiserror 1.0.61", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "flexi_logger" +version = "0.27.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469e584c031833564840fb0cdbce99bdfe946fd45480a188545e73a76f45461c" +dependencies = [ + "chrono", + "crossbeam-channel", + "crossbeam-queue", + "glob", + "is-terminal", + "lazy_static", + "log", + "nu-ansi-term 0.49.0", + "regex", + "thiserror 1.0.61", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fon" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad46a0e6c9bc688823a742aa969b5c08fdc56c2a436ee00d5c6fbcb5982c55c4" +dependencies = [ + "libm", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fruitbasket" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898289b8e0528c84fb9b88f15ac9d5109bcaf23e0e49bb6f64deee0d86b6a351" +dependencies = [ + "dirs 2.0.2", + "objc", + "objc-foundation", + "objc_id", + "time 0.1.45", +] + +[[package]] +name = "fsevent" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8836d1f147a0a195bf517a5fd211ea7023d19ced903135faf6c4504f2cf8775f" +dependencies = [ + "bitflags 1.3.2", + "fsevent-sys", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand 2.1.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib 0.18.5", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib 0.18.5", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gdk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90fbf5c033c65d93792192a49a8efb5bb1e640c419682a58bb96f5ae77f3d4a" +dependencies = [ + "gdk-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee8f00f4ee46cad2939b8990f5c70c94ff882c3028f3cc5abf950fa4ab53043" +dependencies = [ + "gdk-sys", + "glib-sys 0.18.1", + "libc", + "system-deps 6.2.2", + "x11 2.21.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib 0.18.5", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.61", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", + "winapi", +] + +[[package]] +name = "git2" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "glib" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "glib-macros 0.10.1", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "libc", + "once_cell", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.9.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros 0.18.5", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.61", +] + +[[package]] +name = "glib-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" +dependencies = [ + "anyhow", + "heck 0.3.3", + "itertools 0.9.0", + "proc-macro-crate 0.1.5", + "proc-macro-error", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "glib-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" +dependencies = [ + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gobject-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" +dependencies = [ + "glib-sys 0.10.1", + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys 0.18.1", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gstreamer" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff5d0f7ff308ae37e6eb47b6ded17785bdea06e438a708cd09e0288c1862f33" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.0", + "futures-channel", + "futures-core", + "futures-util", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer-sys", + "libc", + "muldiv", + "num-rational", + "once_cell", + "paste", + "pretty-hex", + "thiserror 1.0.61", +] + +[[package]] +name = "gstreamer-app" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc80888271338c3ede875d8cafc452eb207476ff5539dcbe0018a8f5b827af0e" +dependencies = [ + "bitflags 1.3.2", + "futures-core", + "futures-sink", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer", + "gstreamer-app-sys", + "gstreamer-base", + "gstreamer-sys", + "libc", + "once_cell", +] + +[[package]] +name = "gstreamer-app-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "813f64275c9e7b33b828b9efcf9dfa64b95996766d4de996e84363ac65b87e3d" +dependencies = [ + "glib-sys 0.10.1", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "gstreamer-base" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafd01c56f59cb10f4b5a10f97bb4bdf8c2b2784ae5b04da7e2d400cf6e6afcf" +dependencies = [ + "bitflags 1.3.2", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", +] + +[[package]] +name = "gstreamer-base-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b7b6dc2d6e160a1ae28612f602bd500b3fa474ce90bf6bb2f08072682beef5" +dependencies = [ + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer-sys", + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "gstreamer-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1f154082d01af5718c5f8a8eb4f565a4ea5586ad8833a8fc2c2aa6844b601d" +dependencies = [ + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "gstreamer-video" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7bbb1485d87469849ec45c08e03c2f280d3ea20ff3c439d03185be54e3ce98e" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-util", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer", + "gstreamer-base", + "gstreamer-base-sys", + "gstreamer-sys", + "gstreamer-video-sys", + "libc", + "once_cell", +] + +[[package]] +name = "gstreamer-video-sys" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92347e46438007d6a2386302125f62cb9df6769cdacb931af5c0f12c1ee21de4" +dependencies = [ + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps 1.3.2", +] + +[[package]] +name = "gtk" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib 0.18.5", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "pango-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if 1.0.0", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "hbb_common" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-recursion", + "backtrace", + "base64 0.22.1", + "bytes", + "chrono", + "confy", + "default_net", + "directories-next", + "dirs-next", + "dlopen", + "env_logger 0.11.6", + "filetime", + "flexi_logger", + "futures", + "futures-util", + "httparse", + "lazy_static", + "libc", + "libloading 0.8.4", + "log", + "mac_address", + "machine-uid", + "osascript", + "protobuf", + "protobuf-codegen", + "rand 0.8.5", + "regex", + "rustls-native-certs", + "rustls-pki-types", + "rustls-platform-verifier", + "serde 1.0.228", + "serde_derive", + "serde_json 1.0.118", + "sha2", + "smithay-client-toolkit 0.20.0", + "socket2 0.3.19", + "sodiumoxide", + "sysinfo", + "thiserror 1.0.61", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-socks", + "tokio-tungstenite", + "tokio-util", + "toml 0.7.8", + "tungstenite", + "url", + "users 0.11.0", + "uuid", + "webpki-roots 1.0.4", + "whoami", + "winapi", + "x11 2.21.0", + "zstd 0.13.1", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hello-agent" +version = "0.1.0" +dependencies = [ + "anyhow", + "env_logger 0.10.2", + "hbb_common", + "log", + "rustdesk", + "tokio", + "winapi", + "windows-service", + "winreg 0.11.0", + "winres", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.11", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hwcodec" +version = "0.7.1" +source = "git+https://github.com/rustdesk-org/hwcodec#398e5a8938dd8768ade0fcdc27ea80e8b4b38738" +dependencies = [ + "bindgen 0.59.2", + "cc", + "log", + "serde 1.0.228", + "serde_derive", + "serde_json 1.0.118", +] + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa 1.0.11", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.4", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys 0.8.7", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits 0.2.19", + "png 0.17.13", + "qoi", + "tiff", +] + +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "num-traits 0.2.19", + "png 0.17.13", + "tiff", +] + +[[package]] +name = "impersonate_system" +version = "0.1.0" +source = "git+https://github.com/rustdesk-org/impersonate-system#2f429010a5a10b1fe5eceb553c6672fd53d20167" +dependencies = [ + "cc", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ioctl-rs" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde 1.0.228", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.0", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_debug" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d198e9919d9822d5f7083ba8530e04de87841eaf21ead9af8f2304efd57c89" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if 1.0.0", + "combine", + "jni-sys", + "log", + "thiserror 1.0.61", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kcp-sys" +version = "0.1.0" +source = "git+https://github.com/rustdesk-org/kcp-sys#32a6c09fc6223f54aea83981a6aa8995931d29be" +dependencies = [ + "anyhow", + "auto_impl", + "bindgen 0.71.1", + "bitflags 2.9.1", + "bytes", + "cc", + "dashmap", + "log", + "parking_lot", + "rand 0.8.5", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "zerocopy 0.7.34", +] + +[[package]] +name = "keepawake" +version = "0.4.3" +source = "git+https://github.com/rustdesk-org/keepawake-rs#64d568586dd16551d02120e19668d2b0fec8e3c9" +dependencies = [ + "anyhow", + "cfg-if 1.0.0", + "core-foundation 0.9.4", + "shadow-rs", + "windows 0.48.0", + "winres", + "zbus", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.9.1", + "serde 1.0.228", + "unicode-segmentation", +] + +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib 0.18.5", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libgit2-sys" +version = "0.14.2+1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +dependencies = [ + "cfg-if 1.0.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libpulse-binding" +version = "2.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libpulse-sys", + "num-derive 0.3.3", + "num-traits 0.2.19", + "winapi", +] + +[[package]] +name = "libpulse-simple-binding" +version = "2.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05fd6b68f33f6a251265e6ed1212dc3107caad7c5c6fdcd847b2e65ef58c308d" +dependencies = [ + "libpulse-binding", + "libpulse-simple-sys", + "libpulse-sys", +] + +[[package]] +name = "libpulse-simple-sys" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6613b4199d8b9f0edcfb623e020cb17bbd0bee8dd21f3c7cc938de561c4152" +dependencies = [ + "libpulse-sys", + "pkg-config", +] + +[[package]] +name = "libpulse-sys" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b" +dependencies = [ + "libc", + "num-derive 0.3.3", + "num-traits 0.2.19", + "pkg-config", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.1", + "libc", + "redox_syscall 0.5.2", +] + +[[package]] +name = "libsodium-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "walkdir", +] + +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +dependencies = [ + "hbb_common", +] + +[[package]] +name = "libz-sys" +version = "1.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "line-wrap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg 1.3.0", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac_address" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8836fae9d0d4be2c8b4efcdd79e828a2faa058a90d005abf42f91cac5493a08e" +dependencies = [ + "nix 0.28.0", + "winapi", +] + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "machine-uid" +version = "0.3.0" +source = "git+https://github.com/rustdesk-org/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542" +dependencies = [ + "bindgen 0.59.2", + "cc", + "winreg 0.11.0", +] + +[[package]] +name = "magnum-opus" +version = "0.4.0" +source = "git+https://github.com/rustdesk-org/magnum-opus#5cd2bf989c148662fa3a2d9d539a71d71fd1d256" +dependencies = [ + "bindgen 0.59.2", + "target_build_utils", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memalloc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "metal" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e198a0ee42bdbe9ef2c09d0b9426f3b2b47d90d93a4a9b0395c4cea605e92dc0" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa 0.20.2", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "log", + "objc", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "mozjpeg" +version = "0.10.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55571bce4f12d80ceb4296526e7614f796df72daaaac85f265ab732fa47b7bc9" +dependencies = [ + "arrayvec", + "bytemuck", + "libc", + "mozjpeg-sys", + "rgb", +] + +[[package]] +name = "mozjpeg-sys" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad3626d7942d5b56cc6d47b1c59724c0a976b786fca059c5aaa904aef6324d55" +dependencies = [ + "cc", + "dunce", + "libc", + "nasm-rs", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png 0.17.13", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "muldiv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "nasm-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fcfa1bd49e0342ec1d07ed2be83b59963e7acbeb9310e1bb2c07b69dadd959" +dependencies = [ + "jobserver", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.10.0", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys 0.4.1+23.1.7779620", + "num_enum 0.5.11", + "raw-window-handle 0.5.2", + "thiserror 1.0.61", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.9.1", + "jni-sys", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum 0.7.2", + "thiserror 1.0.61", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.1", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum 0.7.2", + "raw-window-handle 0.6.2", + "thiserror 1.0.61", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "netlink-packet-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5cf0b54effda4b91615c40ff0fd12d0d4c9a6e0f5116874f03941792ff535a" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea993e32c77d87f01236c38f572ecb6c311d592e56a06262a007fd2a6e31253c" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.61", +] + +[[package]] +name = "netlink-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416060d346fbaf1f23f9512963e3e878f1a78e707cb699ba9215761754244307" +dependencies = [ + "bytes", + "libc", + "log", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg 1.3.0", + "bitflags 1.3.2", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.5", + "pin-utils", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.0", + "libc", + "memoffset 0.7.1", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if 1.0.0", + "cfg_aliases 0.1.1", + "libc", + "memoffset 0.9.1", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.1", + "cfg-if 1.0.0", + "cfg_aliases 0.2.1", + "libc", +] + +[[package]] +name = "nokhwa" +version = "0.10.7" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "flume", + "image 0.25.1", + "nokhwa-bindings-linux", + "nokhwa-bindings-macos", + "nokhwa-bindings-windows", + "nokhwa-core", + "paste", + "thiserror 2.0.17", +] + +[[package]] +name = "nokhwa-bindings-linux" +version = "0.1.1" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "nokhwa-core", + "v4l", +] + +[[package]] +name = "nokhwa-bindings-macos" +version = "0.2.2" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-media-sys", + "core-video-sys", + "flume", + "nokhwa-core", + "objc", + "once_cell", +] + +[[package]] +name = "nokhwa-bindings-windows" +version = "0.4.2" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "dlopen", + "lazy_static", + "nokhwa-core", + "once_cell", + "windows 0.43.0", +] + +[[package]] +name = "nokhwa-core" +version = "0.1.5" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "bytes", + "image 0.25.1", + "mozjpeg", + "thiserror 2.0.17", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg 1.3.0", + "num-integer", + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive 0.7.2", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro-crate 2.0.2", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.9.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.9.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +dependencies = [ + "memchr", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk 0.8.0", + "ndk-context", + "num-derive 0.4.2", + "num-traits 0.2.19", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.9.1", + "cfg-if 1.0.0", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.5.3+3.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6bad8cd0233b63971e232cc9c5e83039375b8586d2312f31fda85db8f888c2" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "libredox", +] + +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os-version" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8a1fed76ac765e39058ca106b6229a93c5a60292a1bd4b602ce2be11e1c020" +dependencies = [ + "anyhow", + "plist", + "uname", + "winapi", +] + +[[package]] +name = "os_pipe" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "osascript" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" +dependencies = [ + "serde 1.0.228", + "serde_derive", + "serde_json 1.0.118", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "pam" +version = "0.7.0" +source = "git+https://github.com/rustdesk-org/pam#7bfd25510202cd269292cbdd7c71f3977a6fd762" +dependencies = [ + "libc", + "pam-macros", + "pam-sys", + "users 0.10.0", +] + +[[package]] +name = "pam-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "pam-sys" +version = "1.0.0-alpha4" +source = "git+https://github.com/rustdesk-org/pam-sys?branch=fix/v1.0.0-alpha4_gnuc_va_list#3337c9bb9a9c68d7497ec8c93cad2368c26091b7" +dependencies = [ + "bindgen 0.59.2", + "libc", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib 0.18.5", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "parity-tokio-ipc" +version = "0.7.3-5" +source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291" +dependencies = [ + "futures", + "libc", + "log", + "rand 0.8.5", + "tokio", + "winapi", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.5.2", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +dependencies = [ + "phf_shared 0.7.24", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" +dependencies = [ + "phf_generator 0.7.24", + "phf_shared 0.7.24", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +dependencies = [ + "phf_shared 0.7.24", + "rand 0.6.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher 0.2.3", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "piet" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e381186490a3e2017a506d62b759ea8eaf4be14666b13ed53973e8ae193451b1" +dependencies = [ + "kurbo", + "unic-bidi", +] + +[[package]] +name = "piet-coregraphics" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819b41d2ddb1d8abf3e45e49422f866cba281b4abb5e2fb948bba06e2c3d3f7" +dependencies = [ + "associative-cache", + "core-foundation 0.9.4", + "core-foundation-sys 0.8.7", + "core-graphics 0.22.3", + "core-text", + "foreign-types 0.3.2", + "piet", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" +dependencies = [ + "atomic-waker", + "fastrand 2.1.0", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "plist" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +dependencies = [ + "base64 0.21.7", + "indexmap", + "line-wrap", + "quick-xml 0.31.0", + "serde 1.0.228", + "time 0.3.36", +] + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.4", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.9.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg 1.3.0", + "bitflags 1.3.2", + "cfg-if 1.0.0", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +dependencies = [ + "cfg-if 1.0.0", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "portable-pty" +version = "0.8.1" +source = "git+https://github.com/rustdesk-org/wezterm?branch=rustdesk/pty_based_0.8.1#80174f8009f41565f0fa8c66dab90d4f9211ae16" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix 0.25.1", + "serial", + "shared_library", + "shell-words", + "winapi", + "winreg 0.10.1", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pretty-hex" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" + +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2 1.0.93", + "syn 2.0.98", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror 1.0.61", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.61", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.61", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.61", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "qrcode-generator" +version = "4.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d06cb9646c7a14096231a2474d7f21e5e8c13de090c68d13bde6157cfe7f159" +dependencies = [ + "html-escape", + "image 0.24.9", + "qrcodegen", +] + +[[package]] +name = "qrcodegen" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases 0.2.1", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.2", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases 0.2.1", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2 1.0.93", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rdev" +version = "0.5.0-2" +source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c116683cd232c8" +dependencies = [ + "cocoa 0.24.1", + "core-foundation 0.9.4", + "core-foundation-sys 0.8.7", + "core-graphics 0.22.3", + "dispatch", + "enum-map", + "epoll", + "inotify", + "lazy_static", + "libc", + "log", + "mio 0.8.11", + "strum 0.24.1", + "strum_macros 0.24.3", + "widestring", + "winapi", + "x11 2.21.0", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.61", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "remote_printer" +version = "0.1.0" +dependencies = [ + "hbb_common", + "winapi", + "windows-strings 0.3.1", +] + +[[package]] +name = "repng" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd57cd2cb5cc699b3eb4824d654e5a32f3bc013766da4966f71fe94805abbda" +dependencies = [ + "byteorder", + "flate2", +] + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "async-compression", + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde 1.0.228", + "serde_json 1.0.118", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.4", +] + +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if 1.0.0", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ringbuf" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79abed428d1fd2a128201cec72c5f6938e2da607c6f3745f769fabea399d950a" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "runas" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96d6b6c505282b007a9b009f2aa38b2fd0359b81a0430ceacc60f69ade4c6a0" +dependencies = [ + "libc", + "security-framework-sys", + "which", + "windows-sys 0.48.0", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if 1.0.0", + "ordered-multimap", +] + +[[package]] +name = "rust-pulsectl" +version = "0.2.12" +source = "git+https://github.com/rustdesk-org/pulsectl#aa34dde499aa912a3abc5289cc0b547bd07dd6e2" +dependencies = [ + "libpulse-binding", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustdesk" +version = "1.4.6" +dependencies = [ + "android-wakelock", + "android_logger", + "arboard", + "async-process", + "async-trait", + "bytemuck", + "bytes", + "cc", + "cfg-if 1.0.0", + "chrono", + "cidr-utils", + "clap 4.5.53", + "clipboard", + "clipboard-master", + "cocoa 0.24.1", + "core-foundation 0.9.4", + "core-graphics 0.22.3", + "cpal", + "crossbeam-queue", + "ctrlc", + "dasp", + "dbus", + "dbus-crossroads", + "default-net", + "dispatch", + "enigo", + "errno", + "evdev", + "fon", + "fontdb", + "foreign-types 0.3.2", + "fruitbasket", + "gtk", + "hbb_common", + "hex", + "image 0.24.9", + "impersonate_system", + "include_dir", + "jni", + "kcp-sys", + "keepawake", + "lazy_static", + "libpulse-binding", + "libpulse-simple-binding", + "libxdo-sys", + "mac_address", + "magnum-opus", + "nix 0.29.0", + "num_cpus", + "objc", + "objc_id", + "openssl", + "os-version", + "pam", + "parity-tokio-ipc", + "piet", + "piet-coregraphics", + "portable-pty", + "qrcode-generator", + "rdev", + "remote_printer", + "repng", + "reqwest", + "ringbuf", + "rpassword", + "runas", + "rust-pulsectl", + "sciter-rs", + "scrap", + "serde 1.0.228", + "serde_derive", + "serde_json 1.0.118", + "serde_repr", + "sha2", + "shared_memory", + "shutdown_hooks", + "softbuffer", + "stunclient", + "sys-locale", + "system_shutdown", + "tao", + "tauri-winrt-notification", + "terminfo", + "termios 0.3.3", + "tiny-skia", + "totp-rs", + "tray-icon", + "ttf-parser", + "url", + "uuid", + "virtual_display", + "wallpaper", + "winapi", + "windows 0.61.1", + "windows-service", + "winit", + "winreg 0.11.0", + "winres", + "wol-rs", + "zip", +] + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys 0.8.7", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "sciter-rs" +version = "0.5.57" +source = "git+https://github.com/rustdesk-org/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821" +dependencies = [ + "lazy_static", + "libc", + "objc", + "objc-foundation", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrap" +version = "0.5.0" +dependencies = [ + "android_logger", + "bindgen 0.65.1", + "block", + "cfg-if 1.0.0", + "dbus", + "gstreamer", + "gstreamer-app", + "gstreamer-video", + "hbb_common", + "hwcodec", + "jni", + "lazy_static", + "log", + "ndk-context", + "nokhwa", + "num_cpus", + "serde 1.0.228", + "serde_json 1.0.118", + "target_build_utils", + "tracing", + "webm", + "winapi", + "zbus", +] + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + +[[package]] +name = "security-framework" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-foundation-sys 0.8.7", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "core-foundation-sys 0.8.7", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "serde_json" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8bcf487be7d2e15d3d543f04312de991d631cfe1b43ea0ade69e6a8a5b16a1" +dependencies = [ + "dtoa", + "itoa 0.3.4", + "num-traits 0.1.43", + "serde 0.9.15", +] + +[[package]] +name = "serde_json" +version = "1.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" +dependencies = [ + "itoa 1.0.11", + "ryu", + "serde 1.0.228", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.11", + "ryu", + "serde 1.0.228", +] + +[[package]] +name = "serial" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86" +dependencies = [ + "serial-core", + "serial-unix", + "serial-windows", +] + +[[package]] +name = "serial-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581" +dependencies = [ + "libc", +] + +[[package]] +name = "serial-unix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7" +dependencies = [ + "ioctl-rs", + "libc", + "serial-core", + "termios 0.2.2", +] + +[[package]] +name = "serial-windows" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162" +dependencies = [ + "libc", + "serial-core", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + +[[package]] +name = "shadow-rs" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427f07ab5f873000cf55324882e12a88c0a7ea7025df4fc1e7e35e688877a583" +dependencies = [ + "const_format", + "git2", + "is_debug", + "time 0.3.36", + "tzdb 0.5.10", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "shared_memory" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8593196da75d9dc4f69349682bd4c2099f8cde114257d1ef7ef1b33d1aba54" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "nix 0.23.2", + "rand 0.8.5", + "win-sys", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shutdown_hooks" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6057adedbec913419c92996f395ba69931acbd50b7d56955394cd3f7bedbfa45" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg 1.3.0", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.9.1", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.34", + "thiserror 1.0.61", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.9.1", + "calloop 0.14.3", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.2", + "thiserror 2.0.17", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "sodiumoxide" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" +dependencies = [ + "ed25519", + "libc", + "libsodium-sys", + "serde 1.0.228", +] + +[[package]] +name = "softbuffer" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases 0.2.1", + "core-graphics 0.23.2", + "drm", + "fastrand 2.1.0", + "foreign-types 0.5.0", + "js-sys", + "log", + "memmap2", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core", + "raw-window-handle 0.6.2", + "redox_syscall 0.5.2", + "rustix 0.38.34", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.52.0", + "x11rb", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +dependencies = [ + "heck 0.3.3", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.93", + "quote 1.0.36", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "stun_codec" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feed9dafe0bda84f2b6ca3ce726b0a1f1ac2e8b63c6ecfb89b08b32313247b5b" +dependencies = [ + "bytecodec", + "byteorder", + "crc", + "hmac", + "md5", + "sha1", + "trackable 1.3.0", +] + +[[package]] +name = "stunclient" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c969a14b4a4c09c320416ebf880b3d5a81ad1612065741eb10521951c06c8991" +dependencies = [ + "bytecodec", + "rand 0.8.5", + "stun_codec", + "tokio", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.29.10" +source = "git+https://github.com/rustdesk-org/sysinfo?branch=rlim_max#90b1705d909a4902dbbbdea37ee64db17841077d" +dependencies = [ + "cfg-if 1.0.0", + "core-foundation-sys 0.8.7", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows 0.51.1", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "system-deps" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" +dependencies = [ + "heck 0.3.3", + "pkg-config", + "strum 0.18.0", + "strum_macros 0.18.0", + "thiserror 1.0.61", + "toml 0.5.11", + "version-compare 0.0.10", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare 0.2.0", +] + +[[package]] +name = "system_shutdown" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7567f71160af5e9abfb4f5a21532cf2174cefe91ac5c336419295685a695cc66" +dependencies = [ + "windows 0.44.0", + "zbus", +] + +[[package]] +name = "tao" +version = "0.25.0" +source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cocoa 0.25.0", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "crossbeam-channel", + "dispatch", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "image 0.24.9", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk 0.7.0", + "ndk-context", + "ndk-sys 0.4.1+23.1.7779620", + "objc", + "once_cell", + "parking_lot", + "png 0.17.13", + "raw-window-handle 0.6.2", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.52.0", + "windows-implement 0.52.0", + "windows-version", + "x11-dl", + "zbus", +] + +[[package]] +name = "tao-macros" +version = "0.1.2" +source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + +[[package]] +name = "target_build_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "013d134ae4a25ee744ad6129db589018558f620ddfa44043887cdd45fa08e75c" +dependencies = [ + "phf 0.7.24", + "phf_codegen 0.7.24", + "serde_json 0.9.10", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006851c9ccefa3c38a7646b8cec804bb429def3da10497bfa977179869c3e8e2" +dependencies = [ + "quick-xml 0.30.0", + "windows 0.51.1", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if 1.0.0", + "fastrand 2.1.0", + "rustix 0.38.34", + "windows-sys 0.52.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminfo" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f" +dependencies = [ + "dirs 4.0.0", + "fnv", + "nom", + "phf 0.11.3", + "phf_codegen 0.11.3", +] + +[[package]] +name = "termios" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" +dependencies = [ + "libc", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "tfc" +version = "0.7.0" +source = "git+https://github.com/rustdesk-org/The-Fat-Controller?branch=history/rebase_upstream_20240722#78bb80a8e596e4c14ae57c8448f5fca75f91f2b0" +dependencies = [ + "anyhow", + "core-graphics 0.23.2", + "unicode-segmentation", + "winapi", + "x11 2.19.0", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl 1.0.61", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa 1.0.11", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde 1.0.228", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if 1.0.0", + "log", + "png 0.17.13", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading 0.8.4", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinyvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio 1.0.3", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.10", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2-3" +source = "git+https://github.com/rustdesk-org/tokio-socks#bdb9aa3de5bac41602d0742b8ef6bbc6bfebd127" +dependencies = [ + "bytes", + "either", + "futures-core", + "futures-sink", + "futures-util", + "pin-project", + "thiserror 2.0.17", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.9", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "futures-util", + "hashbrown 0.15.4", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde 1.0.228", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde 1.0.228", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde 1.0.228", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "serde 1.0.228", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde 1.0.228", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "totp-rs" +version = "5.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c4ae9724c5888c0417d2396037ed3b60665925624766416e3e342b6ba5dbd3f" +dependencies = [ + "base32", + "constant_time_eq 0.2.6", + "hmac", + "rand 0.8.5", + "sha1", + "sha2", + "url", + "urlencoding", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term 0.46.0", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trackable" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32" +dependencies = [ + "trackable 1.3.0", + "trackable_derive", +] + +[[package]] +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" +dependencies = [ + "trackable_derive", +] + +[[package]] +name = "trackable_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" +dependencies = [ + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "git+https://github.com/tauri-apps/tray-icon#0a5835b0e6828e37a1f781de9c2d671ae7a939e6" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png 0.18.1", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "tree_magic_mini" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469a727cac55b41448315cc10427c069c618ac59bb6a4480283fcd811749bdc2" +dependencies = [ + "fnv", + "home", + "memchr", + "nom", + "once_cell", + "petgraph", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "sha1", + "thiserror 2.0.17", + "utf-8", + "webpki-roots 0.26.9", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "tz-rs" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33851b15c848fad2cf4b105c6bb66eb9512b6f6c44a4b13f57c53c73c707e2b4" +dependencies = [ + "const_fn", +] + +[[package]] +name = "tzdb" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a18ee5bde3433d683d41859650804a5ad89cad17f153a53f1e6a96e0da2d969" +dependencies = [ + "iana-time-zone", + "tz-rs", + "tzdb 0.6.1", +] + +[[package]] +name = "tzdb" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b580f6b365fa89f5767cdb619a55d534d04a4e14c2d7e5b9a31e94598687fb1" +dependencies = [ + "iana-time-zone", + "tz-rs", + "tzdb_data", +] + +[[package]] +name = "tzdb_data" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1889fdffac09d65c1d95c42d5202e9b21ad8c758f426e9fe09088817ea998d6" +dependencies = [ + "tz-rs", +] + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset 0.9.1", + "tempfile", + "winapi", +] + +[[package]] +name = "uname" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + +[[package]] +name = "unic-bidi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1356b759fb6a82050666f11dce4b6fe3571781f1449f3ef78074e408d468ec09" +dependencies = [ + "matches", + "unic-ucd-bidi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-bidi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d568b51222484e1f8209ce48caa6b430bf352962b877d592c29ab31fb53d8c" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde 1.0.228", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "users" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "v4l" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fbfea44a46799d62c55323f3c55d06df722fbe577851d848d328a1041c3403" +dependencies = [ + "bitflags 1.3.2", + "libc", + "v4l2-sys-mit", +] + +[[package]] +name = "v4l2-sys-mit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6779878362b9bacadc7893eac76abe69612e8837ef746573c4a5239daf11990b" +dependencies = [ + "bindgen 0.65.1", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version-compare" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "virtual_display" +version = "0.1.0" +dependencies = [ + "hbb_common", + "lazy_static", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wallpaper" +version = "3.2.0" +source = "git+https://github.com/rustdesk-org/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf" +dependencies = [ + "dirs 5.0.1", + "enquote", + "rust-ini", + "thiserror 1.0.61", + "winapi", + "winreg 0.11.0", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote 1.0.36", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.2", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.1", + "rustix 1.1.2", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.9.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +dependencies = [ + "rustix 0.38.34", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfe33d551eb8bffd03ff067a8b44bb963919157841a99957151299a6307d19c" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79f2d57c7fcc6ab4d602adba364bf59a5c24de57bd194486bf9b8360e06bfc4" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2 1.0.93", + "quick-xml 0.37.5", + "quote 1.0.36", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webm" +version = "1.1.0" +source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" +dependencies = [ + "webm-sys", +] + +[[package]] +name = "webm-sys" +version = "1.0.4" +source = "git+https://github.com/rustdesk-org/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" +dependencies = [ + "cc", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d651ec480de84b762e7be71e6efa7461699c19d9e2c272c8d93455f567786e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29aad86cec885cafd03e8305fd727c418e970a521322c91688414d5b8efba16b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.34", +] + +[[package]] +name = "whoami" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +dependencies = [ + "redox_syscall 0.5.2", + "wasite", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "win-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7b128a98c1cfa201b09eb49ba285887deb3cbe7466a98850eb1adabb452be5" +dependencies = [ + "windows 0.34.0", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" +dependencies = [ + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + +[[package]] +name = "windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45296b64204227616fdbf2614cefa4c236b98ee64dfaaaa435207ed99fe7829f" +dependencies = [ + "windows_aarch64_msvc 0.34.0", + "windows_i686_gnu 0.34.0", + "windows_i686_msvc 0.34.0", + "windows_x86_64_gnu 0.34.0", + "windows_x86_64_msvc 0.34.0", +] + +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core 0.51.1", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-implement 0.52.0", + "windows-interface 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core 0.61.0", + "windows-future", + "windows-link 0.1.1", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.0", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link 0.1.1", + "windows-result 0.3.2", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-future" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +dependencies = [ + "windows-core 0.61.0", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-implement" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "windows-interface" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.0", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link 0.1.1", +] + +[[package]] +name = "windows-service" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9db37ecb5b13762d95468a2fc6009d4b2c62801243223aabd44fca13ad13c8" +dependencies = [ + "bitflags 1.3.2", + "widestring", + "windows-sys 0.45.0", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link 0.1.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-version" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-win" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e23e33622b3b52f948049acbec9bcc34bf6e26d74176b88941f213c75cf2dc" +dependencies = [ + "error-code", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0" +dependencies = [ + "ahash 0.8.12", + "android-activity", + "atomic-waker", + "bitflags 2.9.1", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk 0.9.0", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle 0.6.2", + "redox_syscall 0.4.1", + "rustix 0.38.34", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "winreg" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a1a57ff50e9b408431e8f97d5456f2807f8eb2a2cd79b06068fc87f8ecf189" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + +[[package]] +name = "winres" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "wl-clipboard-rs" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de22eebb1d1e2bad2d970086e96da0e12cde0b411321e5b0f7b2a1f876aa26f" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 0.38.34", + "tempfile", + "thiserror 1.0.61", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "wol-rs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5a8a033ef9b208ec8b5946761958ed2b2693ac49b04f647fdc013000870b8f" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.19.0" +source = "git+https://github.com/bjornsnoen/x11-rs#c2e9bfaa7b196938f8700245564d8ac5d447786a" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-clipboard" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.4", + "once_cell", + "rustix 0.38.34", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xattr" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" +dependencies = [ + "libc", + "linux-raw-sys 0.4.14", + "rustix 0.38.34", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xdg-home" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca91dcf8f93db085f3a0a29358cd0b9d670915468f4290e8b85d118a34211ab8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.9.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "zbus" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "byteorder", + "derivative", + "enumflags2", + "event-listener 2.5.3", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.26.4", + "once_cell", + "ordered-stream", + "rand 0.8.5", + "serde 1.0.228", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2 1.0.93", + "quote 1.0.36", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" +dependencies = [ + "serde 1.0.228", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.34", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive 0.8.26", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq 0.1.5", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time 0.3.36", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe 7.1.0", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.11+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zvariant" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde 1.0.228", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 1.0.109", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5587afa --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "hello-agent" +version = "0.1.0" +edition = "2021" +rust-version = "1.75" +description = "Headless RustDesk-protocol-compatible support agent for Windows" +publish = false + +[[bin]] +name = "hello-agent" +path = "src/main.rs" + +# The full RustDesk protocol stack is vendored under `vendor/rustdesk/`. +# We consume it as a path dependency on the `librustdesk` crate (the rlib +# crate-type in its Cargo.toml's [lib] section is what makes this work). +# +# We deliberately turn off rustdesk's `flutter` feature: we don't ship the +# Flutter UI. We keep `hwcodec` for parity with the upstream Windows build +# and `vram` for hardware-accelerated encoding paths. +[dependencies] +# The vendored rustdesk crate's [package] name is "rustdesk" but its [lib] +# name is "librustdesk". `package = "rustdesk"` aliases it so we can keep +# `use librustdesk::…` in source. +librustdesk = { package = "rustdesk", path = "vendor/rustdesk", default-features = false, features = ["use_dasp", "hwcodec", "vram"] } +hbb_common = { path = "vendor/rustdesk/libs/hbb_common" } + +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "io-util"] } +log = "0.4" +env_logger = "0.10" +anyhow = "1" + +[target.'cfg(target_os = "windows")'.dependencies] +windows-service = "0.6" +winapi = { version = "0.3", features = ["winuser", "wtsapi32", "processthreadsapi", "synchapi", "handleapi", "winbase"] } +winreg = "0.11" + +# Embed the icon and EXE metadata via the Windows resource compiler. +# Same crate (and version) the vendored rustdesk uses for its own icon — +# keeping them in lockstep avoids a duplicate `winres` in Cargo.lock. +# +# Unconditional rather than target-gated: build.rs runs on the *host* and +# decides via `CARGO_CFG_TARGET_OS` whether the target is Windows. A +# host-conditional build-dep would hide winres on a Linux/macOS host even +# when cross-compiling to Windows. +[build-dependencies] +winres = "0.1" + +# Match upstream's release profile so the resulting binary has the same +# stripping / LTO behavior. Diverging here would surprise CI. +[profile.release] +lto = true +codegen-units = 1 +panic = "abort" +strip = true + +# Mirror the [patch.crates-io] from the vendored rustdesk Cargo.toml. Cargo +# only honors [patch] at the *outermost* workspace root, so we have to +# repeat it here. (The Linux-only libxdo-sys-stub avoids requiring libxdo +# on the build host; on Windows it's conditionally compiled out anyway, but +# keeping the patch makes a future Linux build configuration easier.) +[patch.crates-io] +libxdo-sys = { path = "vendor/rustdesk/libs/libxdo-sys-stub" } diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b0a2dc --- /dev/null +++ b/README.md @@ -0,0 +1,249 @@ +# hello-agent + +A headless, RustDesk-protocol-compatible remote-support agent for Windows. + +One self-contained binary, no Flutter UI. Designed for one-line MDM +deployment against a self-hosted [rustdesk-server](https://github.com/rustdesk/rustdesk-server) +(or the Pro/admin variant). A supporter using the stock `rustdesk.exe` +client can connect; the controlled-side user gets a native approval +prompt and clicks Yes / No. + +## CLI + +``` +hello-agent.exe --install # register + start service +hello-agent.exe --uninstall # stop, delete, clean up +hello-agent.exe --config # import admin-UI deploy string +hello-agent.exe --install --config # MDM one-liner +``` + +`--config` accepts both forms emitted by the rustdesk-server admin UI: + +* the reversed-base64 deploy string (`0nI900VsFHZ…`) +* the `host=server,key=…,api=…,relay=…` filename form + +If `--config` is **omitted** and no prior install left a rendezvous +configuration behind, hello-agent falls back to a built-in default +pointing at the cybnet rustdesk-server: + +``` +custom-rendezvous-server = rd.gamecom.ch +api-server = https://rd.gamecom.ch +relay-server = rd.gamecom.ch +key = tcxma69cN3OWt25jQ75apSCtaZGIfDqIIP6yGNj3dgs= +``` + +Operators who run their own rustdesk-server must pass `--config` with +their deploy blob; defaults are only applied when the config slot is +empty, so `--config` always wins. + +## Architecture + +``` +hello-agent.exe --install + │ + └──> creates Windows service "HelloAgent", binPath ends in --service + │ +hello-agent.exe --service # Session 0, LocalSystem + │ + └──> spawns into the active console session as SYSTEM token: + │ +hello-agent.exe --server # user session, SYSTEM token + │ + ├── default ipc listener (rustdesk core) + ├── RendezvousMediator ──> rustdesk-server registration + NAT + ├── hbbs_http::sync ──> /api/heartbeat + /api/sysinfo + │ + │ at startup, --server proactively spawns (via WTSQueryUserToken + │ + CreateProcessAsUserW with lpDesktop = winsta0\default — + │ librustdesk's run_as_user uses lpDesktop=NULL which inherits + │ the invisible Session 0 service desktop): + ▼ +hello-agent.exe --cm # user session, USER token, + │ # winsta0\default desktop + ├── binds `_cm` IPC pipe (long-running — one child per session) + ├── reads Data::Login from parent's start_ipc + ├── shows MessageBoxW on the user's interactive desktop + └── replies Data::Authorize / Data::Close (per peer), keeps listening +``` + +The protocol stack (rendezvous, login validation, screen capture, input, +relay) is the upstream `librustdesk` code, **vendored under +[`vendor/rustdesk/`](vendor/rustdesk/)** for an independent build. This +crate is the thin shell that gives us the new CLI surface, the Windows +service shell, and the native approval popup that replaces the stock +Flutter Connection Manager. + +## Repo layout + +``` +hello-agent/ +├── src/ hello-agent sources (~600 lines) +├── vendor/rustdesk/ vendored RustDesk crate + workspace libs +│ ├── Cargo.toml rustdesk's own workspace + package manifest +│ ├── src/ librustdesk source +│ └── libs/ hbb_common, scrap, enigo, clipboard, … +├── Cargo.toml hello-agent package manifest, path-deps on vendor +├── .gitea/workflows/ Gitea CI +└── README.md +``` + +The vendored source has a few local divergences from upstream — all +documented inline at the patch site so they're easy to spot when +re-syncing: + +1. [`vendor/rustdesk/src/lib.rs`](vendor/rustdesk/src/lib.rs): + `mod custom_server` → `pub mod custom_server` so hello-agent can call + the deploy-blob decoder. +2. [`vendor/rustdesk/Cargo.toml`](vendor/rustdesk/Cargo.toml): + `[lib] crate-type` reduced from `["cdylib", "staticlib", "rlib"]` to + `["rlib"]`. We statically link the rlib into hello-agent.exe; the + cdylib link step (used by upstream for Flutter FFI) trips + `LNK1169 multiply-defined symbols` from overlapping + windows-targets/windows_x86_64_msvc versions and we don't need it. +3. Heartbeat intervals lowered 15s → 1s so device-online status in the + admin UI reacts faster: + [`vendor/rustdesk/libs/hbb_common/src/config.rs`](vendor/rustdesk/libs/hbb_common/src/config.rs) + (`REG_INTERVAL`, UDP rendezvous re-register) and + [`vendor/rustdesk/src/hbbs_http/sync.rs`](vendor/rustdesk/src/hbbs_http/sync.rs) + (`TIME_HEARTBEAT`, HTTP `/api/heartbeat`). + +## Build + +### Local (Windows) + +```powershell +$env:VCPKG_ROOT = "C:\vcpkg" +cd vendor\rustdesk +& "$env:VCPKG_ROOT\vcpkg" install --triplet x64-windows-static +cd ..\.. +cargo build --release --bin hello-agent +# → target\release\hello-agent.exe +``` + +The first build is slow (~15 min) because cargo compiles the entire +RustDesk crate plus its workspace libraries. Subsequent builds are +incremental. + +### CI + +[`.gitea/workflows/build-windows.yml`](.gitea/workflows/build-windows.yml) +builds on a self-hosted Windows runner. It checks out hello-agent +(self-contained, no submodules), runs vcpkg against the vendored +`vcpkg.json`, builds, and uploads `SignOutput\hello-agent--x86_64.exe`. + +## Re-syncing the vendored copy + +To pull updates from upstream RustDesk: + +1. Sync the upstream rustdesk repo locally and `git submodule update --init` for `libs/hbb_common`. +2. `rsync -a --delete --exclude=.git --exclude=target --exclude=flutter --exclude=appimage … upstream-rustdesk/ vendor/rustdesk/` +3. Re-apply the one-line `pub mod custom_server` patch in + [`vendor/rustdesk/src/lib.rs`](vendor/rustdesk/src/lib.rs). +4. `cargo build --release --bin hello-agent` — fix any breakage from + upstream API drift in our [src/](src/) modules. + +## Stale keys / supporter "stuck on connecting" + +The agent's identity (`id`) and `key_pair` live in `HelloAgent.toml`. +They're generated once on first run, registered with the rendezvous +server, and re-used forever after. **If the rendezvous server's cached +entry and the agent's local keypair drift apart, the encrypted handshake +silently fails on the supporter side** — the supporter's stock rustdesk +client shows "Please wait for the remote side…" / similar, the agent log +shows a `Connection opened` followed by ~30 seconds of nothing then +`Peer close`, and the popup never fires (because no `LoginRequest` ever +decrypts). + +How to recognize it: agent log says `register_pk of rd due to key not +confirmed` followed by `Generated new keypair for id:`, *and* the +rustdesk-server admin UI already has a record for that agent id from +prior runs. + +How to recover: + +1. Delete the device record for that agent id from the rustdesk-server + admin UI's device list. The next agent heartbeat re-creates it with + the current public key. +2. Restart the supporter's stock rustdesk app (clears its in-process + pubkey cache). +3. Reconnect — the supporter now resolves the current pubkey, the + handshake succeeds, the popup fires. + +`hello-agent --uninstall` deliberately preserves the LocalService config +dir so the agent keypair survives an uninstall→reinstall cycle. To force +a fresh keypair, also run after `--uninstall`: + +``` +rmdir /s /q "%SystemRoot%\ServiceProfiles\LocalService\AppData\Roaming\HelloAgent" +``` + +…and then delete the device record from the admin UI as above. + +## Verifying end-to-end + +1. Install: `hello-agent.exe --install --config ` from elevated PowerShell. +2. Confirm: `sc query HelloAgent` → `RUNNING`. +3. From another machine running stock `rustdesk.exe`, enter the agent's + ID and click Connect. +4. The agent's logged-in user sees `HelloAgent — Allow remote support?`. + Click Yes; session opens, mouse/keyboard/screen all work. +5. Uninstall: `hello-agent.exe --uninstall`. Confirm `sc query` returns 1060. + +## Namespacing + +`hbb_common` ships a single global, `APP_NAME`, that drives the location +of every piece of on-disk state (config dir, log dir) and the prefix of +every named pipe. Upstream defaults it to `"RustDesk"`. Hello-agent +rewrites it to `"HelloAgent"` as the very first line of `main()` — +identical to the write path the upstream Flutter build uses for OEM +rebrands ([`read_custom_client`](vendor/rustdesk/src/common.rs)). Because +`APP_NAME` is a `RwLock` read lazily on first use, doing the +write before any path code runs is enough to redirect *every* hbb_common +consumer in the same process tree. + +In practice that means: + +| What | Stock rustdesk | hello-agent | +| --------------------------------- | ----------------------------------------- | ------------------------------------------------- | +| User-mode config / logs | `%APPDATA%\RustDesk\` | `%APPDATA%\HelloAgent\` | +| Service-mode config / logs | `…\LocalService\AppData\Roaming\RustDesk\`| `…\LocalService\AppData\Roaming\HelloAgent\` | +| Identity file (id + keypair) | `RustDesk.toml` | `HelloAgent.toml` | +| IPC pipe namespace | `\\.\pipe\RustDesk\query…` | `\\.\pipe\HelloAgent\query…` | +| Windows service name | `RustDesk` | `HelloAgent` | +| Install dir | `%ProgramFiles%\RustDesk\` | `%ProgramFiles%\hello-agent\` | + +The two binaries can therefore coexist on the same machine without +clobbering each other's state. The override is set in +[`src/main.rs`](src/main.rs) (`pub const APP_NAME: &str = "HelloAgent"`) +— change it there if you ever need to re-brand. + +## Where logs go + +`hbb_common`'s logger writes per-mode rolling files under `/log//`: + +| Mode (CLI flag) | Effective user | Log dir | +| --------------------- | ------------------------------- | ---------------------------------------------------------------------------------------- | +| `--install` / `--uninstall` | calling user (must be admin) | `%APPDATA%\HelloAgent\log\install\` (or `…\uninstall\`) | +| `--service` | LocalSystem (mirrored) | `%SystemRoot%\ServiceProfiles\LocalService\AppData\Roaming\HelloAgent\log\service\` | +| `--server` (worker) | LocalSystem (mirrored) | `%SystemRoot%\ServiceProfiles\LocalService\AppData\Roaming\HelloAgent\log\server\` | +| no flags (dev mode) | calling user | `%APPDATA%\HelloAgent\log\hello-agent\` | + +The `cm_popup` module also writes a parallel diagnostic trace at +`%TEMP%\hello-agent-cm.log` (kept around for debugging the IPC handshake; +it duplicates info that's already in the main log). + +## Status + +- ✅ Windows x64 (physical console *and* RDP sessions — the agent picks + whichever session the user is actively using) +- ✅ Coexists with stock RustDesk on the same box — config dir, log dir, + and named pipes are namespaced under `HelloAgent` rather than the + upstream default of `RustDesk` (see [Namespacing](#namespacing) below). + The only residual contention is the optional direct-server port + (TCP 21118) and LAN-discovery port (UDP 21119); both default to off, + so a vanilla install of each side can run simultaneously. +- ⏳ Linux / macOS (out of scope for v0) +- ⏳ Code signing (CI warns, doesn't sign) +- ⏳ Multiple simultaneous interactive users (only one can receive the + approval popup at a time — the one in the `WTSActive` session) diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..ddc13d5 --- /dev/null +++ b/build.rs @@ -0,0 +1,40 @@ +// Embed the application icon and EXE metadata on Windows. +// +// The icon (`resources/icon.ico`, multi-frame: 16/32/48/64/128/256) ends +// up as the executable's IDI_ICON1 resource — that's what Explorer, the +// taskbar, Alt+Tab, the Task Manager, and the title-bar of any dialog +// hosted by this process pick up. Regenerate the .ico from the source +// PNG by running `python3 resources/build_ico.py`. +// +// The version-info block populates the "Details" tab in the EXE's +// Properties dialog (ProductName / FileDescription / etc.). winres +// derives FileVersion / ProductVersion from CARGO_PKG_VERSION +// automatically. +// +// We gate on `CARGO_CFG_TARGET_OS` (the *target* OS, not the host) so a +// cross-compile from Linux/macOS to Windows still embeds the icon. winres +// inspects the active linker/toolchain (windres for GNU, rc.exe for MSVC) +// when invoked. + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=resources/icon.ico"); + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + if target_os != "windows" { + return; + } + + let mut res = winres::WindowsResource::new(); + res.set_icon("resources/icon.ico") + .set("ProductName", "HelloAgent") + .set("FileDescription", "HelloAgent — RustDesk-protocol support agent") + .set("CompanyName", "cStudio GmbH") + .set("LegalCopyright", "Copyright © 2026 cStudio GmbH") + .set("OriginalFilename", "hello-agent.exe") + .set("InternalName", "hello-agent"); + if let Err(e) = res.compile() { + eprintln!("winres: failed to compile icon resource: {e}"); + std::process::exit(1); + } +} diff --git a/ci/runners/linux/provision.sh b/ci/runners/linux/provision.sh new file mode 100755 index 0000000..aa7d06c --- /dev/null +++ b/ci/runners/linux/provision.sh @@ -0,0 +1,271 @@ +#!/usr/bin/env bash +# Provisions a Debian 13 (Trixie) container or VM as a Gitea Actions runner +# that does Authenticode code signing for hello-agent via osslsigncode. +# +# Idempotent: safe to re-run. Does NOT generate or import the signing key — +# operators do that out-of-band after provisioning. The script only sets up +# the directory layout, ACLs, runner, and systemd sandbox. +# +# Designed for an unprivileged Incus/LXC container on a hardened host: +# * No build toolchains. Smallest possible attack surface. +# * Service unit is heavily sandboxed (Read*Only*Paths, NoNewPrivileges, …). +# * Outbound network restriction is the LXC HOST's responsibility — the +# container itself can't enforce it because nothing inside the namespace +# is privileged enough to load nf_tables. Configure on the host. +# +# Usage: +# sudo ./provision.sh \ +# --gitea-url https://gitea.example.com \ +# --runner-token + +set -euo pipefail + +# ---- pinned versions (mirror .gitea/workflows/build-windows.yml where they overlap) ---- +RUNNER_VERSION="0.2.11" +NODE_MAJOR="20" # act_runner spawns Node for JS actions (upload/download-artifact) + +# ---- defaults ---- +RUNNER_NAME="$(hostname)-helloagent-sign" +RUNNER_LABELS="self-hosted,linux,signing" +SERVICE_USER="hello-signer" +PKI_DIR="/etc/pki/hello-agent" +GITEA_URL="" +RUNNER_TOKEN="" + +# ---- arg parse ---- +while [[ $# -gt 0 ]]; do + case "$1" in + --gitea-url) GITEA_URL="$2"; shift 2 ;; + --runner-token) RUNNER_TOKEN="$2"; shift 2 ;; + --runner-name) RUNNER_NAME="$2"; shift 2 ;; + --runner-labels) RUNNER_LABELS="$2"; shift 2 ;; + --service-user) SERVICE_USER="$2"; shift 2 ;; + -h|--help) + sed -n '2,20p' "$0" + exit 0 ;; + *) echo "Unknown arg: $1" >&2; exit 2 ;; + esac +done + +[[ "$EUID" -eq 0 ]] || { echo "Run as root (use sudo)." >&2; exit 1; } +[[ -n "$GITEA_URL" && -n "$RUNNER_TOKEN" ]] \ + || { echo "Missing --gitea-url or --runner-token" >&2; exit 2; } + +. /etc/os-release +case "${ID}-${VERSION_ID:-}" in + debian-13|debian-trixie) ;; + *) + echo "WARNING: tested only on Debian 13 (Trixie). You're on $PRETTY_NAME." + sleep 3 ;; +esac + +log() { printf '\n==> %s\n' "$*"; } + +# ---- 1. apt packages (deliberately minimal — no compilers on a signing host) ---- +log "Installing apt packages" +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y --no-install-recommends \ + osslsigncode openssl ca-certificates \ + curl wget git \ + sudo gnupg + +# Node.js (act_runner spawns node for JS actions like actions/download-artifact) +if ! command -v node >/dev/null; then + log "Installing Node.js ${NODE_MAJOR} LTS" + curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - + apt-get install -y --no-install-recommends nodejs +fi + +# Sanity-check osslsigncode. Debian 13 ships 2.9, which has -ts (RFC 3161). +ver="$(osslsigncode --version 2>&1 | awk '/^osslsigncode/ {print $2; exit}')" +if [[ -z "$ver" ]]; then + echo "could not parse osslsigncode version" >&2; exit 1 +fi +log "osslsigncode $ver OK" + +# ---- 2. dedicated runner user ---- +if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then + log "Creating system user $SERVICE_USER" + # No login shell on purpose: this user only runs systemd's exec, never logs in. + useradd --system --create-home --shell /usr/sbin/nologin "$SERVICE_USER" +fi +RUNNER_HOME="$(getent passwd "$SERVICE_USER" | cut -d: -f6)" + +# ---- 3. PKI directory ---- +# Layout: +# /etc/pki/hello-agent/ +# chain.pem leaf || intermediate || root 0444 root:root +# codesign.key PEM private key (or PKCS#11 stub) 0400 root:hello-signer +# +# Why root owns the key file but hello-signer can read it: prevents the +# runner user from rewriting / deleting the key (rotate operations require +# root), while still letting osslsigncode open it for signing. +log "Preparing PKI directory at $PKI_DIR" +install -d -m 0755 -o root -g root "$PKI_DIR" + +# Touch stub files if they don't exist yet so systemd's ReadOnlyPaths +# resolves cleanly on first start. Operator overwrites these post-provision. +[[ -f "$PKI_DIR/chain.pem" ]] || install -m 0444 -o root -g root /dev/null "$PKI_DIR/chain.pem" +[[ -f "$PKI_DIR/codesign.key" ]] || install -m 0400 -o root -g "$SERVICE_USER" /dev/null "$PKI_DIR/codesign.key" + +# Re-assert ACLs unconditionally — defends against an operator copying files +# in with overly-permissive umask. +chmod 0755 "$PKI_DIR" +chown root:root "$PKI_DIR/chain.pem"; chmod 0444 "$PKI_DIR/chain.pem" +chown root:"$SERVICE_USER" "$PKI_DIR/codesign.key"; chmod 0400 "$PKI_DIR/codesign.key" + +# ---- 4. act_runner ---- +RUNNER_DIR=/var/lib/gitea-runner +mkdir -p "$RUNNER_DIR" +chown -R "$SERVICE_USER:$SERVICE_USER" "$RUNNER_DIR" + +if [[ ! -x "$RUNNER_DIR/act_runner" ]]; then + log "Downloading act_runner $RUNNER_VERSION" + case "$(uname -m)" in + x86_64) arch_label="amd64" ;; + aarch64) arch_label="arm64" ;; + *) echo "Unsupported arch: $(uname -m)" >&2; exit 1 ;; + esac + curl -fsSL -o "$RUNNER_DIR/act_runner" \ + "https://gitea.com/gitea/act_runner/releases/download/v${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-${arch_label}" + chmod +x "$RUNNER_DIR/act_runner" + chown "$SERVICE_USER:$SERVICE_USER" "$RUNNER_DIR/act_runner" +fi + +if [[ ! -f "$RUNNER_DIR/.runner" ]]; then + log "Registering runner with $GITEA_URL (labels: $RUNNER_LABELS)" + sudo -u "$SERVICE_USER" -H bash -c " + cd '$RUNNER_DIR' && \ + ./act_runner register --no-interactive \ + --instance '$GITEA_URL' \ + --token '$RUNNER_TOKEN' \ + --name '$RUNNER_NAME' \ + --labels '$RUNNER_LABELS' + " +fi + +# ---- 5. systemd unit (heavily sandboxed) ---- +# +# Why these flags: the signing runner does almost nothing — pulls a PE file, +# calls osslsigncode, uploads. So we can lock it down far more than the +# rustdesk build runner. +# +# Notable omissions: +# * NO MemoryDenyWriteExecute=yes — Node.js (V8 JIT) needs w+x mappings. +# Action runners that invoke JS actions (download-artifact etc.) break +# under MDWX. The other sandbox flags still cover the realistic +# post-exploitation paths. +# * PrivateDevices=yes is fine for software-key signing. If you migrate to +# a USB hardware token (YubiKey via opensc-pkcs11), set PrivateDevices=no +# and add a DeviceAllow= line for /dev/bus/usb//. +log "Installing systemd unit" +cat > /etc/systemd/system/gitea-act-runner.service < Site Admin > Actions > Runners + # for "${RUNNER_NAME}" with labels "${RUNNER_LABELS}" + + 4. Lock the LXC HOST firewall down. Outbound from the container should + reach ONLY: + - your Gitea instance (HTTPS, your Gitea host) + - the RFC 3161 timestamp authority (HTTP, e.g. timestamp.digicert.com) + - apt + node mirrors (HTTPS, only during provisioning; + revoke after first successful run) + Drop all inbound. Configure on the host (nftables / Proxmox firewall / + Incus proxy device) — the container can't enforce this on itself. + +---------------------------------------------------------------- +EOF diff --git a/resources/build_ico.py b/resources/build_ico.py new file mode 100644 index 0000000..c37356f --- /dev/null +++ b/resources/build_ico.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Pack icon.png into a multi-size icon.ico used by the Windows resource compiler. + +ICO format note: + Vista+ allows PNG-encoded frames inside ICO files, BUT the Microsoft + resource compiler (rc.exe) only reliably accepts PNG payloads for the + 256x256 frame. PNG payloads for 16/32/48/64/128 are silently dropped + (or worse — rc.exe writes a resource section Explorer can't decode and + the EXE shows the generic icon). So we encode small sizes as DIB + (BITMAPINFOHEADER + BGRA pixels + AND-mask) and keep PNG only for 256. + +Requires Pillow. We use it to decode the source PNG and resample. + + python3 -m venv .venv && .venv/bin/pip install Pillow + .venv/bin/python3 resources/build_ico.py +""" +import io +import struct +import sys +from pathlib import Path + +try: + from PIL import Image +except ImportError: + sys.exit("Pillow required: pip install Pillow") + +DIB_SIZES = [16, 32, 48, 64, 128] # BITMAPINFOHEADER + BGRA +PNG_SIZES = [256] # PNG payload (preserves alpha cleanly at large size) +HERE = Path(__file__).parent +SRC = HERE / "icon.png" + + +def encode_dib(img: Image.Image, size: int) -> bytes: + """Encode an RGBA image as a DIB-format ICO frame. + + Layout: BITMAPINFOHEADER (40 bytes), then BGRA pixels bottom-up, then + a 1-bit AND-mask (also bottom-up, row-padded to 4 bytes). The header + declares double the actual height so Windows knows the AND-mask is + appended — this is the (counter-intuitive but mandatory) ICO + convention; the file is otherwise an ordinary 32bpp BMP minus the + 14-byte BITMAPFILEHEADER. + """ + img = img.convert("RGBA").resize((size, size), Image.LANCZOS) + pixels = img.load() + + # 32bpp + alpha — AND mask can be all zero (fully opaque-from-mask; + # the per-pixel alpha is what Windows actually composites). Still + # required structurally. + bgra_rows = [] + for y in range(size - 1, -1, -1): # bottom-up + row = bytearray() + for x in range(size): + r, g, b, a = pixels[x, y] + row += bytes((b, g, r, a)) + bgra_rows.append(bytes(row)) + bgra = b"".join(bgra_rows) + + mask_row_bytes = ((size + 31) // 32) * 4 # 4-byte aligned + mask = b"\x00" * (mask_row_bytes * size) + + header = struct.pack( + " bytes: + img = img.convert("RGBA").resize((size, size), Image.LANCZOS) + buf = io.BytesIO() + img.save(buf, format="PNG", optimize=True) + return buf.getvalue() + + +def main() -> None: + if not SRC.exists(): + sys.exit(f"missing {SRC}") + src = Image.open(SRC) + + frames = [] + for sz in DIB_SIZES: + frames.append((sz, "dib", encode_dib(src, sz))) + for sz in PNG_SIZES: + frames.append((sz, "png", encode_png(src, sz))) + + out = bytearray() + # ICONDIR: reserved(2) + type=1(2) + count(2) + out += struct.pack("*lPK2G3q0-5#_}aq+KM?h4*5rF&`o-|@_`+c4AcfNc?{_MVal&km9Wc?YuKSu5G98G-E5@k{ax zu@~6Bjr-Y~^mO^bEr-~#$iLXB{ij%T=sww-v<&&OsVgG3uh_NcWYkIdy16T50iOPB z<@9BY;9w`CPqOs1mx{Y_$%>aLscg^sC?0p==y|qn`40Yl(6m*1CRusgC11PCHu$Z} zT<^D%>aaq(#&@;!_LT%_{G~WW{N*@B;`KXHxinL{KkR^P%d%~NKlARjWH!8?haI7y z@JgRGA@^=PV5j$Q!=i-?3^?R0?@s3{~#XV&6MlTG- zw^~|C(oZ(^GL2ZhXZ5bt5t~95ZrU8SWJ}oE+3S*m=d4er`z`C{glyg57aG2HPRQEf zy~mgX_u|_^g}^HR{0(VjI?=GPiRVAxNF>?62@hBDFV(H^xuA1D;A-90p&f#H9C+BP zv)3nDf<)pa@!|kk>e_ZfK=~prCs?w_F^RH0m^QbqhJg1XE_>Q<{LI_mw=4s4Mw@~j z1;`)#ydiirzc0c7_yWA8;xA=m`l2th5@mg#7aoH=r*=UCHY*T79n>B2XV(bvl$7VV<;T=nNZ-?|=UR(~S z)lQNiQF7w#U zGdJ1GXQ?bb^`$brc**YGNM>tiZeT_Y%{QYgfd>^n3mG*v-+eedmZDkay@b26(1l@& zg=3a5{|SML-NAblX(`X8PaZv#FQ2r6_4vbJGS#CPg&$R+Sl4pBCvTXwk-dJEF3(7N zDSdkXvEt?PR0Z9$XZN2d(w@DLN#DQAjNN*Sp)C4N$tQI4XNA5_I|3t6_WL6{W7)|5 zW7yo`v)S6&E7)qESmfHRENa7c7Q1UNTkaLWCYgCLsuz3y+?s? zP@V$iJiYgr=#|QnZzVBLQ!h4p-~@I&>NtCw@tVJ*wDK1ZQ{?V_J>pPyO-ZA+t7ooL zpz@2xE@6R_7W1-WckX3#+-9>?RR6=<_b@zr+jz0vYxgiFm$Ab;j>uQ}1k0CASs{l` z0bKyFrKP-J)UWgUZ(F{d1$nRJ@yRy{&d1OA{oC{m#qy~u6^$giZ9A6Kw~pBw%ih0z zD<__3J649Vt;;sDAg`rtoZ$pE&2BndLAuA@kR5C#=`VqkgFs_m=fEjJqzi3kt7ood zuU@3_0QyZNT7~(Cvp1CP9o75j?xQ>oX+an8NaUJ5tn;rutUHzLYI}6&Uka2z#nPKa ztO=)Y*R$EfX0cKIJ=g?ePv$pbF6lYj*qWKkS%Bv<_WETyr|F}l2b`w&6Yg@{qP*Q} zBbf)`e7V<3(yuZ=@2oiBf|vjDl{{))w4zIyp0;%*O}m8oZw2}lvxoVyHNHXY;FjI& z*zSGI*Lfz_yN>LPB78@&*{;59OW-!5Wd;L}f$j(|9;a}4K!4nyCSC3+@!Qdeqij3z z--{<ACoN~!&t7NmUcX^6+YfU&2wM5N z%;f?1kd5Fy&=>8Tw*|Tfyub-PD^T{0Q#Y7x2j>eFB^7@}*$9xI^&~nK9Xbs=c`E8G zV8Bm7<;mW^c_X8H8OdWA@t2I+j*Mg)g;z4j*9-)@fBo{6l<11|Qu>Yuq~{?$U6J-Q zmBnm6!t6S_o@p*=R|$Qis_q~`Ayt1{tXGwRo>R=GC4|fmV}~{$W{0=LvV&oVSxop5 zet($S>XEHS*p;WZl-#r zx|?+>-Q5(SbLk!?{7UH?OIo#|JkeJubpq5k=O5P*M1PiZk`J*Y8!|jiCS`nx`NSRJ zTh-U0*Zka%_vc98xpU_t*@L5w;}3|lXl-}WvbEhA+>5yEY4Mzgaz6(be~&zIW2R%} zZmMH79QPs)_uu*_$}-cj9*uzVMBKM7TfHoE9h*@I>T%z*{Qvv(0(Z_DqyqvP8weo z9^BAOuYV0Aul}`+HXGD7IykWQ&?tkNMym{J8M~OZq+z=1mzOfs(;|<4H4I()Y7bu3 zzm{PXg@b)-4B3n^(crpfdVnE2T%yeE*DO5JD8{IXx#hsRCYM~g4rhLD3)#Ba8)(jG z3&S{i+2j>$s`+$g*~g8&DvPEKwjDGl-aZA&mxUk8^;!egY6c&cJF6c&tmIm zg|diMk!;V}y)4{+8(TCgfVp-X$p8b{_AJH7DhtMJxr_J-788COT+d{edv^~Ou{x3^ z-Aa_deEvd8wi)T`^mN&))KuBC$B(6VZpBHD?u})O#|AL7<~EteP0YuDQ&sQ@8h&); z?~sPlnl!V{44S-(T|09P_8}SSu@J=lI=V|YUM+}1&M z7_#rl0SjOPY#swW%2A$_lXA`o_uxm&K^fFF{%gT-f8ZlC_33kNqrH6MGFv=;DcN4e zG3Rc>nQQkEY{Jkf+>Z7n`4P9LB;UTH_-p^MOxG?WnQ>#w5yZ2Ff`WoDr&UP(-mtOR z2v_})?B?G$GGS{1ys&?AJ1E&gU=snp@T`n`%IgH%NqTBpCSU`M$XnHhvTXMTb&V!Z zGMWZ>GGTi{UhDlrm`QUhvQLd+OFb9UJmo5GNAnv#huL>_Vvb$h*tx^!xNRmW;kIJW z#yxVfb+aK2P2&bPFpV41&@AhUI74~|TNq$}Om+VN=MzrTtEaEfoawPFo0oVG+iNP> zol+h>&73%RD%D{q?9^i1FL7?`T#nl7tMjoJKaaaK^vrn4K3s4E5*s^Q^3(H=RcjnNBq~j z>HzmSE(_UPvM;?N8#C-(R3EZQhH`u1?%*xlPP#c@1KGX9*q(LUxD6Y&W7smCySfV- zG~qFoX!k;%bmNYk+M+y>t`sLHI~HBjxXnx*cG&JC>sL``FH5{j`U;bKt2Z^r=2*@1gJ`WQTh%1MiF-?8$&z*tcQ#1^lpm4(~IHEgUnS z@Ebw#TUg|}twfLAWIJCa-MMy~r4ddcYXr}nCfl(;$%e}(F7djg zU=v2YA(Jo{a^c7Y;tz{NFM@qwJ+q3IZ5>jd+zbG_HEil^Aby#Th9?}w80pniv#hx8_{_T^Dr1o zc4%e$SUod{4eK#dX}c#}0mrFNp0m3&R|A})Txt*8rmn&vZ?85ToY-FS*RX2iz~V06 z;_uNOz)zSHS~hhho8?Nl8nuYG5heW36}K-ZFsrtXX@K3ev+K_!Bk$jje?X#It~h(> zEVF6v$abv`=W|TppZ%M6aa;cgngam;9Nitw_EX(3_W_x-h}s0&jIe0~_o5D z;tz!%W&>&)9Ruw6^{PBz+KMT`fFHJc($T_*rgm(9*e>3N0VmqRvZ(=VFUBOF$oBVxPDA8gJ(Qqxb*%5umsQQV=yseA6tKgRg zj9tchS2LKdgkB;sZemf#x{ZVE8ODBtnacHDXm=Xl4w73M2ZAJhW@^H!k? z*D}LKX4DQycG4V~I_`6WIeR>ZiN1{p-c9X&6!USK!=_qI=dx8D7r3W(uef#LmK?B> zF2Z$pu1D}bU|{gFi7Pn`(oMQkU{KZ4&OGco7oZR^eZILHRb z&F2r5JdXLLPX+J-`k3tXK>Svk7X=S-epm4fPe=TRdXik0La**$YsgyQ9grwGUk0k{?c`Ze{rncD()K)1~>Z*9{WxECivI&yW}4tLh`L-mUJ_T2i)JibvG*@kDR_oTr#D* zarP#Q*>Z&WdjvxFcxO=Cc%@yx;oMh7z^DfHN4M%@GvkJqHGBUta18YiQA3+r#!+5R ztlBv~qrUH%b#9@ZLkiwowRL!J*HPJCsDG7OwR3n${r6{sRU7*kCe5vr3FesI+JmMV zH@6@^A>}VS&xrUOu36(6{*L4m`~~~h?O(EUwE?BN)iC@ux6rko!EbmsptkXvIWBY9 zJU2f!pqBAE!&-*FLthzKV_>;Y1%rAPzme^yxagZuz6$56-YN%ZbBO{iU|Y&Vy|a8Q z`_?p^G~0Q$!f%+L0{6KTk!y5T*&oKaxu*TX7pVVdgAUEXBHiHnrp{vrOkm>%da^SS zo3#l4VgA`lG}!uIgJ0-a=DN1l2!LP2;a>CmJ{W)wgYbPy)0CpYfCd8^3}`Tr7Yx8p zOOJv#1>vV9(WeK^PlEvs1~eGZU_gTb4FjxXpw-_5DPhmg$6acUVt84=KLJL)1Sw(u8dHv$l#jUALd*@!Xp2^s{ zoznpOPR<=i4;Wnq`)r6G;LQ&_WX}g>j2hC^x&y;&F@CtdDd;DbA!NuI9snsr;KX^2xff=K{LztzGJu`1%$4r`7F^YdfbLJP# zb?xT5{y73m1o-dYz@Yvu@Z7qLwNVJtdquk7HCgReP{bW%v;$-n>4m&lT5s5 z-ujU2VbXoss~4#X@_8gb4DuIvLOwwcX`T4}2kc%_k|O@bP5H@#$E5+2mJ%LZ2?nbS zn|98aV-h&{&_Hqn0P|5KPw|@e6>u?XWSN1y;QM#$!c8gp*>FEc%)i5jg|8_kKL^b5 zQ=Ec)>tu(v#mGDfX25PqbwFK^pU4;YIsL?Y$4M#ewrTA|z7N}^R9EuzQ?8-L zJUo1dxPJ|m4__PMUv&RA%`L~>W{C;$?Bt;nQusQVHlcRX)?p#aQ|13D9Awp<_FGv9 z_`R%V@Os1dP53r&9{`pA0(>U9FN4ZA0R97j6)*#K)B$xtoz!6XNA-oQvg_vAc7qA}GQ{v^OR*6PzczFwRBeaT1T8uxX8pAvj55^vsS2{&)CxNFxH z^F0PxyNo@xz6sy$0=x!pG-57QTCl(ts%e3??OxL$t12K&Pm_R8gviv z49p}tD)&erT4&W+<>LjvA>qHq{fSgQZ0hNNGr)qn*i)TQw^4?pN@V9l!S~dc_*pc! zCqKGlneYpR&l-690r{oOa`hws9z$l`)}Hp~m_dHP%gBFp4edFxl6->}(!LF2$*0~J z{(I!Zd4T(t19vwsUn4%ap%`yGNj{*Kk(7LwuMqBX40zYK`e14|-g5Ylpf3NQxhjtP z+!7A3wjTbf+?O*q98g^j?>L-^x}lEX0a0f$uk74oL|yXl&!l}GWblUp?C=G|J`nD` zJ$T&~j#c@+YZ!&N4r+1C&vfX8FlA=(c~ve{@;9$2l$s}Pb;3` zZw%jX|8W5_>OV74S3&`wHy1k9&=yw9ojSR*hlvlR@cBaf0e`_?ZV36pZ6JSd`1Qfp zY&Y%mfxSN9FBi4p!!vx5fse`NQ#cN=R~O{+-S{N-Ad&W*c=$*`eTDqRvuE<>PoBsh z-=q7KXYv%)eahn}@TZ2~QI2#M^#NS)OZ6Buk^IQEQdxt^A2FKCJ+yt1PF%r5*ykhR za-0-x+q9+4a`1qtJ9Ii)L-Q=iYc;u=%H{BFMZ3Y86y&|iCy4x!mA;wW*N$)iKVbL( zW4{UbGb3K?e*zr1^%_b0COC0_J@~N!7T^?h!`hC7%ki9!S5IAKw8t3dHMC>UJSU## zvX0)vNAD5&otf%bQ=b^jQXW6&yt#2v7<{mEmXlOA@IthZV_nL?ddAOCXW#%o+m0^X zhc>oi=Z{^Kzkl* z{pZq}m1B|n*nu!w2SV@AE`SI4U(X-q$A&huVzhsrlD`QC=n~+E*>3YFe_KvhzYzhad8Orjrlx^fZ3u>qv+{fU8wAmUBJf z9IaJAo~S$Oq>@DpYkHzK?B%}n@HgH?Iy`Wr&I38hx=^L8Q?98}>P0!G%O(?nSJIzl zGi+y*-eMdn>OY{aQPf=5g}nYLBwL~DOeMY3wYxj72Y4R!@uoVG4#C$8kUt@>KlP{7 z=Q}{ZS>+Ls@^NY!fe&--w00KJaN5au0GIKJR--4nXc_ zwSU45)+HdF;D4-(T1xv+Vr|xyQ&)(OX@|)VHnEr^>TllKcBo}b2O75|$rK6&rv=t4kv_`z(t=#$ zbSL@(2jGL9t2R^nwIIKKA7%d-tm+?C@)B*vw1pL|>sZ9sTnHW%^~%$g=!U-QgRD#a zr>x5+6L}w~hEpy}sa=!5y39(~fuYW#{^JIY`-5cQyB#a}o^lG%3Oe;nr`aUGEqPrb z|3v?$YV)>)QQWwPw0Q2;xqbQHrBvX zF{H6sw5YqNza!~GMh(na%#I`2zl-Wm>xZb%I!NorFzy@CceHX{8?^+91kD9*{-Y=kSXHr3LPRyPR)K%zlp!pmc(_)Pd)}VO|9RJCHG)RkaFuql<%SZQWGBLZX6Y04}Co!lRshD_i>8lG`{9`n%zv&Eprc8my5LcCWUOP zqAodILED^U9r{%8Jm>&B;N0Q!WfWf@Eyid80)}( zI<%1$vuf`+4E6lnzH8#WeJ7`Z)D}{(r@`J$`{mRpKrSj2#1bz_-vDm{$I!tala4BM zanwJ1;AxO9k0EsKp3#*D)jcg! z&dhV5Z)I5D|MbCA>BZxhWT6W-F^~QeXy0B7Mtj{wV*cby=$;)H>TQJ9O?NVGY!NcB z&d{f3H1Fm9rw5x%<7FSaxono>JmyOQ*Utr!Zl=RrKE9kn^F>oErgM3KI9~_aqcbnT z(T`x5!={){XTy7p;d5c6+q^PvVzu3|qkEQpL%lB9>-nkY>~oL47QVmPwsYxj+{AJW z%||UEo7sBO8$-V|kY2EHKyAa2LG_IH4kp_KNpO5 z(Y_no$qv8PsHx>Nn>J1ZM;Urn7InhB->2pqM7qyifxoZCVNQ8_n)y_uv4adY^x#=5 z(5K4BnbB6G`Se5Io}cilfu-~V;QKOt*qaIR;!A?q^V7Yj`|pDrn7zPW35&-rgpDB8 z`A?5>fEoD+e1GW`-*^#BNS`0{R;zD*-}1RXoDD+tkMni%Rm^dlqZm}*G#-1$s0!n0 z^1I-Ilb>=AArl>|%g$ZgnRA!nuqj>n->7?*-mOLZQ<<8#v`evQX_rRp811P3|Cx1+ z|CX0T8#U9lJ%xb$MBKm4`@ir#>Izw9*3#x2!hb++8VqPKpuvC!0~!o`B?d4#>Wyzn`_9@Yg3dzM z{4^NQU_gTb4F)tA&|pA=0SyK;7|>uqg8>Z&G#JoeK!X7d1~eGZU_gTb4F)tA&|pA= z0SyK;7|>uqg8>Z&G#JoeK!X7d1~eGRD+b^TjJ0sewZtFR7b8x@W1TPdslc;pU+4dy zoK^T<)yleh@iooa*F$H=&41i~ny)8Eo$|^n>gffJDDJ<=3;%OC0MF(`a|<1ds%AP? zdS<$I_Ovc;Hm!|XZr;)+l+G&LLH8SIt;`bICvpaz7wv4KW7EM*$E>0{AK)Hs=fBV< z03%?{4nOLIx}lD!E9%Va4jd3JfD_<`-{2LIJ`7ofG^) z#?$`GHE92XnG|j+>pG0twsU6m%#52^$_*Qt%Vn)|de5AC8 zBEJ$YWXfk_{yey$sf_lklVcA~;11_ba2ya$fm?MPsBr&(f&+CqWZuTEmSt;)01I9F zS2)W8w4;5j<+RTx(bz<8-pW?t-e(k>Z0XGwj$6!D%vj0R`mJLji$YoOoV9G3R}jVf zv#B=InQM;`*fSV-;@_LJuvS>p-t)+p>Yr|@Yqx5su4O}k@0?_SY5azy=rv z4oq5DDOCKzK}RkZoe)vSusSt1iHb?slG&Q@(4m~j&eDed_r zw{GV^d#KK0VS$_3@%_ixjZ4>9Vq5}CzI}%!-?_t*Zqs=h@$qzyf-1zt^Rp$coV&!+ zhc6H3`|8_ua>icxa^#D>h|z~wwz7Y1p=;wU`UL^cx2$vayhWLS0k8ljz$S1&?OJZv z$%UV~QAubC#?lp46c_pSNr4!uN@${Zt{>6{7}>>k{{5S1ov2Sd?LGjBc8=wxxubqyh40(g`caC7@xpy z--=`BkDsM;c{cGjLGna_a)1MrOXXj-Xlc_KbyT&*?;#7baDcJ_1IGb)zo|tQ4t7zy zrhN)&e{3bav1cRpQN&(`IqvtT+FwxYt%~&E6YMd1=h|)BmwqQ3O}GV))p20a%He`V zbK92R0S8(87(s_6-g{%))){&V+Peb#{zrxEx!2?oFd96)YIZ``NE0nrY7i}G`Ph_)ODw3lU0dtBxehxD9J zsLdcB;1ul#I0mj!9&m3%G$0;$YoTN73?5MJ|C--8$l7m@&a9ee+lJaa@%zxG7BZ9O zR&4dmwG6TiFoLFlJ13gwR8RFde2e`qafN($u!)ZBOU@m?~Vmk6@?v zpW=J6LbhR_TmkbxuBv@p#oo66kb%V8{2T?<-oQ%P@QluWs+{Iz@EnEW6$%wazBd$;){RaC695y zG0MR{xWGMVAaDTMfJPG9A2Xl&LmJ~`+Gaj&iz1Y7! zr@C=^6Kz3H=swsh7`hJH3GQ*_x)0ssJ<^G9u-~!ZgL`xa8)%RAkG|m!=?0h1oM)5F zz4%#47H#Y@?Fc?<18YGrL&I>`VDh645H5LM657)*7-gU=>W7(^^$dpdbT|&E?SGBT z%L!+ni$giUIm*R38=wVf0@{E^pcQbSqTN^HK+vAXa&vjRr~I+MEA|TqUC_s)9p?nE zh==}y{iGi!KVp}TU*c!u;7p@cKEZ6+)D`@SXPlXX_eh8I_zwAse4rzMFA}cb;(Y?- zLEP0FTvwd!K9`?ILVK5U9-uL@zgqoq`_;cJ98jL1y^{Y;*a8oK-Y@e$J{KJ1gqQq` z18zVA&;m38Z2%u=1)8DK0{*-rK#Xlkw=q}t_x7+)u-SB89N!0?`f1Q3dwrn4f1Go4Ss&luu53!NQ>^P{rgLytl_q{nwSYXbQoKjbU) z3&4;Ze@Dp!7nM8!K0seUe54?KNDjCGe{OaB(s+RbfHPUIU%bp92XGeSrPJqW-*I>P-ck;mDfNSQX`B#2{Xrz%N8}~&Z|Ga( z0eif0GYh%PpCfpG&-=X`WM6i=3|?Q&bt4t|~}@f&ZuXva85YTNRS{QMx$S9#u`@{CcOsq|5x^M;gX?;2Fy=2Gyx9S`gtMB za{ndm(MF-O2Y4>y?bN2d6Tjja@p%7lc>v=n%q75POl<&jNl!sj*f2oj+~_ZKMVgnN zf-x?&Un%A+UZtvdGn;&ZEJk~TZ4$a6+A8{VI#+|Oo3o0a6^V29IGt7Jh~a#xPX(Mi zh4lCiXH6m>V3yVYfdGI?| z?c(P{L(k97`+0eWY{%J2ICI&9&O#bc+Xy;3@8fXAGve`HoUxRb{6$((4#uUh4Uin* zwh_=2bQd(vfd_csVA|NFG|B$QbVjTKZU5lb!#FC5pM@jJz*X7b)48iVk8qtHv@xcB z0%v963_YA1#?KR0oiF$)n&1b(N)ADosvI3oepV~=TvUx`J)_^hjLNA zke84H0#`!60uDt$-}p#%o|A~fHMe-YLp^y~wZ2A_k=HBWLtlwG0-6`5agG(u73^nk z=v+=*@r-!9M>_TT)Nsh}vQ_wb789-;t;2r6^N9@|WDhAXsmhD`< zo40*2?hn|B<)xMGeM zIvd_2-M{Gr05kX&G61$Uk^wTi_AUz07Br?wqK|X{j1Le^^BQ%@_ac*I1+%2{f1lic z%4DSXL7oUafHz@x#JPBo!@SQYzQviq`!_3PzG&k)UAcT9+HaV@hM%>JbGX3+h|ej# zh~s?$<&SdEPDDQdH~0lCnPw*%@=(HHj79zNbXdL+k9{ z3OK@-Zsq5$T2p@ynnRZ1if5e1iuXdd&7MbIpFu0w&T#f5jiFU@WHv79G{?Od7m|(` zX5Yb;&ipizMy}t>=`Z?Ml#Ovd<V?`#10J0Xy1ZIGvj+`sh#59q(|a?DiF#m`w+= z&rm;3XHxQK#DDHvE<30nKzS$^Yz_PoCI8Q+SK{TsCHx$Xmor?yDJpIf2t(y&n=hyvk<{8`1zaT`RJ=PT}MG zFKzpvGx~Acj!v9*xZ?Rs=$|D6$ZmvkQ9j@RJfCbcq`QNUr;=?HeJuIp(YSI0{|2_V zf5a1Cpsv|z_qk`#2mMfPc0Ig@?HoA8dAcZfG@YA^E1nV0ZR;xD`&@pXg9osnU7S07 zj&Nlu2W?6I-!W|9#pg9Zd(rn>x3sQ8cCNQ3&8c#I7guL_dT68GME-o&m3T|E2kfI_7jmUZCH7(kGp|x+^f= zB!5lv6>{Wj3)Jkw60=tJ9bqrU{K;gCX`J>7xq|a@3gv^m(1u{IB-;St-kOiWcC7ex z+XY{!hbZdjA^+9eKb{e#c@l&z(1NA@{e$ z`On$oKc`cw;~TU>*?F<+f$qR5_!d`ndGj^-sl$`~9y~CC=F$uso6)=w`DW|db`pJn z@EfIm(H#8)#tN8!#g9$^rg5awHxF$Hc!CWaZ4Po`)Bq1@{zq~TZkPS}fu>=4&TDi3K;KHva6+%`|}0nY68$FtyH*bHn)=TPch zmfThj84!2zmRh_8JGt-VXU7#HU*MP9^#6l`U4iupM+QRvAfqsVt$e9-%~87=G^!5@%;|A zYxQQf$$uT4FTFl1KzGB~5BeY4F0Ob+ym~r(hwq~|?qIMN2-@Sy#{r}RpnQx201x2$ zqz*tl06T^l6T?>m@^GTbR9-*W05&e%#C<44yx{$u_({Z}-J;wtvFm*fKIED2`4Qb9 z_uy+ob0SI|kZ6te588jAzwLO34sKlc-?%tDi~g{C3;6`xg)iO0^(WvR^R@?w4(hre z?6zq0p$pb>yFKg(#A_4urN`VMw~x^?*uCH-actUeX_U|wnCB5EV#y92z>a-GHJXg;DpjnOf0 z<2QT`AKzpC4!TwLAZU+k_IG&3cd!p2ALN%)87N;#e-jQD;QKWFKlB5)sUN`lisYM# zERFkVE9cL%$AkAj@XyC{076#hmt8Nv^8?(1ZX4!>a@me`hGH%Vw9jh)IyR&54rBk& zg<+ij(Erk3y!;^l3BOpY1lud{?&Ik5kM<8)4gC!Bw_^s6BfZm&+j?N{5y82e8*EX$ z{o{&f@lIUv9ll3Cd~B%J2Z;8M@;Utp9>Dea_OGNL@XUScNp7dA`ZwSzxA@%XFXGW= z^V6=EpZO`(TG#5zN)Cp0Hoi}v4H)&G-D=)mi<`hFFSq;g@WdyGL-Wm=>Mr7 z!TJN(efikmY;w+HKk$G`=Yvnj?zJDn9@5>m1%0gl;~DYl>F^zNJmm9_{$Di)oJ8dV z4!{Gr0N>}@zn~dtjkLImI9zj!S5KpUFYqhudKf=KAB9}Q70-FG>*afXs{ViJp|1Ohdz6dvq5lIOzy}I(X!KBJpWtzi@qP_qI~%J z0iI9Ue!c`h|2B=_JNRyVWY?o-e7yd_&O;D#*W<7Apg#TWo>Z|valG@thQ`P+Zc zzmv-TPuD!z|Ecdsc_)48fKE7Sz!*Nh#}&^)K78sb=#TQy_EA3I06hO!dY?MJ zKXngUK$d@ro$%lK{|Pt*&v}xK6EYpNwa~F10Qw7>M5GzyWv)I{ncf0LJ{X6ABvVc12m(OXR}Q^RTZeXrBs_mp0E# zJLK#GD)_-v*JicS4rDA{Pc|d`Aph}u1zV6x4*-t_d9Q+8;kI4K;n0P_WDkhor)X|nZr|CJ^y;*%egN}zBGh?ZXcgzc=I$FZ2NT zmRvr0nfn()A433*Lgoi6ZT6(EeQF+GwC|j*_znRwANg_GtIB{sBg$3!e0|WbbHhhL zi0AE*WTvnaeoOy9`0KvS$lyLs2JroDN4~!;txsUQr=sx(qO08rzMwv!FV;|#eky}+ z#gltaxD1fXmHmLwwb935Zt-~33Gi5!P7b{sYdbM-2VWg-3#56W&+!7MIZwBZe~0gp z5As6*f1wSRn`!t!gw(Sp63wy5` z*+06?k^wr_v9JfjA7sJk#UIH4J{~0b4>`m2TJoA3E84dD74divodD_a9mWR82l?@Q$(D|C;KQGt_FN_rKY%B{X`nJtR$gc# z(sDf@5B`7RyS!Xccla)0Z3Jxj(D@^XLLXRMM3 zNC&`A`aUxY2b>3}Ozc^Q`SXGGjQRW?pZg^pePn+R-i|jdUPrp2atsf-06hU$jOh`N z_h|1(kMSM8$NV7jL%zsAN7@q~LMFimg!hmQ-zL1`WeE9~7rKbF-~rqNzK`sL^o)4% zUZl^DFB( z52(fi@87=9T1x{Sz+Qj22G3f@T}!L^d7-_EC-@!^@DHFd!*R$ET5BP5@9jZrB42PG5Mx5l z11i3SZ25|yj^ul&^7F9fSI}7mrT>RA9_f%CK6>~b`5-^!3)%J& zK4?CI#}h8GC*FYChOl$+b>dhnp5Fm^qYRV<`k`#~wPYW`MmYIi&%ABWI*-Kbci6C= zqqwbCwfDnW_nz*>RPCADcyep=+2e&@xOpr4>XxnSQf%5e!GBOb-e@vQr!_(N2_8US z0AD#Fy8zEif(Nn~^Kv^@?qV~Y=kWE&XwO);gE@Q92Uk2J9_y8n4(ah7zDGWyt>lVN z+7}OVKn8V;xt;*uUhq-=26LV_%0O8uRA#R5d`H@&-D8cZ?~YyO=e1JX=d zTh|6%zog&KPJcX$egON{(_S)fNS{;;YA{qj-f%KYdHkI70Bi!B7ietolI(CoCW05x zUg2AX_6t8Ethu>DN`L@{0)GO(9Zs z#eIGSWu7Pl^J&5_GB>!sM?Cs_=z59QZ!-^CM?!17tqv+KYJt%oRc|iZz3r575_9-+*;7;8noP^)sR~u6RZ~ z-Xq;-+k3Y5#d(1IxWG5xBdGg~eGfWkF9ZE7@PxUY3r8;~_ke6fv~S}`Iv?5YT3QVS zZ3XSVgwCHzBk~!lW7UVo1R2=lzv{sVwJd;1k~fsfIM zFZh+@1k&MM_PT!Qv*0uE&bJQ#2F@?7PfqDjE_~S0Rv;&_4+`{T8Y9y_z>e4tJCl4n zjq;QBf-eLQ7&o(SXQ}IO5Bmv_eTeqE9;%q-IxlN%Ao>Ex0~yU3p=~SWfr`GL@TJbF z&YC!-4;OeEJ}zkUS5IHzYkElzD`>1Fg|27S%09(h$NJB2LHq1HVAjg)57IB9FuoxB z02@el2H0fR&JAIY?mo_v2e<(rVD1q80`vo+C!&Agfp{8xtqCd|p#LM>pl?S2UNHBL zxg4xf3ZnTOjFV}M#BmN9(779b30+V4atNB`_f_bG$d}GI8B2QMOZXI%93sCFW7vxo zn3ub9>MCD9_7OAC1I7&SDTQA#<`YxNXAgc6*q;M)OaE&DvQY%+Kd>!jkWQ3IIFe>& zavc3l`%A-r7q)oVV@bD@V@yu+{=Ioi`&nWQhdOQYo9^oQDaVG4k0-%*#E+ZF*2K^2 zL4SlbWHk1Y(_EKgjqf^kV&7@@koNTz`<-Ox3C;5d47`2wj`qHP#7^!%O?I#KY@E>~ z^zU-a;ZlFAfKA?p=JKe`?wQjmXx95^F+ z%G;5$Z4G0%g6`y>(@W6zYiv8>+x)olc?Gq-WK49d>QSFCna)AF2w!7y-W!b_nJM|| z!)FURBiZ!j26czZz#rc+$l*7FJz#Jp94mub9Kj-o{Q9neWIA*x*ltW{6EAEc_|Mb+5-H@r zgFPwv*=CSs-!ov`s$5@6?TB*# z@OFsC*VD-Us_)vs_2&;1%{trRLsS8}10SgO3+kYmb|C-4RyGaHT3EF))v@hj*2=bz zc}u%~-#VDIvN9mQ$KLRLMZZM8jq+hVhO;FT7jYlNk^MZdhDf<*bW59HtcAl~Sl9#k zTL$XColq9F4^!X(=SjidPWs%B>UhIFc9ey?Jk8zY8xZ-Q5$6fP%irxvdt0F_ ztJd~(W*y0W@-4t#^Sf4ubKi5UBc!=Vtc_wOI(Drfzpx(`uHP#NT!8L`_Wxh#o&Oy4 z6*;PKDlx_$SGu-uuooijC$GS|qexm;;?$MaMYQ1SN#2?^x2=XE#5Z42oaU2)hXLN6 zs7?P&w0}h76X<`6&9vvtF4}ttyg+?0=Aa+jwzDm%0$>+R~DuDtN>y_ z%!^@-Ge0L%*Y-X953!bz=>FceE!IfbE(Yi#Z2_!?2BfdVfN0a0BPTmL@9!<>Tqg=N zcdk57+*HRtr?s(PS)%5%0*C>w)6rhYWS6$vXx$qAQl$6k+U_yawdz^`;njeY&lnJM z<+Qe8q7AKaLm>L|{Sd{wd@fRxT>-*?7xp~GIIyMd^ep<*J^BZ{D?p%XK=}q35Huz~ zNpC@a+@mP*?i&=V$+Cc8K+sr2{{ljg?+k{9{@)ql1*|v?{RMG&Ucide(7%8YlH1scE1o_V3Xz2f)5njNG6Z9uvZ!a8VqPKpuvC!0~!oyFrdMJ1_K%lXfU9`fCd8^3}`T*!GHz>8VqPK zpuvC!0~!oyFrdMJ1_K%l|~gInvRdwR?5z zGGs#B^F!k&SX>KgqS(G?T-!g7`!_a;a7b7=uW}P>yPYL`{PoI|DY9;+-twP=f&zlt zd!+Z3o?g>_e2dcw75x`gFHx(M{>fThe_B^$=i}(4LgiY$=oe+U)$>e~VcQa`4NKkN zF~se3pxZ6W)bz)(C*n^^Q{+<;GoMPeDiyGwQ`_)?{k9D3_j<|2i){|*O%J{^b;r7O zqc+~VaN+Q%SHZzv_w41h&c9fqcdyYjpISwV>}?Xg=VqCAwd-v!Dk*+2X^`ywv0+s^ z)u`S@FJ!8zQ?cb!i#Yx6T+;Ra!ApxhdjT3b`3`sX=chIPF#O|bS7Z)a_=ttCa z>3XJv*6HHKt7~`DYtb`kSxV2i%n~kz*N!TB%r-Ek{)xKUmu&+(z22jBk$y$>x_0ea zSyHlONq0%hmMtR-c;x*4(FwIP>pP=1>wh$i^mN%%z42+!19cM5moMYr$YOuMle*)^ z9U)Adn^mNEZ0`k?BzkA7{4{)VM~g@AeyOfqMmJ5X)N5ni^Y+fmQd1_Je0BZ3)#_o# zqKzaEwBJ>dG&C4lW@w2f({zi5j{RkPu;Tgtx>b#RyH?h!QpkB#U|4tEAGAjVdp&b# zwI?y6uQa&vvHdy*iiE=6-a+#$j?|NmJ~yx9IO)NaF06s z(b&FL@BB;IzTPN%+O*V~I+WBp zTPL#7yGC+7qH3Gf24;qRBuBLBZ&>T-)uC9461__-NpiRq9I9ne^lF>i+Gi^0CYKmc zs??AAM>*#YfL-a)aGPO?Rn!|+iHa@($;n`TxxjxKv7+42o*Bxxrfd74lAV> z$|S|TTNG*-_e5)X)DOp6wrDFUq+hGn?0}kn>t{c$yBXPUdD2;;H?ZZPzS`*_0YUf7 z^rsv6_UurobhON;`OSf^C+YWm*sfHWAJ*4warg$cI=jdBJckUFr2TBtq{Ob#0mi*6 zY}C3E)uL`Ehq6ZI6N{`~)=RQ8y4RVjPL?|y{-pM~s%lELX=VIcbgx&kys%79SSY> zFHtgqABDt-^g74EG+q^YE03BAA_4Ij1HE!FC& z8{W&^u<^i%h@X!%xEdIE=FHNiqkk|urL(K<-M-BwA>rY3jjkK}$6YqvH~K;EU-W*D zxwb<~d+lFa+E)I#eKGI$4-;GJPh_!yl)HH)?0M zE?}}ly@nmn_9C#hY~4{e%%o1C4cfXjFV>H^K6L-wqD|MY9C@eJo2cTI|DKy1TDQcU z(cXV;?zXB_P+dJ+pG6&R^_oAn=(s|?O3Z0G@Jx4))3vp#YkS`>^K$*4w<`}3DQS{1BAt@e!hy`*z^i+6r!!aKCSu%cm+w!x+5l|NFe zP+xz&idwewS{RfnrQao>s@|F(s%)?Nwz~cJpt9zAzE`}GXP2tJw)^ z>blRs-QejB{oe16&RBGLahV>)cdjn=s=oG(UUT-wEjhob;V|Re5r&jeM-3%W3FPh%7 z#*7O~w8N`C?0Z38s?ha&?d!NEZHnI=*zwHPfJ3s_pnHu=2c=IbeAv6EewQOXt;^P1 z=jz?N_k^SA+B;{IdB1VdJ8vEKdRC_iqsK=%t*vo!sd;6|AJsOv6APT4>OKD4;jvA$ z57#IWb5yTR<-S!+Vv?SQEJ<^yaJWpbo=z!S`V6g9cg!!RkCZJ_s&u>V0WB&7bSbVi zU_yx+Pa0}ZSTQG~+6%M25xVU=>kcWYRndR$Leq#cPp949m-g_->Z2MjzTd9b&YO;b z$$frl(ni}bqE8tEfBBT7zcp#5RjbXb>g@;Y(~%imX|te|c6wr?pv5gRO4u83vh3Zn zM6V)R#n<^%9P;}{AtX0wWk2gVX>8;Nn9=pHn-KMoq zHEy;0$I!BAK0EqPtl=_p|B}A4a~*0w5Iy|esvv0o40JYH&~ zyTo(k;up)=wSE2-y#A~;Jo4v;Pk;JVzh~8i#(xj=>|K4P}e|O|QIhc{5gV(9~-!+>YI=8WPYcqKrxJCnZ7_rJ4WQ z|ID=CcP%nrKCAiQ^RWSKuQZ!_qR6~Toh$trdYnX0;PCms`7RlK(cG)Z*aN><-*~wC z-57bfl=!IfRT@7ZS*85AmW8gg(1{4TSFz{FUbS?CdTDDld1p3g(bHnrE#`mVN#7PKoNHcPEDQId@}lIrB%w&+mWvB&~e%<07>qZ#3JrXY&v7t%e_qdTq8d z=3&=A5-f_nkP>z)Y`oO5n}M(Q&~qU%Ns9(0JUZ~=ux#ADGlfYLIXQZCgUsTmHg|Y^ zxKnE2g-Y{eTe{4j8Iu+}Gh=rjW1j}c#}EFc!-XEBT$($YH~a0UCT6oe%KzQHR=F-W zGRM7pyT8u({ecHU7A18m_UMmQe&?h6*V9_0Pu;++TP&dVFTM^(BKzJfeXU2JbDy>2 zUY{KLHnWnOBB|ydZ6_D;bvY8Tt!hcF;$g=d-7PVq+BEZ*TdpmXWe%BGxb8Jc2SdZ- z+pYHbCpLfm)BBK}Nj=JDG|{g&`9-?+lHhw!+t)7^^UUU4z^-@a#xAo-eo%gGSY&jM znRR>AfA&+8q+ea0Rx26B^!_rZ!%U^A?vESpZ^-32a?ZLUjkNU+|{L;$y zcI7QE&wJ>$*=g7L=d<1p@f|A_>RsP1AU0jTtc}Ov*B6Toc+u%rckfjx-4eH@A8vP} z&O`Y|oAVx%jl*W$JUz5e#hEL|CGV?xVnDTpeMUt8;n&$~;KRsYe{Y@fi)X_)&+Wx7 zyc{z6qVZDiwXZuqeseiuYjcBsg;rPHU{i7DhE{Qv%yzwbeQm;{_mxr-cm3+9ceG4Z zQ=7q+7Iz!!dwge!iHH%8p(^rnX-veLD>4vOik=Do6f_vdh{31?d`4`F}SZ=4o= zvc!{==Ai-l#i!X%Dzu^Q{8-)blS}w@K3rs@gJb`f8|rtIOxXVGjZw)V79$sSEmCfU zv-^!Ur%xPdwzRtQ2$z$yMr@58HXvqR*8{)SSW*7(Q=Wmf(<<6^PpZB3-Rl*zmW{u* z`pRXAk5y9Cu0A!ppI#nZ>6}c*Dsz$kEA7+EM!g;Uy#CPUKP|KHYP&t=UY~mPnmk@| z_owRAKtl%qGWmjW;LwJLPLyh13_e6VX&tq@*b$X)f>vmqRxYm^xHr*S! znH~P2sVuYNto+;L<&|hXrR& z*y%sR&EU-BJq_P)^BA#wxY>zneI3t_o6@?+`}!MtMvk+-`tH)ZH%wZ&ks`tWT;cTr zZ^C7p?JK!At}^m+|3}3)8BHy@s?NO=K1Vz{N{(dwpwnzzT$_~43B^`aUSQwghuA#< zKgE_j(Y@Kxm=3#B65dvmURg3JkgAt*Yj=;IO$wJX-E8*I%4_@IDaI8Xew%I|yf~=s zrXr``)G9Y+c!?N!HLv+|d)c*kF?Gb;)O}Us%*ynvJ*Z+$8Zfr|waPx1@(+INZujO- zk5Tv2N~f?fzxItkH_zp_LT6^)pLem)na5*2y8k`lZP0kDo4OafdzXxSmALEi#$CQG zEp4v0IeBkQ@HW|?-XZMXwYyBu=plF4YgB`k`+HglY=jhJ9ZWBDJTt5_5 zY4d{n6{|l^uHX96-HB;4Dh%Dw{by-V(c{^VDvu&li2RwN%fMOM=dof6=|p zUk-1#O*%MG-@l1l_qw+wi^9VjWL`b9>8~p3Bi>K>Yv|K`+3||-%@4z&8oEP(UR3ScRREmcjLuh>pB)IJkz*i zp<1VO?3ev~?UBz|vvr=)HCy$K*S~dh^t>&rymwtWVUvgWk5<%e>Gxah3T@ZzEp@%_;h3b2UUMT(v0l>c z1Ew$OJny~5$>UZs>G++G!+(lxYn+gNsGc5}Cvwt|ZxCf;}3&5Y}w z5I)WwFaMbM+Io4>3R4QgW^9oyNrY`OBbXC_{0{%rle z>p*`zf4qM)#XrlZH zGkUS@qDT3T%l5Usd+gQA7wIGJF0R%o?8Lmn7C-j!DXn80axi1z)ZK3`r3O1Ml(b(Q zlIYho=2Ys?k?EoHDja?|{p|Ld2cLBNaj*Q|>Y<*`Yxgjm8r#-%Wqk7fI|G+mjnq0e zWJdYFOZG5GKX4;0<3iPw@fWrqjoEe}cAsnX&h=3>hMgLenG*Z|NVv+dsGhgIba!`m zhti!tLRvt&73uEo?rs4IrMqD%N$D1r6sZM)rTHJ<_XF3ma2Dq5&OCEJcMJ`gm$~PC zu*E#@wW+Lg2p=-bf%$Bf$tv_Pdi8q7uY5h7l&=dBtcV$o%8a40ulprq$lZAH>E%sF zQY~xs(4z=J5UM#TW_;ZT_yxfw?*-(N=pImHz8kOWAtQhbX?mOaqB|6|S}~k>z2?5n z3A`15JjW<5P?iiSRGz7dkz}`!r13sDA<_A}l9H;JViFg)0S~;>Em=SN=&50i|vG-Id7 z`Kr6V_59T>aaG;$I~*H@6+D1_+Qs&$xeHs^5md5v}~mkItUhMq{kiqgSQ?#G(X19j74<_fyaxNvmZ zDi-9neHZa-7?UItZL{0a>+9=-vv3hNFB`<09{P(7!RLORWUn6liSCt2BT^C_l&L-~ zD_6NY@WAcJ&;;{P-e}Wj(Ttt17y8`3Y(vtQ@-1>OD;;k>G?N$)z{GK7%@sL^(XUd> zjAINes4XM!{}2k18tlAxokf9(Y_`N(e-=yoPW4f0KF-<*rKbghjR|vMijlkfC4b^W zH&i0&7{hKSKKPdCUn#Nnr{6QYW}$E(MMh;3?t$U1=WV?6=P`t5Wn~{2cb}k*jJP<_ zHV@tp<%n ziVr7`hNx3Y=QlHsiO?{8%zT;OscUP};wM|KjBQ+Q2gA&E`o?vU8Ouu|WL4H|QTZSH zJaH~+>*|;}-d_hstp2ux;(QD7pnG+WSYxWYur&^rMFrk@XU={(wqTwENjANNWLW*VULvj z<*2ZNFZn&+_GxS|Bbi26xp@ZfI^nC^uql6ef>7GdWxz$@xCKX~VNjMln(!kZpHn3q zi&>U#m$UY|Xrk67iQC3N5kkq?o0)=X8*5ayzKq;aMxxtl#4bFo=!laJD5bZwY7Yzi z1nYb(A@g)m*yop14xc|`$?UTqBH@93nhQ*xm3179;-cdSpf5rk+5%{I;LCSaGN$_4 zK&>=*+6*nmb^MNZpmo=*a+OnS+DBYh=C@A^(c zapO_}{4})riIq$Y9cX%B0jM(qxkr0}vAOc}aI99jvrelz(S;}NJJ1<~6zzeAsRGv- zO&8G*Vw#;U$WD)6oy*L9=n8Z{%<>j!p9jO)k~Yv2|M8+3$Xd!LYIgoC2BNL!rLkfz z4*!Ho~;XJPgV3+RhpfcUHbJ9 z^&EXU5NBq7vAh*?bQJ1dz*e!DXsicCt!a+D-Ldy8LfxKM z6udTiI@#jQb8@_zH)QLgUd@|HT(Xp18UTjf?YE|`PIE|RZ*p(4ORu82RhlGX>z_uj za=d>85W!%y`+Y}WzIXO2UA?12q;Ew11EU_|d3pIN+)<0&SjR1;*RnHZU1iKq(#&HP zF*I-KvNEZQhpcel-O&)Isaw*}LQ1qS25#XJTw1F27ch7?G7z^4AhQUK<$#3azjo%U zF&5A;#EQ`$dM^)w(Jrr2uR{NDSA^y&9);~Nyns4sW%c?X7eUdU#Yxu(hm8iJ^dmI| z2-HH(GAa~TkBF7b1b*eR;XXXsT;W?onQTPL=G*U0j4T@~D~{_wSAK;^l7+0Vf6(PI zv2PJY!qHZljHvM>P+=?3B`2^|8>V{Q7V7rivSGRbc6g#*eVkz1HR2ECMlRb@&zw8z zC)5}IN%2EzmYf~7WTP{Am~_G&V;X+*iezMEFOG`5Zn8*G$<(oj;fcy^URBl%Zi&*L ztuWf09|(`Qi^6?^#0A!>`0kvVKav0IFL_=s?N-}8ae(4gOPp*i#l|Im#8Q`fhG5h%_Q=Oq@UZ2!FX}eZd;IoO}@$?Ue zo;|`J%z8d1jozAYQ6)??- z?ltn>>wFBxvCWmEr&BG#F6tQ)uIsq@Q@6zV$5kV5s^EBTxD{j#OhE|?9CLxU2umg3 z#qxq|aNn2u^vHl;&W|x>M?AOt=oUs-iGUt?F6;M_UtU|JQt&l}gWqR~EA+ozZ2d`zq*gUa;t^?|EU<@p)f0+Dv5WRfT(==kMIls;t}gmiA1I7melcORknsMl z>z8M=wHm8*^ttWZX7lb;ZF;+uIw2S@!bVc*c<57UJ3KUKg2L;2OPHYx;Ie3GR%tZx zSvde|J8WOGk>9S^^?=~%Su5mG`MjLf`? z!AxdB_%QdW&H%hhuDn7vrpTQF24@56<(>!0_Y%)F6T3xpe_!MlUs0?u^M9Ep>ngDe zr7D3|TwNhcs?e?OSK{s7N9TV}c0`?$7Sz}jY8WEbo$K|#pUMq?PB)I6CJA&kbgE1a z@!d>E79c)6ds|OW+XQg+RNgewHZ~RZlFZlGidEM#5q{i+$JXfSNAE^9$}_0Hwo8c*|dlA?IB$wRxmfT*ijvKgGar26wUSfk_3 zSVL1^J|Vu>;wVPHAM%kJRdj4KGehCx!Qfy`@mGAd)2%LwN*j-^i46Ko<=wP;<2bSG z?aa=X9@Rr|+`7-6cRd|>vd!CaQ{FreLx-~aYVaGon}-_Ox&;zjj-Opl+bw3`$YXPGE7-7sUN~e%57h} z#-24jJz0)-U{b;8@7Ce0IFFEgKap73y&L^D%X!L}B23kDutOcV0tTKAujZyhDz?qeWSL9T!Fo3PA2>S<8StF?=to1W(g}wT1AdR+4!1?DD zTJm@qx70NhWwL)4im&KIeZa239wf~Qa3zyZK|H;N51^Of?skVv6Upe)p0&eOnSM|Y z54+9g1aqcnUzwBR744N-S|S~q@SB;)b``(;NXnoOqczzgN%|-rCRFT;&@H*1Dgb+j z8hX*eO05?sQfyNG@=BYKXq&u-?ZcnM3e!Oi1%4Q@t5pGQ9t!>WxOOv#-E%)h5i`n= zQt-Kgs(jsd1-~c-zS5EQJ?!HgnKt+|Pm72B!L z8AgQdk}}ih&S4L^VKMvn7nU-ze<_NWC>mh7lCd{#iijiF@_MwiSnU~|g#U~OvDX%o zUCf*x0a{xQ(sciSn; zi=;cqB?g2xR+9*#9q-&oZOMK9uq0b;oqS#ttUCJl2wl{m*1wQFd|RBE$Q4Z$k1Y|! z439d<_jjmsA|2Z^Ddc_=1*vyu21#g|IpkYiU1gvKeBB3?P;xqv?)A4^;7em{ctth^ z_S~TsppOoEh@cjE5Fv5Ve%C%+%wC(b+8@aCcJbR}r-ef5X{pEN$AtVhhBiM@I!5d9 zMTu_%uGyAfAm686r5Zei4ufCl6ADF;icYYr=FpTP$ENcV5R70YLEcnTF}W5LNy7~P z8Z6a*H@1nf#IWpTc`}5f7gbVZ@Zx>u-Od55E76!`o#5_x`-w$Soe}|5wb6(Zp+y=s z$HiG7+K(SKxPyu|G@erS!F_>y^c_XCdu75%QgRzdO&}cz%_*1JJ;3)(2$n%>oGe}= z)dB+TKIhNDb+#tX%hu8MsYi2@S(c6ELz)P! zhfALI;(KBevE`*OO>_=>psdhlSdMkA!|A^`gfe7U02gf`N6UI&125DK`??T{0P7=@ z5zQU_#^mlgv)ITE$;(W5gV|cw?KCF2c(#hN(B_s=tc?y7j)H0{zRy35%hQkPDxNxj z44~SY3bz{xQ~y4D>TEFV1NV*}Yw zM;i{QqnhcbKVtm}20_Np)N;)zBi?~(c>+jtK7^aI5patnlI0l~&PIM4yz{D+@wY#p z z4`M&zK!~Gk95zReLB!!oO!wmbG($*Qxn^~{{hx@$I@ue+ee;El6L)Is99E8ELFADG z2EPIlVIAR1pig(bN6pY6U*H=2k8Px~eFglVNEh2jTtddB)S>y$JR?F`+mU7SmW&_7 zfObupTNKTtBP8D~<>Q&O)+fHc#wTvh-dk;e#g&ufVd#(+luB~VBxsUQgGY1XMTnTd zaQAp*{ZG27%r=r;MYW05nr_-!gAH7sn=bG@%cQxkZb2fJslwi9v)NPV)?p0^m{3jC zizQW61tHCr(>!$lzIitC^0bm(Z?+-1b>5C{)EQ&@;3C1oTcSu?s0`^*B{qNROmBFo z@NT?+Ru^u8;dUI^)#x0lae)&sY%L&}@mjjt5gzoO9~f=GrCp5+V|!atJ-Irq`Vu_W zcp$_s^``D<{~Gx5KaK4Qx1suOgW-2lF<;SlbHx2sXeV1Hk1xX9=d8}QTetw_fCym5 zcaiLOqHzHPo=s@sGonoNSUgDPa!}(-YRZRy&k*zs*EiqyVTXuGbC>8x$(xpYs==Nvy+m)I|qmssLA&V_c5biT;`MPpU=JsdDVy@ zgT>cZkN1`Yzt1j(sz`V4(548~jQt`uY&aC!MaGUJoyh*A#LEVTSf9AI0U6_!2Mi^%;zbWgsv~UK%R;&6*S_ zPssq=YGv?)7bIP#?pSsne}!}XT=;RQ*=OHnpXl)D=P5@WC_$JvE!*zICz)CrkjDpA zr190tBG&nh3R^+c>) z_!PlXk*MX=cmXpj9V6z(aMAZ}fbp6WVN;%Q!D zA6QHcyYo6iI!WII5gLzSv5Q>i+~@r4zLMzQD8BLBv1>TLxD%sUdjAb#1D^9~pd$^D zRVK7lrDuU#CHmu&j7+PBuWY6!s7^r*69y^9G!;}3F)aPAClnz9^yb09Ij#f}?E!u+ z%1yoK+rzeRsVeY4kWPItk(d*ZwJ^bEH=k?pq zphniXNu`i_G+9tA0G6EqL;M2~X<0k6}dEZl$l!rVwPnB*ZHAw-S5O_tWo zhvSK_&tI7O<^Z9*Z>$^7dY^=(>epww8D=yT0W9E&2ZQvbMAG2{gRl_>$iiUNtDLCc z^u_)3-kJpG->;BXDUG==-Zc{*r64W<0MV>q#w#5&Cx1&q&eN&?`LN zvza6vdYMkb6#1md<;!0WAbCyrsoAQBqwDf;?vM@pH4cvbjs6Xurj5eNkns_}xmg z;V#kgwj~6K@-Vl{hPQ?-nPrVA>AGl$P1K%zOh|FOPuWPlZxB~qM($RqE6Z7dL^eE+nwAI8b^`Zj-Xwhh9mD5N5(ng!5~njtTX z{Kf-KXVl6*jMBIlpJ-y{tT>=f`~N8`)1%QL;{F*<8a{QR1Hq8wd93DZ#3A7S#Mp^b z3cs@0V7}+X$L6G5QNDOtFLYUOlH!MI+gSeR6f-12LnGeJZbtxZG*{r%)qOWm^~1_N zM(r^VrNI(!1bNu^s2Qz`3rrPnx8$Npg5BR^;k&l?oeaAlqpk-R1btDx*m=(0){!r2 zYMxpFY9-K}jb(H$@B`!BeNj#^i!&%mu*E+~Q^s%bKH@UzTCA$BzD&+TaQ18F4Qol(oSM&mDtMX z=!Y8-?eRStrmR1bsC2k%btGC61hV7r&foA0$9_9){WGSbd;@zBNCjWC?2u-}{m9`% zKX*OYo>beXD)uxdu9c~S`|X7?-DrzID}ySn+yP4GLl>hyzIC>(O!iB;a1hj@GLU{Tz)34ZPsA%W#%K{6DdSZp8@nqHQR&PjzCWB> zJk+0nb$VaR{kaq7g92Y0lFmhn*5jG?L(r`HrC3no#m986&7Q|kCtRZ8kIz3KT4RTg zGmUBfm8##{^A-X$OU{l{_$gz4mS!fHe7%$fwp6=VwApZ*oAA>RjL7Y-BK_0P9Zn3C zdgO4hbJaEtZ8z7!^i{XxWMi@f*2n!NcSOD4m@BMWYEs873s&MA`Nf;L;w@c#D78$* zaO&@Ah?Ed9@Kwi>qtexi=SdA^hyJV75)JZT5wfJ&5`;&TrR^4ar@)D;*u;4UTCV?s z2WaJ!KKm#*=6@e)AYi@Em+`?JC>N5i@1@PSlO7-7_MjA5!Ml_};474@e*-c*4AI4u zksYViU}^SbB>R0khb8UK{s-YV>bC;K#puYofcC;KyQGcWRt*eZGoGdM?z1SY$Jyi< zbiB_)BBVl}C)f_rS%63MN;BcFX2!1Ccx+V{)TH63cAu0^ORU^>^7U^WH^TL)1MwQe zgy}C)ckf+uDnr|gjVhS8!xn1$S8zC^&DKCJcbQni(c7B+{imscWPS%Sp=>`7lBC;{ z2hj{Ll8}_GRUKB*ewWX8l1avzs)m$W6UY+HDI_6je(PlSI^QPAC;%s{8n{f4d~~*t zOjm>bz>e5NIC+73VV|TloiZN=2E~@0{)rc&hZ7@~d4b9Gof~!50%(wK7%DBlBJAtsh2e?CM{@dVDH1|98d?~8|k*BDK z=g4mUtriO06Az3|f+{4bN;req%qMJL_pp!2? zqw4sJE17bLc9br!=crm18yq?}phBPs7$!FpjrqGN?C}L$pCkX1o9QnE1CkI&I~RYfPQc*UepRtt5VS>hi#&-;0D%o3gx=(7eb z;a)o?a@kgDYTdhQ+Ol6J8-hW)O}B%^aeCdq`9D8!LLM#BZiGz048~dIt@v1Snr`;Sq$UvYcnqn7qkGHNacY%+JQ-BZQ-Pn%H9oF$927cP~ zjryQOs&Q*pOD9X=SBdWk4D#o+5z=MupQHETZys^H9fW|LaW|@>ub%&- z`{_~DI4M|?F(`adSCAPjaRhpgL?iEwIw9b(qh0mu`QiD7ESc6LUR0y8C^|G?d+awF z5nwPXbP$q}lP{~FqMCMOr8LAmzPFR)DaoL%3{3(nrF{H!;lTPM?}%EW6uvKbcfX2` zXoEtdKknf<8jmN&&e(7JFQ0=v9(VU8yUVsdlTm30#iQG^`GOUJ$_}Hp@I~5vj?1}x zfD0ye|KED!1Phi_uruo~8XLlQ>oN<2ic5+`WU9;&wXH>BO`bh=2%61mpf@}6r_cx< zG*qnBNHhV`KQNm2u^eBT3DCz}YCDgt3_jsajAj4hWD7qhu!UxO%>^{TjeJoV*b)ea z=XN0cB)JNjb+u5`oln(9AN$Yh^xBp5{RhG+juZk;!?o4FKi>5`24De;euf(P>(-#2 zSvT^*o)X<$9thQ3=`rZ}hrz#9G=lNhWoadO2$D?_nJ#)y85HgtJZ1z3HRj<3(OqIQ zf)3rW(K``9_McU3>gfjDII}_Kcd2LE^a}hD11c`bi!?GypZ$*_w0`#asZH$q5L$iJ z(CmRT;g)J~?-z}n!GCXl{wUny9^9T*ee^&o76@Nv(sVv%bc+y@O>>WcfA(MNGckTd zO=6Q(WkEQTnQF&P7JhZiHeln)vY@g_Gr|dwra7XWuJ;hQb4zL%YA&~1 z=Mdrp8b_Foch*|D<_&B#FnVrzH7pEI0WSYISL|d46TvZY`s1dz!L);&1jb|=uf=FN z!7qHy2x&o{s6^+$7(m+BM`rp8*4S+p3dX(%qo+9Ne+>?4#mwh3~R=fdC6m1lLd&-VKgSUH42kx)Wq*c1TjrpfaKx3LtV>_rw3u;w|B(Lu31J4ZvK)GJ z{z~<}|B8A&>3RsFZyXYt%`0mx#-XAt`62+%s`EH9clT0y)b_rz0SL!f_ z{;|}Q@>;mx57YZs6_err>E6G)9f1V(J5oquAtu@~Atnde-}fblK_YWp2fq~@(9yBH z^sQ4V7lthyl=smprrK+mo1G=m{=W7^#M@EMtg0(f;7h!9a8X7szmD)gjW{;Yl>sO($!WA3~j0(+~U3ng@%*_1U%Bh3EUV7pyTG^OMqn1n6%?WRY%`ipWHSshfn_bWA6uh z^!|zptsBIQNEgr0cA5R>N1cSAmIUVf#_--Txvo&veXKn-<{QV1T8<)iFzXf>$BB`F zsJGL{K;|qkZG!yi8L1iqDurT^q zQ#&@}Sv=5^SnvY%U{6F7V6hy%7l1G>AfzFrrrZq`2O>4|6rsqX(l@g9d8~ML+^x@E zE6XiypJ?bO4N}gs$Ad4ryL$%oho|}};S=zv-iVo;y#jT6BzvrrimY1vm zLD@jd2y2UO(GXtw-Ye8%hX} zV^KE9uE>J%982E2Maju#W(a`)&HAzxMeRGlhS^ytIWmf+u~6FCEvIBB4wy*Yz5QpM z0^FS=6(bT(l;;;WUrpvctK9FfLf`;bWh+fUMAj&AtM)r8>ooic%5z38d-0V8$&fGp zppYs+3+R8^Dcu6yrw?N0lKsYsqFjDD4o|?aG*m1me8b5#-FYWI)#+oJWP?p$bL0O< z^YPX%C$Mn(!j2(npK|DEkkvI+1%x$Z-_&5Q4g7BLqI&W_mnfFWg7_ngnUN!`kJ&l) zTY7>v2YZU84IP?|{5U^vzN(%Eivpl(*f@4ic=hh~!T3HC7g>N&`sI2OhkBi=Dbs{-f#MLceI4+6NkP!lsgx&DaIvbeI{GdqEaWZQr^Osz& zG|mh4nhtM>d<^=Dk_>ktODH=Kx4!y!?yZAavxoq7-t^}jAx@m)#|M(d&t&FuB$67l zHxGjPKY{u173oXgN+Ec*C6N+aYSaKb+U#whi7*CTv;mtYDx83&)y1;J>dj>mrjX3< z7mh+E4S3q#&ewl6#s0kuNW1G=A^9`>(jMowkKO)`FAYPzRK84B$9zN8i-<)UNaY=QPh;sIu2YQ7=2P z@4i!BC{rta4HLs&77$@F%8|qFAL=1Ef^tkz5MuWeJf~&g_xugOn(u_fT4!hhL#R+S zL!?D8IWL9-(bm?~tjbZ)*uy~!1~JEJC1ypG7^z19E=>ure9&fu^ z_+N@MZZzeEgsmF7g|_l6mRMZS8Sp)}{KiR<9g17~xayDk*{mvbpMLda`6X{>?fUZQNOr*RtIbxfUHwZgeX3-I4Wkh+UJ)-*G1G!suH}BhJV&(uDnjI` za;Bcr2fH7R_SFY=FO=$_N=uyJgmNVpxV?v*@Yw9xAw-$E+zR*%-|Ooyy!t({px9;? zi_N}0{nWQ#01`BIa{cH{#ob}a2e5A`*lp`6l4%}p$zMu3K)?d9lA9JqEfRa9z!rJ& zQ1$461DH-VjZNVJL#P26eH3jE35TVX*%|gX-xu|}y5a>%3T|aSH3GBOBWFt>t?^Hg zTPVhbpZ)uDejUBo9d%}~`^l#Wg&Q$~aK|_#KK+#;BIL-o!BH$4#SC4x|2ov>T)B5^GCdwiPk)gO0_QG2tGS&Ai8$Vv~ zWXpQLNc@#hpOo+64GK_wkIW={z7OqwKooweqhw-Pa03I7I&{laR`IEezKXTtJrTgr($q$rb=CM$P4JRm=~()`lq}!WEHPZ{ zcCck?s@ImIKmFZaRDUN?cEABYbyK%D0}&AsV*@17bG7x8*gRa~ zN;RkIe^sXbA+&iU;1=IZgX?=HTFrRtsIYA2NO?h?IKBR0ERE+oElu*Di_z#j&2ROP zM%B4ia64Z7g{%yf_ue3i=chpK_h3T1ef|APt<87s)=3vVSNW~UCUe;fuQ?(i zb*3P=M;yA5gS99IhX&u>^D*cw&Ua}+YK5jR!Jdv2tU%xKPpVSphISWA0A9Hg#q$Rq z4ok?-ndgrpn=wByY%Pa;bu1LPe3HDiF=g(YtAZCsmSJx#Hu92F-j!!++H7%@A`X%4 z^d+D4`wL{5`m^Dm{V>hkcBN$W*M3Qn&*bm%cx^WLJMSO5BJ(a#U39!HU0QhM`557J zyNyKXX_`yAV>hAT;r$x02dmci65a#8>?xii#pgb`e=&mGR@V2*K1Dd&f3-v_G0nP8 zL<_us4AFgg!pBe0E+_b}=Wd{x?EQC6jU1rU`3d{KyXHio1yIOwGR^C8(%OLl@oLnY zC6X`#c=9eS6z`SNk}8k@aZ!9y)=Tnjl>24+Yxs`MaZM?QuBL8sB zPS?d|44MElz#rh`7DVmC1De#+acnm9CEl4~eeDK?ZO{>HP3`>(-C0+9ZSCsH`{jSp z8^~@*`rM00cd-4iX{Lx4i~Ma+9z9lBJnZ%U;DXovl9Mnqv*ddxmp^~??6T-u)&P>e zX^epkHT7%qc1h=p@=Qucd%Ves6CX3}N!%9bk?7JZni%%(Jn1GlGh^~oD`eumd@p}`?(I<(D8dU?G z@pfN=F6by%J83FE9a?_^Us_L)rhPxF+GxW;k=zQu9VJ#)HfX~FzqL@{)Eo_4$VL>e zEPs>)sSIKg7uX_4WVobbliyMgw=Ly2-cNh3K3-x2^OrQMx{biD_+hY@Ni40(SeU^3 z+Z{3UR8EZb<6N%C2O}eh-xit{7_sWa=dQR|JTxmR>%4Z7(PtU?P6w9WDo?*j=W6~a z2`mcP1FG0ktyVNFr{B55S5~a-Y@-c$3Ju|=1VU%Fq^VK!J^lG!mplDW_yC<2cd6vH z{LBTmtlN3L{n<4Zq#w!d}#3tHA(y zLN4@l!}Sq+)J(%VK;MNX_~{J8=eb&cr_w}zZ*EgZNVOE{2r5A{#S8erw1nM+0fmm% zG$Yx^LK3iPYlc_fU(jj+Nq*fS{n`nD##0t--y0uPY@v3N6-|YQ1(H^l`b~6Ocgi88 zsgwf}O2Fa6r0dd39kLDbW{bPWf%9iM_QG>lo-kbaR3AbfDX93b;1@3Wt8nU@Zz~r3 z={tl`YY-i1M>fHk&S=U<$1%IQe)yHwn`z}%mgs$5ixvA3%~L)+W1#Sk?7iOe+6TRU zh^mvQJ&tx6Z12K8)Y!aMwihX9q;=E81c7u*VIPuf6@$XN55`bSGZqfv?mfoQ-WW~F7&_@*1}@l=m0-9R99#vjrg zi-87A(kvum=8%#Y%FZLOx5-^y9uDeJP41v>HP<)gabu0hkO6hE9m%m4BmwBXSYO0{ z&CKBcL~|L{omsR#8&POi>n6*xbVz@rO`3Lg9=uaUbMmGKSK~#!29tM_fNw z;G|sP!n@##W@+69&fU{1SBhqMx%%G}TLIC}s2JRw3>shRbSsd&5e$I0#!L?VtBs@W>xCG+`?kGq$(RY22rzeQPGWZu z)Fh!;b2Rv8C@~FI=iGUQgo{4aMsm2Q#!{Ia_s(iy8YK-TJ5J%CX4!w-Gd% zCE?TXuZWPkbf+7KG6N4r>jCmfK0jHcEE8|CnSOgdOfc&2Ha|O6?8!i?AJ!Kv!JJ`T zR4CB>eyIq+&zPOAG1QK=pzIE_!>RMV-jI{gbT)JOD}iVGocZNuXk|0kKFfvCV zu(0~$u6&LKz1JX!a^tPT2+u2ox+5un!LVk3fPX&dp$G-Yj23FLB!5Y95BPFEsip44 zv*kF)Sl?LH`E$>(7cOJKU7Qe`XShA+P9h;sbI#Ko(t%Hxp)GHz1vtteV4>xmv!m_d z7e{}>N7dn(0$vlj2&YFx|I=@oW0WZnNgM-9GA?5EO~{LalkI)(qHupLWydEH1%~)blGhiBj6@D4(bQ$d5*(*s0Q83zK0^P#J`;?n ze)P^p(D$iTNZ_K<7nm_gBD(TH2ai!*1A)J~0d&`~-Tbfk$vqmVTsvtDtIQb|J!B%4 znA;B!7;jI2a6!5I5RVz^(kGHwg`actq0<-6L!<4I!);hbbIXf0O@Fhh6*HQV69_f@ zc-Gv*Hudbv|0=i?gWV;hRR}S8hWABu_#Vst^6rHhj#Fesd+-NNjo?^R*MIM&O4& zGNb$uoES|UGJAq{v$AuYlPuk?RXur4DJ~2$KRrE4y`LJMQ@*WorbQ`J&$|jk`4#AK z5<5PKW+U-0uLW9u#flz|Kx+L}^|HKSzsb%6?Kft?#D)c`RPjKODn zWuTTLLY8?{9G_}6A&rI1^5t$n=VUHmTj}#>jviCnGqB^Ecl3#0gRYHEH@ZlG%`N|n zrfe9+6tDK?^B?eM8viRFlfFS|AD;G^{^Kat<|614{NTBprU;z(^Ntum-a$m_-op24 z=6JWX?2!b&>YTn!3c{QMKAtgXM2Pyp_ZlPoz3P{IJ)}{@dJMKZ zLTEC3xtl=i2)s7|@_+%-Thes8lhqbAX~pzDY9&rbaHl|%0))MH$05vu%Wq1)v{Gr0 zV={U81Pdx(|6_26tMCbvDU$%a#A`cYkdVhq4tUAbHo|~xx4&ueqpjpAuvp+`cWl_ZZth#J_-Nxa-n3s!UHhX zQzwY6!a2VU@-P#Q^Ue1|L?T;G0+TZ2FIbYSm4`qO84G-{i3QP(e zy$`c|Hs^vKEnb+|7>T-uFDZje;t;gb+{A`8f@ppnI$Saw87(gqqR^nGKR!j#w@KI? zP8Y*@+Ev8c&qlD_wPK0LQG}bA;claV$p*~m?s^~WzNhg@r8Io}=jy+^`(6n6@L@)_ zTlfNwPrq!J51cfpE&S0dtQ2>bgYS@;XN$t*{dd1%q6}Gknp#jxn)D;#)ZH=VQRLyp zDs*x5TTm2TNOnu~U3A$@X2lY5ydMu0M*JPp`$#0Mu8!57KEuTg`_+WCk%AwT6i*Oe z(%MMW|Mch&eWBTw5#*^9{f!(7OV&(ty^yQnK7WKt;1ul*sf)M(M=mIJ-fY4I7OgUB zzKcYpNpb{N{idR#5}x1Oti6gxX2~OwJL}Ge-r-OWr^+S94Ewzg%J#1{rmv-5yH+j( z&T*<@4+ff*r7GrR2R}v#zTttJbEb^}a@V+wpj2FCIM&@gEI47QzAxDzNc^wy4EX^L zyCRTc{Pnsgc6YG79lkueOr1f~=eQa*+1q|v?yd^l&sbdOXG9uanjYjgP2@}0uWvR_ zrq~J}!a$v@mIg;IgU=0mx+-s^E<~s$ja!MUZxJh}3XwVNy_@Q=clXyDZ8ww0XeJU; z{J7UY=-HV$naozq3Z3bjZb>edUR}@TC^TNAvLTxEB7IzU4-q zIQDPxWK;V9G$t%7kFBIr0EzvuU0uK9Gv;9Z{dCk=asl_mwjOrS``6~huq(1zUwKn} zi(67Nmtt!w~@!3{U5lpP7ss-7ymaAV;AmN%rAe` z;Om06EgS;=%8_P;0&c#+CN7^MA#4BUZuTI5C6pfXoUdZ3(d_yk+K@&u9ee`0iuz%n zPJi_fcnOB3_)7}kFr~`NV@|qs=*Y3YUezy)&$)E51?!94)rU)|7U*l;7w`;+#aL+k zW=275;KcB6J0BVql0p=~Di8=1S5x&xEj{vL9)3vuhzNon3OMh~(Yzo7<$&%z?!}+y z|L9{iA;pi%{zIL|jmg2qq?{tO;ajxuAaBl&1E=`h?Y|K?bP?msBo!G$uz-%F_)mX1 zTyLn%hRQ{BAQdJ)bt^|xk0l%uG4^n`;^k@ol~aAycV-HJB=D`yp<Wc2<~!(y~+Aw4(A?E)!Mj^ zF$v8*;TeNAL4bo}irlZT;LtuqRb)U#qEEV9C{dh#qPPf-2w@`F4$A^6H^nc5XjqXy zL2MTG>g7kx(s8O|n*@G3$bU1hD+njly><;|57qqeAeXV8{C;)PM-3HoH}0-U8D2xO z$z+ItUiRJ)fZiJBEn9~dZr;UjA(4%=s{>)wN;&m^eGl~3y>_RZQvPpQXAd@+h`Abi z>S4-uu*)=q6jsqji1njJCfx6N7MDGG*@Q=R`x%$OY*qSCUdqyJkvl-uMrTmW-VLke z9=_M-RQzv5`K}0O)>S}I2Z0L_aug}BC$8(a5_#lHrn==$QExBsR*j(2v8=TbDSvxC z1gpMY;rYccrwt9v+jTWD>5{dP9njBnw?gyb@9)nDdfQZSpVBq4os59=Iq};QBQ$OVux$g8`$c?!Q7COm;@o7lUL(ewln1|n0e`;tB~W{{ zY&`tWCU-$1Yqv(9?mqo#;{Ei)5+5N>>a98h9 zm79synhW5i3gGOq!~^geG8XZNIP+kmw`g`|nc~s2n#hHrvevi5zJ1$v6*&1xtX9Hq z4QMpI_jGUT9&nYg-y-de*(QILERD#Wia7CgzMt=XZa*p7bS-l%{E9Giao6fAq$U`1 zQ(}`x;=376*renv9f`ypH>qg;&rg9=Y!n7;{-Pg1l5O=VBP;DXz%+LmxVAfqo6s0|hCD;4+Tv~hirRmIZql3J;JC3<= z+lERMtHCDKUCN?XL%f_&6P$PJdf~$EU(4L)IMoV7(2z!uK&a(BZn=R&pZdQk7CXR< zk@rUURUv`f`iiu7e~UJ!PSNB>NimIVpgL#&QLdXrJD#+a+-|9VtaFaC$tAljD!`Ol z#Iq2Hf7q0w4-Kw05P+N$8PG*p`d^q&%DkyL?n4smR|*Xl%+We^(MNnnlo$`iVaXhA zNVA+Wd5hD`T@deHnW$iDAz6-StaQn5{d+S6Zu>rG!Bs_hfZ$UYe^gS|TZe5Z+uOcp zI(w92QWoLkOTCVbuMkTWk?)xtA+Pny6B(cc}QX>gqx$rPK z2j1;bJW1VXD<`|9P2*{>B3TIrE7HIil@20#4J)-6Ar<2K~o~hb)kjf!_zEIhe2icW|UP&^U3A zx%oCx^p00cB7t^JFr0daX!p&q?5B=~P)O)GiRPTFDYbzDd{>7fW(yct_!QO26SW99 zc>S(OF)0iw1vhd)i;C2EoRnxRuS4GIUAWO6R|sjC&)NC!O~Scuray{nCd+l7G(3Kx z0*(s+V%TJU#|=pvH^VGx$|`IERlwnBMlpo1(Aqc2FSFGRw{mL~nx>HH6&2an>GDHC zGrfeaA2N4Ah$Oi^DKvOxsUuu<&X77e$q_O&w5ZEL?z*_Huj{Y4 zK9A2YpWi=^&*S}hykGC<`Zx;z6#1aEl!&_kZU~gjr~_y!AI_VH0i=^mY2C95tGPQ; zK}Cw1Y+*KK*7PU^k#c&l0L(Oa{uDxDcS#G$JpqfxnOQ;PaH-3|@0NzS2(@K)^Tb#w ztJxg*X@t^d-oxP7Id0p!YHaG|!B|h1gQg_7`wmGXKh55|@G7#mzwR0&zU9h_*BrcaGU3nX75Ucw zD^J8W**urKQg6%33-88>er{A1-g$p<=!dgugfw5%?%zskPEFfiBm@fS?)h{l7{xf} zgqa5k*RxYgx0n#RfvG!l@~{Z31=oj^!L;-7zp@EGo-YStSo?H?vq(#oalxzRWboRe zN;4=nR#ZxA?bu|5Y%!n*Y8rFBy|<{rsVU!UxZ7r}FMJ|ttKI{(08Bk`2F{KAK@iRT zyA!+caTzA{2X&3#W!0(E{}f$3F(e4!W(?P&UncjDVnNL4NW3IhhIt(xvzN$u){p)G zh(v%VuE@%7p6@p_m)U-9_kI))1LYkHAE4E+67fz@tr2fO z;5ty$huHCr=uC(X7j;g)+gz!BGD0fAQd?IQE!8%NDEpE;i2RdKdAyRlYX|UzD0<;q z2EF=CUAX9|d*#dH{3(SeZz!JdGI{Or1k#|>_JS^XWA0#fBch-b5arUlkqWqLJHBV; zu)*KxMnjhS>?cy*Et4#}l0|r+LEQ56_X&6LVbSuRH_#?_(8kb~M^I3Pt~hE>bV7Pj zuFu-$N=R?ReAKT$H)r@iTUEUmPb=O_x367vHtSYOkM24Dcvr`;S5Vs%7gwY4aQl=1 zK;d0_r^&p<$h#m9{oD7&g+V!Lvm&z^q?Ni8duL%L_m2XLuit>0(Bep4wR0QmmsWj^xAhd}ItJVpE??e; zV>V2(kHoMeb!&`3;IS593lS6-m49@FrWQhJEDX`UkT_zQV^9VX!$cASyTVvY$v|qA zVGIBF@)1{8q=r*WUG}3uR9Zd`(!Ogl5YLavvXBp;!c9q0qAnk#!wxtUcf>I+sV{j! zO&ASM*)x)7)BV)Y4@PsS22Bcm3xr7~)iza+{kXF>^K)CWWS8~*cfDWps7(lAVeTfc zc-mII>QZ!Gk7+dCE>hO4rJ(}(#fCZ#2+xOkP64S*r2Cf~??HM_pkT?;?lE=veq8j) z(k7lNJxi06pbsSu5r%ipU9%qI%^@{siL*4ilU!;5K2x;g*NOC3!KmlIpsm=&Trksq zMW$|7$T3pC_>M93RUJ2&svel%Z~Zrazt;=!IL+UF7jQ|2J9G@v=!sxa2uVFG>Y?_fs_X=3&%#oX9Adk0%x5s}y? z^6@w-(H-;<4|~|8FjTCuj7|TO-A*2uT2W4EwRQ;2J?e%}Q(gI?Z;!3xYd(A?I`gG{ z^lKG~rqOqt6W68@t-}O#!?!5yeSbo1q4*H|p8uDYq55cTM$71j`I7oKOzM1rhxjyW zrRB4cMLv=@o+ua458JCv1Oz?c7l4MNzulVmQ7%lPGbs;dD40JRN8<+We|=LG@@vu! zf9_#N)-f{iSmgyy80!#}Ab+~)Mrf}_03jUCNc z9GorEPXq}7fDDk66w~m`I9m4b)R@f_KD!->HlGe*kS!*Uw9MrRLdIM2L5M(}Gtx_3 z?zK|-HE;W?yjITNDRCLgLY-&;&os!62$zV^G~HYsM*L&oo)qWAG*_Dc*eU84#;<%H zCz-ikB4a@@eL{U4JxixT+dcrr|NZ;_CV@yqib-O!+%uogS8aU$WoClF9OS}IQR$D2 zDA(+8x>h^@tuMig(&C3~+4iZp(L+%H3$<2c;LRGxOw=L83=ah&D=Z-CWU?}s`i-2~ zL7eP47gx4U+_F))*6bY4_=Esm3V;G~dCvy-@bQJC+N4FX3=jAbyg0=!QG80Rtv)n8 zNJ}OdN4Ua;5BmDA-@Ric*>P1BI#`M1w_YjmG z+Attjq=!>{r356GJM*4N-0E&E%dy0fIO@simnXkYykjsd=Bg*AU?;|fz;J{5JJK(s z&~XnHh!!EfK|ZxePB8_l=>duTJKYazOBf9e;e(u@!kR2S(W~1ArVus2!aR(n^RhDA z9-}AFziEFHzeSZVaO2p3f7aw3Rx3ooP6CbukM^^;*xX{BPR|MrI{w>=iLQl5zbSHx z!g=A282tFwW(JndoI`(t<+t$YJ|K{;UoJv+DHnsLM-fjiecAV^ z1N+@CLzxUSj>uqJrhq8cX0W8g_lzvQ5Fu6ex>XDR9XGEeNT3TH$W0lbf9nA@E(E_? zV*43~Pa+hK&6y#bnln0?YWdr}k4g6Z5x58d(3y}?tfp9Xp8{I&T)h5{PRaMSE4^M; zkg}TeE;ZIru=#JjT%qE0?kD<=;lD|4L){YM+LQ3GyaCYITqJ4=ySGUL?*MKal&e9~@#RJuA@KtJq#v5225NV-G_WY5XRL z)FgSH^$*CG9`)QghlQ!g@pM1l8ncVk1zHUDiufVPpj{x)`q)9U_FjSRWI(yKS4-p? z9jJsLi!^&{AVtF$(IcPWH>{}L!fci;qkq?vKPpfxZ-&k_0qVXS&5zXlJeD!h$7 z@2|ziVS(C?bI%jQJP;zf6OF)u6^0y6r^5z5RP{O!^$JV;ShQ*w`Law2D&L2Q2nM|T z_LwBY_Xuccz3_nt;-cyOZeVZTY+iYyHIE@V^pRZb2ZKDYuUwT-WRaTx{oWg=Qxcw> z#V=;V3lOhCmBbQylO7$p<~Kn9W#7P$k?b1;dLIKgt{QnKQhzocj~S>h8ja)_s#ivD zJ=NeiPkj%WlLp$-TsUx0DbzEQ02~KojyFl5#vv5C6zZEuX-jl_lvs51R`m5Cz9bqZU&lVoeW9h5|r zR0XRf7V+E2$mp5n)qxwT0kc}bASVM!w&v}dCT||4k(Bp6XA1>p0O6DJJ?|4|t51&jUVUU2_Ip2{+ zBK&~-kCr;W1DW7dP$Hax?cLkJ%V)~mJ@WO$_(zyt@XnC&8)(iW&j#yrB{sHj{?;#& z30|1H1zo49OkD33;T1xXg2;mx&TSsIqn zEVOzgOCqd*LMW4Rv}&nlLrF>VM2rELEe7&x2D0a}tmTKv8rfTVlJ@|7-o1L^oKh3w zfF(hpT(}jaGe-7V1l@_;Q8Hv1@SCEZ{_vO22M}HC|`YL-H7EZS#dlc5` za%K~iRbrrryMN-!g9IR5y&(pJD8Ept$VYNV>bnOFbbK-S2rlup-;zbU-$iCv?BZO* znxR&5#5qm-k!*&3_8v0hc!}h5PDA`+mhZv8=48_H&$ zYxeZY_rdsp2S8i>Yx=Ywk#^UNbs-Qq7~>kiLPJlDAZPVQRzY6UM4kfU8)iaE#`OJZ zk|eM2ix-A1f&!1IWag8w$PzJ2Q>MMAo;~>9B(Rhza>)C-%EF^(N9@R_pKxNIoG*zT z{B-}7nY9(goTQZr7D#qloqPO+ng4_n?2CmaM6pa7H50kC@_SD+G45#+aw~Z&Pg_pd z`#TO97bVx^GG(rPWm^GpQEWzUNwfF#>`dOv)9F|i?>lC+aMxQPLnKGOhc&Y7q3w#7 zWR_w%+p-VHd50R?QB;I9*N#fw0UMqqdqvL246~gFn`>)nxRs2P1GayG=+kXoE1fGK zDXEWm2gU31EEwLlc$1SiivL?l?r1jWd27BR$}m!oHWwEva4hw`X(-om z__HB(Z@@b;$_zV!M4~;BT%w?Ur2d?mAXUK}tnTi{@NkoF(^Ro10$-PPt!sR2m~R|b z-^DRL$-U(F%9o+N4spN>M|mVGgKATb01+XA5T3AZIcDDY(?RO>FX2l{*B(bcYxYS* z@#0(8S`W$G9gdOUQBLMiJ7!5MciPm34C@zLJ3cmBSzWPwhNlGM9Fhz+$`6+U{`Al! zU;k7^^jS;wVfV0|?=ZeU8bYrYI5Ov!k;JNRC1JJxXEzYq_|JHtKdri)JYJ3D$K*wf zMHx<;owunf%L){5-q|#N8BCKMh&FG|t-)T6h3U|Yv02#xxu3dk&Rvi`O_iTC{&}~9 z^{KISFu=u+8Qo6-$h{;*6VG^sS$jHV#({|HgN&MiBxmtow6e37+K!BwJGMrMIcj0)y9vYG2d8tW97ZsZXkyc2)Hj8!! z#~%O#d6xYugq|=TEY;~Pj92}Ux%wkhHT9fP?I)2eNFlF1Xztc)a_+WT2t|u@g`*f( z@s0D`q|FXL-{XbqQ7Or&rgwn3LP|msj^h&~Mx1!(8G?r~Iv$4|5zVSFc2GY`#2ke% zJ+-d^=~H{>0FO@q8BlUQkr|%*4GmH&9h*BHy8)Vz3FjS(Zyhcvi*8g!8H3{EOJcl0 z0gZ*QZoOw1J+fp;6C$t^-R_YyW8#M|8ZzOgTdt+&v?;kHc*}jobYkXuolIR2yt*4U zyBRbFzHmLdn9$wS!G_cRavIHiI5oL!Ngw&dNVcAcBiNjCao<5`=;a!j4j8SZJe~xN zzt~=7`KK?3GCHJ26YefWW;_v5mbEiw&!T3}!c`5xb&Rd@@hD49$8cp@^}J z?sDi3iVfD=x(pBSYTvnsr(uJSvnK^cQrIZzxz_ITfF1l#RYDW7^d3q*$h8jQaZ3u6 zWhaSA7$l9jB+0nKRdGDN6p^|WG0b;s^_p|7CX~J;RX^Q6(0k~Hxb;uzW-Xdo_Nf-? z!U5odbBM1=fQxDQElUXo>PQjZsCqG z)W+d=al=rGrVKU|xgV??tOhPDl7Cuj7Sv#WxX(~5d>U5QRCdGDb=q=k0oD>G;;0^j zFJFVvTw+RVbIN&69@nAp*m9SO8Cd1tSvs}|ow<6h?KTKfP?&|;psrtwbVN_dDC29F zNLJJh;{OM9D<=mJwIDi|-W$?mR5SBf53VL`bsnRxhS;38k>Heh(R)9?0Q?L3=zkIv zC8w7yA&?J$RCGPEwnJ4d7GFb5B7AAdD)C*{4`PUJvd07j+3f!rS(=2P(Q)Rst7^w} zjSQSfm>c{+pV3bOb)$CKYz;0cJeZc=XX(*(c%J$X79HJfLARc8IO_q9 zKq-?Wmmgu!L8x{+gf^y_DbeI*@3+^c`7+X)BDv%s<7MUjklWouRYweHgrU4|qzvM4 z?U;|jw#d=ex`ion9S7+L`|b{_0tdFcYW&=OYS98to8ey(7jM1^Xc10y$VTj=2f_{f zP0N!zuS2U2QB!u-VJEr!(}Tz57OkrPh!nRN73-c%FUJUmvYsv6Gi%>Pn7#~K^+t4F zl;ko$zic}Rl6+HkeNPCC5ON$oqHQ1q-@T4o$Qt?{of0 zCC=-?C!)mw&aGyut+EouPaN5TXP-wZHe&k#Q9(BI8;9Ud4~dGs=2V`w535bPo0!GX z6a~S<`_eKkQ4u~MAXFhk>%6d7?QJ%*5wH9pUw)&rMa-(vK2G30)=gUo8TbYkV%@Nq z0XBgo@5Cv!v$ZD~Hj@jwAnzN5s)4W@E!wzQYKlmR`tJfYSGQ5?)~L*5alz#`J5lcL-KL$K_T;Wg77TZJYEqN^bn?$kDg^ODPn?e8G zcKOyi(XwvXk5KdFYxr>AsNOB7d(0miGunN^cPd0&4Z1r3S8YCNLUUN zFP6JS!q=K7~Ldf>)|+UMO}6Xk7|XMx|rDV!7m9}jOH(iHn|{_?7} zy!&raFn2zp$xf=TfUC6Q>&6u0N-!# zS6c*L34tRylsQadM=HKGS=Y`i*Oyy4l+QcnGY&h<}RAz$TZpER`8GU*D(H9kSl&qS|4tQD~Jxf5U zJ*vQeKA_J|ONexMzVV?Wv|f@AXdDi@!LgV=I=uDq}&%N@l6fvI)6!;;*v{6KQM6Y7u* zYOJevaXN2970NsR2Q2=DjOL*ave{@&)C9`t(aoSo8BC?Fj=V<8ZOpp(BvgC8OdDYI zQTX*Qc>wJaaf1FH?irk;>nS=>`Fvau*Q2AWp?~6|#u1KukuRdV46m}wmT|`-kx-<^JzZ&j z#fyG;!Va$X%!<{tIiFQXuZX#U=TKBp)z=MKk3VdR76j*u4#MZ>bqIa1V5q)&-_w!m zox#(W9IC#1wj1y$w2;2&`aKzA;8{F6yiih1^?Ht*-(l|+f!uylof|~DtB)O|xj726 zon5$he*Sfw!2#a`HcMd3Uw7f|2EMVH9G5XVRhyF%DDy~O+e0zu{5*GsLiu;p^TUHD z*Lzf}DT8<~;af!h^p>m#E7fWH8P0(uk;bfJ_>AOlmJ7 zI?!b=PPR<({`2FXG-?`G<>X=nfCNwI(15&`^(v`;-tO+71F6JnRKFJl94&Vt-ulX`Qi}?rCIJ!LhTwPo9h5HlxpCA+--%vB+RgK4~aG zj1UJn1fW-IAdpXL3?oK$c06sUMrU#Vi*NT_GN|Kf4BOrbxEOaT_p`%xw_AV+NJ2R! z>@!#Hj7Pvuao**`O2U?I77%-^@(F~WQZmI1f-*1^ISx|UCw^@MK&{oC^+ zA8<`axHc)5kq<&IBE89(If#84j?`Efp_vFAm$A*T-!+mfqfZn?g~$ET$#_{64@dVN zT6r1NPjQ~*lz3RTJrtE`8IEsm{?*})s>SSw8srGtHsPgF8vGB#Qt2`UcI)N_{w!O>jOB<)|ZBACSMeR1Y5B{}iHir6rMSqm7gcpMkxO8hQ z4q2`ShsE9fmGWDzVA>vWOdjDDd6Cgfy|>Ia7*|t?J?iV!mg#B1zqj)g>x(?5^~m7-X_Jhd0IW^;$MFl&v*=&G z%-{3fosjwiKDk0${-B!m}I_#R18Q4sr_}alfAhASQiU?Qb)f3Cy63=9!M~@^VOhNTQMJ8lq5$sp0 zWblKAJK%nu6b-(ZHuhU*Hjb^=%OQ>HVPZE4@ZZ|}`NIvhs@3aRz*{qw+G==DSr%BUkb2m#|L|yy-d<{_EPN zjA_c5?`-@b$)Wr8I7k|Wd~q>|e#YIQp{MSH@O-Vp!a$pamYl0hD%QvQa)SZ0d8VNH z&F81KiNos^>Dc#fsd`@VtRnpOYaBMc)9L`h^LmJ(P!z|2628u9A8#UNg8tm)QvvY1L?Xz_csK*w*w@|Vq$5&e8wQ1qQj%DayZ$en){Gtu$=^iH z&VRIP3&sxgG_VSmLv#jho}`l(_-xn>G?s9VCP3Mz?MA*7Mnb z&fWL^%v~@gsr!3E)u`Uzu_?G1m+RcGEFEn7N+brHOva4s(o^2Eb{P*G8@3Ue7P=Vn zpi8l8`@p@NWv=GzE95zU`uV5ox$PjNBW=7Qsu8y`^u_HgTEpX)U)%wGfb{6&qc7+o z>Ik?6Pu+aZLA5YhJYWu;zV*71ghPZ)%DL?n-kadyM&Yy)UJl3fLD}4D=kJN7dT9mW zejl-6y73@|3Izqno<1rWH38=5oTrjf>Rp9{1&d%iTv4e%#fGBchnmBF_vyDLBUeu3 z^~4}}nY3IQeI2P$_k0j}4Tj=+L@GI*`sKZAe72>E$8E=S@o*(=Vx72dmqPuX3&E=Z zhK}%4>{Tn1C;yV;zMw=43p#;z_ z`$r7keJRsGoIm1zx=l2ss4BLQjcMd;=stYElaskHl=<=EFysv1(^1*L%HfJs09we~ zj8Myh(^_~Y@LAV=eFS~;f`V!e)A96n*XXS2XXuRFH1U0_@X>-skbb(m(P_i-nE}mx zs?eV|E5Y_z941r&FBIBbW4hqy3z7fh9_!4va5-Ct!%h5J*L}5Xp%6rcY)DVXqjEm9 z4c*dh`&17BX+z=#gaG1WmL`Gd)Tot<6`Izk#a;9>!b|_pTV}>1qu(YxrbVd=X$aLU zI6o*TN}pa}Y<<6xKAflVM8XrBfFy7)UyZ#wLb^I36re@I2#G4Rn!9+R5qy2k5}PTN z&){=sC_b$Y6bS^Em+@{tfks7nFeNqmy`MK>q90clRS~tsJ4@cW%Rn8mF9tW}x;;1F zxY9@W=R?dDI2|@xIqwAFAQH;dSgP1iMf}c_$)z%dt4K$YnVW|L77hwdjr%Sg@YS@x z_QYN=hW_-l>}Yx(rYh*2$b_r*FpV*8p{>mJ^dAzFav?m4>Wyb-OcFWqi$e8lr~^V4)lZ)if$eyPsw``CP*BV?JH&rN-oT znVq?Nj$4arwSlm7-MLvs*g76Uh$_-q8UPwx7h)t!W% zu9K}HG2|EE0h!Q91vSEC49rigA6q1gS@&B|eNXGH92Ho*^qdHg{Jnndq0I;LyI8~- z!vb5zl#0z(V?RYileCPRdLr5iq7z$4#t#{1M1_7+)_G0$^${;oJO$H(Z#k6cxaB3A z)Tl3aJnZ>W-pBOmSX)LPGTSmSBkOCon3<)GZ3ss%?GMWOu=u1O(Hjs$y9(%it)LGL zy8Tg{tz1=VKV}SCW>xm;>8pn3sKhS?#l9YSll<)D@zBiUYJR`W+wlF^sR2(gZH$oC zcgr4#^xfIR|M&wQw^i_oyPvR#PP#bh#lnjiw!x#wc5p~GITH?28YiSgQ8Wr4Yk*+& zLiwJ)0c5%m1ZeC5a)njU$OL74)w^Pt5Q&k*vboVb?PBaOBxHR?5fiU}%pmWp&He{e7?_t)ZuRS$90W(J`M67?%Txb3EIUC_4@Evb@gy0?0F)gB58jId_UNT zO+}%pEX)b-uI`4^eJ^XPr`&qJAGn!WpMUjd$iIdX@;2X$Y$>8FfD3!NmVaB^UkWS* zoutR^7XbdCK>c*5c%XnVV-1!E^YWOyLm1fFg@K*S#q4mQ{>9p z6pMrnVHeWI9IAl{TFYY84+jtS#)H9#Ti7L;whUyEez~R%6Mpac0W`F`1nZyaJ1Yc} z4NVO{#xF)c(4TL-vLIRwD|7{7M7O!IYu^(NrWy6j>C)zr1;h9qkJ@>j2jGU-86?py zX?0w*8zT%|!QVlBd>(p4w-NaVPwse~Tsfht_TnhKL+CxmUH1!gILnAR;spx!r-IQP zFNn)_&HPK`4)?m(7cIOWYYjc_g(-*LUpz#h;i&)6=;bo5G1|JuoS+S6mVvXmF`XGc zC75H(-OI5eu;w`(*W`ZBhYlvhHvhq$c2=OSe2I+c0Sg$#Wt9t~0^Q9oWdD$}1ra|* z?yrWRLB+-jol#Rh8t-kE2H$zi?A{TccEI3d@+c`zfw_lq&6}?h0;opYQH-w1rvlz= zuu4QEYNUG?u!dZSIAz#yW!7%r4evJ)!K@ChnZzUh5=ZXfTbM%3ue4O2!b^nubVvSq z%h%mcVfeQ>)7|{kT+KRV!2=kTzlF7ap~3F9KCNIrJH>m}we^8^d)ZtB8f^imUir4C zIC3W@RY(I~L52QXYI=lRBV8u?75?9UvGIxeyt)fDTRda?v3gOk$>%$y(onQ0G6{dD zUO5$e5?2M`XtkNtd4BmEvTU>r?*L2&GZ1}O#wnn~ z+HfOt#=I{{pol)X_G7P~^mSJ}u=dm){E5MAl6{KpKDNxQah_zOyQAXN;a{7wU>O;1 zo_z+^?EMp^6#N%e9wl+DFO&Cbq;D~i?kdl9AaicDm7Rvb+0ny1PY-&pUw-7&fT&u0(> z^hjR2o~Nl~Id`PQLj?v93sPdodz2>O`_IVc=QE=CWb8UOOX%EbH}0~PYH1xX=sI10 z%)gSITIR-QERZ2!s3a*Xvz-}|2kS7lj&b4LAKdQl=3ep&^~#D^r|J2)@Pqo1HJPX# zK-LtJpF~bJhBL4xSF?%?U(kE}&Hi8Gg`qtDq+EcGXwPAQL8%S;$Mg7ha_W3?7c3+? zv6|Ql88!LVm)(MK$fXvA8b%?RW0IU-Cvt8vN!EsYn0cDGN$(Q&8mWBwrrjk_fP4+Y z7v*6EiN0Rr<^^XDPjPwfn)M1)m4K~$=ihZlBzWvn0i`DFv+?wK35Ara0nZ8F=Fa}X z(OiYo2~Kj%*f*atc@xGI5wQ5^gL#_P+C4kVhl%l4ZOiFB;-y)POt02BJPNybIIv5vtI(7FFC#YP$`xkb$$IDi{I`>odPxSM* z=fD4EOh+#remc(+94*#CJ#rt5ZvW!?c=@EutE)}Ae-gq(%|ZO!U-~x>A`69wyZ!nn z0?3eAHQN5Y`S<7ZJx4(BvTl1ylf~^1{S=W)ndS!B!~J)DCRd!>*9sIlf8w1XgX@x2 zWS5{qD$YWlBUSV&9Y~h3yAjo^7NgvT|1l(g7*&v2RfTeEBxH`AA(2$XeMc&!gv*dz z(N(>_slv(IFy(ne%!NjKLnPf|;#>sYvjL`_4_1E2W6`(CBaTzDz8ke5_&&rno!#Tm z{_-LJMpk0}o{KZgop{=hOee0D>54&h0YNEST}3P0fcl=d;7pW;tQ~T#$a`^G2=c5m z9L||Mc=9vov#X|Oek^W_kp&mM4tenGE!^<99;Bp*tPRW@NFKv2f|CutuUW?&x1D#J zbd8soCJnpQ@u}XQz+!q!er{j?CcVe9xUv+QcD<-t>+d65^fPo4?jiWWL=-8D!##Fk zzB@bj@x;95u?VtDPP%?xdWAsktYiXEvfC;!Jbf#NBQ zXJHW(ty2_PsAo(%&x`^s~~n zUD`2fN@&vyb6-m}A1)-&q|}^s5DS;Ee-oZ1wG>kFDO%C;y1k{Yz|^M-10}>Y6wKc# zk#^=_HHHpyOa@o3&3`qHr;v(M)i*QWhqi~LQIHr6-I*-yEV;KAEO%s;Bj}X` z(jSgddK!JNMUBt9WtprF!ADQF*BTA3a?hF_Ey2@%#j%xdIbt6~Xq8Ge^R<+&p<~N5 z*eaPnYnC=p+VdqX`9S8xuNwoIJ(-Gl-!)09=udMvn&Hz}d)xmVrEZ zjSE|sbq=mjlu3lTeI4kLbu2F0EUdgH{%5%F4KZ>0)2Y_=xNDjR(x{eRil_v?gzmtw zd?|WhTa)F|O0%qyE}6b$Z%prXSriOQ5)tb=WCPpBkW@MMK25*76?7K~?t4;~50P5S z8aR;|qgXy1D}Hxh05hb{kY+x?g_y$#sARg(kqXJqPW-k{miavb11=$Jo?{gTQ!shN zt!8Bul>2dh(0O}Hv_9Q^ZwOZm3mGxPZ7YL*D4V6kXNV7cHkZBsJ>|d$}dCH~P#JQI08U|k-p2kdz*&GA> z`|{E6`dsDON`qv3JJwJ5;4P{YNvRd;d_|3R$A$qFUCz_k+K|TCW_$k*xzj+&8>3*>4 zg-QsZ?3EUk=aSlIxTIIQ>E$ci8!P8qn9h-gm z@90khuG1=pWL|Jk7^apWA9Lpnlg5%P162a z*VBT+piGK|g19eh770;$T7xqV1`uN3yd@-Et-umaLQdj{0@-uIMD2yIsi|9%?f~(pek2CC3M`T-l&Y+EZ*iap}j**kq?r<+U)~a zA0pNPS!XNLkqwsvM}^n264vJY?7uiv1al<$G~HU)4sRotR=kQud@1fACv{NB_BI{9 zLjnkVD%)NLZjag1pJo39@Gl6wd4ILk6UQx97}}O<+E@mp!&y?kh}atxDis~+~hJ|NHXIM_`rDQ9C?&L z18Qrp+%|HQSJDM1QLkqJD)8>-gTV)SC(8i{q&AZjIicK-MOFkLXxeD&8~%P{>$6O9 zGpK(^*Yy)oQ^uPARAbX?!p_o{&aW81?9ZD!Z?7$5rah z!d#4bi$sqfl6OH{Ypn&lfZc8hmLx#RB(9t57<%i)7Dfi2KYzYAsAFewBJJU*hK4b$ zLf)*Aa6bs9y66UGPcNK(7)5dzmh*y8lHV9B(Bw(ZqBjg$gb-&TysrT#y`KsWUGA<( zJv3HbX2!7k!a!%@c`y)wbiZ?LWf7a*uP*XJS*oQ;pk-0FafnMaqwk8A`?)_DU#`o5 z#1)7y<%0#fEP%n0VKwjOf1Jqv0c3lJtUF78IOJz7=>Jx!9I5+_X^N;%KQkNN(foMv zcalB)-n?;p|5o6x@m)_3oh4?zdw?0s+7p#_NBvGgp-%(j7cY8-Nu$Pq%)FIcdJLR- zNj$R2(od)aD3wIx7n!NLf^8BWjD;j6e{D_%@q-aqPE&NS0N-ou=Lx135qR4D4>V>W zEOp5C+yF5+KwNx3wz3(9ZTdK1)FM5JLY^Etkz-Qa`kRTevviu8Jz~dL+)k|`%}uyT zU;BZOr%%_f>iXN8uV!=FZM3$>sWDaA!leQb8YeSgV``%GQSv&d^|Z_Z;qQR4EEU31 zMJ45szJ)fe=*jI*g3^y&px1V5g<$rp6Lj_IRU+&%0k zC;q(ggCYOw|4TCkGH`O!*(7q|;6W6==oO-qua9*-+ zqE8D;(K|*%U)rN%0a71_$3hMLa^-MDZrX$_jU98 zflt-LeD29x;*mkyHu~e^cSi31D~tFlL(3#oDh@MfKn-LvdecHel2^XK@z(;Svin`= zfu02qD`ox-#Xg^&hkW3n#b0A58PVX4z=WDq`LoLT^}Cpdi2SbAvLIz>s)Rykr0OB8 zKy-+}qMiy@KPpV%zD<~OCR^e%6}D-yM}*0pUO>y?ef>-CG!UH)iYrlOH zlrX2F1Zb9-qld?o2E(T9}<~saXm-vO1>(f2NADeMpQcvz4LX_MPMa`Op z_W)5k7D$aWNHz+%AYFFxD$EBf?gtTG_Nf~;l=c|+J?w9JiOz-=^sN|HpwLVR{f4NM%#cfuRQhq*>rtF-9%-`FODR6 z#c%)-M7VZKBUl!6nXywpJ6`oW1xc;I;Nbojhu0IIrh^80JwQNh5bH67y1IPbtL1I)5pBY>>s$f}-xIzi~$^VK_ zHTKuf=Oi^snO`rmj`a2rql}0zb#mWruCJ&G&Op(PR&txs7r=WXi=MRrWY2-{58iRZ zLHS9;%20UqBAnzCm38`zYnNee1Y|-*a0JR1mh(|^9fKyM957f#CKwpUG}lyu`AK)X ztA+zR*o4BEBjx_ve)(pFru~iPCY^&hW2gu|&k0Fs386nicXFykK*RzY4S%v!XZje& za7+E~trKI%n$7K(2E;_)xd1zcO!ymfCN5JY9ra9Fgj;puIFb{cCPPASE&CBFxq4kX zRGB=d+ps>oF8`a3go%5_CT(qCus)2-2b1*$hngtaszSxm^{oGiPtd-PUN*$1zMBB} zi3LEnI4>0EX<=gT$~}>jh?JL}7K_Qse?YNGuK}6f_|P96yv|~V z_YYd7os@L`*Eo`(0B{sm>cC0tTH-Pu7s#PsnK^y;+Fe<`;*}zJDZNFGtxTMm zRvHYmDgh`R_YbTI3zW!ayHjH%3vOt~qw-^_q{Ab_;{H}&Dt{&P&a%3eQUYoedv=-( zkMDB5kpmCd?D89f=e`jQzUzTg;{xdLOB))waX`aV!wXXly2GI_f5#a^WfY$$KF@up ztN{bB=Aj0j#-HZT?h+4ZW#AM^%aCAj{AY+@+un8N0A!ly;o278+GP_>vi~#nk#!){ z{x9dNAn%RXZz%Gf;UJD%{Ui-%!ir4hEwbcdHaQN91nqz8Pzx~f5gKos5&%mF^0SRR z^#qeFeI);R9c(!;0ds$4Gqhd#`&N1`XwbvR{ds~8WIn5(rjgX#6s>R(|FNP6^RF79 zsma^qy}(<5F&ZZTW>~E(IH3M>AOmWEN_{B?EeeS`!&Ge(ZvQIU($w$yYnd`ehxD&U z2G(I?c;qeGD3#Mr7y5PUhed|2N08OUiV&RHc8CZ3dD4J0w)VG{MSZ~xT9kV~|E9&6 zSW~TMTd7EpG#Dx6K%d-l{j55f)Z&uFDXrJXF zqcKg&_&?)B3vOEkYq+{%kc@n`eL505?{}EoJ*%8)4XcREOLSBZ;*W+vDf&xs9~Z)m zisaQ;Iwn|HplxQynlP?RN(<}pKOCo#-`ci@yiH#r*Qg}s?(cZ{%Y2sf!+){j%Z94D zbSRh|J)bcYj7(6MCN%JCxNs>Q#4-OzFWig8UT`nj+Q>=Y0z*{Vo!DW>Vu<*iiAyCn|Sp?!u}?Wr)2@g zkDgZnBoY&W7(EBkTE5R;^L+h7lTIc_3&cpj6|~BWJjz_s$UOxWF%KY+4BqeI| zF>V8_imi|Tg=#*aCtN_~8Er!jd5%IpF?QWN^zgFKlqm_Bdj%Q&U)%JoD5Md8%ZNgg z0@M2b(4B)^0Cmtq=MN)wf>K*v|4<0g3`>=#;m;qce}dGoZ{DWS%%^tZ&+C}-W{3}0 z{c|?7l2XshV#co~f)|$#Ya{1Vb8TL1Z-r8y#Suq5r>PYyXaKn{=UgH|8*kMj z=1ii8NX-o7?Q0WgjbK-!dMBo$$7w)FYE-qdtk3Cb&Oaxw*Zq_V@6 zYt#A3<-y{Gz1QB~oR^hU{Q!?|+H;s*AwuuLG|^deo28xY{T=SO7twKuVNDv=9|hm% zc3D5f&R#%NwgwI{pz1JA&z~_TK^*|z2zCX)+ER^ryjuS@u9jee+6H(cZ}wnd{=hJ) z)&9f+0#o5zD1gm%!!ZfQYb9v@+QZ^TQotF}*_0tdgLD0Gc0c-pp9}OsbY{Eo@L=w! z6V!P0GdLEnm zqmL)^+tuY=I#gIUnwSj)jp$>2dMF*aDTDahpWMg)=mwJ!iLPD26(^npdCSqGj* zN9CrF6noo0d>a1c?qQU1#3-R#*XvAJIO5-E!ijuzdN^~s6I@AA#+Sq0z4UxLEnxS9 zP42R;Pn*sgC%UsFeIt&1mm5|XnBl05iF}~{1#VD@J>mQMqaUY^t3LPmX&%o?XfokP zYHV~l5*8{G7uP3L{gp0a>lTXq-hFU`FvB+Tze|);7&KR)8r-%~*>)Ss^12lL`?hP7 z5!@QcoU;y+Ig%vL<5~AToDDObsf0XzL*&{JVhXK1FHhfQzv&cb4wE&TVe#mJo*mp! z;BHsiRjDXY?Xh?=tQ0o?jR?$9!%NU;7!3v+tG3}awD5ndRjv=8oD#_ai4GVuXz={g ztJ-b2X@ze0`3_eUq5z)zj4Wu^%j6|e25yPWfpJCR)jaMe2QYnoRGL6@O~VNW2pe@9 zY2Um3r9j@Vt;V$SIvBn*R4zr|;&$Y@7Dvx+cg0>W)|fuKgDIM|v{W9Wk*o9f0mbU& zwZ56 zt%YWzchs%lS-qLSt?AF-yz{e%_q%4kbKbxF*frpRo+yC<3P>evnl!=hWAj z+rkC)H85|-PbU+!Z}ielep!8fkQ%l`Y+t~xrryE>#SaO@!I8?#X3M7v>DshBag-rI z06=s`>5HVZ(IE?_w|GMSuF%ZoJVMxa*7m#%T;Ds*yst{;pTWlogB*a3s@L=USi8^q z0$}#&<$+Y$WU*Af=O8~y0IVQZ1WJlNFk3IWgM#*K-|;E}8j6$V+X@Z=fQ=R&kJp_I zqo^^BzAs=0y(ai|wVcpo)wu=~cYD23iNaSQ@;y5*^I7Hf zTJZ>@|HB1%nm6;jzl}maD>>ePm+r*v9PTiT-X3G&XtjUn{Xz^Hlg@?^Qf3X3?tffA zDXFze^g3Si+LANkcuWa8t?NyiHUuk^FyKnv30H6{%3Yqay7vWmz19n}Hn)Gk_*i3dwh$S(Gp zLpH|l&z~#*cUeAd!F^W6GA)$4!=N~WC5`1c=ip3atsX{gdgt`-6gI%5*D{VA9>ZV; zre}(WMK~_|ITU4_6Z2Y7a+!lQYbU5ESsC|ird-$+kL2#^xEj`q4{!+f9?OG2kAYhG_ILXpcB-CXr zGhh70crW31>?vQg^fgDF({;2>-a{exb>+U;wZnsUdj?Sm2^UjVQw z@87#{s#NM~TayII2o@_IE7Jq>M={i_=8QKN4_h*l>la3b1@zxr1y`Cr;F$LIW8ev0^4zG##qzm!35~%kJCGZ;X}{&hB6m{2~jv7qjfvq9q~X zSv`tlcdA2L7jRm^`n&6QMFk~EFqUt+?hEf<$x$v_Zlx?gL~W*|r*5dw zS};aMm|wTOwg0zRcv5p)`oLiK`P3K_TGFf9Oiu|HHkwK1L@Ko4xgB^C?pQg|OGJab zkqof_27t83Jfd;AWrqrH8be#31oaad>7n zKvRJz_VoEY3@qAnA`x-mK@A}F2NTk44C&HrZmoagbx~mc5Ti2wlJtbJ;@-ljHhZ4NHAXDdQU}z$M^mTeG<-RdV921=6rimk74JXsPq=j<=6)V0d6 z8wEX!ocZXfOSNsAEVE4$KptJ|B;*wn;}JJcfN~h&3?nxk?K4E6E=el~1Lmuql64%X zh60iO!sz_b!yh~g?Cfcm3l~R_C}4Qg?9(AY$QOCq_EtAGXoM5sac@&U+4$_TEA+7= z9y0K4gD%9`FL``0ZBQB|$=^*8Z**}#_2yI8@UA(lz!FbGM~mt#a_9(kLGO(R&jI2% zY%G2>IcDzVoMEm}G0BC?^HKUbyh+w-LPq$Z#j5SwlLWYZp{sPg+UAFMPrI5!TCWVO z4{rtEf*`$_sts(4``=^ZL z`qX5O`_!@S#8?uT@(2;GvHSjT){*6Nx*I|SFcs_6p#-XJY_u)98@jF*3n8WU{qi>3 zJYdz&D?57mAMJf*Q(RB<=HP?728V$>qr9Q|+OMqB5%ss5Q*;{w zPUq^G#jy8Pn4jKgfqg|*hkOyRJ-7-wypARx-EAaLmOPj!5-BlhKd$Rs^`9yrLOuY| zvGzs4k*u_|R+0++Il9ohvy ze{vc$8?7p??o*EW8|i~!K~Q6*tEqr*{hC@Kj-S#_fB~{X+TcacZu4jGVLm zPJZ^u$(~nd@nyimMdu06bRGD(+w{r}JI_{PVpzUKckeIhoWGayvxraaFOtNIQZu_* zhlzFqQD~oo(k$N3zPEHHfQ?$$`Tc?{*cfa;0s^5BaAROwXB3o`1C-hbr|O_bT#owg zwTr^zfJGAydR4Q2Ph>n$zHn;B2T*%AGWF@_yy5HOJ0(G*x(hC)PylVR0;Gae11$62 za|rCbpxSN5lK~CBf%cnD=z(&0x$cty&$2+e3^VZwh_Pv}n^!?zwYbbmOBA`Gt(hZ_ zmTO10i>xb&b0JUmz%bwMw}_6GaA=;&g_p&K-v8lFY-9d$Cxtp^9)xopeZ0vtfv7eo zDFX|3RcB!aP(J&$9p!Ff)c`^oN#1z13NBBoCXu$3t~QuQ-|vatomA;4c_VK4qKELM z3n#^N2AT*M2Y<>8jsmKR{)OC#pNgzas-s}a|K-gA#7WQj7roWFI`>h=KiQ8|dt2V^ zo}p$R-X49g?o1GmraV-CRoo{V9)`6d4E*tu0`rwC{&V;`HSby{Rs&YFGHtnglU<5T zb=dLV-Ez;bmH-E3f4|J}CsV=bwY%ry(|oWwSEb<8(4UKm?rQq)L9^(ME2!BM_=+T# zj1&}5p`lM#V>A7)S6kV68Z5m>a3%cH@(=;{ja(gssY(Z52O!}8^4I3Qq|A!%s-^v5 zPc%9|U-l=RlGMx3?)d{UmQu1H0W`RRJ;V=nt{C0fER*ZgQS4QfeYdsNPY-5MV(#iN zJ_c%0ecdDo)sG~_i0nXG4QiM4*)IQ9xv+UmgUdZ-H$tm{l(b@vDleb;{wJaCm6vh1y>vlp zXsG{OdxpFS+~92v422%^EShnd7=kyRhlPa@?*gi2VYL65r$b$I5a^3pdi7S~y%UpM zK9hYtPYo~@ZF$5`z}*H?%)ljNic)nASNk@wh&dIJE9Q}3|F6`5v7hzbSN{?WZh-^? z>fLbhFK?|~lb}tH$WG8~2R*=K@AR5j3S8HUWnN#oEuA8!JjVx%@{Elvtp=f1wFS>R zNDWd=eyhu}90*cdN2WaxG1bM}f7{)PDKcGNUcm(0))XmtcnN06!HxR=h z1F%R_s&6@(U2Aj=D9Us@J zv5A=Gd#q0Tx?&aNOA@oq5yj+!u>;9S1|Kun$d_&roil}_zh}D>i+)FUpeL1#)BkGk z5dAaqDDWvAY(P(T9s`g`Sy|7nNu#lEwSGOVtoTR%R*iJ3lPXUpy{gFE&>nl7+zmZ$t>g-ZElyc-sx_a+%VKwUsl@KHB zKj$&=!!FxEN@STr=`*h83vi2@0Q()BMLkj3+cPggmC*0(hC9eN$m>c4WHzHrO!mJ- zdfZOvH1T}jyEIhaCw-U!8Pf0++8u3ytd-=e2#Ol{}$peJ!~Vpo`+K$zqVjr)y& zZ^6C?``gou;0@my(^SV;v&P1im<;I$P`RuwN4jz;sTnr`|9>c1>o2QwX&|83Q-Ie@ z&z1}Q{YA8J#l%O&g5A)JM&I!T)@!914Kp|IFR@fqhTrMFzBJf3_>G<<)FFN?s;sOG zS!X}%qeyesZo*%KfoCrF@(_n_H!+$O;?oth>MwnYRW{)Bvn%wytqMV#;N8D}WPCKs z2I4N5M5p=gJIy@dFt@4%Utui9)$SiODMlSQ_J2B`g)DRaUF4+i>?r1;bvyPDiZ~y3 zI!j*0Bq9uAv10+X6wQ!8Hp+3)NZ3)fU=VVs8y72tL72JCzeywUEipN4K`5_&xDmha zEo{t%&$D`qx!tE;g_EQL5jyXg3&*n0A=+rcy=R%n9JRQuxs$#9`OfG28$bJ8Htc@lDaIXc#R|d-jeSj^5vfUb}}+q1?H{3=MNqEjC(H# zhm(b?XA|_KlP07jnm@v&iUYf@vUHt0{u1vqq#=05kP<~MmvKW?ax$25bOqgdC7u$m38c_Aq0Fj02#7& zT>ec>^?aXN%&WHp;q%ndQlcM09*yx>K`B;lOWqd`L`C`Dv-%&iuElpHCx6PHa+#SS zH+tnqcBz{kl{1T>5%?TEM~9E$0xtl)aN}}aXY$^h#;UHvW-GhV_Src z#X*Cjvqb#JD3)+1AtAAC`unYY9>A_SU%y|Xwt6O&%z=k#4Ltl< zy$3x5DTaI=mO@ifu!57GOzWCczA&2}b1}GE;QkXG3=Pjojn4a!_-(Tkeq{kV=bkBl znIqrA)dRh`z2mkW^!)ve?wpBAN&;5!P_ZwDNZ0<(n9`O%yuP9oaoCTHAX(r4*oSFz zFAcv~NHK~0QguRyRrck^i1|pUVm7|Bi|}1uOEGtmac^>nx!nD&HH4H?B=)XcwQS|& zZSGg%gRRy?X>pB>s*0j7OZ*_EVaR=S%Pqtp)<8m+8RoK)n!;le#ASt+q8X`|Uh%CM zR$P|YU*mq&@7oD!-|90yIYXTDQ|tD2Dzb}>QS^^~7#|(DxptZpq|Ro?{10u@JLQs+ z@2Id|b2BG-;^@6qY+1o_Ric&-t$NDqmO!!LfZ4mRDd*L+H?F4yhGCo`i^T|6F-(De z(1SOEpD z!{(JgMI3&{8=gs_(J-p8=N#X_aalnKMEr73ehudEXvuz_VCpV`8Jtwtg&7{By*=q) z`-)qZ<%RQ#5I{Gb)w|_0Wo*wochM2qs^Fcg8k@41yu`p!H_O?ov*7D=fC@e*^Mewl zWOkfoMgdd+Opq9Plea|nT6#g9j`xJPhVXwAOw^@^lZ#}Gm9%#c=gw6xviJ))yhW(e zpM$%o_}d$P*FlSZN9!+MoT^?SjxpIFE1Cw5TayV93I)9OX9gw5S(m9|AQORxJE z`x{%SRDoPI3sf{ti|lC@M3@}R#8XDicYN1+aKYErf;08MgrI6DulqjeRnDD4>FPM@ z^7FsyKrtL2IRPp@Yw3;;9wpSLX08({R5@A2;5l4NdtKjwDnKf*J3o)v@zhofjVvtZ z?TrP_;)pr_S0NNCzQv@n2Kv?Jrd8J__}>WIBxvau zX2nm?nZm(jZ7K?veAcWANv`)aQ~f#j-T+1a~!$N;>{m@naD+eSOU?grCm)9 zj?=HiLvwZ!h}0y)hc*sg{l>*fWrg6d9tW=H{{+TKn8b`+e>!)QqGvf}^EuF|3SJXX z2BRsL%W5P|S$}PL)rd18{HLMC89{sOaY`a4l5mQ6>wVv0QuXYXNfsYY1@+r@Mc|bD zzNwaVdY0ZLERcTEB>`oW^3P>`wW1RaAx*AittXnATK5I^W!Pzt+H~Sa-{LtT7c-W*v*b`d*Y_THy+XRzBuxt#W3L#w|q^nBx&T z1-nud6Phr2`x~6&3ANtE+2h1U9nWg0`^sjr`|@VJDKX|Xv)9NlnPBu<(A@ zn!@}A4GnI5$6W;Yu{%e5xL>bp2O1nvCZ1*{9&oq#H-NXOxU(=juI44@CbnJ7j5$T) zIz!`Pd_T4Xth3!j+B43|p)(w2xB1teN};It<=+-^*-2tWlBMey;UbB7@r2camBSt09*= z_7qL+QalM**Qx!Cvx{aD36MI!=TbX#>6-19YZtJpg61Wi0cu_^ab>kL)W{60_LSE~ zzjmHJ327F=p+2vNF&?4JhouIg3ic+%dRBB9qxGAPnVSJR$ znI?2>qB^r?c(qH*vwU4(b#m-7b?|z20_|f%%23m@-Eq&x7|Wkw$n)?Uca7T%ZPhvZ%n$@F zK9F68PGLgQL2hjoD~pC=I=?CV`GUiBt3p~4-OEQvIcDtN_I;& zn4Ur``rv|bjy=Dk1}@%g;Q2LKm&{7YN{YB}LjW5zDX*|_U?MQx z(4RlvC?jNYYARRe0DW_ZglzMs2(A)-hO*c7Ia^?(_d9&0L-B7#IRZRqA))%7!~ZgGY9QeSee;8RfDb3@9%rH2fja)1TxFNuzp017K91; zzS;AnCu+j~hs3n+)1WIk&)$*$O-c+-c=~x7T)k%GQy5P&o!G4@u*>$>1<6LUGy{h@ zWdqm!-MNi5f~Ji9bbQ&pK@T4H2D|6H`^S6K@pMko>yV8Lv|A-OCo>sDh0vnFF4m^M ze86JD1ow7-^G{JbUH{8*CBU zZ0eKvu7D=s`J6MlfIStib;bH#DZva?p3_nNXYN%PB=34h3CLPpmMGuRJVNJoE6S6r zZRVEWJ^3E$Rkk}ud;1y+w7LF721>{lhz68Ym}%%We59)XRJ@_>HGd;H$44}x3V5J> ziS43?H_8u0z`IK6u>Ewd)d}m&*omJ{(=W6WSnv(fsuN2ssFuMGAwpnXE&2OBI})Wt zWBBBabHC#fGdqPi7a6oQjo(t9lQ09v7X9dPVdmm}V$G@fH``OltJbB1ZZ8)TdKsDm zV{7DA1k86egWKLAA3`npdF;!;us=6fxud7E7zkv0!N)wg_E_%r#7mHkQ)X-2 zVbHiN4?uO~ZwCsx%0bJ>>|TlRgoj>};?X2gDJ45BkYlchsBqu$&2(ba%*;$?tps{r zZHG#vix)eD3`lY^rbN)gd{RaPIu3Fvx(2)FVH&JQ3E zMw!wyYXHW7l^i*WCDjk1ztp1F;OHoUYy~Z6iabkii!^;Jy9;AUbz?4gRI&)+srNTR z0)7GjAV192NJ}OeDBISOfq*E>c6R3qyhHHG2ewe#CWnZh!Vn1LLbLaht)!&n*bD!b z4B5B2vH^2d5H|X?^TOzFNUl`Sz*9&D%wYAkjZF*tvy_-dxr*YTND;9S{I97=jWQhNXVt5JjQYwg^$=cjcHV;=ztbk7*5 zr%n3Ki?>>-vrfdMN?R^s<}{fw#_-w9s_s_rv7leQr}iIUOQ%Otnkkq@>y>>^k>@`a zedAW<`lP*z0|s@!`o-Q|&yh;Aj6u(IpTazlJ49>!#F!HJ63DO|)7X6haem$d`h-vK z%!t;xc`*18p4YKGuQxZ{PxX7<&X25$iXEOF`+e)a^~!N?e1H3L(cE|$S3ECy?7DK* zV3$h+a9&0?B(;VkIA^VLH#?bjp4BV;!+(tyZ44p#n?n zdXFLCy5l3A_*%&!YL3(O^2IgPWCU&yw5j}B(okS<=?ctBJ`Si4ijNTLncn|0N!syv+ zsg6v%Ef!D0S3m6}lbA&iGd%zx)ah=JXdV1m`334B)^1^Ckb^T1Y zuN|kuA!?QL(oq<#Gq`+~&@TaLGrp^-X!@r8w&)v6*-mW3DZj7QI$ON!5yv58vah1Z z(?l)Vw;3NZm;b~%@%MS1xvFC_g*fxZSLP-J18iTPIUgkbnO7xjF@fvQ-=ozs^ETql z-jFOf&gK==5|Qb0ut!qN&~d2BD`p-a*k&tjt(q&6TT>OQr?!}Y!N2E2ei_3%;SV^E z82}ZsxXT3_*tCj*OkSu;#le41M>pQo==ME;qh6lLn_~bIs z%s`bxRZOLF5n1*~)JaHC$@teK9K1+qd&2foA*rlU3sA2KW2XL7#sQ+;JDzIVpSglF ztJ)Byi%xi7TNRSYpXT@5&a9d6erM4Z)0avU;CZWq-3Txk@spaa_`qa9$66!pb-f`s zT8*R{K}#mc8~+DLnY*i4$YwxHven4iTdc^It9BJ32qYw|Gu6C%_(1G~*Ln>+e%Q8s z0)C=_h+_E!_ENQXya{0;8g|Pc)6Y$_-YV;FJBytr(a*F=hL7p_B<>cUVNQ6H<*CUY4zfa*L})VWA#6HDPQouaX#u8oc-S1 zjC$pAKdoE&i8=HM7VjrdY`vcEWJ>z{ABk3fa4THE3U{%Q()QmjY;=mRtTLgKDcN*m zyy2YhDJ2rPs?dxj#z$F^uDsfnsHgPp_Q2VFZqC|q*(=qmi>D#Jj#czgM6ff9S;gu%N5~zu3L0pk96a2 z{xX|?eFlAwd+=61B>*0sh%$dQOI?3MD9I#c%v8NxHQMQgZg zrIhr`C`DA(J-{`310gO$bye(j(~A%GZ+2e@1PJ#Af1V<~u1R{5cDat$C}yy`n4Mq` z+jLy7r+ts)`9*Zopvw;^fJ8+4-Z{eT+nf(j89hCw{@}+9Zf?4nu zvf5&tyCAK_W&%(~9#?n454#oUTaKUWN>cuo?8-AOyIGXEe^c`)UjUeR{k^tCc$H*_ zV2o|AU(3W?Rv7;P%nmjU-JEguCrPY7rfdKK^S#WVj-zg~Yo5ROi_-5^4w7Icw;ays zo9DRmRN-4*pC-u?j^GI5uz!@ExEyhXa{M3U5hSV0^D>()MV(s1pIB3j++x>5^P*Z6 zVeH3LP%H)fDy}2kUXW9*C&;b55Ht|kSi-WjO zEHJheb8OSuei0i;Q2iabfcJkc(fWaNgFpBLaTP@A2WSQ0TC=`)c^wT3;{nQC5t1#^ za4h4&a=2TAAFf&7^VIYIoXmeJ^8SJ6qA`3Ryx3*S;N?MUsI;A65l1VQWa80r&vrVo zjGV5~jPFRvmD^Lrkv-(0P$T{-)P)N2IWPD9E#FQ78$`c!UY)Yl&c|t&QVWbxBZ&`nJ42y6*HSD-;`42cz-oQ6p3TR!`Nx$%2i}&$%ml8 zxK+tIYVaq}WV3jxH^C~Yxwy0dx>eT`*=dh`ghNJIy1>oFL3hAxgyYm5iW}IMdo#)3 zEhubY`qrcx`6+1eu9Me&0isumbFB;m^Ay5c&hg-ab7jq8R-> zJ(fK?WOy0W7D4yxgK0-CqwAwYQ5Xa|rO+B9_8FUYbtQr@gmnp040b16Th;XudMG{`fjk z>A=_ti?Fd89p1k;!YLOcF*%d&@cRw_?*m=o1T64UldipdVQ3o=!B*&9a6ii6%WPnZ zy@QC*C$v~Y3aD&$`{2M7<<+m8}!xC9;4e!5yP*Vgo*!a&n zg|@$eC#?KP1-z*DnVL!LIAsxs4t$v~F~g#>2f9yRi@B?IC9|1U3shCNV!zP*h87re z&VF>P1g0)5_o@nBJt=WVi69MF3g4&LN}@hQ4bpYBR*$3R%9P{EkmJfk0}l0&?)l#; zzzm1%xC@BNmLQnzgc5;E_tG8M1a4MkQ#v&=e|zNmWaKJ$_*AhP@5-;X>_Vtlz-j+vKR{9W{f_8fV4^E3t7rmDxsy^z(MUTO(Ga4Hhw1H|4-2x{? z4mPXT8ljQ(&xkzZxHU=chD=JryvlOr*_=E$@Kj4PnwGn_pyWUnsK@Fz*6x9st!D^$Y-Ci2Ln8xtdmdTtf)#8w0tx+jR%P=U^B08;+EV z^~q+Vcvg3(H{LfDrilX#a&KUd!)GQQbcdq2|J-bbBq^k$udnntNkQ5>gnap>X625DJ z!VlZ;?XWu3j18HMzq6wYTF%YGEkyOlgkN7YU+nkY^x`=S6hdyC-T}1lH;b4dQG`*& zEWMTIACFOinngzE2a_gw<)A2z_-M*p2<7F?&}xmhH`)Yk7Jqr;HdL3IL?^dr^}H`W zg?V#h5?qJeVHdMlWT?29GBsroTAxoYW(WZhVr29JN+tv$%`$CLd1-QOON}GvS0!QG z%)Ru9*iExkg_qUN<(ZMz%KF%O?)ttB#a_mxMTzphWTj zp}t88v1k(CVwR~xvURec7;@NKWD$7@`TGl|_v2$PDj;^;xTt^h8_*|vY2zK3vh0z} z^#rdLFszsv&~+qT$iU8#@+67w1f+}V;wb(^ka~FFqYjs&_NTLi-Qp0%?GS|rxXe*` z4sU>gn>lLB`8K~Py;air_!{I_e&e#6)`FziB7MNic45X@uqO!$vWI0MQ#xQorNFC^ zx;H4eB0~5D-)%cLnNX1Y7|~Iz@aV1ZK3J)0-AnuQLvFLVN!IlS9AZgwzR*#4-9Lr? z3GYA~*tktYuM$*earjm@K+6voav?-Mv~YQ22ccX>!_{5(pe$Ql=&tTCyN#&h8gfwt z*PyiT_LJ*lGJx9mXN&b#Ay!%yRlP@FTtJ(CH|aA7C&V|{Slfs;8HA<_Z);!9H=%r>6_u3c za&ezl5Y?f2UfcL`h$(rHLy~#M8p=<%ttN^PjQ+@$*p;++bw7Re$-)tlxpLxz^4It6 zVuZ3{i)5=w{w8RiXWF zCa}v8s)j(QylUqtFvLgH?=uXK=UribI-mCE0-LSzVgGsRFCV@U#iI2fQS`y;4ektr zg(}HU9pma8EZ3v3^bq<|rd2-)VwGVY_fuCB2q8T@KG%mF62mqi(wJC}Z0H^VkBI9H zYBzSWf07lMWChzHJjia2aIf|H^~vQH>QrS9m0mi@M7WOTvrPxZQ=iiaha(zR>g^F; zW@2HS(O|7`9+;5cJ287gp@Tg1b?h}03i2Ms456Tq12Bv{NaEEn7%=MeK)ksP@8O&m z%=7K31aappunh2_yiX&fl$0%>eWC+%qUU~dHBm5?tuL_Lvh#ud!dS*7MN-yVK*5gT z2fzXB#{vtj4@jF@9*{qUts+$*@hZTtoXiOloP3%8ScMEJZQaKU?FK-iM{1yv_l9$zTwp^D(de91H7 zD3Ahl-NQIfnfE)`Yv*rh(y4^uq^Nyip+6$&BL(~&RrH08gUdw{qV-V(^HlgfO`dQQ z9(a&^c%~!;-C)rlRD2DQ8+3tU!K9PH2|&bH^M|ofVWf^h#rQ&=5!6MCy9=!m;y12v zrzjBDn{xj@7Ek5)Q)vj6NimkNgijZa4<%f{I*?Sx>!XMvCAmp#oPbrF@buqk1L^Yd zTFb_tk9;67=!mljeV)5tLPP#D^_1R645Xn~Npc*FS12U7&^KssV9pyXCY=zh2ycKn zO%93J8HPE&e7%3M4!90k_zX?bI#_BUO26(pIgRDL(SBVD<_|d73VoqeWlM}Q1H6Sn znL!@Ba|P5ipyk=?Y1c4;K$%$2p===RIo%iwl literal 0 HcmV?d00001 diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..c307d01 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,190 @@ +// Hand-rolled CLI parser. Matches the upstream rustdesk style (see +// `src/core_main.rs` in the rustdesk crate) — clap is not pulled into the +// main path. Only a handful of flags are supported on purpose: the surface +// area is the user-facing contract. + +use anyhow::{bail, Result}; + +#[derive(Debug, PartialEq, Eq)] +pub enum Action { + /// `--install`. Optionally combined with `--config ` for MDM + /// one-liner deployment. + Install, + /// `--uninstall`. Stops the service, deletes it, removes config dir. + Uninstall, + /// `--service`. SCM entry point; user code should never invoke this + /// manually except via the service dispatcher. + Service, + /// `--server`. Worker mode launched into the active console session by + /// the service shell. + Server, + /// `--config ` without `--install`. Persist config and exit. + ConfigOnly, + /// No flags. Foreground dev mode. + None, + /// `--cm`. Connection-manager popup mode. Spawned as a USER-token child + /// by the SYSTEM-token `--server` worker (via librustdesk's + /// `run_as_user`) when a peer needs interactive approval. Binds the + /// `_cm` IPC pipe, shows MessageBoxW, replies, exits. + Cm, +} + +#[derive(Debug)] +pub struct ParsedArgs { + pub action: Action, + pub config_blob: Option, +} + +impl ParsedArgs { + pub fn from_argv>(argv: I) -> Result { + let args: Vec = argv.into_iter().collect(); + let mut install = false; + let mut uninstall = false; + let mut service = false; + let mut server = false; + let mut cm = false; + let mut config_blob: Option = None; + + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + "--install" => install = true, + "--uninstall" => uninstall = true, + "--service" => service = true, + "--server" => server = true, + // Connection-manager popup mode. Treat `--cm-no-ui` (the + // Linux-headless variant librustdesk also tries) as a + // synonym; either way we run cm_popup. + "--cm" | "--cm-no-ui" => cm = true, + "--config" => { + let next = args.get(i + 1).cloned().ok_or_else(|| { + anyhow::anyhow!("--config requires a value") + })?; + config_blob = Some(next); + i += 1; + } + "--help" | "-h" => { + print_usage(); + std::process::exit(0); + } + "--version" | "-V" => { + println!("hello-agent {}", env!("CARGO_PKG_VERSION")); + std::process::exit(0); + } + other => bail!("unknown argument: {}", other), + } + i += 1; + } + + // Mutual-exclusion rules. --install + --config is the MDM one-liner; + // everything else is one-action-at-a-time. + let exclusive = [uninstall, service, server, cm].iter().filter(|x| **x).count(); + if exclusive > 1 { + bail!("--uninstall, --service, --server, --cm are mutually exclusive"); + } + if uninstall && (install || config_blob.is_some()) { + bail!("--uninstall cannot be combined with other flags"); + } + + let action = if uninstall { + Action::Uninstall + } else if install { + Action::Install + } else if service { + Action::Service + } else if server { + Action::Server + } else if cm { + Action::Cm + } else if config_blob.is_some() { + Action::ConfigOnly + } else { + Action::None + }; + + Ok(ParsedArgs { + action, + config_blob, + }) + } +} + +pub fn print_usage() { + eprintln!( + "hello-agent — headless RustDesk-protocol-compatible support agent + +USAGE: + hello-agent [OPTIONS] + +OPTIONS: + --install Register and start the Windows service. + --uninstall Stop, delete, and clean up the Windows service. + --config Import an admin-UI deploy blob. Accepts either the + reversed-base64 string emitted by the rustdesk-server + admin UI or the `host=...,key=...,api=...,relay=...` + filename form. May be combined with --install for + one-line MDM deployment. + --service SCM entry point. Do not invoke manually. + --server Worker mode (launched by the service shell into + the active console session). + -h, --help Show this help. + -V, --version Show version. + +EXAMPLES: + hello-agent.exe --install --config 0nI900VsFHZ... + hello-agent.exe --uninstall +" + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(s: &[&str]) -> Result { + ParsedArgs::from_argv(s.iter().map(|s| s.to_string())) + } + + #[test] + fn no_args_is_none() { + assert_eq!(parse(&[]).unwrap().action, Action::None); + } + + #[test] + fn install_with_config() { + let p = parse(&["--install", "--config", "BLOB"]).unwrap(); + assert_eq!(p.action, Action::Install); + assert_eq!(p.config_blob.as_deref(), Some("BLOB")); + } + + #[test] + fn config_only() { + let p = parse(&["--config", "BLOB"]).unwrap(); + assert_eq!(p.action, Action::ConfigOnly); + } + + #[test] + fn uninstall_alone() { + assert_eq!(parse(&["--uninstall"]).unwrap().action, Action::Uninstall); + } + + #[test] + fn install_uninstall_conflict() { + assert!(parse(&["--install", "--uninstall"]).is_err()); + } + + #[test] + fn service_server_conflict() { + assert!(parse(&["--service", "--server"]).is_err()); + } + + #[test] + fn config_missing_value() { + assert!(parse(&["--config"]).is_err()); + } + + #[test] + fn unknown_arg() { + assert!(parse(&["--no-such-flag"]).is_err()); + } +} diff --git a/src/cm_popup.rs b/src/cm_popup.rs new file mode 100644 index 0000000..3dd787d --- /dev/null +++ b/src/cm_popup.rs @@ -0,0 +1,399 @@ +// Approval popup, run in a dedicated `--cm` child process. +// +// Architecture (matches stock rustdesk): +// +// --service (Session 0, SYSTEM) +// │ launches into active console session as SYSTEM token +// ▼ +// --server (user session, SYSTEM token) --- screen capture, rendezvous, … +// │ on incoming peer requiring approval, librustdesk's start_ipc +// │ tries `ipc::connect("_cm")`, fails (no listener), then falls +// │ back to `run_as_user(["--cm"])`: +// ▼ +// --cm (user session, USER token) --- this module +// │ binds `_cm`, accepts one connection from the parent's start_ipc, +// │ reads frames until it sees Data::Login{authorized:false, …}, +// │ shows MessageBoxW (works cleanly because USER token + interactive +// │ desktop), replies Data::Authorize / Data::Close, drains the +// │ stream until the server closes it, exits. +// +// The previous design (run cm_popup as a thread inside the SYSTEM-token +// --server worker) hit Windows' UI-isolation rules — `MessageBoxW` from a +// SYSTEM-token process technically returns successfully but draws on a +// desktop the logged-in user can't see, so the popup was invisible. +// Spawning as a USER child sidesteps the whole class of issues. + +use anyhow::Result; +use librustdesk::ipc; + +#[cfg(target_os = "windows")] +use std::os::windows::ffi::OsStrExt; + +const POSTFIX: &str = "_cm"; + +/// Diagnostic trace: writes to stderr AND a debug log file. +/// Bypasses `log` so we still see output even when env_logger / flexi_logger +/// init went wrong. Drop these calls once the popup mechanism is stable. +fn trace(msg: &str) { + let line = format!( + "[{:?}] cm_popup: {msg}\n", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0), + ); + let _ = std::io::Write::write_all(&mut std::io::stderr(), line.as_bytes()); + + if let Ok(temp) = std::env::var("TEMP") { + let path = format!("{temp}\\hello-agent-cm.log"); + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path) + { + let _ = std::io::Write::write_all(&mut f, line.as_bytes()); + } + } +} + +/// Run the popup loop forever on a freshly-created Tokio runtime. +/// Safe to call from a `std::thread::spawn` body. +pub fn run_blocking() { + trace("run_blocking entered"); + + let rt = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(rt) => rt, + Err(e) => { + trace(&format!("build runtime: {e}")); + return; + } + }; + trace("runtime built; entering serve()"); + + if let Err(e) = rt.block_on(serve()) { + trace(&format!("serve exited: {e:#}")); + } else { + trace("serve returned cleanly"); + } +} + +/// Bind `_cm`, accept connections from `--server`'s `start_ipc` for as +/// long as the user session lasts. Each connection corresponds to one +/// peer requesting approval; we handle them concurrently. +async fn serve() -> Result<()> { + trace(&format!("calling new_listener({POSTFIX})")); + let mut incoming = match ipc::new_listener(POSTFIX).await { + Ok(i) => { + trace("new_listener succeeded"); + i + } + Err(e) => { + trace(&format!("new_listener failed: {e}")); + return Err(anyhow::anyhow!("new_listener({POSTFIX}): {e}")); + } + }; + + trace("entering accept loop"); + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + trace("accepted incoming connection"); + let conn = ipc::Connection::new(stream); + tokio::spawn(async move { + if let Err(e) = handle_one(conn).await { + trace(&format!("handle_one error: {e:#}")); + } + }); + } + Err(e) => { + trace(&format!("accept error: {e}")); + } + } + } + trace("accept loop exited"); + Ok(()) +} + +async fn handle_one(mut conn: ipc::Connection) -> Result<()> { + // Frame ordering on the `_cm` pipe is NOT "Login first, then chatter". + // For an installed/portable controlled side, the server first emits + // `Data::DataPortableService(CmShowElevation(...))` so the Flutter CM + // can render its elevation banner. The `Data::Login` we care about + // arrives a moment later. We loop through frames, ignore everything + // until we see Login{authorized:false}, decide once, and from then on + // just drain the stream so the server's `tx_to_cm.send()` calls don't + // back up. + // + // We use `conn.next()` (no timeout). A long active session can sit + // quiet for tens of minutes — `tx_to_cm` only fires on Login, FS + // transfers, and connection-close — so a short read timeout would + // false-positive into "session ended" UX during normal use. + trace("handle_one: entering frame loop"); + let mut decided = false; + // Set when the user clicks Yes on the approval popup. Carries the + // peer's id / name for the matching "session ended" notification we + // fire after the server tears the connection down. + let mut approved_peer: Option<(String, String)> = None; + + loop { + match conn.next().await { + Ok(Some(ipc::Data::Login { + peer_id, + name, + authorized: false, + .. + })) if !decided => { + trace(&format!( + "handle_one: Login peer_id={peer_id} name={name} authorized=false" + )); + decided = true; + + let approved = ask_user_blocking(&peer_id, &name).await; + trace(&format!( + "handle_one: MessageBox returned approved={approved}" + )); + + if approved { + let _ = conn.send(&ipc::Data::Authorize).await; + trace("handle_one: sent Authorize"); + approved_peer = Some((peer_id, name)); + } else { + let _ = conn.send(&ipc::Data::Close).await; + trace("handle_one: sent Close — exiting handler"); + return Ok(()); + } + } + Ok(Some(ipc::Data::Close)) | Ok(Some(ipc::Data::Disconnected)) => { + // Server signals the supporter has left (or the + // connection failed). Fall through to the post-loop + // notification path. + trace("handle_one: server sent Close/Disconnected"); + break; + } + Ok(Some(other)) => { + // Pre-login chatter (CmShowElevation), or post-Authorize + // chatter (chat, file transfer events, voice call). We + // don't act on any of it — the Flutter CM would, we just + // need to consume frames so the server's send buffer + // drains. + trace(&format!("handle_one: ignoring frame: {other:?}")); + continue; + } + Ok(None) => { + trace("handle_one: stream closed by peer"); + break; + } + Err(e) => { + trace(&format!("handle_one: stream error: {e}")); + break; + } + } + } + + // Tell the user the supporter is gone. Only fires when we approved + // the connection — denied/cancelled connections already returned + // above, and pre-approval Close from the server (e.g., auth failure + // before the popup even fired) shouldn't show a "session ended" + // banner the user has no context for. + if let Some((peer_id, name)) = approved_peer { + notify_session_ended(&peer_id, &name).await; + } + trace("handle_one: returning"); + Ok(()) +} + +/// Show a native MessageBox in the calling (user) session. Runs the dialog +/// on tokio's blocking thread pool so we don't park the reactor while it +/// waits for the user to click. +async fn ask_user_blocking(peer_id: &str, name: &str) -> bool { + let peer_id = peer_id.to_string(); + let name = name.to_string(); + tokio::task::spawn_blocking(move || show_messagebox(&peer_id, &name)) + .await + .unwrap_or(false) +} + +/// Inform the user that the remote support session has ended. Best-effort: +/// errors out of the OS dialog APIs are logged (via `trace`) and otherwise +/// ignored — failing to show the post-session banner shouldn't block the +/// handler from cleaning up. +async fn notify_session_ended(peer_id: &str, name: &str) { + let peer_id = peer_id.to_string(); + let name = name.to_string(); + let _ = tokio::task::spawn_blocking(move || show_session_ended(&peer_id, &name)).await; +} + +#[cfg(target_os = "windows")] +fn show_session_ended(peer_id: &str, name: &str) { + use std::ffi::OsStr; + use winapi::um::winuser::{MB_ICONINFORMATION, MB_OK}; + + let display_name = if name.is_empty() { "Unknown" } else { name }; + let body = format!( + "{display_name} ({peer_id}) has ended the remote support session.\n\nThe supporter is no longer connected." + ); + let caption = "HelloAgent — Remote session ended"; + + let body_w: Vec = OsStr::new(&body).encode_wide().chain(Some(0)).collect(); + let caption_w: Vec = OsStr::new(caption).encode_wide().chain(Some(0)).collect(); + let style = MB_OK | MB_ICONINFORMATION; + + // Same dual-path rendering as the approval popup: SYSTEM-token + // callers route through `WTSSendMessageW` to land on the user's + // interactive desktop, user-token callers go straight to MessageBoxW. + let res = if librustdesk::platform::is_root() { + match wts_send_message(&caption_w, &body_w, style) { + Ok(r) => Some(r), + Err(e) => { + trace(&format!( + "show_session_ended: WTSSendMessageW failed ({e}); falling back to MessageBoxW" + )); + messagebox_w(&caption_w, &body_w, style) + } + } + } else { + messagebox_w(&caption_w, &body_w, style) + }; + trace(&format!("show_session_ended: dialog returned {res:?}")); +} + +#[cfg(not(target_os = "windows"))] +fn show_session_ended(_peer_id: &str, _name: &str) {} + +#[cfg(target_os = "windows")] +fn show_messagebox(peer_id: &str, name: &str) -> bool { + use std::ffi::OsStr; + use winapi::um::winuser::{IDYES, MB_DEFBUTTON2, MB_ICONQUESTION, MB_YESNO}; + + let display_name = if name.is_empty() { "Unknown" } else { name }; + let body = format!( + "{display_name} ({peer_id}) is requesting remote control of this computer.\n\nAllow?" + ); + let caption = "HelloAgent — Allow remote support?"; + + // Pick the right rendering path. When the worker runs under the SYSTEM + // token (the service-launched case), a direct MessageBoxW call usually + // *does* succeed but draws on a desktop the logged-in user can't see — + // the call returns IDNO/IDCANCEL with no user input. WTSSendMessageW is + // the supported way for a SYSTEM caller to ask the *interactive* user + // a question: Windows itself renders the dialog on the user's session's + // active input desktop and ferries the click result back. + // + // For standalone (user-context) runs we keep the simple MessageBoxW + // path — the calling thread already owns the right desktop. + let body_w: Vec = OsStr::new(&body).encode_wide().chain(Some(0)).collect(); + let caption_w: Vec = OsStr::new(caption).encode_wide().chain(Some(0)).collect(); + let style = MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON2; + + let response: Option = if librustdesk::platform::is_root() { + match wts_send_message(&caption_w, &body_w, style) { + Ok(r) => Some(r), + Err(e) => { + trace(&format!( + "show_messagebox: WTSSendMessageW failed ({e}); falling back to MessageBoxW" + )); + messagebox_w(&caption_w, &body_w, style) + } + } + } else { + messagebox_w(&caption_w, &body_w, style) + }; + + response.map(|r| r == IDYES).unwrap_or(false) +} + +#[cfg(target_os = "windows")] +fn messagebox_w(caption_w: &[u16], body_w: &[u16], style: u32) -> Option { + use winapi::um::winuser::{MessageBoxW, MB_SETFOREGROUND, MB_SYSTEMMODAL, MB_TOPMOST}; + let flags = style | MB_TOPMOST | MB_SETFOREGROUND | MB_SYSTEMMODAL; + trace("show_messagebox: calling MessageBoxW (user-context path)"); + let result = unsafe { + MessageBoxW( + std::ptr::null_mut(), + body_w.as_ptr(), + caption_w.as_ptr(), + flags, + ) + }; + trace(&format!("show_messagebox: MessageBoxW returned {result}")); + Some(result) +} + +/// `WTSSendMessageW` from `wtsapi32.dll`. Not exposed by `winapi 0.3`, so we +/// declare it manually. The link to `WtsApi32.lib` comes from the vendored +/// rustdesk `build.rs` (`cargo:rustc-link-lib=WtsApi32`), which is already +/// linked into our final binary because we depend on `librustdesk`. +#[cfg(target_os = "windows")] +fn wts_send_message( + caption_w: &[u16], + body_w: &[u16], + style: u32, +) -> std::result::Result { + use winapi::shared::ntdef::HANDLE; + use winapi::um::winbase::WTSGetActiveConsoleSessionId; + + extern "system" { + fn WTSSendMessageW( + h_server: HANDLE, + session_id: u32, + p_title: *const u16, + title_length: u32, + p_message: *const u16, + message_length: u32, + style: u32, + timeout: u32, + p_response: *mut u32, + b_wait: i32, + ) -> i32; + } + + // WTS_CURRENT_SERVER_HANDLE is `(HANDLE)NULL` per the SDK header. + const WTS_CURRENT_SERVER_HANDLE: HANDLE = std::ptr::null_mut(); + + let session_id = unsafe { WTSGetActiveConsoleSessionId() }; + if session_id == 0xFFFF_FFFF { + return Err("no active console session (lock screen?)".into()); + } + trace(&format!( + "show_messagebox: calling WTSSendMessageW (session {session_id})" + )); + + // Lengths are in BYTES (despite the wide-char strings). Subtract the + // trailing null terminator we appended. + let title_bytes = ((caption_w.len().saturating_sub(1)) * 2) as u32; + let body_bytes = ((body_w.len().saturating_sub(1)) * 2) as u32; + + let mut response: u32 = 0; + let ok = unsafe { + WTSSendMessageW( + WTS_CURRENT_SERVER_HANDLE, + session_id, + caption_w.as_ptr() as *const u16, + title_bytes, + body_w.as_ptr() as *const u16, + body_bytes, + style, + 0, // timeout=0 → no timeout (block until user responds) + &mut response, + 1, // bWait=TRUE → block until response + ) + }; + if ok == 0 { + let err = std::io::Error::last_os_error(); + return Err(format!("WTSSendMessageW returned 0 (GetLastError: {err})")); + } + trace(&format!( + "show_messagebox: WTSSendMessageW returned response={response}" + )); + Ok(response as i32) +} + +#[cfg(not(target_os = "windows"))] +fn show_messagebox(_peer_id: &str, _name: &str) -> bool { + // Non-Windows is a stub. The whole module is only wired in when + // cfg(windows), so this branch should be unreachable in practice. + false +} diff --git a/src/config_import.rs b/src/config_import.rs new file mode 100644 index 0000000..003cbf8 --- /dev/null +++ b/src/config_import.rs @@ -0,0 +1,93 @@ +// Decode and persist an admin-UI deploy blob. +// +// The rustdesk-server admin UI emits a config string in two compatible forms, +// both handled by `librustdesk::custom_server::get_custom_server_from_string`: +// +// 1. A reversed URL-safe-base64-encoded JSON object containing +// {host, key, api, relay}. Example: `0nI900VsFHZ...` +// +// 2. A filename-style blob `host=server.example.net,key=...,api=...,relay=...` +// (used when the installer is renamed by the admin UI to deliver config). +// +// We treat the input as opaque, append `.exe` if missing (the upstream +// decoder strips it back off), and persist the four resulting fields via +// `hbb_common::config::Config::set_option`. Identical to what +// `core_main.rs` does on `--config` in stock rustdesk +// (see [src/core_main.rs:478](../rustdesk/src/core_main.rs#L478)) — we +// just don't gate it on `is_installed()` since we run before the service +// is registered (one-line MDM deploy: `--install --config `). + +use anyhow::{anyhow, Result}; +use hbb_common::config::Config; +use librustdesk::custom_server; + +/// Built-in fallback rendezvous configuration. Applied by +/// `apply_defaults_if_empty` when no `--config ` was provided and +/// no prior install left a value behind. The key here is the public +/// signing key of the cybnet rustdesk-server (`rd.gamecom.ch`) — distinct +/// from the per-agent identity keypair that the agent generates locally +/// on first run. +const DEFAULT_RENDEZVOUS_HOST: &str = "rd.gamecom.ch"; +const DEFAULT_API_URL: &str = "https://rd.gamecom.ch"; +const DEFAULT_RELAY_HOST: &str = "rd.gamecom.ch"; +const DEFAULT_PUBLIC_KEY: &str = "tcxma69cN3OWt25jQ75apSCtaZGIfDqIIP6yGNj3dgs="; + +pub fn apply(blob: &str) -> Result<()> { + let probe = if blob.to_lowercase().ends_with(".exe") { + blob.to_string() + } else { + format!("{blob}.exe") + }; + + let lic = custom_server::get_custom_server_from_string(&probe) + .map_err(|e| anyhow!("decode failed: {e}"))?; + + if lic.host.is_empty() { + return Err(anyhow!( + "config blob decoded but contains no rendezvous host" + )); + } + + log::info!( + "applying config: host={} api={} relay={} key.len={}", + lic.host, + lic.api, + lic.relay, + lic.key.len(), + ); + + Config::set_option("key".into(), lic.key); + Config::set_option("custom-rendezvous-server".into(), lic.host); + Config::set_option("api-server".into(), lic.api); + Config::set_option("relay-server".into(), lic.relay); + + Ok(()) +} + +/// Apply the built-in fallback rendezvous config if no `custom-rendezvous-server` +/// is currently set. Idempotent: a prior `--install --config ` (or +/// any earlier explicit configuration) wins, and re-runs without `--config` +/// don't clobber it. +/// +/// Why this exists: an MDM deployment that just runs `hello-agent.exe --install` +/// (no blob) needs *something* to register against. The defaults baked in +/// here are the cybnet `rd.gamecom.ch` rustdesk-server, so a no-arg install +/// produces a working agent out of the box. Operators who target a +/// different server still pass `--config ` and the defaults are +/// skipped. +pub fn apply_defaults_if_empty() { + if !Config::get_option("custom-rendezvous-server").is_empty() { + log::info!("custom-rendezvous-server already set; built-in defaults skipped"); + return; + } + log::info!( + "no rendezvous configured; applying built-in defaults: host={} api={} relay={}", + DEFAULT_RENDEZVOUS_HOST, + DEFAULT_API_URL, + DEFAULT_RELAY_HOST, + ); + Config::set_option("key".into(), DEFAULT_PUBLIC_KEY.into()); + Config::set_option("custom-rendezvous-server".into(), DEFAULT_RENDEZVOUS_HOST.into()); + Config::set_option("api-server".into(), DEFAULT_API_URL.into()); + Config::set_option("relay-server".into(), DEFAULT_RELAY_HOST.into()); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2bb244d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,213 @@ +// hello-agent: a headless RustDesk-protocol-compatible support agent. +// +// One binary, two run modes: a console / installer entry point that handles +// --install / --uninstall / --config, and a "--service" entry registered with +// the Windows SCM that spawns the actual worker into the active console +// session as "--server". +// +// The protocol stack (rendezvous, NAT punch, screen capture, input, login +// flow) is reused unchanged from `librustdesk`. This crate is just the +// thin shell that gives us a different CLI surface, our own service install +// path, and a native approval popup in place of the Flutter CM. +// +// We override `hbb_common`'s default `APP_NAME` ("RustDesk") with our own +// product name as the very first thing every process does. APP_NAME is read +// lazily from a `RwLock` whenever any path is computed (config dir, +// log dir, named-pipe namespace, …), so setting it before any of those +// initializers fire is enough to redirect all hbb_common state under +// `%APPDATA%\HelloAgent\` and the matching LocalService path. Identical +// to the `read_custom_client` write path the upstream Flutter build uses +// for OEM rebrands. + +#![cfg_attr(not(target_os = "windows"), allow(dead_code, unused_imports))] + +mod cli; +mod config_import; + +#[cfg(target_os = "windows")] +mod cm_popup; +#[cfg(target_os = "windows")] +mod service; +#[cfg(target_os = "windows")] +mod unattended_password; + +use cli::{Action, ParsedArgs}; + +/// Product name used to namespace all on-disk state and the IPC pipe path. +/// Written into `hbb_common::config::APP_NAME` at the top of `main` so +/// every subsequent path computation (config dir, log dir, named pipe) +/// targets `%APPDATA%\HelloAgent\` rather than the upstream default of +/// `%APPDATA%\RustDesk\`. Must be set before any code touches a path — +/// `hbb_common` initializes path globals lazily on first read. +pub const APP_NAME: &str = "HelloAgent"; + +/// Set up logging. We delegate to `hbb_common::init_log`, which: +/// * In **debug** builds: installs `env_logger` writing to stderr. +/// * In **release** builds: installs `flexi_logger` writing to a rolling +/// file under `/log//` — the SYSTEM service log ends +/// up at `%SystemRoot%\ServiceProfiles\LocalService\AppData\Roaming\HelloAgent\log\\` +/// and the user-mode log at `%APPDATA%\HelloAgent\log\\`. +/// +/// The `mode` label segregates per-run-mode log files so service worker +/// chatter doesn't tangle with --install diagnostics. `init_log` is +/// `Once`-guarded internally so calling it twice is harmless. +fn init_logging(mode: &str) { + let _ = hbb_common::init_log(false, mode); +} + +fn main() { + // MUST be the very first line. See the doc-comment on `APP_NAME` — + // anything that lazily reads a config / log / pipe path before this + // runs would cache `"RustDesk"` in `hbb_common`'s path globals and + // we'd never recover. + *hbb_common::config::APP_NAME.write().unwrap() = APP_NAME.to_owned(); + // Identify ourselves to the rustdesk-server's /api/sysinfo endpoint + // so the admin Devices page can show "HelloAgent 0.1.0" instead of + // the embedded rustdesk core version. These RwLocks are read once + // per sysinfo upload by hbbs_http::sync; setting them here (before + // start_server) ensures the very first upload carries the identity. + *hbb_common::config::AGENT_NAME.write().unwrap() = APP_NAME.to_owned(); + *hbb_common::config::AGENT_VERSION.write().unwrap() = env!("CARGO_PKG_VERSION").to_owned(); + + let parsed = match ParsedArgs::from_argv(std::env::args().skip(1)) { + Ok(p) => p, + Err(e) => { + eprintln!("hello-agent: {e}"); + eprintln!(); + cli::print_usage(); + std::process::exit(2); + } + }; + + // Initialize logging *after* arg parsing so the per-mode log file path + // is deterministic. `init_log` is Once-guarded internally. + let mode = match parsed.action { + Action::Install => "install", + Action::Uninstall => "uninstall", + Action::Service => "service", + Action::Server => "server", + Action::Cm => "cm", + Action::ConfigOnly | Action::None => "hello-agent", + }; + init_logging(mode); + + // --config is allowed to combine with --install (one-line MDM deploy) + // but on its own is a separate operation. Apply it first so --install + // sees the populated config. + if let Some(blob) = parsed.config_blob.as_deref() { + if let Err(e) = config_import::apply(blob) { + eprintln!("hello-agent: --config failed: {e:#}"); + std::process::exit(2); + } + } + + // Bake in fallback rendezvous defaults. Idempotent — if --config above + // (or a prior install) already set custom-rendezvous-server, this is a + // no-op. Without this, a bare `hello-agent.exe --install` would land + // at an unconfigured agent that can't reach any server. + config_import::apply_defaults_if_empty(); + + match parsed.action { + Action::Install => { + #[cfg(target_os = "windows")] + { + if let Err(e) = service::install() { + eprintln!("hello-agent: install failed: {e:#}"); + std::process::exit(1); + } + println!("hello-agent: installed and started."); + } + #[cfg(not(target_os = "windows"))] + { + eprintln!("hello-agent: --install is Windows-only for now."); + std::process::exit(1); + } + } + Action::Uninstall => { + #[cfg(target_os = "windows")] + { + if let Err(e) = service::uninstall() { + eprintln!("hello-agent: uninstall failed: {e:#}"); + std::process::exit(1); + } + println!("hello-agent: uninstalled."); + } + #[cfg(not(target_os = "windows"))] + { + eprintln!("hello-agent: --uninstall is Windows-only for now."); + std::process::exit(1); + } + } + Action::Service => { + #[cfg(target_os = "windows")] + { + if let Err(e) = service::run_as_service() { + eprintln!("hello-agent: service dispatcher failed: {e:#}"); + std::process::exit(1); + } + } + #[cfg(not(target_os = "windows"))] + { + eprintln!("hello-agent: --service is Windows-only."); + std::process::exit(1); + } + } + Action::Server => run_server(), + Action::Cm => { + // Spawned by the SYSTEM-token --server worker (via librustdesk's + // run_as_user) when the rustdesk core wants a CM. Runs as the + // logged-in user, binds the `_cm` IPC pipe, services one Login + // request with a MessageBoxW, replies, exits. + #[cfg(target_os = "windows")] + cm_popup::run_blocking(); + } + Action::ConfigOnly => { + // --config without --install or --service: just persist and exit. + } + Action::None => { + // No flags: dev mode. Run as a foreground server so the operator + // can watch logs. Production deployments use --install + --service. + run_server(); + } + } +} + +fn run_server() { + // Clear any stale `approve-mode = click` left by older hello-agent + // versions. ApproveMode comes from `password_security::approve_mode`: + // "password" → password only, "click" → popup only, anything else → + // both (try password first, fall back to popup). We want both so + // that (a) attended sessions still go through the cm_popup approval, + // and (b) unattended sessions can authenticate with the per-boot + // password we report to the admin UI. Setting to "" is idempotent + // and overrides any leftover "click" value on disk. + hbb_common::config::Config::set_option("approve-mode".into(), "".into()); + + // Pre-spawn the --cm child *on the user's interactive desktop* before + // start_server boots. librustdesk's start_ipc has its own + // run_as_user(["--cm"]) fallback, but it goes through C-side + // LaunchProcessWin with show=FALSE → lpDesktop=NULL → child inherits + // the parent's desktop, which (because we were spawned by the Session-0 + // service) is the invisible Session 0 service desktop. Our spawn + // helper sets lpDesktop = winsta0\\default explicitly, putting the + // popup on the user's screen. Once our --cm is bound to `_cm`, + // start_ipc's first ipc::connect("_cm") succeeds and rustdesk's + // built-in fallback never fires. + // + // We target *our own* session (whichever the supervisor placed us in + // — physical console, RDP, multi-user) rather than the physical + // console specifically. WTSGetActiveConsoleSessionId would point at + // the empty / lock-screen console session in RDP-only scenarios. + #[cfg(target_os = "windows")] + match service::spawn_cm_in_my_session() { + Ok(pid) => log::info!("spawned --cm child pid={pid} on winsta0\\default"), + Err(e) => log::warn!( + "could not pre-spawn --cm child ({e:#}); rustdesk's start_ipc fallback may be invisible" + ), + } + + // `start_server` is `#[tokio::main]` and runs forever. (is_server=true, + // no_server=false). It boots the default IPC server, input service, + // rendezvous mediator, and heartbeat sync. + librustdesk::start_server(true, false); +} diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000..c0ab340 --- /dev/null +++ b/src/service.rs @@ -0,0 +1,919 @@ +// Windows service shell. +// +// Three responsibilities: +// +// 1. `install()` — copy the binary to %ProgramFiles%\hello-agent, mirror the +// calling user's `HelloAgent.toml` into the LocalService-effective +// config dir so the SYSTEM service inherits the --config blob, register +// the service with the SCM pointing at the installed exe, and start it. +// Idempotent. +// +// 2. `uninstall()` — stop the service, delete it, remove the install dir +// (best effort if uninstall is run from somewhere other than the install +// dir itself), and clear the LocalService config copy. +// +// 3. `run_as_service()` — the SCM dispatcher entry. Watches for active +// console session changes and (re)launches `hello-agent.exe --server` +// into that session via `librustdesk::platform::launch_privileged_process`, +// so the worker inherits the SYSTEM token in the user's session. (We +// intentionally do NOT use `run_as_user` here — that drops to the +// logged-in user's token, and the worker would then read config from +// the user's %APPDATA% instead of the LocalService path the install +// flow mirrors to.) + +use anyhow::{anyhow, Context, Result}; +use std::ffi::OsString; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use windows_service::service::{ + ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode, + ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType, +}; +use windows_service::service_control_handler::{self, ServiceControlHandlerResult}; +use windows_service::service_dispatcher; +use windows_service::service_manager::{ServiceManager, ServiceManagerAccess}; + +const SERVICE_NAME: &str = "HelloAgent"; +const DISPLAY_NAME: &str = "HelloAgent Remote Support"; +const SERVICE_DESCRIPTION: &str = + "HelloAgent — headless remote-support agent (RustDesk-protocol-compatible). \ + Lets a remote supporter connect, subject to local user approval."; + +const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + +const INSTALL_SUBDIR: &str = "hello-agent"; +const INSTALLED_EXE_NAME: &str = "hello-agent.exe"; + +// ----------------------------- paths --------------------------------------- + +/// `%ProgramFiles%\hello-agent`. Falls back to `C:\Program Files\hello-agent` +/// if the env var isn't set (shouldn't happen on a real Windows install, +/// but we don't want to crash the installer if it does). +fn install_dir() -> PathBuf { + let base = std::env::var_os("ProgramFiles") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(r"C:\Program Files")); + base.join(INSTALL_SUBDIR) +} + +/// hbb_common's `patch()` rewrites `system32\config\systemprofile` → +/// `ServiceProfiles\LocalService` on Windows so that LocalSystem and +/// LocalService share a config root. The SYSTEM service therefore reads +/// from this path; we mirror the calling user's config files here so the +/// --config blob makes it across. +/// +/// Note the trailing `config` segment: `directories_next::ProjectDirs`, +/// which hbb_common uses on Windows, appends a literal `\config` to the +/// app's roaming dir (so the user-side path is +/// `%APPDATA%\HelloAgent\config\HelloAgent.toml`, not +/// `…\HelloAgent\…`). The SYSTEM-side path follows the same convention. +/// The `HelloAgent` segment is sourced from `crate::APP_NAME` so it stays +/// in lockstep with the `APP_NAME` we install into hbb_common at startup. +fn service_config_dir() -> PathBuf { + let system_root = std::env::var_os("SystemRoot") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(r"C:\Windows")); + system_root + .join("ServiceProfiles") + .join("LocalService") + .join("AppData") + .join("Roaming") + .join(crate::APP_NAME) + .join("config") +} + +// ----------------------------- install -------------------------------------- + +pub fn install() -> Result<()> { + let scm = ServiceManager::local_computer( + None::<&str>, + ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE, + ) + .context("open SCM")?; + + // 1. If a previous install left a running service, stop it before we + // overwrite its binary. Otherwise the file copy in step 2 fails + // with "access denied" because the SCM holds an exclusive handle on + // the running exe. + stop_existing_service(&scm); + + // 1b. Kill any lingering hello-agent.exe (notably the `--cm` user-token + // child, which lives outside the service's process tree and is + // therefore not stopped by SCM Stop). This makes `--install` + // idempotent / usable as an in-place update — without it, the + // `stage_binary` file copy below fails with "access denied" + // whenever a `--cm` child is still holding the old exe open. + // `kill_orphan_processes` uses taskkill with `/FI "PID ne "` + // so it never kills the running installer. + kill_orphan_processes(); + + // 2. Pin the binary to %ProgramFiles%\hello-agent. The user might be + // running --install from C:\Users\…\Downloads\, a USB stick, etc.; + // we don't want the SCM pointing back at any of those. + let target_exe = stage_binary().context("stage_binary")?; + + // 3. Clear stop-service and reset approve-mode to "both" (empty + // string → librustdesk treats as ApproveMode::Both: try password + // first, fall back to popup). Older hello-agent installs wrote + // "click" here, which disabled the password path; clearing it + // every install makes upgrades idempotent. These write into the + // *calling user's* %APPDATA%\HelloAgent\ — we mirror the result + // into the service's effective dir in step 4. + hbb_common::config::Config::set_option("stop-service".into(), "".into()); + hbb_common::config::Config::set_option("approve-mode".into(), "".into()); + + // 4. Mirror the calling user's `HelloAgent.toml` / `HelloAgent2.toml` + // into the LocalService-effective config root that the SYSTEM + // service will actually read. Without this, --config writes to e.g. + // C:\Users\Admin\AppData\Roaming\HelloAgent\, but the service runs + // as LocalSystem and (via hbb_common's `patch()`) reads from + // C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\HelloAgent\. + if let Err(e) = mirror_config_to_service_dir() { + log::warn!( + "could not mirror config to service dir ({e:#}); the service may not see --config until first heartbeat" + ); + } + + // 5. Register / reconfigure the SCM entry. Idempotent: if the service + // already exists we reuse the handle and change_config it to the + // new exe path + args. + + let info = ServiceInfo { + name: OsString::from(SERVICE_NAME), + display_name: OsString::from(DISPLAY_NAME), + service_type: SERVICE_TYPE, + start_type: ServiceStartType::AutoStart, + error_control: ServiceErrorControl::Normal, + executable_path: target_exe.clone(), + launch_arguments: vec![OsString::from("--service")], + dependencies: vec![], + account_name: None, // LocalSystem + account_password: None, + }; + + let svc = match scm.create_service( + &info, + ServiceAccess::CHANGE_CONFIG + | ServiceAccess::START + | ServiceAccess::STOP + | ServiceAccess::QUERY_STATUS, + ) { + Ok(s) => s, + Err(windows_service::Error::Winapi(e)) + if e.raw_os_error() == Some(winapi::shared::winerror::ERROR_SERVICE_EXISTS as i32) => + { + log::info!("service exists; reusing"); + let svc = scm + .open_service( + SERVICE_NAME, + ServiceAccess::CHANGE_CONFIG + | ServiceAccess::START + | ServiceAccess::STOP + | ServiceAccess::QUERY_STATUS, + ) + .context("open existing service")?; + svc.change_config(&info).context("change_config")?; + svc + } + Err(e) => return Err(anyhow!("create_service: {e}")), + }; + + let _ = svc.set_description(SERVICE_DESCRIPTION); + + // 6. Start the service. (Step 1 already stopped any prior instance.) + svc.start::<&str>(&[]).context("start service")?; + log::info!( + "service '{}' installed at {} and started", + SERVICE_NAME, + target_exe.display() + ); + Ok(()) +} + +/// Best-effort stop + wait of an existing HelloAgent service. No-op if the +/// service doesn't exist or is already stopped. We use a short connection +/// here (STOP|QUERY_STATUS only) so the install path can call this without +/// holding the broader CHANGE_CONFIG handle from later steps. +fn stop_existing_service(scm: &ServiceManager) { + let svc = match scm.open_service( + SERVICE_NAME, + ServiceAccess::STOP | ServiceAccess::QUERY_STATUS, + ) { + Ok(s) => s, + Err(_) => return, // doesn't exist; nothing to stop + }; + + if let Ok(status) = svc.query_status() { + if status.current_state == ServiceState::Stopped { + return; + } + } + let _ = svc.stop(); + wait_for_state(&svc, ServiceState::Stopped, Duration::from_secs(20)); +} + +/// Copy the running exe to %ProgramFiles%\hello-agent\hello-agent.exe and +/// return the destination path. If the running exe is already the installed +/// path (e.g., the user ran `hello-agent.exe --install` from the install +/// directory after a manual update), we skip the copy. +fn stage_binary() -> Result { + let src = std::env::current_exe().context("current_exe")?; + let src = src.canonicalize().unwrap_or(src); + let dest_dir = install_dir(); + let dest = dest_dir.join(INSTALLED_EXE_NAME); + + let dest_canon = dest.canonicalize().ok(); + if dest_canon.as_ref() == Some(&src) { + log::info!("running exe is already installed at {}", dest.display()); + return Ok(dest); + } + + std::fs::create_dir_all(&dest_dir) + .with_context(|| format!("create_dir_all {}", dest_dir.display()))?; + + // If something is already there (an old install), Windows allows + // overwriting if no process holds the file open. The service was either + // never installed or we'll restart it after this; either way, the + // running --install process is the only handle we worry about, and that + // handle is on `src`, not `dest`. + std::fs::copy(&src, &dest).with_context(|| { + format!("copy {} -> {}", src.display(), dest.display()) + })?; + log::info!( + "installed binary: {} -> {}", + src.display(), + dest.display() + ); + Ok(dest) +} + +/// Copy the calling user's `HelloAgent.toml` + `HelloAgent2.toml` into +/// the LocalService-effective config dir so the SYSTEM service sees them. +fn mirror_config_to_service_dir() -> Result<()> { + let dest_dir = service_config_dir(); + std::fs::create_dir_all(&dest_dir) + .with_context(|| format!("create_dir_all {}", dest_dir.display()))?; + + let user_main = hbb_common::config::Config::file(); + let user_aux = hbb_common::config::Config2::file(); + + let mut copied = 0usize; + for src in [user_main, user_aux] { + let Some(name) = src.file_name() else { continue }; + let dest = dest_dir.join(name); + match std::fs::copy(&src, &dest) { + Ok(_) => { + copied += 1; + log::info!("mirrored {} -> {}", src.display(), dest.display()); + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + // Calling user never had this file (e.g. --install without + // --config, or first ever run on this machine, or the user + // wiped %APPDATA%\HelloAgent\ between tests). Logged at + // info so the post-install log shows clearly which toml + // files were available and which weren't. + log::info!( + "no source file at {} (skipped — service worker will generate it)", + src.display() + ); + } + Err(e) => { + log::warn!("mirror {} -> {}: {e}", src.display(), dest.display()); + } + } + } + + if copied == 0 { + log::info!( + "no user-side config files to mirror to {}", + dest_dir.display() + ); + } + Ok(()) +} + +// ----------------------------- uninstall ------------------------------------ + +pub fn uninstall() -> Result<()> { + // Kill every hello-agent.exe process except ourselves *first*. We can't + // rely on the SCM Stop control alone because the `--cm` child spawned + // via `run_as_user` runs under the logged-in user's token, not SYSTEM, + // so it isn't in the service's process tree and SCM won't reach it. + // Doing this up front means the SCM stop below is usually a no-op + // (service process already gone) and the rmdir at the end no longer + // races a lingering child holding hello-agent.exe open. Our own PID + // is excluded via taskkill's `/FI` so the uninstaller doesn't suicide. + kill_orphan_processes(); + + let scm = ServiceManager::local_computer( + None::<&str>, + ServiceManagerAccess::CONNECT, + ) + .context("open SCM")?; + + match scm.open_service( + SERVICE_NAME, + ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE, + ) { + Ok(svc) => { + // Stop, wait, delete. Each step is best-effort; we want + // --uninstall to leave nothing behind even if the service is + // already in a weird state. After the kill above the service + // process is typically already gone, so SCM transitions to + // Stopped within a poll cycle; the 20s wait is a safety net + // for the rare case taskkill couldn't reach the supervisor. + if let Ok(status) = svc.query_status() { + if status.current_state != ServiceState::Stopped { + let _ = svc.stop(); + wait_for_state(&svc, ServiceState::Stopped, Duration::from_secs(20)); + } + } + svc.delete().context("delete service")?; + log::info!("service '{}' deleted", SERVICE_NAME); + } + Err(windows_service::Error::Winapi(e)) + if e.raw_os_error() + == Some(winapi::shared::winerror::ERROR_SERVICE_DOES_NOT_EXIST as i32) => + { + log::info!("service '{}' not present (no-op)", SERVICE_NAME); + } + Err(e) => return Err(anyhow!("open_service: {e}")), + } + + cleanup_install_dir(); + // We deliberately do NOT delete the LocalService config dir here. + // `HelloAgent.toml` in that directory holds the agent's id + keypair, + // which the rustdesk-server / rendezvous server has registered against + // the agent's id. Wiping it forces the next --install to generate + // fresh keys, which the rendezvous server's cached entry (and any + // supporter that resolved the agent recently) will mismatch with — the + // encrypted handshake then silently fails on the supporter side and + // the connection sits idle until the peer times out. + // + // Operators who want a true hard wipe can run: + // rmdir /s /q "%SystemRoot%\ServiceProfiles\LocalService\AppData\Roaming\HelloAgent" + // and then delete the device record from the rustdesk-server admin UI. + log::info!("preserved LocalService config dir to keep agent keys/id stable across reinstalls"); + Ok(()) +} + +/// Best-effort sweep of every hello-agent.exe process other than ourselves. +/// Used by both `--install` (so an in-place update isn't blocked by an +/// old `--cm` child holding the exe open) and `--uninstall` (so the +/// rmdir at the end isn't racing a lingering child). +/// +/// Shells out to the built-in `taskkill` rather than re-implementing the +/// Toolhelp32 enumeration in winapi: taskkill ships in every Windows +/// install since XP, runs in milliseconds, and the `/FI "PID ne "` +/// filter handles the "don't suicide ourselves" requirement declaratively. +/// +/// Exit code 128 from taskkill means "no matching processes" — common +/// case when there's no orphan to clean up — and we treat it the same +/// as success. Anything else gets logged but does not fail the caller. +fn kill_orphan_processes() { + let our_pid = std::process::id(); + let pid_filter = format!("PID ne {our_pid}"); + let output = std::process::Command::new("taskkill") + .args([ + "/F", + "/IM", + INSTALLED_EXE_NAME, + "/FI", + &pid_filter, + ]) + .output(); + match output { + Ok(out) => { + let code = out.status.code(); + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + if out.status.success() { + log::info!( + "taskkill killed orphan {INSTALLED_EXE_NAME} processes (excluding pid {our_pid}): {}", + stdout.trim() + ); + // TerminateProcess is synchronous w.r.t. the kernel marking + // the process as exited, but kernel-mode finalization + // (releasing file handles, paging out the image section) + // can lag by up to a few hundred ms. The rmdir that follows + // races against this: without the pause, an immediate + // remove_dir_all can still see "file in use" on the just- + // killed process's exe. + std::thread::sleep(Duration::from_millis(500)); + } else if code == Some(128) { + log::info!("no orphan {INSTALLED_EXE_NAME} processes to kill"); + } else { + log::warn!( + "taskkill returned {code:?}: stdout={} stderr={}", + stdout.trim(), + stderr.trim(), + ); + } + } + Err(e) => { + log::warn!("could not invoke taskkill: {e}"); + } + } +} + +/// Remove %ProgramFiles%\hello-agent. Best-effort: if the user ran +/// --uninstall from inside the install dir, the running exe is locked +/// open by the OS and the rmdir will fail. We log and move on; the +/// remaining files are harmless and can be deleted manually after exit. +fn cleanup_install_dir() { + let dir = install_dir(); + if !dir.exists() { + return; + } + match std::fs::remove_dir_all(&dir) { + Ok(()) => log::info!("removed install dir {}", dir.display()), + Err(e) => log::warn!( + "could not remove {} ({}); delete manually if needed", + dir.display(), + e + ), + } +} + +fn wait_for_state( + svc: &windows_service::service::Service, + target: ServiceState, + timeout: Duration, +) -> bool { + let start = Instant::now(); + while start.elapsed() < timeout { + match svc.query_status() { + Ok(s) if s.current_state == target => return true, + _ => std::thread::sleep(Duration::from_millis(250)), + } + } + false +} + +// ----------------------------- service runtime ------------------------------ + +windows_service::define_windows_service!(ffi_service_main, service_main); + +pub fn run_as_service() -> Result<()> { + service_dispatcher::start(SERVICE_NAME, ffi_service_main) + .map_err(|e| anyhow!("service_dispatcher::start: {e}")) +} + +fn service_main(_args: Vec) { + if let Err(e) = service_main_inner() { + log::error!("service_main: {e:#}"); + } +} + +fn service_main_inner() -> Result<()> { + let stop_flag = Arc::new(AtomicBool::new(false)); + let stop_flag_handler = stop_flag.clone(); + + // We poll WTSGetActiveConsoleSessionId every iteration of the main loop, + // so we don't need session-change events from the SCM. Keeping the + // handler set narrow (Stop/Shutdown/Interrogate) means SCM won't deliver + // events we'd just throw away. + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + ServiceControl::Stop | ServiceControl::Shutdown => { + stop_flag_handler.store(true, Ordering::SeqCst); + ServiceControlHandlerResult::NoError + } + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + let status_handle = service_control_handler::register(SERVICE_NAME, event_handler) + .map_err(|e| anyhow!("register handler: {e}"))?; + + set_status( + &status_handle, + ServiceState::Running, + ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, + )?; + + log::info!("hello-agent service started"); + + // Generate a fresh per-boot unattended-access password and report it + // to the rustdesk-server admin API. Runs in a background thread with + // its own Tokio runtime so it doesn't block the supervisor poll loop; + // retries internally until the server acknowledges (early attempts + // can race the rendezvous registration done by `--server`). + crate::unattended_password::rotate_and_report(); + + // Worker process handle. Killed on Stop, replaced on session change. + // `last_state` carries (session_id, had_user). The `had_user` bit is + // what forces a respawn when a user logs in to a session we're + // *already* running in (login-screen console → same session, but now + // with a user) — the new `--server` needs to pre-spawn its `--cm` + // child against the freshly-available user token, which the prior + // `--server` couldn't do. + let mut worker: Option = None; + let mut last_state: Option<(u32, bool)> = None; + + while !stop_flag.load(Ordering::SeqCst) { + // Pick a target session in this priority order: + // + // 1. Active *user* session (RDP-connected user, or physical + // console with a logged-in user) — the normal case, full + // screen capture / input / popup. + // 2. Physical console session at the login or lock screen + // (no user, but `winlogon.exe` is running so + // `launch_privileged_process` works and DXGI desktop + // duplication can capture the login screen). This is what + // enables unattended access via the per-boot password — the + // supporter sees the actual login screen, not a black + // "No displays" panel. + // 3. Session 0 (where this supervisor itself lives as + // LocalSystem). Last-ditch fallback, no display, no input — + // rendezvous + heartbeat keep flowing but capture is + // empty. We avoid it whenever (2) is reachable. + let active = find_active_user_session(); + let target = active + .or_else(active_console_session_for_capture) + .unwrap_or(0); + let target_has_user = active.is_some(); + let target_state = (target, target_has_user); + let worker_dead = worker.as_ref().map(|w| !w.is_alive()).unwrap_or(false); + + let needs_respawn = match (worker.is_some(), last_state) { + (false, _) => true, + (_, Some(prev)) if prev != target_state => true, + _ if worker_dead => true, + _ => false, + }; + + if needs_respawn { + if let Some(prev) = worker.take() { + prev.kill_and_wait(Duration::from_secs(5)); + } + let spawn_result = if target == 0 { + Worker::spawn_in_service_session() + } else { + Worker::spawn(target) + }; + match spawn_result { + Ok(w) => { + if target == 0 { + log::info!( + "no console or user session reachable; spawned --server \ + in Session 0 (registration only — screen capture \ + unavailable until a session is available)" + ); + } else if active.is_some() { + log::info!( + "spawned --server worker into user session {target}" + ); + } else { + log::info!( + "no user logged in; spawned --server into console \ + session {target} (login screen capture)" + ); + } + worker = Some(w); + last_state = Some(target_state); + } + Err(e) => { + log::warn!("spawn worker failed: {e:#}"); + std::thread::sleep(Duration::from_secs(5)); + } + } + } + + std::thread::sleep(Duration::from_millis(750)); + } + + // Shutdown. + if let Some(prev) = worker.take() { + prev.kill_and_wait(Duration::from_secs(5)); + } + + set_status( + &status_handle, + ServiceState::Stopped, + ServiceControlAccept::empty(), + )?; + log::info!("hello-agent service stopped"); + Ok(()) +} + +fn set_status( + handle: &service_control_handler::ServiceStatusHandle, + state: ServiceState, + accept: ServiceControlAccept, +) -> Result<()> { + handle + .set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: state, + controls_accepted: accept, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::from_secs(5), + process_id: None, + }) + .map_err(|e| anyhow!("set_service_status: {e}")) +} + +/// Worker process handle. We use `librustdesk::platform::launch_privileged_process` +/// (the same path stock rustdesk's `--service` uses) which calls +/// `LaunchProcessWin(..., as_user=FALSE, ...)` — the new process runs as +/// SYSTEM in the active console session. SYSTEM-in-user-session can both +/// (a) read config from the LocalService-effective path our install flow +/// mirrors to, and (b) draw UI / capture screen / send input on the user's +/// desktop (it's the standard service-side-of-remote-control pattern). +/// +/// We get back a Win32 HANDLE rather than a `std::process::Child`; this +/// thin wrapper exposes the few operations the supervisor loop needs and +/// closes the handle on drop. +struct Worker { + handle: winapi::shared::ntdef::HANDLE, +} + +// HANDLE is `*mut c_void`, which isn't Send by default; the inner pointer +// is opaque to the OS and safe to move between threads. +unsafe impl Send for Worker {} + +impl Worker { + fn spawn(session_id: u32) -> Result { + let exe = std::env::current_exe().context("current_exe")?; + let exe_str = exe + .to_str() + .ok_or_else(|| anyhow!("non-UTF-8 exe path: {}", exe.display()))?; + let cmd = format!("\"{exe_str}\" --server"); + let handle = librustdesk::platform::launch_privileged_process(session_id, &cmd) + .map_err(|e| anyhow!("launch_privileged_process: {e}"))?; + if handle.is_null() { + return Err(anyhow!( + "launch_privileged_process returned NULL handle (session {session_id} not ready?)" + )); + } + Ok(Self { handle }) + } + + /// Spawn `--server` in our own session (Session 0, LocalSystem). Used + /// when no user is logged in: we can't `launch_privileged_process` for + /// session 0 because that helper resolves the target token via + /// `winlogon.exe`/`explorer.exe`, neither of which run in Session 0. + /// The supervisor itself is LocalSystem-in-Session-0, so a plain + /// `Command::spawn` puts the child in the same place with the same + /// token — exactly what we want for the no-user-logged-in fallback. + fn spawn_in_service_session() -> Result { + use std::os::windows::io::IntoRawHandle; + + let exe = std::env::current_exe().context("current_exe")?; + let child = std::process::Command::new(&exe) + .arg("--server") + .spawn() + .with_context(|| format!("spawn {} --server", exe.display()))?; + // Take ownership of the child's process HANDLE; this suppresses + // `Child::Drop`'s close so kill_and_wait / Drop on Worker manage + // the lifetime cleanly via TerminateProcess + CloseHandle. + let handle = child.into_raw_handle() as winapi::shared::ntdef::HANDLE; + Ok(Self { handle }) + } + + fn is_alive(&self) -> bool { + // WAIT_TIMEOUT (0x102) means the wait expired without the handle + // being signaled — i.e., the process is still running. Anything + // else (WAIT_OBJECT_0 = exited, WAIT_FAILED = error) we treat as + // dead so the supervisor will respawn. + const WAIT_TIMEOUT: u32 = 0x0000_0102; + let r = unsafe { winapi::um::synchapi::WaitForSingleObject(self.handle, 0) }; + r == WAIT_TIMEOUT + } + + fn kill_and_wait(self, timeout: Duration) { + unsafe { + winapi::um::processthreadsapi::TerminateProcess(self.handle, 1); + let ms = timeout.as_millis().min(u32::MAX as u128) as u32; + let _ = winapi::um::synchapi::WaitForSingleObject(self.handle, ms); + } + // Drop closes the handle. + } +} + +impl Drop for Worker { + fn drop(&mut self) { + unsafe { + winapi::um::handleapi::CloseHandle(self.handle); + } + } +} + +/// Pick the session that hosts the user's *active* interactive desktop — +/// physical console *or* RDP. Returns `None` if no user is actively logged +/// in anywhere. +/// +/// We can't use `WTSGetActiveConsoleSessionId()` here: it only returns the +/// session attached to the **physical** console. When the user is connected +/// via RDP only, the console session is empty (or at the lock screen), and +/// this primitive gives us the wrong target. The popup ends up rendered on +/// the invisible console desktop while the RDP user sees nothing. +/// +/// Instead enumerate sessions and pick one in `WTSActive` state with a +/// resolvable user token. `WTSActive` means "the user is at the keyboard +/// of this session right now" — which is true for the RDP session when +/// they're on RDP, and for the console session when they're at the +/// physical machine. A user who logged in to RDP and then disconnected +/// without logging out shows up as `WTSDisconnected` and we correctly +/// skip them. +pub(crate) fn find_active_user_session() -> Option { + use winapi::shared::ntdef::HANDLE; + use winapi::um::handleapi::CloseHandle; + use winapi::um::wtsapi32::WTSQueryUserToken; + + #[repr(C)] + struct WtsSessionInfoW { + session_id: u32, + win_station_name: *mut u16, + state: i32, // WTS_CONNECTSTATE_CLASS + } + + const WTS_ACTIVE: i32 = 0; + extern "system" { + fn WTSEnumerateSessionsW( + h_server: HANDLE, + reserved: u32, + version: u32, + pp_session_info: *mut *mut WtsSessionInfoW, + p_count: *mut u32, + ) -> i32; + fn WTSFreeMemory(p_memory: *mut std::ffi::c_void); + } + + let mut sessions: *mut WtsSessionInfoW = std::ptr::null_mut(); + let mut count: u32 = 0; + let ok = unsafe { + WTSEnumerateSessionsW( + std::ptr::null_mut(), // WTS_CURRENT_SERVER_HANDLE + 0, + 1, // version + &mut sessions, + &mut count, + ) + }; + if ok == 0 || sessions.is_null() { + return None; + } + + let mut chosen: Option = None; + for i in 0..count { + let info = unsafe { &*sessions.add(i as usize) }; + if info.state != WTS_ACTIVE { + continue; + } + // Skip the login-screen session (no logged-in user → no token). + let mut token: HANDLE = std::ptr::null_mut(); + let token_ok = unsafe { WTSQueryUserToken(info.session_id, &mut token) }; + if token_ok != 0 && !token.is_null() { + unsafe { CloseHandle(token) }; + chosen = Some(info.session_id); + break; + } + } + + unsafe { WTSFreeMemory(sessions as *mut _) }; + chosen +} + +/// Physical-console session ID — used as the fallback target when no user +/// is logged in. At the login or lock screen `winlogon.exe` is running in +/// this session, which is enough for `launch_privileged_process` to find +/// a SYSTEM token there and spawn `--server` into a session that has an +/// actual display (Session 0 doesn't). Returns None when Windows reports +/// no console attached (boot, fast-user-switching mid-detach). +pub(crate) fn active_console_session_for_capture() -> Option { + use winapi::um::winbase::WTSGetActiveConsoleSessionId; + let id = unsafe { WTSGetActiveConsoleSessionId() }; + // 0xFFFF_FFFF: no console attached. 0: same as our own session, no + // gain over the Session 0 fallback that comes after. + if id == 0xFFFF_FFFF || id == 0 { + None + } else { + Some(id) + } +} + +/// Returns the session ID of the calling process. Used by `--server` to +/// know which session it itself was launched into, so the `--cm` child +/// lands in the *same* session (and therefore on the same interactive +/// desktop the user is actually using). +fn current_process_session() -> Option { + use winapi::um::processthreadsapi::{GetCurrentProcessId, ProcessIdToSessionId}; + let mut sid: u32 = 0; + let ok = unsafe { ProcessIdToSessionId(GetCurrentProcessId(), &mut sid) }; + if ok == 0 { + None + } else { + Some(sid) + } +} + +/// Spawn `hello-agent.exe --cm` into the active console session as the +/// logged-in user, **on the user's interactive desktop**. +/// +/// Why we don't just call `librustdesk::platform::run_as_user(["--cm"])`: +/// the C-side `LaunchProcessWin` only sets `STARTUPINFO.lpDesktop = +/// L"winsta0\\default"` when its `show` parameter is `TRUE`. `run_as_user` +/// hardcodes `show=false`, leaving `lpDesktop = NULL`. With NULL, the new +/// process inherits the *parent's* desktop. Our parent chain (`--service` +/// in Session 0 → `--server` in user session as SYSTEM token) is rooted +/// in Session 0's `Service-0x...\Default` desktop, so any UI rendered by +/// the resulting `--cm` child draws there — invisible to the logged-in +/// user. This helper sets `lpDesktop` explicitly so the popup actually +/// reaches the user's screen. +/// Convenience wrapper used by `run_server`: spawn `--cm` into the same +/// session the calling process itself is running in. Falls back to +/// `find_active_user_session` if `ProcessIdToSessionId` fails for some +/// reason. +pub(crate) fn spawn_cm_in_my_session() -> Result { + let session_id = current_process_session() + .or_else(find_active_user_session) + .ok_or_else(|| anyhow!("no active user session to spawn --cm into"))?; + spawn_cm_into_user_desktop(session_id) +} + +pub(crate) fn spawn_cm_into_user_desktop(session_id: u32) -> Result { + use std::os::windows::ffi::OsStrExt; + use winapi::shared::ntdef::HANDLE; + use winapi::um::handleapi::CloseHandle; + use winapi::um::processthreadsapi::{CreateProcessAsUserW, PROCESS_INFORMATION, STARTUPINFOW}; + use winapi::um::winbase::DETACHED_PROCESS; + use winapi::um::wtsapi32::WTSQueryUserToken; + + // 1. Grab the user's primary access token for this session. Requires + // SE_TCB_NAME; SYSTEM has it by default. + let mut user_token: HANDLE = std::ptr::null_mut(); + let ok = unsafe { WTSQueryUserToken(session_id, &mut user_token) }; + if ok == 0 { + let err = std::io::Error::last_os_error(); + return Err(anyhow!( + "WTSQueryUserToken(session={}): {} (no user logged in?)", + session_id, + err + )); + } + + // 2. Build the command line. CreateProcessAsUserW may patch the + // lpCommandLine buffer in place, so it has to be a mutable Vec. + let exe = std::env::current_exe().context("current_exe")?; + let cmd_str = format!("\"{}\" --cm", exe.display()); + let mut cmd_w: Vec = std::ffi::OsStr::new(&cmd_str) + .encode_wide() + .chain(Some(0)) + .collect(); + + // 3. The desktop string is referenced by si.lpDesktop and must stay + // alive until CreateProcessAsUserW returns. + let mut desktop_w: Vec = std::ffi::OsStr::new("winsta0\\default") + .encode_wide() + .chain(Some(0)) + .collect(); + + let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() }; + si.cb = std::mem::size_of::() as u32; + si.lpDesktop = desktop_w.as_mut_ptr(); + + let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() }; + + // 4. Spawn. DETACHED_PROCESS so the child has no console attached and + // isn't tied to ours. We do not pass an environment block — NULL + // means "inherit ours", which is fine for cm_popup. + let cp_ok = unsafe { + CreateProcessAsUserW( + user_token, + std::ptr::null(), + cmd_w.as_mut_ptr(), + std::ptr::null_mut(), + std::ptr::null_mut(), + 0, + DETACHED_PROCESS, + std::ptr::null_mut(), + std::ptr::null(), + &mut si, + &mut pi, + ) + }; + let cp_err = std::io::Error::last_os_error(); + + unsafe { CloseHandle(user_token) }; + + if cp_ok == 0 { + return Err(anyhow!("CreateProcessAsUserW: {}", cp_err)); + } + + let pid = pi.dwProcessId; + // We don't track the child's lifetime here. It will outlive the + // calling --server until either the user session ends (Windows reaps + // it) or it exits voluntarily on cm_popup error. + unsafe { + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } + Ok(pid) +} diff --git a/src/unattended_password.rs b/src/unattended_password.rs new file mode 100644 index 0000000..e3ef197 --- /dev/null +++ b/src/unattended_password.rs @@ -0,0 +1,123 @@ +// Per-boot unattended-access password. +// +// On every service start (= every host reboot, since `--service` is the +// Windows service entry the SCM auto-starts on boot) hello-agent generates +// a random "permanent password" and reports it to the rustdesk-server +// admin API. A supporter reaching the device when no user is logged in +// can read the password from the admin UI and authenticate without the +// per-session approval popup ever firing. +// +// The password is: +// 1. Persisted locally via `Config::set_permanent_password` so the +// rustdesk auth path accepts it on the next LoginRequest. +// 2. POSTed to `/api/unattended-password` with a retry +// loop. The first few attempts can legitimately fail with +// ID_NOT_FOUND because rendezvous registration runs in the +// `--server` child (which the supervisor hasn't even spawned yet +// when this fires), not in this `--service` process — we just back +// off and retry until the peer row exists server-side. + +use anyhow::{anyhow, Result}; +use hbb_common::rand::{distributions::Alphanumeric, Rng}; +use std::time::Duration; + +const PASSWORD_LEN: usize = 12; +const MAX_RETRY_DELAY: Duration = Duration::from_secs(60); +const MAX_ATTEMPTS: u32 = 20; + +/// Generate a fresh password, write it to local config, and kick off a +/// background reporter thread. Returns immediately; the reporter has its +/// own Tokio runtime so it doesn't tangle with the supervisor's poll loop. +pub fn rotate_and_report() { + let password = generate(); + hbb_common::config::Config::set_permanent_password(&password); + log::info!( + "rotated unattended-access password (len={})", + password.len() + ); + + std::thread::spawn(move || { + let rt = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(rt) => rt, + Err(e) => { + log::warn!("unattended-password reporter: build runtime: {e}"); + return; + } + }; + rt.block_on(report_with_retry(password)); + }); +} + +fn generate() -> String { + hbb_common::rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(PASSWORD_LEN) + .map(char::from) + .collect() +} + +async fn report_with_retry(password: String) { + // Start at 2s and double up to MAX_RETRY_DELAY. The early ID_NOT_FOUND + // window typically clears within a minute (heartbeat sync registers + // the peer on its first iteration), so most boots land on the second + // or third attempt. After MAX_ATTEMPTS we give up — the password is + // already set locally, the only thing missing is its visibility in + // the admin UI, so silent forever-retry would just be log spam. + let mut delay = Duration::from_secs(2); + for attempt in 1..=MAX_ATTEMPTS { + match try_report(&password).await { + Ok(_) => { + log::info!( + "unattended-password: server acknowledged on attempt {attempt}" + ); + return; + } + Err(e) => { + log::warn!( + "unattended-password: report attempt {attempt}/{MAX_ATTEMPTS} \ + failed ({e:#}); retrying in {:?}", + delay + ); + } + } + tokio::time::sleep(delay).await; + delay = (delay * 2).min(MAX_RETRY_DELAY); + } + log::error!( + "unattended-password: gave up after {MAX_ATTEMPTS} attempts — admin UI \ + won't show the password until the next service start" + ); +} + +async fn try_report(password: &str) -> Result<()> { + let api = librustdesk::common::get_api_server( + hbb_common::config::Config::get_option("api-server"), + hbb_common::config::Config::get_option("custom-rendezvous-server"), + ); + if api.is_empty() { + return Err(anyhow!("no api-server configured yet")); + } + + let url = format!("{api}/api/unattended-password"); + let id = hbb_common::config::Config::get_id(); + let uuid = librustdesk::common::encode64(hbb_common::get_uuid()); + let body = hbb_common::serde_json::json!({ + "id": id, + "uuid": uuid, + "password": password, + }) + .to_string(); + + let resp = librustdesk::common::post_request(url, body, "") + .await + .map_err(|e| anyhow!("post: {e}"))?; + let trimmed = resp.trim(); + if trimmed == "OK" { + Ok(()) + } else { + Err(anyhow!("unexpected response: {trimmed}")) + } +} diff --git a/vendor/rustdesk/.cargo/config.toml b/vendor/rustdesk/.cargo/config.toml new file mode 100644 index 0000000..42a4adb --- /dev/null +++ b/vendor/rustdesk/.cargo/config.toml @@ -0,0 +1,16 @@ +[target.x86_64-pc-windows-msvc] +rustflags = ["-Ctarget-feature=+crt-static"] +[target.i686-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"] +[target.'cfg(target_os="macos")'] +rustflags = [ + "-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null", +] +#[target.'cfg(target_os="linux")'] +# glibc-static required, this may fix https://github.com/rustdesk/rustdesk/issues/9103, but I do not want this big change +# this is unlikely to help also, because the other so files still use libc dynamically +#rustflags = [ +# "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic" +#] +[net] +git-fetch-with-cli = true diff --git a/vendor/rustdesk/.gitattributes b/vendor/rustdesk/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/vendor/rustdesk/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/vendor/rustdesk/Cargo.toml b/vendor/rustdesk/Cargo.toml new file mode 100644 index 0000000..f580142 --- /dev/null +++ b/vendor/rustdesk/Cargo.toml @@ -0,0 +1,258 @@ +[package] +name = "rustdesk" +version = "1.4.6" +authors = ["rustdesk "] +edition = "2021" +build= "build.rs" +description = "RustDesk Remote Desktop" +default-run = "rustdesk" +rust-version = "1.75" + +[lib] +name = "librustdesk" +# Local divergence vs upstream rustdesk: ["cdylib", "staticlib", "rlib"]. +# hello-agent statically links the rlib into hello-agent.exe and never +# needs the cdylib (used by upstream for Flutter FFI) or the staticlib. +# Cargo builds all crate-types of a [lib] together, and the cdylib link +# step aggregates multiple windows-targets/windows_x86_64_msvc versions +# into one DLL alongside the explicitly-linked windows.lib import library, +# producing LNK1169 multiply-defined-symbol failures. Restricting to rlib +# skips the cdylib link entirely and is fine for our consumer. +crate-type = ["rlib"] + +[[bin]] +name = "naming" +path = "src/naming.rs" + +[[bin]] +name = "service" +path = "src/service.rs" + +[features] +inline = [] +cli = [] +use_samplerate = ["samplerate"] +use_rubato = ["rubato"] +use_dasp = ["dasp"] +flutter = ["flutter_rust_bridge"] +default = ["use_dasp"] +hwcodec = ["scrap/hwcodec"] +vram = ["scrap/vram"] +mediacodec = ["scrap/mediacodec"] +plugin_framework = [] +linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"] +unix-file-copy-paste = [ + "dep:x11-clipboard", + "dep:x11rb", + "dep:percent-encoding", + "dep:once_cell", + "clipboard/unix-file-copy-paste", +] +screencapturekit = ["cpal/screencapturekit"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = "0.1" +scrap = { path = "libs/scrap", features = ["wayland"] } +hbb_common = { path = "libs/hbb_common" } +serde_derive = "1.0" +serde = "1.0" +serde_json = "1.0" +serde_repr = "0.1" +cfg-if = "1.0" +lazy_static = "1.4" +sha2 = "0.10" +repng = "0.2" +parity-tokio-ipc = { git = "https://github.com/rustdesk-org/parity-tokio-ipc" } +magnum-opus = { git = "https://github.com/rustdesk-org/magnum-opus" } +dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true } +rubato = { version = "0.12", optional = true } +samplerate = { version = "0.2", optional = true } +uuid = { version = "1.3", features = ["v4"] } +clap = "4.2" +rpassword = "7.2" +num_cpus = "1.15" +bytes = { version = "1.4", features = ["serde"] } +default-net = "0.14" +wol-rs = "1.0" +flutter_rust_bridge = { version = "=1.80", features = ["uuid"], optional = true} +errno = "0.3" +rdev = { git = "https://github.com/rustdesk-org/rdev" } +url = { version = "2.3", features = ["serde"] } +crossbeam-queue = "0.3" +hex = "0.4" +chrono = "0.4" +cidr-utils = "0.5" +fon = "0.6" +zip = "0.6" +shutdown_hooks = "0.1" +totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } +stunclient = "0.4" +kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys"} +reqwest = { version = "0.12", features = ["blocking", "socks", "json", "native-tls", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false } + +[target.'cfg(not(target_os = "linux"))'.dependencies] +# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux +cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" } +ringbuf = "0.3" + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +mac_address = "1.1" +sciter-rs = { git = "https://github.com/rustdesk-org/rust-sciter", branch = "dyn" } +sys-locale = "0.3" +enigo = { path = "libs/enigo", features = [ "with_serde" ] } +clipboard = { path = "libs/clipboard" } +ctrlc = "3.2" +# arboard = { version = "3.4", features = ["wayland-data-control"] } +arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] } +clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } +portable-pty = { git = "https://github.com/rustdesk-org/wezterm", branch = "rustdesk/pty_based_0.8.1", package = "portable-pty" } + +system_shutdown = "4.0" +qrcode-generator = "4.1" + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3", features = [ + "winuser", + "wincrypt", + "shellscalingapi", + "pdh", + "synchapi", + "memoryapi", + "shellapi", + "devguid", + "setupapi", + "cguid", + "cfgmgr32", + "ioapiset", + "winspool", +] } +windows = { version = "0.61", features = [ + "Win32", + "Win32_Foundation", + "Win32_Security", + "Win32_Security_Authorization", + "Win32_Storage_FileSystem", + "Win32_System", + "Win32_System_Diagnostics", + "Win32_System_Diagnostics_ToolHelp", + "Win32_System_Environment", + "Win32_System_IO", + "Win32_System_Memory", + "Win32_System_Pipes", + "Win32_System_Threading", + "Win32_UI_Shell", +] } +winreg = "0.11" +windows-service = "0.6" +virtual_display = { path = "libs/virtual_display" } +remote_printer = { path = "libs/remote_printer" } +impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" } +shared_memory = "0.12" +tauri-winrt-notification = "0.1" +runas = "1.2" + +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2" +cocoa = "0.24" +dispatch = "0.2" +core-foundation = "0.9" +core-graphics = "0.22" +include_dir = "0.7" +fruitbasket = "0.10" +objc_id = "0.1" +# If we use piet "0.7" here, we must also update core-graphics to "0.24". +piet = "0.6" +piet-coregraphics = "0.6" +foreign-types = "0.3" + +[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] +tray-icon = { git = "https://github.com/tauri-apps/tray-icon", version = "0.21.3" } +tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" } +image = "0.24" + +[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] +keepawake = { git = "https://github.com/rustdesk-org/keepawake-rs" } + +[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] +wallpaper = { git = "https://github.com/rustdesk-org/wallpaper.rs" } +tiny-skia = "0.11" +softbuffer = "0.4" +fontdb = "0.23" +bytemuck = "1.23" +ttf-parser = "0.25" + +[target.'cfg(target_os = "linux")'.dependencies] +libxdo-sys = "0.11" +psimple = { package = "libpulse-simple-binding", version = "2.27" } +pulse = { package = "libpulse-binding", version = "2.27" } +rust-pulsectl = { git = "https://github.com/rustdesk-org/pulsectl" } +async-process = "1.7" +evdev = { git="https://github.com/rustdesk-org/evdev" } +dbus = "0.9" +dbus-crossroads = "0.5" +pam = { git="https://github.com/rustdesk-org/pam" } +x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} +x11rb = {version = "0.12", features = ["all-extensions"], optional = true} +percent-encoding = {version = "2.3", optional = true} +once_cell = {version = "1.18", optional = true} +nix = { version = "0.29", features = ["term", "process"]} +gtk = "0.18" +termios = "0.3" +terminfo = "0.8" +winit = "0.30" + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +openssl = { version = "0.10", features = ["vendored"] } + +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.13" +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"] +exclude = ["vdi/host", "examples/custom_plugin"] + +# Patch libxdo-sys to use a stub implementation that doesn't require libxdo +# This allows building and running on systems without libxdo installed (e.g., Wayland-only) +[patch.crates-io] +libxdo-sys = { path = "libs/libxdo-sys-stub" } + +[package.metadata.winres] +LegalCopyright = "Copyright © 2025 cStudio GmbH. All rights reserved." +ProductName = "RustDesk" +FileDescription = "RustDesk Remote Desktop" +OriginalFilename = "rustdesk.exe" + +[target.'cfg(target_os="windows")'.build-dependencies] +winres = "0.1" +winapi = { version = "0.3", features = [ "winnt", "pdh", "synchapi" ] } + +[build-dependencies] +cc = "1.0" +hbb_common = { path = "libs/hbb_common" } +os-version = "0.2" + +[dev-dependencies] +hound = "3.5" +docopt = "1.1" + +[package.metadata.bundle] +name = "RustDesk" +identifier = "com.carriez.rustdesk" +icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"] +osx_minimum_system_version = "10.14" + +#https://github.com/johnthagen/min-sized-rust +[profile.release] +lto = true +codegen-units = 1 +panic = 'abort' +strip = true +#opt-level = 'z' # only have smaller size after strip +rpath = true + +[profile.dev] +debug = 1 diff --git a/vendor/rustdesk/LICENCE b/vendor/rustdesk/LICENCE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/vendor/rustdesk/LICENCE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/vendor/rustdesk/build.rs b/vendor/rustdesk/build.rs new file mode 100644 index 0000000..92fb1f4 --- /dev/null +++ b/vendor/rustdesk/build.rs @@ -0,0 +1,94 @@ +#[cfg(windows)] +fn build_windows() { + let file = "src/platform/windows.cc"; + let file2 = "src/platform/windows_delete_test_cert.cc"; + cc::Build::new().file(file).file(file2).compile("windows"); + println!("cargo:rustc-link-lib=WtsApi32"); + println!("cargo:rerun-if-changed={}", file); + println!("cargo:rerun-if-changed={}", file2); +} + +#[cfg(target_os = "macos")] +fn build_mac() { + let file = "src/platform/macos.mm"; + let mut b = cc::Build::new(); + if let Ok(os_version::OsVersion::MacOS(v)) = os_version::detect() { + let v = v.version; + if v.contains("10.14") { + b.flag("-DNO_InputMonitoringAuthStatus=1"); + } + } + b.flag("-std=c++17").file(file).compile("macos"); + println!("cargo:rerun-if-changed={}", file); +} + +#[cfg(all(windows, feature = "inline"))] +fn build_manifest() { + use std::io::Write; + if std::env::var("PROFILE").unwrap() == "release" { + let mut res = winres::WindowsResource::new(); + res.set_icon("res/icon.ico") + .set_language(winapi::um::winnt::MAKELANGID( + winapi::um::winnt::LANG_ENGLISH, + winapi::um::winnt::SUBLANG_ENGLISH_US, + )) + .set_manifest_file("res/manifest.xml"); + match res.compile() { + Err(e) => { + write!(std::io::stderr(), "{}", e).unwrap(); + std::process::exit(1); + } + Ok(_) => {} + } + } +} + +fn install_android_deps() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + if target_os != "android" { + return; + } + let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if target_arch == "x86_64" { + target_arch = "x64".to_owned(); + } else if target_arch == "x86" { + target_arch = "x86".to_owned(); + } else if target_arch == "aarch64" { + target_arch = "arm64".to_owned(); + } else { + target_arch = "arm".to_owned(); + } + let target = format!("{}-android", target_arch); + let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); + let mut path: std::path::PathBuf = vcpkg_root.into(); + if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") { + path = vcpkg_root.into(); + } else { + path.push("installed"); + } + path.push(target); + println!( + "cargo:rustc-link-search={}", + path.join("lib").to_str().unwrap() + ); + println!("cargo:rustc-link-lib=ndk_compat"); + println!("cargo:rustc-link-lib=oboe"); + println!("cargo:rustc-link-lib=c++"); + println!("cargo:rustc-link-lib=OpenSLES"); +} + +fn main() { + hbb_common::gen_version(); + install_android_deps(); + #[cfg(all(windows, feature = "inline"))] + build_manifest(); + #[cfg(windows)] + build_windows(); + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + if target_os == "macos" { + #[cfg(target_os = "macos")] + build_mac(); + println!("cargo:rustc-link-lib=framework=ApplicationServices"); + } + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/vendor/rustdesk/libs/clipboard/Cargo.toml b/vendor/rustdesk/libs/clipboard/Cargo.toml new file mode 100644 index 0000000..afe2f2f --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "clipboard" +version = "0.1.0" +edition = "2021" +build = "build.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +cc = "1.0" + +[features] +default = [] +unix-file-copy-paste = [ +"dep:x11rb", +"dep:x11-clipboard", +"dep:rand", +"dep:fuser", +"dep:libc", +"dep:dashmap", +"dep:percent-encoding", +"dep:utf16string", +"dep:once_cell", +"dep:cacao" +] + +[dependencies] +thiserror = "1.0" +lazy_static = "1.4" +serde = "1.0" +serde_derive = "1.0" +hbb_common = { path = "../hbb_common" } +parking_lot = {version = "0.12"} + +[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] +rand = {version = "0.8", optional = true} +libc = {version = "0.2", optional = true} +dashmap = {version ="5.5", optional = true} +utf16string = {version = "0.2", optional = true} +once_cell = {version = "1.18", optional = true} + +[target.'cfg(target_os = "linux")'.dependencies] +percent-encoding = {version ="2.3", optional = true} +x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} +x11rb = {version = "0.12", features = ["all-extensions"], optional = true} +fuser = {version = "0.15", default-features = false, optional = true} + +[target.'cfg(target_os = "macos")'.dependencies] +cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true} +# Use `relax-void-encoding`, as that allows us to pass `c_void` instead of implementing `Encode` correctly for `&CGImageRef` +objc2 = { version = "0.5.1", features = ["relax-void-encoding"] } +objc2-foundation = { version = "0.2.0", features = ["NSArray", "NSString", "NSEnumerator", "NSGeometry", "NSProgress"] } +objc2-app-kit = { version = "0.2.0", features = ["NSPasteboard", "NSPasteboardItem", "NSImage", "NSFilePromiseProvider"] } +uuid = { version = "1.3", features = ["v4"] } +fsevent = "2.1.2" +dirs = "5.0" +xattr = "1.4.0" diff --git a/vendor/rustdesk/libs/clipboard/build.rs b/vendor/rustdesk/libs/clipboard/build.rs new file mode 100644 index 0000000..3902eaa --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/build.rs @@ -0,0 +1,35 @@ +#[cfg(target_os = "windows")] +fn build_c_impl() { + let mut build = cc::Build::new(); + + build.file("src/windows/wf_cliprdr.c"); + + { + build.flag_if_supported("-Wno-c++0x-extensions"); + build.flag_if_supported("-Wno-return-type-c-linkage"); + build.flag_if_supported("-Wno-invalid-offsetof"); + build.flag_if_supported("-Wno-unused-parameter"); + + if build.get_compiler().is_like_msvc() { + build.define("WIN32", ""); + // build.define("_AMD64_", ""); + build.flag("-Z7"); + build.flag("-GR-"); + // build.flag("-std:c++11"); + } else { + build.flag("-fPIC"); + // build.flag("-std=c++11"); + // build.flag("-include"); + // build.flag(&confdefs_path.to_string_lossy()); + } + + build.compile("mycliprdr"); + } + + println!("cargo:rerun-if-changed=src/windows/wf_cliprdr.c"); +} + +fn main() { + #[cfg(target_os = "windows")] + build_c_impl(); +} diff --git a/vendor/rustdesk/libs/clipboard/src/cliprdr.h b/vendor/rustdesk/libs/clipboard/src/cliprdr.h new file mode 100644 index 0000000..33e3d52 --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/cliprdr.h @@ -0,0 +1,247 @@ +#ifndef WF_CLIPRDR_H__ +#define WF_CLIPRDR_H__ + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef signed char INT8, *PINT8; + typedef signed short INT16, *PINT16; + typedef signed int INT32, *PINT32; + typedef unsigned char UINT8, *PUINT8; + typedef unsigned short UINT16, *PUINT16; + typedef unsigned int UINT32, *PUINT32; + typedef unsigned int UINT; + typedef int BOOL; + typedef unsigned char BYTE; + +/* Clipboard Messages */ +#define DEFINE_CLIPRDR_HEADER_COMMON() \ + UINT32 connID; \ + UINT16 msgType; \ + UINT16 msgFlags; \ + UINT32 dataLen + + struct _CLIPRDR_HEADER + { + DEFINE_CLIPRDR_HEADER_COMMON(); + }; + typedef struct _CLIPRDR_HEADER CLIPRDR_HEADER; + + struct _CLIPRDR_CAPABILITY_SET + { + UINT16 capabilitySetType; + UINT16 capabilitySetLength; + }; + typedef struct _CLIPRDR_CAPABILITY_SET CLIPRDR_CAPABILITY_SET; + + struct _CLIPRDR_GENERAL_CAPABILITY_SET + { + UINT16 capabilitySetType; + UINT16 capabilitySetLength; + + UINT32 version; + UINT32 generalFlags; + }; + typedef struct _CLIPRDR_GENERAL_CAPABILITY_SET CLIPRDR_GENERAL_CAPABILITY_SET; + + struct _CLIPRDR_CAPABILITIES + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 cCapabilitiesSets; + CLIPRDR_CAPABILITY_SET *capabilitySets; + }; + typedef struct _CLIPRDR_CAPABILITIES CLIPRDR_CAPABILITIES; + + struct _CLIPRDR_MONITOR_READY + { + DEFINE_CLIPRDR_HEADER_COMMON(); + }; + typedef struct _CLIPRDR_MONITOR_READY CLIPRDR_MONITOR_READY; + + struct _CLIPRDR_TEMP_DIRECTORY + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + char szTempDir[520]; + }; + typedef struct _CLIPRDR_TEMP_DIRECTORY CLIPRDR_TEMP_DIRECTORY; + + struct _CLIPRDR_FORMAT + { + UINT32 formatId; + char *formatName; + }; + typedef struct _CLIPRDR_FORMAT CLIPRDR_FORMAT; + + struct _CLIPRDR_FORMAT_LIST + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 numFormats; + CLIPRDR_FORMAT *formats; + }; + typedef struct _CLIPRDR_FORMAT_LIST CLIPRDR_FORMAT_LIST; + + struct _CLIPRDR_FORMAT_LIST_RESPONSE + { + DEFINE_CLIPRDR_HEADER_COMMON(); + }; + typedef struct _CLIPRDR_FORMAT_LIST_RESPONSE CLIPRDR_FORMAT_LIST_RESPONSE; + + struct _CLIPRDR_LOCK_CLIPBOARD_DATA + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 clipDataId; + }; + typedef struct _CLIPRDR_LOCK_CLIPBOARD_DATA CLIPRDR_LOCK_CLIPBOARD_DATA; + + struct _CLIPRDR_UNLOCK_CLIPBOARD_DATA + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 clipDataId; + }; + typedef struct _CLIPRDR_UNLOCK_CLIPBOARD_DATA CLIPRDR_UNLOCK_CLIPBOARD_DATA; + + struct _CLIPRDR_FORMAT_DATA_REQUEST + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 requestedFormatId; + }; + typedef struct _CLIPRDR_FORMAT_DATA_REQUEST CLIPRDR_FORMAT_DATA_REQUEST; + + struct _CLIPRDR_FORMAT_DATA_RESPONSE + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + const BYTE *requestedFormatData; + }; + typedef struct _CLIPRDR_FORMAT_DATA_RESPONSE CLIPRDR_FORMAT_DATA_RESPONSE; + + struct _CLIPRDR_FILE_CONTENTS_REQUEST + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 streamId; + UINT32 listIndex; + UINT32 dwFlags; + UINT32 nPositionLow; + UINT32 nPositionHigh; + UINT32 cbRequested; + BOOL haveClipDataId; + UINT32 clipDataId; + }; + typedef struct _CLIPRDR_FILE_CONTENTS_REQUEST CLIPRDR_FILE_CONTENTS_REQUEST; + + struct _CLIPRDR_FILE_CONTENTS_RESPONSE + { + DEFINE_CLIPRDR_HEADER_COMMON(); + + UINT32 streamId; + UINT32 cbRequested; + const BYTE *requestedData; + }; + typedef struct _CLIPRDR_FILE_CONTENTS_RESPONSE CLIPRDR_FILE_CONTENTS_RESPONSE; + + typedef struct _cliprdr_client_context CliprdrClientContext; + + struct _NOTIFICATION_MESSAGE + { + // 0 - info, 1 - warning, 2 - error + UINT32 type; + char *msg; + char *details; + }; + typedef struct _NOTIFICATION_MESSAGE NOTIFICATION_MESSAGE; + + typedef UINT (*pcCliprdrServerCapabilities)(CliprdrClientContext *context, + const CLIPRDR_CAPABILITIES *capabilities); + typedef UINT (*pcCliprdrClientCapabilities)(CliprdrClientContext *context, + const CLIPRDR_CAPABILITIES *capabilities); + typedef UINT (*pcCliprdrMonitorReady)(CliprdrClientContext *context, + const CLIPRDR_MONITOR_READY *monitorReady); + typedef UINT (*pcCliprdrTempDirectory)(CliprdrClientContext *context, + const CLIPRDR_TEMP_DIRECTORY *tempDirectory); + + typedef UINT (*pcNotifyClipboardMsg)(UINT32 connID, const NOTIFICATION_MESSAGE *msg); + + typedef UINT (*pcHandleClipboardFiles)(UINT32 connID, size_t nFiles, WCHAR **fileNames); + + typedef UINT (*pcCliprdrClientFormatList)(CliprdrClientContext *context, + const CLIPRDR_FORMAT_LIST *formatList); + typedef UINT (*pcCliprdrServerFormatList)(CliprdrClientContext *context, + const CLIPRDR_FORMAT_LIST *formatList); + typedef UINT (*pcCliprdrClientFormatListResponse)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse); + typedef UINT (*pcCliprdrServerFormatListResponse)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse); + typedef UINT (*pcCliprdrClientLockClipboardData)( + CliprdrClientContext *context, const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData); + typedef UINT (*pcCliprdrServerLockClipboardData)( + CliprdrClientContext *context, const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData); + typedef UINT (*pcCliprdrClientUnlockClipboardData)( + CliprdrClientContext *context, const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData); + typedef UINT (*pcCliprdrServerUnlockClipboardData)( + CliprdrClientContext *context, const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData); + typedef UINT (*pcCliprdrClientFormatDataRequest)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest); + typedef UINT (*pcCliprdrServerFormatDataRequest)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest); + typedef UINT (*pcCliprdrClientFormatDataResponse)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse); + typedef UINT (*pcCliprdrServerFormatDataResponse)( + CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse); + typedef UINT (*pcCliprdrClientFileContentsRequest)( + CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest); + typedef UINT (*pcCliprdrServerFileContentsRequest)( + CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest); + typedef UINT (*pcCliprdrClientFileContentsResponse)( + CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse); + typedef UINT (*pcCliprdrServerFileContentsResponse)( + CliprdrClientContext *context, const CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse); + + // TODO: hide more members of clipboard context + struct _cliprdr_client_context + { + void *Custom; + BOOL EnableFiles; + BOOL EnableOthers; + + BOOL IsStopped; + UINT32 ResponseWaitTimeoutSecs; + pcCliprdrServerCapabilities ServerCapabilities; + pcCliprdrClientCapabilities ClientCapabilities; + pcCliprdrMonitorReady MonitorReady; + pcCliprdrTempDirectory TempDirectory; + pcNotifyClipboardMsg NotifyClipboardMsg; + pcHandleClipboardFiles HandleClipboardFiles; + pcCliprdrClientFormatList ClientFormatList; + pcCliprdrServerFormatList ServerFormatList; + pcCliprdrClientFormatListResponse ClientFormatListResponse; + pcCliprdrServerFormatListResponse ServerFormatListResponse; + pcCliprdrClientLockClipboardData ClientLockClipboardData; + pcCliprdrServerLockClipboardData ServerLockClipboardData; + pcCliprdrClientUnlockClipboardData ClientUnlockClipboardData; + pcCliprdrServerUnlockClipboardData ServerUnlockClipboardData; + pcCliprdrClientFormatDataRequest ClientFormatDataRequest; + pcCliprdrServerFormatDataRequest ServerFormatDataRequest; + pcCliprdrClientFormatDataResponse ClientFormatDataResponse; + pcCliprdrServerFormatDataResponse ServerFormatDataResponse; + pcCliprdrClientFileContentsRequest ClientFileContentsRequest; + pcCliprdrServerFileContentsRequest ServerFileContentsRequest; + pcCliprdrClientFileContentsResponse ClientFileContentsResponse; + pcCliprdrServerFileContentsResponse ServerFileContentsResponse; + + UINT32 LastRequestedFormatId; + }; + +#ifdef __cplusplus +} +#endif + +#endif // WF_CLIPRDR_H__ diff --git a/vendor/rustdesk/libs/clipboard/src/context_send.rs b/vendor/rustdesk/libs/clipboard/src/context_send.rs new file mode 100644 index 0000000..caa9d4a --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/context_send.rs @@ -0,0 +1,79 @@ +use hbb_common::{log, ResultType}; +use std::{ops::Deref, sync::Mutex}; + +use crate::CliprdrServiceContext; + +const CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS: u32 = 30; + +lazy_static::lazy_static! { + static ref CONTEXT_SEND: ContextSend = ContextSend::default(); +} + +#[derive(Default)] +pub struct ContextSend(Mutex>>); + +impl Deref for ContextSend { + type Target = Mutex>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ContextSend { + #[inline] + pub fn is_enabled() -> bool { + CONTEXT_SEND.lock().unwrap().is_some() + } + + pub fn set_is_stopped() { + let _res = Self::proc(|c| c.set_is_stopped().map_err(|e| e.into())); + } + + pub fn enable(enabled: bool) { + let mut lock = CONTEXT_SEND.lock().unwrap(); + if enabled { + if lock.is_some() { + return; + } + match crate::create_cliprdr_context(true, false, CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS) { + Ok(context) => { + log::info!("clipboard context for file transfer created."); + *lock = Some(context) + } + Err(err) => { + log::error!( + "create clipboard context for file transfer: {}", + err.to_string() + ); + } + } + } else if let Some(_clp) = lock.take() { + *lock = None; + log::info!("clipboard context for file transfer destroyed."); + } + } + + /// make sure the clipboard context is enabled. + pub fn make_sure_enabled() -> ResultType<()> { + let mut lock = CONTEXT_SEND.lock().unwrap(); + if lock.is_some() { + return Ok(()); + } + + let ctx = crate::create_cliprdr_context(true, false, CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS)?; + *lock = Some(ctx); + log::info!("clipboard context for file transfer recreated."); + Ok(()) + } + + pub fn proc) -> ResultType<()>>( + f: F, + ) -> ResultType<()> { + let mut lock = CONTEXT_SEND.lock().unwrap(); + match lock.as_mut() { + Some(context) => f(context), + None => Ok(()), + } + } +} diff --git a/vendor/rustdesk/libs/clipboard/src/lib.rs b/vendor/rustdesk/libs/clipboard/src/lib.rs new file mode 100644 index 0000000..5ce9afe --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/lib.rs @@ -0,0 +1,298 @@ +use std::sync::{Arc, Mutex, RwLock}; + +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] +use hbb_common::ResultType; +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] +use hbb_common::{allow_err, log}; +use hbb_common::{ + lazy_static, + tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex as TokioMutex, + }, +}; +use serde_derive::{Deserialize, Serialize}; +use thiserror::Error; + +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] +pub mod context_send; +pub mod platform; +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] +pub use context_send::*; + +#[cfg(target_os = "windows")] +const ERR_CODE_SERVER_FUNCTION_NONE: u32 = 0x00000001; +#[cfg(target_os = "windows")] +const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002; +#[cfg(target_os = "windows")] +const ERR_CODE_SEND_MSG: u32 = 0x00000003; + +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] +pub(crate) use platform::create_cliprdr_context; + +pub struct ProgressPercent { + pub percent: f64, + pub is_canceled: bool, + pub is_failed: bool, +} + +// to-do: This trait may be removed, because unix file copy paste does not need it. +/// Ability to handle Clipboard File from remote rustdesk client +/// +/// # Note +/// There actually should be 2 parts to implement a useable clipboard file service, +/// but this only contains the RPC server part. +/// The local listener and transport part is too platform specific to wrap up in typeclasses. +pub trait CliprdrServiceContext: Send + Sync { + /// set to be stopped + fn set_is_stopped(&mut self) -> Result<(), CliprdrError>; + /// clear the content on clipboard + fn empty_clipboard(&mut self, conn_id: i32) -> Result; + /// run as a server for clipboard RPC + fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>; + /// get the progress of the paste task. + fn get_progress_percent(&self) -> Option; + /// cancel the paste task. + fn cancel(&mut self); +} + +#[derive(Error, Debug)] +pub enum CliprdrError { + #[error("invalid cliprdr name")] + CliprdrName, + #[error("failed to init cliprdr")] + CliprdrInit, + #[error("cliprdr out of memory")] + CliprdrOutOfMemory, + #[error("cliprdr internal error")] + ClipboardInternalError, + #[error("cliprdr occupied")] + ClipboardOccupied, + #[error("conversion failure")] + ConversionFailure, + #[error("failure to read clipboard")] + OpenClipboard, + #[error("failure to read file metadata or content, path: {path}, err: {err}")] + FileError { path: String, err: std::io::Error }, + #[error("invalid request: {description}")] + InvalidRequest { description: String }, + #[error("common request: {description}")] + CommonError { description: String }, + #[error("unknown cliprdr error")] + Unknown(u32), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum ClipboardFile { + NotifyCallback { + r#type: String, + title: String, + text: String, + }, + MonitorReady, + FormatList { + format_list: Vec<(i32, String)>, + }, + FormatListResponse { + msg_flags: i32, + }, + FormatDataRequest { + requested_format_id: i32, + }, + FormatDataResponse { + msg_flags: i32, + format_data: Vec, + }, + FileContentsRequest { + stream_id: i32, + list_index: i32, + dw_flags: i32, + n_position_low: i32, + n_position_high: i32, + cb_requested: i32, + have_clip_data_id: bool, + clip_data_id: i32, + }, + FileContentsResponse { + msg_flags: i32, + stream_id: i32, + requested_data: Vec, + }, + TryEmpty, + Files { + files: Vec<(String, u64)>, + }, +} + +struct MsgChannel { + peer_id: String, + conn_id: i32, + #[allow(dead_code)] + sender: UnboundedSender, + receiver: Arc>>, +} + +lazy_static::lazy_static! { + static ref VEC_MSG_CHANNEL: RwLock> = Default::default(); + static ref CLIENT_CONN_ID_COUNTER: Mutex = Mutex::new(0); +} + +impl ClipboardFile { + pub fn is_stopping_allowed(&self) -> bool { + matches!( + self, + ClipboardFile::MonitorReady + | ClipboardFile::FormatList { .. } + | ClipboardFile::FormatDataRequest { .. } + ) + } + + pub fn is_beginning_message(&self) -> bool { + matches!( + self, + ClipboardFile::MonitorReady | ClipboardFile::FormatList { .. } + ) + } +} + +pub fn get_client_conn_id(peer_id: &str) -> Option { + VEC_MSG_CHANNEL + .read() + .unwrap() + .iter() + .find(|x| x.peer_id == peer_id) + .map(|x| x.conn_id) +} + +fn get_conn_id() -> i32 { + let mut lock = CLIENT_CONN_ID_COUNTER.lock().unwrap(); + *lock += 1; + *lock +} + +pub fn get_rx_cliprdr_client( + peer_id: &str, +) -> (i32, Arc>>) { + let mut lock = VEC_MSG_CHANNEL.write().unwrap(); + match lock.iter().find(|x| x.peer_id == peer_id) { + Some(msg_channel) => (msg_channel.conn_id, msg_channel.receiver.clone()), + None => { + let (sender, receiver) = unbounded_channel(); + let receiver = Arc::new(TokioMutex::new(receiver)); + let receiver2 = receiver.clone(); + let conn_id = get_conn_id(); + let msg_channel = MsgChannel { + peer_id: peer_id.to_owned(), + conn_id, + sender, + receiver, + }; + lock.push(msg_channel); + (conn_id, receiver2) + } + } +} + +pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc>> { + let mut lock = VEC_MSG_CHANNEL.write().unwrap(); + match lock.iter().find(|x| x.conn_id == conn_id) { + Some(msg_channel) => msg_channel.receiver.clone(), + None => { + let (sender, receiver) = unbounded_channel(); + let receiver = Arc::new(TokioMutex::new(receiver)); + let receiver2 = receiver.clone(); + let msg_channel = MsgChannel { + peer_id: "".to_string(), + conn_id, + sender, + receiver, + }; + lock.push(msg_channel); + receiver2 + } + } +} + +pub fn remove_channel_by_conn_id(conn_id: i32) { + let mut lock = VEC_MSG_CHANNEL.write().unwrap(); + if let Some(index) = lock.iter().position(|x| x.conn_id == conn_id) { + lock.remove(index); + } +} + +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] +#[inline] +pub fn send_data(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> { + #[cfg(target_os = "windows")] + return send_data_to_channel(conn_id, data); + #[cfg(not(target_os = "windows"))] + if conn_id == 0 { + let _ = send_data_to_all(data); + Ok(()) + } else { + send_data_to_channel(conn_id, data) + } +} + +#[inline] +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] +fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> { + if let Some(msg_channel) = VEC_MSG_CHANNEL + .read() + .unwrap() + .iter() + .find(|x| x.conn_id == conn_id) + { + msg_channel + .sender + .send(data) + .map_err(|e| CliprdrError::CommonError { + description: e.to_string(), + }) + } else { + Err(CliprdrError::InvalidRequest { + description: "conn_id not found".to_string(), + }) + } +} + +#[inline] +#[cfg(target_os = "windows")] +pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) { + // Need more tests to see if it's necessary to handle the error. + for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { + if msg_channel.conn_id != conn_id { + allow_err!(msg_channel.sender.send(data.clone())); + } + } +} + +#[inline] +#[cfg(feature = "unix-file-copy-paste")] +fn send_data_to_all(data: ClipboardFile) { + // Need more tests to see if it's necessary to handle the error. + for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { + allow_err!(msg_channel.sender.send(data.clone())); + } +} + +#[cfg(test)] +mod tests { + // #[test] + // fn test_cliprdr_run() { + // super::cliprdr_run(); + // } +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/mod.rs b/vendor/rustdesk/libs/clipboard/src/platform/mod.rs new file mode 100644 index 0000000..f54f402 --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/mod.rs @@ -0,0 +1,26 @@ +#[cfg(target_os = "windows")] +pub mod windows; +#[cfg(target_os = "windows")] +pub fn create_cliprdr_context( + enable_files: bool, + enable_others: bool, + response_wait_timeout_secs: u32, +) -> crate::ResultType> { + let boxed = + windows::create_cliprdr_context(enable_files, enable_others, response_wait_timeout_secs)? + as Box<_>; + Ok(boxed) +} + +#[cfg(feature = "unix-file-copy-paste")] +pub mod unix; + +#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))] +pub fn create_cliprdr_context( + _enable_files: bool, + _enable_others: bool, + _response_wait_timeout_secs: u32, +) -> crate::ResultType> { + let boxed = unix::macos::pasteboard_context::create_pasteboard_context()? as Box<_>; + Ok(boxed) +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/filetype.rs b/vendor/rustdesk/libs/clipboard/src/platform/unix/filetype.rs new file mode 100644 index 0000000..8436ba0 --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/unix/filetype.rs @@ -0,0 +1,188 @@ +use super::{FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_UNIX_MODE, LDAP_EPOCH_DELTA}; +use crate::CliprdrError; +use hbb_common::{ + bytes::{Buf, Bytes}, + log, +}; +use serde_derive::{Deserialize, Serialize}; +use std::{ + path::PathBuf, + time::{Duration, SystemTime}, +}; +use utf16string::WStr; + +#[cfg(target_os = "linux")] +pub type Inode = u64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum FileType { + File, + Directory, + // todo: support symlink + Symlink, +} + +/// read only permission +pub const PERM_READ: u16 = 0o444; +/// read and write permission +pub const PERM_RW: u16 = 0o644; +/// only self can read and readonly +pub const PERM_SELF_RO: u16 = 0o400; +/// rwx +pub const PERM_RWX: u16 = 0o755; +#[allow(dead_code)] +/// max length of file name +pub const MAX_NAME_LEN: usize = 255; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FileDescription { + pub conn_id: i32, + pub name: PathBuf, + pub kind: FileType, + pub atime: SystemTime, + pub last_modified: SystemTime, + pub last_metadata_changed: SystemTime, + pub creation_time: SystemTime, + pub size: u64, + pub perm: u16, +} + +impl FileDescription { + fn parse_file_descriptor( + bytes: &mut Bytes, + conn_id: i32, + ) -> Result { + let flags = bytes.get_u32_le(); + // skip reserved 32 bytes + bytes.advance(32); + let attributes = bytes.get_u32_le(); + + // in original specification, this is 16 bytes reserved + // we use the last 4 bytes to store the file mode + // skip reserved 12 bytes + bytes.advance(12); + let perm = bytes.get_u32_le() as u16; + + // last write time from 1601-01-01 00:00:00, in 100ns + let last_write_time = bytes.get_u64_le(); + // file size + let file_size_high = bytes.get_u32_le(); + let file_size_low = bytes.get_u32_le(); + // utf16 file name, double \0 terminated, in 520 bytes block + // read with another pointer, and advance the main pointer + let block = bytes.clone(); + bytes.advance(520); + + let block = &block[..520]; + let wstr = WStr::from_utf16le(block).map_err(|e| { + log::error!("cannot convert file descriptor path: {:?}", e); + CliprdrError::ConversionFailure + })?; + + let from_unix = flags & FLAGS_FD_UNIX_MODE != 0; + + let valid_attributes = flags & FLAGS_FD_ATTRIBUTES != 0; + if !valid_attributes { + return Err(CliprdrError::InvalidRequest { + description: "file description must have valid attributes".to_string(), + }); + } + + // todo: check normal, hidden, system, readonly, archive... + let directory = attributes & 0x10 != 0; + let normal = attributes == 0x80; + let hidden = attributes & 0x02 != 0; + let readonly = attributes & 0x01 != 0; + + let perm = if from_unix { + // as is + perm + // cannot set as is... + } else if normal { + PERM_RWX + } else if readonly { + PERM_READ + } else if hidden { + PERM_SELF_RO + } else if directory { + PERM_RWX + } else { + PERM_RW + }; + + let kind = if directory { + FileType::Directory + } else { + FileType::File + }; + + // to-do: use `let valid_size = flags & FLAGS_FD_SIZE != 0;` + // We use `true` to for compatibility with Windows. + // let valid_size = flags & FLAGS_FD_SIZE != 0; + let valid_size = true; + let size = if valid_size { + ((file_size_high as u64) << 32) + file_size_low as u64 + } else { + 0 + }; + + let valid_write_time = flags & FLAGS_FD_LAST_WRITE != 0; + let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA { + let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100; + let last_write_time = Duration::from_nanos(last_write_time); + SystemTime::UNIX_EPOCH + last_write_time + } else { + SystemTime::UNIX_EPOCH + }; + + let name = wstr.to_utf8().replace('\\', "/"); + let name = PathBuf::from(name.trim_end_matches('\0')); + + let desc = FileDescription { + conn_id, + name, + kind, + atime: last_modified, + last_modified, + last_metadata_changed: last_modified, + creation_time: last_modified, + size, + perm, + }; + + Ok(desc) + } + + /// parse file descriptions from a format data response PDU + /// which containing a CSPTR_FILEDESCRIPTORW indicated format data + pub fn parse_file_descriptors( + file_descriptor_pdu: Vec, + conn_id: i32, + ) -> Result, CliprdrError> { + let mut data = Bytes::from(file_descriptor_pdu); + if data.remaining() < 4 { + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with infficient length".to_string(), + }); + } + + let count = data.get_u32_le() as usize; + if data.remaining() == 0 && count == 0 { + return Ok(Vec::new()); + } + + if data.remaining() != 592 * count { + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with invalid length".to_string(), + }); + } + + let mut files = Vec::with_capacity(count); + for _ in 0..count { + let desc = Self::parse_file_descriptor(&mut data, conn_id)?; + files.push(desc); + } + + Ok(files) + } +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/fuse/cs.rs b/vendor/rustdesk/libs/clipboard/src/platform/unix/fuse/cs.rs new file mode 100644 index 0000000..fa1dea7 --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/unix/fuse/cs.rs @@ -0,0 +1,1010 @@ +//! fuse server implement +//! we use fuse to provide file readers, warping data transfer to file interfaces +//! +//! # Name encoding +//! +//! There are different collection of characters forbidden in file names on different platforms: +//! - windows: `\/:*?"<>|` +//! - macos: `:/` +//! - linux: `/` +//! +//! what makes it troublesome is windows also used '\' as path separator. +//! +//! For now, we transfer all file names with windows separators, UTF-16 encoded. +//! *Need a way to transfer file names with '\' safely*. +//! Maybe we can use URL encoded file names and '/' separators as a new standard, while keep the support to old schemes. +//! +//! # Note +//! - all files on FS should be read only, and mark the owner to be the current user +//! - any write operations, hard links, and symbolic links on the FS should be denied + +use std::{ + collections::{BTreeMap, HashMap}, + ffi::OsString, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + mpsc::{Receiver, Sender}, + Arc, + }, + time::{Duration, SystemTime}, +}; + +use fuser::{ReplyDirectory, FUSE_ROOT_ID}; +use hbb_common::log; +use parking_lot::{Condvar, Mutex}; + +use crate::{ + platform::unix::{ + filetype::{FileDescription, FileType, Inode, MAX_NAME_LEN, PERM_RWX}, + BLOCK_SIZE, + }, + send_data, ClipboardFile, CliprdrError, +}; + +/// fuse server ready retry max times +const READ_RETRY: i32 = 3; + +impl From for fuser::FileType { + fn from(value: FileType) -> Self { + match value { + FileType::File => Self::RegularFile, + FileType::Directory => Self::Directory, + FileType::Symlink => Self::Symlink, + } + } +} + +/// fuse client +/// this is a proxy to the fuse server +#[derive(Debug)] +pub struct FuseClient { + server: Arc>, +} + +impl fuser::Filesystem for FuseClient { + fn init( + &mut self, + req: &fuser::Request<'_>, + config: &mut fuser::KernelConfig, + ) -> Result<(), libc::c_int> { + let mut server = self.server.lock(); + server.init(req, config) + } + + fn lookup( + &mut self, + req: &fuser::Request<'_>, + parent: u64, + name: &std::ffi::OsStr, + reply: fuser::ReplyEntry, + ) { + let mut server = self.server.lock(); + server.lookup(req, parent, name, reply) + } + + fn opendir(&mut self, req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) { + let mut server = self.server.lock(); + server.opendir(req, ino, flags, reply) + } + + fn readdir( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + reply: fuser::ReplyDirectory, + ) { + let mut server = self.server.lock(); + server.readdir(req, ino, fh, offset, reply) + } + + fn releasedir( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + _flags: i32, + reply: fuser::ReplyEmpty, + ) { + let mut server = self.server.lock(); + server.releasedir(req, ino, fh, _flags, reply) + } + + fn open(&mut self, req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) { + let mut server = self.server.lock(); + server.open(req, ino, flags, reply) + } + + fn read( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + size: u32, + flags: i32, + lock_owner: Option, + reply: fuser::ReplyData, + ) { + let mut server = self.server.lock(); + server.read(req, ino, fh, offset, size, flags, lock_owner, reply) + } + + fn release( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + _flags: i32, + _lock_owner: Option, + _flush: bool, + reply: fuser::ReplyEmpty, + ) { + let mut server = self.server.lock(); + server.release(req, ino, fh, _flags, _lock_owner, _flush, reply) + } + + fn getattr( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: Option, + reply: fuser::ReplyAttr, + ) { + let mut server = self.server.lock(); + server.getattr(req, ino, fh, reply) + } + + fn statfs(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyStatfs) { + let mut server = self.server.lock(); + server.statfs(req, ino, reply) + } +} + +/// fuse server +/// provides a read-only file system +#[derive(Debug)] +pub(crate) struct FuseServer { + generation: AtomicU64, + files: Vec, + // file handle counter + file_handle_counter: AtomicU64, + // timeout + timeout: Duration, + // file read reply channel + rx: Receiver, +} + +impl FuseServer { + /// create a new fuse server + pub fn new(timeout: Duration) -> (Self, Sender) { + let (tx, rx) = std::sync::mpsc::channel(); + ( + Self { + generation: AtomicU64::new(0), + files: Vec::new(), + file_handle_counter: AtomicU64::new(0), + timeout, + rx, + }, + tx, + ) + } + + pub fn client(server: Arc>) -> FuseClient { + FuseClient { server } + } +} + +impl FuseServer { + pub fn load_file_list(&mut self, files: Vec) -> Result<(), CliprdrError> { + let tree = FuseNode::build_tree(files)?; + self.files = tree; + self.generation.fetch_add(1, Ordering::Relaxed); + Ok(()) + } +} + +impl fuser::Filesystem for FuseServer { + fn init( + &mut self, + _req: &fuser::Request<'_>, + _config: &mut fuser::KernelConfig, + ) -> Result<(), libc::c_int> { + if self.files.is_empty() { + // create a root file + let root = FuseNode::new_root(); + self.files.push(root); + } + Ok(()) + } + + fn lookup( + &mut self, + _req: &fuser::Request<'_>, + parent: u64, + name: &std::ffi::OsStr, + reply: fuser::ReplyEntry, + ) { + if name.len() > MAX_NAME_LEN { + log::debug!("fuse: name too long"); + reply.error(libc::ENAMETOOLONG); + return; + } + + let entries = &self.files; + + let generation = self.generation.load(Ordering::Relaxed); + + let parent_entry = match entries.get(parent as usize - 1) { + Some(f) => f, + None => { + log::error!("fuse: parent not found"); + reply.error(libc::ENOENT); + return; + } + }; + + if parent_entry.attributes.kind != FileType::Directory { + log::error!("fuse: parent is not a directory"); + reply.error(libc::ENOTDIR); + return; + } + + let children_inodes = &parent_entry.children; + + for inode in children_inodes.iter().copied() { + let child = &entries[inode as usize - 1]; + let entry_name = OsString::from(&child.name); + + if &entry_name.as_os_str() == &name { + let ttl = std::time::Duration::new(0, 0); + reply.entry(&ttl, &(&child.attributes).into(), generation); + log::debug!("fuse: found child"); + return; + } + } + // error + reply.error(libc::ENOENT); + log::debug!("fuse: child not found"); + } + + fn opendir( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + _flags: i32, + reply: fuser::ReplyOpen, + ) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: opendir: entry not found"); + return; + }; + if entry.attributes.kind != FileType::Directory { + reply.error(libc::ENOTDIR); + log::error!("fuse: opendir: entry is not a directory"); + return; + } + // in gc, deny open + if entry.marked() { + log::error!("fuse: opendir: entry is in gc"); + reply.error(libc::EBUSY); + return; + } + + let fh = self.alloc_fd(); + entry.add_handler(fh); + reply.opened(fh, 0); + } + + fn readdir( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + mut reply: ReplyDirectory, + ) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: readdir: entry not found"); + return; + }; + if !entry.have_handler(fh) { + reply.error(libc::EBADF); + log::error!("fuse: readdir: entry has no such handler"); + return; + } + if entry.attributes.kind != FileType::Directory { + reply.error(libc::ENOTDIR); + log::error!("fuse: readdir: entry is not a directory"); + return; + } + + let offset = offset as usize; + let mut entries = Vec::new(); + + let self_entry = (ino, FileType::Directory, OsString::from(".")); + entries.push(self_entry); + + if let Some(parent_inode) = entry.parent { + entries.push((parent_inode, FileType::Directory, OsString::from(".."))); + } + + for inode in entry.children.iter().copied() { + let child = &files[inode as usize - 1]; + let kind = child.attributes.kind; + let name = OsString::from(&child.name); + let child_entry = (inode, kind, name.to_owned()); + entries.push(child_entry); + } + + for (i, entry) in entries.into_iter().enumerate().skip(offset) { + if reply.add(entry.0, i as i64 + 1, entry.1.into(), entry.2) { + break; + } + } + + reply.ok(); + } + + fn releasedir( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + _flags: i32, + reply: fuser::ReplyEmpty, + ) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: releasedir: entry not found"); + return; + }; + if entry.attributes.kind != FileType::Directory { + reply.error(libc::ENOTDIR); + log::error!("fuse: releasedir: entry is not a directory"); + return; + } + if !entry.have_handler(fh) { + reply.error(libc::EBADF); + log::error!("fuse: releasedir: entry has no such handler"); + return; + } + + let _ = entry.unregister_handler(fh); + reply.ok(); + } + + fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, _flags: i32, reply: fuser::ReplyOpen) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: open: entry not found"); + return; + }; + + // todo: support link file + if entry.attributes.kind != FileType::File { + reply.error(libc::ENFILE); + log::error!("fuse: open: entry is not a file"); + return; + } + + // check gc + if entry.marked() { + reply.error(libc::EBUSY); + log::error!("fuse: open: entry is in gc"); + return; + } + + let fh = self.alloc_fd(); + entry.add_handler(fh); + reply.opened(fh, 0); + } + + // todo: implement retry + fn read( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + size: u32, + _flags: i32, + _lock_owner: Option, + reply: fuser::ReplyData, + ) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: read: entry not found"); + return; + }; + if !entry.have_handler(fh) { + reply.error(libc::EBADF); + log::error!("fuse: read: entry has no such handler"); + return; + } + if entry.attributes.kind != FileType::File { + reply.error(libc::ENFILE); + log::error!("fuse: read: entry is not a file"); + return; + } + + if entry.marked() { + reply.error(libc::EBUSY); + log::error!("fuse: read: entry is in gc"); + return; + } + + let bytes = match self.read_node(entry, offset, size) { + Ok(b) => b, + Err(e) => { + log::error!("failed to read entry: {:?}", e); + reply.error(libc::EIO); + return; + } + }; + + reply.data(bytes.as_slice()); + } + + fn release( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + _flags: i32, + _lock_owner: Option, + _flush: bool, + reply: fuser::ReplyEmpty, + ) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: release: entry not found"); + return; + }; + + if entry.unregister_handler(fh).is_err() { + reply.error(libc::EBADF); + log::error!("fuse: release: entry has no such handler"); + return; + } + reply.ok(); + } + + fn getattr( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + _fh: Option, + reply: fuser::ReplyAttr, + ) { + let files = &self.files; + let Some(entry) = files.get(ino as usize - 1) else { + reply.error(libc::ENOENT); + log::error!("fuse: getattr: entry not found"); + return; + }; + + let attr = (&entry.attributes).into(); + reply.attr(&std::time::Duration::default(), &attr) + } + + fn statfs(&mut self, _req: &fuser::Request<'_>, _ino: u64, reply: fuser::ReplyStatfs) { + let mut blocks = 0; + for file in self.files.iter() { + blocks += file.attributes.size / (BLOCK_SIZE as u64) + + (file.attributes.size % (BLOCK_SIZE as u64) != 0) as u64; + } + reply.statfs(blocks, 0, 0, 0, 0, BLOCK_SIZE, 512, BLOCK_SIZE) + } +} + +impl FuseServer { + // get files and directory path right in root of FUSE fs + pub fn list_root(&self) -> Vec { + let files = &self.files; + let children = &files[0].children; + let mut paths = Vec::with_capacity(children.len()); + for inode in children.iter().copied() { + let idx = inode as usize - 1; + paths.push(PathBuf::from(&files[idx].name)); + } + paths + } + + /// allocate a new file descriptor + fn alloc_fd(&self) -> u64 { + self.file_handle_counter.fetch_add(1, Ordering::Relaxed) + } + + fn read_node( + &self, + node: &FuseNode, + offset: i64, + size: u32, + ) -> Result, std::io::Error> { + // todo: async and concurrent read, generate stream_id per request + let cb_requested = unsafe { + // convert `size` from u32 to i32 + // yet with same bit representation + std::mem::transmute::(size) + }; + + let (n_position_high, n_position_low) = + ((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32); + let request = ClipboardFile::FileContentsRequest { + stream_id: node.stream_id, + list_index: node.index as i32, + dw_flags: 2, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id: false, + clip_data_id: 0, + }; + + send_data(node.conn_id, request.clone()).map_err(|e| { + log::error!("failed to send file list to channel: {:?}", e); + std::io::Error::new(std::io::ErrorKind::Other, e) + })?; + + let mut retry_times = 0; + + // to-do: more tests needed + loop { + let reply = self.rx.recv_timeout(self.timeout).map_err(|e| { + log::error!("failed to receive file list from channel: {:?}", e); + std::io::Error::new(std::io::ErrorKind::TimedOut, e) + })?; + + match reply { + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + requested_data, + } => { + if stream_id != node.stream_id { + log::debug!("stream id mismatch, ignore"); + continue; + } + + if msg_flags & 1 == 0 { + retry_times += 1; + if retry_times > READ_RETRY { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "failure request", + )); + } + + send_data(node.conn_id, request.clone()).map_err(|e| { + log::error!("failed to send file list to channel: {:?}", e); + std::io::Error::new(std::io::ErrorKind::Other, e) + })?; + continue; + } + return Ok(requested_data); + } + _ => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "invalid reply", + )) + } + } + } + } +} +/// a node in the FUSE file tree +#[derive(Debug)] +struct FuseNode { + /// connection id + pub conn_id: i32, + + // todo: use stream_id to identify a FileContents request-reply + // instead of a whole file + /// stream id + pub stream_id: i32, + + /// file index in peer's file list + /// NOTE: + /// it is NOT the same as inode, this is the index in the file list + pub index: usize, + + /// parent inode + pub parent: Option, + + /// file name + pub name: String, + /// file attributes + pub attributes: InodeAttributes, + /// children inodes + pub children: Vec, + + /// marked gc + pub file_handlers: FileHandles, +} + +impl FuseNode { + pub fn from_description(inode: Inode, desc: FileDescription) -> Self { + Self { + conn_id: desc.conn_id, + stream_id: rand::random(), + index: inode as usize - 2, + name: desc + .name + .to_str() + .map(|s| s.to_string()) + .unwrap_or_default(), + parent: None, + attributes: InodeAttributes::from_description(inode, desc), + children: Vec::new(), + file_handlers: FileHandles::new(), + } + } + + pub fn new_root() -> Self { + Self { + conn_id: 0, + stream_id: rand::random(), + index: 0, + name: String::from("/"), + parent: None, + attributes: InodeAttributes::new_root(), + children: Vec::new(), + file_handlers: FileHandles::new(), + } + } + + #[allow(unused)] + pub fn is_file(&self) -> bool { + self.attributes.kind == FileType::File + } + + pub fn marked(&self) -> bool { + self.file_handlers.marked() + } + + pub fn add_handler(&self, fh: u64) { + self.file_handlers.add_handler(fh) + } + + pub fn unregister_handler(&self, fh: u64) -> Result<(), std::io::Error> { + self.file_handlers.unregister(fh) + } + + pub fn have_handler(&self, fh: u64) -> bool { + self.file_handlers.have_handler(fh) + } + + /// add a child inode + fn add_child(&mut self, inode: Inode) { + self.children.push(inode); + } + + /// calculate the file tree from a pre-ordered file list + /// ## implement detail: + /// - a new root entry will be prepended to the list + /// - all file names will be trimed to the last component + pub fn build_tree(files: Vec) -> Result, CliprdrError> { + // capacity set to file count + 1 (root) + let mut tree_list = Vec::with_capacity(files.len() + 1); + let root = Self::new_root(); + tree_list.push(root); + + // build the tree first + // root map, name -> inode + let mut sub_root_map = HashMap::new(); + sub_root_map.insert(Path::new("/").to_path_buf(), FUSE_ROOT_ID); + sub_root_map.insert(Path::new("").to_path_buf(), FUSE_ROOT_ID); + + for file in files.into_iter() { + let name = file.name.clone(); + + let inode = tree_list.len() as u64 + FUSE_ROOT_ID; + let parent_inode = match name.parent() { + Some(parent) => sub_root_map.get(parent).copied().unwrap_or(FUSE_ROOT_ID), + None => { + // parent should be root + FUSE_ROOT_ID + } + }; + tree_list[parent_inode as usize - 1].add_child(inode); + + let base_name = name.file_name().ok_or_else(|| { + let err = std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("invalid file name {}", file.name.display()), + ); + CliprdrError::FileError { + path: file.name.to_string_lossy().to_string(), + err, + } + })?; + + let mut desc = file.clone(); + + if desc.kind == FileType::Directory { + sub_root_map.insert(desc.name.clone(), inode); + } + + desc.name = Path::new(base_name).to_path_buf(); + + let mut fuse_node = FuseNode::from_description(inode, desc); + fuse_node.parent = Some(parent_inode); + tree_list.push(fuse_node); + } + Ok(tree_list) + } +} + +#[derive(Debug, Clone)] +pub struct InodeAttributes { + inode: Inode, + size: u64, + // file reference meta + // should be the only mutable field in this struct + last_accessed: std::time::SystemTime, + last_modified: std::time::SystemTime, + last_metadata_changed: std::time::SystemTime, + creation_time: std::time::SystemTime, + kind: FileType, + perm: u16, + + // not implemented + _xattrs: BTreeMap, Vec>, +} + +impl InodeAttributes { + pub fn new(inode: u64, size: u64, perm: u16, kind: FileType) -> Self { + Self { + inode, + size, + last_accessed: std::time::SystemTime::now(), + last_modified: std::time::SystemTime::now(), + last_metadata_changed: std::time::SystemTime::now(), + creation_time: std::time::SystemTime::now(), + kind, + perm, + _xattrs: BTreeMap::new(), + } + } + + pub fn from_description(inode: u64, desc: FileDescription) -> Self { + Self { + inode, + size: desc.size, + last_modified: desc.last_modified, + last_metadata_changed: desc.last_metadata_changed, + creation_time: desc.creation_time, + last_accessed: SystemTime::now(), + kind: desc.kind, + perm: desc.perm, + + _xattrs: BTreeMap::new(), + } + } + + pub fn new_root() -> Self { + Self::new(FUSE_ROOT_ID, 0, PERM_RWX, FileType::Directory) + } + + pub fn access(&mut self) { + self.last_accessed = std::time::SystemTime::now(); + } +} + +impl From<&InodeAttributes> for fuser::FileAttr { + fn from(value: &InodeAttributes) -> Self { + let blocks = if value.size % BLOCK_SIZE as u64 == 0 { + value.size / BLOCK_SIZE as u64 + } else { + value.size / BLOCK_SIZE as u64 + 1 + }; + Self { + ino: value.inode, + size: value.size, + blocks, + atime: value.last_accessed, + mtime: value.last_modified, + ctime: value.last_metadata_changed, + crtime: value.creation_time, + kind: value.kind.into(), + + // read only + perm: value.perm, + + nlink: 1, + // set to current user + uid: unsafe { libc::getuid() }, + // set to current user + gid: unsafe { libc::getgid() }, + rdev: 0, + blksize: BLOCK_SIZE, + // todo: support macos flags + flags: 0, + } + } +} + +#[derive(Debug)] +struct FileHandles { + waiter: Condvar, + handlers: Mutex>, + gc: AtomicBool, +} + +impl FileHandles { + pub fn new() -> Self { + Self { + waiter: Condvar::new(), + // the vector in handlers is sorted, from small to big + // prove: + // - later allocated handler will be bigger than previous ones + // - new handlers will append to the end of the vector + // - dropping old handlers won't affect the ordering + handlers: Mutex::new(Vec::new()), + gc: AtomicBool::new(false), + } + } + + pub fn add_handler(&self, fh: u64) { + if self.marked() { + panic!("adding new handler to a marked ref counter"); + } + self.handlers.lock().push(fh); + } + + pub fn marked(&self) -> bool { + self.gc.load(Ordering::Relaxed) + } + + pub fn have_handler(&self, handler: u64) -> bool { + let handlers = self.handlers.lock(); + handlers.binary_search(&handler).is_ok() + } + + pub fn unregister(&self, handler: u64) -> Result<(), std::io::Error> { + let mut handlers = self.handlers.lock(); + + let Ok(idx) = handlers.binary_search(&handler) else { + let e = std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid handler"); + return Err(e); + }; + + handlers.remove(idx); + self.waiter.notify_all(); + Ok(()) + } +} + +#[cfg(test)] +mod fuse_test { + use super::*; + + // todo: more tests needed! + fn desc_gen(name: &str, kind: FileType) -> FileDescription { + FileDescription { + conn_id: 0, + name: PathBuf::from(name), + kind, + atime: SystemTime::UNIX_EPOCH, + last_modified: SystemTime::UNIX_EPOCH, + last_metadata_changed: SystemTime::UNIX_EPOCH, + creation_time: SystemTime::UNIX_EPOCH, + + size: 0, + perm: 0, + } + } + fn generate_descriptions(prefix: &str) -> Vec { + let (d0_path, f0_path, f1_path, d1_path, f2_path, f3_path) = if prefix.is_empty() { + ( + "folder0".to_string(), + "folder0/file0".to_string(), + "folder0/file1".to_string(), + "folder1".to_string(), + "folder1/file2".to_string(), + "folder1/📄3".to_string(), + ) + } else { + ( + format!("{}/folder0", prefix), + format!("{}/folder0/file0", prefix), + format!("{}/folder0/file1", prefix), + format!("{}/folder1", prefix), + format!("{}/folder1/file2", prefix), + format!("{}/folder1/📄3", prefix), + ) + }; + let folder0 = desc_gen(&d0_path, FileType::Directory); + let file0 = desc_gen(&f0_path, FileType::File); + let file1 = desc_gen(&f1_path, FileType::File); + let folder1 = desc_gen(&d1_path, FileType::Directory); + let file2 = desc_gen(&f2_path, FileType::File); + let file3 = desc_gen(&f3_path, FileType::File); + + vec![folder0, file0, file1, folder1, file2, file3] + } + + fn build_tree(prefix: &str) { + let source_list = generate_descriptions(prefix); + + let build_res = FuseNode::build_tree(source_list); + assert!(build_res.is_ok()); + let tree_list = build_res.unwrap(); + + assert_eq!(tree_list.len(), 7); + + assert_eq!(tree_list[0].name, "/"); + assert_eq!(tree_list[1].name, "folder0"); + assert_eq!(tree_list[2].name, "file0"); + assert_eq!(tree_list[3].name, "file1"); + assert_eq!(tree_list[4].name, "folder1"); + assert_eq!(tree_list[5].name, "file2"); + assert_eq!(tree_list[6].name, "📄3"); + + assert_eq!(tree_list[0].children, vec![2, 5]); + assert_eq!(tree_list[1].children, vec![3, 4]); + assert!(tree_list[2].children.is_empty()); + assert!(tree_list[3].children.is_empty()); + assert_eq!(tree_list[4].children, vec![6, 7]); + assert!(tree_list[5].children.is_empty()); + assert!(tree_list[6].children.is_empty()); + + for (idx, node) in tree_list.iter().skip(1).enumerate() { + assert_eq!(idx, node.index) + } + } + + fn build_single_file(prefix: &str) { + let raw_name = "simple_test_file.txt"; + let f_name = if prefix == "" { + raw_name.to_string() + } else { + prefix.to_string() + "/" + raw_name + }; + let desc = desc_gen(&f_name, FileType::File); + let tree = FuseNode::build_tree(vec![desc]).unwrap(); + + assert_eq!(tree.len(), 2); + assert_eq!(tree[0].name, "/"); + assert_eq!(tree[0].children, vec![2]); + + assert_eq!(tree[1].name, raw_name); + assert_eq!(tree[1].index, 0); + assert_eq!(tree[1].attributes.kind, FileType::File); + } + + #[test] + fn test_parse_single() { + build_single_file(""); + build_single_file("/"); + build_single_file("test"); + build_single_file("/test"); + build_single_file("🗂"); + build_single_file("/🗂"); + } + + #[test] + fn test_parse_tree() { + build_tree(""); + build_tree("/"); + build_tree("test"); + build_tree("/test"); + build_tree("/test/test"); + build_tree("🗂"); + build_tree("/🗂"); + build_tree("🗂/test"); + } +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/fuse/mod.rs b/vendor/rustdesk/libs/clipboard/src/platform/unix/fuse/mod.rs new file mode 100644 index 0000000..df74300 --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/unix/fuse/mod.rs @@ -0,0 +1,225 @@ +mod cs; + +use super::filetype::FileDescription; +use crate::{ClipboardFile, CliprdrError}; +use cs::FuseServer; +use fuser::MountOption; +use hbb_common::{config::APP_NAME, log}; +use parking_lot::Mutex; +use std::{ + path::PathBuf, + sync::{mpsc::Sender, Arc}, + time::Duration, +}; + +lazy_static::lazy_static! { + static ref FUSE_MOUNT_POINT_CLIENT: Arc = { + let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client"); + // No need to run `canonicalize()` here. + Arc::new(mnt_path) + }; + + static ref FUSE_MOUNT_POINT_SERVER: Arc = { + let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server"); + // No need to run `canonicalize()` here. + Arc::new(mnt_path) + }; + + static ref FUSE_CONTEXT_CLIENT: Arc>> = Arc::new(Mutex::new(None)); + static ref FUSE_CONTEXT_SERVER: Arc>> = Arc::new(Mutex::new(None)); +} + +static FUSE_TIMEOUT: Duration = Duration::from_secs(3); + +pub fn get_exclude_paths(is_client: bool) -> Arc { + if is_client { + FUSE_MOUNT_POINT_CLIENT.clone() + } else { + FUSE_MOUNT_POINT_SERVER.clone() + } +} + +pub fn is_fuse_context_inited(is_client: bool) -> bool { + if is_client { + FUSE_CONTEXT_CLIENT.lock().is_some() + } else { + FUSE_CONTEXT_SERVER.lock().is_some() + } +} + +pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> { + let mut fuse_context_lock = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + if fuse_context_lock.is_some() { + return Ok(()); + } + let mount_point = if is_client { + FUSE_MOUNT_POINT_CLIENT.clone() + } else { + FUSE_MOUNT_POINT_SERVER.clone() + }; + + let mount_point = std::path::PathBuf::from(&*mount_point); + let (server, tx) = FuseServer::new(FUSE_TIMEOUT); + let server = Arc::new(Mutex::new(server)); + + prepare_fuse_mount_point(&mount_point); + let mnt_opts = [ + MountOption::FSName("rustdesk-cliprdr-fs".to_string()), + MountOption::NoAtime, + MountOption::RO, + ]; + log::info!("mounting clipboard FUSE to {}", mount_point.display()); + // to-do: ignore the error if the mount point is already mounted + // Because the sciter version uses separate processes as the controlling side. + let session = fuser::spawn_mount2( + FuseServer::client(server.clone()), + mount_point.clone(), + &mnt_opts, + ) + .map_err(|e| { + log::error!("failed to mount cliprdr fuse: {:?}", e); + CliprdrError::CliprdrInit + })?; + let session = Mutex::new(Some(session)); + + let ctx = FuseContext { + server, + tx, + mount_point, + session, + conn_id: 0, + }; + *fuse_context_lock = Some(ctx); + Ok(()) +} + +pub fn uninit_fuse_context(is_client: bool) { + uninit_fuse_context_(is_client) +} + +pub fn format_data_response_to_urls( + is_client: bool, + format_data: Vec, + conn_id: i32, +) -> Result, CliprdrError> { + let mut ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_mut() + .ok_or(CliprdrError::CliprdrInit)? + .format_data_response_to_urls(format_data, conn_id) +} + +pub fn handle_file_content_response( + is_client: bool, + clip: ClipboardFile, +) -> Result<(), CliprdrError> { + // we don't know its corresponding request, no resend can be performed + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_ref() + .ok_or(CliprdrError::CliprdrInit)? + .tx + .send(clip) + .map_err(|e| { + log::error!("failed to send file contents response to fuse: {:?}", e); + CliprdrError::ClipboardInternalError + })?; + Ok(()) +} + +pub fn empty_local_files(is_client: bool, conn_id: i32) -> bool { + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_ref() + .map(|c| c.empty_local_files(conn_id)) + .unwrap_or(false) +} + +struct FuseContext { + server: Arc>, + tx: Sender, + mount_point: PathBuf, + // stores fuse background session handle + session: Mutex>, + // Indicates the connection ID of that set the clipboard content + conn_id: i32, +} + +// this function must be called after the main IPC is up +fn prepare_fuse_mount_point(mount_point: &PathBuf) { + use std::{ + fs::{self, Permissions}, + os::unix::prelude::PermissionsExt, + }; + + fs::create_dir(mount_point).ok(); + fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok(); + + if let Err(e) = std::process::Command::new("umount") + .arg(mount_point) + .status() + { + log::warn!("umount {:?} may fail: {:?}", mount_point, e); + } +} + +fn uninit_fuse_context_(is_client: bool) { + if is_client { + let _ = FUSE_CONTEXT_CLIENT.lock().take(); + } else { + let _ = FUSE_CONTEXT_SERVER.lock().take(); + } +} + +impl Drop for FuseContext { + fn drop(&mut self) { + self.session.lock().take().map(|s| s.join()); + log::info!("unmounting clipboard FUSE from {}", self.mount_point.display()); + } +} + +impl FuseContext { + pub fn empty_local_files(&self, conn_id: i32) -> bool { + if conn_id != 0 && self.conn_id != conn_id { + return false; + } + let mut fuse_guard = self.server.lock(); + let _ = fuse_guard.load_file_list(vec![]); + true + } + + pub fn format_data_response_to_urls( + &mut self, + format_data: Vec, + conn_id: i32, + ) -> Result, CliprdrError> { + let files = FileDescription::parse_file_descriptors(format_data, conn_id)?; + + let paths = { + let mut fuse_guard = self.server.lock(); + fuse_guard.load_file_list(files)?; + self.conn_id = conn_id; + + fuse_guard.list_root() + }; + + let prefix = self.mount_point.clone(); + Ok(paths + .into_iter() + .map(|p| prefix.join(p).to_string_lossy().to_string()) + .collect()) + } +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/local_file.rs b/vendor/rustdesk/libs/clipboard/src/platform/unix/local_file.rs new file mode 100644 index 0000000..11d62ca --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/unix/local_file.rs @@ -0,0 +1,387 @@ +use super::{BLOCK_SIZE, LDAP_EPOCH_DELTA}; +use crate::{ + platform::unix::{ + FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_PROGRESSUI, FLAGS_FD_SIZE, + FLAGS_FD_UNIX_MODE, + }, + CliprdrError, +}; +use hbb_common::{ + bytes::{BufMut, BytesMut}, + log, +}; +use std::{ + collections::HashSet, + fs::File, + io::{BufRead, BufReader, Read, Seek}, + os::unix::prelude::PermissionsExt, + path::{Path, PathBuf}, + sync::atomic::{AtomicU64, Ordering}, + time::SystemTime, +}; +use utf16string::WString; + +#[derive(Debug)] +pub(super) struct LocalFile { + pub relative_root: PathBuf, + pub path: PathBuf, + + pub handle: Option>, + pub offset: AtomicU64, + + pub name: String, + pub size: u64, + pub last_write_time: SystemTime, + pub is_dir: bool, + pub perm: u32, + pub read_only: bool, + pub hidden: bool, + pub system: bool, + pub archive: bool, + pub normal: bool, +} + +impl LocalFile { + pub fn try_open(relative_root: &Path, path: &Path) -> Result { + let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { + path: path.to_string_lossy().to_string(), + err: e, + })?; + let size = mt.len() as u64; + let is_dir = mt.is_dir(); + let read_only = mt.permissions().readonly(); + let system = false; + let hidden = path.to_string_lossy().starts_with('.'); + let archive = false; + let normal = !(is_dir || read_only || system || hidden || archive); + let last_write_time = mt.modified().unwrap_or(SystemTime::UNIX_EPOCH); + + let perm = mt.permissions().mode(); + + let name = path + .display() + .to_string() + .trim_start_matches('/') + .replace('/', "\\"); + + // NOTE: open files lazily + let handle = None; + let offset = AtomicU64::new(0); + + Ok(Self { + name, + relative_root: relative_root.to_path_buf(), + path: path.to_path_buf(), + handle, + offset, + size, + last_write_time, + is_dir, + read_only, + system, + hidden, + perm, + archive, + normal, + }) + } + pub fn as_bin(&self) -> Vec { + let mut buf = BytesMut::with_capacity(592); + + let read_only_flag = if self.read_only { 0x1 } else { 0 }; + let hidden_flag = if self.hidden { 0x2 } else { 0 }; + let system_flag = if self.system { 0x4 } else { 0 }; + let directory_flag = if self.is_dir { 0x10 } else { 0 }; + let archive_flag = if self.archive { 0x20 } else { 0 }; + let normal_flag = if self.normal { 0x80 } else { 0 }; + + let file_attributes: u32 = read_only_flag + | hidden_flag + | system_flag + | directory_flag + | archive_flag + | normal_flag; + + let win32_time = self + .last_write_time + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() as u64 + / 100 + + LDAP_EPOCH_DELTA; + + let size_high = (self.size >> 32) as u32; + let size_low = (self.size & (u32::MAX as u64)) as u32; + + let path = self + .path + .strip_prefix(&self.relative_root) + .unwrap_or(&self.path) + .to_string_lossy() + .into_owned(); + + let wstr: WString = WString::from(&path); + let name = wstr.as_bytes(); + + log::trace!( + "put file to list: name_len {}, name {}", + name.len(), + &self.name + ); + + let flags = FLAGS_FD_SIZE + | FLAGS_FD_LAST_WRITE + | FLAGS_FD_ATTRIBUTES + | FLAGS_FD_PROGRESSUI + | FLAGS_FD_UNIX_MODE; + + // flags, 4 bytes + buf.put_u32_le(flags); + // 32 bytes reserved + buf.put(&[0u8; 32][..]); + // file attributes, 4 bytes + buf.put_u32_le(file_attributes); + + // NOTE: this is not used in windows + // in the specification, this is 16 bytes reserved + // lets use the last 4 bytes to store the file mode + // + // 12 bytes reserved + buf.put(&[0u8; 12][..]); + // file permissions, 4 bytes + buf.put_u32_le(self.perm); + + // last write time, 8 bytes + buf.put_u64_le(win32_time); + // file size (high) + buf.put_u32_le(size_high); + // file size (low) + buf.put_u32_le(size_low); + // put name and padding to 520 bytes + let name_len = name.len(); + buf.put(name); + buf.put(&vec![0u8; 520 - name_len][..]); + + buf.to_vec() + } + + #[inline] + pub fn load_handle(&mut self) -> Result<(), CliprdrError> { + if !self.is_dir && self.handle.is_none() { + let handle = std::fs::File::open(&self.path).map_err(|e| CliprdrError::FileError { + path: self.path.to_string_lossy().to_string(), + err: e, + })?; + let mut reader = BufReader::with_capacity(BLOCK_SIZE as usize * 2, handle); + reader.fill_buf().map_err(|e| CliprdrError::FileError { + path: self.path.to_string_lossy().to_string(), + err: e, + })?; + self.handle = Some(reader); + }; + Ok(()) + } + + pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> { + self.load_handle()?; + + let Some(handle) = self.handle.as_mut() else { + return Err(CliprdrError::FileError { + path: self.path.to_string_lossy().to_string(), + err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle not found"), + }); + }; + + if offset != self.offset.load(Ordering::Relaxed) { + handle + .seek(std::io::SeekFrom::Start(offset)) + .map_err(|e| CliprdrError::FileError { + path: self.path.to_string_lossy().to_string(), + err: e, + })?; + } + handle + .read_exact(buf) + .map_err(|e| CliprdrError::FileError { + path: self.path.to_string_lossy().to_string(), + err: e, + })?; + let new_offset = offset + (buf.len() as u64); + self.offset.store(new_offset, Ordering::Relaxed); + + // gc file handle + if new_offset >= self.size { + self.offset.store(0, Ordering::Relaxed); + self.handle = None; + } + + Ok(()) + } +} + +pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, CliprdrError> { + fn constr_file_lst( + relative_root: &Path, + path: &Path, + file_list: &mut Vec, + visited: &mut HashSet, + ) -> Result<(), CliprdrError> { + // prevent fs loop + if visited.contains(path) { + return Ok(()); + } + visited.insert(path.to_path_buf()); + + let local_file = LocalFile::try_open(relative_root, path)?; + file_list.push(local_file); + + let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { + path: path.to_string_lossy().to_string(), + err: e, + })?; + + if mt.is_dir() { + let dir = std::fs::read_dir(path).map_err(|e| CliprdrError::FileError { + path: path.to_string_lossy().to_string(), + err: e, + })?; + for entry in dir { + let entry = entry.map_err(|e| CliprdrError::FileError { + path: path.to_string_lossy().to_string(), + err: e, + })?; + let path = entry.path(); + constr_file_lst(relative_root, &path, file_list, visited)?; + } + } + Ok(()) + } + + let mut file_list = Vec::new(); + let mut visited = HashSet::new(); + + let relative_root = paths + .first() + .ok_or(CliprdrError::InvalidRequest { + description: "empty file list".to_string(), + })? + .parent() + .ok_or(CliprdrError::InvalidRequest { + description: "empty parent".to_string(), + })? + .to_path_buf(); + for path in paths { + constr_file_lst(&relative_root, path, &mut file_list, &mut visited)?; + } + Ok(file_list) +} + +#[cfg(test)] +mod file_list_test { + use std::{path::PathBuf, sync::atomic::AtomicU64}; + + use hbb_common::bytes::{BufMut, BytesMut}; + + use crate::{platform::unix::filetype::FileDescription, CliprdrError}; + + use super::LocalFile; + + #[inline] + fn generate_tree(prefix: &str) -> Vec { + // generate a tree of local files, no handles + // - / + // |- a.txt + // |- b + // |- c.txt + #[inline] + fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile { + LocalFile { + relative_root: PathBuf::from("."), + path: PathBuf::from(path), + handle: None, + name: name.to_string(), + size: 0, + offset: AtomicU64::new(0), + last_write_time: std::time::SystemTime::UNIX_EPOCH, + read_only: false, + is_dir, + perm: 0o754, + hidden: false, + system: false, + archive: false, + normal: false, + } + } + + let p = prefix; + + let (r_path, a_path, b_path, c_path) = if !prefix.is_empty() { + ( + p.to_string(), + format!("{}/a.txt", p), + format!("{}/b", p), + format!("{}/b/c.txt", p), + ) + } else { + ( + ".".to_string(), + "a.txt".to_string(), + "b".to_string(), + "b/c.txt".to_string(), + ) + }; + + let root = generate_file(&r_path, ".", true); + let a = generate_file(&a_path, "a.txt", false); + let b = generate_file(&b_path, "b", true); + let c = generate_file(&c_path, "c.txt", false); + + vec![root, a, b, c] + } + + fn as_bin_parse_test(prefix: &str) -> Result<(), CliprdrError> { + let tree = generate_tree(prefix); + let mut pdu = BytesMut::with_capacity(4 + 592 * tree.len()); + pdu.put_u32_le(tree.len() as u32); + for file in tree { + pdu.put(file.as_bin().as_slice()); + } + + let parsed = FileDescription::parse_file_descriptors(pdu.to_vec(), 0)?; + assert_eq!(parsed.len(), 4); + + if !prefix.is_empty() { + assert_eq!(parsed[0].name.to_str().unwrap(), format!("{}", prefix)); + assert_eq!( + parsed[1].name.to_str().unwrap(), + format!("{}/a.txt", prefix) + ); + assert_eq!(parsed[2].name.to_str().unwrap(), format!("{}/b", prefix)); + assert_eq!( + parsed[3].name.to_str().unwrap(), + format!("{}/b/c.txt", prefix) + ); + } else { + assert_eq!(parsed[0].name.to_str().unwrap(), "."); + assert_eq!(parsed[1].name.to_str().unwrap(), "a.txt"); + assert_eq!(parsed[2].name.to_str().unwrap(), "b"); + assert_eq!(parsed[3].name.to_str().unwrap(), "b/c.txt"); + } + + assert!(parsed[0].perm & 0o777 == 0o754); + assert!(parsed[1].perm & 0o777 == 0o754); + assert!(parsed[2].perm & 0o777 == 0o754); + assert!(parsed[3].perm & 0o777 == 0o754); + + Ok(()) + } + + #[test] + fn test_parse_file_descriptors() -> Result<(), CliprdrError> { + as_bin_parse_test("")?; + as_bin_parse_test("/")?; + as_bin_parse_test("test")?; + as_bin_parse_test("/test")?; + Ok(()) + } +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/item_data_provider.rs b/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/item_data_provider.rs new file mode 100644 index 0000000..9503631 --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/item_data_provider.rs @@ -0,0 +1,77 @@ +use super::pasteboard_context::{PasteObserverInfo, TEMP_FILE_PREFIX}; +use objc2::{ + declare_class, msg_send_id, mutability, + rc::Id, + runtime::{NSObject, NSObjectProtocol}, + ClassType, DeclaredClass, +}; +use objc2_app_kit::{ + NSPasteboard, NSPasteboardItem, NSPasteboardItemDataProvider, NSPasteboardType, + NSPasteboardTypeFileURL, +}; +use objc2_foundation::NSString; +use std::{io::Result, sync::mpsc::Sender}; + +pub(super) struct Ivars { + task_info: PasteObserverInfo, + tx: Sender>, +} + +declare_class!( + pub(super) struct PasteboardFileUrlProvider; + + unsafe impl ClassType for PasteboardFileUrlProvider { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + const NAME: &'static str = "PasteboardFileUrlProvider"; + } + + impl DeclaredClass for PasteboardFileUrlProvider { + type Ivars = Ivars; + } + + unsafe impl NSObjectProtocol for PasteboardFileUrlProvider {} + + unsafe impl NSPasteboardItemDataProvider for PasteboardFileUrlProvider { + #[method(pasteboard:item:provideDataForType:)] + #[allow(non_snake_case)] + unsafe fn pasteboard_item_provideDataForType( + &self, + _pasteboard: Option<&NSPasteboard>, + item: &NSPasteboardItem, + r#type: &NSPasteboardType, + ) { + if r#type == NSPasteboardTypeFileURL { + let path = format!("/tmp/{}{}", TEMP_FILE_PREFIX, uuid::Uuid::new_v4().to_string()); + match std::fs::File::create(&path) { + Ok(_) => { + let url = format!("file:///{}", &path); + item.setString_forType(&NSString::from_str(&url), &NSPasteboardTypeFileURL); + let mut task_info = self.ivars().task_info.clone(); + task_info.source_path = path; + self.ivars().tx.send(Ok(task_info)).ok(); + } + Err(e) => { + self.ivars().tx.send(Err(e)).ok(); + } + } + } + } + + // #[method(pasteboardFinishedWithDataProvider:)] + // unsafe fn pasteboardFinishedWithDataProvider(&self, pasteboard: &NSPasteboard) { + // } + } + + unsafe impl PasteboardFileUrlProvider {} +); + +pub(super) fn create_pasteboard_file_url_provider( + task_info: PasteObserverInfo, + tx: Sender>, +) -> Id { + let provider = PasteboardFileUrlProvider::alloc(); + let provider = provider.set_ivars(Ivars { task_info, tx }); + let provider: Id = unsafe { msg_send_id![super(provider), init] }; + provider +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/mod.rs b/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/mod.rs new file mode 100644 index 0000000..8b114aa --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/mod.rs @@ -0,0 +1,14 @@ +mod item_data_provider; +mod paste_observer; +mod paste_task; +pub mod pasteboard_context; + +pub fn should_handle_msg(msg: &crate::ClipboardFile) -> bool { + matches!( + msg, + crate::ClipboardFile::FormatList { .. } + | crate::ClipboardFile::FormatDataResponse { .. } + | crate::ClipboardFile::FileContentsResponse { .. } + | crate::ClipboardFile::TryEmpty + ) +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/paste-files-macos.png b/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/paste-files-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..73e4e3f0b696326515b4ad7acabde96e9f66600c GIT binary patch literal 39355 zcmcG0bwCqZ`|w1Os|a4N0t!q;MO3y=@^~T z4blz2vjJE1z2BeTA2089JLl>1)OpU_pGb<4ouobqfk4O}KDZ|hfgGEGK#tz~iwJyz zBoiM1|98YxTI>!awVrkY{NtGRZHe0u2*!tW>p3y__X(p1ilz_*II+YJb3d=gzzL-%h!|e6mGPOV{zF`SZyW7O$cAPkwSk zQ82!8x_9gH&Fd?C!RPO*)JJr<7f0n3$xiFPw75^xohJwtm&j-;3a$Pu31f|wkrGpB z=VBk03sU%C9^+Q~%@bW^dMoG_Ysc%p$j-$tc|`(R2BWM#!ajoir%(4jAJrNiy?6$_ z!fcw0*gOG@9an95eOtM8hNA?YQ_YCl$wFRUt9nU2Wc6LTaKY4d5`vLaS zGkCmCJhbHW@;)e(g{5Ud?`&7czyRIY0a1S`l=KV3;RWVXZ7VA)m6dMwZA|=1zVs6Z ztPuGYN+I&Ia1-OQ%nbH$YHtnE1KypUghxb72&8E1={*)r#^WRK`PcXQdK9WfC%<>r zh_59HFSLK~LOV7*Ir+#^H&(AbRnTDzk#=@pGENwuJ}ZZxu9x~9l%;N6vR}*F(fuIg zXvLYI_OftkzWnsEO3;N!T8Kkee}8+n(hGZgK`PAaJpqbH7&;~wF!CBW#8LfJXK%gO zr&5iSwKV(Lyx7evT)f@9XbVJ;bll~31qJ%ri?r0#rKp?xYS5*&oK9jDZ1Cc>8}B$R zY|TAcDJ9g5)6#@)PSS8oR<#v)c8sm6`PMmXJ&}UN4LdhtL_C)^x9jR8B2pBabH~QU zZ1R})g~6SGT3(u-N``iexS+PDbC@1ti4e|Z&y0+WE}z*P4Kt+PRhpidNKngiI-uN4 zpBqN$gu5Tz)YSChoPJunj48^;$Gi|#MQiSNX3v0}Lv_uk$4>XJt+kFzWa^s z;>U+ULh})u_b6|pkMHVw%0|U=D=EmO{32WzN$#Z9irnBBT&FqO0{-GJzq6zS2(yff8RK%wR2%f^H!wx z$AB0CdU;DrOA;4nbbyJ2L+TX^M@RLBTl;7DW5mYx4mc*?;^zm;Pbl8Mr6466 zwReD?_&xgg^*pb*Vygd}djr>k>F#)mCTdI0Mb#=5n>#!Ofr@4A*3jO*+SLCej~4`HL59{hqXv%Kl?m{HOdH|+mR zvGx2ImSLpXkcDEM41e+zx6mC{=QdN+G$>c;c*P>G+1=bg=sU=Gj&}^=q!-;I7CUQh zVsYovnDURaDnO z@j)%(MVt-0zHZz1gxZVQ3`1|_=U84yz;gP|!iO@oWfw$hZG&e!GE^ff3rA{?CTET) zt*Wh)5!=ZwzK|pZfdL;9KE~aICMDify4)(fk&!>K>L2L(&5K^%i^bl(t0zv&E4(&5 zSGazswXKIouUE7-T|-p8)%u0D-AZlk>2HN^Lr|>+p*t2>AxFf5!nQd!m9kxU zVy4;Qc%_vBQ$`nyj{uRqNT4!J6ggmT0?G*d^kQBr(}m_h*R#f&bnIg8_6~De;UZ1% zoz|zLtrSQt*>~+}DqBD1 zN=Z_aWz52?FLMpAfl7L(-b)?x90AP`od(Vy#hU8139qk08GTe&jzK2%MjTy02zeI! z^6|%u{N$NN?C~d2w4{$JAJ^8PAtKOb+gyX*=CsSqK*6PtnlPnYJglR#Wf3U*tu+i9 z;-qu!1&DH`QB4>T{>?qMe&XFOf(S;*rr25$ehbLf?jAnKccg#yv9M_B%RIRx6MQk$Y8dIx0@R8 zzL2B<*@O`A3D4O1h>j8s*ZcxlN_6UN`SP;4;E~G^2i0F>FqN*audl4+pdpPGeZPGU z0t{`_DRp)Yy7t!xnzhnCmauH{=;B^ykpJ*@n-vNGMY)FStCBG8vT|T_eMbl7Pqm zn=;=XCRfxl_A8vH6{*`@V`s3q`sHu$i;G0xR)cK+&fGl-G|5H0x6Gj{QckQyB|tzva2{BO5Bc=aRXb+WkCDZ6{gC;x z%s9h`4IZ&RDV*J*`nN)%%<}oCKlHd|<_67a6o%HGXZ_F<*oW@17Enw&I>?+*SG(|x zp}=3MAU$tsLFV4-S!Q%`iN^B0J;g-_hkgd$Z=R!QcPq5gwPnp~MHd!v!F_2h9#?V` zFk92L$Ct?RiW++2b*xi{FJd1>`mG%gp*4hMx@TZjwrTIHd-GCN>2vrNvzzPba$Xox z)%O7E=me%wN(!{`dJIZKbBQPkY0XVxA94Ckl^IuvcvV!V@&dMHV#Z@Br*iYXzDV7J zSC@;-t_i9Z`Ar&#YFTE${h0i^#Pin37d8VsZAM&}XMOXR^J{BsKWDOju$uV3Hrs58 zE@5SMJRLGI*o!r7hR2uG3)K!6U!T&V`fkWyl;FQ4-E>^Kz_-`w`F8cl<2iOM6?rL! z({)(}c1Ycd2gYg}sY7X3hw2h$q8NL!99y_boev3pj4LM()A_^n0n;qi=&xax-L9d;^{n!E9aO-KWLDcd$mJ; zy+$)gnqyV_OLQ%46#6vt@x|?|nK8}4G(@@GT@K#d5T-@%&b+3;jDip9blD_s5$?H^ zbsRl$0sYeM2lj9q*u#fbi|O_CJiGYekFVNRDHLd>la7viEj1@7GycqpOV|@N;VP0M}38~>6Va58ah}!K6 z((_*J?5{+IwXk8e-}Ot<9o83OR}mI>Iog-Fu0Jx6-8^H+S#mF{ck5(=;JXs8cLv;Z zFJra>9|1pI`_5C~Nb0}~Nlt_uHW8QJ-Q7LO+)p~!vF6#5qWN}l^1Z#u->L#JsRG|q zd){W4`{m73#lm&EG;zmkE-F2Bm>Fyn@0Pmxn-pCDg~GgQtmtY(hm3F?c*rJaf76@w zmD(PwE9R@X9Q8cAPQjClWK}9ST=0z|<3gL~&f{));@IZAq#cx7mGC}4_zbYf@UIo& zTA98m3CZ%w3I5bYf5A9O@^_?Sl_PH#JC!}&wgk{U$v*9Rf;mLt3%jjCC7i{8O)c4s zgmZy0NI%(5*)`0wMyxwIoAXNX?(|nb)x`CmWQ$Q3IH23=&)b(?SIlTcR^DuIdA_{` zbEW6trD{)8>$CImmDcqOJ*&Bh6&Jr+!S+@@0ln1@1PKjFRQo$2hnfZRvyN>BR?zA3mBVE!n*q?XAiO0X12DhDT1>a#(oev6?%9#_ z%J~MFI@Ea3!Li3dh7?Q;43f#O4dlu->4m5iQ)-^uSe83I7h|M-w_rQZB}f(qGOk}=-2z|XqG0mmg<5=neD%)>{Fj|uP{#h` znVDDoE1_d0a3D46Psk%5*#VPGw1AQTez<&uo1n08J1!Gs1ACBZ4Udq1s|c6Z8%3#p z*XcNxmbAC)0)E#?DpQj(eQ3dFyl5yzt^&uwRSsn9`s0Np>SY(k^AL@kfc#)T*|T7a zSTss?4Zwnuemn!z5jt>1s+4KVV+)9VI z@h^}Pkbw;6<4u;);V+XY)ssDE6M61fUQXHNmu8{pChO^{#fuRpvBb`T@6O7LIO zIf^IB!;^nKwgZF3V%rWOhlnYO5Q81jX4sX@?Ua<1?QQ$bxuavP6a+q7@W~tpz@`2W zTWy)%J)@?g@_9WFBt-K4c>hhiY@sg|AGzFyO1j(ub)Zc*( zGXHGf=~CYuq$>FW{Oo}k3E+X{=^k0u>4;npiCnL@ERNPrA>WtTz@NFt5Vvu(I^*12EdypVtLc5OXJeBpGCtreP^!y{-B!G(WrB&$_ zfa{G50Q#`v(9(av(#!azDyph-k-x9bLi?Wrph-?BSQmxb;LpomkI)nNoyHe{Er}S_ z*k_<6w`4|kK;;8KWeTvwu6q$~yc>#IOwo+&?nk%O;t(oLkP06{784!-~|4qlN$&;h^(EaAUo zhA)5QK_!(P2LB}+LRqm108jCoQ}fV48Q)6eLLlVh76rgC@%c0Yq@!JRWx#8aoW3|7 z%>Z#aIvjZln4%W|my8#xrG>@l4;=)=DHAvoPJlWce;WFpv32zI16ru%+d%VN{>I;kl1oz)&7YH`Ob(6GwB#cLNg#;3*sn*;rmJVBr^mIJ5$fvka_@mvk}K(AFG8W_1-+i0q=IA+Cknuo z2sX6`0IP3%t(F!~b*BR#M*>?g;cmo`0bFNkFq#b1n0OsldCy?idW@*?6tmfKCIeN z>Tx6Hvop$-WE?@2oR&7_;60yf@fA2pJ1$482{om`N2JSbY}9cAFRGWtFtF$A%JAF3 z*YAYJdFw`B%kpaE&Tz%Z)Ew=&=IrWhqKchyNV3&-L`e6(%LIvs?As6gmg`n-({&5&JaHi>(t7Iv2A!2R&j`d zUy-+qY@4?7V$r4EbN!-~8V-}Tq0&PMNjp0p)xGtM>U@^0?fq4Kq$+ptvQ8%T6{f4{ z?>gbJH3@8|zpfD|+V!k--PzU(ILth?1m~A#SZErLcw%gIFE^2%fi5Orv_bfte0xf> z?Y8~0d)>>?W#i`B_bfvvBCp z7WExuXbm6Hv((D9~jil0a!_S)2`(WDgap%#;hR{A#HS{VO8aBKONntDM%+VIy zyMo-aA_ECw8`~6`1+O&Jf_cqt-S#kl-V@Xj5KBTfpo}xOh--8ZU#|2E#D;v3qNXp( zr+ZU-^Rw0{FSmD>bFHGp^L(7w*3om>GS%s`T%+4hR}rVa$|5RjrA(d|+YsTTM75m1 z8njza)>=k%%T^hLg;2#I1qtUgnY1PH{YZ;SL4qkhJZVmvCEHEWtLNtAY-|^!iIY`| zt*bfi1_e8?j%$1iNmRN~l7O(g&Zh9B;#ATn4QxxqX#RW!u3&!iErV@vXHvC)XvQp8 zCBr9#?`Xv%OQv0~gjNRk&yuMqhDDmR#g^8!9S>hIZTXyZA%0p%rnWU^r^n+6D<_nB znGxTx;Y=0{K71xXlO)T5vzX8+J;NQQ-NQdR@B|x|mg-L(QgqAF3)dbIcdf6ME>xW< zEc&MIa?8R-_a~{yxPDnCT?Z*nDOK;ev>VoDS|g;|pz190pisfH3hLseSWD+v-$=D> zCV*=61{=%)4Z+Gw5h^FfPVTrobhtZjlOIVw9cXAQ|HEwNg*O zoV&Ors_Cq>;q&Ekwh;#mjNmH2%CJ~6QJJ0;!{EJwaEt!MPm+*0+nX!noomP9Fv^te z7&qvXRlX2K{ipU7y*+9n7m>Z@@eeF+nisvWFB_1_`uv1cMDW5k#YjPlVQ6uBb??0D z=b~HRI6*c9d@!!r$3Y%*f42Di*k>S) zPRy0{dzxl*yiv^GTmw&j5}R9T(V>uu=gLwrN&dJXCwL{R85$l5Gbh>e=BBz zvjN2E7+4@KU1a>$QHq~BAw4fx=7(GPr#E^VxO9+kezYeXH`jQ53t&sH z`Gkf0dwOc+7(vDM@)98g4cvs|!Ch|A>6n?B8BDN(Q9>Gp_CeOn0;-QySP@c2NY~qa zS`!F6ZtYcOBAbAHymXiokR$Oyp8U6E_eu+=l~2flBQQ#|k`>S)!wvI4*QX=%g3tcL zy_eJe0uw{{*!z#wb^REX+7s?^dnxI^e2q>Z*3SToeWJCOnL}m>evV0iTN#furfx5f z{+ISdoxP8(e`hJt0sryv1MBM34R<#DKxvkB8YuQl2gcGcIYrfR`vY1jUo1^sYL+#B zDx1b~XnoNmmqNMmc#86H=7;rEmSMO`CZ6u*<1S?&zxXy507=mipoM9`RU7^S%@366F=;eAG^1d`HJ*$RAbLqZskI zW>$4}2e-<6z1!39ebs!|Jo8XULUs$a>xHwO1yTley_f-=o7I9eAyrE&ONGq^-(QKj z2h}&|U+E>_dc4;#CD4(1LrZ$dQc62fbusVhP?aVhxL)-R&LmPSQ^qmV)+ON7;>;KgBpN(}YNjiL zLqn9X0?}qZ3%Q{dpY9mUS7}sOoZ<~&QpJ2w>T}%P7GBie@!Bc`*-T95Ug`NSH#~K> zbeS!2{^mpOD)3mU?^ywKS>jV+!z;I5)aPiVJ|`=$9h9yh3HMd+DLfsEr+kRjJB*`8 zw5lkuHgyJEc{`4zZBYmw={PJVAq#zh0Cq*2>ijs1N38Gj1kr4to7C2w&czi-)^R64 z4^~vU)g|)W-K!;K)c!_*vH(0JKYEFh%<`#88kuEWsa4U7V>JtVg*M^5|4c;=Z9MD^ zWh|&Rubc5#yKGvZGZ1QGOTli)9HNIPXUqH|o8_!=zc9O{ZoPs=D5__$GJB}tK^+g1 zECHkw$~D7xuQ(=ni66>X%0Y%8UAd|I9`iAG!~v1~xf9pLlPKq4f(=&Mt|1k%&8NQ@ zonEhyD0o7bYgN_N31?^0uzRU%;1Y53Ba%)Y)RXwJB!YLbo9a;6{?_WRmap3ztb{B) zlFk2#>bo4`Q+Ct^^JYfN_(u+UArlMS&77W(V=o!alAn}9txHyqpuIIXy2Lr)mho|T zyVLo?Dr%FbyK&$WybdX7p8^MpVpv?wEc&>^n^ZF!{qCJL&AZ&nn%8ylW&;7Ypyw)(Ae(sHUe<9hoEq<7OTeXrA_Yt|G$`FXjzJLoXG!RAV2xL8u82g8xJij<$&e1PgU_a$AWpJSR~Zg z!GWdU^W)(aU*RiYEGa{x^mTdp-EHJsiVNg#|1kkKJuYP;4JS_>7})#s2z@NkU*Evx z`RJ(D4+_w|J`f;8!uU6FK)j8pdAxrS578au(#OvX?8pBjEuiUuzY5w(6}fh9O`NtW|~=J>w^odEw9J@oGv zZ0Gf{d}Q7Dw($WG58PiF7@#8~r*vBG|5|!kW0EWRTL&1)%WTBoJMXV{er8N<;c$pV zqHj7KqM&bnK*HbDlI1L*q44hp&~&jFIK;y-(C(=LZKDGkKQ8l;6$l)H*yW8&e|(7O zk12d)dPkr%c(ttO{^K;qH<;oBvZ1w<<4W$hbc;p|-4`%m2Bzh=UIP>tsV7^EL$#SK;ST`_gr{sis^@fmX zeT43Luz`UA(5KE!w=>iK3(V*ck9XYPhx=oE{4=4?J+9Mn=a1c`df{%W%)l9#F*OpI z>4Td9(10P@8RVL-2BZMQlpm2adnwvaphM{GKlj7K-ONd#G+?&mVL7X`Y=0``!N7?*t8Zdqw}JvM2%2l;40l)u94{PUpc(i7anM9Be5dQ~pDN7q$;MaMzi|P9daht__@e$J^6rlZ*y__^g&9XEvleVN z_bmQ$n!Fj4l2#|ae!K>@@-<`h^z;@6k27@-416lgn7C9x`RU*o%v=@Ls+j`)y(``* zGG{n6se~x-p&D)+@zdle(DBPidVE>@^d8=}M4yZ!{!%me0ScU%OfNiPUIfbi0;7Iq zHDutZMcGq!1Ik2+3H-gjgS~SAhS(x`%d}nxAq$8W!Ctvx#&nF87nq{wN z{DIWDQ^#Gvz2JIoI4=D&!PpODl!-^OvarA~4kc;YvENGu!@|M{UGf`*LwtB8XEu_= zr3_3-)1LE4NnsY`4Geb!3bUbng2L^*e#}w`21a-1|;A2{lY?<_?4C59gcx& z`Nzodi+>d&Sy@YVO)ZOeO;^zZO`J-aBR-OIr6Z<-uRJkGaMK{0CVwN9ST_210YApF z_`sozyJiW}5tD0ch_s`iFZ`P_15v6M#b7I2TPobTU^a*bq-|*@#0if1mmqTJp_@xC zy-6n9P z%F4<*J1;OiW9R(MM6ybd>H>~4eR4fkO;#g)B!)uUvi<(IifZC76-EW+*~}Ep6S1O8 ztohXxbSKS>%A`uh6Qv$NI0%`CJ5Q6RgGW7uoqq?^Sqm)w_0^1@z>=u$@khewZQKt4CPMNs-sPS86M|E715BskAR>S-1^!D1_+kh2XB z9?emhx6O+-r)E_9eQ`Fo7P8(L&bHNMF6zAI7OByPgS!`LIKS)bvKqDGi14rD3HDPI zWfaI~*k}@cuG6ExjJ_xJHB3qvs<325x?U~Uuo52x>s7#E92X6D>cH)Bd3iawxf7%U z)06lhd2m9r@{pV-qtHsY*LXeUvRGC8-~2E~8W<(GN6n(5aGR*_}9PM-zS6fXtwH0nQ@?L#OR z>}*7TZ65%&lU1HGNmmahE~9!`4YPFelE!wGUrQ@GO5y@9{3$xZk27#%rp|_x6R3m^ z%zHCbr3`VVhKO^gJjO=tg@Ys3OB>7Cprf%Y8%+0j&qVpq=+WOiF@q|P-OTLxw z32X{qBKk*bwenIj)bN%APXBqP@H$t%WLlq=s0GTfIcTdjvMB=Ta&t%~!Oe?S7{MNk zOj^}2p?ZoHvQ8{FFLdq6XWX%4f|9}sq8RwDss9UKKGM%GDbG0In_4p_?+i1WP+2hb zhJL(trg;YEc4co1QKli=hhF7ny4F!V7emr@-t&}?&@(!=#XxV#1S^7F*R2uPumAex z!=1|G7c%x-sz?D`{#*M6^pf16w~&sM55v>Ym?wbIn`!oy3G zYwI7L9PKNBPaS87IvYRK+V90HbQkNfAn4wpkdz(eZFgBp$$&ILn`sA^k=Q+f$fCk0 z3vKL=r#kqMrcJt`?I!y;r{I)FqL9zY@ZVdA4!&tIGmg`)sC2!ct#hR}e#kU=h~A(l zrSE~zd+gL(aQ6U8Hhd5PeJg6bj;9~pdm0i>5)d37eztB*OExrtGj6`lZz4u1lUes= zn^jedTUIUV4JgEa-$**$0S0Un;tg^c)1Fp?L48=Eyrh)W2nEw81oF~9DM@r{+iF5rq?rbRs2<3=pAf|e8 zHJ!@I$r*NBmjC+qSTy@(?Qz7SqCBka-TxASSl9V*T{i|dK?{XeY9eXhGy( zC3_yCBpr>^zK;b1;DliJi6ErcAR6j3=q7go4B!$%N#S}cRaw)A&=?INrlASh1g)9? z)GLoa5qTEX%j9+kFooU%jhOc#8%aMkl)0z^UF*ZMk#&zNO(!~jWw!^S^_=N&+>qK$ z(0H1?q|@=Hwq4!zaw0YcYI-wds;(!+_@m?;0;fL|((b#g?DZ$YMN1Ho1Ox?p`}>0= zwwI)K&3Y8Iz#P_zJUK^-;{ox=&iwG6I3muX(HQ2W)038b{zuXg%NpwrU%;YH17$pYFaYH?RGfMu+zd|0Yy~vyfBan@hIqE7u5#-U$0f>?+s`mq5{0|z+V4LjcFv{D&#__K506oI7wWN#fx7>#y zo@8~cg8%${Gp_iR)vf;kq}}S+{ZsZ=>;R5>Ps@@aGq1{Jn*NQT_;hd_0WQl^d$tT| z&>p|#j|6rth-{S&9F%5#@U%Xr2bfK-I@1 zKygS;$wPdPj;mA}j_Zfp{J}SB=*9)33nibH`8R)~@FNn6<^9X(0#v!q^LIQU!+gcJ zoodDrS4)0Jqv1QWcKirSH1g~YUhHBpd5gQ!l<7Zy{1Dmg$o^}uR&!+fbltzkrFrHxo zn}mYzYZC(~1oHK{sH@mx0nCeO@W5QMcF^zog|TM%`$JSD(8cOn2L>D(&?Ed+ZmWWB z2x>7FY2E!(;Be&+N@!BP`VrK~t8)Bfpu^CVul8RCG7%)VU^1jp14R7+%`^rNO&UBQ z`L_g~;sB)goW#?neD`-~m(49L6{H|WKHGR)E5GmJ7YdBlz~i!jfvstjhYMn$z9U%X zG=M7||E}oYo`tz$T05MNukkFTC-WyBCVAAMPDGogx0>n513j|zsP-vPoBd$MQ=2}7 zhd>RL*}J3+;|Pw$+3(p`4)( zP;Bk`e*g%i<*6$3Lw|M#NCtL{Yl-^t-U;8IdNST&^fQzQ)adn5$bswHKFGlN{{R3C zl$YO}HglSALh-$}b1)v-!Z=X97|M4hq=oE?j75_~lUK93sLF%T z9==#4r)6MKLilsulT%te;RW19o=waX6Nru5by;t1!ZOyZma9T3XlmzzeH~6_ThZ9K zx3rIv_^R=-_mymTVcB2V! zRi2Er)%Ppr%^M8Q-sqI(Qrx)_Ri*KWl6y`?e^ih*EmRFIomb`ym0{GJ<0;Bqhc-yj z$opu_rl5xTuD|&&IZm7DUr+Tg{U`5uXpX_W1%-hPDUK2~+VU{Ts&4wVo8s9Jv8RQ~ z6>KGZjG;2xr%g}WC7ZZ_36N8Cp=vxOeOWpwOS9EmU1{50>bp~pg75zeN2r4y^(E}R zZC~#zcNx4#3_>#u1U@{DULp!H<1q8rvG*G^IL{mOUC6_#YA~aMtQPZjeN#40KV1VW z9`?xaW=X3KPN{0zq#0v^*mb$IB_j0}G|E69^oE%}pOpkY!O{lZ#GwSQs%Dus?%1a` z&b2f1Z*F^M-I~2WP@Hs8bhJ$)V6eUc^EQv+epvBs94dq-ps#ZaB^B28NGr&1D7a)h zcXwrDV*|XxK~u2dJxp>Y%Wtio&=Aw*^$F+~yVUpyG2mPXopSAz9OTd!aIyLLGq~8) zIk?#LDEQE;vSqG&sG}S4v4moJbSe#eg(B zQTcy1H#h0P&=a9$1-jQtpbNQ=0^CrnY}yMxy*o4%{8>HYDc3~Io$ld(5_NkYV7z5N zOb~PM(o80&TERQzaA7RdqU)-$1~)rbJ69SDOt05@>C5v@z#6XuP@{rNi+L*7WT7z|!{#HyNYAfCeB z=hPM6NIHBn>AiH57fdsDRnMek-b*8K;i<;#)gq0p<>3$COq8orRI9S`s>!r?B9vJc z*IHirg_$-a<_p?4tP{U*jg6DWvV+UZ`3Nysgk*GydCztH3FsiU-MD?u&@#2p?vr+H z^6ht_v;O$U@CIHftH>+|N0g>;5%0SL{U{w>Ld+P z&gY7~GCKr0kpj+N9D`P}nM*AEdUI|S^L&h2)LQcB#dkUi;)`)lz-V-NLaty=QIX*J zQ%iTs2>no&^HBJGY}dX0etB}{eoFSaEORK>wGo;W&*n|197u!vEz5O`ApwvCP*!Qk7 zupG9*P3CFo#6Zr))<#REbrCPd>W|2jgR7V}MG2qB0x*=~d00+@%J=%EW7o0l+YZ9j z@_t4vJA}@&DjQDOE>ggLVH78Y)#k=>e3ypS=hw<`3-t61DTSxw8%BKPy4 zeMOVf{E1d7DKj>)Rrt;3X`baRjZwepz?w@BGRjV~MAL(Yh@qWZ2rB#a!L1Xo_x)(e z_-Ka7`Dwnmz%~8n2{e`p{TSTJ6V}o9OQt;Ci$OxD4@0)w2vVZFwX=jaN16D;3+BBe zt&Brvwa3*=gm@fC4B78t=Z2WQ5l@>i3B0G`M>UjT?Bt_^=OYDKtS`qkw^@o|m2Mv> zIczxiAu4?7o*){EEmfWv3I@$NKBdRmI|R+r&UN5$GV7|ivubSDMuvyAwXQh_tqV*J zN0jHGB&c-yz#ALGc)k|E*7q++Tx4%7@>SG9_S1V0`&AJ@1jAQ+3Q1X;1c)lL9ZfH@ zFc`X$mo{Cb6*1z}hIXmV=dGlq6>O!s`Ks!#vDSndJq531o7_D}?qE}?L6(H|$7e~` zVU+dNQ0!;q_{nAr81lB606mnuKTNO}X=y*Ja|zD48k4cpCCSp^Asr`}2CkzN)tVO&^8bb|}Ny(E>l zidVFl+a~H_0}A;grN_8-XQvqE#naW*1yuVpA`{v@w+fFyZK&!kS$4_m+i6LAe|bQv za83T|L`TooyiTkByX$iktqRW7Y&{eDQ22?_+D4iLo6DmVnvEEFa0O;0egYh81CTW( z_ZWL^B`21TNBxaD4{|rYZVb57*$8^|sxFQ3P5iFcWZ2}f3C}SqVcDGydI$yj)aW>1kRlp-skfL^V!`B{xzM6P_U7O?O`ThX8F~bQ2<1em20xdtw zr^Q7L#?h0mPGmW*v+l;KMYRI7UpnzJ-3VfC8?>A&2Z=P_z9b^h=-JuXNq3uJzWa{E zOfvI)_pi5p$XZww-d%K@GTdf%SJoDm{vrY#0qrzU?ls$3$B`G{1NJciSZ{5HE~Uz)j0-6YKalCFVzb@ToH)`R;9* zS|%qa2c~U88x1a$s;p?Qw%+1SVyM($U&rXLGM)k_s2ygZt<50W?iCj#i+|AI=CW6X z;7+CG>}^ZfG4}GG86Az|a*}d96GI35jHG$;mS4(OZZQjK)+0=y-$;(S30jG55}buy z)y7^sClPV?vFzYJG(r-HR7@(D9AL79S#VLC7pPLC@-KMCQ4s}#b`8m%VaK_+xPZZY zhRF{@=eH@+#U*@@!wiZJeBd@`*{lH1VLYdW1 z|A*8%Yg$utb8{mjHfrystg$`TbQWY7($>|tCly9=AMO;r(mbgXJ&G)D+ah23eNBY?U7qd@9gYsY~-Wgsqcb@;M&t~8BPBh4c=!hUc!E#To~#? zDO@D1*U#;ih_oMBz68b%_Up1Xv7 zGSzX#8jAZ3$9p@m;ms(9^kYEQiO`=h~aP_*I9xfSRbgzh7ZJ^yhY3$ul{; zOYFyhE5nk&BacT7LCDz~TbKgB&ydg(>D{_}*=PO@k_X@fwLb*391p%v>(3n$TGmFI zu1&#bzpIv38iO^zwPOB;7RXLER8mHO-^8L%mj+$oyb|vT4q$Av;Nr^G-o_@80Rkx8 z8(c^Rq>Vh>?d+gMn~E*j2(DYKEx2p$FZL?}rt{lk&|m~T?&V{Yx3&Qd!%yT`-^lr0 zU6za=st}Et09@fP;LdcbNbeb-eQ!7g_AaQ4fZ>Ycu7^$yxB>U}CR^9a+x>(`gEC7l z6sf8?7wH7iDle1!wT1rX4xT6r%kNXZc48~5tIJtQdna_k?|R8ORQQGCh5H*WL%XMz zhu@!>U0Vn$v0EIK5Ol6s-*nua!{#X^cF#q5`8Gcwp7c_^fqUd1Y*; zC(>`OYk8`DnTY{h3y-(UWPwrf?imZge1F~_)P%8`QYNeTL5>IdpC{iVZ}%o&-;yAW ze;@!J57k&oNF7ZA9uIZ@sMxAP3SXu^!W?bHu>H>$!ChSI}psDwU z_(xVpH1rSngh#-%E zZ^yS2z}EzMI4OQHsy#=P8c+O@T+MHPwg^1ALtY2%KZyCy&{O-vGS4V!IXf|#nWh#N zaST7)2aIlzT*3d`)6p8N9>@Nml9M8c%H%laAEw`hz^hU)v0-a#YeS6y6I1)$%u=Rd zHdRMg%_yWG@XD7`Pt)kyNdm1Pp5r4C(aw4A`wKgo*4_!n{nI>e$N z)HLL8O`K90(#2`RHj?`1EMd?DML{wqoUS?}h(v2*M}jXAdd(l%!OzPfuM0W@Y(*9( zCK)+7mA6kYA!hU{%F00XTkC^QoxwXns2&+~ssI!-4;6)my)>0)^h9&Jff$?4LbOEj zr-Jh&rJ!}of>DN#&BoD? z=`qbooe+V*Jha8~m*Q3$;h!CuYFQdvm(Il*2t;q)k zKY_5ufP?ys6k4Ehi?i>k>viU)0`}P`J_9T|A;&#*(v2yak^tPjaJfH7xXo>Fe6D4vADRwYG?Tu;u|tqk4qtkQMwv z%6XqrXqf)3?azMO$;%=c{n>-akVTz#TRwg=2kWa!_k}51X&M-9A3b(>MmYi=s{Z`> zMCzH&8Z8F|TO~{al0q)(2^~nfD@v7v!h@q(yW9*JQ~zCXO0^-!!+zWAJkyIaZMN-Ldm1y#(k@#eQS?Noy%5e%))A28e@T2xY)76~F$`zCx%s2) zD0uh^3H^u+O1{Q#HHeV1yeK>v|Z11A#bo zfg{6M<<+x}u-Bb+m@JRgwEsu_Z$6aYHIrMfb(Q{V?J8=P7}2%ch-T|oty*dQS_jM7 zc-n4%nSN8T1X*KoS%Nu1Y#^mRJ6C1_8qW5At-WVdR9VnA+Mwtth8YzQ5io$LASg&` zLSp~~1e6StlqiyOY(NGbQ5uO#MoE%wlw<>qBD7@58N?<>Ns{5#X&A#h@3+3Y?z;NJ zwd`~D*`aprs;8=+y;{PBg5!X>(-PF^mJhLQ^DAj*^AdEg*DJMiq!LoqSk;Y=Kg~9q z&}~nO!1x92H0q&Dnu1HyJj=CqY|BWStD@VxC=zQ(jrjH1^1ZS#4N>F!tEnN~C?+Nb zl;PgkE0#sNSr8B}_oSmn#hro0QZfFR82Gnev4o}SUn87dxRw4HsqY>y3N(20f`y+d zF-+0rIPxsLP8{JS?q!j5v8S85(2oDGuCDw(?ABQQ8@ZyP9OUDJXvg(r9b&1bg^8_z zka16b$HTc`?cB6b%=uZjrGaNl+OaT=fAr_2G>*&xytj8TzIAN6+!>X2nZzAl`DB{a z)Sr8aZ=L*VeQh6|$Sak%lvJwp)a@Gv1&?kfGNUXZiT@HnAt;rKH z&&|(v)H<_fbL>eL^UEGSu>8c-nAI@W$oTjl$>HX*6R910OYM}_C1lArjJdzSrhV@E zfYw1P*CxlncPh?BJ(&4M0DhWJ(-)Tmw)A@_l7~Ow_48PN;DOc9PTNxgY+rI=hUJf| z*#7co=ysV~Urfc=V7pX3B^;Ezo~6j0E@ODnENDuulc*dvvK`i(dZ&=%TxMF8GK-y_ zlr*7^?`Gn0#JusnidAmM(4}2>J5)Zb6ck`1#YFroI_im=RoJqtt6&b(y#wA;CNY^K z`x8#=d$Yn<+L)=hVJw)L#=m00Z?3(e*_|?E5YL|GsXus?)PW}wJ>A{YIX>&obh3$A zJ3GHhreb~NRAwhmUy}M6FQ=wz>6MkKL-@WpZ6M~I*ypO%Cxk`={P3*1TW2XxLJ{TV zU_TyIk2gJuZUG?F>rFo^b!P5feP57msHc;H?QQ$S8L=HuC5)wg))p2S5Ge#Th66Q* zcDyj>`}pj`;#D55VSBW8a5=U3!#ySPpb&smpu`NJ0tqfH82DKD z?9(9);}f{1BGLP-xE}hjh&qE&3bhUww_X-@Z13u7I;e3<{Nd5;K5J(vFkR9icYLw_ z^DkQc0ikQupR0X#?$^aG8Bq+9ZOOH+(`T-tP=3fydk_Gn8A zbPMji{%bt`1;?MSA@2>+gv8J6MM)fizJ><}AM6=6rD|=we*&|s0k8DN{RTbC3*mu* z*I)r2H8wWpmiJ>kb8|1xnXK4zg*^ca3x-GfmoN~u6Izxb?b`ub7K(~XAe#5N z`u+P$S*=KW$g=Z8`h>QpB`{P`ethyVlr1c#q~SCEwB1-shGkxrQLKJ1WX2|>$9NrR z?SQg*DgMRU_YontF7C4w4Hnj<)W;u_B{*vnd#fDC@g`1|SH)8Dtk>?_^dsq>?bf>Up#c8rUFQkW-8Up&r1p4448Rr=#v7-9z zWKzJu&VU7Sf6AOI2lv}oS+0WR{7j^Ywl_kfUk?=?uj*2-N>K0?AM>ZRD3s`OyGb1R z*IXcwbN;m#RDJJUYW|))`Q~){#I;NVlAK!fe2r+hEpdie^vZy9l7x=ID80q>=e5xi&c$Zv)W6|q4o0!xbp|(>-i3Co|^8=bwuBjMbg&WZ>%~RuG26Q_l z4acO;ri~oTo@O|?5dGK0{VP8T^!S_RPp%ICk`VR{iod`N&-dAM=Q$JM~=4cc;y9#1itwghj z1URgsi`+x@3mAM#39pFs4*tt3x4qLc&F-yxtYn#Gn#o}CvU5hCPtFc@MdN{BA8SPX zUKmV(m+@Q{dl^c_5Qp{R>+7(ufn!(k*v%l_(R2AJIC(vqv;0b&#@t5s#AmKotW-%6 zq&sNGW%0)i-7R(1%-x*@34&+G+EO`o_8}A^cCcrADor7~8r?ticL3K1Ty!)uwqcJf zRIrJw>CrD@N6AS>HS~HXg!bjKadDz^``OI+2IcwNOm22fRD5_Q_;QI~Jm(f3Fy8)S z+COHY3G`CGx~+kM!TH0(nkaML&cK&7xEvrSQ0Wn70yxI@%t|%{Ps!Tcx$j;~B_}tQJ8Z#mjs_$ckSnJ3 z%>1on+4ZvDbzrlNn8Pak$N_HzEJVrQlPBK1>YBJ5GRWt=8jCvRcB=(HeaA>~-+d^| zF*UaEZ{5oT3g(+ckS-5*@O^&We}?NC;|29+dVWxLpbFw6GxxcX-bZfN~U%J;c$dA#R{oNa$QB{D?R- zcmOab9Ot*&4r}O3LTC4W#&Q4iTKOAP#nD2-`1i`>O%Fh&{hx(~cRX0J+HKM-kzy+f&B4jbJ=QQEr zVU23wN4<{4T6VunO!gLj#md1QyRU@*Com8sH0F=gov?28eRMofPP;DFm z3aXL2al0G>Ij~ri5RjyPE&)<;z}rDXe-k$3s@ybry1M$&V$UH_{!(%yxX}nLyTrD< zJFe7nS4elf`$=~#EGCe0iETL+R#vlG+e_q;4#Q}`gU=V;Pk#;jGT-`w@#m=pRtz>B zYzB7Zcf?dTegdXCBMKFV9NAQlIAyrN9zlx8PzZ_ckvM}1A&_6-Z$Aj^xSx=1{rXqs zx%HwqO#Bis0Bv^21=!F55TrmVsFEvNT^ZKkQlykSX#dI744e^M7`D}geJDs(fi`n; zfCK-c4m>?>9nDTOI%uQtKapU94A6Qj~5bPzl zO}PS4SRqu0)-8VsH$m@n4BDbEgDIvE5r&w zufhDt2Lb--BYQMOw$BM~{Bu;pf;01f=E^<}gxMbp+K_di)G!ej)D&v=LZL zAec-=bzf&63dXX!_mS$IN5~IZM++uq>zV3H-RJ%5=ItdwuR@e628H6Vxwadq?Y)mi zm+Cd(A>Fi*fw)-KyXfEh`QYo*CHo(zVoFc)rzIyVIXL}Fh;L3?bhHtcpAAi~1`7=* z!2Eu;Lc6u)dCYwBdI;>cluT%000@B~roG&JmMu9sd8Sf(mM7gEJC(=^JtIGKZE#`5 z&X}om$GRsZGd)i<7|IuU1~41z(WAq=P}T>SY18_zO;b&U&9xwBvV=-jJJ=YOjDNct>NHukH{+OlJJ|5 ze&{YHh@D)f>v2-cgLh*>(J~5e@+73y% zczZ6k7gfxs#q_$Z*cp(*ddVS#v@X?Txbk3HX@7~w(X;Q*qFe6G6a`PkQS=k%C6dDN zMAwzU+5EHYrTt?8R>X`WJThI2Ulj#|XR5EpkE*uc-zU7(E9ApBt0$X*oeg#qjOWqy zH%?utQ5h)B=L?RmrxO!qIFjgK>tsf*mebH#YaGMfu-2OCK~-t`Sk2Fk83!BixaT$1@-Y z!+Nhx6(s@XQ0ZG>UtV4^>`wb}@Lv21WwSCeLf5hxQghyRsW>WpeVyL*WR`p|q2P73 z)s3a$(z>sLbLH_GM28RAhT++`jCZ6;LS%Lt3fVGSbKry1p+gHCuk%SrpXSLr1QCgi zTib?Zdy(4a0tu{N@9;Q4^(2cV`aH9ce|&Roea zF4kQob52GDsJfkq7GCN~xvl(oyzR(>X^%>gjsvylG_4oU)_3FGMf#n<)0^AQ^_4?Z zezzJrcQIB4u&d_>l_j4==&4zjFExs^s62Tf-#pS5^e`@0V5AMbU|L9sAiQ%ufQN#w zxj7Y(_tKvY4!5cHafnL08e3SLTZlRw#8c2_mD)Hr@7HKmd5%+%ysC-*e#N7MkB|~n zU3<2(v$|Sq!So#g{@Ox3a)y}*K|wgf&cmLr$Lo?$_wEx8=|a%b*Z;(RdkHY=uE@Eu z+zYf0p)!W4j}J?>pNj8c{KP1trdV0@(P=rTw)R6(P}x^zw-h#0RWk+p$HiEF?6H*m z47-4GLRvu@CkJLDS|NE{sTuVJiqDXbKx738Tu~f^!n5|~fowH7_gVqW1kRKQgK>@N zrP9bchuxJ=^{ZA3nm)Z0Q3Lg1-0*juUeQwca(tFAVbR>yKQS`=ZSCUuoHjXD!0kvD z%WM`!ZWnuWYIJlm;M^3C+F!!3DL|)e*dHbSD?U;b7-a~|V8!?%P@79q+rH6&(izss zs>eGXWo4b3ni|`sag`JXJLFkJ^Fh-TERhJ_67bh9LOMQZ5NX$<9teq{v(*Iz1&K4E zS)Xp=+z%eCAne7T9&K-dnKNNIE+Q-(hQ$d0mxFe@}4@JcRJZOk6Mihu7LpQPrC zJR>9oTy1c+eG#S13=0&Rzf-HS(d;&g+k9fokx-bh?Hd69&;s$(f*erE0lHaqn*JeW zcRkjpRWFLaq~sRV3a6S&4T!gv}Wow zwrd>=O=I|Q8IAxuHAD8{wg@=p`{}%fcB5Wj-kk9_uwPU85wD&7wdra^`flc1v93m6 zcG96dwSWfXZeXSy-7-D$z5eLZyR?3=_jz;UvE>c33)7&S9b>hZMj{SwH~?TiX*n ze-r{cp+2}ol;^U6mW#RSk)H>Ca>STX&kY4-s#=i&mMiRiV7hTLY|XR9s$ZY z_^5o$?VjAC1NP&4z}!THR8Y`G^II$A#v$QM6iRV((p8qTroK@gH(a9|e`#Q1XJdoM zrU`>h7dKL&xaC;}Efea8zpL2ILRLwWfQ^%pz8Kqf!voIfh}FuOs} z2suae8JGzY&{OOpScyxe2`De4ID^}p6&BbR1`Kk##t{ENBA9^PJchCmq~;|2g#i=@ z2~X>$zrk3mGwOLD6()t~VYZsXwKaHV#H{*&tK zZ*E36j{kfSP2cy_b0<(O>&E{wfWL4G$#&M(aU5ogt8i%V)LXQ_Ehe&tOb&7ll9URm zc)7ekB;5p#SB@+xqxA}DSJz!!T{M4Q{AD~X_W_xLeKX+*HPA6MW8flyiiV8s*zPcv zDo3Eqhy?7bO7h2=3@on0t+|!e*rOxEZQ;NQU|Wx9 zM#&q1Vrc5NG$1=AM=HidrX4h?*xtud@i&tjg+)c18bc{J8?9eA$7{(muPl`cisQ5t z{GW*zFE|c1++crsPnP{&QG_06&!Aye4AFJ=js59z!e1Wu7vl3U1!B76&5_izCZ3aCm%@I%RRYjo-H2Is&V3TAbVv8#g36Isk%^BF&AcrG(kmHC}fp79{l zuB!;2>$o;A?VDA4iL?yYzdea?GOySvpPZtb)~^=&80g9*jWfD+RY>J3e+zoN zM2M$h0{%zwFXct!;kN0}hmAT6;o9zPSBY1I2rY1#R}8l3t6}K7K+T|Z2V@eRSIFZ` zkM4!?p$<}7+N|g8d@BcuOyCpFZHG0Xiq}Lt7KT9tB}R4XMTVt-hwVIx-4o-OE$={+ zH2opiE+*%svf%9E{*oh;FU^tpbjBN!%{FM;_rwA63?0Nh>fk*X`R2T3!c+XE2j-0s zAeUR}YdQ#Eza#<74e=QGOdBqy2%lDf2OI#fGPR) z>uH~?W*xt8x0?8Lh#ZhynIDc_mmz$zq4v?B9pBlSCdS6a!3uk&jCOkdi}wl*&8}z}OJDx{)&Q7# zDKHGot_7jS>y~BOJ%4r>7B91L?g8e7L{)#4CnTBf`6PLCJHQ z&M)#oNm8aCI`oK~Q^IjBWjQnZ@#+XiF6JC9(Tz#SyMUY`$eKaei$1sm%rHRneCVLW zJ&ID&xxkzvBRg2i8>0|vma9F|mIO-b+p0(+B^4vnJ@+)6Lhv#iO{<$THIRKSg0||i zNo@yykeys$Z%%_Y%W+=UX@r~}elqYmC>reRwtTj68Wjf>TS(6&R03o~29MaXp zd+Hy4+3bwV?WX8p%b?zqJzAn1F4T|*2h2v`0AV(_&_7oMbTX)n&B?d{4!azvRXsda zv*R-_r3>4FXxmR4l)5V8;|yR4sZn2ODKA};Nxt`#QI6#L02rROoQ}Uhkq`jIbK6iu zu73LEe>rKZ&bm^!)Z-_a{G}j54*m$3T7QC(@lzYp&fLq!<1auu{zcTWYg-2`G+10* z%UZ5+9Nj`o@nR?mr3Ob_fQ)%R!VE<@#MtlayPpdvn{eU!h(&a96iMPG4Y*RLNswF( zfiw#KSXx%{;9_WkAz#$xCB)m_ZrMPIH%S6wE{}cV4c1QZkpImWIt9W|cVL?-+ye4* zfFd(%UrS@@q~ZfbF@P1p1ZY87u_~p87c}5}U|~!aq+VqO#7Oy}jcSNaD6L1Ezc-wg zA23nE^3HEp^M>bE!(k#_<|xw>n^;=@}WD*r_SQX2)gYabm*4ebp5Zz%&g8 zJEf@T9Drn0aXMRODELwc*P3&1uZS2G%9Q8FYrwccNz#HT%msWEtc`;F{LhUjuP|UJ z*qmjJ6J`U8^lwZ4#aoa1OS#gc;{1O$4Zr#n76S4y&Fz6Q75ve-n5d{Iup+Lz}6Nl6E z9uO81iZc*gW4=tXJc4!{ zXv({;SmFte11vlhRuK$1QR__4eQ~hkG|HGEGKNVz#>RA#17$7Hp)~=k4l8YEt|q$09%0TTEyYv&u5;u2Ba6DvpVsM;GjSNT4IcH z!q8NHdciTZO&hqmE`ra~ZRvz0;$SNFPiR#tZpR?*F5ZnOy7M7T^ndTY9p{(5&XFXP;Bp~dT#-b=ysvx&n@r3 zL<7T`v=T0laT`*m4tqj^QMjc^@3thQORjbkSDZFhV>f0~Cquvu&+653hsX!3%p*ui za}xvdyNB|yccJrrN$a~gFOzb60IioLL_xk;(ccdNK*1hnUrgEYS@x&^;EeQ;DJ=yA z^okJoXrp&qU7QB*a3A45^tLD}wrQt_KcFt|#HVyy_9D%b{$q$A=Sqz<-8?cf0wI=t zA_sdnIz}Pu9m_@oD#!IWYHLq;{lz>+jm`}Py+P+NP+_tr1 zo81%G4xW`!tNY9XfoH!g=D~$Cw>+3oZx3y@GdwqEj}%uU6C|<8_QdRd1`bTuUA>Tr zvUd0m;QPHO)WoLGaULx!+zx#|sPPwC;B@(8400oF?|3NW{;*0ssS=_315y!x^p7dx zYgsIt<-A3)Ob=HP5on^QB!HR=S->Z*MJx;qI*@}7AwbO1$^{3yex*0a5l!u-M)Bro z58u4O{e|#n`dnG8=!C@lqh2tyK;2AiikQ@)5ZXoVzqtw)8Ha!8-MgLoAcm9dTQz3X)webKWUz0!Mjlm<@El_f7P9F*8R zLykXXQ5N%B2CM_KxN}27F)p`kJ^Ff%yx3nm&|&YbEwsgEW&#dx`%anZz3)3ZB1Wm2 z7V6g9F)MI)17vKtIyySiuSh#J8_!=oJgl*!G-uj#DN(P57>hc)`I5JNI#Rg-c^PO& zg=fT#QW165{(bR`d&+l1@@mt^zEd#CKpXa9AKDQcyUrRrgnI!$n*H}j^{L{OiG}s^ zPKRss&#MU)&lbs12K+D*!~iMV1TLPX=_RBS@8|uzBZRvCR;8qfw_PID2H=d&Ycp+h zu6bs8VyINUS|0>;0rkjGPA`I?jP=`+M%<}=f^du=CD;&tnsr<4KNA~ZbUq4}{>H)I zK4VjK2R;KlulzRoyO8W`Fd2Ms(Y>9$MRmQDSEj@mhkF%Kp}+T{j>LO7b8n{lp8qj$ zdoG{8j>Vjfkul2R{ML3o#GkaIA*3HoWm;v%Ee$Rt@aD$JTnQi`3 zeSp_K8*-leY@9p1-#?`;3>6=W`fO+mKOjk;W8Hn~8FW z*(u0Woip=c+4Tj8@%~+~A45-CLaVWjIDGSIWw4bE!&%H5B>xa5AIvhj>%v8!_Zkz6 zHCF4n>ucL@+?~X&$;`ZHmm!J!)i$o{cjeW~x6)c2z|MWcRPYwioMPQB?>`rLmsM=F ze!hRtYppaXQIjdT@0gA79_fXq49vU8qQ_%d>#r8EBWG6PV%c2;#0%VRS$yA_g?VC` zg4H_9Lg+GZ<*i?|@qbo`bu)^-ju5f|gTnagqC2w4U(kvY@{gz5Y((g!Rh0c)!aFgU z;~EtoR^_Lko41%A4fYLmJtQ;LeV!{yyiVJIwVXN9tlQI*+&7n;m})>8OA^VgAXko8 zJ70I{0i5|?xu?uJ`D=Ia)+ptTafH)m$?BeS%cY9tRqiv(LND2d?vUr?a}UP8E6n6B zzctg-p;9%XjH}SBOif!<_`W2)Tsbi}n0D@nrD^(E`24NHIqPo+qZzacTwQBwfDKs`&G))|VMx>!F!S+NG0q*#YJk2We142Ima2izcRJr)Spr^^IRj*nma z422A0c{@1J-$!=Zyw}Ycxt@NR1XsN+{ZtP%o%p5Q@;S;j31*`Q&%CS7$Yypht8cRn z=JPMV*??!))DVr;Y{0jdnN41iVQ+EOf(y!`0@7qivUaK42vNFO9D=Hcf#2|;CX85x zbQWCg1AF+1PT7U)W)3lhPIOM=JD<5e2u>=`y}LE^(QYT=S`X{srjjK&6%Wl}w7o=N z{O;n3i3+=LE4c{=5=kJpJRTFhLNxw?YFd`fNilg%vvS|hyScs@6o1Z%?`c$eV=&~d zEsw;9WtrOsAbx%=Q0?jX%{2qIywL&KPv7Uo!mG9GO|N?9WrOk4PiUM{*YUkf->3(7 zh!KE*;2x;df0Z$PH(usZ;*pfM204nltyX-QpUTXYTKFgI5`d2T`}ttUiaTi9u?Zji zfqUZT21&Zw+0*2R0s{txBdQKbyK8xsQb)ZS{_)Gbg3)7#(7`~;d90V3WwTy})L<}v z`k#B>%Vq_6RJ&xi8)vOvEI1MJB(1f_jfLZDvQ=t$iWc9+1eflAv=(!cV|9xc-g5@i z%u)IEsF)S<7R#Kub*%kiRq}(HQ-pI1X{=V+c>{zaz4rV)5$Qoa`u#HBi|52YQv$UrXFz_GVn~u?3)|Dt>ia1&7QkmI=Pr-qEqUZjaHQ*UEZ6t-hqvb%WDW?BHrxV;mzMGqu$Ij8f8W#!- zV9n!!ONVJyrn8^1V5=wn`WdFxFT7fdjdE*U4S0)hixtY^tX29#{BXP_ zNij!ycA7m}RL(M!IrD0Gf7EE>6B&6i*Tq=NLbMF&L)ps)b?@a%qjgb(;)?h3D?=(c z0$3C@IlJBJKU4%QTU8{yLvPQH zZOw@eP!Lq1{%H4nrg3<@K=$PaW1>dlK##1h;xlrC3pf3!vgy|2*=Yfb?z`{r;ti6= z&1}p0MY5SiTp}+pcb>5FToCuffQ4pOxL_DY?Ao{e9~{)lLB&w9Gh`>4HSGrY25Or| zi$3194F*jCMY(Q4?>dwZ87EzfP>Y>a;6h zEB&-mu9BpXavKdj75i~u_yp!=#Lajs5mv94f$N*aFEo)ve6ayGt5WULEB;{f^e*4E zx9E{!KV@}J?b)pS!|4!*WSXJ=i+ArzM$$je)l?>_O^x>JzVu0kV)A@k-qTyp>PWe} znXAB*DRx}lG=IJ|)!`Dak^Tc5>4weE3^ooUKa?;efgM-5=*=GGX6gXP|WUC(yCRQycjij5~c(Nr?g!Z?PCgu0b)T@!Pgjtx__P$Vq}jEP%^j%2}dmfP(@gYfC+AhG(@oj|6?;# zOvf*?U^@Nw_`*c5$KwS$zQYQX?)>(x{Mv(Cg87?uYV}RpGfL!v@%h7P%*3UdUWyg} zn|;fQn8Vsk+C#fu{oZ@Z?AlCQ>TO`DU;S>={~!A2w6uaLw41*)c|PL3e!4Yc`u)~_ z(!3vH_WozM|37#E#Mze4|JP?!)*M=IVf7|URhm3UYYI* z_WtiuZyAEFW&dNy(ui~5pT)fK?!WK+6yM*L>=%LiZG|b1Pjl*dsQ!0B^ft<^JZ5KZ zZ_T`QG{+AQ0&bs?+E$fYP#{C9eYB|swq)6MSOyRh*!=F#sAzZiNUpz-1g}Xy-JRk# zQNR~e6rDZ^yW-RfTn>94C?j421(l;M(WG+ZYIV7@HCGh39#y;hwZTr$IZ}ZE;)--k zNA|8-{PbTbKQH|>m^WQlUp{=Jl@py8{<%7r1}@3TFv!VACBMqua*6i9LMb+MI;d+FMp* zh_|geLDU>$pqLwgzn5HxG{DRaI+T<<6i>Sg`D;vzP<={HjXYX=js`oJ<;ptDViZ)|N7$iW_@RHQm+VXVaVS()fdQ{dz5F{X= zGyd_BAKGkd{K(_Bvx=VICOutdiS>CI8Qn`h(gidfg8dgN<7;c8(2xVNSDY0QL2A^W zuQv4?zlzVJE_eQ*gKr*gEJKg)yh6HxXl2TzeRVC{Y`6JZj|JzYNW!BRJK$30pK~!W z)iX3jN_}zUdjDYBaf>7?Zp|;XtfJ`E){RqZE?Oh+M(Y{{v6 z7LHakyV0w5<9qA9h|HjIFZbgU$;Ar&^a410_oRW$p^&bd7i^5!@DV2oD&2TKQOWoF z5bT9A@Y84kMs%ud#5cj>6}QoruPc7Rmsjf6dpy@UH^>$#LZJ~dg7hL54^$1Q9D=#B zil2%F+J|g}`uEdw<4Qf(teUixGG;WI#43{vb=t4FR`lb*P8my${M-Bn{nNlGy-@iS z9z)s!Iuqx@&-w#&YiE1ok0e zf5tF2ZS*k{4M{2VNjsAp$v8uuUA0~EWr18#V{QG0=9a}rDjZ&aTvfdJWm8il^2i*y zM^*VUFBcVArqfkb5hovAUpmnmUU0iG-Y}0-R;Ybq?z;)L@c91B+3i~^C(atV zCsav__$@7qv6Dxp7~Mo~tikT2_r@We?T+nb)oV3j9xTny(iPQcVY99YbOYxDgl_a- z4N>v=99^r5Yd^i*{sPr3Wgl0O$vtWPllO9Qp~ufeCM2COTFDWy8lZ<_XI{G$=3C!! z1D`3r6rjPVKbrD97tXD<{Smzf`Z(L zZ>MoV;e1Z!jU=WS#0!ALb`*7)Lrl;MTe<#0^*t#(IkJ-71Lb?tUInHLnUGGMTqD^U zk}KTF%cH@orjtX7|XEKt*?PGlaxQNV6H%UK@Xq$gF=ts-EoA-UH;w^rCt zxy&N0J22u*17Bll8=eo-^OR(Toz2@i$3XjioyAvBUQUyq5j1F+RuhAF%X!f)`8C-h zvW}h3-lxM;Ea?hJ{GH^zE)?6~hhE8mZ>Eu2Ckqo>Qk_dJEURDpJ#j&+weSlhn5RMzkAe6e-Zaa% zlqh0}tX8V)HE%-x$hkPyyO?36D_+ek#dr7#-M(M#tl&2pudBzGr2m4oLe)yR*f0DK z>ad%Dz5xvew)Lsg?Kb?;yJ{xPGTx0C2YSp(a@+<&56HJ(5vpYpD=PUC2 zl;J>LGtFT9*eUZJ(@{hyFpse}9}Vmg6jdT`leyjwNs;asbjR1DTYCd0xv@cfEOdi) zucryix#^YcX{^U?eEi`W!5W-PygwAJ+vI$+Vb1ai6HlW~-Pb=t!WWFZd-4w}Cu_vc zm*2do`(>RuCH{0cIpAAcQ_wuT5CQM;hiZEMijGJ-~$YY?& z0ee9%@m0 zGu}-wWZmq?i~?zsXqVX8aVEhtIt~5{&ZUCC8M>xtu88ka6$uqfQHzQCgAO;cT>l0{_x46=auVok9xaW2HW#5jRZCKo+)`k5$a#8B-0e_GyW;*m4$t3 zlw!+RYte=wgW!XaJL?~woAoy|=U{%i+?@gCmX4!K<&PT9S0x+NNNX)ij80fr6dn4Y zRFop*#NyoI&!EbsIUh|8qJyBE2(%5NohW-h*?RrF2i>ctYZ=cATyG5W%BceH!Ctfg z!`}znoy1!`6O8sJ^z3`KBSBXOaF?PK=N^9s6>iOSXo}@`7lHbm>H*P$Hz8(9_stqa zjS>&(wFp@zuxWFmTdY}fUNlTpJBtehdLEm1%s+8Vc_MZ4;ym})0>Kz%6PZ${-RH@! z;D}MeH$X}q#=A41GL`M^^-xt!+0^4{YJzXOp2+3~yZyn>dRKQDCa^CxnH~6qJp;vt zL>J26nDKq-R=I7xt-$jpjWL5TTW)UA9HZb=RG#x((@=c7_&{ZwS3m|L$X;#<%D!IIA)H#C#&suFRYX|uWAu43{X)^QFi%a$8^?~ z#}=0u^u@&{+&+4`0~63Qm7<|UXRjWn&Uu!HTbbd$UzTs0)#q~7vXFW?j;?rs@>Li>?V|_9+v`Q@j)2u!kGGRJfZvpI0^5l>4VmNoLIA z8)4^OMbyr}w)Z*yL;}hSamH#l5hEHc2xp()OTogh18G^Kn;vCcBIv$G_x~kyhlpF( zgocOY%gTB}8pDp)S2s7z53~K{kI><-rc3oEuDW$pM05OlRG9s57!c(U@!LWLe^_Cg z5#qwm)*qUG^ZeNK!x208e{;bRmqT$Qpl)Mm<6G^@Fx@^$+x3suff8Vig4#z^3CI8CUsfoF-`I6Y WG^SpOPV+eOij1Vf)y&KK_x~Stl|~)_ literal 0 HcmV?d00001 diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/paste_observer.rs b/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/paste_observer.rs new file mode 100644 index 0000000..01e8b6c --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/paste_observer.rs @@ -0,0 +1,179 @@ +use super::pasteboard_context::PasteObserverInfo; +use fsevent::{self, StreamFlags}; +use hbb_common::{bail, log, ResultType}; +use std::{ + sync::{ + mpsc::{channel, Receiver, RecvTimeoutError, Sender}, + Arc, Mutex, + }, + thread, + time::Duration, +}; + +enum FseventControl { + Start, + Stop, + Exit, +} + +struct FseventThreadInfo { + tx: Sender, + handle: thread::JoinHandle<()>, +} + +pub struct PasteObserver { + exit: Arc>, + observer_info: Arc>>, + tx_handle_fsevent_thread: Option, + handle_observer_thread: Option>, +} + +impl Drop for PasteObserver { + fn drop(&mut self) { + *self.exit.lock().unwrap() = true; + if let Some(handle_observer_thread) = self.handle_observer_thread.take() { + handle_observer_thread.join().ok(); + } + if let Some(tx_handle_fsevent_thread) = self.tx_handle_fsevent_thread.take() { + tx_handle_fsevent_thread.tx.send(FseventControl::Exit).ok(); + tx_handle_fsevent_thread.handle.join().ok(); + } + } +} + +impl PasteObserver { + const OBSERVE_TIMEOUT: Duration = Duration::from_secs(30); + + pub fn new() -> Self { + Self { + exit: Arc::new(Mutex::new(false)), + observer_info: Default::default(), + tx_handle_fsevent_thread: None, + handle_observer_thread: None, + } + } + + pub fn init(&mut self, cb_pasted: fn(&PasteObserverInfo) -> ()) -> ResultType<()> { + let Some(home_dir) = dirs::home_dir() else { + bail!("No home dir is set, do not observe."); + }; + + let (tx_observer, rx_observer) = channel::(); + let handle_observer = Self::init_thread_observer( + self.exit.clone(), + self.observer_info.clone(), + rx_observer, + cb_pasted, + ); + self.handle_observer_thread = Some(handle_observer); + let (tx_control, rx_control) = channel::(); + let handle_fsevent = Self::init_thread_fsevent( + home_dir.to_string_lossy().to_string(), + tx_observer, + rx_control, + ); + self.tx_handle_fsevent_thread = Some(FseventThreadInfo { + tx: tx_control, + handle: handle_fsevent, + }); + Ok(()) + } + + #[inline] + fn get_file_from_path(path: &String) -> String { + let last_slash = path.rfind('/').or_else(|| path.rfind('\\')); + match last_slash { + Some(index) => path[index + 1..].to_string(), + None => path.clone(), + } + } + + fn init_thread_observer( + exit: Arc>, + observer_info: Arc>>, + rx_observer: Receiver, + cb_pasted: fn(&PasteObserverInfo) -> (), + ) -> thread::JoinHandle<()> { + thread::spawn(move || loop { + match rx_observer.recv_timeout(Duration::from_millis(300)) { + Ok(event) => { + if (event.flag & StreamFlags::ITEM_CREATED) != StreamFlags::NONE + && (event.flag & StreamFlags::ITEM_REMOVED) == StreamFlags::NONE + && (event.flag & StreamFlags::IS_FILE) != StreamFlags::NONE + { + let source_file = observer_info + .lock() + .unwrap() + .as_ref() + .map(|x| Self::get_file_from_path(&x.source_path)); + if let Some(source_file) = source_file { + let file = Self::get_file_from_path(&event.path); + if source_file == file { + if let Some(observer_info) = observer_info.lock().unwrap().as_mut() + { + observer_info.target_path = event.path.clone(); + cb_pasted(observer_info); + } + } + } + } + } + Err(_) => { + if *(exit.lock().unwrap()) { + break; + } + } + } + }) + } + + fn new_fsevent(home_dir: String, tx_observer: Sender) -> fsevent::FsEvent { + let mut evt = fsevent::FsEvent::new(vec![home_dir.to_string()]); + evt.observe_async(tx_observer).ok(); + evt + } + + fn init_thread_fsevent( + home_dir: String, + tx_observer: Sender, + rx_control: Receiver, + ) -> thread::JoinHandle<()> { + log::debug!("fsevent observe dir: {}", &home_dir); + thread::spawn(move || { + let mut fsevent = None; + loop { + match rx_control.recv_timeout(Self::OBSERVE_TIMEOUT) { + Ok(FseventControl::Start) => { + if fsevent.is_none() { + fsevent = + Some(Self::new_fsevent(home_dir.clone(), tx_observer.clone())); + } + } + Ok(FseventControl::Stop) | Err(RecvTimeoutError::Timeout) => { + let _ = fsevent.as_mut().map(|e| e.shutdown_observe()); + fsevent = None; + } + Ok(FseventControl::Exit) | Err(RecvTimeoutError::Disconnected) => { + break; + } + } + } + log::info!("fsevent thread exit"); + let _ = fsevent.as_mut().map(|e| e.shutdown_observe()); + }) + } + + pub fn start(&mut self, observer_info: PasteObserverInfo) { + if let Some(tx_handle_fsevent_thread) = self.tx_handle_fsevent_thread.as_ref() { + self.observer_info.lock().unwrap().replace(observer_info); + tx_handle_fsevent_thread.tx.send(FseventControl::Start).ok(); + } + } + + pub fn stop(&mut self) { + if let Some(tx_handle_fsevent_thread) = &self.tx_handle_fsevent_thread { + self.observer_info = Default::default(); + tx_handle_fsevent_thread.tx.send(FseventControl::Stop).ok(); + } + } +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/paste_task.rs b/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/paste_task.rs new file mode 100644 index 0000000..33a11ed --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/paste_task.rs @@ -0,0 +1,639 @@ +use crate::{ + platform::unix::{FileDescription, FileType, BLOCK_SIZE}, + send_data, ClipboardFile, CliprdrError, ProgressPercent, +}; +use hbb_common::{allow_err, log, tokio::time::Instant}; +use std::{ + cmp::min, + fs::{File, FileTimes}, + io::{BufWriter, Write}, + os::macos::fs::FileTimesExt, + path::{Path, PathBuf}, + sync::{ + mpsc::{Receiver, RecvTimeoutError}, + Arc, Mutex, + }, + thread, + time::{Duration, SystemTime}, +}; + +const RECV_RETRY_TIMES: usize = 3; + +const DOWNLOAD_EXTENSION: &str = "rddownload"; +const RECEIVE_WAIT_TIMEOUT: Duration = Duration::from_millis(5_000); + +// https://stackoverflow.com/a/15112784/1926020 +// "1984-01-24 08:00:00 +0000" +const TIMESTAMP_FOR_FILE_PROGRESS_COMPLETED: u64 = 443779200; +const ATTR_PROGRESS_FRACTION_COMPLETED: &str = "com.apple.progress.fractionCompleted"; + +pub struct FileContentsResponse { + pub conn_id: i32, + pub msg_flags: i32, + pub stream_id: i32, + pub requested_data: Vec, +} + +#[derive(Debug)] +struct PasteTaskProgress { + // Use list index to identify the file + // `list_index` is also used as the stream id + list_index: i32, + offset: u64, + total_size: u64, + current_size: u64, + last_sent_time: Instant, + download_file_index: i32, + download_file_size: u64, + download_file_path: String, + download_file_current_size: u64, + file_handle: Option>, + error: Option, + is_canceled: bool, +} + +struct PasteTaskHandle { + progress: PasteTaskProgress, + target_dir: PathBuf, + files: Vec, +} + +pub struct PasteTask { + exit: Arc>, + handle: Arc>>, + handle_worker: Option>, +} + +impl Drop for PasteTask { + fn drop(&mut self) { + *self.exit.lock().unwrap() = true; + if let Some(handle_worker) = self.handle_worker.take() { + handle_worker.join().ok(); + } + } +} + +impl PasteTask { + const INVALID_FILE_INDEX: i32 = -1; + + pub fn new(rx_file_contents: Receiver) -> Self { + let exit = Arc::new(Mutex::new(false)); + let handle = Arc::new(Mutex::new(None)); + let handle_worker = + Self::init_worker_thread(exit.clone(), handle.clone(), rx_file_contents); + Self { + handle, + exit, + handle_worker: Some(handle_worker), + } + } + + pub fn start(&mut self, target_dir: PathBuf, files: Vec) { + let mut task_lock = self.handle.lock().unwrap(); + if task_lock + .as_ref() + .map(|x| !x.is_finished()) + .unwrap_or(false) + { + log::error!("Previous paste task is not finished, ignore new request."); + return; + } + let total_size = files.iter().map(|f| f.size).sum(); + let mut task_handle = PasteTaskHandle { + progress: PasteTaskProgress { + list_index: -1, + offset: 0, + total_size, + current_size: 0, + last_sent_time: Instant::now(), + download_file_index: Self::INVALID_FILE_INDEX, + download_file_size: 0, + download_file_path: "".to_owned(), + download_file_current_size: 0, + file_handle: None, + error: None, + is_canceled: false, + }, + target_dir, + files, + }; + task_handle.update_next(0).ok(); + if task_handle.is_finished() { + task_handle.on_finished(); + } else { + if let Err(e) = task_handle.send_file_contents_request() { + log::error!("Failed to send file contents request, error: {}", &e); + task_handle.on_error(e); + } + } + *task_lock = Some(task_handle); + } + + pub fn cancel(&self) { + let mut task_handle = self.handle.lock().unwrap(); + if let Some(task_handle) = task_handle.as_mut() { + task_handle.progress.is_canceled = true; + task_handle.on_cancelled(); + } + } + + fn init_worker_thread( + exit: Arc>, + handle: Arc>>, + rx_file_contents: Receiver, + ) -> thread::JoinHandle<()> { + thread::spawn(move || { + let mut retry_count = 0; + loop { + if *exit.lock().unwrap() { + break; + } + + match rx_file_contents.recv_timeout(Duration::from_millis(300)) { + Ok(file_contents) => { + let mut task_lock = handle.lock().unwrap(); + let Some(task_handle) = task_lock.as_mut() else { + continue; + }; + if task_handle.is_finished() { + continue; + } + + if file_contents.stream_id != task_handle.progress.list_index { + // ignore invalid stream id + continue; + } else if file_contents.msg_flags != 0x01 { + retry_count += 1; + if retry_count > RECV_RETRY_TIMES { + task_handle.progress.error = Some(CliprdrError::InvalidRequest { + description: format!( + "Failed to read file contents, stream id: {}, msg_flags: {}", + file_contents.stream_id, + file_contents.msg_flags + ), + }); + } + } else { + let resp_list_index = file_contents.stream_id; + let Some(file) = &task_handle.files.get(resp_list_index as usize) + else { + // unreachable + // Because `task_handle.progress.list_index >= task_handle.files.len()` should always be false + log::warn!( + "Invalid response list index: {}, file length: {}", + resp_list_index, + task_handle.files.len() + ); + continue; + }; + if file.conn_id != file_contents.conn_id { + // unreachable + // We still add log here to make sure we can see the error message when it happens. + log::error!( + "Invalid response conn id: {}, expected: {}", + file_contents.conn_id, + file.conn_id + ); + continue; + } + + if let Err(e) = task_handle.handle_file_contents_response(file_contents) + { + log::error!("Failed to handle file contents response: {}", &e); + task_handle.on_error(e); + } + } + + if !task_handle.is_finished() { + if let Err(e) = task_handle.send_file_contents_request() { + log::error!("Failed to send file contents request: {}", &e); + task_handle.on_error(e); + } + } else { + retry_count = 0; + task_handle.on_finished(); + } + } + Err(RecvTimeoutError::Timeout) => { + let mut task_lock = handle.lock().unwrap(); + if let Some(task_handle) = task_lock.as_mut() { + if task_handle.check_receive_timemout() { + retry_count = 0; + task_handle.on_finished(); + } + } + } + Err(RecvTimeoutError::Disconnected) => { + break; + } + } + } + }) + } + + pub fn is_finished(&self) -> bool { + self.handle + .lock() + .unwrap() + .as_ref() + .map(|handle| handle.is_finished()) + .unwrap_or(true) + } + + pub fn progress_percent(&self) -> Option { + self.handle + .lock() + .unwrap() + .as_ref() + .map(|handle| handle.progress_percent()) + } +} + +impl PasteTaskHandle { + fn update_next(&mut self, size: u64) -> Result<(), CliprdrError> { + if self.is_finished() { + return Ok(()); + } + self.progress.current_size += size; + + let is_start = self.progress.list_index == -1; + if is_start || (self.progress.offset + size) >= self.progress.download_file_size { + if !is_start { + self.on_done(); + } + for i in (self.progress.list_index + 1)..self.files.len() as i32 { + let Some(file_desc) = self.files.get(i as usize) else { + return Err(CliprdrError::InvalidRequest { + description: format!("Invalid file index: {}", i), + }); + }; + match file_desc.kind { + FileType::File => { + if file_desc.size == 0 { + if let Some(new_file_path) = + Self::get_new_filename(&self.target_dir, file_desc) + { + if let Ok(f) = std::fs::File::create(&new_file_path) { + f.set_len(0).ok(); + Self::set_file_metadata(&f, file_desc); + } + }; + } else { + self.progress.list_index = i; + self.progress.offset = 0; + self.open_new_writer()?; + break; + } + } + FileType::Directory => { + let path = self.target_dir.join(&file_desc.name); + if !path.exists() { + std::fs::create_dir_all(path).ok(); + } + } + FileType::Symlink => { + // to-do: handle symlink + } + } + } + } else { + self.progress.offset += size; + self.progress.download_file_current_size += size; + self.update_progress_completed(None); + } + if self.progress.file_handle.is_none() { + self.progress.list_index = self.files.len() as i32; + self.progress.offset = 0; + self.progress.download_file_size = 0; + self.progress.download_file_current_size = 0; + } + Ok(()) + } + + fn start_progress_completed(&self) { + if let Some(file) = self.progress.file_handle.as_ref() { + let creation_time = + SystemTime::UNIX_EPOCH + Duration::from_secs(TIMESTAMP_FOR_FILE_PROGRESS_COMPLETED); + file.get_ref() + .set_times(FileTimes::new().set_created(creation_time)) + .ok(); + xattr::set( + &self.progress.download_file_path, + ATTR_PROGRESS_FRACTION_COMPLETED, + "0.0".as_bytes(), + ) + .ok(); + } + } + + fn update_progress_completed(&mut self, fraction_completed: Option) { + let fraction_completed = fraction_completed.unwrap_or_else(|| { + let current_size = self.progress.download_file_current_size as f64; + let total_size = self.progress.download_file_size as f64; + if total_size > 0.0 { + current_size / total_size + } else { + 1.0 + } + }); + xattr::set( + &self.progress.download_file_path, + ATTR_PROGRESS_FRACTION_COMPLETED, + &fraction_completed.to_string().as_bytes(), + ) + .ok(); + } + + #[inline] + fn remove_progress_completed(path: &str) { + if !path.is_empty() { + xattr::remove(path, ATTR_PROGRESS_FRACTION_COMPLETED).ok(); + } + } + + fn open_new_writer(&mut self) -> Result<(), CliprdrError> { + let Some(file) = &self.files.get(self.progress.list_index as usize) else { + return Err(CliprdrError::InvalidRequest { + description: format!( + "Invalid file index: {}, file count: {}", + self.progress.list_index, + self.files.len() + ), + }); + }; + + let original_file_path = self + .target_dir + .join(&file.name) + .to_string_lossy() + .to_string(); + let Some(download_file_path) = Self::get_first_filename( + format!("{}.{}", original_file_path, DOWNLOAD_EXTENSION), + file.kind, + ) else { + return Err(CliprdrError::CommonError { + description: format!("Failed to get download file path: {}", original_file_path), + }); + }; + let Some(download_path_parent) = Path::new(&download_file_path).parent() else { + return Err(CliprdrError::CommonError { + description: format!( + "Failed to get parent of the download file path: {}", + original_file_path + ), + }); + }; + if !download_path_parent.exists() { + if let Err(e) = std::fs::create_dir_all(download_path_parent) { + return Err(CliprdrError::FileError { + path: download_path_parent.to_string_lossy().to_string(), + err: e, + }); + } + } + match std::fs::File::create(&download_file_path) { + Ok(handle) => { + let writer = BufWriter::with_capacity(BLOCK_SIZE as usize * 2, handle); + self.progress.download_file_index = self.progress.list_index; + self.progress.download_file_size = file.size; + self.progress.download_file_path = download_file_path; + self.progress.download_file_current_size = 0; + self.progress.file_handle = Some(writer); + self.start_progress_completed(); + } + Err(e) => { + self.progress.error = Some(CliprdrError::FileError { + path: download_file_path, + err: e, + }); + } + }; + Ok(()) + } + + fn get_first_filename(path: String, r#type: FileType) -> Option { + let p = Path::new(&path); + if !p.exists() { + return Some(path); + } else { + for i in 1..9999999 { + let new_path = match r#type { + FileType::File => { + if let Some(ext) = p.extension() { + let new_name = format!( + "{}-{}.{}", + p.file_stem().unwrap_or_default().to_string_lossy(), + i, + ext.to_string_lossy() + ); + p.with_file_name(new_name).to_string_lossy().to_string() + } else { + format!("{} ({})", path, i) + } + } + FileType::Directory => format!("{} ({})", path, i), + FileType::Symlink => { + // to-do: handle symlink + return None; + } + }; + if !Path::new(&new_path).exists() { + return Some(new_path); + } + } + } + // unreachable + None + } + + fn progress_percent(&self) -> ProgressPercent { + let percent = self.progress.current_size as f64 / self.progress.total_size as f64; + ProgressPercent { + percent, + is_canceled: self.progress.is_canceled, + is_failed: self.progress.error.is_some(), + } + } + + fn is_finished(&self) -> bool { + self.progress.is_canceled + || self.progress.error.is_some() + || self.progress.list_index >= self.files.len() as i32 + } + + fn check_receive_timemout(&mut self) -> bool { + if !self.is_finished() { + if self.progress.last_sent_time.elapsed() > RECEIVE_WAIT_TIMEOUT { + self.progress.error = Some(CliprdrError::InvalidRequest { + description: "Failed to read file contents".to_string(), + }); + return true; + } + } + false + } + + fn on_finished(&mut self) { + if self.progress.error.is_some() { + self.on_cancelled(); + } else { + self.on_done(); + } + if self.progress.current_size != self.progress.total_size { + self.progress.error = Some(CliprdrError::InvalidRequest { + description: "Failed to download all files".to_string(), + }); + } + } + + fn on_error(&mut self, error: CliprdrError) { + self.progress.error = Some(error); + self.on_cancelled(); + } + + fn on_cancelled(&mut self) { + self.progress.file_handle = None; + std::fs::remove_file(&self.progress.download_file_path).ok(); + } + + fn on_done(&mut self) { + self.update_progress_completed(Some(1.0)); + Self::remove_progress_completed(&self.progress.download_file_path); + + let Some(file) = self.progress.file_handle.as_mut() else { + return; + }; + if self.progress.download_file_index == PasteTask::INVALID_FILE_INDEX { + return; + } + + if let Err(e) = file.flush() { + log::error!("Failed to flush file: {:?}", e); + } + self.progress.file_handle = None; + + let Some(file_desc) = self.files.get(self.progress.download_file_index as usize) else { + // unreachable + log::error!( + "Failed to get file description: {}", + self.progress.download_file_index + ); + return; + }; + let Some(rename_to_path) = Self::get_new_filename(&self.target_dir, file_desc) else { + return; + }; + match std::fs::rename(&self.progress.download_file_path, &rename_to_path) { + Ok(_) => Self::set_file_metadata2(&rename_to_path, file_desc), + Err(e) => { + log::error!("Failed to rename file: {:?}", e); + } + } + self.progress.download_file_path = "".to_owned(); + self.progress.download_file_index = PasteTask::INVALID_FILE_INDEX; + } + + fn get_new_filename(target_dir: &PathBuf, file_desc: &FileDescription) -> Option { + let mut rename_to_path = target_dir + .join(&file_desc.name) + .to_string_lossy() + .to_string(); + if Path::new(&rename_to_path).exists() { + let Some(new_path) = Self::get_first_filename(rename_to_path.clone(), file_desc.kind) + else { + log::error!("Failed to get new file name: {}", &rename_to_path); + return None; + }; + rename_to_path = new_path; + } + Some(rename_to_path) + } + + #[inline] + fn set_file_metadata(f: &File, file_desc: &FileDescription) { + let times = FileTimes::new() + .set_accessed(file_desc.atime) + .set_modified(file_desc.last_modified) + .set_created(file_desc.creation_time); + f.set_times(times).ok(); + } + + #[inline] + fn set_file_metadata2(path: &str, file_desc: &FileDescription) { + let times = FileTimes::new() + .set_accessed(file_desc.atime) + .set_modified(file_desc.last_modified) + .set_created(file_desc.creation_time); + File::options() + .write(true) + .open(path) + .map(|f| f.set_times(times)) + .ok(); + } + + fn send_file_contents_request(&mut self) -> Result<(), CliprdrError> { + if self.is_finished() { + return Ok(()); + } + + let stream_id = self.progress.list_index; + let list_index = self.progress.list_index; + let Some(file) = &self.files.get(list_index as usize) else { + // unreachable + return Err(CliprdrError::InvalidRequest { + description: format!("Invalid file index: {}", list_index), + }); + }; + let cb_requested = min(BLOCK_SIZE as u64, file.size - self.progress.offset); + let conn_id = file.conn_id; + + let (n_position_high, n_position_low) = ( + (self.progress.offset >> 32) as i32, + (self.progress.offset & (u32::MAX as u64)) as i32, + ); + let request = ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags: 2, + n_position_low, + n_position_high, + cb_requested: cb_requested as _, + have_clip_data_id: false, + clip_data_id: 0, + }; + allow_err!(send_data(conn_id, request)); + self.progress.last_sent_time = Instant::now(); + + Ok(()) + } + + fn handle_file_contents_response( + &mut self, + file_contents: FileContentsResponse, + ) -> Result<(), CliprdrError> { + if let Some(file) = self.progress.file_handle.as_mut() { + let data = file_contents.requested_data.as_slice(); + let mut write_len = 0; + while write_len < data.len() { + match file.write(&data[write_len..]) { + Ok(len) => { + write_len += len; + } + Err(e) => { + return Err(CliprdrError::FileError { + path: self.progress.download_file_path.clone(), + err: e, + }); + } + } + } + self.update_next(write_len as _)?; + } else { + return Err(CliprdrError::FileError { + path: self.progress.download_file_path.clone(), + err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle is not opened"), + }); + } + Ok(()) + } +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs b/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs new file mode 100644 index 0000000..4c74740 --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs @@ -0,0 +1,460 @@ +use super::{ + item_data_provider::create_pasteboard_file_url_provider, + paste_observer::PasteObserver, + paste_task::{FileContentsResponse, PasteTask}, +}; +use crate::{ + platform::unix::{ + filetype::FileDescription, FILECONTENTS_FORMAT_NAME, FILEDESCRIPTORW_FORMAT_NAME, + }, + send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ProgressPercent, +}; +use hbb_common::{allow_err, bail, log, ResultType}; +use objc2::{msg_send_id, rc::autoreleasepool, rc::Id, runtime::ProtocolObject, ClassType}; +use objc2_app_kit::{NSPasteboard, NSPasteboardTypeFileURL}; +use objc2_foundation::{NSArray, NSString}; +use std::{ + io, + path::Path, + sync::{ + mpsc::{channel, Receiver, RecvTimeoutError, Sender}, + Arc, Mutex, + }, + thread, + time::Duration, +}; + +lazy_static::lazy_static! { + static ref PASTE_OBSERVER_INFO: Arc>> = Default::default(); +} + +pub const TEMP_FILE_PREFIX: &str = ".rustdesk_"; + +#[derive(Default, Debug, Clone, PartialEq)] +pub(super) struct PasteObserverInfo { + pub file_descriptor_id: i32, + pub conn_id: i32, + pub source_path: String, + pub target_path: String, +} + +impl PasteObserverInfo { + fn exit_msg() -> Self { + Self::default() + } +} + +struct ContextInfo { + tx: Sender>, + handle: thread::JoinHandle<()>, +} + +pub struct PasteboardContext { + pasteboard: Id, + observer: Arc>, + tx_handle: Option, + tx_remove_file: Option>, + remove_file_handle: Option>, + tx_paste_task: Sender, + paste_task: Arc>, +} + +unsafe impl Send for PasteboardContext {} +unsafe impl Sync for PasteboardContext {} + +impl Drop for PasteboardContext { + fn drop(&mut self) { + self.observer.lock().unwrap().stop(); + if let Some(tx_handle) = self.tx_handle.take() { + if tx_handle.tx.send(Ok(PasteObserverInfo::exit_msg())).is_ok() { + tx_handle.handle.join().ok(); + } + } + } +} + +impl CliprdrServiceContext for PasteboardContext { + fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { + Ok(()) + } + + fn empty_clipboard(&mut self, conn_id: i32) -> Result { + Ok(self.empty_clipboard_(conn_id)) + } + + fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + self.server_clip_file_(conn_id, msg) + } + + fn get_progress_percent(&self) -> Option { + self.paste_task.lock().unwrap().progress_percent() + } + + fn cancel(&mut self) { + self.paste_task.lock().unwrap().cancel(); + } +} + +impl PasteboardContext { + fn init(&mut self) { + let (tx_remove_file, rx_remove_file) = channel(); + let handle_remove_file = Self::init_thread_remove_file(rx_remove_file); + self.tx_remove_file = Some(tx_remove_file.clone()); + self.remove_file_handle = Some(handle_remove_file); + + let (tx, rx) = channel(); + let observer: Arc> = self.observer.clone(); + let handle = Self::init_thread_observer(tx_remove_file, rx, observer); + self.tx_handle = Some(ContextInfo { tx, handle }); + } + + fn init_thread_observer( + tx_remove_file: Sender, + rx: Receiver>, + observer: Arc>, + ) -> thread::JoinHandle<()> { + let exit_msg = PasteObserverInfo::exit_msg(); + thread::spawn(move || loop { + match rx.recv() { + Ok(Ok(task_info)) => { + if task_info == exit_msg { + log::debug!("pasteboard item data provider: exit"); + break; + } + tx_remove_file.send(task_info.source_path.clone()).ok(); + observer.lock().unwrap().start(task_info); + } + Ok(Err(e)) => { + log::error!("pasteboard item data provider, inner error: {e}"); + } + Err(e) => { + log::error!("pasteboard item data provider, error: {e}"); + break; + } + } + }) + } + + fn init_thread_remove_file(rx: Receiver) -> thread::JoinHandle<()> { + thread::spawn(move || { + let mut cur_file: Option = None; + loop { + match rx.recv_timeout(Duration::from_secs(30)) { + Ok(path) => { + if let Some(file) = cur_file.take() { + if !file.is_empty() { + std::fs::remove_file(&file).ok(); + } + } + if !path.is_empty() { + cur_file = Some(path); + } + } + Err(e) => { + if let Some(file) = cur_file.take() { + if !file.is_empty() { + std::fs::remove_file(&file).ok(); + } + } + if e == RecvTimeoutError::Disconnected { + break; + } + } + } + } + }) + } + + // Just removing the file can also make paste option in the context menu disappear. + fn empty_clipboard_(&mut self, _conn_id: i32) -> bool { + self.tx_remove_file + .as_ref() + .map(|tx| tx.send("".to_string()).ok()); + true + } + + fn temp_files_count() -> usize { + let mut count = 0; + if let Ok(entries) = std::fs::read_dir("/tmp") { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_file() { + if let Some(file_name) = path.file_name() { + if let Some(file_name_str) = file_name.to_str() { + if file_name_str.starts_with(TEMP_FILE_PREFIX) { + count += 1; + } + } + } + } + } + } + } + count + } + + fn server_clip_file_(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + match msg { + ClipboardFile::FormatList { format_list } => { + let temp_files = Self::temp_files_count(); + if temp_files >= 3 { + // The temp files should be 0 or 1 in normal case. + // We should not continue to paste files if there are more than 3 temp files. + return Err(CliprdrError::CommonError { + description: format!( + "too many temp files, current: {}, limit: {}", + temp_files, 3 + ), + }); + } + + let task_lock = self.paste_task.lock().unwrap(); + if !task_lock.is_finished() { + return Err(CliprdrError::CommonError { + description: "previous file paste task is not finished".to_string(), + }); + } + self.handle_format_list(conn_id, format_list)?; + } + ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + } => { + self.handle_format_data_response(conn_id, msg_flags, format_data)?; + } + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + requested_data, + } => { + self.handle_file_contents_response(conn_id, msg_flags, stream_id, requested_data)?; + } + ClipboardFile::TryEmpty => self.handle_try_empty(conn_id), + _ => {} + } + Ok(()) + } + + fn handle_format_list( + &self, + conn_id: i32, + format_list: Vec<(i32, String)>, + ) -> Result<(), CliprdrError> { + if let Some(tx_handle) = self.tx_handle.as_ref() { + if !format_list + .iter() + .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) + .map(|(id, _)| *id) + .is_some() + { + return Err(CliprdrError::CommonError { + description: "no file contents format found".to_string(), + }); + }; + let Some(file_descriptor_id) = format_list + .iter() + .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) + .map(|(id, _)| *id) + else { + return Err(CliprdrError::CommonError { + description: "no file descriptor format found".to_string(), + }); + }; + + autoreleasepool(|_| self.set_clipboard_item(tx_handle, conn_id, file_descriptor_id))?; + } else { + return Err(CliprdrError::CommonError { + description: "pasteboard context is not inited".to_string(), + }); + } + Ok(()) + } + + fn set_clipboard_item( + &self, + tx_handle: &ContextInfo, + conn_id: i32, + file_descriptor_id: i32, + ) -> Result<(), CliprdrError> { + let tx = tx_handle.tx.clone(); + let provider = create_pasteboard_file_url_provider( + PasteObserverInfo { + file_descriptor_id, + conn_id, + source_path: "".to_string(), + target_path: "".to_string(), + }, + tx, + ); + unsafe { + let types = NSArray::from_vec(vec![NSString::from_str( + &NSPasteboardTypeFileURL.to_string(), + )]); + let item = objc2_app_kit::NSPasteboardItem::new(); + item.setDataProvider_forTypes(&ProtocolObject::from_id(provider), &types); + self.pasteboard.clearContents(); + if !self + .pasteboard + .writeObjects(&Id::cast(NSArray::from_vec(vec![item]))) + { + return Err(CliprdrError::CommonError { + description: "failed to write objects".to_string(), + }); + } + } + Ok(()) + } + + fn handle_format_data_response( + &self, + conn_id: i32, + msg_flags: i32, + format_data: Vec, + ) -> Result<(), CliprdrError> { + log::debug!("handle format data response, msg_flags: {msg_flags}"); + if msg_flags != 0x1 { + // return failure message? + } + + let mut task_lock = self.paste_task.lock().unwrap(); + let target_dir = PASTE_OBSERVER_INFO + .lock() + .unwrap() + .as_ref() + .map(|task| task.target_path.clone()); + // unreachable in normal case + let Some(target_dir) = target_dir.as_ref().map(|d| Path::new(d).parent()).flatten() else { + return Err(CliprdrError::CommonError { + description: "failed to get parent path".to_string(), + }); + }; + // unreachable in normal case + if !target_dir.exists() { + return Err(CliprdrError::CommonError { + description: "target path does not exist".to_string(), + }); + } + let target_dir = target_dir.to_owned(); + match FileDescription::parse_file_descriptors(format_data, conn_id) { + Ok(files) => { + task_lock.start(target_dir, files); + Ok(()) + } + Err(e) => { + PASTE_OBSERVER_INFO + .lock() + .unwrap() + .replace(PasteObserverInfo::default()); + Err(e) + } + } + } + + fn handle_file_contents_response( + &self, + conn_id: i32, + msg_flags: i32, + stream_id: i32, + requested_data: Vec, + ) -> Result<(), CliprdrError> { + log::debug!("handle file contents response"); + self.tx_paste_task + .send(FileContentsResponse { + conn_id, + msg_flags, + stream_id, + requested_data, + }) + .ok(); + Ok(()) + } + + fn handle_try_empty(&mut self, conn_id: i32) { + log::debug!("empty_clipboard called"); + let ret = self.empty_clipboard_(conn_id); + log::debug!( + "empty_clipboard called, conn_id {}, return {}", + conn_id, + ret + ); + } +} + +fn handle_paste_result(task_info: &PasteObserverInfo) { + log::info!( + "file {} is pasted to {}", + &task_info.source_path, + &task_info.target_path + ); + if Path::new(&task_info.target_path).parent().is_none() { + log::error!( + "failed to get parent path of {}, no need to perform pasting", + &task_info.target_path + ); + return; + } + + PASTE_OBSERVER_INFO + .lock() + .unwrap() + .replace(task_info.clone()); + // to-do: add a timeout to clear data in `PASTE_OBSERVER_INFO`. + std::fs::remove_file(&task_info.source_path).ok(); + std::fs::remove_file(&task_info.target_path).ok(); + let data = ClipboardFile::FormatDataRequest { + requested_format_id: task_info.file_descriptor_id, + }; + allow_err!(send_data(task_info.conn_id as _, data)); +} + +#[inline] +pub fn create_pasteboard_context() -> ResultType> { + let pasteboard: Option> = + unsafe { msg_send_id![NSPasteboard::class(), generalPasteboard] }; + let Some(pasteboard) = pasteboard else { + bail!("failed to get general pasteboard"); + }; + let mut observer = PasteObserver::new(); + observer.init(handle_paste_result)?; + let (tx, rx) = channel(); + let mut context = Box::new(PasteboardContext { + pasteboard, + observer: Arc::new(Mutex::new(observer)), + tx_handle: None, + tx_remove_file: None, + remove_file_handle: None, + tx_paste_task: tx, + paste_task: Arc::new(Mutex::new(PasteTask::new(rx))), + }); + context.init(); + Ok(context) +} + +#[cfg(test)] +mod tests { + #[test] + fn test_temp_files_count() { + let mut c = super::PasteboardContext::temp_files_count(); + + let mut created_files = vec![]; + for _ in 0..10 { + let path = format!( + "/tmp/{}{}", + super::TEMP_FILE_PREFIX, + uuid::Uuid::new_v4().to_string() + ); + if std::fs::File::create(&path).is_ok() { + created_files.push(path); + c += 1; + } + } + + assert_eq!(c, super::PasteboardContext::temp_files_count()); + + // Clean up the created files. + for file in created_files { + std::fs::remove_file(&file).ok(); + } + } +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/mod.rs b/vendor/rustdesk/libs/clipboard/src/platform/unix/mod.rs new file mode 100644 index 0000000..de5917f --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/unix/mod.rs @@ -0,0 +1,58 @@ +use dashmap::DashMap; +use lazy_static::lazy_static; + +mod filetype; +pub use filetype::{FileDescription, FileType}; +/// use FUSE for file pasting on these platforms +#[cfg(target_os = "linux")] +pub mod fuse; +#[cfg(target_os = "macos")] +pub mod macos; + +pub mod local_file; +pub mod serv_files; + +/// has valid file attributes +pub const FLAGS_FD_ATTRIBUTES: u32 = 0x04; +/// has valid file size +pub const FLAGS_FD_SIZE: u32 = 0x40; +/// has valid last write time +pub const FLAGS_FD_LAST_WRITE: u32 = 0x20; +/// show progress +pub const FLAGS_FD_PROGRESSUI: u32 = 0x4000; +/// transferred from unix, contains file mode +/// P.S. this flag is not used in windows +pub const FLAGS_FD_UNIX_MODE: u32 = 0x08; + +// not actual format id, just a placeholder +pub const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334; +pub const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW"; +// not actual format id, just a placeholder +pub const FILECONTENTS_FORMAT_ID: i32 = 49267; +pub const FILECONTENTS_FORMAT_NAME: &str = "FileContents"; + +/// block size for fuse, align to our asynchronic request size over FileContentsRequest. +pub(crate) const BLOCK_SIZE: u32 = 4 * 1024 * 1024; + +// begin of epoch used by microsoft +// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00 +const LDAP_EPOCH_DELTA: u64 = 116444772610000000; + +lazy_static! { + static ref REMOTE_FORMAT_MAP: DashMap = DashMap::from_iter( + [ + ( + FILEDESCRIPTOR_FORMAT_ID, + FILEDESCRIPTORW_FORMAT_NAME.to_string() + ), + (FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME.to_string()) + ] + .iter() + .cloned() + ); +} + +#[inline] +pub fn get_local_format(remote_id: i32) -> Option { + REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone()) +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/unix/serv_files.rs b/vendor/rustdesk/libs/clipboard/src/platform/unix/serv_files.rs new file mode 100644 index 0000000..6f4fb54 --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/unix/serv_files.rs @@ -0,0 +1,271 @@ +use super::local_file::LocalFile; +use crate::{platform::unix::local_file::construct_file_list, ClipboardFile, CliprdrError}; +use hbb_common::{ + bytes::{BufMut, BytesMut}, + log, +}; +use parking_lot::Mutex; +use std::{path::PathBuf, sync::Arc, usize}; + +lazy_static::lazy_static! { + // local files are cached, this value should not be changed when copying files + // Because `CliprdrFileContentsRequest` only contains the index of the file in the list. + // We need to keep the file list in the same order as the remote side. + // We may add a `FileId` field to `CliprdrFileContentsRequest` in the future. + static ref CLIP_FILES: Arc> = Default::default(); +} + +#[derive(Debug)] +enum FileContentsRequest { + Size { + stream_id: i32, + file_idx: usize, + }, + + Range { + stream_id: i32, + file_idx: usize, + offset: u64, + length: u64, + }, +} + +#[derive(Default)] +struct ClipFiles { + files: Vec, + file_list: Vec, + first_file_index: usize, + files_pdu: Vec, +} + +impl ClipFiles { + fn clear(&mut self) { + self.files.clear(); + self.file_list.clear(); + self.first_file_index = usize::MAX; + self.files_pdu.clear(); + } + + fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> { + let clipboard_paths = clipboard_files + .iter() + .map(|s| PathBuf::from(s)) + .collect::>(); + self.file_list = construct_file_list(&clipboard_paths)?; + self.first_file_index = self + .file_list + .iter() + .position(|f| !f.path.is_dir()) + .unwrap_or(usize::MAX); + self.files = clipboard_files.to_vec(); + Ok(()) + } + + fn build_file_list_pdu(&mut self) { + let mut data = BytesMut::with_capacity(4 + 592 * self.file_list.len()); + data.put_u32_le(self.file_list.len() as u32); + for file in self.file_list.iter() { + data.put(file.as_bin().as_slice()); + } + self.files_pdu = data.to_vec() + } + + fn get_files_for_audit(&self, request: &FileContentsRequest) -> Option { + if let FileContentsRequest::Range { + file_idx, offset, .. + } = request + { + if *file_idx == self.first_file_index && *offset == 0 { + let files: Vec<(String, u64)> = self + .file_list + .iter() + .filter_map(|f| { + if f.path.is_file() { + Some((f.path.to_string_lossy().to_string(), f.size)) + } else { + None + } + }) + .collect::<_>(); + if files.is_empty() { + return None; + } else { + return Some(ClipboardFile::Files { files }); + } + } + } + None + } + + fn serve_file_contents( + &mut self, + conn_id: i32, + request: FileContentsRequest, + ) -> Result { + let (file_idx, file_contents_resp) = match request { + FileContentsRequest::Size { + stream_id, + file_idx, + } => { + log::debug!("file contents (size) requested from conn: {}", conn_id); + let Some(file) = self.file_list.get(file_idx) else { + log::error!( + "invalid file index {} requested from conn: {}", + file_idx, + conn_id + ); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid file index {} requested from conn: {}", + file_idx, conn_id + ), + }); + }; + + log::debug!( + "conn {} requested file-{}: {}", + conn_id, + file_idx, + file.name + ); + + let size = file.size; + ( + file_idx, + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: size.to_le_bytes().to_vec(), + }, + ) + } + FileContentsRequest::Range { + stream_id, + file_idx, + offset, + length, + } => { + log::debug!( + "file contents (range from {} length {}) request from conn: {}", + offset, + length, + conn_id + ); + let Some(file) = self.file_list.get_mut(file_idx) else { + log::error!( + "invalid file index {} requested from conn: {}", + file_idx, + conn_id + ); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid file index {} requested from conn: {}", + file_idx, conn_id + ), + }); + }; + log::debug!( + "conn {} requested file-{}: {}", + conn_id, + file_idx, + file.name + ); + + if offset > file.size { + log::error!("invalid reading offset requested from conn: {}", conn_id); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid reading offset requested from conn: {}", + conn_id + ), + }); + } + let read_size = if offset + length > file.size { + file.size - offset + } else { + length + }; + + let mut buf = vec![0u8; read_size as usize]; + + file.read_exact_at(&mut buf, offset)?; + + ( + file_idx, + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: buf, + }, + ) + } + }; + + log::debug!("file contents sent to conn: {}", conn_id); + // hot reload next file + for next_file in self.file_list.iter_mut().skip(file_idx + 1) { + if !next_file.is_dir { + next_file.load_handle()?; + break; + } + } + Ok(file_contents_resp) + } +} + +#[inline] +pub fn clear_files() { + CLIP_FILES.lock().clear(); +} + +pub fn read_file_contents( + conn_id: i32, + stream_id: i32, + list_index: i32, + dw_flags: i32, + n_position_low: i32, + n_position_high: i32, + cb_requested: i32, +) -> Vec> { + let fcr = if dw_flags == 0x1 { + FileContentsRequest::Size { + stream_id, + file_idx: list_index as usize, + } + } else if dw_flags == 0x2 { + let offset = (n_position_high as u64) << 32 | n_position_low as u64; + let length = cb_requested as u64; + + FileContentsRequest::Range { + stream_id, + file_idx: list_index as usize, + offset, + length, + } + } else { + return vec![Err(CliprdrError::InvalidRequest { + description: format!("got invalid FileContentsRequest, dw_flats: {dw_flags}"), + })]; + }; + + let mut clip_files = CLIP_FILES.lock(); + let mut res = vec![]; + if let Some(files_res) = clip_files.get_files_for_audit(&fcr) { + res.push(Ok(files_res)); + } + res.push(clip_files.serve_file_contents(conn_id, fcr)); + res +} + +pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> { + let mut files_lock = CLIP_FILES.lock(); + if files_lock.files == files { + return Ok(()); + } + files_lock.sync_files(files)?; + Ok(files_lock.build_file_list_pdu()) +} + +pub fn get_file_list_pdu() -> Vec { + CLIP_FILES.lock().files_pdu.clone() +} diff --git a/vendor/rustdesk/libs/clipboard/src/platform/windows.rs b/vendor/rustdesk/libs/clipboard/src/platform/windows.rs new file mode 100644 index 0000000..cdeb3e4 --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/platform/windows.rs @@ -0,0 +1,1327 @@ +//! windows implementation +#![allow(dead_code)] +#![allow(non_camel_case_types)] +#![allow(unused_variables)] +#![allow(non_snake_case)] +#![allow(deref_nullptr)] + +use crate::{ + send_data, send_data_exclude, ClipboardFile, CliprdrError, CliprdrServiceContext, + ProgressPercent, ResultType, ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, + ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, +}; +use hbb_common::{allow_err, log}; +use std::{ + boxed::Box, + ffi::{CStr, CString}, + result::Result, +}; + +// only used error code will be recorded here +/// success +const CHANNEL_RC_OK: u32 = 0; +/// error code from WinError.h +/// success +const ERROR_SUCCESS: u32 = 0; +/// allocation failure +const CHANNEL_RC_NO_MEMORY: u32 = 12; +/// error code from WinError.h +/// used by FreeRDP to represent errors. +const ERROR_INTERNAL_ERROR: u32 = 0x54F; + +pub type size_t = ::std::os::raw::c_ulonglong; +pub type __vcrt_bool = bool; +pub type wchar_t = ::std::os::raw::c_ushort; + +pub type POINTER_64_INT = ::std::os::raw::c_ulonglong; +pub type INT8 = ::std::os::raw::c_schar; +pub type PINT8 = *mut ::std::os::raw::c_schar; +pub type INT16 = ::std::os::raw::c_short; +pub type PINT16 = *mut ::std::os::raw::c_short; +pub type INT32 = ::std::os::raw::c_int; +pub type PINT32 = *mut ::std::os::raw::c_int; +pub type INT64 = ::std::os::raw::c_longlong; +pub type PINT64 = *mut ::std::os::raw::c_longlong; +pub type UINT8 = ::std::os::raw::c_uchar; +pub type PUINT8 = *mut ::std::os::raw::c_uchar; +pub type UINT16 = ::std::os::raw::c_ushort; +pub type PUINT16 = *mut ::std::os::raw::c_ushort; +pub type UINT32 = ::std::os::raw::c_uint; +pub type PUINT32 = *mut ::std::os::raw::c_uint; +pub type UINT64 = ::std::os::raw::c_ulonglong; +pub type PUINT64 = *mut ::std::os::raw::c_ulonglong; +pub type LONG32 = ::std::os::raw::c_int; +pub type PLONG32 = *mut ::std::os::raw::c_int; +pub type ULONG32 = ::std::os::raw::c_uint; +pub type PULONG32 = *mut ::std::os::raw::c_uint; +pub type DWORD32 = ::std::os::raw::c_uint; +pub type PDWORD32 = *mut ::std::os::raw::c_uint; +pub type INT_PTR = ::std::os::raw::c_longlong; +pub type PINT_PTR = *mut ::std::os::raw::c_longlong; +pub type UINT_PTR = ::std::os::raw::c_ulonglong; +pub type PUINT_PTR = *mut ::std::os::raw::c_ulonglong; +pub type LONG_PTR = ::std::os::raw::c_longlong; +pub type PLONG_PTR = *mut ::std::os::raw::c_longlong; +pub type ULONG_PTR = ::std::os::raw::c_ulonglong; +pub type PULONG_PTR = *mut ::std::os::raw::c_ulonglong; +pub type SHANDLE_PTR = ::std::os::raw::c_longlong; +pub type HANDLE_PTR = ::std::os::raw::c_ulonglong; +pub type UHALF_PTR = ::std::os::raw::c_uint; +pub type PUHALF_PTR = *mut ::std::os::raw::c_uint; +pub type HALF_PTR = ::std::os::raw::c_int; +pub type PHALF_PTR = *mut ::std::os::raw::c_int; +pub type SIZE_T = ULONG_PTR; +pub type PSIZE_T = *mut ULONG_PTR; +pub type SSIZE_T = LONG_PTR; +pub type PSSIZE_T = *mut LONG_PTR; +pub type DWORD_PTR = ULONG_PTR; +pub type PDWORD_PTR = *mut ULONG_PTR; +pub type LONG64 = ::std::os::raw::c_longlong; +pub type PLONG64 = *mut ::std::os::raw::c_longlong; +pub type ULONG64 = ::std::os::raw::c_ulonglong; +pub type PULONG64 = *mut ::std::os::raw::c_ulonglong; +pub type DWORD64 = ::std::os::raw::c_ulonglong; +pub type PDWORD64 = *mut ::std::os::raw::c_ulonglong; +pub type KAFFINITY = ULONG_PTR; +pub type PKAFFINITY = *mut KAFFINITY; +pub type PVOID = *mut ::std::os::raw::c_void; +pub type CHAR = ::std::os::raw::c_char; +pub type SHORT = ::std::os::raw::c_short; +pub type LONG = ::std::os::raw::c_long; +pub type WCHAR = wchar_t; +pub type PWCHAR = *mut WCHAR; +pub type LPWCH = *mut WCHAR; +pub type PWCH = *mut WCHAR; +pub type LPCWCH = *const WCHAR; +pub type PCWCH = *const WCHAR; +pub type NWPSTR = *mut WCHAR; +pub type LPWSTR = *mut WCHAR; +pub type PWSTR = *mut WCHAR; +pub type PZPWSTR = *mut PWSTR; +pub type PCZPWSTR = *const PWSTR; +pub type LPUWSTR = *mut WCHAR; +pub type PUWSTR = *mut WCHAR; +pub type LPCWSTR = *const WCHAR; +pub type PCWSTR = *const WCHAR; +pub type PZPCWSTR = *mut PCWSTR; +pub type PCZPCWSTR = *const PCWSTR; +pub type LPCUWSTR = *const WCHAR; +pub type PCUWSTR = *const WCHAR; +pub type PZZWSTR = *mut WCHAR; +pub type PCZZWSTR = *const WCHAR; +pub type PUZZWSTR = *mut WCHAR; +pub type PCUZZWSTR = *const WCHAR; +pub type PNZWCH = *mut WCHAR; +pub type PCNZWCH = *const WCHAR; +pub type PUNZWCH = *mut WCHAR; +pub type PCUNZWCH = *const WCHAR; +pub type PCHAR = *mut CHAR; +pub type LPCH = *mut CHAR; +pub type PCH = *mut CHAR; +pub type LPCCH = *const CHAR; +pub type PCCH = *const CHAR; +pub type NPSTR = *mut CHAR; +pub type LPSTR = *mut CHAR; +pub type PSTR = *mut CHAR; +pub type PZPSTR = *mut PSTR; +pub type PCZPSTR = *const PSTR; +pub type LPCSTR = *const CHAR; +pub type PCSTR = *const CHAR; +pub type PZPCSTR = *mut PCSTR; +pub type PCZPCSTR = *const PCSTR; +pub type PZZSTR = *mut CHAR; +pub type PCZZSTR = *const CHAR; +pub type PNZCH = *mut CHAR; +pub type PCNZCH = *const CHAR; +pub type TCHAR = ::std::os::raw::c_char; +pub type PTCHAR = *mut ::std::os::raw::c_char; +pub type TBYTE = ::std::os::raw::c_uchar; +pub type PTBYTE = *mut ::std::os::raw::c_uchar; +pub type LPTCH = LPCH; +pub type PTCH = LPCH; +pub type LPCTCH = LPCCH; +pub type PCTCH = LPCCH; +pub type PTSTR = LPSTR; +pub type LPTSTR = LPSTR; +pub type PUTSTR = LPSTR; +pub type LPUTSTR = LPSTR; +pub type PCTSTR = LPCSTR; +pub type LPCTSTR = LPCSTR; +pub type PCUTSTR = LPCSTR; +pub type LPCUTSTR = LPCSTR; +pub type PZZTSTR = PZZSTR; +pub type PUZZTSTR = PZZSTR; +pub type PCZZTSTR = PCZZSTR; +pub type PCUZZTSTR = PCZZSTR; +pub type PZPTSTR = PZPSTR; +pub type PNZTCH = PNZCH; +pub type PUNZTCH = PNZCH; +pub type PCNZTCH = PCNZCH; +pub type PCUNZTCH = PCNZCH; +pub type PSHORT = *mut SHORT; +pub type PLONG = *mut LONG; +pub type ULONG = ::std::os::raw::c_ulong; +pub type PULONG = *mut ULONG; +pub type USHORT = ::std::os::raw::c_ushort; +pub type PUSHORT = *mut USHORT; +pub type UCHAR = ::std::os::raw::c_uchar; +pub type PUCHAR = *mut UCHAR; +pub type PSZ = *mut ::std::os::raw::c_char; +pub type DWORD = ::std::os::raw::c_ulong; +pub type BOOL = ::std::os::raw::c_int; +pub type BYTE = ::std::os::raw::c_uchar; +pub type WORD = ::std::os::raw::c_ushort; +pub type FLOAT = f32; +pub type PFLOAT = *mut FLOAT; +pub type PBOOL = *mut BOOL; +pub type LPBOOL = *mut BOOL; +pub type PBYTE = *mut BYTE; +pub type LPBYTE = *mut BYTE; +pub type PINT = *mut ::std::os::raw::c_int; +pub type LPINT = *mut ::std::os::raw::c_int; +pub type PWORD = *mut WORD; +pub type LPWORD = *mut WORD; +pub type LPLONG = *mut ::std::os::raw::c_long; +pub type PDWORD = *mut DWORD; +pub type LPDWORD = *mut DWORD; +pub type LPVOID = *mut ::std::os::raw::c_void; +pub type LPCVOID = *const ::std::os::raw::c_void; +pub type INT = ::std::os::raw::c_int; +pub type UINT = ::std::os::raw::c_uint; +pub type PUINT = *mut ::std::os::raw::c_uint; +pub type va_list = *mut ::std::os::raw::c_char; + +pub const TRUE: ::std::os::raw::c_int = 1; +pub const FALSE: ::std::os::raw::c_int = 0; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_HEADER { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, +} +pub type CLIPRDR_HEADER = _CLIPRDR_HEADER; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_CAPABILITY_SET { + pub capabilitySetType: UINT16, + pub capabilitySetLength: UINT16, +} +pub type CLIPRDR_CAPABILITY_SET = _CLIPRDR_CAPABILITY_SET; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_GENERAL_CAPABILITY_SET { + pub capabilitySetType: UINT16, + pub capabilitySetLength: UINT16, + pub version: UINT32, + pub generalFlags: UINT32, +} +pub type CLIPRDR_GENERAL_CAPABILITY_SET = _CLIPRDR_GENERAL_CAPABILITY_SET; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_CAPABILITIES { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub cCapabilitiesSets: UINT32, + pub capabilitySets: *mut CLIPRDR_CAPABILITY_SET, +} +pub type CLIPRDR_CAPABILITIES = _CLIPRDR_CAPABILITIES; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_MONITOR_READY { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, +} +pub type CLIPRDR_MONITOR_READY = _CLIPRDR_MONITOR_READY; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_TEMP_DIRECTORY { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub szTempDir: [::std::os::raw::c_char; 520usize], +} +pub type CLIPRDR_TEMP_DIRECTORY = _CLIPRDR_TEMP_DIRECTORY; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FORMAT { + pub formatId: UINT32, + pub formatName: *mut ::std::os::raw::c_char, +} +pub type CLIPRDR_FORMAT = _CLIPRDR_FORMAT; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FORMAT_LIST { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub numFormats: UINT32, + pub formats: *mut CLIPRDR_FORMAT, +} +pub type CLIPRDR_FORMAT_LIST = _CLIPRDR_FORMAT_LIST; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FORMAT_LIST_RESPONSE { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, +} +pub type CLIPRDR_FORMAT_LIST_RESPONSE = _CLIPRDR_FORMAT_LIST_RESPONSE; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_LOCK_CLIPBOARD_DATA { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub clipDataId: UINT32, +} +pub type CLIPRDR_LOCK_CLIPBOARD_DATA = _CLIPRDR_LOCK_CLIPBOARD_DATA; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_UNLOCK_CLIPBOARD_DATA { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub clipDataId: UINT32, +} +pub type CLIPRDR_UNLOCK_CLIPBOARD_DATA = _CLIPRDR_UNLOCK_CLIPBOARD_DATA; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FORMAT_DATA_REQUEST { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub requestedFormatId: UINT32, +} +pub type CLIPRDR_FORMAT_DATA_REQUEST = _CLIPRDR_FORMAT_DATA_REQUEST; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FORMAT_DATA_RESPONSE { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub requestedFormatData: *const BYTE, +} +pub type CLIPRDR_FORMAT_DATA_RESPONSE = _CLIPRDR_FORMAT_DATA_RESPONSE; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FILE_CONTENTS_REQUEST { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub streamId: UINT32, + pub listIndex: UINT32, + pub dwFlags: UINT32, + pub nPositionLow: UINT32, + pub nPositionHigh: UINT32, + pub cbRequested: UINT32, + pub haveClipDataId: BOOL, + pub clipDataId: UINT32, +} +pub type CLIPRDR_FILE_CONTENTS_REQUEST = _CLIPRDR_FILE_CONTENTS_REQUEST; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _CLIPRDR_FILE_CONTENTS_RESPONSE { + pub connID: UINT32, + pub msgType: UINT16, + pub msgFlags: UINT16, + pub dataLen: UINT32, + pub streamId: UINT32, + pub cbRequested: UINT32, + pub requestedData: *const BYTE, +} +pub type CLIPRDR_FILE_CONTENTS_RESPONSE = _CLIPRDR_FILE_CONTENTS_RESPONSE; +pub type CliprdrClientContext = _cliprdr_client_context; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _NOTIFICATION_MESSAGE { + pub r#type: UINT32, // 0 - info, 1 - warning, 2 - error + pub msg: *const BYTE, + pub details: *const BYTE, +} +pub type NOTIFICATION_MESSAGE = _NOTIFICATION_MESSAGE; +pub type pcCliprdrServerCapabilities = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + capabilities: *const CLIPRDR_CAPABILITIES, + ) -> UINT, +>; +pub type pcCliprdrClientCapabilities = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + capabilities: *const CLIPRDR_CAPABILITIES, + ) -> UINT, +>; +pub type pcCliprdrMonitorReady = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + monitorReady: *const CLIPRDR_MONITOR_READY, + ) -> UINT, +>; +pub type pcCliprdrTempDirectory = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + tempDirectory: *const CLIPRDR_TEMP_DIRECTORY, + ) -> UINT, +>; +pub type pcNotifyClipboardMsg = ::std::option::Option< + unsafe extern "C" fn(connID: UINT32, msg: *const NOTIFICATION_MESSAGE) -> UINT, +>; +pub type pcHandleClipboardFiles = ::std::option::Option< + unsafe extern "C" fn(connID: UINT32, nFiles: size_t, fileNames: *mut *mut WCHAR) -> UINT, +>; +pub type pcCliprdrClientFormatList = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatList: *const CLIPRDR_FORMAT_LIST, + ) -> UINT, +>; +pub type pcCliprdrServerFormatList = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatList: *const CLIPRDR_FORMAT_LIST, + ) -> UINT, +>; +pub type pcCliprdrClientFormatListResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatListResponse: *const CLIPRDR_FORMAT_LIST_RESPONSE, + ) -> UINT, +>; +pub type pcCliprdrServerFormatListResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatListResponse: *const CLIPRDR_FORMAT_LIST_RESPONSE, + ) -> UINT, +>; +pub type pcCliprdrClientLockClipboardData = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + lockClipboardData: *const CLIPRDR_LOCK_CLIPBOARD_DATA, + ) -> UINT, +>; +pub type pcCliprdrServerLockClipboardData = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + lockClipboardData: *const CLIPRDR_LOCK_CLIPBOARD_DATA, + ) -> UINT, +>; +pub type pcCliprdrClientUnlockClipboardData = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + unlockClipboardData: *const CLIPRDR_UNLOCK_CLIPBOARD_DATA, + ) -> UINT, +>; +pub type pcCliprdrServerUnlockClipboardData = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + unlockClipboardData: *const CLIPRDR_UNLOCK_CLIPBOARD_DATA, + ) -> UINT, +>; +pub type pcCliprdrClientFormatDataRequest = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatDataRequest: *const CLIPRDR_FORMAT_DATA_REQUEST, + ) -> UINT, +>; +pub type pcCliprdrServerFormatDataRequest = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatDataRequest: *const CLIPRDR_FORMAT_DATA_REQUEST, + ) -> UINT, +>; +pub type pcCliprdrClientFormatDataResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatDataResponse: *const CLIPRDR_FORMAT_DATA_RESPONSE, + ) -> UINT, +>; +pub type pcCliprdrServerFormatDataResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + formatDataResponse: *const CLIPRDR_FORMAT_DATA_RESPONSE, + ) -> UINT, +>; +pub type pcCliprdrClientFileContentsRequest = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + fileContentsRequest: *const CLIPRDR_FILE_CONTENTS_REQUEST, + ) -> UINT, +>; +pub type pcCliprdrServerFileContentsRequest = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + fileContentsRequest: *const CLIPRDR_FILE_CONTENTS_REQUEST, + ) -> UINT, +>; +pub type pcCliprdrClientFileContentsResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + fileContentsResponse: *const CLIPRDR_FILE_CONTENTS_RESPONSE, + ) -> UINT, +>; +pub type pcCliprdrServerFileContentsResponse = ::std::option::Option< + unsafe extern "C" fn( + context: *mut CliprdrClientContext, + fileContentsResponse: *const CLIPRDR_FILE_CONTENTS_RESPONSE, + ) -> UINT, +>; + +// TODO: hide more members of clipboard context +#[repr(C)] +#[derive(Debug, Clone)] +pub struct _cliprdr_client_context { + pub Custom: *mut ::std::os::raw::c_void, + pub EnableFiles: BOOL, + pub EnableOthers: BOOL, + pub IsStopped: BOOL, + pub ResponseWaitTimeoutSecs: UINT32, + pub ServerCapabilities: pcCliprdrServerCapabilities, + pub ClientCapabilities: pcCliprdrClientCapabilities, + pub MonitorReady: pcCliprdrMonitorReady, + pub TempDirectory: pcCliprdrTempDirectory, + pub NotifyClipboardMsg: pcNotifyClipboardMsg, + pub HandleClipboardFiles: pcHandleClipboardFiles, + pub ClientFormatList: pcCliprdrClientFormatList, + pub ServerFormatList: pcCliprdrServerFormatList, + pub ClientFormatListResponse: pcCliprdrClientFormatListResponse, + pub ServerFormatListResponse: pcCliprdrServerFormatListResponse, + pub ClientLockClipboardData: pcCliprdrClientLockClipboardData, + pub ServerLockClipboardData: pcCliprdrServerLockClipboardData, + pub ClientUnlockClipboardData: pcCliprdrClientUnlockClipboardData, + pub ServerUnlockClipboardData: pcCliprdrServerUnlockClipboardData, + pub ClientFormatDataRequest: pcCliprdrClientFormatDataRequest, + pub ServerFormatDataRequest: pcCliprdrServerFormatDataRequest, + pub ClientFormatDataResponse: pcCliprdrClientFormatDataResponse, + pub ServerFormatDataResponse: pcCliprdrServerFormatDataResponse, + pub ClientFileContentsRequest: pcCliprdrClientFileContentsRequest, + pub ServerFileContentsRequest: pcCliprdrServerFileContentsRequest, + pub ClientFileContentsResponse: pcCliprdrClientFileContentsResponse, + pub ServerFileContentsResponse: pcCliprdrServerFileContentsResponse, + pub LastRequestedFormatId: UINT32, +} + +// #[link(name = "user32")] +// #[link(name = "ole32")] +extern "C" { + pub(crate) fn init_cliprdr(context: *mut CliprdrClientContext) -> BOOL; + pub(crate) fn uninit_cliprdr(context: *mut CliprdrClientContext) -> BOOL; + pub(crate) fn empty_cliprdr(context: *mut CliprdrClientContext, connID: UINT32) -> BOOL; +} + +unsafe impl Send for CliprdrClientContext {} + +unsafe impl Sync for CliprdrClientContext {} + +impl CliprdrClientContext { + pub fn create( + enable_files: bool, + enable_others: bool, + response_wait_timeout_secs: u32, + notify_callback: pcNotifyClipboardMsg, + handle_clipboard_files: pcHandleClipboardFiles, + client_format_list: pcCliprdrClientFormatList, + client_format_list_response: pcCliprdrClientFormatListResponse, + client_format_data_request: pcCliprdrClientFormatDataRequest, + client_format_data_response: pcCliprdrClientFormatDataResponse, + client_file_contents_request: pcCliprdrClientFileContentsRequest, + client_file_contents_response: pcCliprdrClientFileContentsResponse, + ) -> Result, CliprdrError> { + let context = CliprdrClientContext { + Custom: 0 as *mut _, + EnableFiles: if enable_files { TRUE } else { FALSE }, + EnableOthers: if enable_others { TRUE } else { FALSE }, + IsStopped: FALSE, + ResponseWaitTimeoutSecs: response_wait_timeout_secs, + ServerCapabilities: None, + ClientCapabilities: None, + MonitorReady: None, + TempDirectory: None, + NotifyClipboardMsg: notify_callback, + HandleClipboardFiles: handle_clipboard_files, + ClientFormatList: client_format_list, + ServerFormatList: None, + ClientFormatListResponse: client_format_list_response, + ServerFormatListResponse: None, + ClientLockClipboardData: None, + ServerLockClipboardData: None, + ClientUnlockClipboardData: None, + ServerUnlockClipboardData: None, + ClientFormatDataRequest: client_format_data_request, + ServerFormatDataRequest: None, + ClientFormatDataResponse: client_format_data_response, + ServerFormatDataResponse: None, + ClientFileContentsRequest: client_file_contents_request, + ServerFileContentsRequest: None, + ClientFileContentsResponse: client_file_contents_response, + ServerFileContentsResponse: None, + LastRequestedFormatId: 0, + }; + let mut context = Box::new(context); + unsafe { + if FALSE == init_cliprdr(&mut (*context)) { + println!("Failed to init cliprdr"); + Err(CliprdrError::CliprdrInit) + } else { + Ok(context) + } + } + } +} + +impl Drop for CliprdrClientContext { + fn drop(&mut self) { + unsafe { + if FALSE == uninit_cliprdr(&mut *self) { + println!("Failed to uninit cliprdr"); + } else { + println!("Succeeded to uninit cliprdr"); + } + } + } +} + +impl CliprdrServiceContext for CliprdrClientContext { + fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { + self.IsStopped = TRUE; + Ok(()) + } + + fn empty_clipboard(&mut self, conn_id: i32) -> Result { + Ok(empty_clipboard(self, conn_id)) + } + + fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + let ret = server_clip_file(self, conn_id, msg); + ret_to_result(ret) + } + + fn get_progress_percent(&self) -> Option { + None + } + + fn cancel(&mut self) {} +} + +fn ret_to_result(ret: u32) -> Result<(), CliprdrError> { + match ret { + #[allow(unreachable_patterns)] + // CHANNEL_RC_OK is unreachable, but ignore it + ERROR_SUCCESS | CHANNEL_RC_OK => Ok(()), + CHANNEL_RC_NO_MEMORY => Err(CliprdrError::CliprdrOutOfMemory), + ERROR_INTERNAL_ERROR => Err(CliprdrError::ClipboardInternalError), + e => Err(CliprdrError::Unknown(e)), + } +} + +pub fn empty_clipboard(context: &mut CliprdrClientContext, conn_id: i32) -> bool { + unsafe { TRUE == empty_cliprdr(context, conn_id as u32) } +} + +pub fn server_clip_file( + context: &mut CliprdrClientContext, + conn_id: i32, + msg: ClipboardFile, +) -> u32 { + let mut ret = 0; + match msg { + ClipboardFile::NotifyCallback { .. } => { + // unreachable + } + ClipboardFile::MonitorReady => { + log::debug!("server_monitor_ready called"); + ret = server_monitor_ready(context, conn_id); + log::debug!( + "server_monitor_ready called, conn_id {}, return {}", + conn_id, + ret + ); + } + ClipboardFile::FormatList { format_list } => { + log::debug!( + "server_format_list called, conn_id {}, format_list: {:?}", + conn_id, + &format_list + ); + send_data_exclude(conn_id as _, ClipboardFile::TryEmpty); + ret = server_format_list(context, conn_id, format_list); + log::debug!( + "server_format_list called, conn_id {}, return {}", + conn_id, + ret + ); + } + ClipboardFile::FormatListResponse { msg_flags } => { + log::debug!("server_format_list_response called"); + ret = server_format_list_response(context, conn_id, msg_flags); + log::debug!( + "server_format_list_response called, conn_id {}, msg_flags {}, return {}", + conn_id, + msg_flags, + ret + ); + } + ClipboardFile::FormatDataRequest { + requested_format_id, + } => { + log::debug!("server_format_data_request called"); + ret = server_format_data_request(context, conn_id, requested_format_id); + log::debug!( + "server_format_data_request called, conn_id {}, requested_format_id {}, return {}", + conn_id, + requested_format_id, + ret + ); + } + ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + } => { + log::debug!("server_format_data_response called"); + ret = server_format_data_response(context, conn_id, msg_flags, format_data); + log::debug!( + "server_format_data_response called, conn_id {}, msg_flags: {}, return {}", + conn_id, + msg_flags, + ret + ); + } + ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id, + clip_data_id, + } => { + log::debug!("server_file_contents_request called"); + ret = server_file_contents_request( + context, + conn_id, + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id, + clip_data_id, + ); + log::debug!("server_file_contents_request called, conn_id {}, stream_id: {}, list_index {}, dw_flags {}, n_position_low {}, n_position_high {}, cb_requested {}, have_clip_data_id {}, clip_data_id {}, return {}", conn_id, + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id, + clip_data_id, + ret + ); + } + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + requested_data, + } => { + log::debug!("server_file_contents_response called"); + ret = server_file_contents_response( + context, + conn_id, + msg_flags, + stream_id, + requested_data, + ); + log::debug!("server_file_contents_response called, conn_id {}, msg_flags {}, stream_id {}, return {}", + conn_id, + msg_flags, + stream_id, + ret + ); + } + ClipboardFile::TryEmpty => { + log::debug!("empty_clipboard called"); + let ret = empty_clipboard(context, conn_id); + log::debug!( + "empty_clipboard called, conn_id {}, return {}", + conn_id, + ret + ); + } + ClipboardFile::Files { .. } => { + // unreachable + } + } + ret +} + +pub fn server_monitor_ready(context: &mut CliprdrClientContext, conn_id: i32) -> u32 { + unsafe { + let monitor_ready = CLIPRDR_MONITOR_READY { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: 0 as UINT16, + dataLen: 0 as UINT32, + }; + if let Some(f) = context.MonitorReady { + let ret = f(context, &monitor_ready); + ret as u32 + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn server_format_list( + context: &mut CliprdrClientContext, + conn_id: i32, + format_list: Vec<(i32, String)>, +) -> u32 { + unsafe { + let num_formats = format_list.len() as UINT32; + let mut formats = format_list + .into_iter() + .map(|format| { + if format.1.is_empty() { + CLIPRDR_FORMAT { + formatId: format.0 as UINT32, + formatName: 0 as *mut _, + } + } else { + let n = match CString::new(format.1) { + Ok(n) => n, + Err(_) => CString::new("").unwrap_or_default(), + }; + CLIPRDR_FORMAT { + formatId: format.0 as UINT32, + formatName: n.into_raw(), + } + } + }) + .collect::>(); + + let format_list = CLIPRDR_FORMAT_LIST { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: 0 as UINT16, + dataLen: 0 as UINT32, + numFormats: num_formats, + formats: formats.as_mut_ptr(), + }; + + let ret = if let Some(f) = context.ServerFormatList { + f(context, &format_list) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + }; + + for f in formats { + if !f.formatName.is_null() { + // retake pointer to free memory + let _ = CString::from_raw(f.formatName); + } + } + + ret as u32 + } +} + +pub fn server_format_list_response( + context: &mut CliprdrClientContext, + conn_id: i32, + msg_flags: i32, +) -> u32 { + unsafe { + let format_list_response = CLIPRDR_FORMAT_LIST_RESPONSE { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: msg_flags as UINT16, + dataLen: 0 as UINT32, + }; + + if let Some(f) = context.ServerFormatListResponse { + f(context, &format_list_response) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn server_format_data_request( + context: &mut CliprdrClientContext, + conn_id: i32, + requested_format_id: i32, +) -> u32 { + unsafe { + let format_data_request = CLIPRDR_FORMAT_DATA_REQUEST { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: 0 as UINT16, + dataLen: 0 as UINT32, + requestedFormatId: requested_format_id as UINT32, + }; + if let Some(f) = context.ServerFormatDataRequest { + f(context, &format_data_request) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn server_format_data_response( + context: &mut CliprdrClientContext, + conn_id: i32, + msg_flags: i32, + mut format_data: Vec, +) -> u32 { + unsafe { + let format_data_response = CLIPRDR_FORMAT_DATA_RESPONSE { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: msg_flags as UINT16, + dataLen: format_data.len() as UINT32, + requestedFormatData: format_data.as_mut_ptr(), + }; + if let Some(f) = context.ServerFormatDataResponse { + f(context, &format_data_response) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn server_file_contents_request( + context: &mut CliprdrClientContext, + conn_id: i32, + stream_id: i32, + list_index: i32, + dw_flags: i32, + n_position_low: i32, + n_position_high: i32, + cb_requested: i32, + have_clip_data_id: bool, + clip_data_id: i32, +) -> u32 { + unsafe { + let file_contents_request = CLIPRDR_FILE_CONTENTS_REQUEST { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: 0 as UINT16, + dataLen: 0 as UINT32, + streamId: stream_id as UINT32, + listIndex: list_index as UINT32, + dwFlags: dw_flags as UINT32, + nPositionLow: n_position_low as UINT32, + nPositionHigh: n_position_high as UINT32, + cbRequested: cb_requested as UINT32, + haveClipDataId: if have_clip_data_id { TRUE } else { FALSE }, + clipDataId: clip_data_id as UINT32, + }; + if let Some(f) = context.ServerFileContentsRequest { + f(context, &file_contents_request) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn server_file_contents_response( + context: &mut CliprdrClientContext, + conn_id: i32, + msg_flags: i32, + stream_id: i32, + mut requested_data: Vec, +) -> u32 { + unsafe { + let file_contents_response = CLIPRDR_FILE_CONTENTS_RESPONSE { + connID: conn_id as UINT32, + msgType: 0 as UINT16, + msgFlags: msg_flags as UINT16, + dataLen: 4 + requested_data.len() as UINT32, + streamId: stream_id as UINT32, + cbRequested: requested_data.len() as UINT32, + requestedData: requested_data.as_mut_ptr(), + }; + if let Some(f) = context.ServerFileContentsResponse { + f(context, &file_contents_response) + } else { + ERR_CODE_SERVER_FUNCTION_NONE + } + } +} + +pub fn create_cliprdr_context( + enable_files: bool, + enable_others: bool, + response_wait_timeout_secs: u32, +) -> ResultType> { + Ok(CliprdrClientContext::create( + enable_files, + enable_others, + response_wait_timeout_secs, + Some(notify_callback), + Some(handle_clipboard_files), + Some(client_format_list), + Some(client_format_list_response), + Some(client_format_data_request), + Some(client_format_data_response), + Some(client_file_contents_request), + Some(client_file_contents_response), + )?) +} + +extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE) -> UINT { + log::debug!("notify_callback called"); + let data = unsafe { + let msg = &*msg; + let details = if msg.details.is_null() { + Ok("") + } else { + CStr::from_ptr(msg.details as _).to_str() + }; + match (CStr::from_ptr(msg.msg as _).to_str(), details) { + (Ok(m), Ok(d)) => { + let msgtype = format!( + "custom-{}-nocancel-nook-hasclose", + if msg.r#type == 0 { + "info" + } else if msg.r#type == 1 { + "warn" + } else { + "error" + } + ); + let title = "Clipboard"; + let text = if d.is_empty() { + m.to_string() + } else { + format!("{} {}", m, d) + }; + ClipboardFile::NotifyCallback { + r#type: msgtype, + title: title.to_string(), + text, + } + } + _ => { + log::error!("notify_callback: failed to convert msg"); + return ERR_CODE_INVALID_PARAMETER; + } + } + }; + // no need to handle result here + allow_err!(send_data(conn_id as _, data)); + + 0 +} + +extern "C" fn handle_clipboard_files( + conn_id: UINT32, + n_files: size_t, + file_names: *mut *mut WCHAR, +) -> UINT { + if n_files == 0 { + return 0; + } + + let data = unsafe { + let mut files = Vec::new(); + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + for i in 0..n_files { + let file_name_ptr = *file_names.offset(i as isize); + if !file_name_ptr.is_null() { + let mut len = 0; + while *file_name_ptr.offset(len) != 0 { + len += 1; + } + let slice = std::slice::from_raw_parts(file_name_ptr, len as usize); + let os_string = OsString::from_wide(slice); + match os_string.to_str() { + Some(n) => match std::fs::metadata(n) { + Ok(meta) => { + if meta.is_file() { + files.push((n.to_owned(), meta.len())); + } + } + Err(e) => { + log::warn!( + "handle_clipboard_files: Failed to get metadata for file '{}': {}", + n, + e + ); + } + }, + None => { + log::warn!("handle_clipboard_files: Failed to convert file name to UTF-8"); + } + }; + } + } + if files.is_empty() { + return 0; + } + + ClipboardFile::Files { files } + }; + // no need to handle result here + allow_err!(send_data(conn_id as _, data)); + + 0 +} + +extern "C" fn client_format_list( + _context: *mut CliprdrClientContext, + clip_format_list: *const CLIPRDR_FORMAT_LIST, +) -> UINT { + let conn_id; + let mut format_list: Vec<(i32, String)> = Vec::new(); + unsafe { + let mut i = 0u32; + while i < (*clip_format_list).numFormats { + let format_data = &(*(*clip_format_list).formats.offset(i as isize)); + if format_data.formatName.is_null() { + format_list.push((format_data.formatId as i32, "".to_owned())); + } else { + let format_name = CStr::from_ptr(format_data.formatName).to_str(); + let format_name = match format_name { + Ok(n) => n.to_owned(), + Err(_) => { + log::warn!("failed to get format name"); + "".to_owned() + } + }; + format_list.push((format_data.formatId as i32, format_name)); + } + // log::debug!("format list item {}: format id: {}, format name: {}", i, format_data.formatId, &format_name); + i += 1; + } + conn_id = (*clip_format_list).connID as i32; + } + log::debug!( + "client_format_list called, client id: {}, format_list: {:?}", + conn_id, + &format_list + ); + let data = ClipboardFile::FormatList { format_list }; + // no need to handle result here + if conn_id == 0 { + // msg_channel is used for debug, VEC_MSG_CHANNEL cannot be inspected by the debugger. + let msg_channel = VEC_MSG_CHANNEL.read().unwrap(); + msg_channel + .iter() + .for_each(|msg_channel| allow_err!(msg_channel.sender.send(data.clone()))); + } else { + match send_data(conn_id, data) { + Ok(_) => {} + Err(e) => { + log::error!("failed to send format list: {:?}", e); + return ERR_CODE_SEND_MSG; + } + } + } + + 0 +} + +extern "C" fn client_format_list_response( + _context: *mut CliprdrClientContext, + format_list_response: *const CLIPRDR_FORMAT_LIST_RESPONSE, +) -> UINT { + let conn_id; + let msg_flags; + unsafe { + conn_id = (*format_list_response).connID as i32; + msg_flags = (*format_list_response).msgFlags as i32; + } + log::debug!( + "client_format_list_response called, client id: {}, msg_flags: {}", + conn_id, + msg_flags + ); + let data = ClipboardFile::FormatListResponse { msg_flags }; + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format list response: {:?}", e); + ERR_CODE_SEND_MSG + } + } +} + +extern "C" fn client_format_data_request( + _context: *mut CliprdrClientContext, + format_data_request: *const CLIPRDR_FORMAT_DATA_REQUEST, +) -> UINT { + let conn_id; + let requested_format_id; + unsafe { + conn_id = (*format_data_request).connID as i32; + requested_format_id = (*format_data_request).requestedFormatId as i32; + } + let data = ClipboardFile::FormatDataRequest { + requested_format_id, + }; + log::debug!( + "client_format_data_request called, conn_id: {}, requested_format_id: {}", + conn_id, + requested_format_id + ); + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format data request: {:?}", e); + ERR_CODE_SEND_MSG + } + } +} + +extern "C" fn client_format_data_response( + _context: *mut CliprdrClientContext, + format_data_response: *const CLIPRDR_FORMAT_DATA_RESPONSE, +) -> UINT { + let conn_id; + let msg_flags; + let format_data; + unsafe { + conn_id = (*format_data_response).connID as i32; + msg_flags = (*format_data_response).msgFlags as i32; + if (*format_data_response).requestedFormatData.is_null() { + format_data = Vec::new(); + } else { + format_data = std::slice::from_raw_parts( + (*format_data_response).requestedFormatData, + (*format_data_response).dataLen as usize, + ) + .to_vec(); + } + } + log::debug!( + "client_format_data_response called, client id: {}, msg_flags: {}", + conn_id, + msg_flags + ); + let data = ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + }; + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send format data response: {:?}", e); + ERR_CODE_SEND_MSG + } + } +} + +extern "C" fn client_file_contents_request( + _context: *mut CliprdrClientContext, + file_contents_request: *const CLIPRDR_FILE_CONTENTS_REQUEST, +) -> UINT { + // TODO: support huge file? + // if (!cliprdr->hasHugeFileSupport) + // { + // if (((UINT64)fileContentsRequest->cbRequested + fileContentsRequest->nPositionLow) > + // UINT32_MAX) + // return ERROR_INVALID_PARAMETER; + // if (fileContentsRequest->nPositionHigh != 0) + // return ERROR_INVALID_PARAMETER; + // } + + let conn_id; + let stream_id; + let list_index; + let dw_flags; + let n_position_low; + let n_position_high; + let cb_requested; + let have_clip_data_id; + let clip_data_id; + unsafe { + conn_id = (*file_contents_request).connID as i32; + stream_id = (*file_contents_request).streamId as i32; + list_index = (*file_contents_request).listIndex as i32; + dw_flags = (*file_contents_request).dwFlags as i32; + n_position_low = (*file_contents_request).nPositionLow as i32; + n_position_high = (*file_contents_request).nPositionHigh as i32; + cb_requested = (*file_contents_request).cbRequested as i32; + have_clip_data_id = (*file_contents_request).haveClipDataId == TRUE; + clip_data_id = (*file_contents_request).clipDataId as i32; + } + let data = ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id, + clip_data_id, + }; + log::debug!("client_file_contents_request called, data: {:?}", &data); + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send file contents request: {:?}", e); + ERR_CODE_SEND_MSG + } + } +} + +extern "C" fn client_file_contents_response( + _context: *mut CliprdrClientContext, + file_contents_response: *const CLIPRDR_FILE_CONTENTS_RESPONSE, +) -> UINT { + let conn_id; + let msg_flags; + let stream_id; + let requested_data; + unsafe { + conn_id = (*file_contents_response).connID as i32; + msg_flags = (*file_contents_response).msgFlags as i32; + stream_id = (*file_contents_response).streamId as i32; + if (*file_contents_response).requestedData.is_null() { + requested_data = Vec::new(); + } else { + requested_data = std::slice::from_raw_parts( + (*file_contents_response).requestedData, + (*file_contents_response).cbRequested as usize, + ) + .to_vec(); + } + } + let data = ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + requested_data, + }; + log::debug!( + "client_file_contents_response called, conn_id: {}, msg_flags: {}, stream_id: {}", + conn_id, + msg_flags, + stream_id + ); + match send_data(conn_id, data) { + Ok(_) => 0, + Err(e) => { + log::error!("failed to send file contents response: {:?}", e); + ERR_CODE_SEND_MSG + } + } +} diff --git a/vendor/rustdesk/libs/clipboard/src/windows/wf_cliprdr.c b/vendor/rustdesk/libs/clipboard/src/windows/wf_cliprdr.c new file mode 100644 index 0000000..95d1d1a --- /dev/null +++ b/vendor/rustdesk/libs/clipboard/src/windows/wf_cliprdr.c @@ -0,0 +1,3381 @@ +// to-do: TOO MANY compilation warnings. Fix them. + +/** + * FreeRDP: A Remote Desktop Protocol Implementation + * Windows Clipboard Redirection + * + * Copyright 2012 Jason Champion + * Copyright 2014 Marc-Andre Moreau + * Copyright 2015 Thincast Technologies GmbH + * Copyright 2015 DI (FH) Martin Haimberger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define CINTERFACE +#define COBJMACROS + +#include +#include +#include +#include +#include + +#include + +#include + +#include "../cliprdr.h" + +#define CLIPRDR_SVC_CHANNEL_NAME "cliprdr" + +/** + * Clipboard Formats + */ +#define CB_FORMAT_HTML 0xD010 +#define CB_FORMAT_PNG 0xD011 +#define CB_FORMAT_JPEG 0xD012 +#define CB_FORMAT_GIF 0xD013 +#define CB_FORMAT_TEXTURILIST 0xD014 +#define CB_FORMAT_GNOMECOPIEDFILES 0xD015 +#define CB_FORMAT_MATECOPIEDFILES 0xD016 + +/* CLIPRDR_HEADER.msgType */ +#define CB_MONITOR_READY 0x0001 +#define CB_FORMAT_LIST 0x0002 +#define CB_FORMAT_LIST_RESPONSE 0x0003 +#define CB_FORMAT_DATA_REQUEST 0x0004 +#define CB_FORMAT_DATA_RESPONSE 0x0005 +#define CB_TEMP_DIRECTORY 0x0006 +#define CB_CLIP_CAPS 0x0007 +#define CB_FILECONTENTS_REQUEST 0x0008 +#define CB_FILECONTENTS_RESPONSE 0x0009 +#define CB_LOCK_CLIPDATA 0x000A +#define CB_UNLOCK_CLIPDATA 0x000B + +/* CLIPRDR_HEADER.msgFlags */ +#define CB_RESPONSE_OK 0x0001 +#define CB_RESPONSE_FAIL 0x0002 +#define CB_ASCII_NAMES 0x0004 + +/* CLIPRDR_CAPS_SET.capabilitySetType */ +#define CB_CAPSTYPE_GENERAL 0x0001 + +/* CLIPRDR_GENERAL_CAPABILITY.lengthCapability */ +#define CB_CAPSTYPE_GENERAL_LEN 12 + +/* CLIPRDR_GENERAL_CAPABILITY.version */ +#define CB_CAPS_VERSION_1 0x00000001 +#define CB_CAPS_VERSION_2 0x00000002 + +/* CLIPRDR_GENERAL_CAPABILITY.generalFlags */ +#define CB_USE_LONG_FORMAT_NAMES 0x00000002 +#define CB_STREAM_FILECLIP_ENABLED 0x00000004 +#define CB_FILECLIP_NO_FILE_PATHS 0x00000008 +#define CB_CAN_LOCK_CLIPDATA 0x00000010 +#define CB_HUGE_FILE_SUPPORT_ENABLED 0x00000020 + +/* File Contents Request Flags */ +#define FILECONTENTS_SIZE 0x00000001 +#define FILECONTENTS_RANGE 0x00000002 + +/* Special Clipboard Response Formats */ + +struct _CLIPRDR_MFPICT +{ + UINT32 mappingMode; + UINT32 xExt; + UINT32 yExt; + UINT32 metaFileSize; + BYTE *metaFileData; +}; +typedef struct _CLIPRDR_MFPICT CLIPRDR_MFPICT; + +struct _FORMAT_IDS +{ + UINT32 connID; + UINT32 size; + UINT32 *formats; +}; +typedef struct _FORMAT_IDS FORMAT_IDS; + +/* File Contents Request Flags */ +#define FILECONTENTS_SIZE 0x00000001 +#define FILECONTENTS_RANGE 0x00000002 + +#define CHANNEL_RC_OK 0 +#define CHANNEL_RC_ALREADY_INITIALIZED 1 +#define CHANNEL_RC_NOT_INITIALIZED 2 +#define CHANNEL_RC_ALREADY_CONNECTED 3 +#define CHANNEL_RC_NOT_CONNECTED 4 +#define CHANNEL_RC_TOO_MANY_CHANNELS 5 +#define CHANNEL_RC_BAD_CHANNEL 6 +#define CHANNEL_RC_BAD_CHANNEL_HANDLE 7 +#define CHANNEL_RC_NO_BUFFER 8 +#define CHANNEL_RC_BAD_INIT_HANDLE 9 +#define CHANNEL_RC_NOT_OPEN 10 +#define CHANNEL_RC_BAD_PROC 11 +#define CHANNEL_RC_NO_MEMORY 12 +#define CHANNEL_RC_UNKNOWN_CHANNEL_NAME 13 +#define CHANNEL_RC_ALREADY_OPEN 14 +#define CHANNEL_RC_NOT_IN_VIRTUALCHANNELENTRY 15 +#define CHANNEL_RC_NULL_DATA 16 +#define CHANNEL_RC_ZERO_LENGTH 17 +#define CHANNEL_RC_INVALID_INSTANCE 18 +#define CHANNEL_RC_UNSUPPORTED_VERSION 19 +#define CHANNEL_RC_INITIALIZATION_ERROR 20 + +#define TAG "windows" + +#ifdef WITH_DEBUG_CLIPRDR +#define DEBUG_CLIPRDR(fmt, ...) \ + fprintf(stderr, "DEBUG %s[%d] %s() " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__); \ + fflush(stderr) +#else +#define DEBUG_CLIPRDR(fmt, ...) \ + do \ + { \ + } while (0) +#endif + +typedef BOOL(WINAPI *fnAddClipboardFormatListener)(HWND hwnd); +typedef BOOL(WINAPI *fnRemoveClipboardFormatListener)(HWND hwnd); +typedef BOOL(WINAPI *fnGetUpdatedClipboardFormats)(PUINT lpuiFormats, UINT cFormats, + PUINT pcFormatsOut); + +struct format_mapping +{ + UINT32 remote_format_id; + UINT32 local_format_id; + WCHAR *name; +}; +typedef struct format_mapping formatMapping; + +struct _CliprdrEnumFORMATETC +{ + IEnumFORMATETC iEnumFORMATETC; + + LONG m_lRefCount; + LONG m_nIndex; + LONG m_nNumFormats; + FORMATETC *m_pFormatEtc; +}; +typedef struct _CliprdrEnumFORMATETC CliprdrEnumFORMATETC; + +struct _CliprdrStream +{ + IStream iStream; + + LONG m_lRefCount; + ULONG m_lIndex; + ULARGE_INTEGER m_lSize; + ULARGE_INTEGER m_lOffset; + FILEDESCRIPTORW m_Dsc; + void *m_pData; + UINT32 m_connID; +}; +typedef struct _CliprdrStream CliprdrStream; + +struct _CliprdrDataObject +{ + IDataObject iDataObject; + + LONG m_lRefCount; + FORMATETC *m_pFormatEtc; + STGMEDIUM *m_pStgMedium; + ULONG m_nNumFormats; + ULONG m_nStreams; + IStream **m_pStream; + void *m_pData; + DWORD m_processID; + UINT32 m_connID; +}; +typedef struct _CliprdrDataObject CliprdrDataObject; + +struct wf_clipboard +{ + // wfContext* wfc; + // rdpChannels* channels; + CliprdrClientContext *context; + + BOOL sync; + UINT32 capabilities; + + // This flag is not really needed, + // but we can use it to double confirm that files can only be pasted after `Ctrl+C`. + // Not sure `is_file_descriptor_from_remote()` is engough to check all cases on all Windows. + BOOL copied; + + size_t map_size; + size_t map_capacity; + formatMapping *format_mappings; + + UINT32 requestedFormatId; + + HWND hwnd; + HANDLE hmem; + HANDLE thread; + HANDLE formatDataRespEvent; + BOOL formatDataRespReceived; + + LPDATAOBJECT data_obj; + HANDLE data_obj_mutex; + + ULONG req_fsize; + char *req_fdata; + HANDLE req_fevent; + BOOL req_f_received; + + size_t nFiles; + size_t file_array_size; + WCHAR **file_names; + size_t first_file_index; + FILEDESCRIPTORW **fileDescriptor; + + BOOL legacyApi; + HMODULE hUser32; + HWND hWndNextViewer; + fnAddClipboardFormatListener AddClipboardFormatListener; + fnRemoveClipboardFormatListener RemoveClipboardFormatListener; + fnGetUpdatedClipboardFormats GetUpdatedClipboardFormats; +}; +typedef struct wf_clipboard wfClipboard; + +#define WM_CLIPRDR_MESSAGE (WM_USER + 156) +#define OLE_SETCLIPBOARD 1 +#define DELAYED_RENDERING 2 + +BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr); +BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr); +BOOL wf_do_empty_cliprdr(wfClipboard *clipboard); + +static BOOL wf_create_file_obj(UINT32 *connID, wfClipboard *clipboard, IDataObject **ppDataObject); +static void wf_destroy_file_obj(IDataObject *instance); +static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format); +static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format); +static UINT cliprdr_send_lock(wfClipboard *clipboard); +static UINT cliprdr_send_unlock(wfClipboard *clipboard); +static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, + ULONG index, UINT32 flag, DWORD positionhigh, + DWORD positionlow, ULONG request); + +static BOOL is_file_descriptor_from_remote(); +static BOOL is_set_by_instance(wfClipboard *clipboard); + +static void CliprdrDataObject_Delete(CliprdrDataObject *instance); + +static CliprdrEnumFORMATETC *CliprdrEnumFORMATETC_New(ULONG nFormats, FORMATETC *pFormatEtc); +static void CliprdrEnumFORMATETC_Delete(CliprdrEnumFORMATETC *instance); + +static void CliprdrStream_Delete(CliprdrStream *instance); + +static BOOL try_open_clipboard(HWND hwnd) +{ + size_t x; + for (x = 0; x < 10; x++) + { + if (OpenClipboard(hwnd)) + return TRUE; + Sleep(10); + } + return FALSE; +} + +/** + * IStream + */ + +static HRESULT STDMETHODCALLTYPE CliprdrStream_QueryInterface(IStream *This, REFIID riid, + void **ppvObject) +{ + if (ppvObject == NULL) + return E_INVALIDARG; + + if (IsEqualIID(riid, &IID_IStream) || IsEqualIID(riid, &IID_IUnknown)) + { + IStream_AddRef(This); + *ppvObject = This; + return S_OK; + } + else + { + *ppvObject = 0; + return E_NOINTERFACE; + } +} + +static ULONG STDMETHODCALLTYPE CliprdrStream_AddRef(IStream *This) +{ + CliprdrStream *instance = (CliprdrStream *)This; + + if (!instance) + return 0; + + return InterlockedIncrement(&instance->m_lRefCount); +} + +static ULONG STDMETHODCALLTYPE CliprdrStream_Release(IStream *This) +{ + LONG count; + CliprdrStream *instance = (CliprdrStream *)This; + + if (!instance) + return 0; + + count = InterlockedDecrement(&instance->m_lRefCount); + + if (count == 0) + { + CliprdrStream_Delete(instance); + return 0; + } + else + { + return count; + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb, + ULONG *pcbRead) +{ + int ret; + CliprdrStream *instance = (CliprdrStream *)This; + wfClipboard *clipboard; + + if (!pv || !pcbRead || !instance) + return E_INVALIDARG; + + clipboard = (wfClipboard *)instance->m_pData; + *pcbRead = 0; + + if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart) + return S_FALSE; + + ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)This, instance->m_lIndex, + FILECONTENTS_RANGE, instance->m_lOffset.HighPart, + instance->m_lOffset.LowPart, cb); + + if (ret < 0) + return E_FAIL; + + if (clipboard->req_fdata) + { + CopyMemory(pv, clipboard->req_fdata, clipboard->req_fsize); + free(clipboard->req_fdata); + clipboard->req_fdata = NULL; + } + + *pcbRead = clipboard->req_fsize; + // Check overflow, can not be a real case + if ((instance->m_lOffset.QuadPart + clipboard->req_fsize) < instance->m_lOffset.QuadPart) { + // It's better to crash to release the explorer.exe + // This is a critical error, because the explorer is waiting for the data + // and the m_lOffset is wrong(overflowed) + return S_FALSE; + } + instance->m_lOffset.QuadPart += clipboard->req_fsize; + + if (clipboard->req_fsize < cb) + return S_FALSE; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Write(IStream *This, const void *pv, ULONG cb, + ULONG *pcbWritten) +{ + (void)This; + (void)pv; + (void)cb; + (void)pcbWritten; + return STG_E_ACCESSDENIED; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Seek(IStream *This, LARGE_INTEGER dlibMove, + DWORD dwOrigin, ULARGE_INTEGER *plibNewPosition) +{ + ULONGLONG newoffset; + CliprdrStream *instance = (CliprdrStream *)This; + + if (!instance) + return E_INVALIDARG; + + newoffset = instance->m_lOffset.QuadPart; + + switch (dwOrigin) + { + case STREAM_SEEK_SET: + newoffset = dlibMove.QuadPart; + break; + + case STREAM_SEEK_CUR: + newoffset += dlibMove.QuadPart; + break; + + case STREAM_SEEK_END: + newoffset = instance->m_lSize.QuadPart + dlibMove.QuadPart; + break; + + default: + return E_INVALIDARG; + } + + if (newoffset < 0 || newoffset >= instance->m_lSize.QuadPart) + return E_FAIL; + + instance->m_lOffset.QuadPart = newoffset; + + if (plibNewPosition) + plibNewPosition->QuadPart = instance->m_lOffset.QuadPart; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_SetSize(IStream *This, ULARGE_INTEGER libNewSize) +{ + (void)This; + (void)libNewSize; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_CopyTo(IStream *This, IStream *pstm, + ULARGE_INTEGER cb, ULARGE_INTEGER *pcbRead, + ULARGE_INTEGER *pcbWritten) +{ + (void)This; + (void)pstm; + (void)cb; + (void)pcbRead; + (void)pcbWritten; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Commit(IStream *This, DWORD grfCommitFlags) +{ + (void)This; + (void)grfCommitFlags; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Revert(IStream *This) +{ + (void)This; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_LockRegion(IStream *This, ULARGE_INTEGER libOffset, + ULARGE_INTEGER cb, DWORD dwLockType) +{ + (void)This; + (void)libOffset; + (void)cb; + (void)dwLockType; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_UnlockRegion(IStream *This, ULARGE_INTEGER libOffset, + ULARGE_INTEGER cb, DWORD dwLockType) +{ + (void)This; + (void)libOffset; + (void)cb; + (void)dwLockType; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Stat(IStream *This, STATSTG *pstatstg, + DWORD grfStatFlag) +{ + CliprdrStream *instance = (CliprdrStream *)This; + + if (!instance) + return E_INVALIDARG; + + if (pstatstg == NULL) + return STG_E_INVALIDPOINTER; + + ZeroMemory(pstatstg, sizeof(STATSTG)); + + switch (grfStatFlag) + { + case STATFLAG_DEFAULT: + return STG_E_INSUFFICIENTMEMORY; + + case STATFLAG_NONAME: + pstatstg->cbSize.QuadPart = instance->m_lSize.QuadPart; + pstatstg->grfLocksSupported = LOCK_EXCLUSIVE; + pstatstg->grfMode = GENERIC_READ; + pstatstg->grfStateBits = 0; + pstatstg->type = STGTY_STREAM; + break; + + case STATFLAG_NOOPEN: + return STG_E_INVALIDFLAG; + + default: + return STG_E_INVALIDFLAG; + } + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrStream_Clone(IStream *This, IStream **ppstm) +{ + (void)This; + (void)ppstm; + return E_NOTIMPL; +} + +static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, const FILEDESCRIPTORW *dsc) +{ + IStream *iStream = NULL; + BOOL success = FALSE; + BOOL isDir = FALSE; + CliprdrStream *instance = NULL; + wfClipboard *clipboard = (wfClipboard *)pData; + + if (!(pData && dsc)) + { + return NULL; + } + + instance = (CliprdrStream *)calloc(1, sizeof(CliprdrStream)); + + if (instance) + { + instance->m_Dsc = *dsc; + iStream = &instance->iStream; + iStream->lpVtbl = (IStreamVtbl *)calloc(1, sizeof(IStreamVtbl)); + + if (iStream->lpVtbl) + { + iStream->lpVtbl->QueryInterface = CliprdrStream_QueryInterface; + iStream->lpVtbl->AddRef = CliprdrStream_AddRef; + iStream->lpVtbl->Release = CliprdrStream_Release; + iStream->lpVtbl->Read = CliprdrStream_Read; + iStream->lpVtbl->Write = CliprdrStream_Write; + iStream->lpVtbl->Seek = CliprdrStream_Seek; + iStream->lpVtbl->SetSize = CliprdrStream_SetSize; + iStream->lpVtbl->CopyTo = CliprdrStream_CopyTo; + iStream->lpVtbl->Commit = CliprdrStream_Commit; + iStream->lpVtbl->Revert = CliprdrStream_Revert; + iStream->lpVtbl->LockRegion = CliprdrStream_LockRegion; + iStream->lpVtbl->UnlockRegion = CliprdrStream_UnlockRegion; + iStream->lpVtbl->Stat = CliprdrStream_Stat; + iStream->lpVtbl->Clone = CliprdrStream_Clone; + instance->m_lRefCount = 1; + instance->m_lIndex = index; + instance->m_pData = pData; + instance->m_lOffset.QuadPart = 0; + instance->m_connID = connID; + + if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES) + { + if (instance->m_Dsc.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + isDir = TRUE; + } + + if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir) + { + /* get content size of this stream */ + if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)instance, + instance->m_lIndex, FILECONTENTS_SIZE, 0, 0, + 8) == CHANNEL_RC_OK) + { + success = TRUE; + } + + if (clipboard->req_fdata != NULL) + { + instance->m_lSize.QuadPart = *((LONGLONG *)clipboard->req_fdata); + free(clipboard->req_fdata); + clipboard->req_fdata = NULL; + } + } + else { + instance->m_lSize.QuadPart = + ((UINT64)instance->m_Dsc.nFileSizeHigh << 32) | instance->m_Dsc.nFileSizeLow; + success = TRUE; + } + } + } + + if (!success) + { + CliprdrStream_Delete(instance); + instance = NULL; + } + + return instance; +} + +void CliprdrStream_Delete(CliprdrStream *instance) +{ + if (instance) + { + free(instance->iStream.lpVtbl); + instance->iStream.lpVtbl = NULL; + free(instance); + } +} + +/** + * IDataObject + */ + +static LONG cliprdr_lookup_format(CliprdrDataObject *instance, FORMATETC *pFormatEtc) +{ + ULONG i; + + if (!instance || !pFormatEtc) + return -1; + + for (i = 0; i < instance->m_nNumFormats; i++) + { + if ((pFormatEtc->tymed & instance->m_pFormatEtc[i].tymed) && + pFormatEtc->cfFormat == instance->m_pFormatEtc[i].cfFormat && + pFormatEtc->dwAspect & instance->m_pFormatEtc[i].dwAspect) + { + return (LONG)i; + } + } + + return -1; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_QueryInterface(IDataObject *This, REFIID riid, + void **ppvObject) +{ + (void)This; + + if (!ppvObject) + return E_INVALIDARG; + + if (IsEqualIID(riid, &IID_IDataObject) || IsEqualIID(riid, &IID_IUnknown)) + { + IDataObject_AddRef(This); + *ppvObject = This; + return S_OK; + } + else + { + *ppvObject = 0; + return E_NOINTERFACE; + } +} + +static ULONG STDMETHODCALLTYPE CliprdrDataObject_AddRef(IDataObject *This) +{ + CliprdrDataObject *instance = (CliprdrDataObject *)This; + + if (!instance) + return E_INVALIDARG; + + return InterlockedIncrement(&instance->m_lRefCount); +} + +static ULONG STDMETHODCALLTYPE CliprdrDataObject_Release(IDataObject *This) +{ + LONG count; + CliprdrDataObject *instance = (CliprdrDataObject *)This; + + if (!instance) + return E_INVALIDARG; + + count = InterlockedDecrement(&instance->m_lRefCount); + + if (count == 0) + { + CliprdrDataObject_Delete(instance); + return 0; + } + else + return count; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetData(IDataObject *This, FORMATETC *pFormatEtc, + STGMEDIUM *pMedium) +{ + ULONG i; + LONG idx; + CliprdrDataObject *instance = (CliprdrDataObject *)This; + wfClipboard *clipboard; + + if (!pFormatEtc || !pMedium || !instance) + return E_INVALIDARG; + + // Not the same process id + if (instance->m_processID != GetCurrentProcessId()) + { + return E_INVALIDARG; + } + + clipboard = (wfClipboard *)instance->m_pData; + + if (!clipboard) + return E_INVALIDARG; + + // If `Ctrl+C` is not pressed yet, do not handle the file paste, and empty the clipboard. + if (!clipboard->copied) { + if (try_open_clipboard(clipboard->hwnd)) { + EmptyClipboard(); + CloseClipboard(); + } + return E_UNEXPECTED; + } + + if ((idx = cliprdr_lookup_format(instance, pFormatEtc)) == -1) + { + // empty clipboard here? + return DV_E_FORMATETC; + } + + pMedium->tymed = instance->m_pFormatEtc[idx].tymed; + pMedium->pUnkForRelease = 0; + + if (instance->m_pFormatEtc[idx].cfFormat == RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW)) + { + // FILEGROUPDESCRIPTOR *dsc; + FILEGROUPDESCRIPTORW *dsc; + // DWORD remote_format_id = get_remote_format_id(clipboard, instance->m_pFormatEtc[idx].cfFormat); + // FIXME: origin code may be failed here??? + if (cliprdr_send_data_request(instance->m_connID, clipboard, instance->m_pFormatEtc[idx].cfFormat) != 0) + { + return E_UNEXPECTED; + } + if (!clipboard->hmem) + { + return E_UNEXPECTED; + } + + pMedium->hGlobal = clipboard->hmem; /* points to a FILEGROUPDESCRIPTOR structure */ + /* GlobalLock returns a pointer to the first byte of the memory block, + * in which is a FILEGROUPDESCRIPTOR structure, whose first UINT member + * is the number of FILEDESCRIPTOR's */ + // dsc = (FILEGROUPDESCRIPTOR *)GlobalLock(clipboard->hmem); + dsc = (FILEGROUPDESCRIPTORW *)GlobalLock(clipboard->hmem); + instance->m_nStreams = dsc->cItems; + GlobalUnlock(clipboard->hmem); + + if (instance->m_nStreams > 0) + { + if (!instance->m_pStream) + { + instance->m_pStream = (LPSTREAM *)calloc(instance->m_nStreams, sizeof(LPSTREAM)); + + if (instance->m_pStream) + { + for (i = 0; i < instance->m_nStreams; i++) + { + instance->m_pStream[i] = + (IStream *)CliprdrStream_New(instance->m_connID, i, clipboard, &dsc->fgd[i]); + + if (!instance->m_pStream[i]) + return E_OUTOFMEMORY; + } + } + } + } + + if (!instance->m_pStream) + { + if (clipboard->hmem) + { + GlobalFree(clipboard->hmem); + clipboard->hmem = NULL; + } + + pMedium->hGlobal = NULL; + return E_OUTOFMEMORY; + } + } + else if (instance->m_pFormatEtc[idx].cfFormat == RegisterClipboardFormat(CFSTR_FILECONTENTS)) + { + if ((pFormatEtc->lindex >= 0) && ((ULONG)pFormatEtc->lindex < instance->m_nStreams)) + { + pMedium->pstm = instance->m_pStream[pFormatEtc->lindex]; + IDataObject_AddRef(instance->m_pStream[pFormatEtc->lindex]); + } + else + return E_INVALIDARG; + } + else + return E_UNEXPECTED; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetDataHere(IDataObject *This, + FORMATETC *pformatetc, + STGMEDIUM *pmedium) +{ + (void)This; + (void)pformatetc; + (void)pmedium; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_QueryGetData(IDataObject *This, + FORMATETC *pformatetc) +{ + CliprdrDataObject *instance = (CliprdrDataObject *)This; + + if (!pformatetc) + return E_INVALIDARG; + + if (cliprdr_lookup_format(instance, pformatetc) == -1) + return DV_E_FORMATETC; + + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetCanonicalFormatEtc(IDataObject *This, + FORMATETC *pformatetcIn, + FORMATETC *pformatetcOut) +{ + (void)This; + (void)pformatetcIn; + + if (!pformatetcOut) + return E_INVALIDARG; + + pformatetcOut->ptd = NULL; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_SetData(IDataObject *This, FORMATETC *pformatetc, + STGMEDIUM *pmedium, BOOL fRelease) +{ + (void)This; + (void)pformatetc; + (void)pmedium; + (void)fRelease; + return E_NOTIMPL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumFormatEtc(IDataObject *This, + DWORD dwDirection, + IEnumFORMATETC **ppenumFormatEtc) +{ + CliprdrDataObject *instance = (CliprdrDataObject *)This; + + if (!instance || !ppenumFormatEtc) + return E_INVALIDARG; + + if (dwDirection == DATADIR_GET) + { + *ppenumFormatEtc = (IEnumFORMATETC *)CliprdrEnumFORMATETC_New(instance->m_nNumFormats, + instance->m_pFormatEtc); + return (*ppenumFormatEtc) ? S_OK : E_OUTOFMEMORY; + } + else + { + return E_NOTIMPL; + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_DAdvise(IDataObject *This, FORMATETC *pformatetc, + DWORD advf, IAdviseSink *pAdvSink, + DWORD *pdwConnection) +{ + (void)This; + (void)pformatetc; + (void)advf; + (void)pAdvSink; + (void)pdwConnection; + return OLE_E_ADVISENOTSUPPORTED; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_DUnadvise(IDataObject *This, DWORD dwConnection) +{ + (void)This; + (void)dwConnection; + return OLE_E_ADVISENOTSUPPORTED; +} + +static HRESULT STDMETHODCALLTYPE CliprdrDataObject_EnumDAdvise(IDataObject *This, + IEnumSTATDATA **ppenumAdvise) +{ + (void)This; + (void)ppenumAdvise; + return OLE_E_ADVISENOTSUPPORTED; +} + +static CliprdrDataObject *CliprdrDataObject_New(UINT32 connID, FORMATETC *fmtetc, STGMEDIUM *stgmed, ULONG count, + void *data) +{ + CliprdrDataObject *instance = NULL; + IDataObject *iDataObject = NULL; + instance = (CliprdrDataObject *)calloc(1, sizeof(CliprdrDataObject)); + + if (!instance) + goto error; + + instance->m_pFormatEtc = NULL; + instance->m_pStgMedium = NULL; + + iDataObject = &instance->iDataObject; + iDataObject->lpVtbl = NULL; + iDataObject->lpVtbl = (IDataObjectVtbl *)calloc(1, sizeof(IDataObjectVtbl)); + + if (!iDataObject->lpVtbl) + goto error; + + iDataObject->lpVtbl->QueryInterface = CliprdrDataObject_QueryInterface; + iDataObject->lpVtbl->AddRef = CliprdrDataObject_AddRef; + iDataObject->lpVtbl->Release = CliprdrDataObject_Release; + iDataObject->lpVtbl->GetData = CliprdrDataObject_GetData; + iDataObject->lpVtbl->GetDataHere = CliprdrDataObject_GetDataHere; + iDataObject->lpVtbl->QueryGetData = CliprdrDataObject_QueryGetData; + iDataObject->lpVtbl->GetCanonicalFormatEtc = CliprdrDataObject_GetCanonicalFormatEtc; + iDataObject->lpVtbl->SetData = CliprdrDataObject_SetData; + iDataObject->lpVtbl->EnumFormatEtc = CliprdrDataObject_EnumFormatEtc; + iDataObject->lpVtbl->DAdvise = CliprdrDataObject_DAdvise; + iDataObject->lpVtbl->DUnadvise = CliprdrDataObject_DUnadvise; + iDataObject->lpVtbl->EnumDAdvise = CliprdrDataObject_EnumDAdvise; + instance->m_lRefCount = 1; + instance->m_nNumFormats = count; + instance->m_pData = data; + instance->m_nStreams = 0; + instance->m_pStream = NULL; + instance->m_processID = GetCurrentProcessId(); + instance->m_connID = connID; + + if (count > 0) + { + ULONG i; + instance->m_pFormatEtc = (FORMATETC *)calloc(count, sizeof(FORMATETC)); + + if (!instance->m_pFormatEtc) + goto error; + + instance->m_pStgMedium = (STGMEDIUM *)calloc(count, sizeof(STGMEDIUM)); + + if (!instance->m_pStgMedium) + goto error; + + for (i = 0; i < count; i++) + { + instance->m_pFormatEtc[i] = fmtetc[i]; + instance->m_pStgMedium[i] = stgmed[i]; + } + } + + return instance; +error: + if (iDataObject && iDataObject->lpVtbl) + { + free(iDataObject->lpVtbl); + } + if (instance) + { + if (instance->m_pFormatEtc) + { + free(instance->m_pFormatEtc); + } + + if (instance->m_pStgMedium) + { + free(instance->m_pStgMedium); + } + + CliprdrDataObject_Delete(instance); + } + return NULL; +} + +void CliprdrDataObject_Delete(CliprdrDataObject *instance) +{ + if (instance) + { + free(instance->iDataObject.lpVtbl); + free(instance->m_pFormatEtc); + free(instance->m_pStgMedium); + + if (instance->m_pStream) + { + ULONG i; + + for (i = 0; i < instance->m_nStreams; i++) + CliprdrStream_Release(instance->m_pStream[i]); + + free(instance->m_pStream); + } + + free(instance); + } +} + +static BOOL wf_create_file_obj(UINT32 *connID, wfClipboard *clipboard, IDataObject **ppDataObject) +{ + FORMATETC fmtetc[2]; + STGMEDIUM stgmeds[2]; + + if (!ppDataObject) + return FALSE; + + fmtetc[0].cfFormat = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + fmtetc[0].dwAspect = DVASPECT_CONTENT; + fmtetc[0].lindex = 0; + fmtetc[0].ptd = NULL; + fmtetc[0].tymed = TYMED_HGLOBAL; + stgmeds[0].tymed = TYMED_HGLOBAL; + stgmeds[0].hGlobal = NULL; + stgmeds[0].pUnkForRelease = NULL; + fmtetc[1].cfFormat = RegisterClipboardFormat(CFSTR_FILECONTENTS); + fmtetc[1].dwAspect = DVASPECT_CONTENT; + fmtetc[1].lindex = 0; + fmtetc[1].ptd = NULL; + fmtetc[1].tymed = TYMED_ISTREAM; + stgmeds[1].tymed = TYMED_ISTREAM; + stgmeds[1].pstm = NULL; + stgmeds[1].pUnkForRelease = NULL; + *ppDataObject = (IDataObject *)CliprdrDataObject_New(*connID, fmtetc, stgmeds, 2, clipboard); + return (*ppDataObject) ? TRUE : FALSE; +} + +static void wf_destroy_file_obj(IDataObject *instance) +{ + if (instance) + IDataObject_Release(instance); +} + +/** + * IEnumFORMATETC + */ + +static void cliprdr_format_deep_copy(FORMATETC *dest, FORMATETC *source) +{ + *dest = *source; + + if (source->ptd) + { + dest->ptd = (DVTARGETDEVICE *)CoTaskMemAlloc(sizeof(DVTARGETDEVICE)); + + if (dest->ptd) + *(dest->ptd) = *(source->ptd); + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_QueryInterface(IEnumFORMATETC *This, + REFIID riid, void **ppvObject) +{ + (void)This; + if (!ppvObject) + return E_INVALIDARG; + + if (IsEqualIID(riid, &IID_IEnumFORMATETC) || IsEqualIID(riid, &IID_IUnknown)) + { + IEnumFORMATETC_AddRef(This); + *ppvObject = This; + return S_OK; + } + else + { + *ppvObject = 0; + return E_NOINTERFACE; + } +} + +static ULONG STDMETHODCALLTYPE CliprdrEnumFORMATETC_AddRef(IEnumFORMATETC *This) +{ + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance) + return 0; + + return InterlockedIncrement(&instance->m_lRefCount); +} + +static ULONG STDMETHODCALLTYPE CliprdrEnumFORMATETC_Release(IEnumFORMATETC *This) +{ + LONG count; + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance) + return 0; + + count = InterlockedDecrement(&instance->m_lRefCount); + + if (count == 0) + { + CliprdrEnumFORMATETC_Delete(instance); + return 0; + } + else + { + return count; + } +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Next(IEnumFORMATETC *This, ULONG celt, + FORMATETC *rgelt, ULONG *pceltFetched) +{ + ULONG copied = 0; + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance || !celt || !rgelt) + return E_INVALIDARG; + + while ((instance->m_nIndex < instance->m_nNumFormats) && (copied < celt)) + { + cliprdr_format_deep_copy(&rgelt[copied++], &instance->m_pFormatEtc[instance->m_nIndex++]); + } + + if (pceltFetched != 0) + *pceltFetched = copied; + + return (copied == celt) ? S_OK : E_FAIL; +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Skip(IEnumFORMATETC *This, ULONG celt) +{ + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance) + return E_INVALIDARG; + + if (instance->m_nIndex + (LONG)celt > instance->m_nNumFormats) + return E_FAIL; + + instance->m_nIndex += celt; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Reset(IEnumFORMATETC *This) +{ + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance) + return E_INVALIDARG; + + instance->m_nIndex = 0; + return S_OK; +} + +static HRESULT STDMETHODCALLTYPE CliprdrEnumFORMATETC_Clone(IEnumFORMATETC *This, + IEnumFORMATETC **ppEnum) +{ + CliprdrEnumFORMATETC *instance = (CliprdrEnumFORMATETC *)This; + + if (!instance || !ppEnum) + return E_INVALIDARG; + + *ppEnum = + (IEnumFORMATETC *)CliprdrEnumFORMATETC_New(instance->m_nNumFormats, instance->m_pFormatEtc); + + if (!*ppEnum) + return E_OUTOFMEMORY; + + ((CliprdrEnumFORMATETC *)*ppEnum)->m_nIndex = instance->m_nIndex; + return S_OK; +} + +CliprdrEnumFORMATETC *CliprdrEnumFORMATETC_New(ULONG nFormats, FORMATETC *pFormatEtc) +{ + ULONG i; + CliprdrEnumFORMATETC *instance; + IEnumFORMATETC *iEnumFORMATETC; + + if ((nFormats != 0) && !pFormatEtc) + return NULL; + + instance = (CliprdrEnumFORMATETC *)calloc(1, sizeof(CliprdrEnumFORMATETC)); + + if (!instance) + goto error; + + iEnumFORMATETC = &instance->iEnumFORMATETC; + iEnumFORMATETC->lpVtbl = (IEnumFORMATETCVtbl *)calloc(1, sizeof(IEnumFORMATETCVtbl)); + + if (!iEnumFORMATETC->lpVtbl) + goto error; + + iEnumFORMATETC->lpVtbl->QueryInterface = CliprdrEnumFORMATETC_QueryInterface; + iEnumFORMATETC->lpVtbl->AddRef = CliprdrEnumFORMATETC_AddRef; + iEnumFORMATETC->lpVtbl->Release = CliprdrEnumFORMATETC_Release; + iEnumFORMATETC->lpVtbl->Next = CliprdrEnumFORMATETC_Next; + iEnumFORMATETC->lpVtbl->Skip = CliprdrEnumFORMATETC_Skip; + iEnumFORMATETC->lpVtbl->Reset = CliprdrEnumFORMATETC_Reset; + iEnumFORMATETC->lpVtbl->Clone = CliprdrEnumFORMATETC_Clone; + instance->m_lRefCount = 1; + instance->m_nIndex = 0; + instance->m_nNumFormats = nFormats; + + if (nFormats > 0) + { + instance->m_pFormatEtc = (FORMATETC *)calloc(nFormats, sizeof(FORMATETC)); + + if (!instance->m_pFormatEtc) + goto error; + + for (i = 0; i < nFormats; i++) + cliprdr_format_deep_copy(&instance->m_pFormatEtc[i], &pFormatEtc[i]); + } + + return instance; +error: + CliprdrEnumFORMATETC_Delete(instance); + return NULL; +} + +void CliprdrEnumFORMATETC_Delete(CliprdrEnumFORMATETC *instance) +{ + LONG i; + + if (instance) + { + free(instance->iEnumFORMATETC.lpVtbl); + + if (instance->m_pFormatEtc) + { + for (i = 0; i < instance->m_nNumFormats; i++) + { + if (instance->m_pFormatEtc[i].ptd) + CoTaskMemFree(instance->m_pFormatEtc[i].ptd); + } + + free(instance->m_pFormatEtc); + } + + free(instance); + } +} + +/***********************************************************************************/ + +static UINT32 get_local_format_id_by_name(wfClipboard *clipboard, const TCHAR *format_name) +{ + size_t i; + formatMapping *map; + WCHAR *unicode_name; +#if !defined(UNICODE) + size_t size; + int towchar_count; +#endif + + if (!clipboard || !format_name) + return 0; + +#if defined(UNICODE) + unicode_name = _wcsdup(format_name); + if (!unicode_name) + return 0; +#else + size = _tcslen(format_name); + unicode_name = calloc(size + 1, sizeof(WCHAR)); + + if (!unicode_name) + return 0; + + towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), NULL, 0); + if (towchar_count <= 0 || towchar_count > size) + return 0; + towchar_count = MultiByteToWideChar(CP_OEMCP, 0, format_name, strlen(format_name), unicode_name, size); + if (towchar_count <= 0) + return 0; +#endif + + for (i = 0; i < clipboard->map_size; i++) + { + map = &clipboard->format_mappings[i]; + + if (map->name) + { + if (wcscmp(map->name, unicode_name) == 0) + { + free(unicode_name); + return map->local_format_id; + } + } + } + + free(unicode_name); + return 0; +} + +static BOOL file_transferring(wfClipboard *clipboard) +{ + return get_local_format_id_by_name(clipboard, CFSTR_FILEDESCRIPTORW) ? TRUE : FALSE; +} + +static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format) +{ + UINT32 i; + formatMapping *map; + + if (!clipboard) + return 0; + + for (i = 0; i < clipboard->map_size; i++) + { + map = &clipboard->format_mappings[i]; + + if (map->local_format_id == local_format) + return map->remote_format_id; + } + + return local_format; +} + +static void map_ensure_capacity(wfClipboard *clipboard) +{ + if (!clipboard) + return; + + if (clipboard->map_size >= clipboard->map_capacity) + { + size_t new_size; + formatMapping *new_map; + new_size = clipboard->map_capacity * 2; + new_map = + (formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * new_size); + + if (!new_map) + return; + + clipboard->format_mappings = new_map; + clipboard->map_capacity = new_size; + } +} + +static BOOL clear_format_map(wfClipboard *clipboard) +{ + size_t i; + formatMapping *map; + + if (!clipboard) + return FALSE; + + if (clipboard->format_mappings) + { + for (i = 0; i < clipboard->map_capacity; i++) + { + map = &clipboard->format_mappings[i]; + map->remote_format_id = 0; + map->local_format_id = 0; + free(map->name); + map->name = NULL; + } + } + + clipboard->map_size = 0; + return TRUE; +} + +static UINT cliprdr_send_tempdir(wfClipboard *clipboard) +{ + CLIPRDR_TEMP_DIRECTORY tempDirectory; + + if (!clipboard) + return -1; + + // to-do: + // Directly use the environment variable `TEMP` is not safe. + // But this function is not used for now. + if (GetEnvironmentVariableA("TEMP", tempDirectory.szTempDir, sizeof(tempDirectory.szTempDir)) == + 0) + return -1; + + return clipboard->context->TempDirectory(clipboard->context, &tempDirectory); +} + +static BOOL cliprdr_GetUpdatedClipboardFormats(wfClipboard *clipboard, PUINT lpuiFormats, + UINT cFormats, PUINT pcFormatsOut) +{ + UINT index = 0; + UINT format = 0; + BOOL clipboardOpen = FALSE; + + if (!clipboard->legacyApi) + return clipboard->GetUpdatedClipboardFormats(lpuiFormats, cFormats, pcFormatsOut); + + clipboardOpen = try_open_clipboard(clipboard->hwnd); + + if (!clipboardOpen) + { + *pcFormatsOut = 0; + return TRUE; /* Other app holding clipboard */ + } + + while (index < cFormats) + { + format = EnumClipboardFormats(format); + + if (!format) + break; + + lpuiFormats[index] = format; + index++; + } + + *pcFormatsOut = index; + CloseClipboard(); + return TRUE; +} + +static UINT cliprdr_send_format_list(wfClipboard *clipboard, UINT32 connID) +{ + UINT rc; + int count = 0; + UINT32 index; + UINT32 numFormats = 0; + UINT32 formatId = 0; + char formatName[1024]; + CLIPRDR_FORMAT *formats = NULL; + CLIPRDR_FORMAT_LIST formatList = {0}; + + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + if (!IsClipboardFormatAvailable(CF_HDROP)) + { + return ERROR_SUCCESS; + } + + ZeroMemory(&formatList, sizeof(CLIPRDR_FORMAT_LIST)); + + /* Ignore if other app is holding clipboard */ + if (try_open_clipboard(clipboard->hwnd)) + { + // If current process is running as service with SYSTEM user. + // Clipboard api works fine for text, but copying files works no good. + // GetLastError() returns various error codes + count = CountClipboardFormats(); + if (count == 0) + { + CloseClipboard(); + return CHANNEL_RC_NULL_DATA; + } + + numFormats = (UINT32)count; + formats = (CLIPRDR_FORMAT *)calloc(numFormats, sizeof(CLIPRDR_FORMAT)); + + if (!formats) + { + CloseClipboard(); + return CHANNEL_RC_NO_MEMORY; + } + + index = 0; + // IsClipboardFormatAvailable(CF_HDROP) is checked above + UINT fsid = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + UINT fcid = RegisterClipboardFormat(CFSTR_FILECONTENTS); + formats[index++].formatId = fsid; + formats[index++].formatId = fcid; + numFormats = index; + + if (!CloseClipboard()) + { + free(formats); + return ERROR_INTERNAL_ERROR; + } + + for (index = 0; index < numFormats; index++) + { + if (GetClipboardFormatNameA(formats[index].formatId, formatName, sizeof(formatName))) + { + formats[index].formatName = _strdup(formatName); + } + else + { + formats[index].formatName = NULL; + } + } + } + + formatList.connID = connID; + formatList.numFormats = numFormats; + formatList.formats = formats; + formatList.msgType = CB_FORMAT_LIST; + + // send + rc = clipboard->context->ClientFormatList(clipboard->context, &formatList); + // No need to check `rc`, `copied` is only used to indicate `Ctrl+C` is pressed. + clipboard->copied = TRUE; + + for (index = 0; index < numFormats; index++) + { + if (formats[index].formatName != NULL) + { + free(formats[index].formatName); + formats[index].formatName = NULL; + } + } + free(formats); + + return rc; +} + +// Ensure the event is not signaled, and reset it if it is. +UINT try_reset_event(HANDLE event) +{ + if (!event) + { + return ERROR_INTERNAL_ERROR; + } + + DWORD result = WaitForSingleObject(event, 0); + if (result == WAIT_OBJECT_0) + { + if (!ResetEvent(event)) + { + return GetLastError(); + } + else + { + return ERROR_SUCCESS; + } + } + else if (result == WAIT_TIMEOUT) + { + return ERROR_SUCCESS; + } + else + { + return ERROR_INTERNAL_ERROR; + } +} + +UINT wait_response_event(UINT32 connID, wfClipboard *clipboard, HANDLE event, BOOL* recvedFlag, void **data) +{ + UINT rc = ERROR_SUCCESS; + clipboard->context->IsStopped = FALSE; + DWORD waitOnceTimeoutMillis = 50; + int waitCount = 1000 * clipboard->context->ResponseWaitTimeoutSecs / waitOnceTimeoutMillis; + int i = 0; + for (; i < waitCount; i++) + { + DWORD waitRes = WaitForSingleObject(event, waitOnceTimeoutMillis); + if (waitRes == WAIT_TIMEOUT && clipboard->context->IsStopped == FALSE) + { + if ((*recvedFlag) == TRUE) { + // The data has been received, but the event is still not signaled. + // We just skip the rest of the waiting and reset the flag. + *recvedFlag = FALSE; + // Explicitly set the waitRes to WAIT_OBJECT_0, because we have received the data. + waitRes = WAIT_OBJECT_0; + } else { + // The data has not been received yet, we should continue to wait. + continue; + } + } + + if (!ResetEvent(event)) + { + // NOTE: critical error here, crash may be better + } + + if (clipboard->context->IsStopped == TRUE) + { + wf_do_empty_cliprdr(clipboard); + rc = ERROR_INTERNAL_ERROR; + } + + if (waitRes != WAIT_OBJECT_0) + { + return ERROR_INTERNAL_ERROR; + } + + if ((*data) == NULL) + { + rc = ERROR_INTERNAL_ERROR; + } + + return rc; + } + + if (i == waitCount) + { + NOTIFICATION_MESSAGE msg; + msg.type = 2; + msg.msg = "clipboard_wait_response_timeout_tip"; + msg.details = NULL; + clipboard->context->NotifyClipboardMsg(connID, &msg); + rc = ERROR_INTERNAL_ERROR; + + if (!ResetEvent(event)) + { + // NOTE: critical error here, crash may be better + } + } + else if ((*data) != NULL) + { + if (!ResetEvent(event)) + { + // NOTE: critical error here, crash may be better + } + return ERROR_SUCCESS; + } + + return ERROR_INTERNAL_ERROR; +} + +static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 formatId) +{ + UINT rc; + UINT32 remoteFormatId; + CLIPRDR_FORMAT_DATA_REQUEST formatDataRequest; + + if (!clipboard || !clipboard->context || !clipboard->context->ClientFormatDataRequest) + return ERROR_INTERNAL_ERROR; + + rc = try_reset_event(clipboard->formatDataRespEvent); + if (rc != ERROR_SUCCESS) + { + return rc; + } + clipboard->formatDataRespReceived = FALSE; + + remoteFormatId = get_remote_format_id(clipboard, formatId); + + formatDataRequest.connID = connID; + formatDataRequest.requestedFormatId = remoteFormatId; + clipboard->requestedFormatId = formatId; + rc = clipboard->context->ClientFormatDataRequest(clipboard->context, &formatDataRequest); + if (rc != ERROR_SUCCESS) + { + return rc; + } + + return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem); +} + +UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index, + UINT32 flag, DWORD positionhigh, DWORD positionlow, + ULONG nreq) +{ + UINT rc; + CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest; + + if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest) + return ERROR_INTERNAL_ERROR; + + rc = try_reset_event(clipboard->req_fevent); + if (rc != ERROR_SUCCESS) + { + return rc; + } + clipboard->req_f_received = FALSE; + + fileContentsRequest.connID = connID; + // streamId is `IStream*` pointer, though it is not very good on a 64-bit system. + // But it is OK, because it is only used to check if the stream is the same in + // `wf_cliprdr_server_file_contents_request()` function. + fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid; + fileContentsRequest.listIndex = index; + fileContentsRequest.dwFlags = flag; + fileContentsRequest.nPositionLow = positionlow; + fileContentsRequest.nPositionHigh = positionhigh; + fileContentsRequest.cbRequested = nreq; + fileContentsRequest.clipDataId = 0; + fileContentsRequest.msgFlags = 0; + rc = clipboard->context->ClientFileContentsRequest(clipboard->context, &fileContentsRequest); + if (rc != ERROR_SUCCESS) + { + return rc; + } + + return wait_response_event(connID, clipboard, clipboard->req_fevent, &clipboard->req_f_received, (void **)&clipboard->req_fdata); +} + +static UINT cliprdr_send_response_filecontents( + wfClipboard *clipboard, + UINT32 connID, + UINT16 msgFlags, + UINT32 streamId, + UINT32 size, + BYTE *data) +{ + CLIPRDR_FILE_CONTENTS_RESPONSE fileContentsResponse; + + if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsResponse) + { + data = NULL; + size = 0; + msgFlags = CB_RESPONSE_FAIL; + } + + fileContentsResponse.connID = connID; + fileContentsResponse.streamId = streamId; + fileContentsResponse.cbRequested = size; + fileContentsResponse.requestedData = data; + fileContentsResponse.msgFlags = msgFlags; + return clipboard->context->ClientFileContentsResponse(clipboard->context, + &fileContentsResponse); +} + +static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) +{ + static wfClipboard *clipboard = NULL; + + switch (Msg) + { + case WM_CREATE: + DEBUG_CLIPRDR("info: WM_CREATE"); + clipboard = (wfClipboard *)((CREATESTRUCT *)lParam)->lpCreateParams; + clipboard->hwnd = hWnd; + + if (!clipboard->legacyApi) + clipboard->AddClipboardFormatListener(hWnd); + else + clipboard->hWndNextViewer = SetClipboardViewer(hWnd); + + break; + + case WM_CLOSE: + DEBUG_CLIPRDR("info: WM_CLOSE"); + + if (!clipboard->legacyApi) + clipboard->RemoveClipboardFormatListener(hWnd); + + break; + + case WM_DESTROY: + if (clipboard->legacyApi) + ChangeClipboardChain(hWnd, clipboard->hWndNextViewer); + + break; + + case WM_CLIPBOARDUPDATE: + DEBUG_CLIPRDR("info: WM_CLIPBOARDUPDATE"); + // if (clipboard->sync) + { + if (!is_set_by_instance(clipboard)) + { + if (clipboard->hmem) + { + GlobalFree(clipboard->hmem); + clipboard->hmem = NULL; + } + + cliprdr_send_format_list(clipboard, 0); + } + } + + break; + + case WM_RENDERALLFORMATS: + DEBUG_CLIPRDR("info: WM_RENDERALLFORMATS"); + + /* discard all contexts in clipboard */ + if (!try_open_clipboard(clipboard->hwnd)) + { + DEBUG_CLIPRDR("OpenClipboard failed with 0x%x", GetLastError()); + break; + } + + EmptyClipboard(); + CloseClipboard(); + break; + + case WM_RENDERFORMAT: + DEBUG_CLIPRDR("info: WM_RENDERFORMAT"); + + // https://docs.microsoft.com/en-us/windows/win32/dataxchg/wm-renderformat?redirectedfrom=MSDN + // to-do: ensure usage of 0 + if (cliprdr_send_data_request(0, clipboard, (UINT32)wParam) != 0) + { + DEBUG_CLIPRDR("error: cliprdr_send_data_request failed."); + break; + } + + if (!SetClipboardData((UINT)wParam, clipboard->hmem)) + { + DEBUG_CLIPRDR("SetClipboardData failed with 0x%x", GetLastError()); + + if (clipboard->hmem) + { + GlobalFree(clipboard->hmem); + clipboard->hmem = NULL; + } + } + + /* Note: GlobalFree() is not needed when success */ + break; + + case WM_DRAWCLIPBOARD: + if (clipboard->legacyApi) + { + if ((GetClipboardOwner() != clipboard->hwnd) && + (S_FALSE == OleIsCurrentClipboard(clipboard->data_obj))) + { + cliprdr_send_format_list(clipboard, 0); + } + + SendMessage(clipboard->hWndNextViewer, Msg, wParam, lParam); + } + + break; + + case WM_CHANGECBCHAIN: + if (clipboard->legacyApi) + { + HWND hWndCurrViewer = (HWND)wParam; + HWND hWndNextViewer = (HWND)lParam; + + if (hWndCurrViewer == clipboard->hWndNextViewer) + clipboard->hWndNextViewer = hWndNextViewer; + else if (clipboard->hWndNextViewer) + SendMessage(clipboard->hWndNextViewer, Msg, wParam, lParam); + } + + break; + + case WM_CLIPRDR_MESSAGE: + DEBUG_CLIPRDR("info: WM_CLIPRDR_MESSAGE"); + + switch (wParam) + { + case OLE_SETCLIPBOARD: + DEBUG_CLIPRDR("info: OLE_SETCLIPBOARD"); + + if (WaitForSingleObject(clipboard->data_obj_mutex, INFINITE) != WAIT_OBJECT_0) + { + break; + } + + if (clipboard->data_obj != NULL) + { + wf_destroy_file_obj(clipboard->data_obj); + clipboard->data_obj = NULL; + } + if (wf_create_file_obj((UINT32 *)lParam, clipboard, &clipboard->data_obj)) + { + HRESULT res = OleSetClipboard(clipboard->data_obj); + if (res != S_OK) + { + wf_destroy_file_obj(clipboard->data_obj); + clipboard->data_obj = NULL; + } + } + free((void *)lParam); + + if (!ReleaseMutex(clipboard->data_obj_mutex)) + { + // critical error!!! + } + + break; + + case DELAYED_RENDERING: + FORMAT_IDS *format_ids = (FORMAT_IDS *)lParam; + if (!try_open_clipboard(clipboard->hwnd)) + { + // failed to open clipboard + free(format_ids->formats); + free(format_ids); + break; + } + + for (UINT32 i = 0; i < format_ids->size; ++i) + { + if (cliprdr_send_data_request(format_ids->connID, clipboard, format_ids->formats[i]) != 0) + { + DEBUG_CLIPRDR("error: cliprdr_send_data_request failed."); + continue; + } + + if (!SetClipboardData(format_ids->formats[i], clipboard->hmem)) + { + printf("SetClipboardData failed with 0x%x\n", GetLastError()); + DEBUG_CLIPRDR("SetClipboardData failed with 0x%x", GetLastError()); + + if (clipboard->hmem) + { + GlobalFree(clipboard->hmem); + clipboard->hmem = NULL; + } + } + } + + if (!CloseClipboard() && GetLastError()) + { + // failed to close clipboard? + } + + free(format_ids->formats); + free(format_ids); + break; + + default: + break; + } + + break; + + case WM_DESTROYCLIPBOARD: + // to-do: clear clipboard data? + case WM_ASKCBFORMATNAME: + case WM_HSCROLLCLIPBOARD: + case WM_PAINTCLIPBOARD: + case WM_SIZECLIPBOARD: + case WM_VSCROLLCLIPBOARD: + default: + return DefWindowProc(hWnd, Msg, wParam, lParam); + } + + return 0; +} + +static int create_cliprdr_window(wfClipboard *clipboard) +{ + WNDCLASSEX wnd_cls; + ZeroMemory(&wnd_cls, sizeof(WNDCLASSEX)); + wnd_cls.cbSize = sizeof(WNDCLASSEX); + wnd_cls.style = CS_OWNDC; + wnd_cls.lpfnWndProc = cliprdr_proc; + wnd_cls.cbClsExtra = 0; + wnd_cls.cbWndExtra = 0; + wnd_cls.hIcon = NULL; + wnd_cls.hCursor = NULL; + wnd_cls.hbrBackground = NULL; + wnd_cls.lpszMenuName = NULL; + wnd_cls.lpszClassName = _T("ClipboardHiddenMessageProcessor"); + wnd_cls.hInstance = GetModuleHandle(NULL); + wnd_cls.hIconSm = NULL; + RegisterClassEx(&wnd_cls); + clipboard->hwnd = + CreateWindowEx(WS_EX_LEFT, _T("ClipboardHiddenMessageProcessor"), _T("rdpclip"), 0, 0, 0, 0, + 0, HWND_MESSAGE, NULL, GetModuleHandle(NULL), clipboard); + + if (!clipboard->hwnd) + { + DEBUG_CLIPRDR("error: CreateWindowEx failed with %x.", GetLastError()); + return -1; + } + + return 0; +} + +static DWORD WINAPI cliprdr_thread_func(LPVOID arg) +{ + int ret; + MSG msg; + BOOL mcode; + wfClipboard *clipboard = (wfClipboard *)arg; + OleInitialize(0); + + if ((ret = create_cliprdr_window(clipboard)) != 0) + { + OleUninitialize(); + DEBUG_CLIPRDR("error: create clipboard window failed."); + return 0; + } + + while ((mcode = GetMessage(&msg, 0, 0, 0)) != 0) + { + if (mcode == -1) + { + DEBUG_CLIPRDR("error: clipboard thread GetMessage failed."); + break; + } + else + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + OleUninitialize(); + return 0; +} + +static void clear_file_array(wfClipboard *clipboard) +{ + size_t i; + + if (!clipboard) + return; + + /* clear file_names array */ + if (clipboard->file_names) + { + for (i = 0; i < clipboard->nFiles; i++) + { + free(clipboard->file_names[i]); + clipboard->file_names[i] = NULL; + } + + free(clipboard->file_names); + clipboard->file_names = NULL; + } + + /* clear fileDescriptor array */ + if (clipboard->fileDescriptor) + { + for (i = 0; i < clipboard->nFiles; i++) + { + free(clipboard->fileDescriptor[i]); + clipboard->fileDescriptor[i] = NULL; + } + + free(clipboard->fileDescriptor); + clipboard->fileDescriptor = NULL; + } + + clipboard->file_array_size = 0; + clipboard->nFiles = 0; + clipboard->first_file_index = (size_t)-1; +} + +static BOOL wf_cliprdr_get_file_contents(WCHAR *file_name, BYTE *buffer, LONG positionLow, + LONG positionHigh, DWORD nRequested, DWORD *puSize) +{ + BOOL res = FALSE; + HANDLE hFile = NULL; + DWORD nGet, rc; + + if (!file_name || !buffer || !puSize) + { + printf("get file contents Invalid Arguments.\n"); + return FALSE; + } + + hFile = CreateFileW(file_name, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL); + + if (hFile == INVALID_HANDLE_VALUE) + return FALSE; + + rc = SetFilePointer(hFile, positionLow, &positionHigh, FILE_BEGIN); + + if (rc == INVALID_SET_FILE_POINTER) + goto error; + + if (!ReadFile(hFile, buffer, nRequested, &nGet, NULL)) + { + DEBUG_CLIPRDR("ReadFile failed with 0x%08lX.", GetLastError()); + goto error; + } + + res = TRUE; +error: + if (hFile) + { + if (!CloseHandle(hFile)) + res = FALSE; + } + + if (res) + *puSize = nGet; + + return res; +} + +/* path_name has a '\' at the end. e.g. c:\newfolder\, file_name is c:\newfolder\new.txt */ +static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t pathLen) +{ + HANDLE hFile = NULL; + FILEDESCRIPTORW *fd = NULL; + fd = (FILEDESCRIPTORW *)calloc(1, sizeof(FILEDESCRIPTORW)); + + if (!fd) + return NULL; + + hFile = CreateFileW(file_name, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL); + + if (hFile == INVALID_HANDLE_VALUE) + { + free(fd); + return NULL; + } + + // to-do: use `fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI`. + // We keep `fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI` for compatibility. + // fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI; + fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI; + fd->dwFileAttributes = GetFileAttributesW(file_name); + if (fd->dwFileAttributes == INVALID_FILE_ATTRIBUTES) + { + // TODO: debug handle some errors + } + + if (!GetFileTime(hFile, NULL, NULL, &fd->ftLastWriteTime)) + { + fd->dwFlags &= ~FD_WRITESTIME; + } + + fd->nFileSizeLow = GetFileSize(hFile, &fd->nFileSizeHigh); + if ((wcslen(file_name + pathLen) + 1) > sizeof(fd->cFileName) / sizeof(fd->cFileName[0])) + { + // The file name is too long, which is not a normal case. + // So we just return NULL. + CloseHandle(hFile); + free(fd); + return NULL; + } + + wcsncpy_s(fd->cFileName, sizeof(fd->cFileName) / sizeof(fd->cFileName[0]), file_name + pathLen, wcslen(file_name + pathLen) + 1); + CloseHandle(hFile); + + return fd; +} + +static BOOL wf_cliprdr_array_ensure_capacity(wfClipboard *clipboard) +{ + if (!clipboard) + return FALSE; + + if (clipboard->nFiles == clipboard->file_array_size) + { + size_t new_size; + FILEDESCRIPTORW **new_fd; + WCHAR **new_name; + new_size = (clipboard->file_array_size + 1) * 2; + new_fd = (FILEDESCRIPTORW **)realloc(clipboard->fileDescriptor, + new_size * sizeof(FILEDESCRIPTORW *)); + + if (new_fd) + clipboard->fileDescriptor = new_fd; + + new_name = (WCHAR **)realloc(clipboard->file_names, new_size * sizeof(WCHAR *)); + + if (new_name) + clipboard->file_names = new_name; + + if (!new_fd || !new_name) + return FALSE; + + clipboard->file_array_size = new_size; + } + + return TRUE; +} + +static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_file_name, + size_t pathLen) +{ + if (!wf_cliprdr_array_ensure_capacity(clipboard)) + return FALSE; + + /* add to name array */ + clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc((size_t)MAX_PATH * sizeof(WCHAR)); + + if (!clipboard->file_names[clipboard->nFiles]) + return FALSE; + + // `MAX_PATH` is long enough for the file name. + // So we just return FALSE if the file name is too long, which is not a normal case. + if ((wcslen(full_file_name) + 1) > MAX_PATH) + return FALSE; + + wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1); + /* add to descriptor array */ + clipboard->fileDescriptor[clipboard->nFiles] = + wf_cliprdr_get_file_descriptor(full_file_name, pathLen); + + if (!clipboard->fileDescriptor[clipboard->nFiles]) + { + free(clipboard->file_names[clipboard->nFiles]); + return FALSE; + } + + if ((clipboard->fileDescriptor[clipboard->nFiles]->dwFileAttributes & + FILE_ATTRIBUTE_DIRECTORY) == 0) { + clipboard->first_file_index = clipboard->nFiles; + } + + clipboard->nFiles++; + return TRUE; +} + +static BOOL wf_cliprdr_traverse_directory(wfClipboard *clipboard, WCHAR *Dir, size_t pathLen) +{ + HANDLE hFind; + WCHAR DirSpec[MAX_PATH]; + WIN32_FIND_DATA FindFileData; + + if (!clipboard || !Dir) + return FALSE; + + if (wcslen(Dir) + 3 > MAX_PATH) + return FALSE; + StringCchCopyW(DirSpec, MAX_PATH, Dir); + StringCchCatW(DirSpec, MAX_PATH, L"\\*"); + + // hFind = FindFirstFile(DirSpec, &FindFileData); + hFind = FindFirstFileW(DirSpec, &FindFileData); + + if (hFind == INVALID_HANDLE_VALUE) + { + // printf("FindFirstFile failed with 0x%x.\n", GetLastError()); + DEBUG_CLIPRDR("FindFirstFile failed with 0x%x.", GetLastError()); + return FALSE; + } + + while (FindNextFileW(hFind, &FindFileData)) + { + // if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && + // wcscmp(FindFileData.cFileName, _T(".")) == 0 || + // wcscmp(FindFileData.cFileName, _T("..")) == 0) + if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && + wcscmp(FindFileData.cFileName, L".") == 0 || + wcscmp(FindFileData.cFileName, L"..") == 0) + { + continue; + } + + if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + { + WCHAR DirAdd[MAX_PATH]; + if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH) + return FALSE; + StringCchCopyW(DirAdd, MAX_PATH, Dir); + StringCchCatW(DirAdd, MAX_PATH, L"\\"); + StringCchCatW(DirAdd, MAX_PATH, FindFileData.cFileName); + + if (!wf_cliprdr_add_to_file_arrays(clipboard, DirAdd, pathLen)) + return FALSE; + + if (!wf_cliprdr_traverse_directory(clipboard, DirAdd, pathLen)) + return FALSE; + } + else + { + WCHAR fileName[MAX_PATH]; + if (wcslen(Dir) + wcslen(FindFileData.cFileName) + 2 > MAX_PATH) + return FALSE; + StringCchCopyW(fileName, MAX_PATH, Dir); + StringCchCatW(fileName, MAX_PATH, L"\\"); + StringCchCatW(fileName, MAX_PATH, FindFileData.cFileName); + + if (!wf_cliprdr_add_to_file_arrays(clipboard, fileName, pathLen)) + return FALSE; + } + } + + FindClose(hFind); + return TRUE; +} + +static UINT wf_cliprdr_send_client_capabilities(wfClipboard *clipboard) +{ + CLIPRDR_CAPABILITIES capabilities; + CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet; + + if (!clipboard || !clipboard->context) + return ERROR_INTERNAL_ERROR; + + // Ignore ClientCapabilities for now + if (!clipboard->context->ClientCapabilities) + { + return CHANNEL_RC_OK; + } + + capabilities.connID = 0; + capabilities.cCapabilitiesSets = 1; + capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET *)&(generalCapabilitySet); + generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL; + generalCapabilitySet.capabilitySetLength = 12; + generalCapabilitySet.version = CB_CAPS_VERSION_2; + generalCapabilitySet.generalFlags = + CB_USE_LONG_FORMAT_NAMES | CB_STREAM_FILECLIP_ENABLED | CB_FILECLIP_NO_FILE_PATHS; + return clipboard->context->ClientCapabilities(clipboard->context, &capabilities); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_cliprdr_monitor_ready(CliprdrClientContext *context, + const CLIPRDR_MONITOR_READY *monitorReady) +{ + UINT rc; + wfClipboard *clipboard = (wfClipboard *)context->Custom; + + if (!context || !monitorReady) + return ERROR_INTERNAL_ERROR; + + clipboard->sync = TRUE; + rc = wf_cliprdr_send_client_capabilities(clipboard); + + if (rc != CHANNEL_RC_OK) + return rc; + + return rc; + // Don't send format list here, because we don't want to paste files copied before the connection. + // return cliprdr_send_format_list(clipboard, monitorReady->connID); +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_cliprdr_server_capabilities(CliprdrClientContext *context, + const CLIPRDR_CAPABILITIES *capabilities) +{ + UINT32 index; + CLIPRDR_CAPABILITY_SET *capabilitySet; + wfClipboard *clipboard = (wfClipboard *)context->Custom; + + if (!context || !capabilities) + return ERROR_INTERNAL_ERROR; + + for (index = 0; index < capabilities->cCapabilitiesSets; index++) + { + capabilitySet = &(capabilities->capabilitySets[index]); + + if ((capabilitySet->capabilitySetType == CB_CAPSTYPE_GENERAL) && + (capabilitySet->capabilitySetLength >= CB_CAPSTYPE_GENERAL_LEN)) + { + CLIPRDR_GENERAL_CAPABILITY_SET *generalCapabilitySet = + (CLIPRDR_GENERAL_CAPABILITY_SET *)capabilitySet; + clipboard->capabilities = generalCapabilitySet->generalFlags; + break; + } + } + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context, + const CLIPRDR_FORMAT_LIST *formatList) +{ + UINT rc = ERROR_INTERNAL_ERROR; + UINT32 i; + formatMapping *mapping; + CLIPRDR_FORMAT *format; + wfClipboard *clipboard = NULL; + + if (!context || !formatList) + return ERROR_INTERNAL_ERROR; + + clipboard = (wfClipboard *)context->Custom; + if (!clipboard) + return ERROR_INTERNAL_ERROR; + + if (!clear_format_map(clipboard)) + return ERROR_INTERNAL_ERROR; + + clipboard->copied = TRUE; + + for (i = 0; i < formatList->numFormats; i++) + { + format = &(formatList->formats[i]); + mapping = &(clipboard->format_mappings[i]); + mapping->remote_format_id = format->formatId; + + if (format->formatName) + { + int size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, + strlen(format->formatName), NULL, 0); + mapping->name = calloc(size + 1, sizeof(WCHAR)); + + if (mapping->name) + { + MultiByteToWideChar(CP_UTF8, 0, format->formatName, strlen(format->formatName), + mapping->name, size); + mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name); + } + } + else + { + mapping->name = NULL; + mapping->local_format_id = mapping->remote_format_id; + } + + clipboard->map_size++; + map_ensure_capacity(clipboard); + } + + if (file_transferring(clipboard)) + { + if (context->EnableFiles) + { + UINT32 *p_conn_id = (UINT32 *)calloc(1, sizeof(UINT32)); + if (p_conn_id) { + *p_conn_id = formatList->connID; + if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, OLE_SETCLIPBOARD, p_conn_id)) + rc = CHANNEL_RC_OK; + } + } + else + { + rc = CHANNEL_RC_OK; + } + } + else + { + if (context->EnableOthers) + { + if (!try_open_clipboard(clipboard->hwnd)) + return CHANNEL_RC_OK; /* Ignore, other app holding clipboard */ + + if (EmptyClipboard()) + { + // Modified: do not apply delayed rendering + // for (i = 0; i < (UINT32)clipboard->map_size; i++) + // SetClipboardData(clipboard->format_mappings[i].local_format_id, NULL); + + FORMAT_IDS *format_ids = (FORMAT_IDS *)calloc(1, sizeof(FORMAT_IDS)); + if (format_ids) + { + format_ids->connID = formatList->connID; + format_ids->size = (UINT32)clipboard->map_size; + format_ids->formats = (UINT32 *)calloc(format_ids->size, sizeof(UINT32)); + if (format_ids->formats) + { + for (i = 0; i < format_ids->size; ++i) + { + format_ids->formats[i] = clipboard->format_mappings[i].local_format_id; + } + if (PostMessage(clipboard->hwnd, WM_CLIPRDR_MESSAGE, DELAYED_RENDERING, format_ids)) + { + rc = CHANNEL_RC_OK; + } + else + { + rc = ERROR_INTERNAL_ERROR; + } + } + else + { + rc = ERROR_INTERNAL_ERROR; + } + } + else + { + rc = ERROR_INTERNAL_ERROR; + } + } + + if (!CloseClipboard() && GetLastError()) + return ERROR_INTERNAL_ERROR; + } + else + { + rc = CHANNEL_RC_OK; + } + } + + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_format_list_response(CliprdrClientContext *context, + const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse) +{ + (void)context; + (void)formatListResponse; + + if (formatListResponse->msgFlags != CB_RESPONSE_OK) + return E_FAIL; + + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_lock_clipboard_data(CliprdrClientContext *context, + const CLIPRDR_LOCK_CLIPBOARD_DATA *lockClipboardData) +{ + (void)context; + (void)lockClipboardData; + return CHANNEL_RC_OK; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_unlock_clipboard_data(CliprdrClientContext *context, + const CLIPRDR_UNLOCK_CLIPBOARD_DATA *unlockClipboardData) +{ + (void)context; + (void)unlockClipboardData; + return CHANNEL_RC_OK; +} + +static BOOL wf_cliprdr_process_filename(wfClipboard *clipboard, WCHAR *wFileName, size_t str_len) +{ + size_t pathLen; + size_t offset = str_len; + + if (!clipboard || !wFileName) + return FALSE; + + /* find the last '\' in full file name */ + while (offset > 0) + { + if (wFileName[offset] == L'\\') + break; + else + offset--; + } + + pathLen = offset + 1; + + if (!wf_cliprdr_add_to_file_arrays(clipboard, wFileName, pathLen)) + return FALSE; + + if ((clipboard->fileDescriptor[clipboard->nFiles - 1]->dwFileAttributes & + FILE_ATTRIBUTE_DIRECTORY) != 0) + { + /* this is a directory */ + if (!wf_cliprdr_traverse_directory(clipboard, wFileName, pathLen)) + return FALSE; + } + + return TRUE; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_format_data_request(CliprdrClientContext *context, + const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest) +{ + UINT rc = ERROR_SUCCESS; + size_t size = 0; + void *buff = NULL; + char *globlemem = NULL; + HANDLE hClipdata = NULL; + UINT32 requestedFormatId; + CLIPRDR_FORMAT_DATA_RESPONSE response; + wfClipboard *clipboard; + + if (!context || !formatDataRequest) + { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + + clipboard = (wfClipboard *)context->Custom; + + if (!clipboard) + { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + + requestedFormatId = formatDataRequest->requestedFormatId; + + if (requestedFormatId == RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW)) + { + size_t len; + size_t i; + WCHAR *wFileName; + HRESULT result; + LPDATAOBJECT dataObj; + FORMATETC format_etc; + STGMEDIUM stg_medium; + DROPFILES *dropFiles; + FILEGROUPDESCRIPTORW *groupDsc; + result = OleGetClipboard(&dataObj); + + if (FAILED(result)) + { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + + ZeroMemory(&format_etc, sizeof(FORMATETC)); + ZeroMemory(&stg_medium, sizeof(STGMEDIUM)); + /* get DROPFILES struct from OLE */ + format_etc.cfFormat = CF_HDROP; + format_etc.tymed = TYMED_HGLOBAL; + format_etc.dwAspect = 1; + format_etc.lindex = -1; + result = IDataObject_GetData(dataObj, &format_etc, &stg_medium); + + if (FAILED(result)) + { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + + dropFiles = (DROPFILES *)GlobalLock(stg_medium.hGlobal); + + if (!dropFiles) + { + GlobalUnlock(stg_medium.hGlobal); + ReleaseStgMedium(&stg_medium); + clipboard->nFiles = 0; + goto resp; + } + + clear_file_array(clipboard); + + if (dropFiles->fWide) + { + /* dropFiles contains file names */ + for (wFileName = (WCHAR *)((char *)dropFiles + dropFiles->pFiles); + (len = wcslen(wFileName)) > 0; wFileName += len + 1) + { + wf_cliprdr_process_filename(clipboard, wFileName, wcslen(wFileName)); + } + } + else + { + char *p; + for (p = (char *)((char *)dropFiles + dropFiles->pFiles); (len = strlen(p)) > 0; + p += len + 1, clipboard->nFiles++) + { + int cchWideChar; + cchWideChar = MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, NULL, 0); + wFileName = (LPWSTR)calloc(cchWideChar, sizeof(WCHAR)); + if (wFileName) + { + MultiByteToWideChar(CP_ACP, MB_COMPOSITE, p, len, wFileName, cchWideChar); + wf_cliprdr_process_filename(clipboard, wFileName, cchWideChar); + free(wFileName); + } + else + { + rc = ERROR_INTERNAL_ERROR; + GlobalUnlock(stg_medium.hGlobal); + ReleaseStgMedium(&stg_medium); + goto exit; + } + } + } + + GlobalUnlock(stg_medium.hGlobal); + ReleaseStgMedium(&stg_medium); + resp: + // size will not overflow, because size type is size_t (unsigned __int64) + size = 4 + clipboard->nFiles * sizeof(FILEDESCRIPTORW); + groupDsc = (FILEGROUPDESCRIPTORW *)malloc(size); + + if (groupDsc) + { + groupDsc->cItems = clipboard->nFiles; + + for (i = 0; i < clipboard->nFiles; i++) + { + if (clipboard->fileDescriptor[i]) + groupDsc->fgd[i] = *clipboard->fileDescriptor[i]; + } + + buff = groupDsc; + } + + IDataObject_Release(dataObj); + rc = ERROR_SUCCESS; + } + else + { + /* Ignore if other app is holding the clipboard */ + if (try_open_clipboard(clipboard->hwnd)) + { + hClipdata = GetClipboardData(requestedFormatId); + + if (!hClipdata) + { + CloseClipboard(); + { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + } + else + { + globlemem = (char *)GlobalLock(hClipdata); + size = (int)GlobalSize(hClipdata); + buff = malloc(size); + if (buff) + { + CopyMemory(buff, globlemem, size); + rc = ERROR_SUCCESS; + } + else + { + rc = ERROR_INTERNAL_ERROR; + } + GlobalUnlock(hClipdata); + CloseClipboard(); + } + } + else + { + rc = ERROR_INTERNAL_ERROR; + } + } + +exit: + if (rc == ERROR_SUCCESS) + { + response.msgFlags = CB_RESPONSE_OK; + } + else + { + response.msgFlags = CB_RESPONSE_FAIL; + } + response.connID = formatDataRequest->connID; + response.dataLen = size; + response.requestedFormatData = (BYTE *)buff; + if (ERROR_SUCCESS != clipboard->context->ClientFormatDataResponse(clipboard->context, &response)) + { + // CAUTION: if failed to send, server will wait a long time, default 30 seconds. + } + + if (buff) + { + free(buff); + } + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_format_data_response(CliprdrClientContext *context, + const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse) +{ + UINT rc = ERROR_INTERNAL_ERROR; + BYTE *data; + HANDLE hMem; + wfClipboard *clipboard; + + do + { + if (!context || !formatDataResponse) + { + rc = ERROR_INTERNAL_ERROR; + break; + } + + clipboard = (wfClipboard *)context->Custom; + if (!clipboard) + { + rc = ERROR_INTERNAL_ERROR; + break; + } + clipboard->hmem = NULL; + + if (formatDataResponse->msgFlags != CB_RESPONSE_OK) + { + // BOOL emptyRes = wf_do_empty_cliprdr((wfClipboard *)context->custom); + // (void)emptyRes; + rc = E_FAIL; + break; + } + + hMem = GlobalAlloc(GMEM_MOVEABLE, formatDataResponse->dataLen); + if (!hMem) + { + rc = ERROR_INTERNAL_ERROR; + break; + } + + data = (BYTE *)GlobalLock(hMem); + if (!data) + { + GlobalFree(hMem); + rc = ERROR_INTERNAL_ERROR; + break; + } + + CopyMemory(data, formatDataResponse->requestedFormatData, formatDataResponse->dataLen); + + if (!GlobalUnlock(hMem) && GetLastError()) + { + GlobalFree(hMem); + rc = ERROR_INTERNAL_ERROR; + break; + } + + clipboard->hmem = hMem; + rc = CHANNEL_RC_OK; + } while (0); + + if (!SetEvent(clipboard->formatDataRespEvent)) + { + // If failed to set event, set flag to indicate the event is received. + DEBUG_CLIPRDR("wf_cliprdr_server_format_data_response(), SetEvent failed with 0x%x", GetLastError()); + clipboard->formatDataRespReceived = TRUE; + rc = ERROR_INTERNAL_ERROR; + } + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_file_contents_request(CliprdrClientContext *context, + const CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest) +{ + DWORD uSize = 0; + BYTE *pData = NULL; + HRESULT hRet = S_OK; + FORMATETC vFormatEtc; + LPDATAOBJECT pDataObj = NULL; + STGMEDIUM vStgMedium; + BOOL bIsStreamFile = TRUE; + static LPSTREAM pStreamStc = NULL; + static UINT32 uStreamIdStc = 0; + wfClipboard *clipboard; + UINT rc = ERROR_INTERNAL_ERROR; + UINT sRc; + UINT32 cbRequested; + + if (!context || !fileContentsRequest) + { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + + clipboard = (wfClipboard *)context->Custom; + + if (!clipboard) + { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + + // If the clipboard is set by the instance, or the file descriptor is from remote, + // we should not process the request. + // Because this may be the following cases: + // 1. `A` -> `B`, `C` + // 2. Copy in `A` + // 3. Copy in `B` + // 4. Paste in `C` + // In this case, `C` should not get the file content from `A`. The clipboard is set by `B`. + // + // Or + // 1. `B` -> `A` -> `C` + // 2. Copy in `A` + // 2. Copy in `B` + // 3. Paste in `C` + // In this case, `C` should not get the file content from `A`. The clipboard is set by `B`. + // + // We can simply notify `C` to clear the clipboard when `A` received copy message from `B`, + // if connections are in the same process. + // But if connections are in different processes, it is not easy to notify the other process. + // So we just ignore the request from `C` in this case. + if (is_set_by_instance(clipboard) || is_file_descriptor_from_remote()) { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + + cbRequested = fileContentsRequest->cbRequested; + if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) + cbRequested = sizeof(UINT64); + + pData = (BYTE *)calloc(1, cbRequested); + + if (!pData) + { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + + hRet = OleGetClipboard(&pDataObj); + + if (FAILED(hRet)) + { + printf("filecontents: get ole clipboard failed.\n"); + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + + ZeroMemory(&vFormatEtc, sizeof(FORMATETC)); + ZeroMemory(&vStgMedium, sizeof(STGMEDIUM)); + vFormatEtc.cfFormat = RegisterClipboardFormat(CFSTR_FILECONTENTS); + vFormatEtc.tymed = TYMED_ISTREAM; + vFormatEtc.dwAspect = 1; + vFormatEtc.lindex = fileContentsRequest->listIndex; + vFormatEtc.ptd = NULL; + + if ((uStreamIdStc != fileContentsRequest->streamId) || !pStreamStc) + { + LPENUMFORMATETC pEnumFormatEtc; + ULONG CeltFetched; + FORMATETC vFormatEtc2; + + if (pStreamStc) + { + IStream_Release(pStreamStc); + pStreamStc = NULL; + } + + bIsStreamFile = FALSE; + hRet = IDataObject_EnumFormatEtc(pDataObj, DATADIR_GET, &pEnumFormatEtc); + + if (hRet == S_OK) + { + do + { + hRet = IEnumFORMATETC_Next(pEnumFormatEtc, 1, &vFormatEtc2, &CeltFetched); + + if (hRet == S_OK) + { + if (vFormatEtc2.cfFormat == RegisterClipboardFormat(CFSTR_FILECONTENTS)) + { + hRet = IDataObject_GetData(pDataObj, &vFormatEtc, &vStgMedium); + + if (hRet == S_OK) + { + pStreamStc = vStgMedium.pstm; + uStreamIdStc = fileContentsRequest->streamId; + bIsStreamFile = TRUE; + } + + break; + } + } + } while (hRet == S_OK); + } + } + + if (bIsStreamFile == TRUE) + { + if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) + { + STATSTG vStatStg; + ZeroMemory(&vStatStg, sizeof(STATSTG)); + hRet = IStream_Stat(pStreamStc, &vStatStg, STATFLAG_NONAME); + + if (hRet == S_OK) + { + *((UINT32 *)&pData[0]) = vStatStg.cbSize.LowPart; + *((UINT32 *)&pData[4]) = vStatStg.cbSize.HighPart; + uSize = cbRequested; + } + } + else if (fileContentsRequest->dwFlags == FILECONTENTS_RANGE) + { + LARGE_INTEGER dlibMove; + ULARGE_INTEGER dlibNewPosition; + + if (clipboard->nFiles > 0 && + fileContentsRequest->listIndex == (UINT32)clipboard->first_file_index && + fileContentsRequest->nPositionLow == 0 && + fileContentsRequest->nPositionHigh == 0) { + clipboard->context->HandleClipboardFiles(fileContentsRequest->connID, clipboard->nFiles, clipboard->file_names); + } + + dlibMove.HighPart = fileContentsRequest->nPositionHigh; + dlibMove.LowPart = fileContentsRequest->nPositionLow; + hRet = IStream_Seek(pStreamStc, dlibMove, STREAM_SEEK_SET, &dlibNewPosition); + + if (SUCCEEDED(hRet)) + hRet = IStream_Read(pStreamStc, pData, cbRequested, (PULONG)&uSize); + } + } + else + { + if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) + { + if (clipboard->nFiles <= fileContentsRequest->listIndex) + { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + *((UINT32 *)&pData[0]) = + clipboard->fileDescriptor[fileContentsRequest->listIndex]->nFileSizeLow; + *((UINT32 *)&pData[4]) = + clipboard->fileDescriptor[fileContentsRequest->listIndex]->nFileSizeHigh; + uSize = cbRequested; + } + else if (fileContentsRequest->dwFlags == FILECONTENTS_RANGE) + { + BOOL bRet; + if (clipboard->nFiles <= fileContentsRequest->listIndex) + { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + + if (clipboard->nFiles > 0 && + fileContentsRequest->listIndex == (UINT32)clipboard->first_file_index && + fileContentsRequest->nPositionLow == 0 && + fileContentsRequest->nPositionHigh == 0) { + clipboard->context->HandleClipboardFiles(fileContentsRequest->connID, clipboard->nFiles, clipboard->file_names); + } + bRet = wf_cliprdr_get_file_contents( + clipboard->file_names[fileContentsRequest->listIndex], pData, + fileContentsRequest->nPositionLow, fileContentsRequest->nPositionHigh, cbRequested, + &uSize); + + if (bRet == FALSE) + { + printf("get file contents failed.\n"); + uSize = 0; + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + } + } + + rc = CHANNEL_RC_OK; +exit: + + if (pDataObj) + IDataObject_Release(pDataObj); + + // https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-idataobject-getdata#:~:text=value%20of%20its-,pUnkForRelease,-member.%20If%20pUnkForRelease + if (pStreamStc && vStgMedium.pUnkForRelease == NULL) + { + IStream_Release(pStreamStc); + pStreamStc = NULL; + } + + if (rc != CHANNEL_RC_OK) + { + uSize = 0; + } + + if (uSize == 0) + { + if (pData) + { + free(pData); + pData = NULL; + } + } + + sRc = + cliprdr_send_response_filecontents( + clipboard, + fileContentsRequest->connID, + rc == CHANNEL_RC_OK ? CB_RESPONSE_OK : CB_RESPONSE_FAIL, + fileContentsRequest->streamId, + uSize, + pData); + + if (pData) + { + free(pData); + } + + // if (sRc != CHANNEL_RC_OK) + // return sRc; + + return rc; +} + +/** + * Function description + * + * @return 0 on success, otherwise a Win32 error code + */ +static UINT +wf_cliprdr_server_file_contents_response(CliprdrClientContext *context, + const CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse) +{ + wfClipboard *clipboard; + UINT rc = ERROR_INTERNAL_ERROR; + + do + { + if (!context || !fileContentsResponse) + { + rc = ERROR_INTERNAL_ERROR; + break; + } + + clipboard = (wfClipboard *)context->Custom; + if (!clipboard) + { + rc = ERROR_INTERNAL_ERROR; + break; + } + clipboard->req_fsize = 0; + clipboard->req_fdata = NULL; + + if (fileContentsResponse->msgFlags != CB_RESPONSE_OK) + { + rc = E_FAIL; + break; + } + + clipboard->req_fsize = fileContentsResponse->cbRequested; + clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested); + if (!clipboard->req_fdata) + { + rc = ERROR_INTERNAL_ERROR; + break; + } + + CopyMemory(clipboard->req_fdata, fileContentsResponse->requestedData, + fileContentsResponse->cbRequested); + + rc = CHANNEL_RC_OK; + } while (0); + + if (!SetEvent(clipboard->req_fevent)) + { + // If failed to set event, set flag to indicate the event is received. + DEBUG_CLIPRDR("wf_cliprdr_server_file_contents_response(), SetEvent failed with 0x%x", GetLastError()); + clipboard->req_f_received = TRUE; + } + return rc; +} + +BOOL is_set_by_instance(wfClipboard *clipboard) +{ + if (GetClipboardOwner() == clipboard->hwnd || S_OK == OleIsCurrentClipboard(clipboard->data_obj)) { + return TRUE; + } + return FALSE; +} + +BOOL is_file_descriptor_from_remote() +{ + UINT fsid = 0; + if (IsClipboardFormatAvailable(CF_HDROP)) { + return FALSE; + } + fsid = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + if (IsClipboardFormatAvailable(fsid)) { + return TRUE; + } + return FALSE; +} + +BOOL wf_cliprdr_init(wfClipboard *clipboard, CliprdrClientContext *cliprdr) +{ + if (!clipboard || !cliprdr) + return FALSE; + + clipboard->context = cliprdr; + clipboard->sync = FALSE; + clipboard->map_capacity = 32; + clipboard->map_size = 0; + clipboard->hUser32 = LoadLibraryA("user32.dll"); + clipboard->data_obj = NULL; + clipboard->copied = FALSE; + + if (clipboard->hUser32) + { + clipboard->AddClipboardFormatListener = (fnAddClipboardFormatListener)GetProcAddress( + clipboard->hUser32, "AddClipboardFormatListener"); + clipboard->RemoveClipboardFormatListener = (fnRemoveClipboardFormatListener)GetProcAddress( + clipboard->hUser32, "RemoveClipboardFormatListener"); + clipboard->GetUpdatedClipboardFormats = (fnGetUpdatedClipboardFormats)GetProcAddress( + clipboard->hUser32, "GetUpdatedClipboardFormats"); + } + + if (!(clipboard->hUser32 && clipboard->AddClipboardFormatListener && + clipboard->RemoveClipboardFormatListener && clipboard->GetUpdatedClipboardFormats)) + clipboard->legacyApi = TRUE; + + if (!(clipboard->format_mappings = + (formatMapping *)calloc(clipboard->map_capacity, sizeof(formatMapping)))) + goto error; + + if (!(clipboard->formatDataRespEvent = CreateEvent(NULL, TRUE, FALSE, NULL))) + goto error; + clipboard->formatDataRespReceived = FALSE; + + if (!(clipboard->data_obj_mutex = CreateMutex(NULL, FALSE, "data_obj_mutex"))) + goto error; + + if (!(clipboard->req_fevent = CreateEvent(NULL, TRUE, FALSE, NULL))) + goto error; + clipboard->req_f_received = FALSE; + + if (!(clipboard->thread = CreateThread(NULL, 0, cliprdr_thread_func, clipboard, 0, NULL))) + goto error; + + cliprdr->MonitorReady = wf_cliprdr_monitor_ready; + cliprdr->ServerCapabilities = wf_cliprdr_server_capabilities; + cliprdr->ServerFormatList = wf_cliprdr_server_format_list; + cliprdr->ServerFormatListResponse = wf_cliprdr_server_format_list_response; + cliprdr->ServerLockClipboardData = wf_cliprdr_server_lock_clipboard_data; + cliprdr->ServerUnlockClipboardData = wf_cliprdr_server_unlock_clipboard_data; + cliprdr->ServerFormatDataRequest = wf_cliprdr_server_format_data_request; + cliprdr->ServerFormatDataResponse = wf_cliprdr_server_format_data_response; + cliprdr->ServerFileContentsRequest = wf_cliprdr_server_file_contents_request; + cliprdr->ServerFileContentsResponse = wf_cliprdr_server_file_contents_response; + cliprdr->Custom = (void *)clipboard; + return TRUE; +error: + wf_cliprdr_uninit(clipboard, cliprdr); + return FALSE; +} + +BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr) +{ + if (!clipboard || !cliprdr) + return FALSE; + + clipboard->copied = FALSE; + cliprdr->Custom = NULL; + + /* discard all contexts in clipboard */ + if (try_open_clipboard(clipboard->hwnd)) + { + if (is_set_by_instance(clipboard) || is_file_descriptor_from_remote()) + { + if (!EmptyClipboard()) + { + DEBUG_CLIPRDR("EmptyClipboard failed with 0x%x", GetLastError()); + } + } + if (!CloseClipboard()) + { + // critical error!!! + } + } + else + { + DEBUG_CLIPRDR("OpenClipboard failed with 0x%x", GetLastError()); + } + + if (clipboard->hwnd) + PostMessage(clipboard->hwnd, WM_QUIT, 0, 0); + + if (clipboard->thread) + { + WaitForSingleObject(clipboard->thread, INFINITE); + CloseHandle(clipboard->thread); + } + + if (clipboard->data_obj) + { + wf_destroy_file_obj(clipboard->data_obj); + clipboard->data_obj = NULL; + } + + if (clipboard->formatDataRespEvent) + CloseHandle(clipboard->formatDataRespEvent); + + if (clipboard->data_obj_mutex) + CloseHandle(clipboard->data_obj_mutex); + + if (clipboard->req_fevent) + CloseHandle(clipboard->req_fevent); + + clear_file_array(clipboard); + clear_format_map(clipboard); + free(clipboard->format_mappings); + return TRUE; +} + +wfClipboard clipboard; + +BOOL init_cliprdr(CliprdrClientContext *context) +{ + return wf_cliprdr_init(&clipboard, context); +} + +BOOL uninit_cliprdr(CliprdrClientContext *context) +{ + return wf_cliprdr_uninit(&clipboard, context); +} + +BOOL empty_cliprdr(CliprdrClientContext *context, UINT32 connID) +{ + wfClipboard *clipboard = NULL; + CliprdrDataObject *instance = NULL; + BOOL rc = FALSE; + if (!context) + { + return FALSE; + } + if (connID == 0) + { + return TRUE; + } + + clipboard = (wfClipboard *)context->Custom; + if (!clipboard) + { + return FALSE; + } + + instance = clipboard->data_obj; + if (instance) + { + if (instance->m_connID != connID) + { + return TRUE; + } + } + + return wf_do_empty_cliprdr(clipboard); +} + +BOOL wf_do_empty_cliprdr(wfClipboard *clipboard) +{ + BOOL rc = FALSE; + if (!clipboard) + { + return FALSE; + } + + clipboard->copied = FALSE; + + if (WaitForSingleObject(clipboard->data_obj_mutex, INFINITE) != WAIT_OBJECT_0) + { + return FALSE; + } + + do + { + if (clipboard->data_obj != NULL) + { + wf_destroy_file_obj(clipboard->data_obj); + clipboard->data_obj = NULL; + } + + /* discard all contexts in clipboard */ + if (!try_open_clipboard(clipboard->hwnd)) + { + DEBUG_CLIPRDR("OpenClipboard failed with 0x%x", GetLastError()); + rc = FALSE; + break; + } + + if (is_file_descriptor_from_remote()) + { + if (!EmptyClipboard()) + { + rc = FALSE; + } + } + + if (!CloseClipboard()) + { + // critical error!!! + } + rc = TRUE; + } while (0); + + if (!ReleaseMutex(clipboard->data_obj_mutex)) + { + // critical error!!! + } + return rc; +} diff --git a/vendor/rustdesk/libs/enigo/.gitattributes b/vendor/rustdesk/libs/enigo/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/vendor/rustdesk/libs/enigo/.gitignore b/vendor/rustdesk/libs/enigo/.gitignore new file mode 100644 index 0000000..0e497c2 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/.gitignore @@ -0,0 +1,14 @@ +.DS_Store +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock + +# RustFmt files +**/*.rs.bk + +# intellij +.idea \ No newline at end of file diff --git a/vendor/rustdesk/libs/enigo/.travis.yml b/vendor/rustdesk/libs/enigo/.travis.yml new file mode 100644 index 0000000..0152a83 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/.travis.yml @@ -0,0 +1,15 @@ +language: rust +rust: + - stable + - beta + - nightly +matrix: + allow_failures: + - rust: nightly +before_install: + - if [ "$TRAVIS_OS_NAME" == "linux" ]; then sudo apt-get -qq update; fi + - if [ "$TRAVIS_OS_NAME" == "linux" ]; then sudo apt-get install -y libxdo-dev; fi +os: + - linux + - osx + diff --git a/vendor/rustdesk/libs/enigo/.vscode/launch.json b/vendor/rustdesk/libs/enigo/.vscode/launch.json new file mode 100644 index 0000000..123e0bc --- /dev/null +++ b/vendor/rustdesk/libs/enigo/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + + { + "name": "Debug", + "type": "gdb", + "request": "launch", + "target": "./target/debug/examples/keyboard", + "cwd": "${workspaceRoot}" + } + ] +} \ No newline at end of file diff --git a/vendor/rustdesk/libs/enigo/Cargo.toml b/vendor/rustdesk/libs/enigo/Cargo.toml new file mode 100644 index 0000000..6468eee --- /dev/null +++ b/vendor/rustdesk/libs/enigo/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "enigo" +version = "0.0.14" +authors = ["Dustin Bensing "] +edition = "2018" +build = "build.rs" + +description = "Enigo lets you control your mouse and keyboard in an abstract way on different operating systems (currently only Linux, macOS, Win – Redox and *BSD planned)" +documentation = "https://docs.rs/enigo/" +homepage = "https://github.com/enigo-rs/enigo" +repository = "https://github.com/enigo-rs/enigo" +readme = "README.md" +keywords = ["input", "mouse", "testing", "keyboard", "automation"] +categories = ["development-tools::testing", "api-bindings", "hardware-support"] +license = "MIT" + +[badges] +travis-ci = { repository = "enigo-rs/enigo" } +appveyor = { repository = "pythoneer/enigo-85xiy" } + +[dependencies] +serde = { version = "1.0", optional = true } +serde_derive = { version = "1.0", optional = true } +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" } + +[features] +with_serde = ["serde", "serde_derive"] + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3", features = ["winuser", "winbase"] } + +[target.'cfg(target_os = "macos")'.dependencies] +core-graphics = "0.22" +objc = "0.2" +unicode-segmentation = "1.10" + +[target.'cfg(target_os = "linux")'.dependencies] +libxdo-sys = "0.11" + +[build-dependencies] +pkg-config = "0.3" diff --git a/vendor/rustdesk/libs/enigo/LICENSE b/vendor/rustdesk/libs/enigo/LICENSE new file mode 100644 index 0000000..d4b9c09 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 pythoneer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/rustdesk/libs/enigo/appveyor.yml b/vendor/rustdesk/libs/enigo/appveyor.yml new file mode 100644 index 0000000..5ad7bc2 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/appveyor.yml @@ -0,0 +1,121 @@ +# AppVeyor configuration template for Rust using rustup for Rust installation +# https://github.com/starkat99/appveyor-rust + +## Operating System (VM environment) ## + +# Rust needs at least Visual Studio 2013 AppVeyor OS for MSVC targets. +os: Visual Studio 2015 + +## Build Matrix ## + +# This configuration will setup a build for each channel & target combination (12 windows +# combinations in all). +# +# There are 3 channels: stable, beta, and nightly. +# +# Alternatively, the full version may be specified for the channel to build using that specific +# version (e.g. channel: 1.5.0) +# +# The values for target are the set of windows Rust build targets. Each value is of the form +# +# ARCH-pc-windows-TOOLCHAIN +# +# Where ARCH is the target architecture, either x86_64 or i686, and TOOLCHAIN is the linker +# toolchain to use, either msvc or gnu. See https://www.rust-lang.org/downloads.html#win-foot for +# a description of the toolchain differences. +# See https://github.com/rust-lang-nursery/rustup.rs/#toolchain-specification for description of +# toolchains and host triples. +# +# Comment out channel/target combos you do not wish to build in CI. +# +# You may use the `cargoflags` and `RUSTFLAGS` variables to set additional flags for cargo commands +# and rustc, respectively. For instance, you can uncomment the cargoflags lines in the nightly +# channels to enable unstable features when building for nightly. Or you could add additional +# matrix entries to test different combinations of features. +environment: + matrix: + +### MSVC Toolchains ### + + # Stable 64-bit MSVC + - channel: stable + target: x86_64-pc-windows-msvc + # Stable 32-bit MSVC + - channel: stable + target: i686-pc-windows-msvc + # Beta 64-bit MSVC + - channel: beta + target: x86_64-pc-windows-msvc + # Beta 32-bit MSVC + - channel: beta + target: i686-pc-windows-msvc + # Nightly 64-bit MSVC + - channel: nightly + target: x86_64-pc-windows-msvc + #cargoflags: --features "unstable" + # Nightly 32-bit MSVC + - channel: nightly + target: i686-pc-windows-msvc + #cargoflags: --features "unstable" + +### GNU Toolchains ### + + # Stable 64-bit GNU + - channel: stable + target: x86_64-pc-windows-gnu + # Stable 32-bit GNU + - channel: stable + target: i686-pc-windows-gnu + # Beta 64-bit GNU + - channel: beta + target: x86_64-pc-windows-gnu + # Beta 32-bit GNU + - channel: beta + target: i686-pc-windows-gnu + # Nightly 64-bit GNU + - channel: nightly + target: x86_64-pc-windows-gnu + #cargoflags: --features "unstable" + # Nightly 32-bit GNU + - channel: nightly + target: i686-pc-windows-gnu + #cargoflags: --features "unstable" + +### Allowed failures ### + +# See AppVeyor documentation for specific details. In short, place any channel or targets you wish +# to allow build failures on (usually nightly at least is a wise choice). This will prevent a build +# or test failure in the matching channels/targets from failing the entire build. +matrix: + allow_failures: + - channel: nightly + +# If you only care about stable channel build failures, uncomment the following line: + #- channel: beta + +## Install Script ## + +# This is the most important part of the AppVeyor configuration. This installs the version of Rust +# specified by the 'channel' and 'target' environment variables from the build matrix. This uses +# rustup to install Rust. +# +# For simple configurations, instead of using the build matrix, you can simply set the +# default-toolchain and default-host manually here. +install: + - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe + - rustup-init -yv --default-toolchain %channel% --default-host %target% + - set PATH=%PATH%;%USERPROFILE%\.cargo\bin + - rustc -vV + - cargo -vV + +## Build Script ## + +# 'cargo test' takes care of building for us, so disable AppVeyor's build stage. This prevents +# the "directory does not contain a project or solution file" error. +build: false + +# Uses 'cargo test' to run tests and build. Alternatively, the project may call compiled programs +#directly or perform other testing commands. Rust will automatically be placed in the PATH +# environment variable. +test_script: + - cargo test --verbose %cargoflags% diff --git a/vendor/rustdesk/libs/enigo/build.rs b/vendor/rustdesk/libs/enigo/build.rs new file mode 100644 index 0000000..6672b22 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/build.rs @@ -0,0 +1,61 @@ +#[cfg(target_os = "windows")] +fn main() {} + +#[cfg(target_os = "macos")] +fn main() {} + +#[cfg(target_os = "linux")] +use pkg_config; +#[cfg(target_os = "linux")] +use std::env; +#[cfg(target_os = "linux")] +use std::fs::File; +#[cfg(target_os = "linux")] +use std::io::Write; +#[cfg(target_os = "linux")] +use std::path::Path; + +#[cfg(target_os = "linux")] +fn main() { + let libraries = [ + "xext", + "gl", + "xcursor", + "xxf86vm", + "xft", + "xinerama", + "xi", + "x11", + "xlib_xcb", + "xmu", + "xrandr", + "xtst", + "xrender", + "xscrnsaver", + "xt", + ]; + + let mut config = String::new(); + for lib in libraries.iter() { + let libdir = match pkg_config::get_variable(lib, "libdir") { + Ok(libdir) => format!("Some(\"{}\")", libdir), + Err(_) => "None".to_string(), + }; + config.push_str(&format!( + "pub const {}: Option<&'static str> = {};\n", + lib, libdir + )); + } + let config = format!("pub mod config {{ pub mod libdir {{\n{}}}\n}}", config); + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("config.rs"); + let mut f = File::create(&dest_path).unwrap(); + f.write_all(&config.into_bytes()).unwrap(); + + let target = env::var("TARGET").unwrap(); + if target.contains("linux") { + println!("cargo:rustc-link-lib=dl"); + } else if target.contains("freebsd") || target.contains("dragonfly") { + println!("cargo:rustc-link-lib=c"); + } +} diff --git a/vendor/rustdesk/libs/enigo/rustfmt.toml b/vendor/rustdesk/libs/enigo/rustfmt.toml new file mode 100644 index 0000000..b2715b2 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/rustfmt.toml @@ -0,0 +1 @@ +wrap_comments = true diff --git a/vendor/rustdesk/libs/enigo/src/dsl.rs b/vendor/rustdesk/libs/enigo/src/dsl.rs new file mode 100644 index 0000000..dfb8adb --- /dev/null +++ b/vendor/rustdesk/libs/enigo/src/dsl.rs @@ -0,0 +1,184 @@ +use crate::{Key, KeyboardControllable}; +use std::error::Error; +use std::fmt; + +/// An error that can occur when parsing DSL +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError { + /// When a tag doesn't exist. + /// Example: {+TEST}{-TEST} + /// ^^^^ ^^^^ + UnknownTag(String), + + /// When a { is encountered inside a {TAG}. + /// Example: {+HELLO{WORLD} + /// ^ + UnexpectedOpen, + + /// When a { is never matched with a }. + /// Example: {+SHIFT}Hello{-SHIFT + /// ^ + UnmatchedOpen, + + /// Opposite of UnmatchedOpen. + /// Example: +SHIFT}Hello{-SHIFT} + /// ^ + UnmatchedClose, +} +impl Error for ParseError { + fn description(&self) -> &str { + match *self { + ParseError::UnknownTag(_) => "Unknown tag", + ParseError::UnexpectedOpen => "Unescaped open bracket ({) found inside tag name", + ParseError::UnmatchedOpen => "Unmatched open bracket ({). No matching close (})", + ParseError::UnmatchedClose => "Unmatched close bracket (}). No previous open ({)", + } + } +} +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.to_string()) + } +} + +/// Evaluate the DSL. This tokenizes the input and presses the keys. +pub fn eval(enigo: &mut K, input: &str) -> Result<(), ParseError> +where + K: KeyboardControllable, +{ + for token in tokenize(input)? { + match token { + Token::Sequence(buffer) => { + for key in buffer.chars() { + enigo.key_click(Key::Layout(key)); + } + } + Token::Unicode(buffer) => enigo.key_sequence(&buffer), + Token::KeyUp(key) => enigo.key_up(key), + Token::KeyDown(key) => enigo.key_down(key).unwrap_or(()), + } + } + Ok(()) +} + +#[derive(Debug, PartialEq, Eq)] +enum Token { + Sequence(String), + Unicode(String), + KeyUp(Key), + KeyDown(Key), +} + +fn tokenize(input: &str) -> Result, ParseError> { + let mut unicode = false; + + let mut tokens = Vec::new(); + let mut buffer = String::new(); + let mut iter = input.chars().peekable(); + + fn flush(tokens: &mut Vec, buffer: String, unicode: bool) { + if !buffer.is_empty() { + if unicode { + tokens.push(Token::Unicode(buffer)); + } else { + tokens.push(Token::Sequence(buffer)); + } + } + } + + while let Some(c) = iter.next() { + if c == '{' { + match iter.next() { + Some('{') => buffer.push('{'), + Some(mut c) => { + flush(&mut tokens, buffer, unicode); + buffer = String::new(); + + let mut tag = String::new(); + loop { + tag.push(c); + match iter.next() { + Some('{') => match iter.peek() { + Some(&'{') => { + iter.next(); + c = '{' + } + _ => return Err(ParseError::UnexpectedOpen), + }, + Some('}') => match iter.peek() { + Some(&'}') => { + iter.next(); + c = '}' + } + _ => break, + }, + Some(new) => c = new, + None => return Err(ParseError::UnmatchedOpen), + } + } + match &*tag { + "+UNICODE" => unicode = true, + "-UNICODE" => unicode = false, + "+SHIFT" => tokens.push(Token::KeyDown(Key::Shift)), + "-SHIFT" => tokens.push(Token::KeyUp(Key::Shift)), + "+CTRL" => tokens.push(Token::KeyDown(Key::Control)), + "-CTRL" => tokens.push(Token::KeyUp(Key::Control)), + "+META" => tokens.push(Token::KeyDown(Key::Meta)), + "-META" => tokens.push(Token::KeyUp(Key::Meta)), + "+ALT" => tokens.push(Token::KeyDown(Key::Alt)), + "-ALT" => tokens.push(Token::KeyUp(Key::Alt)), + _ => return Err(ParseError::UnknownTag(tag)), + } + } + None => return Err(ParseError::UnmatchedOpen), + } + } else if c == '}' { + match iter.next() { + Some('}') => buffer.push('}'), + _ => return Err(ParseError::UnmatchedClose), + } + } else { + buffer.push(c); + } + } + + flush(&mut tokens, buffer, unicode); + + Ok(tokens) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn success() { + assert_eq!( + tokenize("{{Hello World!}} {+CTRL}hi{-CTRL}"), + Ok(vec![ + Token::Sequence("{Hello World!} ".into()), + Token::KeyDown(Key::Control), + Token::Sequence("hi".into()), + Token::KeyUp(Key::Control) + ]) + ); + } + #[test] + fn unexpected_open() { + assert_eq!(tokenize("{hello{}world}"), Err(ParseError::UnexpectedOpen)); + } + #[test] + fn unmatched_open() { + assert_eq!( + tokenize("{this is going to fail"), + Err(ParseError::UnmatchedOpen) + ); + } + #[test] + fn unmatched_close() { + assert_eq!( + tokenize("{+CTRL}{{this}} is going to fail}"), + Err(ParseError::UnmatchedClose) + ); + } +} diff --git a/vendor/rustdesk/libs/enigo/src/lib.rs b/vendor/rustdesk/libs/enigo/src/lib.rs new file mode 100644 index 0000000..397081c --- /dev/null +++ b/vendor/rustdesk/libs/enigo/src/lib.rs @@ -0,0 +1,552 @@ +//! Enigo lets you simulate mouse and keyboard input-events as if they were +//! made by the actual hardware. The goal is to make it available on different +//! operating systems like Linux, macOS and Windows – possibly many more but +//! [Redox](https://redox-os.org/) and *BSD are planned. Please see the +//! [Repo](https://github.com/enigo-rs/enigo) for the current status. +//! +//! I consider this library in an early alpha status, the API will change in +//! in the future. The keyboard handling is far from being very usable. I plan +//! to build a simple +//! [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) +//! that will resemble something like: +//! +//! `"hello {+SHIFT}world{-SHIFT} and break line{ENTER}"` +//! +//! The current status is that you can just print +//! [unicode](http://unicode.org/) +//! characters like [emoji](http://getemoji.com/) without the `{+SHIFT}` +//! [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) +//! or any other "special" key on the Linux, macOS and Windows operating system. +//! +//! Possible use cases could be for testing user interfaces on different +//! platforms, +//! building remote control applications or just automating tasks for user +//! interfaces unaccessible by a public API or scripting language. +//! +//! For the keyboard there are currently two modes you can use. The first mode +//! is represented by the [key_sequence]() function +//! its purpose is to simply write unicode characters. This is independent of +//! the keyboardlayout. Please note that +//! you're not be able to use modifier keys like Control +//! to influence the outcome. If you want to use modifier keys to e.g. +//! copy/paste +//! use the Layout variant. Please note that this is indeed layout dependent. + +//! # Examples +//! ```no_run +//! use enigo::*; +//! let mut enigo = Enigo::new(); +//! //paste +//! enigo.key_down(Key::Control); +//! enigo.key_click(Key::Layout('v')); +//! enigo.key_up(Key::Control); +//! ``` +//! +//! ```no_run +//! use enigo::*; +//! let mut enigo = Enigo::new(); +//! enigo.mouse_move_to(500, 200); +//! enigo.mouse_down(MouseButton::Left); +//! enigo.mouse_move_relative(100, 100); +//! enigo.mouse_up(MouseButton::Left); +//! enigo.key_sequence("hello world"); +//! ``` +#![deny(missing_docs)] + +#[cfg(target_os = "macos")] +#[macro_use] +extern crate objc; + +// TODO(dustin) use interior mutability not &mut self + +#[cfg(target_os = "windows")] +mod win; +#[cfg(target_os = "windows")] +pub use win::Enigo; +#[cfg(target_os = "windows")] +pub use win::ENIGO_INPUT_EXTRA_VALUE; + +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "macos")] +pub use macos::Enigo; +#[cfg(target_os = "macos")] +pub use macos::ENIGO_INPUT_EXTRA_VALUE; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +pub use crate::linux::Enigo; + +/// DSL parser module +pub mod dsl; + +#[cfg(feature = "with_serde")] +#[macro_use] +extern crate serde_derive; + +#[cfg(feature = "with_serde")] +extern crate serde; + +/// +pub type ResultType = std::result::Result<(), Box>; + +#[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq)] +/// MouseButton represents a mouse button, +/// and is used in for example +/// [mouse_click](trait.MouseControllable.html#tymethod.mouse_click). +/// WARNING: Types with the prefix Scroll +/// IS NOT intended to be used, and may not work on +/// all operating systems. +pub enum MouseButton { + /// Left mouse button + Left, + /// Middle mouse button + Middle, + /// Right mouse button + Right, + /// Back mouse button + Back, + /// Forward mouse button + Forward, + + /// Scroll up button + ScrollUp, + /// Left right button + ScrollDown, + /// Left right button + ScrollLeft, + /// Left right button + ScrollRight, +} + +/// Representing an interface and a set of mouse functions every +/// operating system implementation _should_ implement. +pub trait MouseControllable { + // https://stackoverflow.com/a/33687996 + /// Offer the ability to confer concrete type. + fn as_any(&self) -> &dyn std::any::Any; + + /// Offer the ability to confer concrete type. + fn as_mut_any(&mut self) -> &mut dyn std::any::Any; + + /// Lets the mouse cursor move to the specified x and y coordinates. + /// + /// The topleft corner of your monitor screen is x=0 y=0. Move + /// the cursor down the screen by increasing the y and to the right + /// by increasing x coordinate. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_move_to(500, 200); + /// ``` + fn mouse_move_to(&mut self, x: i32, y: i32); + + /// Lets the mouse cursor move the specified amount in the x and y + /// direction. + /// + /// The amount specified in the x and y parameters are added to the + /// current location of the mouse cursor. A positive x values lets + /// the mouse cursor move an amount of `x` pixels to the right. A negative + /// value for `x` lets the mouse cursor go to the left. A positive value + /// of y + /// lets the mouse cursor go down, a negative one lets the mouse cursor go + /// up. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_move_relative(100, 100); + /// ``` + fn mouse_move_relative(&mut self, x: i32, y: i32); + + /// Push down one of the mouse buttons + /// + /// Push down the mouse button specified by the parameter `button` of + /// type [MouseButton](enum.MouseButton.html) + /// and holds it until it is released by + /// [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). + /// Calls to [mouse_move_to](trait.MouseControllable.html#tymethod. + /// mouse_move_to) or + /// [mouse_move_relative](trait.MouseControllable.html#tymethod. + /// mouse_move_relative) + /// will work like expected and will e.g. drag widgets or highlight text. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_down(MouseButton::Left); + /// ``` + fn mouse_down(&mut self, button: MouseButton) -> ResultType; + + /// Lift up a pushed down mouse button + /// + /// Lift up a previously pushed down button (by invoking + /// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down)). + /// If the button was not pushed down or consecutive calls without + /// invoking [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) + /// will emit lift up events. It depends on the + /// operating system whats actually happening – my guess is it will just + /// get ignored. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_up(MouseButton::Right); + /// ``` + fn mouse_up(&mut self, button: MouseButton); + + /// Click a mouse button + /// + /// it's essentially just a consecutive invocation of + /// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) followed + /// by a [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). Just + /// for + /// convenience. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_click(MouseButton::Right); + /// ``` + fn mouse_click(&mut self, button: MouseButton); + + /// Scroll the mouse (wheel) left or right + /// + /// Positive numbers for length lets the mouse wheel scroll to the right + /// and negative ones to the left. The value that is specified translates + /// to `lines` defined by the operating system and is essentially one 15° + /// (click)rotation on the mouse wheel. How many lines it moves depends + /// on the current setting in the operating system. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_scroll_x(2); + /// ``` + fn mouse_scroll_x(&mut self, length: i32); + + /// Scroll the mouse (wheel) up or down + /// + /// Positive numbers for length lets the mouse wheel scroll down + /// and negative ones up. The value that is specified translates + /// to `lines` defined by the operating system and is essentially one 15° + /// (click)rotation on the mouse wheel. How many lines it moves depends + /// on the current setting in the operating system. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.mouse_scroll_y(2); + /// ``` + fn mouse_scroll_y(&mut self, length: i32); +} + +/// A key on the keyboard. +/// For alphabetical keys, use Key::Layout for a system independent key. +/// If a key is missing, you can use the raw keycode with Key::Raw. +#[cfg_attr(feature = "with_serde", derive(Serialize, Deserialize))] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum Key { + /// alt key on Linux and Windows (option key on macOS) + Alt, + /// backspace key + Backspace, + /// caps lock key + CapsLock, + // #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + /// command key on macOS (super key on Linux, windows key on Windows) + Command, + /// control key + Control, + /// delete key + Delete, + /// down arrow key + DownArrow, + /// end key + End, + /// escape key (esc) + Escape, + /// F1 key + F1, + /// F10 key + F10, + /// F11 key + F11, + /// F12 key + F12, + /// F2 key + F2, + /// F3 key + F3, + /// F4 key + F4, + /// F5 key + F5, + /// F6 key + F6, + /// F7 key + F7, + /// F8 key + F8, + /// F9 key + F9, + /// home key + Home, + /// left arrow key + LeftArrow, + /// meta key (also known as "windows", "super", and "command") + Meta, + /// option key on macOS (alt key on Linux and Windows) + Option, // deprecated, use Alt instead + /// page down key + PageDown, + /// page up key + PageUp, + /// return key + Return, + /// right arrow key + RightArrow, + /// shift key + Shift, + /// space key + Space, + // #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + /// super key on linux (command key on macOS, windows key on Windows) + Super, + /// tab key (tabulator) + Tab, + /// up arrow key + UpArrow, + // #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + /// windows key on Windows (super key on Linux, command key on macOS) + Windows, + /// + Numpad0, + /// + Numpad1, + /// + Numpad2, + /// + Numpad3, + /// + Numpad4, + /// + Numpad5, + /// + Numpad6, + /// + Numpad7, + /// + Numpad8, + /// + Numpad9, + /// + Cancel, + /// + Clear, + /// + Pause, + /// + Kana, + /// + Hangul, + /// + Junja, + /// + Final, + /// + Hanja, + /// + Kanji, + /// + Convert, + /// + Select, + /// + Print, + /// + Execute, + /// + Snapshot, + /// + Insert, + /// + Help, + /// + Sleep, + /// + Separator, + /// + VolumeUp, + /// + VolumeDown, + /// + Mute, + /// + Scroll, + /// scroll lock + NumLock, + /// + RWin, + /// + Apps, + /// + Multiply, + /// + Add, + /// + Subtract, + /// + Decimal, + /// + Divide, + /// + Equals, + /// + NumpadEnter, + /// + RightShift, + /// + RightControl, + /// + RightAlt, + /// + /// Function, /// mac + /// keyboard layout dependent key + Layout(char), + /// raw keycode eg 0x38 + Raw(u16), +} + +/// Representing an interface and a set of keyboard functions every +/// operating system implementation _should_ implement. +pub trait KeyboardControllable { + // https://stackoverflow.com/a/33687996 + /// Offer the ability to confer concrete type. + fn as_any(&self) -> &dyn std::any::Any; + + /// Offer the ability to confer concrete type. + fn as_mut_any(&mut self) -> &mut dyn std::any::Any; + + /// Types the string parsed with DSL. + /// + /// Typing {+SHIFT}hello{-SHIFT} becomes HELLO. + /// TODO: Full documentation + fn key_sequence_parse(&mut self, sequence: &str) + where + Self: Sized, + { + if let Err(..) = self.key_sequence_parse_try(sequence) { + println!("Could not parse sequence"); + } + } + /// Same as key_sequence_parse except returns any errors + fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), dsl::ParseError> + where + Self: Sized, + { + dsl::eval(self, sequence) + } + + /// Types the string + /// + /// Emits keystrokes such that the given string is inputted. + /// + /// You can use many unicode here like: ❤️. This works + /// regardless of the current keyboardlayout. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// enigo.key_sequence("hello world ❤️"); + /// ``` + fn key_sequence(&mut self, sequence: &str); + + /// presses a given key down + fn key_down(&mut self, key: Key) -> ResultType; + + /// release a given key formally pressed down by + /// [key_down](trait.KeyboardControllable.html#tymethod.key_down) + fn key_up(&mut self, key: Key); + + /// Much like the + /// [key_down](trait.KeyboardControllable.html#tymethod.key_down) and + /// [key_up](trait.KeyboardControllable.html#tymethod.key_up) + /// function they're just invoked consecutively + fn key_click(&mut self, key: Key); + + /// + fn get_key_state(&mut self, key: Key) -> bool; +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +struct Enigo; + +impl Enigo { + /// Constructs a new `Enigo` instance. + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut enigo = Enigo::new(); + /// ``` + pub fn new() -> Self { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Enigo {}; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Self::default() + } +} + +use std::fmt; + +impl fmt::Debug for Enigo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Enigo") + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_get_key_state() { + let mut enigo = Enigo::new(); + let keys = [Key::CapsLock, Key::NumLock]; + for k in keys.iter() { + enigo.key_click(k.clone()); + let a = enigo.get_key_state(k.clone()); + enigo.key_click(k.clone()); + let b = enigo.get_key_state(k.clone()); + assert!(a != b); + } + let keys = [Key::Control, Key::Alt, Key::Shift]; + for k in keys.iter() { + enigo.key_down(k.clone()).ok(); + let a = enigo.get_key_state(k.clone()); + enigo.key_up(k.clone()); + let b = enigo.get_key_state(k.clone()); + assert!(a != b); + } + } +} diff --git a/vendor/rustdesk/libs/enigo/src/linux/mod.rs b/vendor/rustdesk/libs/enigo/src/linux/mod.rs new file mode 100644 index 0000000..1f73004 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/src/linux/mod.rs @@ -0,0 +1,4 @@ +mod nix_impl; +mod xdo; + +pub use self::nix_impl::Enigo; diff --git a/vendor/rustdesk/libs/enigo/src/linux/nix_impl.rs b/vendor/rustdesk/libs/enigo/src/linux/nix_impl.rs new file mode 100644 index 0000000..c16be34 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/src/linux/nix_impl.rs @@ -0,0 +1,392 @@ +use super::xdo::EnigoXdo; +use crate::{Key, KeyboardControllable, MouseButton, MouseControllable, ResultType}; +use std::io::Read; +use tfc::{traits::*, Context as TFC_Context, Key as TFC_Key}; + +pub type CustomKeyboard = Box; +pub type CustomMouce = Box; + +/// The main struct for handling the event emitting +// #[derive(Default)] +pub struct Enigo { + xdo: EnigoXdo, + is_x11: bool, + tfc: Option, + custom_keyboard: Option, + custom_mouse: Option, +} + +impl Enigo { + /// Get delay of xdo implementation. + pub fn delay(&self) -> u64 { + self.xdo.delay() + } + /// Set delay of xdo implementation. + pub fn set_delay(&mut self, delay: u64) { + self.xdo.set_delay(delay) + } + /// Set custom keyboard. + pub fn set_custom_keyboard(&mut self, custom_keyboard: CustomKeyboard) { + self.custom_keyboard = Some(custom_keyboard) + } + /// Set custom mouse. + pub fn set_custom_mouse(&mut self, custom_mouse: CustomMouce) { + self.custom_mouse = Some(custom_mouse) + } + /// Get custom keyboard. + pub fn get_custom_keyboard(&mut self) -> &mut Option { + &mut self.custom_keyboard + } + /// Get custom mouse. + pub fn get_custom_mouse(&mut self) -> &mut Option { + &mut self.custom_mouse + } + + /// Clear remapped keycodes + pub fn tfc_clear_remapped(&mut self) { + if let Some(tfc) = &mut self.tfc { + tfc.recover_remapped_keycodes(); + } + } + + fn tfc_key_click(&mut self, key: Key) -> ResultType { + if let Some(tfc) = &mut self.tfc { + let res = match key { + Key::Layout(chr) => tfc.unicode_char(chr), + key => { + let tfc_key: TFC_Key = match convert_to_tfc_key(key) { + Some(key) => key, + None => { + return Err(format!("Failed to convert {:?} to TFC_Key", key).into()); + } + }; + tfc.key_click(tfc_key) + } + }; + if res.is_err() { + Err(format!("Failed to click {:?} by tfc", key).into()) + } else { + Ok(()) + } + } else { + Err("Not Found TFC".into()) + } + } + + fn tfc_key_down_or_up(&mut self, key: Key, down: bool, up: bool) -> bool { + match &mut self.tfc { + None => false, + Some(tfc) => { + if let Key::Layout(chr) = key { + if down { + if let Err(_) = tfc.unicode_char_down(chr) { + return false; + } + } + if up { + if let Err(_) = tfc.unicode_char_up(chr) { + return false; + } + } + return true; + } + let key = match convert_to_tfc_key(key) { + Some(key) => key, + None => { + return false; + } + }; + + if down { + if let Err(_) = tfc.key_down(key) { + return false; + } + }; + if up { + if let Err(_) = tfc.key_up(key) { + return false; + } + }; + return true; + } + } + } +} + +impl Default for Enigo { + fn default() -> Self { + let is_x11 = hbb_common::platform::linux::is_x11_or_headless(); + Self { + is_x11, + tfc: if is_x11 { + match TFC_Context::new() { + Ok(ctx) => Some(ctx), + Err(..) => { + println!("kbd context error"); + None + } + } + } else { + None + }, + custom_keyboard: None, + custom_mouse: None, + xdo: EnigoXdo::default(), + } + } +} + +impl MouseControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn mouse_move_to(&mut self, x: i32, y: i32) { + if self.is_x11 { + self.xdo.mouse_move_to(x, y); + } else { + if let Some(mouse) = &mut self.custom_mouse { + mouse.mouse_move_to(x, y) + } + } + } + fn mouse_move_relative(&mut self, x: i32, y: i32) { + if self.is_x11 { + self.xdo.mouse_move_relative(x, y); + } else { + if let Some(mouse) = &mut self.custom_mouse { + mouse.mouse_move_relative(x, y) + } + } + } + fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { + if self.is_x11 { + self.xdo.mouse_down(button) + } else { + if let Some(mouse) = &mut self.custom_mouse { + mouse.mouse_down(button) + } else { + Ok(()) + } + } + } + fn mouse_up(&mut self, button: MouseButton) { + if self.is_x11 { + self.xdo.mouse_up(button) + } else { + if let Some(mouse) = &mut self.custom_mouse { + mouse.mouse_up(button) + } + } + } + fn mouse_click(&mut self, button: MouseButton) { + if self.is_x11 { + self.xdo.mouse_click(button) + } else { + if let Some(mouse) = &mut self.custom_mouse { + mouse.mouse_click(button) + } + } + } + fn mouse_scroll_x(&mut self, length: i32) { + if self.is_x11 { + self.xdo.mouse_scroll_x(length) + } else { + if let Some(mouse) = &mut self.custom_mouse { + mouse.mouse_scroll_x(length) + } + } + } + fn mouse_scroll_y(&mut self, length: i32) { + if self.is_x11 { + self.xdo.mouse_scroll_y(length) + } else { + if let Some(mouse) = &mut self.custom_mouse { + mouse.mouse_scroll_y(length) + } + } + } +} + +fn get_led_state(key: Key) -> bool { + let led_file = match key { + // FIXME: the file may be /sys/class/leds/input2 or input5 ... + Key::CapsLock => "/sys/class/leds/input1::capslock/brightness", + Key::NumLock => "/sys/class/leds/input1::numlock/brightness", + _ => { + return false; + } + }; + + let status = if let Ok(mut file) = std::fs::File::open(&led_file) { + let mut content = String::new(); + file.read_to_string(&mut content).ok(); + let status = content.trim_end().to_string().parse::().unwrap_or(0); + status + } else { + 0 + }; + status == 1 +} + +impl KeyboardControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn get_key_state(&mut self, key: Key) -> bool { + if self.is_x11 { + self.xdo.get_key_state(key) + } else { + if let Some(keyboard) = &mut self.custom_keyboard { + keyboard.get_key_state(key) + } else { + get_led_state(key) + } + } + } + + /// Warning: Get 6^ in French. + fn key_sequence(&mut self, sequence: &str) { + if self.is_x11 { + self.xdo.key_sequence(sequence) + } else { + if let Some(keyboard) = &mut self.custom_keyboard { + keyboard.key_sequence(sequence) + } else { + log::warn!("Enigo::key_sequence: no custom_keyboard set for Wayland!"); + } + } + } + + fn key_down(&mut self, key: Key) -> crate::ResultType { + if self.is_x11 { + let has_down = self.tfc_key_down_or_up(key, true, false); + if !has_down { + self.xdo.key_down(key) + } else { + Ok(()) + } + } else { + if let Some(keyboard) = &mut self.custom_keyboard { + keyboard.key_down(key) + } else { + log::warn!("Enigo::key_down: no custom_keyboard set for Wayland!"); + Ok(()) + } + } + } + fn key_up(&mut self, key: Key) { + if self.is_x11 { + let has_down = self.tfc_key_down_or_up(key, false, true); + if !has_down { + self.xdo.key_up(key) + } + } else { + if let Some(keyboard) = &mut self.custom_keyboard { + keyboard.key_up(key) + } else { + log::warn!("Enigo::key_up: no custom_keyboard set for Wayland!"); + } + } + } + fn key_click(&mut self, key: Key) { + if self.is_x11 { + // X11: try tfc first, then fallback to key_down/key_up + if self.tfc_key_click(key).is_err() { + self.key_down(key).ok(); + self.key_up(key); + } + } else { + if let Some(keyboard) = &mut self.custom_keyboard { + keyboard.key_click(key); + } else { + log::warn!("Enigo::key_click: no custom_keyboard set for Wayland!"); + } + } + } +} + +fn convert_to_tfc_key(key: Key) -> Option { + let key = match key { + Key::Alt => TFC_Key::Alt, + Key::Backspace => TFC_Key::DeleteOrBackspace, + Key::CapsLock => TFC_Key::CapsLock, + Key::Control => TFC_Key::Control, + Key::Delete => TFC_Key::ForwardDelete, + Key::DownArrow => TFC_Key::DownArrow, + Key::End => TFC_Key::End, + Key::Escape => TFC_Key::Escape, + Key::F1 => TFC_Key::F1, + Key::F10 => TFC_Key::F10, + Key::F11 => TFC_Key::F11, + Key::F12 => TFC_Key::F12, + Key::F2 => TFC_Key::F2, + Key::F3 => TFC_Key::F3, + Key::F4 => TFC_Key::F4, + Key::F5 => TFC_Key::F5, + Key::F6 => TFC_Key::F6, + Key::F7 => TFC_Key::F7, + Key::F8 => TFC_Key::F8, + Key::F9 => TFC_Key::F9, + Key::Home => TFC_Key::Home, + Key::LeftArrow => TFC_Key::LeftArrow, + Key::PageDown => TFC_Key::PageDown, + Key::PageUp => TFC_Key::PageUp, + Key::Return => TFC_Key::ReturnOrEnter, + Key::RightArrow => TFC_Key::RightArrow, + Key::Shift => TFC_Key::Shift, + Key::Space => TFC_Key::Space, + Key::Tab => TFC_Key::Tab, + Key::UpArrow => TFC_Key::UpArrow, + Key::Numpad0 => TFC_Key::N0, + Key::Numpad1 => TFC_Key::N1, + Key::Numpad2 => TFC_Key::N2, + Key::Numpad3 => TFC_Key::N3, + Key::Numpad4 => TFC_Key::N4, + Key::Numpad5 => TFC_Key::N5, + Key::Numpad6 => TFC_Key::N6, + Key::Numpad7 => TFC_Key::N7, + Key::Numpad8 => TFC_Key::N8, + Key::Numpad9 => TFC_Key::N9, + Key::Decimal => TFC_Key::NumpadDecimal, + Key::Clear => TFC_Key::NumpadClear, + Key::Pause => TFC_Key::Pause, + Key::Print => TFC_Key::Print, + Key::Snapshot => TFC_Key::PrintScreen, + Key::Insert => TFC_Key::Insert, + Key::Scroll => TFC_Key::ScrollLock, + Key::NumLock => TFC_Key::NumLock, + Key::RWin => TFC_Key::Meta, + Key::Apps => TFC_Key::Apps, + Key::Multiply => TFC_Key::NumpadMultiply, + Key::Add => TFC_Key::NumpadPlus, + Key::Subtract => TFC_Key::NumpadMinus, + Key::Divide => TFC_Key::NumpadDivide, + Key::Equals => TFC_Key::NumpadEquals, + Key::NumpadEnter => TFC_Key::NumpadEnter, + Key::RightShift => TFC_Key::RightShift, + Key::RightControl => TFC_Key::RightControl, + Key::RightAlt => TFC_Key::RightAlt, + Key::Command | Key::Super | Key::Windows | Key::Meta => TFC_Key::Meta, + _ => { + return None; + } + }; + Some(key) +} + +#[test] +fn test_key_seq() { + // Get 6^ in French. + let mut en = Enigo::new(); + en.key_sequence("^^"); +} diff --git a/vendor/rustdesk/libs/enigo/src/linux/xdo.rs b/vendor/rustdesk/libs/enigo/src/linux/xdo.rs new file mode 100644 index 0000000..7796904 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/src/linux/xdo.rs @@ -0,0 +1,459 @@ +//! XDO-based input emulation for Linux. +//! +//! This module uses libxdo-sys (patched to use dynamic loading stub) for input emulation. +//! The stub handles dynamic loading of libxdo, so we just call the functions directly. +//! +//! If libxdo is not available at runtime, all operations become no-ops. + +use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; + +use hbb_common::libc::c_int; +use hbb_common::x11::xlib::{Display, XCloseDisplay, XGetPointerMapping, XOpenDisplay}; +use libxdo_sys::{self, xdo_t, CURRENTWINDOW}; +use std::{borrow::Cow, ffi::CString}; + +/// Default delay per keypress in microseconds. +/// This value is passed to libxdo functions and must fit in `useconds_t` (u32). +const DEFAULT_DELAY: u64 = 12000; + +/// Maximum allowed delay value (u32::MAX as u64). +const MAX_DELAY: u64 = u32::MAX as u64; + +fn mousebutton(button: MouseButton) -> c_int { + match button { + MouseButton::Left => 1, + MouseButton::Middle => 2, + MouseButton::Right => 3, + MouseButton::ScrollUp => 4, + MouseButton::ScrollDown => 5, + MouseButton::ScrollLeft => 6, + MouseButton::ScrollRight => 7, + MouseButton::Back => 8, + MouseButton::Forward => 9, + } +} + +/// Minimum number of buttons the X11 core pointer must support. +/// Buttons 8 (Back) and 9 (Forward) are needed for mouse side buttons. +const MIN_POINTER_BUTTONS: usize = 9; + +/// Check that the X11 core pointer's button map includes at least 9 buttons +/// so that `XTestFakeButtonEvent` can simulate Back (8) and Forward (9). +/// +/// RustDesk's uinput "Mouse passthrough" device normally provides enough +/// buttons, but we log a warning if the map is too small so the issue is +/// diagnosable. `XSetPointerMapping` cannot extend the button count (its +/// length must match `XGetPointerMapping`), so we only diagnose here. +fn check_x11_button_map() { + // Skip on non-X11 sessions to avoid noisy "XOpenDisplay failed" warnings + // on pure Wayland or headless environments without $DISPLAY. + if std::env::var_os("DISPLAY").is_none() { + return; + } + + let display: *mut Display = unsafe { XOpenDisplay(std::ptr::null()) }; + if display.is_null() { + log::warn!("XOpenDisplay failed, cannot check button map"); + return; + } + + let mut current_map = [0u8; 32]; + let nbuttons = + unsafe { XGetPointerMapping(display, current_map.as_mut_ptr(), current_map.len() as i32) }; + unsafe { XCloseDisplay(display) }; + + if nbuttons < 0 { + log::warn!("XGetPointerMapping failed (returned {nbuttons})"); + return; + } + + let nbuttons = nbuttons as usize; + if nbuttons >= MIN_POINTER_BUTTONS { + log::info!("X11 pointer has {nbuttons} buttons, side buttons supported"); + } else { + log::warn!( + "X11 pointer has only {nbuttons} buttons (need {MIN_POINTER_BUTTONS}); \ + back/forward side buttons may not work until a device with more buttons is added" + ); + } +} + +/// The main struct for handling the event emitting +pub(super) struct EnigoXdo { + xdo: *mut xdo_t, + delay: u64, +} +// This is safe, we have a unique pointer. +// TODO: use Unique once stable. +unsafe impl Send for EnigoXdo {} + +impl Default for EnigoXdo { + /// Create a new EnigoXdo instance. + /// + /// If libxdo is not available, the xdo pointer will be null and all + /// input operations will be no-ops. + fn default() -> Self { + let xdo = unsafe { libxdo_sys::xdo_new(std::ptr::null()) }; + if xdo.is_null() { + log::warn!("Failed to create xdo context, xdo functions will be disabled"); + } else { + log::info!("xdo context created successfully"); + check_x11_button_map(); + } + Self { + xdo, + delay: DEFAULT_DELAY, + } + } +} + +impl EnigoXdo { + /// Get the delay per keypress in microseconds. + /// + /// Default value is 12000 (12ms). This is Linux-specific. + pub fn delay(&self) -> u64 { + self.delay + } + + /// Set the delay per keypress in microseconds. + /// + /// This is Linux-specific. The value is clamped to `u32::MAX` (approximately + /// 4295 seconds) because libxdo uses `useconds_t` which is typically `u32`. + /// + /// # Arguments + /// * `delay` - Delay in microseconds. Values exceeding `u32::MAX` will be clamped. + pub fn set_delay(&mut self, delay: u64) { + self.delay = delay.min(MAX_DELAY); + if delay > MAX_DELAY { + log::warn!( + "delay value {} exceeds maximum {}, clamped", + delay, + MAX_DELAY + ); + } + } +} + +impl Drop for EnigoXdo { + fn drop(&mut self) { + if !self.xdo.is_null() { + unsafe { + libxdo_sys::xdo_free(self.xdo); + } + } + } +} + +impl MouseControllable for EnigoXdo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn mouse_move_to(&mut self, x: i32, y: i32) { + if self.xdo.is_null() { + return; + } + unsafe { + libxdo_sys::xdo_move_mouse(self.xdo as *const _, x, y, 0); + } + } + + fn mouse_move_relative(&mut self, x: i32, y: i32) { + if self.xdo.is_null() { + return; + } + unsafe { + libxdo_sys::xdo_move_mouse_relative(self.xdo as *const _, x, y); + } + } + + fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { + if self.xdo.is_null() { + return Ok(()); + } + unsafe { + libxdo_sys::xdo_mouse_down(self.xdo as *const _, CURRENTWINDOW, mousebutton(button)); + } + Ok(()) + } + + fn mouse_up(&mut self, button: MouseButton) { + if self.xdo.is_null() { + return; + } + unsafe { + libxdo_sys::xdo_mouse_up(self.xdo as *const _, CURRENTWINDOW, mousebutton(button)); + } + } + + fn mouse_click(&mut self, button: MouseButton) { + if self.xdo.is_null() { + return; + } + unsafe { + libxdo_sys::xdo_click_window(self.xdo as *const _, CURRENTWINDOW, mousebutton(button)); + } + } + + fn mouse_scroll_x(&mut self, length: i32) { + let button; + let mut length = length; + + if length < 0 { + button = MouseButton::ScrollLeft; + } else { + button = MouseButton::ScrollRight; + } + + if length < 0 { + length = -length; + } + + for _ in 0..length { + self.mouse_click(button); + } + } + + fn mouse_scroll_y(&mut self, length: i32) { + let button; + let mut length = length; + + if length < 0 { + button = MouseButton::ScrollUp; + } else { + button = MouseButton::ScrollDown; + } + + if length < 0 { + length = -length; + } + + for _ in 0..length { + self.mouse_click(button); + } + } +} + +fn keysequence<'a>(key: Key) -> Cow<'a, str> { + if let Key::Layout(c) = key { + return Cow::Owned(format!("U{:X}", c as u32)); + } + if let Key::Raw(k) = key { + return Cow::Owned(format!("{}", k as u16)); + } + #[allow(deprecated)] + // I mean duh, we still need to support deprecated keys until they're removed + // https://www.rubydoc.info/gems/xdo/XDo/Keyboard + // https://gitlab.com/cunidev/gestures/-/wikis/xdotool-list-of-key-codes + Cow::Borrowed(match key { + Key::Alt => "Alt", + Key::Backspace => "BackSpace", + Key::CapsLock => "Caps_Lock", + Key::Control => "Control", + Key::Delete => "Delete", + Key::DownArrow => "Down", + Key::End => "End", + Key::Escape => "Escape", + Key::F1 => "F1", + Key::F10 => "F10", + Key::F11 => "F11", + Key::F12 => "F12", + Key::F2 => "F2", + Key::F3 => "F3", + Key::F4 => "F4", + Key::F5 => "F5", + Key::F6 => "F6", + Key::F7 => "F7", + Key::F8 => "F8", + Key::F9 => "F9", + Key::Home => "Home", + //Key::Layout(_) => unreachable!(), + Key::LeftArrow => "Left", + Key::Option => "Option", + Key::PageDown => "Page_Down", + Key::PageUp => "Page_Up", + //Key::Raw(_) => unreachable!(), + Key::Return => "Return", + Key::RightArrow => "Right", + Key::Shift => "Shift", + Key::Space => "space", + Key::Tab => "Tab", + Key::UpArrow => "Up", + Key::Numpad0 => "U30", //"KP_0", + Key::Numpad1 => "U31", //"KP_1", + Key::Numpad2 => "U32", //"KP_2", + Key::Numpad3 => "U33", //"KP_3", + Key::Numpad4 => "U34", //"KP_4", + Key::Numpad5 => "U35", //"KP_5", + Key::Numpad6 => "U36", //"KP_6", + Key::Numpad7 => "U37", //"KP_7", + Key::Numpad8 => "U38", //"KP_8", + Key::Numpad9 => "U39", //"KP_9", + Key::Decimal => "U2E", //"KP_Decimal", + Key::Cancel => "Cancel", + Key::Clear => "Clear", + Key::Pause => "Pause", + Key::Kana => "Kana", + Key::Hangul => "Hangul", + Key::Junja => "", + Key::Final => "", + Key::Hanja => "Hanja", + Key::Kanji => "Kanji", + Key::Convert => "", + Key::Select => "Select", + Key::Print => "Print", + Key::Execute => "Execute", + Key::Snapshot => "3270_PrintScreen", + Key::Insert => "Insert", + Key::Help => "Help", + Key::Sleep => "", + Key::Separator => "KP_Separator", + Key::VolumeUp => "", + Key::VolumeDown => "", + Key::Mute => "", + Key::Scroll => "Scroll_Lock", + Key::NumLock => "Num_Lock", + Key::RWin => "Super_R", + Key::Apps => "Menu", + Key::Multiply => "KP_Multiply", + Key::Add => "KP_Add", + Key::Subtract => "KP_Subtract", + Key::Divide => "KP_Divide", + Key::Equals => "KP_Equal", + Key::NumpadEnter => "KP_Enter", + Key::RightShift => "Shift_R", + Key::RightControl => "Control_R", + Key::RightAlt => "Alt_R", + + Key::Command | Key::Super | Key::Windows | Key::Meta => "Super", + + _ => "", + }) +} + +impl KeyboardControllable for EnigoXdo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn get_key_state(&mut self, key: Key) -> bool { + if self.xdo.is_null() { + return false; + } + /* + // modifier keys mask + pub const ShiftMask: c_uint = 0x01; + pub const LockMask: c_uint = 0x02; + pub const ControlMask: c_uint = 0x04; + pub const Mod1Mask: c_uint = 0x08; + pub const Mod2Mask: c_uint = 0x10; + pub const Mod3Mask: c_uint = 0x20; + pub const Mod4Mask: c_uint = 0x40; + pub const Mod5Mask: c_uint = 0x80; + */ + let mod_shift = 1 << 0; + let mod_lock = 1 << 1; + let mod_control = 1 << 2; + let mod_alt = 1 << 3; + let mod_numlock = 1 << 4; + let mod_meta = 1 << 6; + let mask = unsafe { libxdo_sys::xdo_get_input_state(self.xdo as *const _) }; + match key { + Key::Shift => mask & mod_shift != 0, + Key::CapsLock => mask & mod_lock != 0, + Key::Control => mask & mod_control != 0, + Key::Alt => mask & mod_alt != 0, + Key::NumLock => mask & mod_numlock != 0, + Key::Meta => mask & mod_meta != 0, + _ => false, + } + } + + fn key_sequence(&mut self, sequence: &str) { + if self.xdo.is_null() { + return; + } + if let Ok(string) = CString::new(sequence) { + unsafe { + libxdo_sys::xdo_enter_text_window( + self.xdo as *const _, + CURRENTWINDOW, + string.as_ptr(), + self.delay as libxdo_sys::useconds_t, + ); + } + } + } + + fn key_down(&mut self, key: Key) -> crate::ResultType { + if self.xdo.is_null() { + return Ok(()); + } + let string = CString::new(&*keysequence(key))?; + unsafe { + libxdo_sys::xdo_send_keysequence_window_down( + self.xdo as *const _, + CURRENTWINDOW, + string.as_ptr(), + self.delay as libxdo_sys::useconds_t, + ); + } + Ok(()) + } + + fn key_up(&mut self, key: Key) { + if self.xdo.is_null() { + return; + } + if let Ok(string) = CString::new(&*keysequence(key)) { + unsafe { + libxdo_sys::xdo_send_keysequence_window_up( + self.xdo as *const _, + CURRENTWINDOW, + string.as_ptr(), + self.delay as libxdo_sys::useconds_t, + ); + } + } + } + + fn key_click(&mut self, key: Key) { + if self.xdo.is_null() { + return; + } + if let Ok(string) = CString::new(&*keysequence(key)) { + unsafe { + libxdo_sys::xdo_send_keysequence_window( + self.xdo as *const _, + CURRENTWINDOW, + string.as_ptr(), + self.delay as libxdo_sys::useconds_t, + ); + } + } + } + + fn key_sequence_parse(&mut self, sequence: &str) + where + Self: Sized, + { + if let Err(..) = self.key_sequence_parse_try(sequence) { + println!("Could not parse sequence"); + } + } + + fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), crate::dsl::ParseError> + where + Self: Sized, + { + crate::dsl::eval(self, sequence) + } +} diff --git a/vendor/rustdesk/libs/enigo/src/macos/keycodes.rs b/vendor/rustdesk/libs/enigo/src/macos/keycodes.rs new file mode 100644 index 0000000..540d0e1 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/src/macos/keycodes.rs @@ -0,0 +1,120 @@ +// https://stackoverflow.com/questions/3202629/where-can-i-find-a-list-of-mac-virtual-key-codes + +/* keycodes for keys that are independent of keyboard layout */ + +#![allow(non_upper_case_globals)] +#![allow(dead_code)] + +pub const kVK_Return: u16 = 0x24; +pub const kVK_Tab: u16 = 0x30; +pub const kVK_Space: u16 = 0x31; +pub const kVK_Delete: u16 = 0x33; +pub const kVK_Escape: u16 = 0x35; +pub const kVK_Command: u16 = 0x37; +pub const kVK_Shift: u16 = 0x38; +pub const kVK_CapsLock: u16 = 0x39; +pub const kVK_Option: u16 = 0x3A; +pub const kVK_Control: u16 = 0x3B; +pub const kVK_RightShift: u16 = 0x3C; +pub const kVK_RightOption: u16 = 0x3D; +pub const kVK_RightControl: u16 = 0x3E; +pub const kVK_Function: u16 = 0x3F; +pub const kVK_F17: u16 = 0x40; +pub const kVK_VolumeUp: u16 = 0x48; +pub const kVK_VolumeDown: u16 = 0x49; +pub const kVK_Mute: u16 = 0x4A; +pub const kVK_F18: u16 = 0x4F; +pub const kVK_F19: u16 = 0x50; +pub const kVK_F20: u16 = 0x5A; +pub const kVK_F5: u16 = 0x60; +pub const kVK_F6: u16 = 0x61; +pub const kVK_F7: u16 = 0x62; +pub const kVK_F3: u16 = 0x63; +pub const kVK_F8: u16 = 0x64; +pub const kVK_F9: u16 = 0x65; +pub const kVK_F11: u16 = 0x67; +pub const kVK_F13: u16 = 0x69; +pub const kVK_F16: u16 = 0x6A; +pub const kVK_F14: u16 = 0x6B; +pub const kVK_F10: u16 = 0x6D; +pub const kVK_F12: u16 = 0x6F; +pub const kVK_F15: u16 = 0x71; +pub const kVK_Help: u16 = 0x72; +pub const kVK_Home: u16 = 0x73; +pub const kVK_PageUp: u16 = 0x74; +pub const kVK_ForwardDelete: u16 = 0x75; +pub const kVK_F4: u16 = 0x76; +pub const kVK_End: u16 = 0x77; +pub const kVK_F2: u16 = 0x78; +pub const kVK_PageDown: u16 = 0x79; +pub const kVK_F1: u16 = 0x7A; +pub const kVK_LeftArrow: u16 = 0x7B; +pub const kVK_RightArrow: u16 = 0x7C; +pub const kVK_DownArrow: u16 = 0x7D; +pub const kVK_UpArrow: u16 = 0x7E; +pub const kVK_ANSI_Keypad0: u16 = 0x52; +pub const kVK_ANSI_Keypad1: u16 = 0x53; +pub const kVK_ANSI_Keypad2: u16 = 0x54; +pub const kVK_ANSI_Keypad3: u16 = 0x55; +pub const kVK_ANSI_Keypad4: u16 = 0x56; +pub const kVK_ANSI_Keypad5: u16 = 0x57; +pub const kVK_ANSI_Keypad6: u16 = 0x58; +pub const kVK_ANSI_Keypad7: u16 = 0x59; +pub const kVK_ANSI_Keypad8: u16 = 0x5B; +pub const kVK_ANSI_Keypad9: u16 = 0x5C; +pub const kVK_ANSI_KeypadClear: u16 = 0x47; +pub const kVK_ANSI_KeypadDecimal: u16 = 0x41; +pub const kVK_ANSI_KeypadMultiply: u16 = 0x43; +pub const kVK_ANSI_KeypadPlus: u16 = 0x45; +pub const kVK_ANSI_KeypadDivide: u16 = 0x4B; +pub const kVK_ANSI_KeypadEnter: u16 = 0x4C; +pub const kVK_ANSI_KeypadMinus: u16 = 0x4E; +pub const kVK_ANSI_KeypadEquals: u16 = 0x51; +pub const kVK_RIGHT_COMMAND: u16 = 0x36; +pub const kVK_ANSI_A : u16 = 0x00; +pub const kVK_ANSI_S : u16 = 0x01; +pub const kVK_ANSI_D : u16 = 0x02; +pub const kVK_ANSI_F : u16 = 0x03; +pub const kVK_ANSI_H : u16 = 0x04; +pub const kVK_ANSI_G : u16 = 0x05; +pub const kVK_ANSI_Z : u16 = 0x06; +pub const kVK_ANSI_X : u16 = 0x07; +pub const kVK_ANSI_C : u16 = 0x08; +pub const kVK_ANSI_V : u16 = 0x09; +pub const kVK_ANSI_B : u16 = 0x0B; +pub const kVK_ANSI_Q : u16 = 0x0C; +pub const kVK_ANSI_W : u16 = 0x0D; +pub const kVK_ANSI_E : u16 = 0x0E; +pub const kVK_ANSI_R : u16 = 0x0F; +pub const kVK_ANSI_Y : u16 = 0x10; +pub const kVK_ANSI_T : u16 = 0x11; +pub const kVK_ANSI_1 : u16 = 0x12; +pub const kVK_ANSI_2 : u16 = 0x13; +pub const kVK_ANSI_3 : u16 = 0x14; +pub const kVK_ANSI_4 : u16 = 0x15; +pub const kVK_ANSI_6 : u16 = 0x16; +pub const kVK_ANSI_5 : u16 = 0x17; +pub const kVK_ANSI_Equal : u16 = 0x18; +pub const kVK_ANSI_9 : u16 = 0x19; +pub const kVK_ANSI_7 : u16 = 0x1A; +pub const kVK_ANSI_Minus : u16 = 0x1B; +pub const kVK_ANSI_8 : u16 = 0x1C; +pub const kVK_ANSI_0 : u16 = 0x1D; +pub const kVK_ANSI_RightBracket : u16 = 0x1E; +pub const kVK_ANSI_O : u16 = 0x1F; +pub const kVK_ANSI_U : u16 = 0x20; +pub const kVK_ANSI_LeftBracket : u16 = 0x21; +pub const kVK_ANSI_I : u16 = 0x22; +pub const kVK_ANSI_P : u16 = 0x23; +pub const kVK_ANSI_L : u16 = 0x25; +pub const kVK_ANSI_J : u16 = 0x26; +pub const kVK_ANSI_Quote : u16 = 0x27; +pub const kVK_ANSI_K : u16 = 0x28; +pub const kVK_ANSI_Semicolon : u16 = 0x29; +pub const kVK_ANSI_Backslash : u16 = 0x2A; +pub const kVK_ANSI_Comma : u16 = 0x2B; +pub const kVK_ANSI_Slash : u16 = 0x2C; +pub const kVK_ANSI_N : u16 = 0x2D; +pub const kVK_ANSI_M : u16 = 0x2E; +pub const kVK_ANSI_Period : u16 = 0x2F; +pub const kVK_ANSI_Grave : u16 = 0x32; diff --git a/vendor/rustdesk/libs/enigo/src/macos/macos_impl.rs b/vendor/rustdesk/libs/enigo/src/macos/macos_impl.rs new file mode 100644 index 0000000..20f5d0c --- /dev/null +++ b/vendor/rustdesk/libs/enigo/src/macos/macos_impl.rs @@ -0,0 +1,864 @@ +use core_graphics; +// TODO(dustin): use only the things i need + +use self::core_graphics::display::*; +use self::core_graphics::event::*; +use self::core_graphics::event_source::*; +use std::collections::HashMap as Map; +use std::ffi::c_void; +use std::ffi::CStr; +use std::os::raw::*; +use std::ptr::null_mut; + +use crate::macos::keycodes::*; +use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; +use objc::runtime::Class; + +struct MyCGEvent; +type TISInputSourceRef = *mut c_void; +type CFDataRef = *const c_void; +type OptionBits = u32; +type OSStatus = i32; +type UniChar = u16; +type UniCharCount = usize; +type Boolean = c_uchar; +type CFStringEncoding = u32; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct __CFString([u8; 0]); +type CFStringRef = *const __CFString; + +#[allow(non_upper_case_globals)] +const kCFStringEncodingUTF8: u32 = 134_217_984; +#[allow(non_upper_case_globals)] +const kUCKeyActionDisplay: u16 = 3; +#[allow(non_upper_case_globals)] +const kUCKeyTranslateDeadKeysBit: OptionBits = 1 << 31; +const BUF_LEN: usize = 4; + +const MOUSE_EVENT_BUTTON_NUMBER_BACK: i64 = 3; +const MOUSE_EVENT_BUTTON_NUMBER_FORWARD: i64 = 4; + +/// The event source user data value of cgevent. +pub const ENIGO_INPUT_EXTRA_VALUE: i64 = 100; + +#[allow(improper_ctypes)] +#[allow(non_snake_case)] +#[link(name = "ApplicationServices", kind = "framework")] +#[link(name = "Carbon", kind = "framework")] +extern "C" { + fn CFDataGetBytePtr(theData: CFDataRef) -> *const u8; + fn TISCopyCurrentKeyboardInputSource() -> TISInputSourceRef; + fn TISCopyCurrentKeyboardLayoutInputSource() -> TISInputSourceRef; + fn TISCopyCurrentASCIICapableKeyboardLayoutInputSource() -> TISInputSourceRef; + static kTISPropertyUnicodeKeyLayoutData: *mut c_void; + static kTISPropertyInputSourceID: *mut c_void; + fn UCKeyTranslate( + keyLayoutPtr: *const u8, //*const UCKeyboardLayout, + virtualKeyCode: u16, + keyAction: u16, + modifierKeyState: u32, + keyboardType: u32, + keyTranslateOptions: OptionBits, + deadKeyState: *mut u32, + maxStringLength: UniCharCount, + actualStringLength: *mut UniCharCount, + unicodeString: *mut [UniChar; BUF_LEN], + ) -> OSStatus; + fn LMGetKbdType() -> u8; + fn CFStringGetCString( + theString: CFStringRef, + buffer: *mut c_char, + bufferSize: CFIndex, + encoding: CFStringEncoding, + ) -> Boolean; + + fn CGEventPost(tapLocation: CGEventTapLocation, event: *mut MyCGEvent); + // Actually return CFDataRef which is const here, but for coding convenience, return *mut c_void + fn TISGetInputSourceProperty(source: TISInputSourceRef, property: *const c_void) + -> *mut c_void; + // not present in servo/core-graphics + fn CGEventCreateScrollWheelEvent( + source: &CGEventSourceRef, + units: ScrollUnit, + wheelCount: u32, + wheel1: i32, + ... + ) -> *mut MyCGEvent; + fn CGEventSourceKeyState(stateID: i32, key: u16) -> bool; +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct NSPoint { + x: f64, + y: f64, +} + +// not present in servo/core-graphics +#[allow(dead_code)] +#[derive(Debug)] +enum ScrollUnit { + Pixel = 0, + Line = 1, +} +// hack + +/// The main struct for handling the event emitting +pub struct Enigo { + event_source: Option, + double_click_interval: u32, + last_click_time: Option, + multiple_click: i64, + ignore_flags: bool, + flags: CGEventFlags, + char_to_vkey_map: Map>, +} + +impl Enigo { + /// Set if ignore flags when posting events. + pub fn set_ignore_flags(&mut self, ignore: bool) { + self.ignore_flags = ignore; + } + + /// + pub fn reset_flag(&mut self) { + self.flags = CGEventFlags::CGEventFlagNull; + } + + /// + pub fn add_flag(&mut self, key: &Key) { + let flag = match key { + &Key::CapsLock => CGEventFlags::CGEventFlagAlphaShift, + &Key::Shift => CGEventFlags::CGEventFlagShift, + &Key::Control => CGEventFlags::CGEventFlagControl, + &Key::Alt => CGEventFlags::CGEventFlagAlternate, + &Key::Meta => CGEventFlags::CGEventFlagCommand, + &Key::NumLock => CGEventFlags::CGEventFlagNumericPad, + _ => CGEventFlags::CGEventFlagNull, + }; + self.flags |= flag; + } + + // Just check F11 for minimal changes. + // Since enigo (legacy mode) is deprecated, it is currently in maintenance only. + fn post(&self, event: CGEvent, keycode: Option) { + if keycode == Some(kVK_F11) { + // Some key events require the flags to work. + // We can't simply set the flag to `CGEventFlags::CGEventFlagNull`. + // eg. `F11` requires flags `CGEventFlags::CGEventFlagSecondaryFn | 0x20000000` to work. + self.post_event(event, false); + } else { + // macOS system may use the previous event flag to generate the next event. + // Only found this issue when locking the screen. + // When we use enigo to lock the screen, the next mouse event will have the flag + // `CGEventFlagControl | CGEventFlagCommand | 0x20000000`. + // The key event will also have the flag `CGEventFlagControl | CGEventFlagCommand | 0x20000000`. + // Therefore, we need to set the flag to `event.set_flags(self.flags)` to avoid this. + self.post_event(event, true); + } + } + + fn post_event(&self, event: CGEvent, force_flags: bool) { + if !self.ignore_flags && (force_flags || self.flags != CGEventFlags::CGEventFlagNull) { + event.set_flags(self.flags); + } + event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE); + event.post(CGEventTapLocation::HID); + } +} + +impl Default for Enigo { + fn default() -> Self { + let mut double_click_interval = 500; + if let Some(ns_event) = Class::get("NSEvent") { + let tm: f64 = unsafe { msg_send![ns_event, doubleClickInterval] }; + if tm > 0. { + double_click_interval = (tm * 1000.) as u32; + log::info!("double click interval: {}ms", double_click_interval); + } + } + Self { + // TODO(dustin): return error rather than panic here + event_source: if let Ok(src) = + CGEventSource::new(CGEventSourceStateID::CombinedSessionState) + { + Some(src) + } else { + None + }, + double_click_interval, + multiple_click: 1, + last_click_time: None, + ignore_flags: false, + flags: CGEventFlags::CGEventFlagNull, + char_to_vkey_map: Default::default(), + } + } +} + +impl MouseControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn mouse_move_to(&mut self, x: i32, y: i32) { + // For absolute movement, we don't set delta values + // This maintains backward compatibility + self.mouse_move_to_impl(x, y, None); + } + + fn mouse_move_relative(&mut self, x: i32, y: i32) { + let (display_width, display_height) = Self::main_display_size(); + let (current_x, y_inv) = Self::mouse_location_raw_coords(); + let current_y = (display_height as i32) - y_inv; + // Use saturating arithmetic to prevent overflow/wraparound + let mut new_x = current_x.saturating_add(x); + let mut new_y = current_y.saturating_add(y); + + // Define screen center and edge margins for cursor reset + let center_x = (display_width / 2) as i32; + let center_y = (display_height / 2) as i32; + // Margin calculation: 5% of the smaller screen dimension with a minimum of 50px. + // This provides a comfortable buffer zone to detect when the cursor is approaching + // screen edges, allowing us to reset it to center before it hits the boundary. + // This ensures continuous relative mouse movement without getting stuck at edges. + let margin = (display_width.min(display_height) / 20).max(50) as i32; + + // Check if cursor is approaching screen boundaries + // Use saturating_sub to prevent negative thresholds on very small displays + let right = (display_width as i32).saturating_sub(margin); + let bottom = (display_height as i32).saturating_sub(margin); + let near_edge = new_x < margin + || new_x > right + || new_y < margin + || new_y > bottom; + + if near_edge { + // Reset cursor to screen center to allow continuous movement + // The delta values are still passed correctly for games/apps + new_x = center_x; + new_y = center_y; + } + + // Clamp to screen bounds as a safety measure. + // Use saturating_sub(1) to ensure coordinates don't exceed the last valid pixel. + let max_x = (display_width as i32).saturating_sub(1).max(0); + let max_y = (display_height as i32).saturating_sub(1).max(0); + new_x = new_x.clamp(0, max_x); + new_y = new_y.clamp(0, max_y); + + // Pass delta values for relative movement + // This is critical for browser Pointer Lock API support + // The delta fields (MOUSE_EVENT_DELTA_X/Y) are used by browsers + // to calculate movementX/Y in Pointer Lock mode + self.mouse_move_to_impl(new_x, new_y, Some((x, y))); + } + + fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { + let now = std::time::Instant::now(); + if let Some(t) = self.last_click_time { + if t.elapsed().as_millis() as u32 <= self.double_click_interval { + self.multiple_click += 1; + } else { + self.multiple_click = 1; + } + } + self.last_click_time = Some(now); + let (current_x, current_y) = Self::mouse_location(); + let (button, event_type, btn_value) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown, None), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown, None), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown, None), + MouseButton::Back => ( + CGMouseButton::Left, + CGEventType::OtherMouseDown, + Some(MOUSE_EVENT_BUTTON_NUMBER_BACK), + ), + MouseButton::Forward => ( + CGMouseButton::Left, + CGEventType::OtherMouseDown, + Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD), + ), + _ => { + log::info!("Unsupported button {:?}", button); + return Ok(()); + } + }; + let dest = CGPoint::new(current_x as f64, current_y as f64); + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_mouse_event(src.clone(), event_type, dest, button) { + if self.multiple_click > 1 { + event.set_integer_value_field( + EventField::MOUSE_EVENT_CLICK_STATE, + self.multiple_click, + ); + } + if let Some(v) = btn_value { + event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v); + } + self.post(event, None); + } + } + Ok(()) + } + + fn mouse_up(&mut self, button: MouseButton) { + let (current_x, current_y) = Self::mouse_location(); + let (button, event_type, btn_value) = match button { + MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp, None), + MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp, None), + MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp, None), + MouseButton::Back => ( + CGMouseButton::Left, + CGEventType::OtherMouseUp, + Some(MOUSE_EVENT_BUTTON_NUMBER_BACK), + ), + MouseButton::Forward => ( + CGMouseButton::Left, + CGEventType::OtherMouseUp, + Some(MOUSE_EVENT_BUTTON_NUMBER_FORWARD), + ), + _ => { + log::info!("Unsupported button {:?}", button); + return; + } + }; + let dest = CGPoint::new(current_x as f64, current_y as f64); + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_mouse_event(src.clone(), event_type, dest, button) { + if self.multiple_click > 1 { + event.set_integer_value_field( + EventField::MOUSE_EVENT_CLICK_STATE, + self.multiple_click, + ); + } + if let Some(v) = btn_value { + event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v); + } + self.post(event, None); + } + } + } + + fn mouse_click(&mut self, button: MouseButton) { + self.mouse_down(button).ok(); + self.mouse_up(button); + } + + fn mouse_scroll_x(&mut self, length: i32) { + let mut scroll_direction = -1; // 1 left -1 right; + let mut length = length; + + if length < 0 { + length *= -1; + scroll_direction *= -1; + } + + if let Some(src) = self.event_source.as_ref() { + for _ in 0..length { + unsafe { + let mouse_ev = CGEventCreateScrollWheelEvent( + &src, + ScrollUnit::Line, + 2, // CGWheelCount 1 = y 2 = xy 3 = xyz + 0, + scroll_direction, + ); + + CGEventPost(CGEventTapLocation::HID, mouse_ev); + CFRelease(mouse_ev as *const std::ffi::c_void); + } + } + } + } + + fn mouse_scroll_y(&mut self, length: i32) { + let mut scroll_direction = -1; // 1 left -1 right; + let mut length = length; + + if length < 0 { + length *= -1; + scroll_direction *= -1; + } + + if let Some(src) = self.event_source.as_ref() { + for _ in 0..length { + unsafe { + let mouse_ev = CGEventCreateScrollWheelEvent( + &src, + ScrollUnit::Line, + 1, // CGWheelCount 1 = y 2 = xy 3 = xyz + scroll_direction, + ); + + CGEventPost(CGEventTapLocation::HID, mouse_ev); + CFRelease(mouse_ev as *const std::ffi::c_void); + } + } + } + } +} + +// https://stackoverflow. +// com/questions/1918841/how-to-convert-ascii-character-to-cgkeycode + +impl KeyboardControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn key_sequence(&mut self, sequence: &str) { + // NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68 + // TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time + // but i am unsure what would happen for grapheme clusters greater than 20 bytes ... + use unicode_segmentation::UnicodeSegmentation; + let clusters = UnicodeSegmentation::graphemes(sequence, true).collect::>(); + for cluster in clusters { + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), 0, true) { + event.set_string(cluster); + self.post(event, None); + } + } + } + } + + fn key_click(&mut self, key: Key) { + let keycode = self.key_to_keycode(key); + if keycode == u16::MAX { + return; + } + + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, true) { + self.post(event, Some(keycode)); + } + + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, false) { + self.post(event, Some(keycode)); + } + } + } + + fn key_down(&mut self, key: Key) -> crate::ResultType { + let code = self.key_to_keycode(key); + if code == u16::MAX { + return Err("".into()); + } + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), code, true) { + self.post(event, Some(code)); + } + } + Ok(()) + } + + fn key_up(&mut self, key: Key) { + let code = self.key_to_keycode(key); + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), code, false) { + self.post(event, Some(code)); + } + } + } + + fn get_key_state(&mut self, key: Key) -> bool { + let keycode = self.key_to_keycode(key); + unsafe { CGEventSourceKeyState(1, keycode) } + } +} + +impl Enigo { + fn pressed_buttons() -> usize { + if let Some(ns_event) = Class::get("NSEvent") { + unsafe { msg_send![ns_event, pressedMouseButtons] } + } else { + 0 + } + } + + /// Internal implementation for mouse movement with optional delta values. + /// + /// The `delta` parameter is crucial for browser Pointer Lock API support. + /// When a browser enters Pointer Lock mode, it reads mouse delta values + /// (MOUSE_EVENT_DELTA_X/Y) directly from CGEvent to calculate movementX/Y. + /// Without setting these fields, the browser sees zero movement. + fn mouse_move_to_impl(&mut self, x: i32, y: i32, delta: Option<(i32, i32)>) { + let pressed = Self::pressed_buttons(); + + // Determine event type and corresponding mouse button based on pressed buttons. + // The CGMouseButton must match the event type for drag events. + let (event_type, button) = if pressed & 1 > 0 { + (CGEventType::LeftMouseDragged, CGMouseButton::Left) + } else if pressed & 2 > 0 { + (CGEventType::RightMouseDragged, CGMouseButton::Right) + } else if pressed & 4 > 0 { + (CGEventType::OtherMouseDragged, CGMouseButton::Center) + } else { + (CGEventType::MouseMoved, CGMouseButton::Left) // Button doesn't matter for MouseMoved + }; + + let dest = CGPoint::new(x as f64, y as f64); + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = + CGEvent::new_mouse_event(src.clone(), event_type, dest, button) + { + // Set delta fields for relative mouse movement + // This is essential for Pointer Lock API in browsers + if let Some((dx, dy)) = delta { + event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_X, dx as i64); + event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_Y, dy as i64); + } + self.post(event, None); + } + } + } + + /// Fetches the `(width, height)` in pixels of the main display + pub fn main_display_size() -> (usize, usize) { + let display_id = unsafe { CGMainDisplayID() }; + let width = unsafe { CGDisplayPixelsWide(display_id) }; + let height = unsafe { CGDisplayPixelsHigh(display_id) }; + (width, height) + } + + /// Returns the current mouse location in Cocoa coordinates which have Y + /// inverted from the Carbon coordinates used in the rest of the API. + /// This function exists so that mouse_move_relative only has to fetch + /// the screen size once. + fn mouse_location_raw_coords() -> (i32, i32) { + if let Some(ns_event) = Class::get("NSEvent") { + let pt: NSPoint = unsafe { msg_send![ns_event, mouseLocation] }; + (pt.x as i32, pt.y as i32) + } else { + (0, 0) + } + } + + /// The mouse coordinates in points, only works on the main display + pub fn mouse_location() -> (i32, i32) { + let (x, y_inv) = Self::mouse_location_raw_coords(); + let (_, display_height) = Self::main_display_size(); + (x, (display_height as i32) - y_inv) + } + + fn key_to_keycode(&mut self, key: Key) -> CGKeyCode { + #[allow(deprecated)] + // I mean duh, we still need to support deprecated keys until they're removed + match key { + Key::Alt => kVK_Option, + Key::Backspace => kVK_Delete, + Key::CapsLock => kVK_CapsLock, + Key::Control => kVK_Control, + Key::Delete => kVK_ForwardDelete, + Key::DownArrow => kVK_DownArrow, + Key::End => kVK_End, + Key::Escape => kVK_Escape, + Key::F1 => kVK_F1, + Key::F10 => kVK_F10, + Key::F11 => kVK_F11, + Key::F12 => kVK_F12, + Key::F2 => kVK_F2, + Key::F3 => kVK_F3, + Key::F4 => kVK_F4, + Key::F5 => kVK_F5, + Key::F6 => kVK_F6, + Key::F7 => kVK_F7, + Key::F8 => kVK_F8, + Key::F9 => kVK_F9, + Key::Home => kVK_Home, + Key::LeftArrow => kVK_LeftArrow, + Key::Option => kVK_Option, + Key::PageDown => kVK_PageDown, + Key::PageUp => kVK_PageUp, + Key::Return => kVK_Return, + Key::RightArrow => kVK_RightArrow, + Key::Shift => kVK_Shift, + Key::Space => kVK_Space, + Key::Tab => kVK_Tab, + Key::UpArrow => kVK_UpArrow, + Key::Numpad0 => kVK_ANSI_Keypad0, + Key::Numpad1 => kVK_ANSI_Keypad1, + Key::Numpad2 => kVK_ANSI_Keypad2, + Key::Numpad3 => kVK_ANSI_Keypad3, + Key::Numpad4 => kVK_ANSI_Keypad4, + Key::Numpad5 => kVK_ANSI_Keypad5, + Key::Numpad6 => kVK_ANSI_Keypad6, + Key::Numpad7 => kVK_ANSI_Keypad7, + Key::Numpad8 => kVK_ANSI_Keypad8, + Key::Numpad9 => kVK_ANSI_Keypad9, + Key::Mute => kVK_Mute, + Key::VolumeDown => kVK_VolumeUp, + Key::VolumeUp => kVK_VolumeDown, + Key::Help => kVK_Help, + Key::Snapshot => kVK_F13, + Key::Clear => kVK_ANSI_KeypadClear, + Key::Decimal => kVK_ANSI_KeypadDecimal, + Key::Multiply => kVK_ANSI_KeypadMultiply, + Key::Add => kVK_ANSI_KeypadPlus, + Key::Divide => kVK_ANSI_KeypadDivide, + Key::NumpadEnter => kVK_ANSI_KeypadEnter, + Key::Subtract => kVK_ANSI_KeypadMinus, + Key::Equals => kVK_ANSI_KeypadEquals, + Key::NumLock => kVK_ANSI_KeypadClear, + Key::RWin => kVK_RIGHT_COMMAND, + Key::RightShift => kVK_RightShift, + Key::RightControl => kVK_RightControl, + Key::RightAlt => kVK_RightOption, + + Key::Raw(raw_keycode) => raw_keycode, + Key::Layout(c) => self.map_key_board(c), + + Key::Super | Key::Command | Key::Windows | Key::Meta => kVK_Command, + _ => u16::MAX, + } + } + + #[inline] + fn map_key_board(&mut self, ch: char) -> CGKeyCode { + // no idea why below char not working with shift, https://github.com/rustdesk/rustdesk/issues/406#issuecomment-1145157327 + // seems related to numpad char + if ch == '-' || ch == '=' || ch == '.' || ch == '/' || (ch >= '0' && ch <= '9') { + return self.map_key_board_en(ch); + } + let mut code = u16::MAX; + unsafe { + let (keyboard, layout) = get_layout(); + if !keyboard.is_null() && !layout.is_null() { + let name_ref = TISGetInputSourceProperty(keyboard, kTISPropertyInputSourceID); + if !name_ref.is_null() { + let name = get_string(name_ref as _); + if let Some(name) = name { + if let Some(m) = self.char_to_vkey_map.get(&name) { + code = *m.get(&ch).unwrap_or(&u16::MAX); + } else { + let m = get_map(&name, layout); + code = *m.get(&ch).unwrap_or(&u16::MAX); + self.char_to_vkey_map.insert(name.clone(), m); + } + } + } + } + if !keyboard.is_null() { + CFRelease(keyboard); + } + } + if code != u16::MAX { + return code; + } + self.map_key_board_en(ch) + } + + #[inline] + fn map_key_board_en(&mut self, ch: char) -> CGKeyCode { + match ch { + 'a' => kVK_ANSI_A, + 'b' => kVK_ANSI_B, + 'c' => kVK_ANSI_C, + 'd' => kVK_ANSI_D, + 'e' => kVK_ANSI_E, + 'f' => kVK_ANSI_F, + 'g' => kVK_ANSI_G, + 'h' => kVK_ANSI_H, + 'i' => kVK_ANSI_I, + 'j' => kVK_ANSI_J, + 'k' => kVK_ANSI_K, + 'l' => kVK_ANSI_L, + 'm' => kVK_ANSI_M, + 'n' => kVK_ANSI_N, + 'o' => kVK_ANSI_O, + 'p' => kVK_ANSI_P, + 'q' => kVK_ANSI_Q, + 'r' => kVK_ANSI_R, + 's' => kVK_ANSI_S, + 't' => kVK_ANSI_T, + 'u' => kVK_ANSI_U, + 'v' => kVK_ANSI_V, + 'w' => kVK_ANSI_W, + 'x' => kVK_ANSI_X, + 'y' => kVK_ANSI_Y, + 'z' => kVK_ANSI_Z, + '0' => kVK_ANSI_0, + '1' => kVK_ANSI_1, + '2' => kVK_ANSI_2, + '3' => kVK_ANSI_3, + '4' => kVK_ANSI_4, + '5' => kVK_ANSI_5, + '6' => kVK_ANSI_6, + '7' => kVK_ANSI_7, + '8' => kVK_ANSI_8, + '9' => kVK_ANSI_9, + '-' => kVK_ANSI_Minus, + '=' => kVK_ANSI_Equal, + '[' => kVK_ANSI_LeftBracket, + ']' => kVK_ANSI_RightBracket, + '\\' => kVK_ANSI_Backslash, + ';' => kVK_ANSI_Semicolon, + '\'' => kVK_ANSI_Quote, + ',' => kVK_ANSI_Comma, + '.' => kVK_ANSI_Period, + '/' => kVK_ANSI_Slash, + '`' => kVK_ANSI_Grave, + _ => u16::MAX, + } + } + + #[inline] + fn mouse_scroll_impl(&mut self, length: i32, is_track_pad: bool, is_horizontal: bool) { + let mut scroll_direction = -1; // 1 left -1 right; + let mut length = length; + + if length < 0 { + length *= -1; + scroll_direction *= -1; + } + + if let Some(src) = self.event_source.as_ref() { + for _ in 0..length { + unsafe { + let units = if is_track_pad { + ScrollUnit::Pixel + } else { + ScrollUnit::Line + }; + let mouse_ev = if is_horizontal { + CGEventCreateScrollWheelEvent( + &src, + units, + 2, // CGWheelCount 1 = y 2 = xy 3 = xyz + 0, + scroll_direction, + ) + } else { + CGEventCreateScrollWheelEvent( + &src, + units, + 1, // CGWheelCount 1 = y 2 = xy 3 = xyz + scroll_direction, + ) + }; + + CGEventPost(CGEventTapLocation::HID, mouse_ev); + CFRelease(mouse_ev as *const std::ffi::c_void); + } + } + } + } + + /// handle scroll vertically + pub fn mouse_scroll_y(&mut self, length: i32, is_track_pad: bool) { + self.mouse_scroll_impl(length, is_track_pad, false) + } + + /// handle scroll horizontally + pub fn mouse_scroll_x(&mut self, length: i32, is_track_pad: bool) { + self.mouse_scroll_impl(length, is_track_pad, true) + } +} + +#[inline] +unsafe fn get_string(cf_string: CFStringRef) -> Option { + if !cf_string.is_null() { + let mut buf: [i8; 255] = [0; 255]; + let success = CFStringGetCString( + cf_string, + buf.as_mut_ptr(), + buf.len() as _, + kCFStringEncodingUTF8, + ); + if success != 0 { + let name: &CStr = CStr::from_ptr(buf.as_ptr()); + if let Ok(name) = name.to_str() { + return Some(name.to_string()); + } + } + } + None +} + +#[inline] +unsafe fn get_layout() -> (TISInputSourceRef, *const u8) { + let mut keyboard = TISCopyCurrentKeyboardInputSource(); + let mut layout = null_mut(); + if !keyboard.is_null() { + layout = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); + } + if layout.is_null() { + if !keyboard.is_null() { + CFRelease(keyboard); + } + // https://github.com/microsoft/vscode/issues/23833 + keyboard = TISCopyCurrentKeyboardLayoutInputSource(); + if !keyboard.is_null() { + layout = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); + } + } + if layout.is_null() { + if !keyboard.is_null() { + CFRelease(keyboard); + } + keyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + if !keyboard.is_null() { + layout = TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData); + } + } + if layout.is_null() { + if !keyboard.is_null() { + CFRelease(keyboard); + } + return (null_mut(), null_mut()); + } + let layout_ptr = CFDataGetBytePtr(layout as _); + if layout_ptr.is_null() { + if !keyboard.is_null() { + CFRelease(keyboard); + } + return (null_mut(), null_mut()); + } + (keyboard, layout_ptr) +} + +#[inline] +fn get_map(name: &str, layout: *const u8) -> Map { + log::info!("Create keyboard map for {}", name); + let mut keys_down: u32 = 0; + let mut map = Map::new(); + for keycode in 0..128 { + let mut buff = [0_u16; BUF_LEN]; + let kb_type = unsafe { LMGetKbdType() }; + let mut length: UniCharCount = 0; + let _retval = unsafe { + UCKeyTranslate( + layout, + keycode, + kUCKeyActionDisplay as _, + 0, + kb_type as _, + kUCKeyTranslateDeadKeysBit as _, + &mut keys_down, + BUF_LEN, + &mut length, + &mut buff, + ) + }; + if length > 0 { + if let Ok(str) = String::from_utf16(&buff[..length]) { + if let Some(chr) = str.chars().next() { + map.insert(chr, keycode as _); + } + } + } + } + map +} +unsafe impl Send for Enigo {} diff --git a/vendor/rustdesk/libs/enigo/src/macos/mod.rs b/vendor/rustdesk/libs/enigo/src/macos/mod.rs new file mode 100644 index 0000000..f9aebb5 --- /dev/null +++ b/vendor/rustdesk/libs/enigo/src/macos/mod.rs @@ -0,0 +1,4 @@ +mod macos_impl; + +pub mod keycodes; +pub use self::macos_impl::{Enigo, ENIGO_INPUT_EXTRA_VALUE}; diff --git a/vendor/rustdesk/libs/enigo/src/win/keycodes.rs b/vendor/rustdesk/libs/enigo/src/win/keycodes.rs new file mode 100644 index 0000000..500582b --- /dev/null +++ b/vendor/rustdesk/libs/enigo/src/win/keycodes.rs @@ -0,0 +1,83 @@ +#![allow(dead_code)] +// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731 +// +// JP/KR mapping https://github.com/TigerVNC/tigervnc/blob/1a008c1380305648ab50f1d99e73439747e9d61d/vncviewer/win32.c#L267 +// altgr handle: https://github.com/TigerVNC/tigervnc/blob/dccb95f345f7a9c5aa785a19d1bfa3fdecd8f8e0/vncviewer/Viewport.cxx#L1066 + +pub const EVK_RETURN: u16 = 0x0D; +pub const EVK_TAB: u16 = 0x09; +pub const EVK_SPACE: u16 = 0x20; +pub const EVK_BACK: u16 = 0x08; +pub const EVK_ESCAPE: u16 = 0x1b; +pub const EVK_LWIN: u16 = 0x5b; +pub const EVK_SHIFT: u16 = 0x10; +//pub const EVK_LSHIFT: u16 = 0xa0; +pub const EVK_RSHIFT: u16 = 0xa1; +//pub const EVK_LMENU: u16 = 0xa4; +pub const EVK_RMENU: u16 = 0xa5; +pub const EVK_CAPITAL: u16 = 0x14; +pub const EVK_MENU: u16 = 0x12; +pub const EVK_LCONTROL: u16 = 0xa2; +pub const EVK_RCONTROL: u16 = 0xa3; +pub const EVK_HOME: u16 = 0x24; +pub const EVK_PRIOR: u16 = 0x21; +pub const EVK_NEXT: u16 = 0x22; +pub const EVK_END: u16 = 0x23; +pub const EVK_LEFT: u16 = 0x25; +pub const EVK_RIGHT: u16 = 0x27; +pub const EVK_UP: u16 = 0x26; +pub const EVK_DOWN: u16 = 0x28; +pub const EVK_DELETE: u16 = 0x2E; +pub const EVK_F1: u16 = 0x70; +pub const EVK_F2: u16 = 0x71; +pub const EVK_F3: u16 = 0x72; +pub const EVK_F4: u16 = 0x73; +pub const EVK_F5: u16 = 0x74; +pub const EVK_F6: u16 = 0x75; +pub const EVK_F7: u16 = 0x76; +pub const EVK_F8: u16 = 0x77; +pub const EVK_F9: u16 = 0x78; +pub const EVK_F10: u16 = 0x79; +pub const EVK_F11: u16 = 0x7a; +pub const EVK_F12: u16 = 0x7b; +pub const EVK_NUMPAD0: u16 = 0x60; +pub const EVK_NUMPAD1: u16 = 0x61; +pub const EVK_NUMPAD2: u16 = 0x62; +pub const EVK_NUMPAD3: u16 = 0x63; +pub const EVK_NUMPAD4: u16 = 0x64; +pub const EVK_NUMPAD5: u16 = 0x65; +pub const EVK_NUMPAD6: u16 = 0x66; +pub const EVK_NUMPAD7: u16 = 0x67; +pub const EVK_NUMPAD8: u16 = 0x68; +pub const EVK_NUMPAD9: u16 = 0x69; +pub const EVK_CANCEL: u16 = 0x03; +pub const EVK_CLEAR: u16 = 0x0C; +pub const EVK_PAUSE: u16 = 0x13; +pub const EVK_KANA: u16 = 0x15; +pub const EVK_HANGUL: u16 = 0x15; +pub const EVK_JUNJA: u16 = 0x17; +pub const EVK_FINAL: u16 = 0x18; +pub const EVK_HANJA: u16 = 0x19; +pub const EVK_KANJI: u16 = 0x19; +pub const EVK_CONVERT: u16 = 0x1C; +pub const EVK_SELECT: u16 = 0x29; +pub const EVK_PRINT: u16 = 0x2A; +pub const EVK_EXECUTE: u16 = 0x2B; +pub const EVK_SNAPSHOT: u16 = 0x2C; +pub const EVK_INSERT: u16 = 0x2D; +pub const EVK_HELP: u16 = 0x2F; +pub const EVK_SLEEP: u16 = 0x5F; +pub const EVK_SEPARATOR: u16 = 0x6C; +pub const EVK_VOLUME_MUTE: u16 = 0xAD; +pub const EVK_VOLUME_DOWN: u16 = 0xAE; +pub const EVK_VOLUME_UP: u16 = 0xAF; +pub const EVK_NUMLOCK: u16 = 0x90; +pub const EVK_SCROLL: u16 = 0x91; +pub const EVK_RWIN: u16 = 0x5C; +pub const EVK_APPS: u16 = 0x5D; +pub const EVK_ADD: u16 = 0x6B; +pub const EVK_MULTIPLY: u16 = 0x6A; +pub const EVK_SUBTRACT: u16 = 0x6D; +pub const EVK_DECIMAL: u16 = 0x6E; +pub const EVK_DIVIDE: u16 = 0x6F; +pub const EVK_PERIOD: u16 = 0xBE; diff --git a/vendor/rustdesk/libs/enigo/src/win/mod.rs b/vendor/rustdesk/libs/enigo/src/win/mod.rs new file mode 100644 index 0000000..4ec95ee --- /dev/null +++ b/vendor/rustdesk/libs/enigo/src/win/mod.rs @@ -0,0 +1,4 @@ +mod win_impl; + +pub mod keycodes; +pub use self::win_impl::{Enigo, ENIGO_INPUT_EXTRA_VALUE}; diff --git a/vendor/rustdesk/libs/enigo/src/win/win_impl.rs b/vendor/rustdesk/libs/enigo/src/win/win_impl.rs new file mode 100644 index 0000000..a6b465e --- /dev/null +++ b/vendor/rustdesk/libs/enigo/src/win/win_impl.rs @@ -0,0 +1,478 @@ +use self::winapi::ctypes::c_int; +use self::winapi::shared::{basetsd::ULONG_PTR, minwindef::*, windef::*}; +use self::winapi::um::winbase::*; +use self::winapi::um::winuser::*; +use winapi; + +use crate::win::keycodes::*; +use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; +use std::mem::*; + +extern "system" { + pub fn GetLastError() -> DWORD; +} + +/// The main struct for handling the event emitting +#[derive(Default)] +pub struct Enigo; +static mut LAYOUT: HKL = std::ptr::null_mut(); + +/// The dwExtraInfo value in keyboard and mouse structure that used in SendInput() +pub const ENIGO_INPUT_EXTRA_VALUE: ULONG_PTR = 100; + +fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) -> DWORD { + let mut u = INPUT_u::default(); + unsafe { + *u.mi_mut() = MOUSEINPUT { + dx, + dy, + mouseData: data, + dwFlags: flags, + time: 0, + dwExtraInfo: ENIGO_INPUT_EXTRA_VALUE, + }; + } + let mut input = INPUT { + type_: INPUT_MOUSE, + u, + }; + unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } +} + +fn keybd_event(mut flags: u32, vk: u16, scan: u16) -> DWORD { + let mut scan = scan; + unsafe { + // https://github.com/rustdesk/rustdesk/issues/366 + if scan == 0 { + if LAYOUT.is_null() { + let current_window_thread_id = + GetWindowThreadProcessId(GetForegroundWindow(), std::ptr::null_mut()); + LAYOUT = GetKeyboardLayout(current_window_thread_id); + } + scan = MapVirtualKeyExW(vk as _, 0, LAYOUT) as _; + } + } + + if flags & KEYEVENTF_UNICODE == 0 { + if scan >> 8 == 0xE0 || scan >> 8 == 0xE1 { + flags |= winapi::um::winuser::KEYEVENTF_EXTENDEDKEY; + } + } + let mut union: INPUT_u = unsafe { std::mem::zeroed() }; + unsafe { + *union.ki_mut() = KEYBDINPUT { + wVk: vk, + wScan: scan, + dwFlags: flags, + time: 0, + dwExtraInfo: ENIGO_INPUT_EXTRA_VALUE, + }; + } + let mut inputs = [INPUT { + type_: INPUT_KEYBOARD, + u: union, + }; 1]; + unsafe { + SendInput( + inputs.len() as UINT, + inputs.as_mut_ptr(), + size_of::() as c_int, + ) + } +} + +fn get_error() -> String { + unsafe { + let buff_size = 256; + let mut buff: Vec = Vec::with_capacity(buff_size); + buff.resize(buff_size, 0); + let errno = GetLastError(); + let chars_copied = FormatMessageW( + FORMAT_MESSAGE_IGNORE_INSERTS + | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_ARGUMENT_ARRAY, + std::ptr::null(), + errno, + 0, + buff.as_mut_ptr(), + (buff_size + 1) as u32, + std::ptr::null_mut(), + ); + if chars_copied == 0 { + return "".to_owned(); + } + let mut curr_char: usize = chars_copied as usize; + while curr_char > 0 { + let ch = buff[curr_char]; + + if ch >= ' ' as u16 { + break; + } + curr_char -= 1; + } + let sl = std::slice::from_raw_parts(buff.as_ptr(), curr_char); + let err_msg = String::from_utf16(sl); + return err_msg.unwrap_or("".to_owned()); + } +} + +impl MouseControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn mouse_move_to(&mut self, x: i32, y: i32) { + mouse_event( + MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK, + 0, + (x - unsafe { GetSystemMetrics(SM_XVIRTUALSCREEN) }) * 65535 + / unsafe { GetSystemMetrics(SM_CXVIRTUALSCREEN) }, + (y - unsafe { GetSystemMetrics(SM_YVIRTUALSCREEN) }) * 65535 + / unsafe { GetSystemMetrics(SM_CYVIRTUALSCREEN) }, + ); + } + + fn mouse_move_relative(&mut self, x: i32, y: i32) { + mouse_event(MOUSEEVENTF_MOVE, 0, x, y); + } + + fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { + let res = mouse_event( + match button { + MouseButton::Left => MOUSEEVENTF_LEFTDOWN, + MouseButton::Middle => MOUSEEVENTF_MIDDLEDOWN, + MouseButton::Right => MOUSEEVENTF_RIGHTDOWN, + MouseButton::Back => MOUSEEVENTF_XDOWN, + MouseButton::Forward => MOUSEEVENTF_XDOWN, + _ => { + log::info!("Unsupported button {:?}", button); + return Ok(()); + } + }, + match button { + MouseButton::Back => XBUTTON1 as u32, + MouseButton::Forward => XBUTTON2 as u32, + _ => 0, + }, + 0, + 0, + ); + if res == 0 { + let err = get_error(); + if !err.is_empty() { + return Err(err.into()); + } + } + Ok(()) + } + + fn mouse_up(&mut self, button: MouseButton) { + mouse_event( + match button { + MouseButton::Left => MOUSEEVENTF_LEFTUP, + MouseButton::Middle => MOUSEEVENTF_MIDDLEUP, + MouseButton::Right => MOUSEEVENTF_RIGHTUP, + MouseButton::Back => MOUSEEVENTF_XUP, + MouseButton::Forward => MOUSEEVENTF_XUP, + _ => { + log::info!("Unsupported button {:?}", button); + return; + } + }, + match button { + MouseButton::Back => XBUTTON1 as _, + MouseButton::Forward => XBUTTON2 as _, + _ => 0, + }, + 0, + 0, + ); + } + + fn mouse_click(&mut self, button: MouseButton) { + self.mouse_down(button).ok(); + self.mouse_up(button); + } + + fn mouse_scroll_x(&mut self, length: i32) { + mouse_event(MOUSEEVENTF_HWHEEL, length as _, 0, 0); + } + + fn mouse_scroll_y(&mut self, length: i32) { + mouse_event(MOUSEEVENTF_WHEEL, length as _, 0, 0); + } +} + +impl KeyboardControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn key_sequence(&mut self, sequence: &str) { + let mut buffer = [0; 2]; + + for c in sequence.chars() { + // Windows uses uft-16 encoding. We need to check + // for variable length characters. As such some + // characters can be 32 bit long and those are + // encoded in such called hight and low surrogates + // each 16 bit wide that needs to be send after + // another to the SendInput function without + // being interrupted by "keyup" + let result = c.encode_utf16(&mut buffer); + if result.len() == 1 { + self.unicode_key_click(result[0]); + } else { + for utf16_surrogate in result { + self.unicode_key_down(utf16_surrogate.clone()); + } + // do i need to produce a keyup? + // self.unicode_key_up(0); + } + } + } + + fn key_click(&mut self, key: Key) { + let vk = self.key_to_keycode(key); + keybd_event(0, vk, 0); + keybd_event(KEYEVENTF_KEYUP, vk, 0); + } + + fn key_down(&mut self, key: Key) -> crate::ResultType { + match &key { + Key::Layout(c) => { + // to-do: dup code + // https://github.com/rustdesk/rustdesk/blob/1bc0dd791ed8344997024dc46626bd2ca7df73d2/src/server/input_service.rs#L1348 + let code = self.get_layoutdependent_keycode(*c); + if code as u16 != 0xFFFF { + let vk = code & 0x00FF; + let flag = code >> 8; + let modifiers = [Key::Shift, Key::Control, Key::Alt]; + let mod_len = modifiers.len(); + for pos in 0..mod_len { + if flag & (0x0001 << pos) != 0 { + self.key_down(modifiers[pos])?; + } + } + + let res = keybd_event(0, vk, 0); + let err = if res == 0 { get_error() } else { "".to_owned() }; + + for pos in 0..mod_len { + let rpos = mod_len - 1 - pos; + if flag & (0x0001 << rpos) != 0 { + self.key_up(modifiers[rpos]); + } + } + + if !err.is_empty() { + return Err(err.into()); + } + } else { + return Err(format!("Failed to get keycode of {}", c).into()); + } + } + _ => { + let code = self.key_to_keycode(key); + if code == 0 || code == 65535 { + return Err("".into()); + } + let res = keybd_event(0, code, 0); + if res == 0 { + let err = get_error(); + if !err.is_empty() { + return Err(err.into()); + } + } + } + } + Ok(()) + } + + fn key_up(&mut self, key: Key) { + match key { + Key::Layout(c) => { + let code = self.get_layoutdependent_keycode(c); + if code as u16 != 0xFFFF { + let vk = code & 0x00FF; + keybd_event(KEYEVENTF_KEYUP, vk, 0); + } + } + _ => { + keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0); + } + } + } + + fn get_key_state(&mut self, key: Key) -> bool { + let keycode = self.key_to_keycode(key); + let x = unsafe { GetKeyState(keycode as _) }; + if key == Key::CapsLock || key == Key::NumLock || key == Key::Scroll { + return (x & 0x1) == 0x1; + } + return (x as u16 & 0x8000) == 0x8000; + } +} + +impl Enigo { + /// Gets the (width, height) of the main display in screen coordinates + /// (pixels). + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut size = Enigo::main_display_size(); + /// ``` + pub fn main_display_size() -> (usize, usize) { + let w = unsafe { GetSystemMetrics(SM_CXSCREEN) as usize }; + let h = unsafe { GetSystemMetrics(SM_CYSCREEN) as usize }; + (w, h) + } + + /// Gets the location of mouse in screen coordinates (pixels). + /// + /// # Example + /// + /// ```no_run + /// use enigo::*; + /// let mut location = Enigo::mouse_location(); + /// ``` + pub fn mouse_location() -> (i32, i32) { + let mut point = POINT { x: 0, y: 0 }; + let result = unsafe { GetCursorPos(&mut point) }; + if result != 0 { + (point.x, point.y) + } else { + (0, 0) + } + } + + fn unicode_key_click(&self, unicode_char: u16) { + self.unicode_key_down(unicode_char); + self.unicode_key_up(unicode_char); + } + + fn unicode_key_down(&self, unicode_char: u16) { + keybd_event(KEYEVENTF_UNICODE, 0, unicode_char); + } + + fn unicode_key_up(&self, unicode_char: u16) { + keybd_event(KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, unicode_char); + } + + fn key_to_keycode(&self, key: Key) -> u16 { + // do not use the codes from crate winapi they're + // wrongly typed with i32 instead of i16 use the + // ones provided by win/keycodes.rs that are prefixed + // with an 'E' infront of the original name + #[allow(deprecated)] + // I mean duh, we still need to support deprecated keys until they're removed + match key { + Key::Alt => EVK_MENU, + Key::Backspace => EVK_BACK, + Key::CapsLock => EVK_CAPITAL, + Key::Control => EVK_LCONTROL, + Key::Delete => EVK_DELETE, + Key::DownArrow => EVK_DOWN, + Key::End => EVK_END, + Key::Escape => EVK_ESCAPE, + Key::F1 => EVK_F1, + Key::F10 => EVK_F10, + Key::F11 => EVK_F11, + Key::F12 => EVK_F12, + Key::F2 => EVK_F2, + Key::F3 => EVK_F3, + Key::F4 => EVK_F4, + Key::F5 => EVK_F5, + Key::F6 => EVK_F6, + Key::F7 => EVK_F7, + Key::F8 => EVK_F8, + Key::F9 => EVK_F9, + Key::Home => EVK_HOME, + Key::LeftArrow => EVK_LEFT, + Key::Option => EVK_MENU, + Key::PageDown => EVK_NEXT, + Key::PageUp => EVK_PRIOR, + Key::Return => EVK_RETURN, + Key::RightArrow => EVK_RIGHT, + Key::Shift => EVK_SHIFT, + Key::Space => EVK_SPACE, + Key::Tab => EVK_TAB, + Key::UpArrow => EVK_UP, + Key::Numpad0 => EVK_NUMPAD0, + Key::Numpad1 => EVK_NUMPAD1, + Key::Numpad2 => EVK_NUMPAD2, + Key::Numpad3 => EVK_NUMPAD3, + Key::Numpad4 => EVK_NUMPAD4, + Key::Numpad5 => EVK_NUMPAD5, + Key::Numpad6 => EVK_NUMPAD6, + Key::Numpad7 => EVK_NUMPAD7, + Key::Numpad8 => EVK_NUMPAD8, + Key::Numpad9 => EVK_NUMPAD9, + Key::Cancel => EVK_CANCEL, + Key::Clear => EVK_CLEAR, + Key::Pause => EVK_PAUSE, + Key::Kana => EVK_KANA, + Key::Hangul => EVK_HANGUL, + Key::Junja => EVK_JUNJA, + Key::Final => EVK_FINAL, + Key::Hanja => EVK_HANJA, + Key::Kanji => EVK_KANJI, + Key::Convert => EVK_CONVERT, + Key::Select => EVK_SELECT, + Key::Print => EVK_PRINT, + Key::Execute => EVK_EXECUTE, + Key::Snapshot => EVK_SNAPSHOT, + Key::Insert => EVK_INSERT, + Key::Help => EVK_HELP, + Key::Sleep => EVK_SLEEP, + Key::Separator => EVK_SEPARATOR, + Key::Mute => EVK_VOLUME_MUTE, + Key::VolumeDown => EVK_VOLUME_DOWN, + Key::VolumeUp => EVK_VOLUME_UP, + Key::Scroll => EVK_SCROLL, + Key::NumLock => EVK_NUMLOCK, + Key::RWin => EVK_RWIN, + Key::Apps => EVK_APPS, + Key::Add => EVK_ADD, + Key::Multiply => EVK_MULTIPLY, + Key::Decimal => EVK_DECIMAL, + Key::Subtract => EVK_SUBTRACT, + Key::Divide => EVK_DIVIDE, + Key::NumpadEnter => EVK_RETURN, + Key::Equals => '=' as _, + Key::RightShift => EVK_RSHIFT, + Key::RightControl => EVK_RCONTROL, + Key::RightAlt => EVK_RMENU, + + Key::Raw(raw_keycode) => raw_keycode, + Key::Super | Key::Command | Key::Windows | Key::Meta => EVK_LWIN, + Key::Layout(..) => { + // unreachable + 0 + } + } + } + + fn get_layoutdependent_keycode(&self, chr: char) -> u16 { + unsafe { + LAYOUT = std::ptr::null_mut(); + } + // NOTE VkKeyScanW uses the current keyboard LAYOUT + // to specify a LAYOUT use VkKeyScanExW and GetKeyboardLayout + // or load one with LoadKeyboardLayoutW + let current_window_thread_id = + unsafe { GetWindowThreadProcessId(GetForegroundWindow(), std::ptr::null_mut()) }; + unsafe { LAYOUT = GetKeyboardLayout(current_window_thread_id) }; + unsafe { VkKeyScanExW(chr as _, LAYOUT) as _ } + } +} diff --git a/vendor/rustdesk/libs/hbb_common/.gitignore b/vendor/rustdesk/libs/hbb_common/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/vendor/rustdesk/libs/hbb_common/Cargo.toml b/vendor/rustdesk/libs/hbb_common/Cargo.toml new file mode 100644 index 0000000..6ad3012 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/Cargo.toml @@ -0,0 +1,100 @@ +[package] +name = "hbb_common" +version = "0.1.0" +authors = ["open-trade "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = [] +webrtc = ["dep:webrtc"] + +[dependencies] +# new flexi_logger failed on rustc 1.75 +flexi_logger = { version = "0.27", features = ["async"] } +protobuf = { version = "3.7", features = ["with-bytes"] } +tokio = { version = "1.44", features = ["full"] } +tokio-util = { version = "0.7", features = ["full"] } +futures = "0.3" +bytes = { version = "1.10", features = ["serde"] } +log = "0.4" +env_logger = "0.11" +socket2 = { version = "0.3", features = ["reuseport"] } +zstd = "0.13" +anyhow = "1.0" +futures-util = "0.3" +directories-next = "2.0" +rand = "0.8" +serde_derive = "1.0" +serde = "1.0" +serde_json = "1.0" +lazy_static = "1.5" +confy = { git = "https://github.com/rustdesk-org/confy" } +dirs-next = "2.0" +filetime = "0.2" +sodiumoxide = "0.2" +regex = "1.11" +tokio-socks = { git = "https://github.com/rustdesk-org/tokio-socks" } +chrono = "0.4" +backtrace = "0.3" +libc = "0.2" +dlopen = "0.1" +toml = "0.7" +uuid = { version = "1.16", features = ["v4"] } +# new sysinfo issue: https://github.com/rustdesk/rustdesk/pull/6330#issuecomment-2270871442 +sysinfo = { git = "https://github.com/rustdesk-org/sysinfo", branch = "rlim_max" } +# new flexi_logger failed on nightly rustc 1.75 for x86 +thiserror = "1.0" +httparse = "1.10" +base64 = "0.22" +url = "2.5" +sha2 = "0.10" +whoami = "1.5" + +tokio-rustls = { version = "0.26", features = [ + "logging", + "tls12", + "ring", +], default-features = false } +tokio-native-tls = "0.3" +tokio-tungstenite = { version = "0.26", features = ["native-tls", "rustls-tls-native-roots", "rustls-tls-webpki-roots"] } +tungstenite = { version = "0.26", features = ["native-tls", "rustls-tls-native-roots", "rustls-tls-webpki-roots"] } +rustls-platform-verifier = "0.6" +rustls-pki-types = "1.11" +rustls-native-certs = "0.8" +webpki-roots = "1.0.4" +async-recursion = "1.1" +webrtc = { version = "0.14.0", optional = true } +libloading = "0.8" + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +mac_address = "1.1" +default_net = { git = "https://github.com/rustdesk-org/default_net" } +machine-uid = { git = "https://github.com/rustdesk-org/machine-uid" } + +[build-dependencies] +protobuf-codegen = { version = "3.7" } + +[dev-dependencies] +clap = "4.5.51" +webrtc = "0.14.0" + +[target.'cfg(target_os = "windows")'.dependencies] +winapi = { version = "0.3", features = [ + "winuser", + "synchapi", + "pdh", + "memoryapi", + "sysinfoapi", +] } + +[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" } +x11 = "2.21" diff --git a/vendor/rustdesk/libs/hbb_common/build.rs b/vendor/rustdesk/libs/hbb_common/build.rs new file mode 100644 index 0000000..5ebc3a2 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/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/rendezvous.proto", "protos/message.proto"]) + .include("protos") + .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) + .run() + .expect("Codegen failed."); +} diff --git a/vendor/rustdesk/libs/hbb_common/protos/message.proto b/vendor/rustdesk/libs/hbb_common/protos/message.proto new file mode 100644 index 0000000..bf37a0c --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/protos/message.proto @@ -0,0 +1,984 @@ +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 IdPk { + string id = 1; + bytes pk = 2; +} + +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; +} + +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; +} + +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. +} + +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 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; + } +} diff --git a/vendor/rustdesk/libs/hbb_common/protos/rendezvous.proto b/vendor/rustdesk/libs/hbb_common/protos/rendezvous.proto new file mode 100644 index 0000000..c2dad03 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/protos/rendezvous.proto @@ -0,0 +1,259 @@ +syntax = "proto3"; +package hbb; + +message RegisterPeer { + string id = 1; + int32 serial = 2; +} + +enum ConnType { + DEFAULT_CONN = 0; + FILE_TRANSFER = 1; + PORT_FORWARD = 2; + RDP = 3; + VIEW_CAMERA = 4; + TERMINAL = 5; +} + +message RegisterPeerResponse { bool request_pk = 2; } + +message PunchHoleRequest { + string id = 1; + NatType nat_type = 2; + string licence_key = 3; + ConnType conn_type = 4; + string token = 5; + string version = 6; + int32 udp_port = 7; + bool force_relay = 8; + int32 upnp_port = 9; + bytes socket_addr_v6 = 10; +} + +message ControlPermissions { + enum Permission { + keyboard = 0; + remote_printer = 1; + clipboard = 2; + file = 3; + audio = 4; + camera = 5; + terminal = 6; + tunnel = 7; + restart = 8; + recording = 9; + block_input = 10; + remote_modify = 11; + privacy_mode = 12; + } + uint64 permissions = 1; +} + +message PunchHole { + bytes socket_addr = 1; + string relay_server = 2; + NatType nat_type = 3; + int32 udp_port = 4; + bool force_relay = 5; + int32 upnp_port = 6; + bytes socket_addr_v6 = 7; + ControlPermissions control_permissions = 8; +} + +message TestNatRequest { + int32 serial = 1; +} + +// per my test, uint/int has no difference in encoding, int not good for negative, use sint for negative +message TestNatResponse { + int32 port = 1; + ConfigUpdate cu = 2; // for mobile +} + +enum NatType { + UNKNOWN_NAT = 0; + ASYMMETRIC = 1; + SYMMETRIC = 2; +} + +message PunchHoleSent { + bytes socket_addr = 1; + string id = 2; + string relay_server = 3; + NatType nat_type = 4; + string version = 5; + int32 upnp_port = 6; + bytes socket_addr_v6 = 7; +} + +message RegisterPk { + string id = 1; + bytes uuid = 2; + bytes pk = 3; + string old_id = 4; + bool no_register_device = 5; +} + +message RegisterPkResponse { + enum Result { + OK = 0; + UUID_MISMATCH = 2; + ID_EXISTS = 3; + TOO_FREQUENT = 4; + INVALID_ID_FORMAT = 5; + NOT_SUPPORT = 6; + SERVER_ERROR = 7; + } + Result result = 1; + int32 keep_alive = 2; +} + +message PunchHoleResponse { + bytes socket_addr = 1; + bytes pk = 2; + enum Failure { + ID_NOT_EXIST = 0; + OFFLINE = 2; + LICENSE_MISMATCH = 3; + LICENSE_OVERUSE = 4; + } + Failure failure = 3; + string relay_server = 4; + oneof union { + NatType nat_type = 5; + bool is_local = 6; + } + string other_failure = 7; + int32 feedback = 8; + bool is_udp = 9; + int32 upnp_port = 10; + bytes socket_addr_v6 = 11; +} + +message ConfigUpdate { + int32 serial = 1; + repeated string rendezvous_servers = 2; +} + +message RequestRelay { + string id = 1; + string uuid = 2; + bytes socket_addr = 3; + string relay_server = 4; + bool secure = 5; + string licence_key = 6; + ConnType conn_type = 7; + string token = 8; + ControlPermissions control_permissions = 9; +} + +message RelayResponse { + bytes socket_addr = 1; + string uuid = 2; + string relay_server = 3; + oneof union { + string id = 4; + bytes pk = 5; + } + string refuse_reason = 6; + string version = 7; + int32 feedback = 9; + bytes socket_addr_v6 = 10; + int32 upnp_port = 11; +} + +message SoftwareUpdate { string url = 1; } + +// if in same intranet, punch hole won't work both for udp and tcp, +// even some router has below connection error if we connect itself, +// { kind: Other, error: "could not resolve to any address" }, +// so we request local address to connect. +message FetchLocalAddr { + bytes socket_addr = 1; + string relay_server = 2; + bytes socket_addr_v6 = 3; + ControlPermissions control_permissions = 4; +} + +message LocalAddr { + bytes socket_addr = 1; + bytes local_addr = 2; + string relay_server = 3; + string id = 4; + string version = 5; + bytes socket_addr_v6 = 6; +} + +message PeerDiscovery { + string cmd = 1; + string mac = 2; + string id = 3; + string username = 4; + string hostname = 5; + string platform = 6; + string misc = 7; +} + +message OnlineRequest { + string id = 1; + repeated string peers = 2; +} + +message OnlineResponse { + bytes states = 1; +} + +message KeyExchange { + repeated bytes keys = 1; +} + +message HealthCheck { + string token = 1; +} + +message HeaderEntry { + string name = 1; + string value = 2; +} + +message HttpProxyRequest { + string method = 1; + string path = 2; + repeated HeaderEntry headers = 3; + bytes body = 4; +} + +message HttpProxyResponse { + int32 status = 1; + repeated HeaderEntry headers = 2; + bytes body = 3; + string error = 4; +} + +message RendezvousMessage { + oneof union { + RegisterPeer register_peer = 6; + RegisterPeerResponse register_peer_response = 7; + PunchHoleRequest punch_hole_request = 8; + PunchHole punch_hole = 9; + PunchHoleSent punch_hole_sent = 10; + PunchHoleResponse punch_hole_response = 11; + FetchLocalAddr fetch_local_addr = 12; + LocalAddr local_addr = 13; + ConfigUpdate configure_update = 14; + RegisterPk register_pk = 15; + RegisterPkResponse register_pk_response = 16; + SoftwareUpdate software_update = 17; + RequestRelay request_relay = 18; + RelayResponse relay_response = 19; + TestNatRequest test_nat_request = 20; + TestNatResponse test_nat_response = 21; + PeerDiscovery peer_discovery = 22; + OnlineRequest online_request = 23; + OnlineResponse online_response = 24; + KeyExchange key_exchange = 25; + HealthCheck hc = 26; + HttpProxyRequest http_proxy_request = 27; + HttpProxyResponse http_proxy_response = 28; + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/bytes_codec.rs b/vendor/rustdesk/libs/hbb_common/src/bytes_codec.rs new file mode 100644 index 0000000..bfc7987 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/bytes_codec.rs @@ -0,0 +1,280 @@ +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use std::io; +use tokio_util::codec::{Decoder, Encoder}; + +#[derive(Debug, Clone, Copy)] +pub struct BytesCodec { + state: DecodeState, + raw: bool, + max_packet_length: usize, +} + +#[derive(Debug, Clone, Copy)] +enum DecodeState { + Head, + Data(usize), +} + +impl Default for BytesCodec { + fn default() -> Self { + Self::new() + } +} + +impl BytesCodec { + pub fn new() -> Self { + Self { + state: DecodeState::Head, + raw: false, + max_packet_length: usize::MAX, + } + } + + pub fn set_raw(&mut self) { + self.raw = true; + } + + pub fn set_max_packet_length(&mut self, n: usize) { + self.max_packet_length = n; + } + + fn decode_head(&mut self, src: &mut BytesMut) -> io::Result> { + if src.is_empty() { + return Ok(None); + } + let head_len = ((src[0] & 0x3) + 1) as usize; + if src.len() < head_len { + return Ok(None); + } + let mut n = src[0] as usize; + if head_len > 1 { + n |= (src[1] as usize) << 8; + } + if head_len > 2 { + n |= (src[2] as usize) << 16; + } + if head_len > 3 { + n |= (src[3] as usize) << 24; + } + n >>= 2; + if n > self.max_packet_length { + return Err(io::Error::new(io::ErrorKind::InvalidData, "Too big packet")); + } + src.advance(head_len); + src.reserve(n); + Ok(Some(n)) + } + + fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result> { + if src.len() < n { + return Ok(None); + } + Ok(Some(src.split_to(n))) + } +} + +impl Decoder for BytesCodec { + type Item = BytesMut; + type Error = io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, io::Error> { + if self.raw { + if !src.is_empty() { + let len = src.len(); + return Ok(Some(src.split_to(len))); + } else { + return Ok(None); + } + } + let n = match self.state { + DecodeState::Head => match self.decode_head(src)? { + Some(n) => { + self.state = DecodeState::Data(n); + n + } + None => return Ok(None), + }, + DecodeState::Data(n) => n, + }; + + match self.decode_data(n, src)? { + Some(data) => { + self.state = DecodeState::Head; + Ok(Some(data)) + } + None => Ok(None), + } + } +} + +impl Encoder for BytesCodec { + type Error = io::Error; + + fn encode(&mut self, data: Bytes, buf: &mut BytesMut) -> Result<(), io::Error> { + if self.raw { + buf.reserve(data.len()); + buf.put(data); + return Ok(()); + } + if data.len() <= 0x3F { + buf.put_u8((data.len() << 2) as u8); + } else if data.len() <= 0x3FFF { + buf.put_u16_le((data.len() << 2) as u16 | 0x1); + } else if data.len() <= 0x3FFFFF { + let h = (data.len() << 2) as u32 | 0x2; + buf.put_u16_le((h & 0xFFFF) as u16); + buf.put_u8((h >> 16) as u8); + } else if data.len() <= 0x3FFFFFFF { + buf.put_u32_le((data.len() << 2) as u32 | 0x3); + } else { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow")); + } + buf.extend(data); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_codec1() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + bytes.resize(0x3F, 1); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); + let buf_saved = buf.clone(); + assert_eq!(buf.len(), 0x3F + 1); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3F); + assert_eq!(res[0], 1); + } else { + panic!(); + } + let mut codec2 = BytesCodec::new(); + let mut buf2 = BytesMut::new(); + if let Ok(None) = codec2.decode(&mut buf2) { + } else { + panic!(); + } + buf2.extend(&buf_saved[0..1]); + if let Ok(None) = codec2.decode(&mut buf2) { + } else { + panic!(); + } + buf2.extend(&buf_saved[1..]); + if let Ok(Some(res)) = codec2.decode(&mut buf2) { + assert_eq!(res.len(), 0x3F); + assert_eq!(res[0], 1); + } else { + panic!(); + } + } + + #[test] + fn test_codec2() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + assert!(codec.encode("".into(), &mut buf).is_ok()); + assert_eq!(buf.len(), 1); + bytes.resize(0x3F + 1, 2); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); + assert_eq!(buf.len(), 0x3F + 2 + 2); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0); + } else { + panic!(); + } + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3F + 1); + assert_eq!(res[0], 2); + } else { + panic!(); + } + } + + #[test] + fn test_codec3() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + bytes.resize(0x3F - 1, 3); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); + assert_eq!(buf.len(), 0x3F + 1 - 1); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3F - 1); + assert_eq!(res[0], 3); + } else { + panic!(); + } + } + #[test] + fn test_codec4() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + bytes.resize(0x3FFF, 4); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); + assert_eq!(buf.len(), 0x3FFF + 2); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3FFF); + assert_eq!(res[0], 4); + } else { + panic!(); + } + } + + #[test] + fn test_codec5() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + bytes.resize(0x3FFFFF, 5); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); + assert_eq!(buf.len(), 0x3FFFFF + 3); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3FFFFF); + assert_eq!(res[0], 5); + } else { + panic!(); + } + } + + #[test] + fn test_codec6() { + let mut codec = BytesCodec::new(); + let mut buf = BytesMut::new(); + let mut bytes: Vec = Vec::new(); + bytes.resize(0x3FFFFF + 1, 6); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); + let buf_saved = buf.clone(); + assert_eq!(buf.len(), 0x3FFFFF + 4 + 1); + if let Ok(Some(res)) = codec.decode(&mut buf) { + assert_eq!(res.len(), 0x3FFFFF + 1); + assert_eq!(res[0], 6); + } else { + panic!(); + } + let mut codec2 = BytesCodec::new(); + let mut buf2 = BytesMut::new(); + buf2.extend(&buf_saved[0..1]); + if let Ok(None) = codec2.decode(&mut buf2) { + } else { + panic!(); + } + buf2.extend(&buf_saved[1..6]); + if let Ok(None) = codec2.decode(&mut buf2) { + } else { + panic!(); + } + buf2.extend(&buf_saved[6..]); + if let Ok(Some(res)) = codec2.decode(&mut buf2) { + assert_eq!(res.len(), 0x3FFFFF + 1); + assert_eq!(res[0], 6); + } else { + panic!(); + } + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/compress.rs b/vendor/rustdesk/libs/hbb_common/src/compress.rs new file mode 100644 index 0000000..761d916 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/compress.rs @@ -0,0 +1,34 @@ +use std::{cell::RefCell, io}; +use zstd::bulk::Compressor; + +// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(), +// which is currently 22. Levels >= 20 +// Default level is ZSTD_CLEVEL_DEFAULT==3. +// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT +thread_local! { + static COMPRESSOR: RefCell>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL)); +} + +pub fn compress(data: &[u8]) -> Vec { + let mut out = Vec::new(); + COMPRESSOR.with(|c| { + if let Ok(mut c) = c.try_borrow_mut() { + match &mut *c { + Ok(c) => match c.compress(data) { + Ok(res) => out = res, + Err(err) => { + crate::log::debug!("Failed to compress: {}", err); + } + }, + Err(err) => { + crate::log::debug!("Failed to get compressor: {}", err); + } + } + } + }); + out +} + +pub fn decompress(data: &[u8]) -> Vec { + zstd::decode_all(data).unwrap_or_default() +} diff --git a/vendor/rustdesk/libs/hbb_common/src/config.rs b/vendor/rustdesk/libs/hbb_common/src/config.rs new file mode 100644 index 0000000..d644512 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/config.rs @@ -0,0 +1,3491 @@ +use std::{ + collections::{HashMap, HashSet}, + fs, + io::{Read, Write}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + sync::{Mutex, RwLock}, + time::{Duration, Instant, SystemTime}, +}; + +use anyhow::Result; +use bytes::Bytes; +use rand::Rng; +use regex::Regex; +use serde as de; +use serde_derive::{Deserialize, Serialize}; +use serde_json; +use sha2::{Digest, Sha256}; +use sodiumoxide::base64; +use sodiumoxide::crypto::sign; + +use crate::{ + compress::{compress, decompress}, + log, + password_security::{ + decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original, + encrypt_vec_or_original, symmetric_crypt, + }, +}; + +pub const RENDEZVOUS_TIMEOUT: u64 = 12_000; +pub const CONNECT_TIMEOUT: u64 = 18_000; +pub const READ_TIMEOUT: u64 = 18_000; +// https://github.com/quic-go/quic-go/issues/525#issuecomment-294531351 +// https://datatracker.ietf.org/doc/html/draft-hamilton-early-deployment-quic-00#section-6.10 +// 15 seconds is recommended by quic, though oneSIP recommend 25 seconds, +// https://www.onsip.com/voip-resources/voip-fundamentals/what-is-nat-keepalive +// hello-agent local patch: lowered from 15_000 to 1_000 so device-online +// status in the admin UI reacts faster. Re-apply on vendor resync. +pub const REG_INTERVAL: i64 = 1_000; +pub const COMPRESS_LEVEL: i32 = 3; +const SERIAL: i32 = 3; +const PASSWORD_ENC_VERSION: &str = "00"; +pub const ENCRYPT_MAX_LEN: usize = 128; // used for password, pin, etc, not for all + +const PERMANENT_PASSWORD_HASH_PREFIX: &str = "01"; +const PERMANENT_PASSWORD_H1_LEN: usize = 32; +const DEFAULT_SALT_LEN: usize = 6; + +fn is_permanent_password_hashed_storage(v: &str) -> bool { + decode_permanent_password_h1_from_storage(v).is_some() +} + +pub fn compute_permanent_password_h1( + password: &str, + salt: &str, +) -> [u8; PERMANENT_PASSWORD_H1_LEN] { + let mut hasher = Sha256::new(); + hasher.update(password.as_bytes()); + hasher.update(salt.as_bytes()); + let out = hasher.finalize(); + let mut h1 = [0u8; PERMANENT_PASSWORD_H1_LEN]; + h1.copy_from_slice(&out[..PERMANENT_PASSWORD_H1_LEN]); + h1 +} + +fn constant_time_eq_32(a: &[u8; 32], b: &[u8; 32]) -> bool { + sodiumoxide::utils::memcmp(a, b) +} + +fn encode_permanent_password_storage_from_h1(h1: &[u8; PERMANENT_PASSWORD_H1_LEN]) -> String { + PERMANENT_PASSWORD_HASH_PREFIX.to_owned() + &base64::encode(h1, base64::Variant::Original) +} + +pub fn decode_permanent_password_h1_from_storage( + storage: &str, +) -> Option<[u8; PERMANENT_PASSWORD_H1_LEN]> { + let encoded = storage.strip_prefix(PERMANENT_PASSWORD_HASH_PREFIX)?; + + let v = base64::decode(encoded.as_bytes(), base64::Variant::Original).ok()?; + if v.len() != PERMANENT_PASSWORD_H1_LEN { + return None; + } + let mut h1 = [0u8; PERMANENT_PASSWORD_H1_LEN]; + h1.copy_from_slice(&v[..PERMANENT_PASSWORD_H1_LEN]); + Some(h1) +} + +// If password is empty or not hashed storage, it's safe to update salt. +fn password_is_empty_or_not_hashed(permanent_password_storage: &str) -> bool { + permanent_password_storage.is_empty() + || !is_permanent_password_hashed_storage(permanent_password_storage) +} + +#[cfg(target_os = "macos")] +lazy_static::lazy_static! { + pub static ref ORG: RwLock = RwLock::new("com.carriez".to_owned()); +} + +type Size = (i32, i32, i32, i32); +type KeyPair = (Vec, Vec); + +lazy_static::lazy_static! { + static ref CONFIG: RwLock = RwLock::new(Config::load()); + static ref CONFIG2: RwLock = RwLock::new(Config2::load()); + static ref LOCAL_CONFIG: RwLock = RwLock::new(LocalConfig::load()); + static ref STATUS: RwLock = RwLock::new(Status::load()); + static ref TRUSTED_DEVICES: RwLock<(Vec, bool)> = Default::default(); + static ref ONLINE: Mutex> = Default::default(); + pub static ref PROD_RENDEZVOUS_SERVER: RwLock = RwLock::new("".to_owned()); + pub static ref EXE_RENDEZVOUS_SERVER: RwLock = Default::default(); + pub static ref APP_NAME: RwLock = RwLock::new("RustDesk".to_owned()); + /// Optional override for the product / agent name reported in the + /// /api/sysinfo upload as the `agent_name` field. Empty by default, + /// in which case the field is omitted and the server treats the + /// install as vanilla RustDesk. hello-agent (and any other rebrand + /// that wants to be distinguishable in the admin UI) sets this at + /// startup, alongside `APP_NAME`. + pub static ref AGENT_NAME: RwLock = RwLock::new(String::new()); + /// Optional override for the agent's *own* version (vs the embedded + /// rustdesk core version reported in the `version` field). Same + /// "empty = omit" convention as `AGENT_NAME`. For hello-agent this + /// is `env!("CARGO_PKG_VERSION")`. + pub static ref AGENT_VERSION: RwLock = RwLock::new(String::new()); + static ref KEY_PAIR: Mutex> = Default::default(); + static ref USER_DEFAULT_CONFIG: RwLock<(UserDefaultConfig, Instant)> = RwLock::new((UserDefaultConfig::load(), Instant::now())); + pub static ref NEW_STORED_PEER_CONFIG: Mutex> = Default::default(); + pub static ref DEFAULT_SETTINGS: RwLock> = Default::default(); + pub static ref OVERWRITE_SETTINGS: RwLock> = Default::default(); + pub static ref DEFAULT_DISPLAY_SETTINGS: RwLock> = Default::default(); + pub static ref OVERWRITE_DISPLAY_SETTINGS: RwLock> = Default::default(); + pub static ref DEFAULT_LOCAL_SETTINGS: RwLock> = Default::default(); + pub static ref OVERWRITE_LOCAL_SETTINGS: RwLock> = Default::default(); + pub static ref HARD_SETTINGS: RwLock> = Default::default(); + pub static ref BUILTIN_SETTINGS: RwLock> = Default::default(); +} + +#[cfg(target_os = "android")] +lazy_static::lazy_static! { + pub static ref ANDROID_RUSTLS_PLATFORM_VERIFIER_INITIALIZED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); +} + +lazy_static::lazy_static! { + pub static ref APP_DIR: RwLock = Default::default(); +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +lazy_static::lazy_static! { + pub static ref APP_HOME_DIR: RwLock = Default::default(); +} + +pub const LINK_DOCS_HOME: &str = "https://cstudio.ch/hello-agent/docs/en/"; +pub const LINK_DOCS_X11_REQUIRED: &str = "https://cstudio.ch/hello-agent/docs/en/manual/linux/#x11-required"; +pub const LINK_HEADLESS_LINUX_SUPPORT: &str = + "https://github.com/rustdesk/rustdesk/wiki/Headless-Linux-Support"; + +lazy_static::lazy_static! { + pub static ref HELPER_URL: HashMap<&'static str, &'static str> = HashMap::from([ + ("rustdesk docs home", LINK_DOCS_HOME), + ("rustdesk docs x11-required", LINK_DOCS_X11_REQUIRED), + ("rustdesk x11 headless", LINK_HEADLESS_LINUX_SUPPORT), + ]); +} + +const NUM_CHARS: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + +const CHARS: &[char] = &[ + '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', +]; + +pub const RENDEZVOUS_SERVERS: &[&str] = &["rs-ny.rustdesk.com"]; +pub const RS_PUB_KEY: &str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; + +pub const RENDEZVOUS_PORT: i32 = 21116; +pub const RELAY_PORT: i32 = 21117; +pub const WS_RENDEZVOUS_PORT: i32 = 21118; +pub const WS_RELAY_PORT: i32 = 21119; + +macro_rules! serde_field_string { + ($default_func:ident, $de_func:ident, $default_expr:expr) => { + fn $default_func() -> String { + $default_expr + } + + fn $de_func<'de, D>(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let s: String = + de::Deserialize::deserialize(deserializer).unwrap_or(Self::$default_func()); + if s.is_empty() { + return Ok(Self::$default_func()); + } + Ok(s) + } + }; +} + +macro_rules! serde_field_bool { + ($struct_name: ident, $field_name: literal, $func: ident, $default: literal) => { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct $struct_name { + #[serde(default = $default, rename = $field_name, deserialize_with = "deserialize_bool")] + pub v: bool, + } + impl Default for $struct_name { + fn default() -> Self { + Self { v: Self::$func() } + } + } + impl $struct_name { + pub fn $func() -> bool { + UserDefaultConfig::read($field_name) == "Y" + } + } + impl Deref for $struct_name { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.v + } + } + impl DerefMut for $struct_name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.v + } + } + }; +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum NetworkType { + Direct, + ProxySocks, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +pub struct Config { + #[serde( + default, + skip_serializing_if = "String::is_empty", + deserialize_with = "deserialize_string" + )] + pub id: String, // use + #[serde(default, deserialize_with = "deserialize_string")] + enc_id: String, // store + #[serde(default, deserialize_with = "deserialize_string")] + password: String, + #[serde(default, deserialize_with = "deserialize_string")] + salt: String, + #[serde(default, deserialize_with = "deserialize_keypair")] + key_pair: KeyPair, // sk, pk + #[serde(default, deserialize_with = "deserialize_bool")] + key_confirmed: bool, + #[serde(default, deserialize_with = "deserialize_hashmap_string_bool")] + keys_confirmed: HashMap, +} + +#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)] +pub struct Socks5Server { + #[serde(default, deserialize_with = "deserialize_string")] + pub proxy: String, + #[serde(default, deserialize_with = "deserialize_string")] + pub username: String, + #[serde(default, deserialize_with = "deserialize_string")] + pub password: String, +} + +// more variable configs +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +pub struct Config2 { + #[serde(default, deserialize_with = "deserialize_string")] + rendezvous_server: String, + #[serde(default, deserialize_with = "deserialize_i32")] + nat_type: i32, + #[serde(default, deserialize_with = "deserialize_i32")] + serial: i32, + #[serde(default, deserialize_with = "deserialize_string")] + unlock_pin: String, + #[serde(default, deserialize_with = "deserialize_string")] + trusted_devices: String, + + #[serde(default)] + socks: Option, + + // the other scalar value must before this + #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] + pub options: HashMap, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +pub struct Resolution { + pub w: i32, + pub h: i32, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct PeerConfig { + #[serde(default, deserialize_with = "deserialize_vec_u8")] + pub password: Vec, + #[serde(default, deserialize_with = "deserialize_size")] + pub size: Size, + #[serde(default, deserialize_with = "deserialize_size")] + pub size_ft: Size, + #[serde(default, deserialize_with = "deserialize_size")] + pub size_pf: Size, + #[serde( + default = "PeerConfig::default_view_style", + deserialize_with = "PeerConfig::deserialize_view_style", + skip_serializing_if = "String::is_empty" + )] + pub view_style: String, + // Image scroll style, scrolledge, scrollbar or scroll auto + #[serde( + default = "PeerConfig::default_scroll_style", + deserialize_with = "PeerConfig::deserialize_scroll_style", + skip_serializing_if = "String::is_empty" + )] + pub scroll_style: String, + #[serde( + default = "PeerConfig::default_edge_scroll_edge_thickness", + deserialize_with = "PeerConfig::deserialize_edge_scroll_edge_thickness" + )] + pub edge_scroll_edge_thickness: i32, + #[serde( + default = "PeerConfig::default_image_quality", + deserialize_with = "PeerConfig::deserialize_image_quality", + skip_serializing_if = "String::is_empty" + )] + pub image_quality: String, + #[serde( + default = "PeerConfig::default_custom_image_quality", + deserialize_with = "PeerConfig::deserialize_custom_image_quality", + skip_serializing_if = "Vec::is_empty" + )] + pub custom_image_quality: Vec, + #[serde(flatten)] + pub show_remote_cursor: ShowRemoteCursor, + #[serde(flatten)] + pub lock_after_session_end: LockAfterSessionEnd, + #[serde(flatten)] + pub terminal_persistent: TerminalPersistent, + #[serde(flatten)] + pub privacy_mode: PrivacyMode, + #[serde(flatten)] + pub allow_swap_key: AllowSwapKey, + #[serde(default, deserialize_with = "deserialize_vec_i32_string_i32")] + pub port_forwards: Vec<(i32, String, i32)>, + #[serde(default, deserialize_with = "deserialize_i32")] + pub direct_failures: i32, + #[serde(flatten)] + pub disable_audio: DisableAudio, + #[serde(flatten)] + pub disable_clipboard: DisableClipboard, + #[serde(flatten)] + pub enable_file_copy_paste: EnableFileCopyPaste, + #[serde(flatten)] + pub show_quality_monitor: ShowQualityMonitor, + #[serde(flatten)] + pub follow_remote_cursor: FollowRemoteCursor, + #[serde(flatten)] + pub follow_remote_window: FollowRemoteWindow, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub keyboard_mode: String, + #[serde(flatten)] + pub view_only: ViewOnly, + #[serde(flatten)] + pub show_my_cursor: ShowMyCursor, + #[serde(flatten)] + pub sync_init_clipboard: SyncInitClipboard, + // Mouse wheel or touchpad scroll mode + #[serde( + default = "PeerConfig::default_reverse_mouse_wheel", + deserialize_with = "PeerConfig::deserialize_reverse_mouse_wheel", + skip_serializing_if = "String::is_empty" + )] + pub reverse_mouse_wheel: String, + #[serde( + default = "PeerConfig::default_displays_as_individual_windows", + deserialize_with = "PeerConfig::deserialize_displays_as_individual_windows", + skip_serializing_if = "String::is_empty" + )] + pub displays_as_individual_windows: String, + #[serde( + default = "PeerConfig::default_use_all_my_displays_for_the_remote_session", + deserialize_with = "PeerConfig::deserialize_use_all_my_displays_for_the_remote_session", + skip_serializing_if = "String::is_empty" + )] + pub use_all_my_displays_for_the_remote_session: String, + #[serde( + rename = "trackpad-speed", + default = "PeerConfig::default_trackpad_speed", + deserialize_with = "PeerConfig::deserialize_trackpad_speed" + )] + pub trackpad_speed: i32, + + #[serde( + default, + deserialize_with = "deserialize_hashmap_resolutions", + skip_serializing_if = "HashMap::is_empty" + )] + pub custom_resolutions: HashMap, + + // The other scalar value must before this + #[serde( + default, + deserialize_with = "deserialize_hashmap_string_string", + skip_serializing_if = "HashMap::is_empty" + )] + pub options: HashMap, // not use delete to represent default values + // Various data for flutter ui + #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] + pub ui_flutter: HashMap, + #[serde(default)] + pub info: PeerInfoSerde, + #[serde(default)] + pub transfer: TransferSerde, +} + +impl Default for PeerConfig { + fn default() -> Self { + Self { + password: Default::default(), + size: Default::default(), + size_ft: Default::default(), + size_pf: Default::default(), + view_style: Self::default_view_style(), + scroll_style: Self::default_scroll_style(), + edge_scroll_edge_thickness: Self::default_edge_scroll_edge_thickness(), + image_quality: Self::default_image_quality(), + custom_image_quality: Self::default_custom_image_quality(), + show_remote_cursor: Default::default(), + lock_after_session_end: Default::default(), + terminal_persistent: Default::default(), + privacy_mode: Default::default(), + allow_swap_key: Default::default(), + port_forwards: Default::default(), + direct_failures: Default::default(), + disable_audio: Default::default(), + disable_clipboard: Default::default(), + enable_file_copy_paste: Default::default(), + show_quality_monitor: Default::default(), + follow_remote_cursor: Default::default(), + follow_remote_window: Default::default(), + keyboard_mode: Default::default(), + view_only: Default::default(), + show_my_cursor: Default::default(), + reverse_mouse_wheel: Self::default_reverse_mouse_wheel(), + displays_as_individual_windows: Self::default_displays_as_individual_windows(), + use_all_my_displays_for_the_remote_session: + Self::default_use_all_my_displays_for_the_remote_session(), + trackpad_speed: Self::default_trackpad_speed(), + custom_resolutions: Default::default(), + options: Self::default_options(), + ui_flutter: Default::default(), + info: Default::default(), + transfer: Default::default(), + sync_init_clipboard: Default::default(), + } + } +} + +#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] +pub struct PeerInfoSerde { + #[serde(default, deserialize_with = "deserialize_string")] + pub username: String, + #[serde(default, deserialize_with = "deserialize_string")] + pub hostname: String, + #[serde(default, deserialize_with = "deserialize_string")] + pub platform: String, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +pub struct TransferSerde { + #[serde(default, deserialize_with = "deserialize_vec_string")] + pub write_jobs: Vec, + #[serde(default, deserialize_with = "deserialize_vec_string")] + pub read_jobs: Vec, +} + +#[inline] +pub fn get_online_state() -> i64 { + *ONLINE.lock().unwrap().values().max().unwrap_or(&0) +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn patch(path: PathBuf) -> PathBuf { + if let Some(_tmp) = path.to_str() { + #[cfg(windows)] + return _tmp + .replace( + "system32\\config\\systemprofile", + "ServiceProfiles\\LocalService", + ) + .into(); + #[cfg(target_os = "macos")] + return _tmp.replace("Application Support", "Preferences").into(); + #[cfg(target_os = "linux")] + { + if _tmp == "/root" { + if let Ok(user) = crate::platform::linux::run_cmds_trim_newline("whoami") { + if user != "root" { + let cmd = format!("getent passwd '{}' | awk -F':' '{{print $6}}'", user); + if let Ok(output) = crate::platform::linux::run_cmds_trim_newline(&cmd) { + return output.into(); + } + return format!("/home/{user}").into(); + } + } + } + } + } + path +} + +impl Config2 { + fn load() -> Config2 { + let mut config = Config::load_::("2"); + let mut store = false; + if let Some(mut socks) = config.socks { + let (password, _, store2) = + decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION); + socks.password = password; + config.socks = Some(socks); + store |= store2; + } + let (unlock_pin, _, store2) = + decrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION); + config.unlock_pin = unlock_pin; + store |= store2; + if store { + config.store(); + } + config + } + + pub fn file() -> PathBuf { + Config::file_("2") + } + + fn store(&self) { + let mut config = self.clone(); + if let Some(mut socks) = config.socks { + socks.password = + encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); + config.socks = Some(socks); + } + config.unlock_pin = + encrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); + Config::store_(&config, "2"); + } + + pub fn get() -> Config2 { + return CONFIG2.read().unwrap().clone(); + } + + pub fn set(cfg: Config2) -> bool { + let mut lock = CONFIG2.write().unwrap(); + if *lock == cfg { + return false; + } + *lock = cfg; + lock.store(); + true + } +} + +pub fn load_path( + file: PathBuf, +) -> T { + let cfg = match confy::load_path(&file) { + Ok(config) => config, + Err(err) => { + if let confy::ConfyError::GeneralLoadError(err) = &err { + if err.kind() == std::io::ErrorKind::NotFound { + return T::default(); + } + } + log::error!("Failed to load config '{}': {}", file.display(), err); + T::default() + } + }; + cfg +} + +#[inline] +pub fn store_path(path: PathBuf, cfg: T) -> crate::ResultType<()> { + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + Ok(confy::store_path_perms( + path, + cfg, + fs::Permissions::from_mode(0o600), + )?) + } + #[cfg(windows)] + { + Ok(confy::store_path(path, cfg)?) + } +} + +impl Config { + fn load_( + suffix: &str, + ) -> T { + let file = Self::file_(suffix); + let cfg = load_path(file); + if suffix.is_empty() { + log::trace!("{:?}", cfg); + } + cfg + } + + fn store_(config: &T, suffix: &str) { + let file = Self::file_(suffix); + if let Err(err) = store_path(file, config) { + log::error!("Failed to store {suffix} config: {err}"); + } + } + + fn load() -> Config { + let mut config = Config::load_::(""); + let mut store = false; + store |= Self::migrate_permanent_password_to_hashed_storage(&mut config); + let mut id_valid = false; + let (id, encrypted, store2) = decrypt_str_or_original(&config.enc_id, PASSWORD_ENC_VERSION); + if encrypted { + config.id = id; + id_valid = true; + store |= store2; + } else if + // Comment out for forward compatible + // crate::get_modified_time(&Self::file_("")) + // .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation + // .unwrap_or_else(crate::get_exe_time) + // < crate::get_exe_time() + // && + !config.id.is_empty() + && config.enc_id.is_empty() + && !decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION).1 + { + id_valid = true; + store = true; + } + if !id_valid { + log::warn!("ID is invalid, generating new one"); + for _ in 0..3 { + if let Some(id) = Config::gen_id() { + config.id = id; + store = true; + break; + } else { + log::error!("Failed to generate new id"); + } + } + } + if store { + config.store(); + } + config + } + + fn migrate_permanent_password_to_hashed_storage(config: &mut Config) -> bool { + if config.password.is_empty() || is_permanent_password_hashed_storage(&config.password) { + return false; + } + + if config.password.starts_with(PASSWORD_ENC_VERSION) { + let (plain, decrypted, looks_like_plaintext) = + decrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION); + // `decrypt_str_or_original` returns (value, decrypted_ok, should_store). + // If the value looks like an encrypted payload ("00" + base64 with MAC) but cannot be + // decrypted on this machine, it is most likely copied from another device or corrupted. + // In normal single-machine setups this should be extremely rare, so keep it as-is. + if !decrypted && !looks_like_plaintext { + return false; + } + if config.salt.is_empty() { + config.salt = Config::get_auto_password(DEFAULT_SALT_LEN); + } + if is_permanent_password_hashed_storage(&plain) { + config.password = plain; + return true; + } + let h1 = compute_permanent_password_h1(&plain, &config.salt); + config.password = encode_permanent_password_storage_from_h1(&h1); + return true; + } + + if config.salt.is_empty() { + config.salt = Config::get_auto_password(DEFAULT_SALT_LEN); + } + let h1 = compute_permanent_password_h1(&config.password, &config.salt); + config.password = encode_permanent_password_storage_from_h1(&h1); + true + } + + fn store(&self) { + let mut config = self.clone(); + Self::migrate_permanent_password_to_hashed_storage(&mut config); + config.enc_id = encrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); + config.id = "".to_owned(); + Config::store_(&config, ""); + } + + pub fn file() -> PathBuf { + Self::file_("") + } + + fn file_(suffix: &str) -> PathBuf { + let name = format!("{}{}", *APP_NAME.read().unwrap(), suffix); + Config::with_extension(Self::path(name)) + } + + pub fn is_empty(&self) -> bool { + (self.id.is_empty() && self.enc_id.is_empty()) || self.key_pair.0.is_empty() + } + + /// Get the user's home directory for configuration purposes. + /// + /// # Security Note + /// This function uses `dirs_next::home_dir()` which reads the `$HOME` environment + /// variable on Unix systems. This is acceptable for user-space operations (config + /// file storage, logging) where the user may intentionally redirect their home + /// directory. + /// + /// **DO NOT use this function in privileged contexts** (e.g., code executed via + /// `gtk_sudo` or system services running as root). For privileged operations on + /// Linux, use `crate::platform::linux::get_home_dir_trusted()` which bypasses + /// the `$HOME` environment variable and queries the system password database + /// directly via `getpwuid`. + /// + /// Using `$HOME` in privileged contexts creates a confused-deputy vulnerability + /// where an attacker can manipulate the environment variable to inject malicious + /// paths into privileged operations. + pub fn get_home() -> PathBuf { + #[cfg(any(target_os = "android", target_os = "ios"))] + return PathBuf::from(APP_HOME_DIR.read().unwrap().as_str()); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if let Some(path) = dirs_next::home_dir() { + patch(path) + } else if let Ok(path) = std::env::current_dir() { + path + } else { + std::env::temp_dir() + } + } + } + + pub fn path>(p: P) -> PathBuf { + #[cfg(any(target_os = "android", target_os = "ios"))] + { + let mut path: PathBuf = APP_DIR.read().unwrap().clone().into(); + path.push(p); + return path; + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + #[cfg(not(target_os = "macos"))] + let org = "".to_owned(); + #[cfg(target_os = "macos")] + let org = ORG.read().unwrap().clone(); + // /var/root for root + if let Some(project) = + directories_next::ProjectDirs::from("", &org, &APP_NAME.read().unwrap()) + { + let mut path = patch(project.config_dir().to_path_buf()); + path.push(p); + return path; + } + "".into() + } + } + + /// Get the log directory path. + /// + /// # Security Note + /// On macOS, this function uses `dirs_next::home_dir()` which reads the `$HOME` + /// environment variable. On Linux/Android, it uses `Self::get_home()`. + /// See [`Self::get_home()`] for security considerations regarding `$HOME` usage. + #[allow(unreachable_code)] + pub fn log_path() -> PathBuf { + #[cfg(target_os = "macos")] + { + if let Some(path) = dirs_next::home_dir().as_mut() { + path.push(format!("Library/Logs/{}", *APP_NAME.read().unwrap())); + return path.clone(); + } + } + #[cfg(target_os = "linux")] + { + let mut path = Self::get_home(); + path.push(format!(".local/share/logs/{}", *APP_NAME.read().unwrap())); + std::fs::create_dir_all(&path).ok(); + return path; + } + #[cfg(target_os = "android")] + { + let mut path = Self::get_home(); + path.push(format!("{}/Logs", *APP_NAME.read().unwrap())); + std::fs::create_dir_all(&path).ok(); + return path; + } + if let Some(path) = Self::path("").parent() { + let mut path: PathBuf = path.into(); + path.push("log"); + return path; + } + "".into() + } + + pub fn ipc_path(postfix: &str) -> String { + #[cfg(windows)] + { + // \\ServerName\pipe\PipeName + // where ServerName is either the name of a remote computer or a period, to specify the local computer. + // https://docs.microsoft.com/en-us/windows/win32/ipc/pipe-names + format!( + "\\\\.\\pipe\\{}\\query{}", + *APP_NAME.read().unwrap(), + postfix + ) + } + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + #[cfg(target_os = "android")] + let mut path: PathBuf = + format!("{}/{}", *APP_DIR.read().unwrap(), *APP_NAME.read().unwrap()).into(); + #[cfg(not(target_os = "android"))] + let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into(); + fs::create_dir(&path).ok(); + fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok(); + path.push(format!("ipc{postfix}")); + path.to_str().unwrap_or("").to_owned() + } + } + + pub fn icon_path() -> PathBuf { + let mut path = Self::path("icons"); + if fs::create_dir_all(&path).is_err() { + path = std::env::temp_dir(); + } + path + } + + #[inline] + pub fn get_any_listen_addr(is_ipv4: bool) -> SocketAddr { + if is_ipv4 { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) + } else { + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0) + } + } + + pub fn get_rendezvous_server() -> String { + let mut rendezvous_server = EXE_RENDEZVOUS_SERVER.read().unwrap().clone(); + if rendezvous_server.is_empty() { + rendezvous_server = Self::get_option("custom-rendezvous-server"); + } + if rendezvous_server.is_empty() { + rendezvous_server = PROD_RENDEZVOUS_SERVER.read().unwrap().clone(); + } + if rendezvous_server.is_empty() { + rendezvous_server = CONFIG2.read().unwrap().rendezvous_server.clone(); + } + if rendezvous_server.is_empty() { + rendezvous_server = Self::get_rendezvous_servers() + .drain(..) + .next() + .unwrap_or_default(); + } + if !rendezvous_server.contains(':') { + rendezvous_server = format!("{rendezvous_server}:{RENDEZVOUS_PORT}"); + } + rendezvous_server + } + + pub fn get_rendezvous_servers() -> Vec { + let s = EXE_RENDEZVOUS_SERVER.read().unwrap().clone(); + if !s.is_empty() { + return vec![s]; + } + let s = Self::get_option("custom-rendezvous-server"); + if !s.is_empty() { + return vec![s]; + } + let s = PROD_RENDEZVOUS_SERVER.read().unwrap().clone(); + if !s.is_empty() { + return vec![s]; + } + let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL; + if serial_obsolute { + let ss: Vec = Self::get_option("rendezvous-servers") + .split(',') + .filter(|x| x.contains('.')) + .map(|x| x.to_owned()) + .collect(); + if !ss.is_empty() { + return ss; + } + } + return RENDEZVOUS_SERVERS.iter().map(|x| x.to_string()).collect(); + } + + pub fn reset_online() { + *ONLINE.lock().unwrap() = Default::default(); + } + + pub fn update_latency(host: &str, latency: i64) { + ONLINE.lock().unwrap().insert(host.to_owned(), latency); + let mut host = "".to_owned(); + let mut delay = i64::MAX; + for (tmp_host, tmp_delay) in ONLINE.lock().unwrap().iter() { + if tmp_delay > &0 && tmp_delay < &delay { + delay = *tmp_delay; + host = tmp_host.to_string(); + } + } + if !host.is_empty() { + let mut config = CONFIG2.write().unwrap(); + if host != config.rendezvous_server { + log::debug!("Update rendezvous_server in config to {}", host); + log::debug!("{:?}", *ONLINE.lock().unwrap()); + config.rendezvous_server = host; + config.store(); + } + } + } + + pub fn set_id(id: &str) { + let mut config = CONFIG.write().unwrap(); + if id == config.id { + return; + } + config.id = id.into(); + config.store(); + } + + pub fn set_nat_type(nat_type: i32) { + let mut config = CONFIG2.write().unwrap(); + if nat_type == config.nat_type { + return; + } + config.nat_type = nat_type; + config.store(); + } + + pub fn get_nat_type() -> i32 { + CONFIG2.read().unwrap().nat_type + } + + pub fn set_serial(serial: i32) { + let mut config = CONFIG2.write().unwrap(); + if serial == config.serial { + return; + } + config.serial = serial; + config.store(); + } + + pub fn get_serial() -> i32 { + std::cmp::max(CONFIG2.read().unwrap().serial, SERIAL) + } + + #[cfg(any(target_os = "android", target_os = "ios"))] + fn gen_id() -> Option { + Self::get_auto_id() + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn gen_id() -> Option { + let hostname_as_id = BUILTIN_SETTINGS + .read() + .unwrap() + .get(keys::OPTION_ALLOW_HOSTNAME_AS_ID) + .map(|v| option2bool(keys::OPTION_ALLOW_HOSTNAME_AS_ID, v)) + .unwrap_or(false); + if hostname_as_id { + match whoami::fallible::hostname() { + Ok(h) => Some(h.replace(" ", "-")), + Err(e) => { + log::warn!("Failed to get hostname, \"{}\", fallback to auto id", e); + Self::get_auto_id() + } + } + } else { + Self::get_auto_id() + } + } + + fn get_auto_id() -> Option { + #[cfg(any(target_os = "android", target_os = "ios"))] + { + return Some( + rand::thread_rng() + .gen_range(1_000_000_000..2_000_000_000) + .to_string(), + ); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let mut id = 0u32; + if let Ok(Some(ma)) = mac_address::get_mac_address() { + for x in &ma.bytes()[2..] { + id = (id << 8) | (*x as u32); + } + id &= 0x1FFFFFFF; + log::info!("Generated id {}", id); + Some(id.to_string()) + } else { + None + } + } + } + + pub fn get_auto_password(length: usize) -> String { + Self::get_auto_password_with_chars(length, CHARS) + } + + pub fn get_auto_numeric_password(length: usize) -> String { + Self::get_auto_password_with_chars(length, NUM_CHARS) + } + + fn get_auto_password_with_chars(length: usize, chars: &[char]) -> String { + let mut rng = rand::thread_rng(); + (0..length) + .map(|_| chars[rng.gen::() % chars.len()]) + .collect() + } + + pub fn get_key_confirmed() -> bool { + CONFIG.read().unwrap().key_confirmed + } + + pub fn set_key_confirmed(v: bool) { + let mut config = CONFIG.write().unwrap(); + if config.key_confirmed == v { + return; + } + config.key_confirmed = v; + if !v { + config.keys_confirmed = Default::default(); + } + config.store(); + } + + pub fn get_host_key_confirmed(host: &str) -> bool { + matches!(CONFIG.read().unwrap().keys_confirmed.get(host), Some(true)) + } + + pub fn set_host_key_confirmed(host: &str, v: bool) { + if Self::get_host_key_confirmed(host) == v { + return; + } + let mut config = CONFIG.write().unwrap(); + config.keys_confirmed.insert(host.to_owned(), v); + config.store(); + } + + pub fn get_key_pair() -> KeyPair { + // lock here to make sure no gen_keypair more than once + // no use of CONFIG directly here to ensure no recursive calling in Config::load because of password dec which calling this function + let mut lock = KEY_PAIR.lock().unwrap(); + if let Some(p) = lock.as_ref() { + return p.clone(); + } + let mut config = Config::load_::(""); + if config.key_pair.0.is_empty() { + log::info!("Generated new keypair for id: {}", config.id); + let (pk, sk) = sign::gen_keypair(); + let key_pair = (sk.0.to_vec(), pk.0.into()); + config.key_pair = key_pair.clone(); + std::thread::spawn(|| { + let mut config = CONFIG.write().unwrap(); + config.key_pair = key_pair; + config.store(); + }); + } + *lock = Some(config.key_pair.clone()); + config.key_pair + } + + pub fn get_cached_pk() -> Option> { + KEY_PAIR.lock().unwrap().clone().map(|k| k.1) + } + + /// Get existing key pair without generating a new one. + /// Returns None if no key pair exists in cache or config file. + pub fn get_existing_key_pair() -> Option { + let mut lock = KEY_PAIR.lock().unwrap(); + if let Some(p) = lock.as_ref() { + return Some(p.clone()); + } + + // IMPORTANT: this path is called while holding KEY_PAIR lock. + // Config::load_ must remain a raw conf load/deserialize path and must never + // call decrypt_* / symmetric_crypt (directly or indirectly), otherwise this + // can re-enter key loading and deadlock. + let config = Config::load_::(""); + if !config.key_pair.0.is_empty() { + *lock = Some(config.key_pair.clone()); + Some(config.key_pair) + } else { + None + } + } + + pub fn no_register_device() -> bool { + BUILTIN_SETTINGS + .read() + .unwrap() + .get(keys::OPTION_REGISTER_DEVICE) + .map(|v| v == "N") + .unwrap_or(false) + } + + pub fn is_disable_change_permanent_password() -> bool { + BUILTIN_SETTINGS + .read() + .unwrap() + .get(keys::OPTION_DISABLE_CHANGE_PERMANENT_PASSWORD) + .map(|v| v == "Y") + .unwrap_or(false) + } + + pub fn is_disable_change_id() -> bool { + BUILTIN_SETTINGS + .read() + .unwrap() + .get(keys::OPTION_DISABLE_CHANGE_ID) + .map(|v| v == "Y") + .unwrap_or(false) + } + + pub fn is_disable_unlock_pin() -> bool { + BUILTIN_SETTINGS + .read() + .unwrap() + .get(keys::OPTION_DISABLE_UNLOCK_PIN) + .map(|v| v == "Y") + .unwrap_or(false) + } + + pub fn get_id() -> String { + let mut id = CONFIG.read().unwrap().id.clone(); + if id.is_empty() { + if let Some(tmp) = Config::gen_id() { + id = tmp; + Config::set_id(&id); + } + } + id + } + + pub fn get_id_or(b: String) -> String { + let a = CONFIG.read().unwrap().id.clone(); + if a.is_empty() { + b + } else { + a + } + } + + pub fn get_options() -> HashMap { + let mut res = DEFAULT_SETTINGS.read().unwrap().clone(); + res.extend(CONFIG2.read().unwrap().options.clone()); + res.extend(OVERWRITE_SETTINGS.read().unwrap().clone()); + res + } + + #[inline] + fn purify_options(v: &mut HashMap) { + v.retain(|k, v| is_option_can_save(&OVERWRITE_SETTINGS, k, &DEFAULT_SETTINGS, v)); + } + + pub fn set_options(mut v: HashMap) { + Self::purify_options(&mut v); + let mut config = CONFIG2.write().unwrap(); + if config.options == v { + return; + } + config.options = v; + config.store(); + } + + pub fn get_option(k: &str) -> String { + get_or( + &OVERWRITE_SETTINGS, + &CONFIG2.read().unwrap().options, + &DEFAULT_SETTINGS, + k, + ) + .unwrap_or_default() + } + + pub fn get_bool_option(k: &str) -> bool { + option2bool(k, &Self::get_option(k)) + } + + pub fn set_option(k: String, v: String) { + if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) { + let mut config = CONFIG2.write().unwrap(); + if config.options.remove(&k).is_some() { + config.store(); + } + return; + } + let mut config = CONFIG2.write().unwrap(); + let v2 = if v.is_empty() { None } else { Some(&v) }; + if v2 != config.options.get(&k) { + if v2.is_none() { + config.options.remove(&k); + } else { + config.options.insert(k, v); + } + config.store(); + } + } + + pub fn update_id() { + // to-do: how about if one ip register a lot of ids? + let id = Self::get_id(); + let mut rng = rand::thread_rng(); + let new_id = rng.gen_range(1_000_000_000..2_000_000_000).to_string(); + Config::set_id(&new_id); + log::info!("id updated from {} to {}", id, new_id); + } + + pub fn set_permanent_password(password: &str) { + if Self::is_disable_change_permanent_password() { + return; + } + if HARD_SETTINGS + .read() + .unwrap() + .get("password") + .map_or(false, |v| v == password) + { + if CONFIG.read().unwrap().password.is_empty() { + return; + } + } + + let mut config = CONFIG.write().unwrap(); + + let stored = if password.is_empty() { + String::new() + } else { + Self::compute_permanent_password_storage_for_update(&mut config, password) + }; + if stored == config.password { + return; + } + config.password = stored; + config.store(); + Self::clear_trusted_devices(); + } + + fn compute_permanent_password_storage_for_update( + config: &mut Config, + password: &str, + ) -> String { + // Keep salt stable for user-initiated permanent password updates. + // Salt should only change when service->user sync updates storage and salt as a pair. + if config.salt.is_empty() { + config.salt = Config::get_auto_password(DEFAULT_SALT_LEN); + } + let h1 = compute_permanent_password_h1(password, &config.salt); + encode_permanent_password_storage_from_h1(&h1) + } + + /// Returns the locally persisted permanent password storage and salt (NOT the hard/preset one). + /// + /// This function is side-effect free: + /// - It does NOT call `get_salt()` (which may auto-generate salt). + /// - It returns a consistent snapshot under a single lock. + pub fn get_local_permanent_password_storage_and_salt() -> (String, String) { + let config = CONFIG.read().unwrap(); + (config.password.clone(), config.salt.clone()) + } + + /// Persist permanent password storage and salt from service->user config sync. + pub fn set_permanent_password_storage_for_sync( + storage: &str, + salt: &str, + ) -> crate::ResultType { + let mut config = CONFIG.write().unwrap(); + if config.password == storage && config.salt == salt { + return Ok(false); + } + + config.password = storage.to_owned(); + config.salt = salt.to_owned(); + config.store(); + Self::clear_trusted_devices(); + Ok(true) + } + + /// Returns true if `input` (candidate plaintext) matches the currently effective permanent password. + pub fn matches_permanent_password_plain(input: &str) -> bool { + if input.is_empty() { + return false; + } + + let config = CONFIG.read().unwrap(); + let storage = config.password.clone(); + let salt = config.salt.clone(); + drop(config); + + if storage.is_empty() { + return HARD_SETTINGS + .read() + .unwrap() + .get("password") + .map_or(false, |v| v == input); + } + + if let Some(stored_h1) = decode_permanent_password_h1_from_storage(&storage) { + if salt.is_empty() { + log::error!("Salt is empty but permanent password is hashed"); + return false; + } + let h1 = compute_permanent_password_h1(input, &salt); + return constant_time_eq_32(&h1, &stored_h1); + } + + log::warn!("Permanent password storage is not hashed; verifying as plaintext"); + storage == input + } + + pub fn has_permanent_password() -> bool { + if !CONFIG.read().unwrap().password.is_empty() { + return true; + } + HARD_SETTINGS + .read() + .unwrap() + .get("password") + .map_or(false, |v| !v.is_empty()) + } + + pub fn has_local_permanent_password() -> bool { + !CONFIG.read().unwrap().password.is_empty() + } + + // This shouldn't happen under normal circumstances because the salt + // should be automatically generated when migrating to hash storage. + // Actually, it is better to avoid calling set_salt at all. + pub fn set_salt(salt: &str) { + let mut config = CONFIG.write().unwrap(); + if salt == config.salt { + return; + } + if !password_is_empty_or_not_hashed(&config.password) { + if config.salt.is_empty() { + log::warn!("Salt is empty but permanent password is hashed and salt is empty"); + } else { + log::error!("Refusing to set salt because permanent password is hashed"); + return; + } + } + config.salt = salt.into(); + config.store(); + } + + pub fn get_salt() -> String { + let config = CONFIG.read().unwrap(); + let mut salt = config.salt.clone(); + if salt.is_empty() { + drop(config); + salt = Config::get_auto_password(DEFAULT_SALT_LEN); + Config::set_salt(&salt); + } + salt + } + + pub fn set_socks(socks: Option) { + if OVERWRITE_SETTINGS + .read() + .unwrap() + .contains_key(keys::OPTION_PROXY_URL) + { + return; + } + + let mut config = CONFIG2.write().unwrap(); + if config.socks == socks { + return; + } + if config.socks.is_none() { + let equal_to_default = |key: &str, value: &str| { + DEFAULT_SETTINGS + .read() + .unwrap() + .get(key) + .map_or(false, |x| *x == value) + }; + let contains_url = DEFAULT_SETTINGS + .read() + .unwrap() + .get(keys::OPTION_PROXY_URL) + .is_some(); + let url = equal_to_default( + keys::OPTION_PROXY_URL, + &socks.clone().unwrap_or_default().proxy, + ); + let username = equal_to_default( + keys::OPTION_PROXY_USERNAME, + &socks.clone().unwrap_or_default().username, + ); + let password = equal_to_default( + keys::OPTION_PROXY_PASSWORD, + &socks.clone().unwrap_or_default().password, + ); + if contains_url && url && username && password { + return; + } + } + config.socks = socks; + config.store(); + } + + #[inline] + fn get_socks_from_custom_client_advanced_settings( + settings: &HashMap, + ) -> Option { + let url = settings.get(keys::OPTION_PROXY_URL)?; + Some(Socks5Server { + proxy: url.to_owned(), + username: settings + .get(keys::OPTION_PROXY_USERNAME) + .map(|x| x.to_string()) + .unwrap_or_default(), + password: settings + .get(keys::OPTION_PROXY_PASSWORD) + .map(|x| x.to_string()) + .unwrap_or_default(), + }) + } + + pub fn get_socks() -> Option { + Self::get_socks_from_custom_client_advanced_settings(&OVERWRITE_SETTINGS.read().unwrap()) + .or(CONFIG2.read().unwrap().socks.clone()) + .or(Self::get_socks_from_custom_client_advanced_settings( + &DEFAULT_SETTINGS.read().unwrap(), + )) + } + + #[inline] + pub fn is_proxy() -> bool { + Self::get_network_type() != NetworkType::Direct + } + + pub fn get_network_type() -> NetworkType { + if OVERWRITE_SETTINGS + .read() + .unwrap() + .get(keys::OPTION_PROXY_URL) + .is_some() + { + return NetworkType::ProxySocks; + } + if CONFIG2.read().unwrap().socks.is_some() { + return NetworkType::ProxySocks; + } + if DEFAULT_SETTINGS + .read() + .unwrap() + .get(keys::OPTION_PROXY_URL) + .is_some() + { + return NetworkType::ProxySocks; + } + NetworkType::Direct + } + + pub fn get_unlock_pin() -> String { + if Self::is_disable_unlock_pin() { + return String::new(); + } + CONFIG2.read().unwrap().unlock_pin.clone() + } + + pub fn set_unlock_pin(pin: &str) { + if Self::is_disable_unlock_pin() { + return; + } + let mut config = CONFIG2.write().unwrap(); + if pin == config.unlock_pin { + return; + } + config.unlock_pin = pin.to_string(); + config.store(); + } + + pub fn get_trusted_devices_json() -> String { + serde_json::to_string(&Self::get_trusted_devices()).unwrap_or_default() + } + + pub fn get_trusted_devices() -> Vec { + let (devices, synced) = TRUSTED_DEVICES.read().unwrap().clone(); + if synced { + return devices; + } + let devices = CONFIG2.read().unwrap().trusted_devices.clone(); + let (devices, succ, store) = decrypt_str_or_original(&devices, PASSWORD_ENC_VERSION); + if succ { + let mut devices: Vec = + serde_json::from_str(&devices).unwrap_or_default(); + let len = devices.len(); + devices.retain(|d| !d.outdate()); + if store || devices.len() != len { + Self::set_trusted_devices(devices.clone()); + } + *TRUSTED_DEVICES.write().unwrap() = (devices.clone(), true); + devices + } else { + Default::default() + } + } + + fn set_trusted_devices(mut trusted_devices: Vec) { + trusted_devices.retain(|d| !d.outdate()); + let devices = serde_json::to_string(&trusted_devices).unwrap_or_default(); + let max_len = 1024 * 1024; + if devices.bytes().len() > max_len { + log::error!("Trusted devices too large: {}", devices.bytes().len()); + return; + } + let devices = encrypt_str_or_original(&devices, PASSWORD_ENC_VERSION, max_len); + let mut config = CONFIG2.write().unwrap(); + config.trusted_devices = devices; + config.store(); + *TRUSTED_DEVICES.write().unwrap() = (trusted_devices, true); + } + + pub fn add_trusted_device(device: TrustedDevice) { + let mut devices = Self::get_trusted_devices(); + devices.retain(|d| d.hwid != device.hwid); + devices.push(device); + Self::set_trusted_devices(devices); + } + + pub fn remove_trusted_devices(hwids: &Vec) { + let mut devices = Self::get_trusted_devices(); + devices.retain(|d| !hwids.contains(&d.hwid)); + Self::set_trusted_devices(devices); + } + + pub fn clear_trusted_devices() { + Self::set_trusted_devices(Default::default()); + } + + pub fn get() -> Config { + return CONFIG.read().unwrap().clone(); + } + + // TODO: `Config::set()` does not invalidate trusted devices when permanent password/salt changes. + // This matches historical behavior, but may need revisiting in a separate PR. + pub fn set(cfg: Config) -> bool { + let mut lock = CONFIG.write().unwrap(); + if *lock == cfg { + return false; + } + *lock = cfg; + lock.store(); + // Drop CONFIG lock before acquiring KEY_PAIR lock to avoid potential deadlock. + #[cfg(target_os = "macos")] + let new_key_pair = lock.key_pair.clone(); + drop(lock); + #[cfg(target_os = "macos")] + Self::invalidate_key_pair_cache_if_changed(&new_key_pair); + true + } + + /// Invalidate KEY_PAIR cache if it differs from the new key_pair. + /// Use None to invalidate the cache instead of Some(key_pair). + /// If we use Some with an empty key_pair, get_key_pair() would always return + /// the empty key_pair from cache without regenerating. + /// By clearing the cache, get_key_pair() will reload and regenerate if needed. + #[cfg(target_os = "macos")] + fn invalidate_key_pair_cache_if_changed(new_key_pair: &KeyPair) { + let mut key_pair_cache = KEY_PAIR.lock().unwrap(); + if let Some(cached) = key_pair_cache.as_ref() { + if cached != new_key_pair { + *key_pair_cache = None; + log::info!("key pair cache invalidated"); + } + } + } + + fn with_extension(path: PathBuf) -> PathBuf { + let ext = path.extension(); + if let Some(ext) = ext { + let ext = format!("{}.toml", ext.to_string_lossy()); + path.with_extension(ext) + } else { + path.with_extension("toml") + } + } +} + +const PEERS: &str = "peers"; + +impl PeerConfig { + pub fn load(id: &str) -> PeerConfig { + let _lock = CONFIG.read().unwrap(); + match confy::load_path(Self::path(id)) { + Ok(config) => { + let mut config: PeerConfig = config; + let mut store = false; + let (password, _, store2) = + decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); + config.password = password; + store = store || store2; + for opt in ["rdp_password", "os-username", "os-password"] { + if let Some(v) = config.options.get_mut(opt) { + let (encrypted, _, store2) = + decrypt_str_or_original(v, PASSWORD_ENC_VERSION); + *v = encrypted; + store = store || store2; + } + } + if store { + config.store_(id); + } + config + } + Err(err) => { + if let confy::ConfyError::GeneralLoadError(err) = &err { + if err.kind() == std::io::ErrorKind::NotFound { + return Default::default(); + } + } + log::error!("Failed to load peer config '{}': {}", id, err); + Default::default() + } + } + } + + pub fn store(&self, id: &str) { + let _lock = CONFIG.read().unwrap(); + self.store_(id); + } + + fn store_(&self, id: &str) { + let mut config = self.clone(); + config.password = + encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); + for opt in ["rdp_password", "os-username", "os-password"] { + if let Some(v) = config.options.get_mut(opt) { + *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN) + } + } + if let Err(err) = store_path(Self::path(id), config) { + log::error!("Failed to store config: {}", err); + } + NEW_STORED_PEER_CONFIG.lock().unwrap().insert(id.to_owned()); + } + + pub fn remove(id: &str) { + fs::remove_file(Self::path(id)).ok(); + } + + fn path(id: &str) -> PathBuf { + //If the id contains invalid chars, encode it + let forbidden_paths = Regex::new(r".*[<>:/\\|\?\*].*"); + let path: PathBuf; + if let Ok(forbidden_paths) = forbidden_paths { + let id_encoded = if forbidden_paths.is_match(id) { + "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str() + } else { + id.to_string() + }; + path = [PEERS, id_encoded.as_str()].iter().collect(); + } else { + log::warn!("Regex create failed: {:?}", forbidden_paths.err()); + // fallback for failing to create this regex. + path = [PEERS, id.replace(":", "_").as_str()].iter().collect(); + } + Config::with_extension(Config::path(path)) + } + + // The number of peers to load in the first round when showing the peers card list in the main window. + // When there're too many peers, loading all of them at once will take a long time. + // We can load them in two rouds, the first round loads the first 100 peers, and the second round loads the rest. + // Then the UI will show the first 100 peers first, and the rest will be loaded and shown later. + pub const BATCH_LOADING_COUNT: usize = 100; + + pub fn get_vec_id_modified_time_path( + id_filters: &Option>, + ) -> Vec<(String, SystemTime, PathBuf)> { + if let Ok(peers) = Config::path(PEERS).read_dir() { + let mut vec_id_modified_time_path = peers + .into_iter() + .filter_map(|res| match res { + Ok(res) => { + let p = res.path(); + if p.is_file() + && p.extension().map(|p| p.to_str().unwrap_or("")) == Some("toml") + { + Some(p) + } else { + None + } + } + _ => None, + }) + .map(|p| { + let id = p + .file_stem() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("") + .to_owned(); + + let id_decoded_string = if id.starts_with("base64_") && id.len() != 7 { + let id_decoded = + base64::decode(&id[7..], base64::Variant::Original).unwrap_or_default(); + String::from_utf8_lossy(&id_decoded).as_ref().to_owned() + } else { + id + }; + (id_decoded_string, p) + }) + .filter(|(id, _)| { + let Some(filters) = id_filters else { + return true; + }; + filters.contains(id) + }) + .map(|(id, p)| { + let t = crate::get_modified_time(&p); + (id, t, p) + }) + .collect::>(); + vec_id_modified_time_path.sort_unstable_by(|a, b| b.1.cmp(&a.1)); + vec_id_modified_time_path + } else { + vec![] + } + } + + #[inline] + async fn preload_file_async(path: PathBuf) { + let _ = tokio::fs::File::open(path).await; + } + + #[tokio::main(flavor = "current_thread")] + async fn preload_peers_async() { + let now = std::time::Instant::now(); + let vec_id_modified_time_path = Self::get_vec_id_modified_time_path(&None); + let total_count = vec_id_modified_time_path.len(); + let mut futs = vec![]; + for (_, _, path) in vec_id_modified_time_path.into_iter() { + futs.push(Self::preload_file_async(path)); + if futs.len() >= Self::BATCH_LOADING_COUNT { + let first_load_start = std::time::Instant::now(); + futures::future::join_all(futs).await; + if first_load_start.elapsed().as_millis() < 10 { + // No need to preload the rest if the first load is fast. + return; + } + futs = vec![]; + } + } + if !futs.is_empty() { + futures::future::join_all(futs).await; + } + log::info!( + "Preload peers done in {:?}, batch_count: {}, total: {}", + now.elapsed(), + Self::BATCH_LOADING_COUNT, + total_count + ); + } + + // We have to preload all peers in a background thread. + // Because we find that opening files the first time after the system (Windows) booting will be very slow, up to 200~400ms. + // The reason is that the Windows has "Microsoft Defender Antivirus Service" running in the background, which will scan the file when it's opened the first time. + // So we have to preload all peers in a background thread to avoid the delay when opening the file the first time. + // We can temporarily stop "Microsoft Defender Antivirus Service" or add the fold to the white list, to verify this. But don't do this in the release version. + pub fn preload_peers() { + std::thread::spawn(|| { + Self::preload_peers_async(); + }); + } + + pub fn peers(id_filters: Option>) -> Vec<(String, SystemTime, PeerConfig)> { + let vec_id_modified_time_path = Self::get_vec_id_modified_time_path(&id_filters); + Self::batch_peers( + &vec_id_modified_time_path, + 0, + Some(vec_id_modified_time_path.len()), + ) + .0 + } + + pub fn batch_peers( + all: &Vec<(String, SystemTime, PathBuf)>, + from: usize, + to: Option, + ) -> (Vec<(String, SystemTime, PeerConfig)>, usize) { + if from >= all.len() { + return (vec![], 0); + } + + let to = match to { + Some(to) => to.min(all.len()), + None => (from + Self::BATCH_LOADING_COUNT).min(all.len()), + }; + + // to <= from is unexpected, but we can just return an empty vec in this case. + if to <= from { + return (vec![], from); + } + + let peers: Vec<_> = all[from..to] + .iter() + .map(|(id, t, p)| { + let c = PeerConfig::load(&id); + if c.info.platform.is_empty() { + fs::remove_file(p).ok(); + } + (id.clone(), t.clone(), c) + }) + .filter(|p| !p.2.info.platform.is_empty()) + .collect(); + (peers, to) + } + + pub fn exists(id: &str) -> bool { + Self::path(id).exists() + } + + serde_field_string!( + default_view_style, + deserialize_view_style, + UserDefaultConfig::read(keys::OPTION_VIEW_STYLE) + ); + serde_field_string!( + default_scroll_style, + deserialize_scroll_style, + UserDefaultConfig::read(keys::OPTION_SCROLL_STYLE) + ); + serde_field_string!( + default_image_quality, + deserialize_image_quality, + UserDefaultConfig::read(keys::OPTION_IMAGE_QUALITY) + ); + serde_field_string!( + default_reverse_mouse_wheel, + deserialize_reverse_mouse_wheel, + UserDefaultConfig::read(keys::OPTION_REVERSE_MOUSE_WHEEL) + ); + serde_field_string!( + default_displays_as_individual_windows, + deserialize_displays_as_individual_windows, + UserDefaultConfig::read(keys::OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS) + ); + serde_field_string!( + default_use_all_my_displays_for_the_remote_session, + deserialize_use_all_my_displays_for_the_remote_session, + UserDefaultConfig::read(keys::OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION) + ); + + fn default_custom_image_quality() -> Vec { + let f: f64 = UserDefaultConfig::read(keys::OPTION_CUSTOM_IMAGE_QUALITY) + .parse() + .unwrap_or(50.0); + vec![f as _] + } + + fn deserialize_custom_image_quality<'de, D>(deserializer: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + let v: Vec = de::Deserialize::deserialize(deserializer)?; + if v.len() == 1 && v[0] >= 10 && v[0] <= 0xFFF { + Ok(v) + } else { + Ok(Self::default_custom_image_quality()) + } + } + + fn default_options() -> HashMap { + let mut mp: HashMap = Default::default(); + let _ = [ + keys::OPTION_CODEC_PREFERENCE, + keys::OPTION_CUSTOM_FPS, + keys::OPTION_ZOOM_CURSOR, + keys::OPTION_I444, + keys::OPTION_SWAP_LEFT_RIGHT_MOUSE, + keys::OPTION_COLLAPSE_TOOLBAR, + ] + .map(|key| { + mp.insert(key.to_owned(), UserDefaultConfig::read(key)); + }); + mp + } + + fn default_trackpad_speed() -> i32 { + UserDefaultConfig::read(keys::OPTION_TRACKPAD_SPEED) + .parse() + .unwrap_or(100) + } + + fn deserialize_trackpad_speed<'de, D>(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let v: i32 = de::Deserialize::deserialize(deserializer)?; + if v >= 10 && v <= 1000 { + Ok(v) + } else { + Ok(Self::default_trackpad_speed()) + } + } + + fn default_edge_scroll_edge_thickness() -> i32 { + UserDefaultConfig::read(keys::OPTION_EDGE_SCROLL_EDGE_THICKNESS) + .parse() + .unwrap_or(100) + } + + fn deserialize_edge_scroll_edge_thickness<'de, D>(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let v: i32 = de::Deserialize::deserialize(deserializer)?; + if v >= 20 && v <= 150 { + Ok(v) + } else { + Ok(Self::default_edge_scroll_edge_thickness()) + } + } +} + +serde_field_bool!( + ShowRemoteCursor, + "show_remote_cursor", + default_show_remote_cursor, + "ShowRemoteCursor::default_show_remote_cursor" +); +serde_field_bool!( + FollowRemoteCursor, + "follow_remote_cursor", + default_follow_remote_cursor, + "FollowRemoteCursor::default_follow_remote_cursor" +); + +serde_field_bool!( + FollowRemoteWindow, + "follow_remote_window", + default_follow_remote_window, + "FollowRemoteWindow::default_follow_remote_window" +); +serde_field_bool!( + ShowQualityMonitor, + "show_quality_monitor", + default_show_quality_monitor, + "ShowQualityMonitor::default_show_quality_monitor" +); +serde_field_bool!( + DisableAudio, + "disable_audio", + default_disable_audio, + "DisableAudio::default_disable_audio" +); +serde_field_bool!( + EnableFileCopyPaste, + "enable-file-copy-paste", + default_enable_file_copy_paste, + "EnableFileCopyPaste::default_enable_file_copy_paste" +); +serde_field_bool!( + DisableClipboard, + "disable_clipboard", + default_disable_clipboard, + "DisableClipboard::default_disable_clipboard" +); +serde_field_bool!( + LockAfterSessionEnd, + "lock_after_session_end", + default_lock_after_session_end, + "LockAfterSessionEnd::default_lock_after_session_end" +); +serde_field_bool!( + TerminalPersistent, + "terminal-persistent", + default_terminal_persistent, + "TerminalPersistent::default_terminal_persistent" +); +serde_field_bool!( + PrivacyMode, + "privacy_mode", + default_privacy_mode, + "PrivacyMode::default_privacy_mode" +); + +serde_field_bool!( + AllowSwapKey, + "allow_swap_key", + default_allow_swap_key, + "AllowSwapKey::default_allow_swap_key" +); + +serde_field_bool!( + ViewOnly, + "view_only", + default_view_only, + "ViewOnly::default_view_only" +); + +serde_field_bool!( + ShowMyCursor, + "show_my_cursor", + default_show_my_cursor, + "ShowMyCursor::default_show_my_cursor" +); + +serde_field_bool!( + SyncInitClipboard, + "sync-init-clipboard", + default_sync_init_clipboard, + "SyncInitClipboard::default_sync_init_clipboard" +); + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct LocalConfig { + #[serde(default, deserialize_with = "deserialize_string")] + remote_id: String, // latest used one + #[serde(default, deserialize_with = "deserialize_string")] + kb_layout_type: String, + #[serde(default, deserialize_with = "deserialize_size")] + size: Size, + #[serde(default, deserialize_with = "deserialize_vec_string")] + pub fav: Vec, + #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] + options: HashMap, + // Various data for flutter ui + #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] + ui_flutter: HashMap, +} + +impl LocalConfig { + fn load() -> LocalConfig { + Config::load_::("_local") + } + + fn store(&self) { + Config::store_(self, "_local"); + } + + pub fn get_kb_layout_type() -> String { + LOCAL_CONFIG.read().unwrap().kb_layout_type.clone() + } + + pub fn set_kb_layout_type(kb_layout_type: String) { + let mut config = LOCAL_CONFIG.write().unwrap(); + config.kb_layout_type = kb_layout_type; + config.store(); + } + + pub fn get_size() -> Size { + LOCAL_CONFIG.read().unwrap().size + } + + pub fn set_size(x: i32, y: i32, w: i32, h: i32) { + let mut config = LOCAL_CONFIG.write().unwrap(); + let size = (x, y, w, h); + if size == config.size || size.2 < 300 || size.3 < 300 { + return; + } + config.size = size; + config.store(); + } + + pub fn set_remote_id(remote_id: &str) { + let mut config = LOCAL_CONFIG.write().unwrap(); + if remote_id == config.remote_id { + return; + } + config.remote_id = remote_id.into(); + config.store(); + } + + pub fn get_remote_id() -> String { + LOCAL_CONFIG.read().unwrap().remote_id.clone() + } + + pub fn set_fav(fav: Vec) { + let mut lock = LOCAL_CONFIG.write().unwrap(); + if lock.fav == fav { + return; + } + lock.fav = fav; + lock.store(); + } + + pub fn get_fav() -> Vec { + LOCAL_CONFIG.read().unwrap().fav.clone() + } + + pub fn get_option(k: &str) -> String { + get_or( + &OVERWRITE_LOCAL_SETTINGS, + &LOCAL_CONFIG.read().unwrap().options, + &DEFAULT_LOCAL_SETTINGS, + k, + ) + .unwrap_or_default() + } + + // Usually get_option should be used. + pub fn get_option_from_file(k: &str) -> String { + get_or( + &OVERWRITE_LOCAL_SETTINGS, + &Self::load().options, + &DEFAULT_LOCAL_SETTINGS, + k, + ) + .unwrap_or_default() + } + + pub fn get_bool_option(k: &str) -> bool { + option2bool(k, &Self::get_option(k)) + } + + pub fn set_option(k: String, v: String) { + if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k, &DEFAULT_LOCAL_SETTINGS, &v) { + let mut config = LOCAL_CONFIG.write().unwrap(); + if config.options.remove(&k).is_some() { + config.store(); + } + return; + } + let mut config = LOCAL_CONFIG.write().unwrap(); + // The custom client will explictly set "default" as the default language. + let is_custom_client_default_lang = k == keys::OPTION_LANGUAGE && v == "default"; + if is_custom_client_default_lang { + config.options.insert(k, "".to_owned()); + config.store(); + return; + } + let v2 = if v.is_empty() { None } else { Some(&v) }; + if v2 != config.options.get(&k) { + if v2.is_none() { + config.options.remove(&k); + } else { + config.options.insert(k, v); + } + config.store(); + } + } + + pub fn get_flutter_option(k: &str) -> String { + get_or( + &OVERWRITE_LOCAL_SETTINGS, + &LOCAL_CONFIG.read().unwrap().ui_flutter, + &DEFAULT_LOCAL_SETTINGS, + k, + ) + .unwrap_or_default() + } + + pub fn set_flutter_option(k: String, v: String) { + let mut config = LOCAL_CONFIG.write().unwrap(); + let v2 = if v.is_empty() { None } else { Some(&v) }; + if v2 != config.ui_flutter.get(&k) { + if v2.is_none() { + config.ui_flutter.remove(&k); + } else { + config.ui_flutter.insert(k, v); + } + config.store(); + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct DiscoveryPeer { + #[serde(default, deserialize_with = "deserialize_string")] + pub id: String, + #[serde(default, deserialize_with = "deserialize_string")] + pub username: String, + #[serde(default, deserialize_with = "deserialize_string")] + pub hostname: String, + #[serde(default, deserialize_with = "deserialize_string")] + pub platform: String, + #[serde(default, deserialize_with = "deserialize_bool")] + pub online: bool, + #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] + pub ip_mac: HashMap, +} + +impl DiscoveryPeer { + pub fn is_same_peer(&self, other: &DiscoveryPeer) -> bool { + self.id == other.id && self.username == other.username + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct LanPeers { + #[serde(default, deserialize_with = "deserialize_vec_discoverypeer")] + pub peers: Vec, +} + +impl LanPeers { + pub fn load() -> LanPeers { + let _lock = CONFIG.read().unwrap(); + match confy::load_path(Config::file_("_lan_peers")) { + Ok(peers) => peers, + Err(err) => { + log::error!("Failed to load lan peers: {}", err); + Default::default() + } + } + } + + pub fn store(peers: &[DiscoveryPeer]) { + let f = LanPeers { + peers: peers.to_owned(), + }; + if let Err(err) = store_path(Config::file_("_lan_peers"), f) { + log::error!("Failed to store lan peers: {}", err); + } + } + + pub fn modify_time() -> crate::ResultType { + let p = Config::file_("_lan_peers"); + Ok(fs::metadata(p)? + .modified()? + .duration_since(SystemTime::UNIX_EPOCH)? + .as_millis() as _) + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct UserDefaultConfig { + #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] + options: HashMap, +} + +impl UserDefaultConfig { + fn read(key: &str) -> String { + let mut cfg = USER_DEFAULT_CONFIG.write().unwrap(); + // we do so, because default config may changed in another process, but we don't sync it + // but no need to read every time, give a small interval to avoid too many redundant read waste + if cfg.1.elapsed() > Duration::from_secs(1) { + *cfg = (Self::load(), Instant::now()); + } + cfg.0.get(key) + } + + pub fn load() -> UserDefaultConfig { + Config::load_::("_default") + } + + #[inline] + fn store(&self) { + Config::store_(self, "_default"); + } + + pub fn get(&self, key: &str) -> String { + match key { + #[cfg(any(target_os = "android", target_os = "ios"))] + keys::OPTION_VIEW_STYLE => self.get_string(key, "adaptive", vec!["original"]), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + keys::OPTION_VIEW_STYLE => self.get_string(key, "original", vec!["adaptive"]), + keys::OPTION_SCROLL_STYLE => { + self.get_string(key, "scrollauto", vec!["scrolledge", "scrollbar"]) + } + keys::OPTION_IMAGE_QUALITY => { + self.get_string(key, "balanced", vec!["best", "low", "custom"]) + } + keys::OPTION_CODEC_PREFERENCE => { + self.get_string(key, "auto", vec!["vp8", "vp9", "av1", "h264", "h265"]) + } + keys::OPTION_CUSTOM_IMAGE_QUALITY => self.get_num_string(key, 50.0, 10.0, 0xFFF as f64), + keys::OPTION_CUSTOM_FPS => self.get_num_string(key, 30.0, 5.0, 120.0), + keys::OPTION_ENABLE_FILE_COPY_PASTE => self.get_string(key, "Y", vec!["", "N"]), + keys::OPTION_EDGE_SCROLL_EDGE_THICKNESS => self.get_num_string(key, 100, 20, 150), + keys::OPTION_TRACKPAD_SPEED => self.get_num_string(key, 100, 10, 1000), + _ => self + .get_after(key) + .map(|v| v.to_string()) + .unwrap_or_default(), + } + } + + pub fn set(&mut self, key: String, value: String) { + if !is_option_can_save( + &OVERWRITE_DISPLAY_SETTINGS, + &key, + &DEFAULT_DISPLAY_SETTINGS, + &value, + ) { + if self.options.remove(&key).is_some() { + self.store(); + } + return; + } + if value.is_empty() { + self.options.remove(&key); + } else { + self.options.insert(key, value); + } + self.store(); + } + + #[inline] + fn get_string(&self, key: &str, default: &str, others: Vec<&str>) -> String { + match self.get_after(key) { + Some(option) => { + if others.contains(&option.as_str()) { + option.to_owned() + } else { + default.to_owned() + } + } + None => default.to_owned(), + } + } + + #[inline] + fn get_num_string(&self, key: &str, default: T, min: T, max: T) -> String + where + T: ToString + std::str::FromStr + std::cmp::PartialOrd + std::marker::Copy, + { + match self.get_after(key) { + Some(option) => { + let v: T = option.parse().unwrap_or(default); + if v >= min && v <= max { + v.to_string() + } else { + default.to_string() + } + } + None => default.to_string(), + } + } + + fn get_after(&self, k: &str) -> Option { + get_or( + &OVERWRITE_DISPLAY_SETTINGS, + &self.options, + &DEFAULT_DISPLAY_SETTINGS, + k, + ) + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct AbPeer { + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub id: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub hash: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub username: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub hostname: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub platform: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub alias: String, + #[serde(default, deserialize_with = "deserialize_vec_string")] + pub tags: Vec, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct AbEntry { + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub guid: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub name: String, + #[serde(default, deserialize_with = "deserialize_vec_abpeer")] + pub peers: Vec, + #[serde(default, deserialize_with = "deserialize_vec_string")] + pub tags: Vec, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub tag_colors: String, +} + +impl AbEntry { + pub fn personal(&self) -> bool { + self.name == "My address book" || self.name == "Legacy address book" + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct Ab { + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub access_token: String, + #[serde(default, deserialize_with = "deserialize_vec_abentry")] + pub ab_entries: Vec, +} + +impl Ab { + fn path() -> PathBuf { + let filename = format!("{}_ab", APP_NAME.read().unwrap().clone()); + Config::path(filename) + } + + pub fn store(json: String) { + if let Ok(mut file) = std::fs::File::create(Self::path()) { + let data = compress(json.as_bytes()); + let max_len = 64 * 1024 * 1024; + if data.len() > max_len { + // maxlen of function decompress + log::error!("ab data too large, {} > {}", data.len(), max_len); + return; + } + if let Ok(data) = symmetric_crypt(&data, true) { + file.write_all(&data).ok(); + } + }; + } + + pub fn load() -> Ab { + if let Ok(mut file) = std::fs::File::open(Self::path()) { + let mut data = vec![]; + if file.read_to_end(&mut data).is_ok() { + if let Ok(data) = symmetric_crypt(&data, false) { + let data = decompress(&data); + if let Ok(ab) = serde_json::from_str::(&String::from_utf8_lossy(&data)) { + return ab; + } + } + } + }; + Self::remove(); + Ab::default() + } + + pub fn remove() { + std::fs::remove_file(Self::path()).ok(); + } +} + +// use default value when field type is wrong +macro_rules! deserialize_default { + ($func_name:ident, $return_type:ty) => { + fn $func_name<'de, D>(deserializer: D) -> Result<$return_type, D::Error> + where + D: de::Deserializer<'de>, + { + Ok(de::Deserialize::deserialize(deserializer).unwrap_or_default()) + } + }; +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct GroupPeer { + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub id: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub username: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub hostname: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub platform: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub login_name: String, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct GroupUser { + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub name: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub display_name: String, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct DeviceGroup { + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub name: String, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct Group { + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub access_token: String, + #[serde(default, deserialize_with = "deserialize_vec_groupuser")] + pub users: Vec, + #[serde(default, deserialize_with = "deserialize_vec_grouppeer")] + pub peers: Vec, + #[serde(default, deserialize_with = "deserialize_vec_devicegroup")] + pub device_groups: Vec, +} + +impl Group { + fn path() -> PathBuf { + let filename = format!("{}_group", APP_NAME.read().unwrap().clone()); + Config::path(filename) + } + + pub fn store(json: String) { + if let Ok(mut file) = std::fs::File::create(Self::path()) { + let data = compress(json.as_bytes()); + let max_len = 64 * 1024 * 1024; + if data.len() > max_len { + // maxlen of function decompress + return; + } + if let Ok(data) = symmetric_crypt(&data, true) { + file.write_all(&data).ok(); + } + }; + } + + pub fn load() -> Self { + if let Ok(mut file) = std::fs::File::open(Self::path()) { + let mut data = vec![]; + if file.read_to_end(&mut data).is_ok() { + if let Ok(data) = symmetric_crypt(&data, false) { + let data = decompress(&data); + if let Ok(group) = serde_json::from_str::(&String::from_utf8_lossy(&data)) + { + return group; + } + } + } + }; + Self::remove(); + Self::default() + } + + pub fn remove() { + std::fs::remove_file(Self::path()).ok(); + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct TrustedDevice { + pub hwid: Bytes, + pub time: i64, + pub id: String, + pub name: String, + pub platform: String, +} + +impl TrustedDevice { + pub fn outdate(&self) -> bool { + const DAYS_90: i64 = 90 * 24 * 60 * 60 * 1000; + self.time + DAYS_90 < crate::get_time() + } +} + +deserialize_default!(deserialize_string, String); +deserialize_default!(deserialize_bool, bool); +deserialize_default!(deserialize_i32, i32); +deserialize_default!(deserialize_vec_u8, Vec); +deserialize_default!(deserialize_vec_string, Vec); +deserialize_default!(deserialize_vec_i32_string_i32, Vec<(i32, String, i32)>); +deserialize_default!(deserialize_vec_discoverypeer, Vec); +deserialize_default!(deserialize_vec_abpeer, Vec); +deserialize_default!(deserialize_vec_abentry, Vec); +deserialize_default!(deserialize_vec_groupuser, Vec); +deserialize_default!(deserialize_vec_grouppeer, Vec); +deserialize_default!(deserialize_vec_devicegroup, Vec); +deserialize_default!(deserialize_keypair, KeyPair); +deserialize_default!(deserialize_size, Size); +deserialize_default!(deserialize_hashmap_string_string, HashMap); +deserialize_default!(deserialize_hashmap_string_bool, HashMap); +deserialize_default!(deserialize_hashmap_resolutions, HashMap); + +#[inline] +fn get_or( + a: &RwLock>, + b: &HashMap, + c: &RwLock>, + k: &str, +) -> Option { + a.read() + .unwrap() + .get(k) + .or(b.get(k)) + .or(c.read().unwrap().get(k)) + .cloned() +} + +#[inline] +fn is_option_can_save( + overwrite: &RwLock>, + k: &str, + defaults: &RwLock>, + v: &str, +) -> bool { + if overwrite.read().unwrap().contains_key(k) + || defaults.read().unwrap().get(k).map_or(false, |x| x == v) + { + return false; + } + true +} + +#[inline] +pub fn is_incoming_only() -> bool { + HARD_SETTINGS + .read() + .unwrap() + .get("conn-type") + .map_or(false, |x| x == ("incoming")) +} + +#[inline] +pub fn is_outgoing_only() -> bool { + HARD_SETTINGS + .read() + .unwrap() + .get("conn-type") + .map_or(false, |x| x == ("outgoing")) +} + +#[inline] +fn is_some_hard_opton(name: &str) -> bool { + HARD_SETTINGS + .read() + .unwrap() + .get(name) + .map_or(false, |x| x == ("Y")) +} + +#[inline] +pub fn is_disable_tcp_listen() -> bool { + is_some_hard_opton("disable-tcp-listen") +} + +#[inline] +pub fn is_disable_settings() -> bool { + is_some_hard_opton("disable-settings") +} + +#[inline] +pub fn is_disable_ab() -> bool { + is_some_hard_opton("disable-ab") +} + +#[inline] +pub fn is_disable_account() -> bool { + is_some_hard_opton("disable-account") +} + +#[inline] +pub fn is_disable_installation() -> bool { + is_some_hard_opton("disable-installation") +} + +// This function must be kept the same as the one in flutter and sciter code. +// flutter: flutter/lib/common.dart -> option2bool() +// sciter: Does not have the function, but it should be kept the same. +pub fn option2bool(option: &str, value: &str) -> bool { + if option.starts_with("enable-") { + value != "N" + } else if option.starts_with("allow-") + || option == "stop-service" + || option == keys::OPTION_DIRECT_SERVER + || option == "force-always-relay" + { + value == "Y" + } else { + value != "N" + } +} + +pub fn use_ws() -> bool { + let option = keys::OPTION_ALLOW_WEBSOCKET; + option2bool(option, &Config::get_option(option)) +} + +pub fn allow_insecure_tls_fallback() -> bool { + let option = keys::OPTION_ALLOW_INSECURE_TLS_FALLBACK; + option2bool(option, &Config::get_option(option)) +} + +pub mod keys { + pub const OPTION_VIEW_ONLY: &str = "view_only"; + pub const OPTION_SHOW_MONITORS_TOOLBAR: &str = "show_monitors_toolbar"; + pub const OPTION_COLLAPSE_TOOLBAR: &str = "collapse_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_ZOOM_CURSOR: &str = "zoom-cursor"; + 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_ENABLE_FILE_COPY_PASTE: &str = "enable-file-copy-paste"; + 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_I444: &str = "i444"; + pub const OPTION_REVERSE_MOUSE_WHEEL: &str = "reverse_mouse_wheel"; + pub const OPTION_SWAP_LEFT_RIGHT_MOUSE: &str = "swap-left-right-mouse"; + pub const OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS: &str = "displays_as_individual_windows"; + pub const OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION: &str = + "use_all_my_displays_for_the_remote_session"; + pub const OPTION_VIEW_STYLE: &str = "view_style"; + pub const OPTION_SCROLL_STYLE: &str = "scroll_style"; + pub const OPTION_EDGE_SCROLL_EDGE_THICKNESS: &str = "edge-scroll-edge-thickness"; + pub const OPTION_IMAGE_QUALITY: &str = "image_quality"; + pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality"; + pub const OPTION_CUSTOM_FPS: &str = "custom-fps"; + pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference"; + pub const OPTION_SYNC_INIT_CLIPBOARD: &str = "sync-init-clipboard"; + pub const OPTION_THEME: &str = "theme"; + pub const OPTION_LANGUAGE: &str = "lang"; + 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"; + 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_REMOTE_CONFIG_MODIFICATION: &str = "allow-remote-config-modification"; + pub const OPTION_ALLOW_NUMERNIC_ONE_TIME_PASSWORD: &str = "allow-numeric-one-time-password"; + pub const OPTION_ENABLE_LAN_DISCOVERY: &str = "enable-lan-discovery"; + pub const OPTION_DIRECT_SERVER: &str = "direct-server"; + pub const OPTION_DIRECT_ACCESS_PORT: &str = "direct-access-port"; + pub const OPTION_WHITELIST: &str = "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_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_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless"; + 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_ALLOW_WEBSOCKET: &str = "allow-websocket"; + 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"; + pub const OPTION_TRACKPAD_SPEED: &str = "trackpad-speed"; + pub const OPTION_REGISTER_DEVICE: &str = "register-device"; + pub const OPTION_RELAY_SERVER: &str = "relay-server"; + pub const OPTION_ICE_SERVERS: &str = "ice-servers"; + /// 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_ALLOW_INSECURE_TLS_FALLBACK: &str = "allow-insecure-tls-fallback"; + 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_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"; + + // Connection punch-through options + pub const OPTION_ENABLE_UDP_PUNCH: &str = "enable-udp-punch"; + pub const OPTION_ENABLE_IPV6_PUNCH: &str = "enable-ipv6-punch"; + 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_ALLOW_HOSTNAME_AS_ID: &str = "allow-hostname-as-id"; + 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"; + pub const OPTION_DISABLE_CHANGE_PERMANENT_PASSWORD: &str = "disable-change-permanent-password"; + pub const OPTION_DISABLE_CHANGE_ID: &str = "disable-change-id"; + pub const OPTION_DISABLE_UNLOCK_PIN: &str = "disable-unlock-pin"; + + // 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_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"; + + // proxy settings + // The following options are not real keys, they are just used for custom client advanced settings. + // The real keys are in Config2::socks. + pub const OPTION_PROXY_URL: &str = "proxy-url"; + pub const OPTION_PROXY_USERNAME: &str = "proxy-username"; + pub const OPTION_PROXY_PASSWORD: &str = "proxy-password"; + + // 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_AUTO_RECORD_OUTGOING, + OPTION_VIDEO_SAVE_DIRECTORY, + OPTION_ENABLE_UDP_PUNCH, + OPTION_ENABLE_IPV6_PUNCH, + 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_REMOTE_CONFIG_MODIFICATION, + OPTION_ALLOW_NUMERNIC_ONE_TIME_PASSWORD, + OPTION_ENABLE_LAN_DISCOVERY, + OPTION_DIRECT_SERVER, + OPTION_DIRECT_ACCESS_PORT, + OPTION_WHITELIST, + OPTION_ALLOW_AUTO_DISCONNECT, + OPTION_AUTO_DISCONNECT_TIMEOUT, + OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN, + OPTION_ALLOW_AUTO_RECORD_INCOMING, + OPTION_ENABLE_ABR, + OPTION_ALLOW_REMOVE_WALLPAPER, + OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER, + OPTION_ALLOW_LINUX_HEADLESS, + 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, + ]; + + // 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_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, + ]; +} + +pub fn common_load< + T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug, +>( + suffix: &str, +) -> T { + Config::load_::(suffix) +} + +pub fn common_store(config: &T, suffix: &str) { + Config::store_(config, suffix); +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct Status { + #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] + values: HashMap, +} + +impl Status { + fn load() -> Status { + Config::load_::("_status") + } + + fn store(&self) { + Config::store_(self, "_status"); + } + + pub fn get(k: &str) -> String { + STATUS + .read() + .unwrap() + .values + .get(k) + .cloned() + .unwrap_or_default() + } + + pub fn set(k: &str, v: String) { + if Self::get(k) == v { + return; + } + + let mut st = STATUS.write().unwrap(); + st.values.insert(k.to_owned(), v); + st.store(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialize() { + let cfg: Config = Default::default(); + let res = toml::to_string_pretty(&cfg); + assert!(res.is_ok()); + let cfg: PeerConfig = Default::default(); + let res = toml::to_string_pretty(&cfg); + assert!(res.is_ok()); + } + + #[test] + fn test_permanent_password_h1_storage_roundtrip() { + let salt = "salt123"; + let password = "p@ssw0rd"; + let h1 = compute_permanent_password_h1(password, salt); + let stored = encode_permanent_password_storage_from_h1(&h1); + assert!(stored.starts_with(PERMANENT_PASSWORD_HASH_PREFIX)); + assert!(is_permanent_password_hashed_storage(&stored)); + let decoded = decode_permanent_password_h1_from_storage(&stored).unwrap(); + assert_eq!(&decoded[..], &h1[..]); + } + + #[test] + fn test_migrate_plaintext_permanent_password_to_hashed_storage() { + let mut cfg = Config::default(); + cfg.password = "p@ssw0rd".to_owned(); + cfg.salt = "".to_owned(); + let changed = Config::migrate_permanent_password_to_hashed_storage(&mut cfg); + assert!(changed); + assert!(is_permanent_password_hashed_storage(&cfg.password)); + assert_eq!(cfg.salt.chars().count(), DEFAULT_SALT_LEN); + + let stored_h1 = decode_permanent_password_h1_from_storage(&cfg.password).unwrap(); + let expected_h1 = compute_permanent_password_h1("p@ssw0rd", &cfg.salt); + assert_eq!(stored_h1, expected_h1); + } + + #[test] + fn test_migrate_plaintext_with_00_prefix_permanent_password_to_hashed_storage() { + let mut cfg = Config::default(); + cfg.password = "00secret".to_owned(); + cfg.salt = "".to_owned(); + let changed = Config::migrate_permanent_password_to_hashed_storage(&mut cfg); + assert!(changed); + assert!(is_permanent_password_hashed_storage(&cfg.password)); + assert!(!cfg.salt.is_empty()); + + let stored_h1 = decode_permanent_password_h1_from_storage(&cfg.password).unwrap(); + let expected_h1 = compute_permanent_password_h1("00secret", &cfg.salt); + assert_eq!(stored_h1, expected_h1); + } + + #[test] + fn test_overwrite_settings() { + DEFAULT_SETTINGS + .write() + .unwrap() + .insert("b".to_string(), "a".to_string()); + DEFAULT_SETTINGS + .write() + .unwrap() + .insert("c".to_string(), "a".to_string()); + CONFIG2 + .write() + .unwrap() + .options + .insert("a".to_string(), "b".to_string()); + CONFIG2 + .write() + .unwrap() + .options + .insert("b".to_string(), "b".to_string()); + OVERWRITE_SETTINGS + .write() + .unwrap() + .insert("b".to_string(), "c".to_string()); + OVERWRITE_SETTINGS + .write() + .unwrap() + .insert("c".to_string(), "f".to_string()); + OVERWRITE_SETTINGS + .write() + .unwrap() + .insert("d".to_string(), "c".to_string()); + let mut res: HashMap = Default::default(); + res.insert("b".to_owned(), "c".to_string()); + res.insert("d".to_owned(), "c".to_string()); + res.insert("c".to_owned(), "a".to_string()); + Config::purify_options(&mut res); + assert!(res.len() == 0); + res.insert("b".to_owned(), "c".to_string()); + res.insert("d".to_owned(), "c".to_string()); + res.insert("c".to_owned(), "a".to_string()); + res.insert("f".to_owned(), "a".to_string()); + Config::purify_options(&mut res); + assert!(res.len() == 1); + res.insert("b".to_owned(), "c".to_string()); + res.insert("d".to_owned(), "c".to_string()); + res.insert("c".to_owned(), "a".to_string()); + res.insert("f".to_owned(), "a".to_string()); + res.insert("e".to_owned(), "d".to_string()); + Config::purify_options(&mut res); + assert!(res.len() == 2); + res.insert("b".to_owned(), "c".to_string()); + res.insert("d".to_owned(), "c".to_string()); + res.insert("c".to_owned(), "a".to_string()); + res.insert("f".to_owned(), "a".to_string()); + res.insert("c".to_owned(), "d".to_string()); + res.insert("d".to_owned(), "cc".to_string()); + Config::purify_options(&mut res); + DEFAULT_SETTINGS + .write() + .unwrap() + .insert("f".to_string(), "c".to_string()); + Config::purify_options(&mut res); + assert!(res.len() == 2); + DEFAULT_SETTINGS + .write() + .unwrap() + .insert("f".to_string(), "a".to_string()); + Config::purify_options(&mut res); + assert!(res.len() == 1); + let res = Config::get_options(); + assert!(res["a"] == "b"); + assert!(res["c"] == "f"); + assert!(res["b"] == "c"); + assert!(res["d"] == "c"); + assert!(Config::get_option("a") == "b"); + assert!(Config::get_option("c") == "f"); + assert!(Config::get_option("b") == "c"); + assert!(Config::get_option("d") == "c"); + DEFAULT_SETTINGS.write().unwrap().clear(); + OVERWRITE_SETTINGS.write().unwrap().clear(); + CONFIG2.write().unwrap().options.clear(); + + DEFAULT_LOCAL_SETTINGS + .write() + .unwrap() + .insert("b".to_string(), "a".to_string()); + DEFAULT_LOCAL_SETTINGS + .write() + .unwrap() + .insert("c".to_string(), "a".to_string()); + LOCAL_CONFIG + .write() + .unwrap() + .options + .insert("a".to_string(), "b".to_string()); + LOCAL_CONFIG + .write() + .unwrap() + .options + .insert("b".to_string(), "b".to_string()); + OVERWRITE_LOCAL_SETTINGS + .write() + .unwrap() + .insert("b".to_string(), "c".to_string()); + OVERWRITE_LOCAL_SETTINGS + .write() + .unwrap() + .insert("d".to_string(), "c".to_string()); + assert!(LocalConfig::get_option("a") == "b"); + assert!(LocalConfig::get_option("c") == "a"); + assert!(LocalConfig::get_option("b") == "c"); + assert!(LocalConfig::get_option("d") == "c"); + DEFAULT_LOCAL_SETTINGS.write().unwrap().clear(); + OVERWRITE_LOCAL_SETTINGS.write().unwrap().clear(); + LOCAL_CONFIG.write().unwrap().options.clear(); + + DEFAULT_DISPLAY_SETTINGS + .write() + .unwrap() + .insert("b".to_string(), "a".to_string()); + DEFAULT_DISPLAY_SETTINGS + .write() + .unwrap() + .insert("c".to_string(), "a".to_string()); + USER_DEFAULT_CONFIG + .write() + .unwrap() + .0 + .options + .insert("a".to_string(), "b".to_string()); + USER_DEFAULT_CONFIG + .write() + .unwrap() + .0 + .options + .insert("b".to_string(), "b".to_string()); + OVERWRITE_DISPLAY_SETTINGS + .write() + .unwrap() + .insert("b".to_string(), "c".to_string()); + OVERWRITE_DISPLAY_SETTINGS + .write() + .unwrap() + .insert("d".to_string(), "c".to_string()); + assert!(UserDefaultConfig::read("a") == "b"); + assert!(UserDefaultConfig::read("c") == "a"); + assert!(UserDefaultConfig::read("b") == "c"); + assert!(UserDefaultConfig::read("d") == "c"); + DEFAULT_DISPLAY_SETTINGS.write().unwrap().clear(); + OVERWRITE_DISPLAY_SETTINGS.write().unwrap().clear(); + LOCAL_CONFIG.write().unwrap().options.clear(); + } + + #[test] + fn test_config_deserialize() { + let wrong_type_str = r#" + id = true + enc_id = [] + password = 1 + salt = "123456" + key_pair = {} + key_confirmed = "1" + keys_confirmed = 1 + "#; + let cfg = toml::from_str::(wrong_type_str); + assert_eq!( + cfg, + Ok(Config { + salt: "123456".to_string(), + ..Default::default() + }) + ); + + let wrong_field_str = r#" + hello = "world" + key_confirmed = true + "#; + let cfg = toml::from_str::(wrong_field_str); + assert_eq!( + cfg, + Ok(Config { + key_confirmed: true, + ..Default::default() + }) + ); + } + + #[test] + fn test_peer_config_deserialize() { + let default_peer_config = toml::from_str::("").unwrap(); + // test custom_resolution + { + let wrong_type_str = r#" + view_style = "adaptive" + scroll_style = "scrollbar" + custom_resolutions = true + "#; + let mut cfg_to_compare = default_peer_config.clone(); + cfg_to_compare.view_style = "adaptive".to_string(); + cfg_to_compare.scroll_style = "scrollbar".to_string(); + let cfg = toml::from_str::(wrong_type_str); + assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_type_str"); + + let wrong_type_str = r#" + view_style = "adaptive" + scroll_style = "scrollbar" + [custom_resolutions.0] + w = "1920" + h = 1080 + "#; + let mut cfg_to_compare = default_peer_config.clone(); + cfg_to_compare.view_style = "adaptive".to_string(); + cfg_to_compare.scroll_style = "scrollbar".to_string(); + let cfg = toml::from_str::(wrong_type_str); + assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_type_str"); + + let wrong_field_str = r#" + [custom_resolutions.0] + w = 1920 + h = 1080 + hello = "world" + [ui_flutter] + "#; + let mut cfg_to_compare = default_peer_config.clone(); + cfg_to_compare.custom_resolutions = + HashMap::from([("0".to_string(), Resolution { w: 1920, h: 1080 })]); + let cfg = toml::from_str::(wrong_field_str); + assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_field_str"); + } + } + + #[test] + fn test_store_load() { + let peerconfig_id = "123456789"; + let cfg: PeerConfig = Default::default(); + cfg.store(&peerconfig_id); + assert_eq!(PeerConfig::load(&peerconfig_id), cfg); + + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!( + // ignore file type information by masking with 0o777 (see https://stackoverflow.com/a/50045872) + fs::metadata(PeerConfig::path(&peerconfig_id)) + .expect("reading metadata failed") + .permissions() + .mode() + & 0o777, + 0o600 + ); + } + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/fingerprint.rs b/vendor/rustdesk/libs/hbb_common/src/fingerprint.rs new file mode 100644 index 0000000..2d8985e --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/fingerprint.rs @@ -0,0 +1,381 @@ +use serde_derive::{Deserialize, Serialize}; +use sha2::digest::Update; +use sha2::{Digest, Sha512}; +use std::collections::HashMap; +use std::sync::Once; +use sysinfo::System; + +const TABLE: [u8; 256] = [ + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16, +]; + +pub fn expand_key(key: &[u8; 16]) -> Vec<[u8; 16]> { + let mut round_keys = Vec::with_capacity(11); + let mut expanded_key = Vec::with_capacity(176); + expanded_key.extend_from_slice(key); + + for i in 4..44 { + let mut temp = [0u8; 4]; + temp.copy_from_slice(&expanded_key[(i - 1) * 4..i * 4]); + + if i % 4 == 0 { + temp.rotate_left(1); + for j in 0..4 { + temp[j] = TABLE[temp[j] as usize]; + } + temp[0] ^= match i { + 4 => 0x01, + 8 => 0x02, + 12 => 0x04, + 16 => 0x08, + 20 => 0x10, + 24 => 0x20, + 28 => 0x40, + 32 => 0x80, + 36 => 0x1b, + 40 => 0x36, + _ => 0, + }; + } + + for j in 0..4 { + let prev = expanded_key[(i - 4) * 4 + j]; + expanded_key.push(prev ^ temp[j]); + } + } + + for chunk in expanded_key.chunks(16) { + let mut round_key = [0u8; 16]; + round_key.copy_from_slice(chunk); + round_keys.push(round_key); + } + + round_keys +} + +fn finalize_block(input: &[u8; 16], key: &[u8; 16]) -> [u8; 16] { + let round_keys = expand_key(key); + let mut state = *input; + + add_round_key(&mut state, &round_keys[0]); + + for round in 1..10 { + sub_bytes(&mut state); + shift_rows(&mut state); + mix_columns(&mut state); + add_round_key(&mut state, &round_keys[round]); + } + + sub_bytes(&mut state); + shift_rows(&mut state); + add_round_key(&mut state, &round_keys[10]); + + state +} + +fn sub_bytes(state: &mut [u8; 16]) { + for byte in state.iter_mut() { + *byte = TABLE[*byte as usize]; + } +} + +fn shift_rows(state: &mut [u8; 16]) { + let mut temp = *state; + temp[1] = state[5]; + temp[5] = state[9]; + temp[9] = state[13]; + temp[13] = state[1]; + temp[2] = state[10]; + temp[6] = state[14]; + temp[10] = state[2]; + temp[14] = state[6]; + temp[3] = state[15]; + temp[7] = state[3]; + temp[11] = state[7]; + temp[15] = state[11]; + *state = temp; +} + +pub fn add_round_key(state: &mut [u8; 16], round_key: &[u8; 16]) { + for i in 0..16 { + state[i] ^= round_key[i]; + } +} + +pub fn gf_mul(a: u8, b: u8) -> u8 { + let mut p = 0u8; + let mut temp = b; + let mut a = a; + + while a != 0 { + if (a & 1) != 0 { + p ^= temp; + } + let high_bit = temp & 0x80; + temp <<= 1; + if high_bit != 0 { + temp ^= 0x1b; + } + a >>= 1; + } + p +} + +fn mix_columns(state: &mut [u8; 16]) { + for i in 0..4 { + let s0 = state[i * 4]; + let s1 = state[i * 4 + 1]; + let s2 = state[i * 4 + 2]; + let s3 = state[i * 4 + 3]; + + state[i * 4] = gf_mul(0x02, s0) ^ gf_mul(0x03, s1) ^ s2 ^ s3; + state[i * 4 + 1] = s0 ^ gf_mul(0x02, s1) ^ gf_mul(0x03, s2) ^ s3; + state[i * 4 + 2] = s0 ^ s1 ^ gf_mul(0x02, s2) ^ gf_mul(0x03, s3); + state[i * 4 + 3] = gf_mul(0x03, s0) ^ s1 ^ s2 ^ gf_mul(0x02, s3); + } +} + +fn get_system_entropy() -> [u8; 16] { + let mut entropy = [0u8; 16]; + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + for i in 0..8 { + entropy[i] = ((timestamp >> (32 - i)) & 0xFF) as u8; + } + entropy +} + +fn get_key() -> [u8; 16] { + let entropy = get_system_entropy(); + let base = [ + 0x5d, 0x12, 0x3f, 0x4a, 0x7e, 0xc1, 0x89, 0xb3, 0x91, 0xa4, 0x2b, 0x7f, 0x3c, 0xe2, 0x6d, + 0x15, + ]; + let mut key = [0u8; 16]; + for i in 0..16 { + key[i] = base[i] ^ entropy[i]; + } + base +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct FingerprintingInfo { + eol: String, + endianness: String, + brand: String, + speed_max: String, + cores: String, + physical_cores: String, + mem_total: String, + platform: String, + arch: String, + id: String, + addr: String, +} + +static mut FINGERPRINTING_INFO: Option = None; +static INIT: Once = Once::new(); +static mut CACHED_FINGERPRINTS: Option>> = None; + +impl FingerprintingInfo { + fn new() -> Self { + let mut sys = System::new(); + sys.refresh_cpu(); + let cpu = sys.cpus().first(); + let id = { + let mut id = crate::config::Config::get_id(); + id.truncate(16); + format!("{:<16}", id) + }; + + FingerprintingInfo { + eol: if cfg!(windows) { "\r\n" } else { "\n" }.to_string(), + endianness: if cfg!(target_endian = "big") { + "BE" + } else { + "LE" + } + .to_string(), + brand: cpu.map(|cpu| cpu.brand().to_string()).unwrap_or_default(), + speed_max: cpu + .map(|cpu| cpu.frequency().to_string()) + .unwrap_or_default(), + cores: sys.cpus().len().to_string(), + physical_cores: sys.physical_core_count().unwrap_or(1).to_string(), + mem_total: sys.total_memory().to_string(), + platform: std::env::consts::OS.to_string(), + arch: std::env::consts::ARCH.to_string(), + id, + #[cfg(any(target_os = "android", target_os = "ios"))] + addr: "0".repeat(16), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + addr: { + let mut addr = default_net::get_mac().map(|m| m.addr).unwrap_or_default(); + if addr.is_empty() { + addr = mac_address::get_mac_address() + .ok() + .and_then(|mac| mac) + .map(|mac| mac.to_string()) + .unwrap_or_else(|| "".to_string()); + } + addr = addr.replace(":", ""); + format!("{:0<16}", addr) + }, + } + } +} + +pub fn get_fingerprinting_info() -> FingerprintingInfo { + unsafe { + INIT.call_once(|| { + FINGERPRINTING_INFO = Some(FingerprintingInfo::new()); + CACHED_FINGERPRINTS = Some(HashMap::new()); + }); + #[allow(static_mut_refs)] + FINGERPRINTING_INFO.clone().unwrap_or_default() + } +} + +pub fn get_fingerprint(only: Option>, except: Option>) -> Vec { + let all_parameters = vec![ + "eol".to_string(), + "endianness".to_string(), + "brand".to_string(), + "speed_max".to_string(), + "cores".to_string(), + "physical_cores".to_string(), + "mem_total".to_string(), + "platform".to_string(), + "arch".to_string(), + "id".to_string(), + "addr".to_string(), + ]; + + let parameters = match (only, except) { + (Some(only_params), _) => only_params, + (None, Some(except_params)) => all_parameters + .into_iter() + .filter(|param| !except_params.contains(param)) + .collect(), + (None, None) => all_parameters, + }; + + let cache_key = parameters.join(""); + + unsafe { + #[allow(static_mut_refs)] + if let Some(cache) = &mut CACHED_FINGERPRINTS { + if let Some(fingerprint) = cache.get(&cache_key) { + return fingerprint.clone(); + } + + let fingerprint = calculate_fingerprint(¶meters); + cache.insert(cache_key, fingerprint.clone()); + fingerprint + } else { + calculate_fingerprint(¶meters) + } + } +} + +struct Sha512Hasher { + sha512: Sha512, + key: [u8; 16], + buffer: Vec, +} + +impl Sha512Hasher { + fn new() -> Self { + let key = get_key(); + Sha512Hasher { + sha512: Sha512::new(), + key, + buffer: Vec::new(), + } + } + + fn update(&mut self, data: &[u8]) { + if data.len() <= 32 { + self.buffer.extend_from_slice(data); + } else { + let split_point = data.len() - 32; + Update::update(&mut self.sha512, &data[..split_point]); + + self.buffer.clear(); + self.buffer.extend_from_slice(&data[split_point..]); + } + } + + fn finalize(self) -> Vec { + let mut result = Vec::new(); + + result.extend(self.sha512.finalize()); + + if !self.buffer.is_empty() { + let mut first_block = [0u8; 16]; + let mut second_block = [0u8; 16]; + if self.buffer.len() >= 32 { + let start_first = self.buffer.len() - 32; + let start_second = self.buffer.len() - 16; + first_block.copy_from_slice(&self.buffer[start_first..start_second]); + second_block.copy_from_slice(&self.buffer[start_second..]); + } else if self.buffer.len() > 16 { + let start_second = self.buffer.len() - 16; + first_block[..self.buffer.len() - 16].copy_from_slice(&self.buffer[..start_second]); + second_block.copy_from_slice(&self.buffer[start_second..]); + } else { + first_block[..self.buffer.len()].copy_from_slice(&self.buffer); + } + let encrypted_first = finalize_block(&first_block, &self.key); + let encrypted_second = finalize_block(&second_block, &self.key); + result.extend(&encrypted_first); + result.extend(&encrypted_second); + } + + result + } +} + +fn calculate_fingerprint(parameters: &[String]) -> Vec { + let info = get_fingerprinting_info(); + + let mut hasher = Sha512Hasher::new(); + + let fingerprint_string = parameters + .iter() + .filter_map(|param| match param.as_str() { + "eol" => Some(info.eol.as_str()), + "endianness" => Some(&info.endianness), + "brand" => Some(&info.brand), + "speed_max" => Some(&info.speed_max), + "cores" => Some(&info.cores), + "physical_cores" => Some(&info.physical_cores), + "mem_total" => Some(&info.mem_total), + "platform" => Some(&info.platform), + "arch" => Some(&info.arch), + "id" => Some(&info.id), + "addr" => Some(&info.addr), + _ => None, + }) + .collect::>() + .join(""); + hasher.update(fingerprint_string.as_bytes()); + hasher.finalize() +} diff --git a/vendor/rustdesk/libs/hbb_common/src/fs.rs b/vendor/rustdesk/libs/hbb_common/src/fs.rs new file mode 100644 index 0000000..53a8299 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/fs.rs @@ -0,0 +1,1806 @@ +#[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::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream}; +// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html +use crate::{ + compress::{compress, decompress}, + config::Config, +}; + +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(()) +} + +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 crate::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 crate::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 crate::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/vendor/rustdesk/libs/hbb_common/src/keyboard.rs b/vendor/rustdesk/libs/hbb_common/src/keyboard.rs new file mode 100644 index 0000000..10979f5 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/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/vendor/rustdesk/libs/hbb_common/src/lib.rs b/vendor/rustdesk/libs/hbb_common/src/lib.rs new file mode 100644 index 0000000..2b35642 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/lib.rs @@ -0,0 +1,633 @@ +pub mod compress; +pub mod platform; +pub mod protos; +pub use bytes; +use config::Config; +pub use futures; +pub use protobuf; +pub use protos::message as message_proto; +pub use protos::rendezvous as rendezvous_proto; +use serde_derive::{Deserialize, Serialize}; +use std::{ + fs::File, + io::{self, BufRead}, + net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, + path::Path, + time::{self, SystemTime, UNIX_EPOCH}, +}; +pub use tokio; +pub use tokio_util; +pub mod proxy; +pub mod socket_client; +pub mod tcp; +pub mod udp; +pub use env_logger; +pub use log; +pub mod bytes_codec; +pub use anyhow::{self, bail}; +pub use futures_util; +pub mod config; +pub mod fs; +pub mod mem; +pub use lazy_static; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use mac_address; +pub use rand; +pub use regex; +pub use sodiumoxide; +pub use tokio_socks; +pub use tokio_socks::IntoTargetAddr; +pub use tokio_socks::TargetAddr; +pub mod password_security; +pub use chrono; +pub use directories_next; +pub use libc; +pub mod keyboard; +pub use base64; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use dlopen; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use machine_uid; +pub use serde_derive; +pub use serde_json; +pub use sha2; +pub use sysinfo; +pub use thiserror; +pub use toml; +pub use uuid; +pub mod fingerprint; +pub use flexi_logger; +pub mod stream; +pub mod websocket; +#[cfg(feature = "webrtc")] +pub mod webrtc; +#[cfg(any(target_os = "android", target_os = "ios"))] +pub use rustls_platform_verifier; +pub use stream::Stream; +pub use whoami; +pub mod tls; +pub mod verifier; +pub use async_recursion; +#[cfg(target_os = "linux")] +pub use users; +pub use libloading; +#[cfg(target_os = "linux")] +pub use x11; + +pub type SessionID = uuid::Uuid; + +#[inline] +pub async fn sleep(sec: f32) { + tokio::time::sleep(time::Duration::from_secs_f32(sec)).await; +} + +#[macro_export] +macro_rules! allow_err { + ($e:expr) => { + if let Err(err) = $e { + log::debug!( + "{:?}, {}:{}:{}:{}", + err, + module_path!(), + file!(), + line!(), + column!() + ); + } else { + } + }; + + ($e:expr, $($arg:tt)*) => { + if let Err(err) = $e { + log::debug!( + "{:?}, {}, {}:{}:{}:{}", + err, + format_args!($($arg)*), + module_path!(), + file!(), + line!(), + column!() + ); + } else { + } + }; +} + +#[inline] +pub fn timeout(ms: u64, future: T) -> tokio::time::Timeout { + tokio::time::timeout(std::time::Duration::from_millis(ms), future) +} + +pub type ResultType = anyhow::Result; + +/// Certain router and firewalls scan the packet and if they +/// find an IP address belonging to their pool that they use to do the NAT mapping/translation, so here we mangle the ip address + +pub struct AddrMangle(); + +#[inline] +pub fn try_into_v4(addr: SocketAddr) -> SocketAddr { + match addr { + SocketAddr::V6(v6) if !addr.ip().is_loopback() => { + if let Some(v4) = v6.ip().to_ipv4() { + SocketAddr::new(IpAddr::V4(v4), addr.port()) + } else { + addr + } + } + _ => addr, + } +} + +impl AddrMangle { + pub fn encode(addr: SocketAddr) -> Vec { + // not work with [:1]: + let addr = try_into_v4(addr); + match addr { + SocketAddr::V4(addr_v4) => { + let tm = (SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(std::time::Duration::ZERO) + .as_micros() as u32) as u128; + let ip = u32::from_le_bytes(addr_v4.ip().octets()) as u128; + let port = addr.port() as u128; + let v = ((ip + tm) << 49) | (tm << 17) | (port + (tm & 0xFFFF)); + let bytes = v.to_le_bytes(); + let mut n_padding = 0; + for i in bytes.iter().rev() { + if i == &0u8 { + n_padding += 1; + } else { + break; + } + } + bytes[..(16 - n_padding)].to_vec() + } + SocketAddr::V6(addr_v6) => { + let mut x = addr_v6.ip().octets().to_vec(); + let port: [u8; 2] = addr_v6.port().to_le_bytes(); + x.push(port[0]); + x.push(port[1]); + x + } + } + } + + pub fn decode(bytes: &[u8]) -> SocketAddr { + use std::convert::TryInto; + + if bytes.len() > 16 { + if bytes.len() != 18 { + return Config::get_any_listen_addr(false); + } + let tmp: [u8; 2] = bytes[16..].try_into().unwrap_or_default(); + let port = u16::from_le_bytes(tmp); + let tmp: [u8; 16] = bytes[..16].try_into().unwrap_or_default(); + let ip = std::net::Ipv6Addr::from(tmp); + return SocketAddr::new(IpAddr::V6(ip), port); + } + let mut padded = [0u8; 16]; + padded[..bytes.len()].copy_from_slice(bytes); + let number = u128::from_le_bytes(padded); + let tm = (number >> 17) & (u32::max_value() as u128); + let ip = (((number >> 49) - tm) as u32).to_le_bytes(); + let port = (number & 0xFFFFFF) - (tm & 0xFFFF); + SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]), + port as u16, + )) + } +} + +pub fn get_version_from_url(url: &str) -> String { + let n = url.chars().count(); + let a = url.chars().rev().position(|x| x == '-'); + if let Some(a) = a { + let b = url.chars().rev().position(|x| x == '.'); + if let Some(b) = b { + if a > b { + if url + .chars() + .skip(n - b) + .collect::() + .parse::() + .is_ok() + { + return url.chars().skip(n - a).collect(); + } else { + return url.chars().skip(n - a).take(a - b - 1).collect(); + } + } else { + return url.chars().skip(n - a).collect(); + } + } + } + "".to_owned() +} + +pub fn gen_version() { + println!("cargo:rerun-if-changed=Cargo.toml"); + use std::io::prelude::*; + let mut file = File::create("./src/version.rs").unwrap(); + for line in read_lines("Cargo.toml").unwrap().flatten() { + let ab: Vec<&str> = line.split('=').map(|x| x.trim()).collect(); + if ab.len() == 2 && ab[0] == "version" { + file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes()) + .ok(); + break; + } + } + // generate build date + let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M")); + file.write_all( + format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(), + ) + .ok(); + file.sync_all().ok(); +} + +fn read_lines

-|y6x7K?%Vr)$%@Q@gN}O4|93rc#KdwH@#NbGuGO1L5O6dC&@DrP z>hl|c>FQ!yo~+gA2&@alsJ|AV@V11BP9)!o`xA*MI&?S7TAC}`ua-$nBe;RTYNJZ2 zaoBR!WYI2aCMuZxQ;XIkCqMmoV3}CD>X?-Fp1*m*N5VUELHQ_Vk(1ZvW_`GZ=i)S` z|MEsu8~j1fp`wN(-@5!IT4x(M{z|5M9R`y*ey-t3x7F+j!2Chl114%PZA^U|Xo~A9 zV-ixEt_T?xkLv0y3|XSTvac;{FFY1|si*8yXxnslw#v{=_n+VVj&^-X&|O=Lo)3DG zIQrX7*Wuo~9DR%WCf3XU97HtX`Dhmn5a4?l`PhzP!}^cNiD~`BNfg9j+1I~rzi-6t zY|#?z_0PfN_QF2cozJlX9Eew(nkZ-bB1QdsTtyBe%olN{s|>K){#e{HpK>w1q-zT$ z{-3kLtSrEjf@#4ca4rN0J68s;dU>oyZPximLD>DzQgpA>C_+bRO;PiYTio{>{6_~5 zZb;Dm%=;Ijg-%6sU-r2DxyWYTSN}V{fLO?{+}^y+4<(8h7#qv?x{zKXBlBk-hMZe) zlWOoVH*A7nVXNxdo|Ml?`$tvWIQq384(dS+6RCgR{C7}*oB{c?)`1{-kHvAW{6j3FdhkV>BD+9oSL5~*o)a3fve?7*`_LSKaf<=Tcpo=7J80jajKW89_Jqjc+ zYv55WxIW3WJ`wwKilsa2i0tE7jgrshOS}R9fQTrFx|#%?nRnpD5>;6CpHP96yL_Op znE{Do_p$3=nf?wAkpmn7=h2!T@FrFaz+gBsvo>JK=<@@{^ES^%d?4SiDeiBLY+C8rMwv|i{?W(G_{>0$>6rg+ku_gK&+pj-R;)DduPeH|Fwxk?gt^_V;KW?=}-7BaoRY7Cc@b;^cGP{DNZ7wXx7X&ib+XwEa&z!tZ8W?s+mr zImA?%^foiKexvHbDVcMlb)z-Lg}!gPS}gPh7Yf6RFE3xz8EM=+k(;IjoY$R6Ji4)~ zvZ17-e97b>st5w7MjTK3W{z6&>%}DB0-3ChF_#MN{enaJIY zxk5|$SBK-Nl4;@@lIK+)l#5NuvDvCgN4CVYi4_@m(th2It=yL&kjje3V0UA_EH|z# zEi)k*ASX3apDT!+MG6PA);DIy{@{TAi*Zd$9fqFy0vYF=me~9rvvx$dr5e>we;#L* zJM3esY!o4~B`utpEr5A8yqO-{lfgG-7mv@6`W;0AL=dT1#Jm($@?yPc8HNxfQ z=$w|5CuYLRHD+tM!~2U)WUF)B4*<`0VpMo(u$@BEXZ}D}ddUzU_AiU0{rK(gJl`ff zudJ+Txf_}(dstJ*?{#@j@*SDA>ru&4`5#^YUB7x~m!(v3 zgGh^~K1_^G1;hz;o2xgCtAw-M3u|4vqxZrws{l!XZcP4C zS$f&0;6Kwz=}zV&)aHkm%Z0Mz>`!N0Ibxzbpb(m+rs{s;%ET$*VOgJPtw+%8!7R<= zsGs{Scy{}046uI^bk6}JQ(Y-X)$Pudh%l#d&0zk`}0Ic&*6%sqv@q%_ktl> zKB08wC*NdpuFw}L5nm4sh232kPu|#O4! z6>92~nqeSLhQoVeDpI`$pMJvoBuOTJ;x4&Eby~UwL%6EXeyl4@SqUdS+cQGf9a z&QA|L^#soPjXjYkp0@`-l>gOJm^vT$U4ADXR&0d|@+aQ4^-4(87E~tx-Wfj5^y|=K zA)tuI+Izig&xlG)kJ7*ibjS~*cvIaOZMuL z9jQ9n3+tI1>?fV+ete%~hFogq#AYE2ua<1zg1(j?L8o-NG5wdn&#`(`=1W92uSog3 zeyCKTom2(RBLxeiVekzTwxcO_7J{_=hKAx z#1mQbX)Rr4Vpw*_&V`NKrfwhGU4<#CyvrLxf#gDrY<8OAKrgqhe*3eUt&rjTQt|a= zedG0FTC1?FS+M7VHTMl1ZftYy^+ofA#l`32*Ku-KZyWXhDgzlT1I-r_+U}%Z>Rwnf zY}b9me8gK-(rqk)+sXWdi6#beOe@dXJpKC1Xint%1{15Om|f;>{_M*N_I76NA~`SX zU`(E=kQRP0pk%EyRXpn|*cO_qX&>@@$RYbKp47YlvzMfl?PkbpWwwoRew4n?G5g}A zzYPT_wAfndcH*_{_LxNWN441)CU8k5z7ZhZ3}R|yuTR@Wq*&^46p~-OHF1Ni1dCL! zc~zoyJAA23c9IbdYFO?Z1xPQRm{ERv8ZvNdZX+6pXE->V8UI>xXD0`yzwA)DWxw`I zOl1=9bf``5F>$i+UhD}w-72^4P%2`hjDG3#P(J4}8s>Zt@q;q++dN0-qHn~iVE86} zd&al6w}#)VQ@}=-I1;vd+G46H7Q<^%?~XB4z2PU#)%Qhm7w+E(8C4lP5`VAo5kL5I zJP1h|QZW<1r{<|McBevXwvrOwW?pXHcCd_DOR7xMq?b+q@C-EYbbqUQJ;$1^EpFF9 zQ}V_;Rv6eE71-f5ReP_g_#EK}NZ%I#hIxZWheg5*YmK?3!NhQ=!W-G_IdS#f80)2e zFFoY~`5&(?5^qIk*mP5L^FC~$oCyW~k}l)@zUr{->3)hJKmJr`O})d?;k@!mWcMcD5^rPm8-r+*@4F+HDRt9g%sZ)lk^VXv+)!m?8WYg8hUG%%I_HEIs` z^y;cf+i}zTey04yo80kx!*>z@p^e$XJI1rjyv9#iWnD214~g`+?7W=~ltZGReki)K zeDBL34pwd#+%BmMlSX^yiIE;{SS2%bh?SWc!m0|tXLu?}DnxC=(uLwFm{;+Kygk+0 zgQK7`x{C@l}G;*yfXLE2|TaNfo z*72JmS9~m8jjFn?Sj}l^%$4U{VDQY#&bE{sRvg6-c0cnuQ-TB_HY=;Kt$3n-FejRs z^aSxJV)_*w0y)3*%cTC`trG!tcS`nVeKqqj*~Zl2urZ%gSP=kqLYgNf;4=a}5E~ zd024yhatdxYzAJ&ZPd14U$mLi^2NtbQ(r0cMCu6*!wuK%n<-MVdA+c zP-uOhf!;t0eP*6b-=Jm}Sb9hTUe=?16Xd2T5jQ*vKEwt4(U{%b;#73MAzV@>s;AaS zmI>$l#{f*jrbgevZIGX&D@=L1~5Q!~KZwxripS6ms00VF{h zM~Kzxb_})UnT|3R?)}z24O`X~X2%cSbAH^t$60p^ya(KwEo#j*u`Gz))A2Q{GCCs= ziv_uo->g;_u1`iBU$4#Ec>>2M_1!p*^=IV=X9j-&&N8}7mNLAdM(Zc*wn?sdvih* z4(ba{Dl5_!Ue3KvKq9R!dP2QJ!8GcM$>VkJE3{ub7(1^E?@CRcm6J_<>;21b5bjAw z?1V(1Bj32yGO9}Y9Zjq(Uf-yq7f+vUUHlHOYIPw9KA--!c-dUu1l9%fgRmWGpBfti zTDSj%R#C3R421#Yn)$(ZK&EGxin5asEm|}Uov*KQh^(WLI73^JT3tD$08P9Y+Mw`o zP7~)*CzqCHI8-i(Xwee&%k@jh^<|Df8~sS5hPA~z?Sla1`gp8%EM;83(uiS#HL`MM z0?i8{;W(qGl6UAYI(MAFe(l@c=FnmBopNX+0EGsj4XdyWxqn2usoDFYQ*XFop?rF; z{)(pmT3fIWJrFyY9jEW!HDKKHY!d*67eX66G8?z}@e@u8kXDZS81p4935Ks8*(+U=VO=R9sMPk>#`bI9 ze;?w6N`?ID^m9TQs)7k7-3!pKInh@Y)?1r<55R~*q?U7)05Hc5#?ZDQ`$3DP%6`~R zh6IMADx8d( z&4k&gs9px(X?N)d#Twe}*rAMa=G=WA_`$#*KCn5e-iUO*`n^%->*HGerPuweF1SB` z|IbZ;c)>90%#f)fqX7MjyR(GQTCwKRbjGQv$Ux?f%At(2}qHKq^zq`x%O#zpX zzdNf|b4^tK&0^%{y%KKLaq}l^rWC)eq#5gGv@VNj;^4uDH=2bFURG!*&H0@*r&z@* zKxF2_27m#ddfQ;Vpiv1>>|8r1hJ7l8Enf}4^{OZcV2BLMWD~d+HpM$JRF6JyiEa$| z;qwbp9l}N*JF=S(IFe$iGd>6nbWA{NQT3Qf4ik84zN#o3;)jp_fFCq3+M-l10v7cF z@w(i%f>eDsJZz1t_u%2#x4%T&>9eCa#P+yG+c2lqss$o!`-%kv4#>`~%dS=;>KjXQ zN2PS1X9SKtU$<&f#RH(z)T>%%*kQIX{p$0B%q~viG5zko9i_F?qy{$<@adyz?%3Rk2rg1n>JovW#-Lj@Cij}f5 z@lIhJYp)&)_-MLnu?zB!@J?17x5wlbVL)Jj$4FP31XY=+%nfysCVx$nUU=&X2$wSB zGHczl&UR4C1fa&LtPPBlx`3Ebl$1*2Cw&Wr-rD2Ttz-1tQUJtz5Br%8xtxdcE>~PR z5~)mzWdh3s94P5DFT$5X9=ljlzzJ`HNX;C9((Ho(N9Y%($@73%5H_hn7dG3;A}C>sD82qV;1CiD#=--Qyh3}VVUP3vmoyLinvYlX1Os_3*rnO9bQ5ita9&;r>xM(2vMkc#z z^4!231QW}Z60bsh?q>kDxABLr^{)O0=z)6>mVm~{&=tX+^X$O!{np_seV87zE39X6 zlg9u_RZ5~|8O8!pP>!E)VmNrL*e7!*f^6{g5vrFoR86(FSd!5?tMeedSy#Y+xj6sv z{VJ&2Bsl;S#EWq3PFiD+UV!#l-K`~|y}(zl~!*XK}r>i3B!&BL&MjWt{+`=tL0d4h$b zV~D|QpMHaK!A_W&pBXcpItvDN(Z&Km5TZ-7Nk7;25I6D>9O!pb5!pbzH;gL%Axbnd zS8D(_dB-_YLoG}$4pm%0M$MsSGd8d*M(kU_ zeyiECjqq-mCmM#9&H}_644PCJs-`piq_6==E_p@_QPb9PT6!6g?kSD+HZ>C=9wrLX zpqaVwRqttpeQbg(xww*qOI4r(MfYd0uR93YObA;MjJRpXq6?J|nZ4}Q8 zGQC}D!)PUlWNSEUyq9;>SPTnfrxI?}N!8NYHe1l>+48oYp;;(!6vGTwuTqxdpl``8 zIFANS@wtyK5P;SOkE;q>)rS45RFM$5c--KDs6QYz8R7kX4up8Zk4D>=9Xr)pUl&$! zf+j0Lh2y+Tsc2L(?Zd9l#w0fXU<1p`_o4TWv8>bQlKWxZOBL~PQE2Hz(u)C)X{iN8 zI+4xwY`}YU=KDCBpv2fvQ1v<;&iSn(w(7M$p>2gBMZhgbh-fz_vw@361u->9wvl0n z@qRTZz_gIZX>!hr90VOLrb2AxvBUwF3yIuwRrWk#l^HGZGuO~y zF)6kxVF?`!CvDh%N>el?Q(b4(RNE!j+0=ptT89~ zC6c=G?3l~$RNF5SfqPTY&GKH}v}>bY0#;fLVCb1d?w}oAHAt4r!3KI?Unu zSq$G3s9XS0yxDQ_8q8DY8N*^~6&pN&;NzwENrqz4L7u`Oen$$I@*dv>M=sh6G(x3q z>)UtPMEtooX~umhCRi_N<>TF%xY*yn#+%^IWl*`sKL+sA~Q`V)a0+XNB0efyM z=(RSlsuQt{J~gO-az;L&b+yV@S0IEc8iGbstxX23)r;M_iOufSTKaTvmG~WJ141uU zdvpE)^I1`P4>rQfIJSk~6LmXwV{u1geM zvxDFo4;oqll7?m}x;QM@|0_ZY##SF}2VtiiGE%Rc$PKhS9Sq@42IWlh0~n0qAv#zL zaof%67MHcE-Drn?*e{b)1`v6T6d$22h0EJ>#DP)=eyHh#9{-jFnmwA&v*I0k{2XA` zu;^Se+AJA6ZXdj4G97juZURr=GPO&#zM6T18l-wS=0Uyo)W;2AwU!~zY8ELd5xux# zr#I}7lvJjflX`N%97T>wNil%GRT$2q(~1^o(UOg`MNx&y{(Hd@3H=Wzc{5Z7-i^`O z*2YTjem}1a$$4Hl(5ss9jqQWF6HJNk;l@2b-7Zz@$AOlKiDg7B%vkP7DMvR2uL-N z>(xaig&MW;&S&w?DR_OnSFZ2u`$~0m?P_&LQ8ng1APw45)=Z@63AywZW?s?qt%dEz@XP^~ z)}8SG22O#&Ua!^}TIqf?p)wkOk$H*JZ@i1cReT~)Q#vIEf*F`G#DNmoHL4g5mD<0? z6z^5xGj&lP<-OdpJM5(z+lGUuP|Msqcm*>?Hb`X0HJ&7`@(F&mZes@3`)o4~gQp z0Li!g5PxjRJ{DRp8tW(4K9qFo31dNN-@t%)9vdj-M*(5&KMvlX7?vI zO&63z*g$8P@Ax+|)o&V&1790uNzaWSlPfKCE{?vN&^k|WL*ar`{NFCs!h%V!%OUTl zM)w0Pr4JK&q3gnDDqSTYKh5bq8Z)#Jdiv9hEhBfdFb|renmHY2T?U)D z#XF4-Xa7>R_(wJe0n<0lp@}(M(}5`I!~q)wq}k1%TUKZ?XzlixvvO{>RBgl(2P*9j zg;0Z0;JnF?#yV?ZN#Or>v$g}Cd66>-KS|IbiwTh57#`qicK_|TNVo9&;%kJ|!?8S| zGVCk6Mms$LDBo;@^(%xK!OTVRSoA|(?d(G_tD8}<&SS)VS}lokQRI2kG`k$ zdpCFk@F~YW`V5Az73Egtvg9!dx6HF0{9CE8ttmlkhBS3i1roZRDv+Rx(~EeE>z&tO za0K!G{glKa{WIw5>t^{EaPY=#PymOKVkF&?tTw$#1{lJ#DP#=E2!(95rJ$e_4`$t$ zzQDsgeHT>s7kGukB<9Iu@!UUOs_xQ^aiDhwmEB1Yy z3db!3U@B(hVFI_sP}Cz^yM1|L4}JTD0c+3P_S~p;q&vz!CL4BlUPHY4lF-jbP#ssq zUO~BEE8Y99ajkID;hK9;f^hVn=?WQLaTMu6p^fXy@}n^Tlip)-_kHi9VyLr~pWtS@ zSph7QHQU&+gmAia>ZB35k^Qw7{Hqgy$wVO<8FwfV-R*XT%EP`YWsX0DwuK&hjr-d= zeE^A8?!-?K{Va}ZG?YO}Y!|%>!E~fnOi8S+Tl&s81R;Iw{}W#EDMSU?M`hRY5WwJ! z{_)n)#>439mvK=+kACyJ@q*b=Z{e+_w$gOaII_?sxxcIb9%{L6-^>Gvpq0#%O@|g^Q*Qc4mmM@3B z!I2Hb6KL6ndo&ylARHZ2UflP)jw%^y)CQf)4xRbKup5T`XH;}0l@)l6un5=Y(c^K@ zNSqx54{^T9&#;8`g=JZIudQIYr_7>dlmoMP0G`~r13<(U{i}FbCPRcH;mnI2*$GTr zugNX$0lD7Av5ZyB54+&WZm{aOW-ULA$Mz>&R?8OjU5>>9{&IV#T$>(V)kNm8Ici*7 zh4Trw?N|`v_F;(kT%uwssvgZnTOJTAmJ%lfTRR(tP-HV4cmn&N|KtUf$v&?t9pAvs z9}W^)+hMYL%{WJmv^K3%z7;O2G0F(&gK#S^6=;KxbOw3h4XSzEE6^lv({Mb=AEQjn zIoD<;1+{!DRs?VC(a9Vxn*{kV0goSnNAY~0;USqcw&whLH#E7;ydH)Fx)(dXy;I-m z4v2~sX0j~ac)mo0PSHLx5D_Y;VV~3-r-s@G$7wH1_!qgsVBjqSt|dcO>If^L)^pSzxQ76IY|Pr0p?-qK|#5;?|F@IL*Cd;Ws0yx>k} z6MhwiJS#j9V%=e@&LH5$3%_C52DX;GRJ5MrcZ2Sn;5p+5J|0BjduC|xdIkghoJP)a zEzs2{_ABHpgZe%;Y~cm)!YcyjMYK8SoE)QKm>+u{!-5N z&mVdXuN&NHb0drFZV*L<)bhqYFoqey)S!mpi=ex*G@ECI70gENG^2Od%Q0RD%pfz5 zA0w?|z?m-gePULpA0Q4?*?H~-$V?N4ke{!sL@eA8VQ9SM)wrYJkMi^9R^)XK=?sIv zpv#+)1Rm019?9Yk7K9tlsqHX0aw{E?s1(G+N8<8)u`F zp9y_~Zw|8eyh?t%9xjSyeuk6K=9v>|j~M+J(Z#;wfd+ea0fJw0OpPy{pD(#^t`nZ4 zV%O5(MNzn45<4&oZ(ia7fk^@PoDNAx_U=#`D{?eEEui=W#V&)_q52jBSXH91nX<1} z2N^bHm;l^7yM$Xvjr1PtWKW|@MF0aF%Mu{rag+zy1Nu77>N4;krflJh+?QK(1uXZjC#%_D%?S};$1!? zu=NBO9Mcb3_q=*o;|45HTW#1`FENS?%wb@TrReSPpqFZvl`ZfSI6nQHGKMhYl`iA- z;eBO6KM}wD#E)=Wa){FSBcaM^=^7t+_#vwD>qTa83C%X?pU(5mqF69WY_yFGt27tr zLO))f&Iv(epO|=Z5ImJfbzW)AU<6H^9V~E2+w#a3m0kWbWe5hkp>=)09fe{xfQ%a9 zNY7sw9-~}p5}mW|frs?I(oAnFbm+jZWu3XKxj6cqy>Xy_3y2pF43PdA*nApqT1-Yk zzHX>&3rAD;vkrpqD8{DMiCOT^w~X{<2wngm2hkaz?F)w*H)de;+fA%#2j=V??K*renj-ueq_Xd>9LdtUadp2y^BIP`QbkW>2nF&R(?K z(fUttBiw90!;!|HF@SN{1$gBF`{e+NW?XNPNFh8n_%~RkJTm!(hJeZJW&#*3zK(Yq zeIv10u~iEh4Xl3S-j2weIdSlh4Hv1Wg#od_>$mH$)o8>jiibC?qvom{4qSFK+62te zt~vMS8)gtcU%68AN#d6}y`7zs3i|*7*#mC?lGof5>43vVWjsp={@Q+#lt${Ub!Ehi z<#v$0L40@cJ$T{w0lmHVv)W$=Uk%T!jIs8rZ)co5k)ac(Mze#sAy^>gZN6Ozb|t6_DQl6K@Mo7UEMGKtyjxzY`q&(ZVXq zrr%`LEy){g?4I^C;*y`5&i?4+urwT6R_5%)$|D=gxAB9cdjP{mwYm!Hi5b1H-?f#u zwVxR{&x~-IRRqjf)R*X;geW*RFg$~Q_v%Xg1x+~XfHmv^*xappJA5mM_H&?D(o?wS zdFfn$;8ea?>IQ@B;{mKuo|CjCv`wuDw&C>ikC#+if;24lF&(f=q1U(uzw1J5z@YnO z5iIF+Di($Xp{-zW`{uG%*nS*C4h5fVnC?Jvs~rn%5y(EckrjS8uUJiS-VAbA-t@z^ zlwK@4>!|U`3_vk121{ff+(&2@AT0`RRecSBVr7|F$l7}SSQ_>@sm=r{rQ|ojOUr7+ z1XbcvtjxA*_?oAdfx&8Yz0gI3kr_p}!E#VWdb68p$t;?yZVFYF7yzVaAH9|M1$TA6 zJIt^lcUM*44mzjaoPl@{-sUg_fVi0BT#(_Ol@D56%P=(`p2RKBdzB?5Zv@^y5_Ui< z65=43ZcuVO%hkz{mD1q}Xc@T0P_Y6iGHr#7C$#E+svE9HuIKxRg7?Dw&FA^5_P z+Cpf9r4<(N%8ElH;zv7RtLs;iwg)l7dd-q+VO?gg9d~mJ4!FrmqGW=Qj3GQc@gg8b z9e>smj|^M4@RMr8ix3NzW46O;;EASmqC<`n1g^+Ey!H7y6|YF&9+7kK*NNIueE{|6na)z`X^TSe2x6YCtQO$`@D;|*vs}NG>F3#+if2OXz;wYBQ3Ew#wLRe+ zqFKME4%hNJRX+nt)u;6*tUQ+9g`h+(*SMC}iN*xlAgjSKh#_5EXq3STcv-o8dX|hQ zC7?B`m@;R*0|v(xIFCT)>=4%e_Hhi{{mV}Iaf3i!w152>h!R=tqwBO6TmHKSw$WH% z$QzA?!GmlzUrdZcpnL!29bdDRD!yh&S$99019g|PESQAstG-XXcwH83N<8z{6X!2pdvtbJ3#Guyb(=|NC{$@Hm^xYOcUvAJ% z$1xYa#sqhT!7zNs09{Zc20QU=00@ltGY*2_Q@C9b@HOYz|Byb{R^H)Mz66Gtb`b1P zK+A22jm>!_Ft~|;^pj9pG@NmCQsN~jL3DLa4pFfHD3Ur#ASr8&n ziuVy?C0w{e9&5Dbfl?*rMWe495Z87gt{sd~O?5)vOu#tdP}E@o@fX>PQqecTGD^gy z-u#1z*BrRx?jwl?IYm1;z!v3d$rZDMA-?NDxtM?kz>Hi;qzX!Q0=vh@3n6>)+E;>Q zoov$a^G`(iveg{m^h36y8_@Q=GHxpS!G+rTM%Nd*~ zySN$d(+p^BhW7@l62%cii^SA0rrrXCoAg1I_sTfjjjMlE$WdC+S1i!u)9$Xyh~&@0 zyD{+}VQGNf-F)jA4A}A6UfruTcU?h~{#I@z?NvM4kQ1R2@wO_F)#wPU_piO}uW0@) zHT`SyNUljeNdeXkIg`&(}=hb}u52ctwWZDTgI*QSoPHrP35J)o0(LSW zEHx_E%L}}}*?pcB#P2%3rK4rJ>H-w;3tmvj<_r!$037@K8hL>c6O3PWH17c6Xm6jc z+WYn0jYC0x2V!r!|9HqPli4R|AG%=HhHQe1lnY#I{IHy>;ffc#9w9-{k#ic3_}MTw zpnw5w7%X__7=Xvhu{@m+xf7X59pO`?@@&Ogk zSO#h+gIsqdexe@YIzwi=WCDt+{hm+5M^rO_>Sx^<)adL)&VOEocxu{@eE3o4aU0M* zrHroAwg;7%Aam@ARL!a#h-bsO2%B~^*alppxyLdTH~B#MIU-m8+eA1AoPxyQEU0U; ziRy2!Gt?@b*oIg-=Y%$Ml&??aOAD{K1i|4TzxAJCyf8YA4mteRab#c8(e*`6=)s}e zfXlZJhCdJ%NbyQ_A4pZO<)Xc2{p0o3mWk8IT@-3A@($)P-Mj!E?6j2zglu_80bGo>odT zR4thUAqx!cZ{iPL3!yF)zI4cJNAX$vZ9?8+OAOEKI39%jBR?yP6d0fZnqk5@jvV^H z2FtmjVA!d{@E`W8_osvTd!d~G?399tcH>`hr&IutcGKtG!2Nix)HcJ953?z0VOqyNCLv#I77u1O~>co@5NR~+>Yz(~#@x8E* zaf!<8=+dbG6!cNYBXnU+hECseKLOOA`>OOzGq1xVkM*_Us~pujB9tIH6G2^oGvQ|Q z6Il@Q!<>ev<1EOLyQqB~_UY3?u^G6lajZU3!<>>w%n?x5!v>?2%Ekk*#;0zR(D?PE zd<0f88_lL(+KGD#+L7AQNQ1Nr-oWnrjxobn@3&TS;imw^e% zeSDplL;JTXGM*KRMbqGs7}->?O+jX|8*mYe_WIl)r_PQ?75-GYxSKZe)POch3x(;3 zrkFrS-QD)XR**^P?L6~#P{+Ce{d_zWHh{}4g=HN8j==o45Nr2#8rT8j*4HgpqKeNblq)zo40_|o=_HLf zhcg^gvKWz`bN^OfL&%$3`$*G5#HW~WzL>#2zUBGlJ~J9P5yAE#d&G6*0U;~JVhUEu zR9$x66NY{p#BJ>XCnD6nRNl@OPM9IWDY$k3ejO}HRCoh&cojwnj`H=L*~(XJTVO>= zCP~iNv?oOxV`Mvoa0EXGztT5KY8qM%pl)?vQjmxEh9Y9{3Tyyf!eqEB4fS{>{#VWh z{NB>`^o>QKZcB3?uZC1C3z$L*?nZQv(aK!83WA)RX%{ar-woqJAG#pe$bg)!X=6(` zqX@fX%rP=q4=dLFgwoY>AkWFH7o}}%3sfbxo;#;mL<^p|2lbOwYoGa8H^`O(OeyL6 zotFt-qV9H&Ue)wKA-VDd{1^`BVMc4Ba2zD*>+5#Tf@7At{XQR~x!AWCX2`2MU>kxe z+@&m#WRbUDzxQPyb?a=skuOIumv_^Eu~G+?C=JD775-H2yWE`j4+HQSlZ*rE9BW?K zmVSOx5|=15dVCTec@Yz)k90E);^|gW=)1e^&~J1n#ic6@b9hk57{vpBh}1lkQE^}c za$d+$zMr>mqe z&4*rsDv+L6tE5a`jMaagzQBPkq}rcEy*QDIf%34nQ0FjP_{r`1`_vP5LGaf6fc%Dr zd31sj#80kt911c4ucykIh)vGlY7o8BaYeh4m(BcLehwZSs~|Nvxtssy=Qk+~$1y9g zC5KL((wTdlmKl5KaO94egbHjX3g}MT2Yxq(rIxkK`Oiv{AhC~9_{#y)Hq$|Hz@)J1 zdKG#jiqMNTKO<>-#&$d+AAKKN6#n4A9m+4|SW`wU8?;1npFG4ST0gjO=JNI~Cx$2O zU_)yqwLubFuu9^6hw7b|*AQYK54&R!x@7qX0dMDQJs<#P>(?R?x^*yJcAw^Ht#?T6 z{ZWNfp0b}3EAk=4AGh+h#vkpb#Vn$ETMLXrSATt5i_k#lhCuT8DT{ls_f={d+G5rf zQBed>A)LeC{-`FP44I9NDWO3JD_KoqOiij9Epp{+IDefx!P*;LLr*bKSUslc@kpP| zcpp{PUzvMmDnm44M$W+r)Z@+u26Q+DB4xb#4N;8Vf&7W%MktY^kXq$o5*L9ZzXw6k zqeGFZ=ydf1cw!DsPU~=e`s>%-O)MDSC9jL*nUKebiPC*Mwut7zt@>-oo`GPC%1+g= ziU7!?1wfhQ$udBRe>D+IohVN+WRCq4_u4CyCyf@N7oPEKi0HJPzlWUGB%)@t%i{rv zbK@_c%En0Q>AhIqNr)aLvl|3SXawxQkhZ#4{A5dS2BtRt>a!oI%SDH;AG|pKwdv!H z!*$!C;yAN>^{AdzDFGw}tF=p4O+ZlB5<6icBaWeXob_TCZ0r71Fml|oB8bk%obaPl zA5puw7CjdF_>D)}e(JYFBU&3bIIpHYIS+Wk}Dc@>T8`6vF4o~zm@%* z16tDg=Hp*LUwaU1D&!PIpxX!r6vj`^mOjCV8qF;dg4X|aJPnhTEKH7`bA$x8WLiYx zX2eQUt8np(QvrrkD1&PgEs`61T=F~PhUQ3N%wef-J3)g)mU%pq>ROdPi}}CG#RDM7 zU9**}Nk&LDHAJR?^&!6pLNWB{8xP2$4k~G0++q-cFEcXBf<#hh7Au@1Vd6-u-?@Nn zvj-Kv>wq8p{7}GC?cBB<($txlAFDA0_GDf*+8ew!En$W=%g(7`McR#IHcaLuV&gUC znu~VW0c5`Z-|5*>cFg@>o=VlLJDXl90E;Q*pYircHT4K}sKomRJ@Ql0$T4VDkQzPp zVQ;6{9avyoRp0a*>a(UDRP%h<#IGciDjmEch@J6XneI~+`^1DSZ>0opXj+$w`Q}uB z%=dLm7MqTu9zoL-M?Ag&gHeCB>!#S#R6Ddfv)37i#5LXSj_C@g_YcR0iup0GXa9r} z;J_kbE)iMRoV!dG*IR_h&&%Z)v_x%~QZqNFiG!dgNtV}80XdPn09mxs4ue^>jOjgT z{Sp1R66!vCM}9I39VmshCPlq{O#)qzsu9baS$Gg%r%#qvwbImR2kEbuLyxENppYAN zhWA5NU<@$5cOT?}6c=wdIy4%R`#R|fCd=g+Zohwa!vuCXNMbb#JIYh3G-TmG{FmnL z$gbEfOH=prZ4H9e%0MqupcjnB+CvBn8^W5lPckrI2ezHq>b>}LP`K%ZC0OFx;vYX{ zR1tk#a??g+v1-P%#<esX!l=8CJh-kOq6i&5d96dSgKbHVx{041 zQT~r1NW4r88@812MHAD`EX;^Pt#NRgs6Kz`H|mstdowa}%N_+CRTMR&fjQ(M7!P;w zBJQPcwyV<;x2^ekJNA`B>ed0+rz0CLs1a+rI=JZ|kju}soT|Yh*{i>XUooF)JX%O z>O^CjrjKZh)R#3k%=azxk&AZb?AwIQPri9hoeJ!_gYbGRE5mGKn-V8=5Yu4psjhtR z;zaNa3QDIY#|?n4>nGE%JgO+SbP4S`t=&=dB{PGk_8MFW*s}&k0F+BChs2<52J!q zr=9P(*$_as?lE{cr%?gs zslfEQTFDIryw8<3|D3UiASd#4o}$IEHiQN?KZL+$MaAt;R1wRXd-Xl&jEql%sF?op z{RJB0t-{pTE9#2tLf0TlucRmjrp};Y)GdlEb+g^Tmm%bYb@nElsxuD#M2lRH@E$!< zoFTTTj&#%7lrd1I3YTDn6f{iHDC+A$RmyS*&sF@2Bb*T_DfG+~n^i}IaAzd&2PgJH zNRZxl3v22eQ2h=PQyj~s|3ps;`ReyVLW?vrGlq2dwY!fg$3g(D@KBfJX9sBXh~aEUy}Lu3t_t&ZTxDsOq@ z4JMqhL>`+uhQk*}Pm7;o1ms@`I#6H^T73MT;mSPejII4jzFYj7JtkR8j*fS{|I0bkK- z7;v(@G54S*0?DZjd(S%DGV#KzOk#!7dl(zEcV-1|Vqri|IctsgplOuO(0>%0kG~^j zzyTGj%XIW)Q*^II2bRm2f6-oz4i>j;{C=ahs?gjIVSK*9YWPD0p%~XcERj9%eZAI= z<=LAFc+gJt=|di)*78`QpNVeXMR`0z_Ap5g@(QG8N6s(4*t8P|DU%!#ZLX51vD{X& zvG`?K2zr|zziIZ#B*+)bx;#8>b4(pii!3{2%VR|*Ua1jNt?8}Ml*YlJgooutebfr2 zu90U_GB2JGP(jEqu78$Uy@NanWnfx{QZ?k)nQ>5nyq|TAT47r6ET1snZiLJ>6lcH* zjl>f8$!=Nyia|Mxm-_5}cFw1smqkcI-V0jGIWyq`?@6fxmC3eQRP@ghuISq6zs=Rb}xQWKMF z&wt~2}<e9bmV#Rh_h%St6(KKBaWfYVVPZ+x7+0ek(ijv_gH}ZX!FsMOiaK(_ zLe~@002{w6LGE0rG;aJNmc;=67JhDKp~EKsSUUOyNm&KF$=wv7ohTwjo`@c@Dth=$8U>GPZ1I3C7YFu5L3(q^M0 zkmRirh#InzLgBGoX2Vhhxe;C;<;}*Yg3}~cK9t|I2|>Po2vQQ?KLuUm0;+q<+TI&P zdO9Sw($`~j2X510yaEr}Hu+9wTxig?7ALU8AFxnm?%{5zipUJcU?qgz1~q8+WiwDl zg*A`j5OTXf8N~2j%HYNj_174nkF*jMoa`S+r<9oo~j)B8cS>yN(mv$+TLc z@ElgaOr?CBF}&(TgcP1*Cqis6J|1;Qjd<)fBKqno3*74!r1bdY$kB1iqjl(&j0>8& zD&bJ}=%?*CltKGpOKWrT{7^zp$iH}m7+pxRQ>vAx)~!)diw)N?*)6^sx**c7C7p5C2kRR!AYGsx9B*$3U_!`eL3gogh{y8WnSp%_2%^6bw!UsGF|V^0qNLUft0#v_*^7<~u{JgXRepeLQkyf~zKTP7xZ6bjxc}#vG@NY5!ou4!*9} z)Cy~nGWeo%$De#Q(m+2j+!fwsc#M|e7_CiLXLF_qN|3`iLC1+}*H;B7fE2tHHkbIw zeT7zdXGmMuWZiEM)hZ0|GV*d4{8DQ3zGOEYwx$;bD2@_BZ%C*N1s1Wx!63m(}diGPt79iT!XHxo#Ivq2R7FYHwlSGpy;8MqTl@v*UvY!p}$dPM~7}N^n|5#E@Yt)WB8Q{VeUF zoaO9ri>lC5>zdq5T7O-IH$`f+0v#Bf{eLO@#DECD`FEC9wZ)&A zn+u`NyqI7zJ5}-W7g{<+yvm#zmW|`#YqCKBh{`Tm4UQb;w7hLICF9i2`7Z~qOX9U@ z0wJylwgT_JNeEShe71?4kzSkLJut?1bEvO}3X<-$^UF`QPiug&-0Yi(yC;v&7QLGN z7!2#yIL>)5SI=OU|xh@W^ zC^ihwpU{4;sW4}wJr=Dy6zA8lxcV{MG1A_aa#Bj&;rQ7}|me>4DOkX|El!domM z)%t9tPivS1?e5C*fG_?!F>Kr}w@L?oNv!vajIU`VNaH{Z=R+RpIQe8+@)PA(O~h7v zuWM;@t1SW`wF2lmCW^bpDMa=^qbK4Q$_Y5kt0uHu4K|u zsLuycSqB?yCd`^`hN23?y*4vfak6fQ&Np6sohIE_RKJ!v@P-xFVLzpIyyffb?2JYR zl?MNgiJK0##XdHT*xVk*v1@9?vr=l%X$@UZa)rscA9%`8yMWWQ4>_QfQ`$GhrFXiadaTDd4AktnR3DigGT?RKUfsJL$AbMuE9V8~Tkq3(f$V;> zqM(rAFsVr#@YpPSCHN`Rr0QUvV$c5K(d){UaCd`zMZp)j7d0E1Y;WJ22@E)yuW(h3 z)Vg~lQ}j7QEcTb#^WvV?=?zGj-=p4zXZNJ>MYbv*`{mS}&(|22pVjc14U1`Eu$)Lx zHKiZhbX)%R?Y(dMwaL;Ct!-oln7LQ$v?cKSus>fnD@EdMJn>^1t?&60lj_8pVCnbb zw$@jo*dX(F4u5oU{DIl%X@a%oVaADy$iiGs@uw+j0z&Q< zVhR~C2CuAGkh}7DH6J^RTczofvj1G%caiV;wI)`4NsfZ*$aA*FUZpt>wFI*@-E_#XzUU%rJ{yY`61>nrP98WlI_0WBTp8W=b*n# zUnyj*Rxz>7wr-)Uy!1UEu~=x@eZKB|OwPcuW2$w`SMiVC>IcH78YFmGxR;whA6i$5 ztNeq+Yie-#N^+dOOMK)}V@B&aQ9F1ipnh+XTHx3w+vZPl%Q_PHR-ZDT^?c!d#$g^&EUAYn=PBsUb;V{U`vPFG%$FKSBkP7RWo;yw4@U* znSvP8TjiX&D@KXkU&M=C?mw?Cx8EW(snV|vdM4ReD z*&?SGnx)*h0XxRGg3c*Y@gw@7Ee85KIPd$mzM&<)30fEHvu*LHo@~#XBPPQ=WaCqQ z+Y={l)yZ4V;vT!~Ui_9jb>+Y{tzPc6Ii6|mQGN?%a#wprfom#*%t>1&TVZQE)y44{ z&n4H9!Mr3ByqXbar9Ejn!<1vFT-Dx74VLvONAjuAG*|b!#hJD_{QBlo&BVc>Cve#C z>fRoUv!>?O%0o4B>In8nJZY37YskGDtdWoJbO|SIfq-yZU~KkAjkzwCc!F!ugdk$!SWobu6D zzLilk#r8(NhaBweT}b3g-OBF%sIujujDm>w=NLEdy+l4DpNeWC10~;9k}G3#=IwpR z?c0O%dE5NdiCk*P0S4c5xtbGWxb}=2-gZXEzBY0Qa?LK3Io}!xwj-Bin5>(mgtKa> zH50~wCR=8;(&|p!ukE;3is^afl}UxJZQrwoFKb==y!&QGm*lHbwpxdzN>)}m1$(u~ zQ>83*q;2VNTx7nH>bfY&O*3qm8hG?71AIh54SO_*Hl5k$8?eH+Mrd!(E z*Eys->sC^n>Y6*%+6-wwy3o#d{iHp;E^^Ti;J7W*Gy_ipLf`oE%EZED*C(0-n_@oQ z!b@3fdTVG+DgQMey<%m&C-p1cmiCFM7REysk0&URSJ~U9Yn*=^SZA9B>T?WroEbK~ z4$k)W?T)1GlS}g^wEOZHt}Nw@M)dA2=T?$w>gU|7;9uGnXLPfM;)mO;Pk+L6tE~CO zm(^^W2*sOO`vR}$y<0k8@v09;GpAudDJ}iduPT0|$lElql2Mm=B;4+Lw9A6$)Az$Qiw@TOOQpSC zyT=>sEBRY|l4=GEM;TkWS4!I5V*;Fhjkiyn^jWrxs5E$3=<5;&U&uz!~b)?z#Aq@=SlqlXSV;o zB16snSOwbo(x|1dGh2aqnsnR$-JF28K&hfw4Y(e8$(O?p1T8tSt;rt@%8)@@L(sPf z%I>S^&*7cIz)k$Fyc*-``CI>csd9~hvQelwdUFi721r-ptHtVHmqZu};76}knovYZj>dR_KD%&qEdM- zio@g`GgFe>v9!1k74v`Y)VRq<1_k_jzZ)f4Ds*iu>gYD=Crq(6w2!YLA{s<)$1&6# z8tZ2Z&MT}&UH^+EsEA_Ex=^PKmzVr{wDwaQE(QQGAhbk8X#)}I_WrD!Nbh#$F_3KA zojR<0S=mk_;K^?gCNfY@S(u3VtlAKlEeAM`tZLHnqhDj}0qta==y`8QGC`F^nzT5K z|Jyb)mwxhNAN>&{@n2t$KaViZv-?mp`2iQ1jwehd+O73~Mw;?qB%h9>9EJJ)x+NjyL=1H86w=%hFIA z97q9lqnojR?7%O7Ug+GCq6K^g1z??ovK_&L(hP<1%geHzs9(X}{iP9>yNI&0hV-*@ z+yoI+_~Y*qDoblnd{lF1iq(Z-%&5@Ip(G^!A!=owb@hGS)GkVVgc|jOJ4knKNjU40 zn?TM+R8SKs6&t7+P`-#Kca!tdCw?q^lOwe;0BLIp%&236S4a#EN*xp4cd9@?Ix3AX zc`ve?AaC0GKhFnX*|fM4TL+<<*>%paj}V+MdclkeC`mF^&g+Gd`f(jFxm?+Rp~;E8 z2rbA9?THBPvl=nuZ09E^0|iQ4s8UAXAb8A@@2U|mEh~2=^I=wlU08$AQm{Kfz=hC6}3pW($?p*^EUy(GP>ZLX4{IoV!U5KR`ZD<#10{#T zOMhRb6idfIaj~AMXK$v0yuJCOaN8^K$u-WcR3~?_BUU}du;0gURyn#X<+(TkqM|DFA z1DFU9wnk_hdiP%vU{C5*pS-ax;&EMW{%OJ=xe>= zqrYt1rT$#`gWALaE}YWL<0r4YJRgT@q54kc(GgtD`;YfHRfaO@?p!?;=c|kQLT;~4 zHUv{LmD0>N%so*iZc`N3h|nmsr8VC+&%fP>Wj`NTG+Ht$c|lGGy<@K4fVb{H{p}T; z6x}-TbGd#kN(lH|99E2k!eq>roD7+|aIwK<1w@4yya8dzT)<(!E=%BjKi7%N&XyeQ zI*yX$*F5_l#gOl+5GO-d%vMK_Q2~NeF8o@HEc-nuBj6-4`7rKlg3u6J?F zrNQ6#TkT`Sc{9ZVS!piT;^Hr-a!JtLhHmQ9AXRL*-@JQnL~u&IANAb}y_DR#j-rIj z&@&=nYN=)q6}b?Hp$K0sHBbD@LR5&1-Al@HW*6h&oJL@1v%6iz%8_G# zMiYOyuWETx-DQYzOVgHO`$uLpRgk=zPdK0opfjI+nGP4 zq7D>>;M}*4N^x>1D-I>&{{CB#vpiJ4hyW&Y5UZ%->nNQVc6E?Oe6_imki~2J;+y{& zI+cAEFGr6wg@LKq-kXsgsfQR`HL7Dqa2U;fLThVRXM6*V(aXi!-aK=kqeogh7<#B) z7QsUgrz8lux|D@){w9p_-1x*mX~ryS2laERaNvf9=_&2fuP60T!&bIXq$ld#`=4GA z+WBmLdD$Odj}9Naj!L#*GMOHcV#d$^@75`%o_3QN=Ya#p4~6l#yI zuML;hq+Jgheh`kf{cPdK*|}@~6CXKnX~Ag8>vDm17-n=|Tz^UWWV2E~M#(4E$h?U1 zb7YTCF>*(R*Ex0U*MfZZ-#ySmF)k)a&J9(vonDJ#nD4Cn*#9VC>d^@qx!6Rr88wvT zh|U$?UrmL-`zH|rpYr4T%EC<-uLKKgdvGg3Zz96u2QHB&l$r3qvFzK1D;c7JP?h@$ zCOj&WtnH@lU1e~^lzeX8lcAur5OIyew;e*Nx!XIp@s5A%EQ(CKQtK;IniBA>WMH^} ze%w5=wnAY4C*-s?QNZC-nMZ~go(&g-XdwJV=GY^AWSn2F%VZzjuZ;;RB#tBGUD~$= z%%%jm{G*x443@Nq@ei)j4w`nt55wg)r>_+cw(}99j*#WFg9KK5cKA9H*TGPn{X22y zit*9RE>_z21=2ntRr~)TN%;R<<7GJb4a2qGFuA07M0RKue|Gb=?RyD2;$%MU#{x~P zJ>TVfh*{dA|D%u0KpCo&y>wY?|5Z})YoWF!v z4Jqx~5dlKz1>(jB`w3^>?p#&in3av|=ldU25W>q7* z_kRXc#Y#oTC7*fCJe;4T@-{p^Ra`|{_=Z5I+>Wk)>_MpOf>3GTBk%DWc}uCow{N<( zX%E5V6451jH;H9$?Z7#i*<*i^HhXz?>~u}c#Mi78CBt(9L$9__tcC9jIMk?)6-YL_r<;Md8n~%Edo~#UAG)UtP&ET{xlU zrN}^NTRHs^+yiUceTSAs{Uy%Z@1ek3%D@KVp`sfCUXneVudnw4 zFzrLkJ@dE^ajT5Xq5_11?+dt|i6TD25nA7=VY0dPgY*AW8F^#ZWcNMd|_`RIH@liClp0;!RC6^esXshLSHx&5@DH~?Hm=X6c z;-g}O`at^pWk;D_($A3pnO|$hSsLY-Q=db0O&obpnK&wCMpHU7ZPk z%lp>cXXkoFx`&R85_bfW$NTYQTZtzE`x&oP>CShk@ROteXxj6v*~5~Wip!$43IYV7 zVpC+EuUeZV;lcgXRi~%NB^Z&1!jTxo^Y1gc;syi-aD?Mii=$O^#J>9iIj!=zoVl_Q zyLG`^RDYYQEcIeQkTy5qf*oj@;WR+N!w8%;A!74Kv#JraN!AaLI zKYdk5jW15(o|tSxJZgmn2rkXRxy$U0@6`*@lbg5W z)6__=?O*X^^=x6naX;K}G;y~0w-@}yM^0TFkeMjE9pJ>6Ek~$dkl&10#8+#PqEBxP z;Ng^+UjFr`YiC;J1x$C`*q1P-MN%rvLayv%YDg3yoOM1tcy(SY<~W9w47o%gh#p27 zXVXQSFTWhFNeL0|UQ@M@MOdtlkm5;VE{so=_FQ?Fkid$uXaL=0>tJO6Eos(#zkiN# z+!u%lNg{qp$E#Qf5cUcYddC+iah&+2XAbM8=)j3-Egu`((dD#EbV!I+l!M10O!g(HzFCDq!Ut?GHz}jXDCW`nWCDmG*F^B z`(?6_KsSCil0+{+=xO^`CG=gZTZ$T58;;Mm@dO$D2=VA=I192_RhfAIZL?3tOnZ(|9Lt z>nN!&DsJ0Gil4tkwzn6SrBX}0e)pb#dQ5bt zq`}2-fzGWLW+jlP*94O7zL?UmcJH)lDcrv9;blrpqAh|$*%XDID;*=9P}FzSMLf5W z;!`976kWY`Q=S~%zZ7k>bzQonP5U(q?Khb&Uip%hY6NTv&x9y$1CiYoNKS#__VLiQ z?$C`z!_HW@Fw}-zAxGH~X{qy+A2)Wk1AIyz+eZm0WLkw3&v%UY=|JyYADS30Lj`Vh z>}A895yg*oLKCT*pLzs*xvpdM{lP%EK(dYzo~-$8BY~7ewd+& zNxmk-F_=xR++=Alp|G)JKh9Fra%169QoLmQx#?_jOeLLpOS0HH3tY{6YglioH!3}( zd`VHU%p;H#R!dF#peGj8Ymo#3b`7qb>Jci(XQYVXi#kvTe9NPHs`?*h#b~X^Zv^kt zbEuNyzp5lA<6>#5fsKf>=ccE>Z@lK=Aq@i=AR|ml6(kZoe;(99&emfLFO1 z9Sb%cgqN<_WO~Kqb4$9OmtN9C%nOiHy;N{Z}v4`0m7xO~-A`^nyi zSV`ZDCqE}k2@r&L*b)~;?wK&ib@R?w4?YN4F$&3l&tN#l+*A9NPDw%Fdydj4fX1Y!AhqH2^f!jcpM6Nq0r?|fM4=@pn|Y>$0z$;Ko99@Jwzwhywl@XO|j6qk6HWY4j$^N3pElTq_~cqfou!wO(X&1 z zlsoEI8s~N;rG3566nBJ-uw>2AMeyQg-wJ%sKfvGBCG?ZuP0WImV=6UjyMC$o^Y*y? z(z<$Yt&Uo@MUqn-?#GWl6(B6y+tU)SUt^TuB#^TAZ@wo*AAOoVKj5n6@>6nV^0jLQ zi8UXtL$dM0z>NpW-K1ZAVH=e>!YB#$J4)uehYlZp-buWXH>2z6GWY%IDNdKhouW^# zi~60Tm3bxj^QFGLpm$l(Yo9MsKJSYP4Ri~yiH&Xe;5%9H-PiimyHHJ`WiQ`%wjKhe zo=(Tc%J*N7(Px(v@f$x{?8CZ2Fk{ofZ(Q@+a`54*7W$)S z`N=NcFUZRR1swdm?s8T5IuBx>6xJAkzSTP8k_jJ{QGY2WZ?Q*4A1rQzSFP?c@ZwXW zG71)#yu}B({A_Up_97o%yrzYJPIu8HH(o?8KY@P}yV_sr)9mion%)k-u9yhBaUz@v zVG=9oyRdBkVtSP(2irxI2#hyezYY1ZB8oF24Q&?y!~jUOid#Ws`)JIG{agE1<^%zYmjg^%G=)xHgnH z-LC#*2{+{6gA#4{MSkxWzRU3^^0Ey!Yv4wXJQLw+Zp0YI835@8 zuxQvc#)cRRq3>dyU4#(XsqytCI72r>2;L+f zYL||f-AYX1+yO6L>(Y{4FCt%Jlu!a{_r}E26^bD#xq54Y@O$vzwfe2euGYwjXU|Yf zoAq@PQfVxIPM<&#N%<&LcU0pHb51QvK)WiOe$fxL5(;Tz?e0xO7B2uS$U8i{3+?J+ zzhwD#(pWC1qZ+TJZ%I7TyM-v*aCRg7j2sP?xP?1B^iVq{UJ!+?X?T;&hJ-tnPwUvB zs+t#Zh`ftC+&@NI)zgFD$XcSJkriBr^@K(MP=KNyQ}BX=pwzKBl=1$5L{FMw(kR-;nG~%SQ+nJ z^wc7R@PHS_y=n+H@UqT05YN)m0>3ncnKK! zDY=E%G_g2Sh%%5cJy38CzHlyZTuDI@>|ZWPw$ zPjk54(`UF?&ZZVS*nveT&kAfA*%8*Js^O2oyrmrXX?r`5_-+ z>51uQ?5QAcxQ#M0Qk(Mm*pUKg^BZdl0e z8})%2qh23UYc-tbRtFE}?G@h!ZXuo?{4|qwMge3-{IF=N_!@mQ%9CSQ*<1vjO6GZq z>0Ih_FE*s$VsYG7VSqaA;VIHMW@p2*ftY@F#C{6o^jI>gsi43SC{6J`CDOP@G7a#D zyfkR)vj;jqc#L@yRuXsFbHLN(yynIk^lATy|IBJz0z49|)}QGra0*V)p(nCaLfT%R z1l5p%gwTxC#{0O~{vqL+kBeGkCm{|Pv20nqqWGd>Sb#>wBTaHN;+aJknI_%ZLnB%*>xL+C0-Dmy);*Eb*9wkwtn-i_37c z String { + String::from_utf8_lossy(data).to_string() +} +fn replace(data: &[u8]) -> String { + let css = get(&_COMMON_CSS[..]); + let res = get(data).replace("@import url(common.css);", &css); + let tis = get(&_COMMON_TIS[..]); + res.replace("include \\\"common.tis\\\";", &tis) +} +#[inline] +pub fn get_index() -> String { + replace(&_INDEX[..]) +} +#[inline] +pub fn get_remote() -> String { + replace(&_REMOTE[..]) +} +#[inline] +pub fn get_install() -> String { + replace(&_INSTALL[..]) +} +#[inline] +pub fn get_chatbox() -> String { + replace(&_CHATBOX[..]) +} +#[inline] +pub fn get_cm() -> String { + replace(&_CONNECTION_MANAGER[..]) +} +''') diff --git a/vendor/rustdesk/res/job.py b/vendor/rustdesk/res/job.py new file mode 100755 index 0000000..e53105f --- /dev/null +++ b/vendor/rustdesk/res/job.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 + +import requests +import os +import time +import argparse +import logging +import shutil +import zipfile + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]", + handlers=[logging.StreamHandler()], +) + +# The URL of your Flask server +BASE_URL = os.getenv("BASE_URL") or "http://localhost:5000" + +# The secret key for API authentication +SECRET_KEY = os.getenv("SECRET_KEY") or "worldpeace2024" + +# The headers for API requests +HEADERS = {"Authorization": f"Bearer {SECRET_KEY}"} + +SIGN_TIMEOUT = int(os.getenv("SIGN_TIMEOUT") or "30") +TIMEOUT = float(os.getenv("TIMEOUT") or "900") + + +def create(task_name, file_path=None): + if file_path is None: + response = requests.post( + f"{BASE_URL}/tasks/{task_name}", timeout=TIMEOUT, headers=HEADERS + ) + else: + with open(file_path, "rb") as f: + files = {"file": f} + response = requests.post( + f"{BASE_URL}/tasks/{task_name}", + timeout=TIMEOUT, + headers=HEADERS, + files=files, + ) + return get_json(response) + + +def upload_file(task_id, file_path): + with open(file_path, "rb") as f: + files = {"file": f} + response = requests.post( + f"{BASE_URL}/tasks/{task_id}/files", + timeout=TIMEOUT, + headers=HEADERS, + files=files, + ) + return get_json(response) + + +def get_status(task_id): + response = requests.get( + f"{BASE_URL}/tasks/{task_id}/status", timeout=TIMEOUT, headers=HEADERS + ) + return get_json(response) + + +def download_files(task_id, output_dir, fn=None): + response = requests.get( + f"{BASE_URL}/tasks/{task_id}/files", + timeout=TIMEOUT, + headers=HEADERS, + stream=True, + ) + + # Check if the request was successful + if fn is None: + fn = f"task_{task_id}_files.zip" + if response.status_code == 200: + # Save the file to the output directory + with open(os.path.join(output_dir, fn), "wb") as f: + for chunk in response.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + return response.ok + + +def download_one_file(task_id, file_id, output_dir): + response = requests.get( + f"{BASE_URL}/tasks/{task_id}/files/{file_id}", + timeout=TIMEOUT, + headers=HEADERS, + stream=True, + ) + + # Check if the request was successful + if response.status_code == 200: + # Save the file to the output directory + with open(os.path.join(output_dir, file_id), "wb") as f: + for chunk in response.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + return response.ok + + +def fetch(tag=None): + response = requests.get( + f"{BASE_URL}/tasks/fetch_task" + ("?tag=%s" % tag if tag else ""), + timeout=TIMEOUT, + headers=HEADERS, + ) + return get_json(response) + + +def update_status(task_id, status): + response = requests.patch( + f"{BASE_URL}/tasks/{task_id}/status", + timeout=TIMEOUT, + headers=HEADERS, + json=status, + ) + return get_json(response) + + +def delete_task(task_id): + response = requests.delete( + f"{BASE_URL}/tasks/{task_id}", + timeout=TIMEOUT, + headers=HEADERS, + ) + return get_json(response) + + +def sign(file_path): + res = create("sign", file_path) + if res.ok: + task_id = res.task_id + + # Poll the status every second + while True: + status = get_status(task_id) + if status["status"] == "done": + # Download the files + download_files(task_id, "output") + + # Delete the task + delete_task(task_id) + + break + + time.sleep(1) + + +def sign_one_file(file_path): + logging.info(f"Signing {file_path}") + res = create("sign", file_path) + logging.info(f"Uploaded {file_path}") + task_id = res["id"] + n = 0 + while True: + if n >= SIGN_TIMEOUT: + delete_task(task_id) + logging.error(f"Failed to sign {file_path}") + break + time.sleep(6) + n += 1 + status = get_status(task_id) + if status and status.get("state") == "done": + download_one_file( + task_id, os.path.basename(file_path), os.path.dirname(file_path) + ) + delete_task(task_id) + logging.info(f"Signed {file_path}") + return True + return False + + +def get_json(response): + try: + return response.json() + except Exception as e: + raise Exception(response.text) + + +SIGN_EXTENSIONS = [ + ".dll", + ".exe", + ".sys", + ".vxd", + ".msix", + ".msixbundle", + ".appx", + ".appxbundle", + ".msi", + ".msp", + ".msm", + ".cab", + ".ps1", + ".psm1", +] + + +def sign_files(dir_path, only_ext=None): + if only_ext: + only_ext = only_ext.split(",") + for i in range(len(only_ext)): + if not only_ext[i].startswith("."): + only_ext[i] = "." + only_ext[i] + for root, dirs, files in os.walk(dir_path): + is_signed_dir = "RustDeskPrinterDriver" in root or "usbmmidd_v2" in root + for file in files: + file_path = os.path.join(root, file) + _, ext = os.path.splitext(file_path) + # only sign the exe files in signed dirs + if is_signed_dir and ext not in [".exe"]: + continue + if only_ext and ext not in only_ext: + continue + if ext in SIGN_EXTENSIONS: + if not sign_one_file(file_path): + logging.error(f"Failed to sign {file_path}") + break + + +def main(): + parser = argparse.ArgumentParser( + description="Command line interface for task operations." + ) + subparsers = parser.add_subparsers(dest="command") + + # Create a parser for the "sign_one_file" command + sign_one_file_parser = subparsers.add_parser( + "sign_one_file", help="Sign a single file." + ) + sign_one_file_parser.add_argument("file_path", help="The path of the file to sign.") + + # Create a parser for the "sign_files" command + sign_files_parser = subparsers.add_parser( + "sign_files", help="Sign all files in a directory." + ) + sign_files_parser.add_argument( + "dir_path", help="The path of the directory containing the files to sign." + ) + sign_files_parser.add_argument( + "only_ext", help="The file extension to sign.", default=None, nargs="?" + ) + + # Create a parser for the "fetch" command + fetch_parser = subparsers.add_parser("fetch", help="Fetch a task.") + + # Create a parser for the "update_status" command + update_status_parser = subparsers.add_parser( + "update_status", help="Update the status of a task." + ) + update_status_parser.add_argument("task_id", help="The ID of the task to update.") + update_status_parser.add_argument("status", help="The new status of the task.") + + # Create a parser for the "delete_task" command + delete_task_parser = subparsers.add_parser("delete_task", help="Delete a task.") + delete_task_parser.add_argument("task_id", help="The ID of the task to delete.") + + # Create a parser for the "create" command + create_parser = subparsers.add_parser("create", help="Create a task.") + create_parser.add_argument("task_name", help="The name of the task to create.") + create_parser.add_argument( + "file_path", + help="The path of the file for the task.", + default=None, + nargs="?", + ) + + # Create a parser for the "upload_file" command + upload_file_parser = subparsers.add_parser( + "upload_file", help="Upload a file to a task." + ) + upload_file_parser.add_argument( + "task_id", help="The ID of the task to upload the file to." + ) + upload_file_parser.add_argument("file_path", help="The path of the file to upload.") + + # Create a parser for the "get_status" command + get_status_parser = subparsers.add_parser( + "get_status", help="Get the status of a task." + ) + get_status_parser.add_argument( + "task_id", help="The ID of the task to get the status of." + ) + + # Create a parser for the "download_files" command + download_files_parser = subparsers.add_parser( + "download_files", help="Download files from a task." + ) + download_files_parser.add_argument( + "task_id", help="The ID of the task to download files from." + ) + download_files_parser.add_argument( + "output_dir", help="The directory to save the downloaded files to." + ) + + args = parser.parse_args() + + if args.command == "sign_one_file": + sign_one_file(args.file_path) + elif args.command == "sign_files": + sign_files(args.dir_path, args.only_ext) + elif args.command == "fetch": + print(fetch()) + elif args.command == "update_status": + print(update_status(args.task_id, args.status)) + elif args.command == "delete_task": + print(delete_task(args.task_id)) + elif args.command == "create": + print(create(args.task_name, args.file_path)) + elif args.command == "upload_file": + print(upload_file(args.task_id, args.file_path)) + elif args.command == "get_status": + print(get_status(args.task_id)) + elif args.command == "download_files": + print(download_files(args.task_id, args.output_dir)) + + +if __name__ == "__main__": + main() diff --git a/vendor/rustdesk/res/lang.py b/vendor/rustdesk/res/lang.py new file mode 100644 index 0000000..4655d2c --- /dev/null +++ b/vendor/rustdesk/res/lang.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +import os +import glob +import sys +import csv + + +def get_lang(lang): + out = {} + for ln in open('./src/lang/%s.rs' % lang, encoding='utf8'): + ln = ln.strip() + if ln.startswith('("'): + k, v = line_split(ln) + out[k] = v + return out + + +def line_split(line): + toks = line.split('", "') + if len(toks) != 2: + print(line) + assert 0 + # Replace fixed position. + # Because toks[1] may be v") or v"), + k = toks[0][toks[0].find('"') + 1:] + v = toks[1][:toks[1].rfind('"')] + return k, v + + +def main(): + if len(sys.argv) == 1: + expand() + elif sys.argv[1] == '1': + to_csv() + else: + to_rs(sys.argv[1]) + + +def expand(): + for fn in glob.glob('./src/lang/*.rs'): + lang = os.path.basename(fn)[:-3] + if lang in ['en', 'template']: continue + print(lang) + dict = get_lang(lang) + fw = open("./src/lang/%s.rs" % lang, "wt", encoding='utf8') + for line in open('./src/lang/template.rs', encoding='utf8'): + line_strip = line.strip() + if line_strip.startswith('("'): + k, v = line_split(line_strip) + if k in dict: + # embraced with " to avoid empty v + line = line.replace('"%s"' % v, '"%s"' % dict[k]) + else: + line = line.replace(v, "") + fw.write(line) + else: + fw.write(line) + fw.close() + + +def to_csv(): + for fn in glob.glob('./src/lang/*.rs'): + lang = os.path.basename(fn)[:-3] + csvfile = open('./src/lang/%s.csv' % lang, "wt", encoding='utf8') + csvwriter = csv.writer(csvfile) + for line in open(fn, encoding='utf8'): + line_strip = line.strip() + if line_strip.startswith('("'): + k, v = line_split(line_strip) + csvwriter.writerow([k, v]) + csvfile.close() + + +def to_rs(lang): + csvfile = open('%s.csv' % lang, "rt", encoding='utf8') + fw = open("./src/lang/%s.rs" % lang, "wt", encoding='utf8') + fw.write('''lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ +''') + for row in csv.reader(csvfile): + fw.write(' ("%s", "%s"),\n' % (row[0].replace('"', '\"'), row[1].replace('"', '\"'))) + fw.write(''' ].iter().cloned().collect(); +} +''') + fw.close() + + +main() diff --git a/vendor/rustdesk/res/logo-header.svg b/vendor/rustdesk/res/logo-header.svg new file mode 100644 index 0000000..9712636 --- /dev/null +++ b/vendor/rustdesk/res/logo-header.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/rustdesk/res/logo.svg b/vendor/rustdesk/res/logo.svg new file mode 100644 index 0000000..965218c --- /dev/null +++ b/vendor/rustdesk/res/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/rustdesk/res/mac-icon.png b/vendor/rustdesk/res/mac-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e42ee2c420b831106e89c2c365ee9cedc67dfa2a GIT binary patch literal 35842 zcmcG#c|25a^f-R*ojZ$t>^qZeNlcbV%2f&_rUflxDoXn#BE%h9&^DDyWtn=nkcy(T zklRX9rl?e8LMZz#%gk>+pYQAS{qy&F{r>npf1Kw$cb@w!XU;j#Ip>}ymE-HFB(EnA z07~9o9xDMb8HxdeBCEV!{H$t=AO5O`RsIJa zv_WwjJp9eB_{06&!~51p{Om9IGbSFLWS929{dWBQU%{`z-hUF-<4(BOj@<7+iaVgB zmHDiV^RXKW+u_k~c-#sF9q^=;dB0OZL81K51l;X_r!A~k?ZWn9xcx`iIEI(?LH-{# zH8m)085kIVJAdKUU*3ly$o&hY%~00-?b|m|_Z0H1RW^cbo1EtZaI;f2w-=r@!;MaY zAQ%h=TzC|} zl9CeP*9qBNaH(%@{s@8~aIqi5Fz%Bv$moX)|AV);w~GfPtpA4Kd_QtwP*5{LqtW2p zKzZ{Nqz@3UC*aJWY(Q4#D4ZPv!H^7xlS7DLm~(fW_jE$_?5MDI5>AZ3@exQH;TBC4 z)=TjJCLnbLNgEUQN?Gau;OHnj>z}AyiX8pNOdE$IV{mw^cSI_9ErFzeLeUg*XdFoz zCrTy2|Ht`HQr;+KpPholan{L6ASNJT0?&}Z!3iW`65=Mgwoq}iy?3eV8N&EZzAyNuaQrIU=PfwTaQAQIPDP$|BQmG>&BQiurJ=ycj%*^@o z=gV+e2O0eF?S`l_9mYwK7;*S}OZrWYpWYZ=ao=T}4GjYHF&i84wU4 z8$s4VhRccxJ?@i?=wp#9eOH013BHLC-XlMWrF|=ApXOoI|I6tA?+E?>{wyaw`hRdA zsZ^o$e^GNqS*0x9B$dW{NGqALhMI;{O?pQv9nzF)Uv-l`h|)Q&y;iCxmFi0kWlf_O zG9Z<*{ufS@!Rh~t{eJ}wvKLmFrSdGSIxMYiwt28K?Vo;{bKKvhz%}C!zJ2wzXpfNO zC0@%K=2>k9IQr4s!+muO($z?R*ur9B{D)aQn!wnr}z9g?|-a`qeAh z{Ojm}`mc{PE=_s=s$hn9KEiHY*G|aZ{r!@-)oPhrjnn8d%}nPh-QX?Z`S|#Zz;dkp zQ~kfew3X$wt@rkSs@buuhx%co?%*uE2|IP?$**RWJSC&ReJ%QlUh55AuWhL=p&v9% zaa#hXt}fR-4*m4$WSg<9KB{mE{?2|m73t4&`h5O*t#D!Yz9VtVq8f~prMYO~M8#Yp z>_eaP4HwIjiK8}_Hzp}z!r#U_(M-v6bNrvmpplcuS-Ti@PVW4d$;);$V4pLth-?Io zVU{gAM7P^+ib&Ejhls5bGI*0m_vzdcM-}fovhWx@Q8dT%9xC2+u1>PE5Yks&xDPx5c|}lCDlfec(}7X;#X99q{8H8AHb1hTwl&r$%MHjrTScDMTe1jcFHXW}}wS+#Sl$4eZ`}Aw~=wof+_8MmX zp7Ae!0kH_bchYI|kxveur4@U-bmR4ftA{h6PZM%>!)`ezWH-cA+?~&w!}LIO?mB+{ zzGUZwq~!08uFw@9CX)24-CASZ(@oc|a9r-BICwT}{li`BPyPn=M@KfsWE)$qv`o`I z$tsm2E$~D=+okt!{aI^I@0zl1XV$)tbDyR`9at>VKjs9{reV32N7+3^*CQTh=C^9? z^Smb3B%fwh`I{sIh~n=V_zaIbdZMIvC=-R`z2j*l6oBkM29jzKdFjH}k=)o!~x?;b7X znL@p&=OJGqpReft)-}X#jOJ|q>gcVXoz;#PtX3#+7hB``Wt^IcR4d}=xb{%mK{O?e zves6~ee1M0dSp^>n;z*h6X|wr++FgMv8k6K(k#0SBWbUQv@@3sZFXr1@2?=7+u?lC z700Q3!x^A+V#(d3%Me%k@k4)_R5es>w_X2rkszQm@M0~t=GJt)`Q4sG7iY>Reb(;<;DDO`2hV{VQTC^#xpEoDVAWg@ z1xkPF3I&M46b&}H@PBXStIMOsN~SME`w^mke!26b(o)yU!hCtrRw5#j1`qS5<3I4z z2vLE-rqLhA-p$@xTm2Cfw%OtT=F@nM-B~(xW?+X2p<9ax?@Q-7PhC>t*5t_x^tqLG zb6Mu7haQ2#!BI-~Getoe(rkbS?NwLA4Z&qfB3Q=XIROg4ay~8LWFkRqQiRm02v%^b zuK{!61~)g;H}JCE_;Gd*qO8B12tuRwcR~BG#5s2?gW_8`i7#>T3um!J@mYS z2kUnI-jO_}J?i{2Qu1wnGP8cFN2u2vco08>l>XD@{wn*ASh;QNf(wN}{#wk`r%e-) zMM|NUx>hAKzZ|r=g@@AmNSNW2Bwk0{DUt_`d3+%eTlVbWc4qi=Xo*zfrMY>ANYp4D zmuN3uv*LE#u;RK<(qA+=(3qRyBVJ1_toNk#tMvUg-U@y%tyz0%YecnjhvvX)WZJ6P z#G{uNQ-5obTWQl)OYnGF-!*yO3%B95c$Fe2jf2Z+p_>(XO7gtUs5GS6j#;6K_-Hlg zyh?dle(C)d|1H2F+~zCv-T%Q@=OsBLxW7r0w;}P}9tRDbeTS!`8CvCGhnVf!?^XSA zPh)b$E%aeqkqR85S^CYQ9A@|f&+ZnMf^OAHQw8JqFk_-*qw>S9@|N4rP zKAd1hQHoWMQ}dK6$1EZzD;$ulEZlQ0rM}3A!q+WHs(5q-j<9;^KUFf$O=xj>7fzP% zrx^SxK#O(6&T3cG8S3tsyl@Vy#rR`CU2Pb#;UCXYyD>h(W4(Z|VJmoyE>MX0NiAH% zQ|RySjV8X?5f@&-NHNltigZOQaeM^G(mhgf*{{BfSLNY*y(Xt)@62|4^5^{0igGtC zJaG<%pm3Ef?TNE*2a%<3=ka$B`LcAN2-QR0{Zs6le!C)3r1A&8k3;r$8ZVSq7=~6R zXo+ZrR$^b<8b>0v5G7jNLTzY$cCkx1OD5oJuO+ku<5%9@2@0W*YmoQ@vpZBAbV+SJ zL~l1eUynO>Io5(*g$yXdbl_VU)GK|F9A~*BOe|nd z-)EScjjc1-KrNgobYzrEZ`$E*kC92ayb%P#C>rg1otQk$8+=mFj;dj_cS~Cdb&Ro= z%9F&vwYM-niiFCGM$_3D^rcNkJ#WH@ImCVa`pRAUqGFxitDRG+9e*e>sV@AJZ>K)|UWsu=ERP^_G!mBq0kFPAjmkBk`J)n59|xyec@ojQ_fIkJ8LPF$;jW zqZ~7AS(y(;^PM<-g~L0KUqvqMd#c~zWe=9P25|||DtcU$+kkT_FRg=Hq9EdE4N)%#WojtbBgclr04Sf^;}lsT%pGlg$P;Ppm=?$8ANisC(H z17kHWYusTWP4u4r=<79(XRMZZ?>Q11NFb8z$aQB|+vyx3-qVL(m)EF=4}tx1xqZ|E zIJ@@wCR(oMDTK|KocmfAKl=phYdX63Pcq^)3}J5@aX%K;nn?w!4qbAhP9H9lcQ(aK zbZG)(`lT%DM%M%DX@f(?=w zEcY3DYoRA+DLprgO*;KK2D26>uD zauA0cK7%|H-3X<+s__n5ZdI* zt^A&L>HVG+QwO#nSJoPmH7p{{dB*BiMudSGjc{Dkq`Ep&i%T_uY5HQ5#&6QtL*Hv^ z;Kn(;sot73RX=p_cT98$g$l4tj_i5eg^rhxyv4BP{FYuoqp97i1r&u>R6*Ylg5;K4NR))Uvp zFVsoQknj<>a`6!Jf7EnKPJBu7?3gJNR-drx!SMFHig16KjjFgKX ze-3(~Lma{Sls}Fbr>vP#I6Kab_|-_-mD73mqbY8mYUDz{q6r=c6$Q?S*Eizfm+;q; zJ4K=#YVHg^e;W<9h#9x8sxqG@z0D_1xl~f8@6{}Q{J{I|o6!91Ghg`*NRx<``M&{^*S=I0#%S7dfPhuaKLZHar`&P;W1| zb_B}f9+lmxh7*4*`3Qr1s{p<-C^0^qaQ|Ssr3O;HOLZM1-svL*cKApi7LePCb$7+= zqsA=3KJ2YM@y0>)_9JEWzkkq2DXf8UtnhJd5(9ruD}J!7{a+#_m@bsabHfPE4LZZ_ z94TJVOJ!%{Myk9eh$}54hlB3ehK^nHFo;W)_o|?ZuGwKRZU-b3pZ-Bh;pxd4JyeJ~ zcbsKSC6Kx)xSLDoc0u$O?7(a!=3)V`VY3`pM>yvO^1_5wC_j@))y8*hJ^ALZG@+pB z2dy;<*hIn-!8Ef%lP)A^UpC%*^0V+(rZQnJT;ir4TDbTa9C5fIEyE5fQ#?i;6}UTc zzA2xtqJxUp2%soCM~x<*Q7LDdXx_(Kfy~+puE_Zx`KGPqKh0WWiIlzAoccf#Q#qta zsXIIixNZB{f14VDiRgbh*TMEZ?Bs;cI*UsFxu-fmIXY`X4)nMaDM^!Gk0G>2JF&6h zf(z<8<}TcfK}}{M#{HsRX+y2D;>)?ZFMx&Uw{7CU462 zlm(yH;)~ueHEygUMDYt#3fkzrz4ksYHLHIr4LL96I%cV0o}z9eah%yeeX_RT1(SOs zX;NhAD z1WV_%H0>?>Z=hekLm>{TQH?L?!t==Ahyr;3o|UlD(!ky36q5Nag!1FL7(N7(122?e>^bb(eXUoH@lH~avkTxG*2s}jfO;6zi zpCj-vV3v_I>m&~HOSz`smCx76zj+UNWdtk3dkM#Cj=^SVj6($IjzA53Edo#a4Mh9x z*#Z2M)<)7(Z@kK+{9WGy@oT45gjcZp7Rm7oD63mz;j33bVZ{ej#!#nboC4zUS!D(} z;4J*|iyRrhunqlk^SnBv&P<2JTuzyqXi%oT%EqTrOgE6HmL4Wwc>frf%VG$NiZ<;U z`)Y}tLJIYAy^s?@iku6d5mcAPc2lx$xjUbue_(q42oN*J62VQM zY~>Q{Q3p1|H0b%b3-QjHzNLre(toP@GyBvJ3gjk^uW2-Khu(F|pklgKNq)K81~Yc( zAfiD*N$8qM24>Nos4I#kb)R#n4E2S#4(BI|KVIp+M|ffL*B2H^YHB>ekJgdDhwc>F zOKZ!dj@?$v?U(1>Otclo467&}$udI~o^RGCU*9-5B3&_Ojb>qDhzoR)Re>+j#!UQz zx7@zhKc1f~RTiI+78)qhEZ0m?X1>%q4(BHbo33S2z1Pk)=AhvgOfjJ0bBGw7%5RGD z2j=7R+xHh1^vXp(UdozIHB}UJE4=!{Ipw5)uW|lc&USXbH%}EXkxTJzO>zHOi|I$u zVncT!!B9c2C<~(;eHtiZY5ySN2#Zv$prp*_=SrJ}`s$F02TTykqq4|@+kuE|Hf}D* zHUx$+r(ILTEJYX2qA>=pA1+$^yk{1TH&=u6;g{SY?UWp5(zq@2$=36pNhCcoCiL5c z^sUZx-53l9M;q@ zJ@$JXqPjrZ{8=bih723bCkndhJSCST*VH(~zb^3}gS%BXn48~%A30^6pU4LK0ayNt zaZRT89{SNO1IU_1#8?o1@-~pGnRY0SBU0jKU)wma zg}FmMH0b%WfCWT;kNo}~*%=@}|IL+=VUsEups)m#kah zR@&i~Z=q{9%aYW##~0tEg8>t6s`jlc0^8qod`esXvi7|FN0Xj2Vj{R7M~yK-4?}6$*UQF*kA_vOl`p>=*#U_@0mzewUDDw5(pmD(vSXixoSvyOn_pY1 zH?s9?Et$54&Rbjn?E^V4J0n{zZ1eYG~Oh`gL=%V7kutxR# zSLD@x=zlMa{b@wj#L#?BQf4ls5y}JshJkF^`{n^h-`WwwM-g>&P0@dnkg`~LpAl%k z>IsFjTy@pnHYslUioQC@`V&Z;ibM3a{j#a0q_3U9iJ#xFSqFwgJPfiTyFzF>z35tZ za68#49bTnO$vk!;d3e=}EgkYBS14QT@V~J!X8%!nH&3gLIF1kdT>vq*`@1{jl<6(X zLe?mY9vfIC&jK)B-GVy9Oh(Fz{j2^RIb~NSBgF{Jfaf5Q=kb<+#iBpLqA0#*P ztWUH|bhb%MZ8byJ*tZ9O_lZOjYFO<%TYEM4+7tZ19aODu2qS=rE9 zBsKpcboqFHt4CXUG-t^`+ql@L;B)-2IF%ssGAbyZ(EmjQAq};#85G>N!iG) zZE3yczi7%vwC=uP{M*-V6tZ(*L0`rctSJO2FP*r$(@R;{vCzwkt->1Jsgn$8h?%;3 z%dKF=U;e-%1eBf$DHS?vx)b$6uQkwuzS$)Je5`q85|Utqf1wLl#J~i!kVKa&k+%N1 zjpSmm!(tk*Boo=G-vrfV$qVD59wJsf=cIYS=Ypet4&k<}bww&HUv$mnys|YR4~=0~ z^wtXjB_~cv4GzPC7jC8c41Lz}L1g8S_9BNy#uK$*s>j?xnDT6Pv!>_1DyI1C#ZciQJ|^5C8V1gBoMo?p}?4%tA8MITHs7 z2Z=>AWtEkA62n)Fg&7eE#w@*uh6XWCI*N=Fi8pgD(O`)&HT$i<$1oBfKpU%yAXeX6 zV!?az(N{R|o|VGZ>}ocKn8)lLZg6JV5{me1Rz%0vau!Pg?dYa0?V?qwa}B(SAWi1= z0OYv=F{SxNw&2%J&jJ3kC#rvBWf?@8QUQrK_QzWe!apl~_#hmbeGPNU(q-B_imw{9n*3b0* zp@J5(Rk%|HZF+?D;agrxJ0)~c7XI8zh#@N{_1%4z?xE6+6 zt!*WS@NlnIeK4>n?2pi@4pq-qd{H1Yf*BWgbDfwPoT)9MZ(Li#Shl63+U^NkAJYrD z*6?Njt;jA{vTZ9m_Q{_A=S?F6MQ+UGCpOz2U8vSeOKWaIxM8Zh@EwRCXpRQhOI0Sf zLufLCX^QC3ZuY%~TY6`;cD~XMYD=i+q5zwj<7SahFVR zr&UH#y!}a!Mfrr~P5t_s#JQr^ZhoUxW@K06d2=9APf?~zraku(bY179%Gl2$c3Nms zE9JO?9PjW*Fg&M=WlTc;T{&>KI~iXUrY^#vw5I()zilKs?VpKob}!9W;SxU)g~|ow zAtMVxh;$xw-L*EKRRar*sdH4IV8Ex?fOSp2&+P8ueX4?$Wro)71QF_#{NnCmdNr;s zy{gFF`$eM2`uE8dLPwH-sEk9Nn~)Yd{68)6MEcMcW<37ML)6#M(9tnMb6Q z-8B60wNX&AK)=`idK-E;|{O zd@i;4X4L6yAM0LfXl;V0F9mJs6(eHGT+E2m6FETWfZ&i4(@vey|CImd>{GQlPEzc& zZQe*lR>+3j_w2P_1(p z=Dazp@#XMNntMuOY*I6D+x>Ylup|lwsp~ZHdgT#aG``&2n6*5?iS1we?#kKGoQ8hA zypfXyM-JCeMRKC1-sYTU#M=DkZ917W<1Enu3#Va~5t(>OGv!)X7MUJy?-f5YCepku z>(C~{##RGXl?6CTqk*SOY}(bT;D7?MaS)ybISHO=<2=-&-FP(n+Np?V`L-fv?m;8* zSZ{f4Qx#-nZKpvg`z|omVGH-XvwBDEK_XHUO)oQAaC4~EuYhL5l5AUmYhVniWM238 zJ?P2f)#f=AFxLVPW7f=a{d&?$T;nwvmD?RzGIWmJ-6S39Y|8H3=rY>)Z-0%)J#W18 zDHJK#mIUjphSmAxG|;d{?|aILzq+cYTzIKbs*|**Jb)(H2SEzGwR+M!Zb1g62Pd8< zze-M{*#_@%bfh?kJ(f#RBVVCJHibYJ8WUd>ypGPHth&}nzP&uV273@Slp0Uu5)fN1 zJ)IpfWIvy%vUDCT+Xid)pz6FYEEyAz3^L8=mGV0YTDcxARgNwH)vtWhhJ!(<2epxW zHYe*>;>58v;$*lnjqI_3CHxaJkU1lxqdO{*2(=&2HQ-t;5Up{@S^Ki=jV_JL@1R#d z_5}M~c-H$kN`Ij;8LEfw|GHLAwCMm%rdFs>Fmf>$t2_*{oC_-BF7`HxLdA4W95BCi zC*Lp9SbWOgSFG!IIYH+*1zgdfr@Pn4vA0CavF#odqtIakuQ1k6I-y;;42fR?2nF4M z|JyBYeLpYzo=c-e71Fm|+;`yM&!&?8n8`19)*Arcbl^tE%kh1k2z$=c;?iN2Rq|kh zFpjx@Yf=*{eWU?=dayx$`Nw^UQ@e>={WS=F;mwfra?DzpW-h8@usY2OOST_Pt2c zh58@bOrbCv{@NZY%oBUgu2FsvHPSC9G8`PLNGz21%J77AC|b^D6qkmf@KbvkifV+$ zgCpiudk~hrFy2C*o`9EIO|rj?mB(sgxhS~;&9uL!6kbl}%7fkOO?!>6j>EO-?w2Dl zkhw_ijKuYIKffuViqSc#2eNy{=!j1=dK&Rr+eEXDMiNGfJ40S?N?k^v6RHfM4c%*1 zh%yyrO7`c?-~Z&lqL!fp$EU)NOPw!(+(`ZsJZUO5Lx8ZMJ_|W+fQmh*rh~P_ECK}h z3T450XVoF=wz+vJiA+|VoKMh>C?n!biTh10MZmWdRIav1c;?p88pzra^-P6Pxfbw1 zeHQd*{ymjw39v%9?@lLb$|XLtPWIWKG9*t*i{z1lv~|68r^Sv8$E<^icPwF z@F}P)6xkvO3%??<5-;z#!wr$F?1;bIeyvj4qRL88L}IHgkvG;gP#g<9xN^{vAkgdj z3^a6RqG={>0(EhTJrl+rI0{}EgZJ~$24cpKcF=)@f=1-FFCv_NCB(8-Azgcshc|S; z7C8J`nlV$NS&>+(2dU1t2<&09`Woh!9!f%CqJAHnQvdAc0a_tLbD04uiXeh)hCZ`z zjJF{KaUgJV=7~fu-bFF>(WT)uuGFQkx$4XO_x6PQUWCvf@|ZHKQr<&Z7;7u;73`SG zOOsx?k}s&2^N=Uj63=_1D8xnL;Z`_yNoM6FrZEfTEqR_vgNOCn5f*g;xoY%Zpq6wa z#-*bi`sx#uVI3Ybw0hi|^e@vTm#E7Bp>49_v~`P@hxv*xO-$yeN#9+NonR;1Aa(j> zchzY1!UZP^)wqEcV0V2J1BqFT^AEu$P4U-9`pzwLXB?BhTXf;5r9Y&5iC1z@N!(cz zp8UX)`6t%RTzK6IP5=8-G4^Z9C>k_ez?NJTP*(3_`DV!b21T25vyAYFy-{ClZ%T~} zG!CdSh4@8=nkskIVfBt%i4XeSFce>ingSG~vuqFp7{Neh)M)39BKQABfNWRmrS=%c zz_aCj!*^1zDsZJw0f_S$k!LBG3?^RfFSKn?eq$|~9(K0eMk3|u%3I(;k;dFY|CwNI zjBVH52=!ks0sAdVT%x3mpz%y%6V=LKYG1z_o6TGZ-wz9VgJ|46a((aI7pwWhTLy;y ztlL0!U3H-6$7{D$*BQ=}r?M%44Es_ZUnUfx^Xd2pFgTOkru!)>+Nd<|=TvlmO$5RJo=N^F4aQIDW4B&nJ~rFl!JGvN zx6cAo1vD%K=P9nMIweoXj=u7TsU;kbP8)K@4|~`EmGPk~{UC;4P37Fvjcht6wQ7-m zf(x*yu1uD|_xvta_?9ALd+-`U>uM-{zpE0j_e&jG;0DbfdAu$}dH69FbG|>b?u{D*KF`x^Xt5()pwE333XURrrT0&{)y7ia?&J{r zk8*f4I{lJ4va1H(M23Mb<~EnkKydeinuRy&CDHvV)DyVZ7L(kH_ekXyh;=1S_!(dM z53`g-i9=PSVLQ5LAL4v(wHTinLDWhOSk2kAvpXpZzOFz+M;07Znd?F*XDuAF+k zS&}rmS30)8gaShpz=^zIsPr%-q*L!EoW3blyJ=WXK+g|4S=64Ii={-Mzz~F@u_svv zqz%0}zQp>aXi%g~PIgmVZXxj{PCmqj&9h;eLJUO7j+Oz*wAlNfr%!ef$TpU;$@4w zDb<_1B(Z4Dr%Myx^%^snM$)AA92VrETSAnl8s0AvuCi11scUJU(S5NG+kNUiYJ^aZ zbb|^Tt(++7mz;Q#v}X~6dtfH-&YUS<{D&i9QS)96LWTO4y-kx7qvwF#As_reiHJ)q zi}PFwwTmLI4=V=u#kHY3x-lfFP8cbB|4RI$p|0(xfcTvz;r{%g;8{V$ue3Q#4 zr!%U0s)a~A z?Dnx?xTPeg(s-4e;v!Qq^@aBELIlI)?4izhG<5ai4rV0E8)qAianz+uui2g>#Hhu5>KYV$M}KKkQ})3 zL|GWDm7R{>*^mCR6993eS6Th^CYa=fUklQBw7f|*l!pkyr%es)dku?fjNhWWf1MO+ zK6Qj|@;!%6bFi=h>OiYA4)@-CCPPi$B0fGJ>PVgfBt=1nhAM0<201QBC{cw^N^UKIxeY_PZM8p+w4TsBFLl z;kys4T@L5}h<}Kbf-bHGFD3EXDYn@vj5UVvu#B0IW)r;jjU1fEm)f$f1)>EX*3DO0 z(CrA9A!en8D!$jz`f3>RV>>$87=&;lXH?1z33W)z->7+RdiZA}usrJC>N@r=Ru)?> zoNYK6o&s$K#MPo5@194slj#;!g^9X#{>MG3UP+$ zLhPc-GoKH$Zr(UxAZpt{I1M`+nc%Kt8q*-(7(#odj%j^S3ws4-iLX?IB(Ca4`tO)Q z)_KC5AH+lyNBWyWC6)1Sng-iGBM)mWJochGk&trtwwa#HNlk#Uu-4tI@Snl*JkRLq zf*^XrxzZ_}zd#ZhYv?I`OYvt@5E8e<48gCm&VnS|5FF{#qll&Z)p$G7;j8%`E&Mos zs*_c=h3uQCxKCLA>xcqxsE2*PxZ^6q6l_d<${bo4~Z75!gWy&(qX2dNK21YT^;kjPSnja%Oc^DvQAhzYbk3I6=U4@Q%-p|WUQlSY7&lz&syL7nYq%J{yx!%u$-(sc&y}dj*eQ{M03&&FT*Fi( z2>^eK;<4*rSp7#|w53}^wuWhp+tiZt2+@oovyTvuyv*32%t)tf@6-d8sgcq1OULdTQJc|xpF6kMYSTqGpbMj6bkvRJa?@xh!*m z?RhHqux4-KlFU?Lcb<(1sW?bDhEp>-X-6E~$WkB5$&bgcKLz?M7Jh^4$35Bkt2LN( zn?IcoB6x zwigP)8*FaMF_PB0PGBKNO1yH{0XK4U{M!OAdJhdDFIvGTS9-A&RtFa4m$#DaOTkx;3%O zPBz7KF$1m&qA?p6tVStk z%ODdpPo&~&v_ag1He~&sBUaQvzAb}d7d)s^_1ba7D9+^P zX`GV~Sh$B&&j?S|hw{`xag*Hy;V!#a(BHx3NG`kEQOVmy`jMz$3dXv*hrsEYg0OCe z4z1Y+A@u5kai1F05eSP!$fFdt8 zVyi#RCeJt{hNmbcOf!sJ&!7Enn8zZ9;9ryxL*R8&n>azAkP&kspfB}dvamGZU@)e%@QK^(HWqK$l=SImwi9vD=?@m*_R73%~0s~6?r7K+* zSxSLy4MgWDJdk~&-X48mEAsQIx#xSg59*LsP3!qxuO6O9w|sJKF+p-{@hoak*13(W2kHV% za4Xit@HvMZbRHh!U&F%RD`j})Ycpk%AC6^c8xt>|!URB=mdK#Q+IfyU;gqeJYO}}m z>IBkP#Gh+(G{Bc33|6$90ei5%Cyj4Z$--%&(bY!;U$ZN(YLd8_;ER%C|5CBZ4sHB9 zvHbx(wrP_EFq*V%%OWnbE#vMYsB67Lfr4?cj`{R!F=F^5T~c zHy|E9yx})JIHt<#k&27>k$%Dr>O?F2-ZUHP`P+*4US@qsx2e(RQCD44ShAULZd7Wh z?Zei601pNIwK6eC3}k;Gc$pTwbJ|errW7MxFVSN$i34*u?!>$7gWk&G+sYh1Lwv~^ z+xwX|`E0&2C@G<$QA(Ey7So&>G=$V85Hcy=xZP#!NHKD5nfpcy)*huojuvl?Z5!i$ z4&r4@hAN8NFDe}hCapUiShC=HlGqV1RpiEain1@k8RXGh1V6H$n(vu>e;ra~C0xVT z-2r|-o=gKD`o0Z{rxnQ!%EAxmSjnLow}=R%%m*}{h&H#6(xvQe2Z4j|#fr~BY5v6G z+34KulxMsA6cOvEgvUO8SFG>PY*NV^Nry+}FI^OT%*UQJA6d_d+T)`(*Y%_v*YLPVf zgIW$zOldN|RM6oHL^@Tp2|_GrkHo*!rgKj0ozYR-V~M!Dt@tDNY7cGxh=Et+ z?tS(rh=>8zIHXmFHK~K--&et-w22v1=Z>VbF+|S_d$sN^*TM`wYG1~|##2xbNM92= z6ZJhP&uPg=CQFW~3IF30M?PM5r)Mk3fRO5Np4vvJpxa54z4`pqek4@LNQ zXRe_jMRv+N4Z%3};IFm#)lu1*cyEcKI9y{*apA?3B4wc_7CMTA&E%b!MIGCS0nH+L z)29RoOopp&6ewmeJ7Up;5n4QV-{cuUsw0#2omXb#o|i!>!i@cfA>OH<(J|gu!LZy> z0sO~??)hI-k}J0r=ESDDY(pcvdKnQ974S$oAWu`7j6%}@ZfAfvIauWc;>=)DP{AMW zJ!Eb%%0KQ$<44{-YP%HU8iKU*gqko}9o&o>0KLW%v0g!(XO6wTpriW60wl=+L0LH4 zhfteFW1M;xauiuGXlJ$yH-{m$soN z?2Vg`<0WgfM0Iy5+!;h2G{zz&k(-GKEd-@xrnSLHKcXJf2VUjO8pixg2S@{x2_G^h#8@63};+=|A zea9?SiwJ*}bHzw6?+&%8m!CF{T>94upD}-V z3VJ7`>mspSIo5Kx`6OsLgMCA7jd?Y#t&3{C?kAlUjVP}ya8s?Oojps?&c;&N@D59F@1u@B%fxVm0c%_&zkq zTgsvfQw*`uGWouTM`uKv73Q@w(6^>!XvbMArZC2qRq014&JiDa9xDhQFnug&9j*JQPH$9M3({@GrX4;H zZXHLe*3JeN&}D{ViZ-!VC;WR&a?v%|6U$oY)K~UNJvWdB-r78L$UjPmp2NGJ@^9&g zuj=q`Yawnp1-fbb38*ef-cW#PPsdFWH#i~Q!~t_?IK7wq+LG9aI;1>JdQB4t`zK?J$oVqeYr{K1@tX%3 zx5?gE7py}gLqW&g{df(nY9U_nWaR^OO8T2>nvIt{_m7P%Gfs6AbO+;`BRx5bHppkD zY~=1mPvm3ghpqTtikwfwl-u_oAYs!OhTt~+BKIU#BohnibXQf$g~RB`$WGZ zwc+nh$7EQ~fU1<48IPt{)^yNpsunVLYNgDgI*+-G{o%fs0vY zULjKaNxP&*9fr3JBg*c3eSqGoan<(BWQGViASU$P6&24aGtpaoL;rVzEE_kNUvA%s_8t5RO_Ryw+ zZ(||@kOL1QV_24w6npm|EJr84EY(>^5yo3R+Az69RCo}78^0e6?J;DG%jzh6r9bWo*0Njy6%YI{nVbgk2Ba+z-mhiuoSC%m z$MYV4$fEZt!{>+$#&T3-&3dG#5jU@Xno8fiHj|WKObx+Jo~S@J%j6yLY%qUgS$Y|4 zpgOCA*B=VzW6XW~Z{67M>N4V(n#?{?!;^7Su*#v0_#(1p*dB_Z6MtU{Sg`W>0vVrN zoPd=u*7?Ed{V8;hybIwJMkc&}$|s24;~{zLeqji{8w=10cH`*kKHd#4y!XU{=nDtcMGKT3zT3-0uPWb$%1w{;fH6y!ozgG}yM9u{slvMX#1RU2>e!zBVF=+O6Xk2vl)gNU9+YXILm#p!9MJfS z+1xV{c5|pH8?Z~e#=O~UVa?dF5p>gP20M$abW{<|+q@a4;?C}bpj?Fvk>B-nIpwth z-ja+zObQ~ zK^|faV*E{IUz$;G=vcvdy9>8opk<(%C(czl2z1mEJ`m>%ZmN<8W{2mAv&eUS9CGnL zHQW%C;3oxN&zi0x+L}+;D+3jl6XJTwJRi!BlX=|P(9t4PhRS^=rfJS77aJ1q(L8o1;Vd!dEB%q>If|$R$wCO}&BTF{W~XFvAR=C0a(DW#wQvndT3V9qJ^mkBin*U2Fp(JYyxcJC3%=EUfKl4?`w9+-Zt%-{!~jQ_EDvIZJ*Au75Qe zGI~OI;5{OE3Tn&Ac6zFvHMknOl$q$%2EH)aDvKB9PX0Z+4a5sbQ$R1lvfFfFKB4eL zLGT?jjfJaF7-Y`gvKyP*MbkSMvSHc3@yFY^tU|-dyC;#Gj`sT&t3jkbX?KTo9^{0( zmD9)e*5sCMpY|DeSZ;)#r93p7W&~rg5xlGMC&&zMYApyEun(^R&cW}Gm9HK}_!G&~ z43O{YkfDA+&*v<07kItclI!~^<&IVbe@&Mihrrx%bX)8~e^!vy&x~UxuTL5Q>m0N zT9wiWWuM>u{{HdD%y~TSaqpS;eZSx5^?V&j#KOa@pi5_#7wCxMFaP;(of^0=IAGJ+ zB7^zjOYa!+z5*J+sJth@dmXgcA4|VV^&CAo+bU5LHkxQ8gVC)5$fTNA_+JunWy_p< zddj4idxZ+~X(Es5t!2=(Xvu9UZj&ANe$}%QED`3#^P;-h`48n8~j%%yeoO~ z+AIl}%{kmt24|LE{t$$2gc4QprnvoU=*;_;l|Rqpe|t{xm-vxdHjFqEdmX8(5p0~O z95%)&;o7v}^M4^*iJ!sRiD;}MV03MT`(B;L8xqy|Zw!U)S@Kd=(9bc~kL#>5v{|}L zG#ENZ{#&&}^AM<_(lRrH00s4?fRe|)=uY_RcR&J_F-w`(u-v7}(j+UBvasQN0=?trn$ke=p2qtNN#;Ca>_5U_ zp)(cy>i||L343&Zczv6@iNIVj-!glS6S&Y5rbk0lF&==XBy^M?XyB(*_ktdmgg6-j z^8ifNrK?=lH7OpUvJ8t@-wuSb%<>eBx7jcuq67MASddk-UA}OD@{!JovNP(AKm%Fw zmBiS)Er;lW)xJpKVb*>bfgz2B0COm&lBw+d#b%!lPbns9V zm**JQQj>ydpv0GoUt=>nog9nbba>!4JI1vov`0uD9b7CB3~LA~YO1%$vLsDMNiUU` zf6cc*j=S+vJ>pb3Amd3lj6v=u#a<+HwNsC_a87`mzsa{2$+>;od=870qq-$(s+c2y zgp+y$0iWclPW|Ig9ke%z3=5wPasrGL%;2gFU^Z_4Yzc2uP}$0*#z=N9%B(WzM#lc* zs6j5unR{u#WC+SMsm~fb4WXYZ!1awFDG8`4_>vwNze0ID3yHsdN4wkUCPq=;(${iM(~6BhOl&WoeC$#U`qZcG^5 zmEc6k$gFuF&nYuQbbkcKlr4c|^1IF`y+OZR0{}&3L5tQs+nvyW&2j@2#=aInTY%_Q zLh%~FFL4W0##WR6eK+BBSI))AGO!cpajaFY{-lcpfS*dhym!Ch+@sL5@u7s`H;4+G z#?J+_i_cPp72FtRWgkdS#m<*%u{tadWgGk^)jzV&vhX|wN=AG$G((<3PHHd|?u1>r z%H8!N6-u&GNnCdqdyjPN?vuRFu&&8Azj2tibW_JD6eDl7!SYZ}`;d+Um|96o=tdg- zk=qi(b<|$pdovbk^RM+YGj-46dpwFTeXwscNXT#~cSc|MaDFcA6mzkx@O#xdX|8Rl1A zRQM4Zu->>DzZyx77*sHYo{lt=_#$d3VBszSB%bmL8YxB|FLC+c5MZap(imj!tAX?L zM}K`}6<#2$l6V*+S9k%= zg@j)U4R9fAhrM9;f1D3gUZixj_;|wW+fs#Vi7afXcyqJhknH`P zSS|~1xEk?a+|GPDO)x*%uIx|V1czJ8Wi~+S3yxOq+y{?9A=T&{gYqERK5J&v5h!Ev zq}T0(AmP%9WRPgmLk7AlfCFP@eIr$LV@?HMDj#FBrfu5S4Aq8yGzl=ZYhKPT$LT5GRHgIM|znq%_v|WpI?X*4LkY0G5LA(ARJgkEakeY9% zIFYA9(k0n3mbmqKRD>*zBd4YrgqvcQ&f`|W+N{Q|4~%=9Tl8X0@@W{CaH4?oDPT(V zbBnAy7aNoi1tBqH!NI{q8NsgaNcKS|e5u}B1T#w5lL|^Q4e$Z0Q(#*>MOp<`=$9g? zi}3YQJVhW+WtNf6p1`~@Uvw#z6CtYv4&Mf8vZx#@Me>gtrH27#u)`+L5z$Kg(hC-| zpY(wQa&x$DIim-_2_UOQjc!&JR!{^C{2i(BA5`oq$dpA0afOHFs`kV!2)>R@=B>lt zfXVI2sd_;V?6a1C`z3V6cvPd?(oTI7`obCw5Rs1ocU4@Aahv!n2LR6wqwahG5=zP0 zv5L4y=GXk=eUuMAyMeC@UB3jeO4VbbL)%#L634##@Sa1>-(UUO$$%z*Q!LcmSnN+_ z)W{rtUVcQ7H%PHliKnc@KsA&Wvqc@?CD@uEv~a5uKgPN|Kp0?sG)=0P_owA<||eV5YJY-o2>pGZ5{z-Ten34ZAp{^U>^bJ51(w zeTL!%gb@h29dp9TtAVX6-AEGC_Bed48s=|wl(ABz^xEw8oD5}vLX&0%HU56M`!LKK zvWK?n(2cI-=*hzPS#+Nb?I8EZJNml*-e@{&E}A`{#k(ine9jE{MT7C?uXHQC1e&`o z3VbjZCq&j}1vb>XP16Ki*DKas=pfLO#7t9USl->MY$54n8P53`Fczo<8lZc2f7TpG zFVaIBPEvvI1e9yT9ry4`kd2AjA-#`H@QX6wL;+k`AGZo!xz;!n0 z#uk>qR1G-D2wfz2=RpvuvjGaSk8@;#KVWz5A#*R7^Y6(ZDkC%v1#`CWSP+i?>cM0AzA}`-^U##T$zLUe9)W%3g6T4&f8CgxbvjhFr+5fBM zNPj@05&ISy`~&9(kzQFcK7Uzn6)ag=b5K5S4V^Ve2}FbdT#{hFJEB8}EmZZ?N!u5E zamy_bCqnaZD#-EU-69KAS)Ln}i2ea545*Qn;^A}ORvFMa*Y)xJ9_(vhN>~G=MDle& ztZWPD+z)`-0pW`P3s`U&jEq9Vvp{BH$L1?G_$4jgh!4<$z8x(e{bk!qpE{mI=kHG> z(JB)y@v(kXAg^Z_B;hhQM;gvs2c7BAL_fvDafZIQSao&F^VQgjQ7@HF7=$}=Z-)Up z(B}jh4oZ)H&9F23cZ$DK&xw+~6OYy=&tH~}gEy~(azvkAGH!98QfDj-gd*5&=Ncaw zd@I}^DOF?@&lWs=rS}FQHZf9k zt8NCjySJm)s+aA5crRVBZ^)DB526)B_l{dagmoL+93<^d4CFIZc=9@Lj)KLL+j}nr z&@1=;^IHxcnSd}Z{F_1vwp1znu!PgDO+D}I2QP%G09S3U^>u7TKIS&FkFWU+dvG58 zN@&>#8Akv;W@NO#2?Z)7T0G_8cSA5Bk@N^7qHJ9loRIQB4s&JVso3Y7TDW{Wc>Sl} zeZSn87N$-KJW0Fz{#qMks{!h?xk#ZFDYI*o9FqEns1=Vy1Cciqjh5!k%#7af1On3V zx+Z02X$o+mR;_ZoQUPL^Aqi>@o@M0T%qH%867xk@zq>8;VHx+!Klr(NGF32jhFgd* zwMnOa$bD-{++G+%=~gBXg6eS80ek!awk4Se3siny3iz0XYJoJ}&W+LnE$jzrT^Wge zVBg#qd5|!Q1lB2wE)12ga@$HI!Rp*ZMbuf#rSmxLCR3t57d$0^w*n~8+zJpXFrG!a zB{m7HD4y}8dpq)%KrQ|dM~}J9T^Cz~4k@qTQ9R2>!0Q5VJTDcT1JAXDr^nV46)N$Y zQ46Y9=FnjQjSX_QV5k$+F$*w&oH!5LI@3sN!FMaxn%BYi&E88#$CG!+pyf!XJl-Nd zd{3Qr-A#Ergtlj)b&rEM#V{=gy5aG$#4VImSPT!-m`CDCv_X2>DnchF2O`N(P_w4w z3a;7za*W^Vr%-Ak02H?`T0l#xL?3-3dRc<0fIoJa4OHyGJ8=4r3bP^=%keOH1CfY4 zy23t-$V7DEaGU0dZT!$N7V{cJngKEPF|LExqr}!GXJI^9iFJk+w%Fqlws0BWEX8(| zCEXlt$ZWNLz54dlWZ6qs@kcI5Usk*b9blkYXe0za?*Nbtb`W+`pf7X}Ojp(KS{XG5 zjdpp+yf_%id)y_wy?C#(T>TB!$&Rxx6%<60#vRs|_TA82CHs!_{($loUhMKsYqC9q zs7kt$#4&%gbiYY9U(e+jt$x{Hy~1L-z_glDAZ_p~gI zLmXw^N0=o9odH1H0=z9bW3xl~2H?sYuU%d6@KM%61c;~biZDq@GbMaIbZF^*B-sY| zf?T+BKl1&y=-f%rGik`Vs3E-Tsbpm$gpA^)^v(fr9a!~1OZEiAt&7*d{B;3TP;Lm_ zkoP&Bv2@~m)QUTO+YQ*DJ_58KENoWAYLFx0Bp6vuF1CamsZUo)(a|U}_)vT3z|J1X zp;-bzh5dBO+$mf~0h@q(r3gEbN7Y1Ndwdz39Y=dfO2wpRO{!bEmE447U3=LMlcbo( zaHACVhjrm4eA*Ekj=(o;a{C7GKRuAs^hyu#lryL8tz@-(P;zG3{L8^sNoB8{u71XP z5;10fKuNt9=lObc##Y4;DwCk&eK*SBe5W6vm^xMTTdVTVCXGe=$f72tfvN2F2y|2u zJ;N1{lXMNh*WFGR5`y8!<3sH$PvLJ5No+Bt98qM==t|_4I-%`j&saTR`)-&$uYd}m zV*5>&GQtdPp?&+on`&zzS~wLX$)LZX&n9qOhNT|pgw~LY6@swi6>#&2`oUTNN=HDz zix$kog&cB95&Lp&5Fz-}r$mQJ3cS4%2dAeMca}) z#kRfm<)B%sqOltZzq1)y@dL75@Gd4QWq84iH*|qa2-){)CT&XfXM>qD;|fB@TB^#I z^reZp@@{J4ElS`QL7<Qk4XsQ350LEW<$yNoYH70MLJJU}8D7MiE1u=Jn;qNy@8}!VcWG5mfl>8BA{c+67O2ikx?azKbR4li;1a+s{;} z#sKw&5}59B3Mv!vm9S`(EPB*>bGL)klwUp7Q{x9XJ{Zm3v~VY(LW>tc%0w!EYkEpx z$rsBrn^lRp;gxC|Am; zR$9=>22luh1buA@`)T6-hPh}CB?!W-0S zi+CAggO7tXAUAEy5zB!Dx*QYGjnHsc`%o>;$58s`f{ZN(#I(k@PoB3BN0?V>l{-<2gzyv@yqOAqD{DhBd}v`$9U_R<%;+tL_bm=p%YzoO?hTM96ZnRWK%gaeDL=fZ^Vqu$rwa-WRo z)YytXOud*S*}3vGjJ>v`K~iMkW;`az*a!&mq)ayNKaF%)6}*+pN~P8glp;8JgTvvYZ4$6nX8H zATAXeq}YNG>Ojqi8}rRN7T**QaFD-$D&~pTJzxm9;Bzg>u!_Oog4IJume)gB4X~yH zrn7KLIma^cuaXIP2FIx4x)5C!>(~J0f}h2Fj%wvgnAr<{HpQHwxA)^g_xf#L>%?c9d|d=M6r^RVskrB=5x=oNmc(I zTp2+*Q7wnOm5|4i@(5ir%l;SN1(H5+kACN+<%u2#)!5@0K@Gzr~ zZ~3nOiUQ|M+V!(9?HBUCCu$tjqtn)E3A?rv?uI^g)6QtG5Xh!k(FjPr}?5ZpL46?}h07 zWrP9K?c)3a*AfpBW+Q>8LQ`y(1<NM*}nxF38i6*&B|p{3S>(zEB{LpJn1ID8;8 z1uS1{za?*>*FJRld!mDB%leTf)1*K&Upc9O4SVPmZnE<{M*^q0u-F3M#FsLX%Awjg zIUDFU~OQ4 z;>oh-$I=al#N#jiwXeuczw=StF$VBX?}l~%>}E7a{Eg9LY4R_mB(p%(r*&2|OrH)C zp;4KcKCGbSuNDo<)LVEF0etO$`>z+e1y2+yaE8oSd8<*C?-WLh{FKT3X2Iz|n3+Vy zGi1;Qu3Cz`T#zB+OL; z%t%)+kA0xaG}+*Nnrn8nkthgm$h&FR`QX=CVEs8Kr<=0I*ltiYRfcuA0K{$lCg25C zmb~|BbfzzOU0Fj}M?||oOW}-TG+frB|55g)1amGxm<;py&%6EoEm0!cXmKijUp-rp zdmC;TIT)X(NDYH_KS{-gUyg&|jt{gH@1n!2mJCHPbwThc@bs{F{KyqW-~_I~DJ?C{ z-{Q|OrZ?XHcxW5MBzs;Q2LYq@S>R$5SH#S9mu5}DqTVOJjj7lp*P$NhF;uEOkc{=; zVSrnIQnoF&PbkSwznMZe2e(L{n;C`e&S>6cbrlbb>~PsKGA_q=x5K{K_d}IaR675x z7Vv^D2nO90F~P`W{dx~y%eI379v|L%r5;||UhG3=}o$$JI zX)sf00UUq!V?iUwy8{qxxLjhP?p_b*|Xi_dT6YzT98^jRd}AZ zye7MPVV&sF9w&VL`kQn-x|Ey|aRubNikIqh&b*c?A5K!>q$)|-$C8a~ZepdqaV!)r z+oL7ibnKUqYPYL$35!@-sa~%Ef2x(}3vZ*quoEBx?ce8Bi;n3Z{?zR-V?3{XeZd2i|0`jtty~yO>9<65)w}Y?$`|-Z1#iTa%*u%_+;S%@mJ&u;W znsctQ>sVD)$EncCZ*%bUh{J*`(T@OYy5X*mW}tlk66RiDqXdPp5f#-kdB95&!?56L zR)8wz2T~;YXJM#oc)p_1c;eo61JR1pi`~=(F!0Ge09MY)z^-6HwCFdLfZ~aciv-Yt z9lVaQdZfZH?nhonBNsto04Q#i&0Ph4TiwX}XZb<`B>xBnhFFRcCd+F!G|K~QNzYgE zNvF20i6W_$nLevquH)>II({!vhi{6|uuWL8rq0G-{?}B94LW%bmnh{&(qLm0!uSiB z1FilBSc(C^<^s7=o^YSay=xx6&1!hXXs6gwYXz=$bvaQ|-xZovO z2*CnPH`7e>sB57>0a#Zou7oBgOC+%;@lim5AlZ0f)WTZQOK)zRsE7e+B*Wno;ke@I zC{SDme=yq)hUc%#;S%z=gdK86Ar*2UB^1d6h&3st(|-q>JxsIey7}LY+bh+#gO#$( zRxv8O1r=*RR^)R5$}601d$CT3%keIdPpyH<193xJCutXdrGOcsq}tOg#;R&0U#RlsBKe)-|(=ZcbH zvf)zRY4#$&`a#!{+Z>fj@hu5<)Pq^^4^sw|cIO4}?J?Byur}+eL@r$z!GgL@v|v=x zU->Q2;Jq&5h2u_QmxGGmu6aNq*ycfEnHTu- z4lVG(&LtWMUb};X?@)rTb2$nIH=cdFK?`cOOM09LP!hXYoJ|s*I4dC<=sCt>-+YJP zd><=Z8sNkg02M136n#~Oa$peqZFat4F*NZ2IMTo=tm9RiU*2Nu?jiw3VGXl#!6A#Y zQXm&R%jPKmgk-7mCyChVQd8S-~{LC(|q4)GyM2p(nlsJlYVfT*Q+9A z*pPrc8?kDV>V){jZP^)$u1yY3*lMxs!Mw zq{6BGENQsIKFjBTI5vJ;+@I>UCl6NvZS&ZtUnl0{SIYCMFRH5y$d>zHVdp|I#C5~c zZNKRxP?(7sM>9d3-*Q<&68QOTVt$nz&$k;Eowg$8d^bgt)ogsrqzZBPo(j7yzRcBu zo1YYi%Hmr-9Yy1Jfe+8V&8{M`$C0V6dx5Rihw=^}wVl0k8JNJ;nDtnx!pEs;P@4f_U%U)ecyY}5=~&GJbT$5!36b(XdRS&HSr zW*WgG%VH1Hd3!BD`3qV1K^dNSyUKh7kOZoiZ?wF8Hg^2UA!W!?s>N<`-M(e}f0s$f zqj#i*YfZQpmRs^HLgWo0?AldF+;KjRWr$Y{MtN+zb2F)E7R*TfgCy_S(Lrb5?3u$* zSTQhu03vRx@ZKB&ao5)0*KqDfbH#2CTQc@luQ~S>@O5E_r$e(9a#$cm6e$k)Nm-J8 z0S!|nketd$$$&u_!Ku*S+UsBnz$|!=zj2p>(|>Eejm&2yzJ*BrsEjPy{)=P&4@S3y zN%AxO4&h$}acZ0&vs7T_*Y1VZS!N|@<1YsiY6iroUEtgmz?XPT7T1uDowWZtRH>?k^I*{WIN-3oof0x5mrHk+!=Je8hgAq8Hk|=>aDRWj0(xJ#0kg;!IZqMk z&6V&ri@Tbo2P1F=o51!8pkg~nwX#j$b7Obm5fW%!u+?;Hlol1HFD8Bd?-8issSiJV z0D|spXzRIy?UjYr`O&Sqd|vksy@zG zcQC)`l3on?r}*pk^jcfzI4UqyKE0bmp4Fliv!ivcF5|#BC2n zRcE=mH5|TJy{h8W$`IP-MvL=At7|=!(TXe&#r6eTeUNzZg~~LsIfYNg%L$91=*DA9 zyzGybkzvaOE9n?wlgWSCH4^VOGM`ar)epJ6 zjd$n7ydSUi_vk(zCE)@%zo#~G*AjvMPpYRKrSidkq@QNP7J2XU=glOFOl%@Htx}Ri zVBqm^DTrGi3HSB~9f96&&3L>GlL3MjQY~gxTj~VWE*)6XH7>P@2*4J+3&cx<|t4qNl^IB+> z;1hOPdu~-?d!GWVmJ7H{b&@zXqX6M_h(cMz2nF;y4Ag(1HcQCqaZD#8umT#dLYb{B zk6+mW1eA*(BU4v+CsM7+=I#`^M=G=5Jb~oXMo+qKq*FYNc`q4#F zaFw<9gl(f#tPFS~{qOASl;+f{=Z|4vz3wWu`UdHhEd2N2qrKQw2a4!~9M7*;@#&Ym z7djIS91yM4p4A7P>9p#}+S3&{=yDtYJ8WT|-E5Sb`mPvV zBz?@@CxV_=ieBk@VK;If3vZO`T|FQb+kSafX7_<9at3;H6LcYO5px4m8o^f_K<4zJ_D4L~20vOli*T!7=0U5T3v)k~(yYgQ}&ZEdpGJzj-u2e?_66ksG zlZwA`D+qI6#XPVw_Zyv#G1*yzyGg7MQdJK-fZwoo4imw@TVB7FcE1Ue*ApVvS|BUl zcefs1H}Z7*0WJ+Q;S$Z9`YsMLZMgmSN&D=MDu&#C#QqC$19c=qw*1{Du3U3YJ?9j! zUIp@dSp0fsZlt1shMBoBnljhVnJ$qKb?XVE{tazf%U|`I+_RZ3%#(sL9kCUSS`jX$ zP$q9CzT|@T`2k9UAza+}cou8;Q-rYKM1^@^?nWHnqT6{y553%=N1KGE?8l-D9ViJ_ zptMm1Jav8Z{lVD7r?+1Cf<&F>-Dz+CHi0Hx63>w}xB4?}yX;FaRKv-IdiIlEW02ju z@4`4ecATTiGxCQ!T~$4QZF(s{6&oYbEV9R!;5$!WXL1(V5or|=JZ8F{q;~pH#Y=Dd zUW_XSH9wBAY*k6E-Dg4DZ2ZO3Wn)boH)Vb|Wy8m%K%3^0p6t9|2e5cZYl4kclqA{- z->q0Nmdmk8_b#s38q8NeOA0J{1X!Ylu>ar*un3l9_?I)(uhGM-D&4U07z8h)ZE(tr!=(2 z^$-PKb6(25Ao7~B;N*MxUY-x-_2r#yahEo<1raYbO}Eyj`(|%?pKJnXMw!7%?Bd~V zF47a3?`k45ZkQ6n;wJ8&Q>g#0C2a9k_GbFbVZowQ)NNZ&ItDnGp;itjfJ6oNuk#gLq=5)Z(FtS2gIZFkf_HtFKfA; zvuz~r{?Gl^vo2e-t?RCDI#eThI3)9?PI+bJ@vaj_d25WfU)iJjegi5v#u*o3&?zS27Uxw^AttItr_Yp8HhXXLNvD~2d< zJbuA4mpt3PkHCbBuO8@j{Qw)K!JF=8AG`!64MmSNhiHDssuv?*D<>3uJ!snb`qmsI z{d5}er6k+$D@3$yU&rxTm!NZJ{gju3M9egj_AlHa%YQeOc8Iq`0mD_NnAKd(bBNWF zpR6U?obFpx77F1E;rFwo{|dM!2Shu9Vte!cDu{|~`du%+UOE?}D3xpFr#j?Lc9gS- zYB{XjChNle>WsyuHEM@~=9qN}_kD$VCX3xKQ#xq?FpGj!9HG*c)_pdTZiO! z+p$RDcM+zQIqQu%b}nP>P7ZnDqn#j2mURh^=|!^RE{dMjwozGM3%sXTS1@M)n`p64gvdAh~2ILWtBF^O9rcWqPpv9?}qUJhLt?XJLC5{^!E?ftS_%J_; zlH{Jk`*+{&WJ&w9>m5gxT0&EO;-?btxdeMFMRB46Q_T7-(9V#piE;^neBSY4uu`;{ zNBvQsy{D3DfVo`ju|lmY*q30P`n@LL0%CVLOX7I+(3!P(-X%Ia5L>CXu%SJ)VxI8} zuUbvjF$B-Ss3z(Dde&c{V|#wN`Ysi>d$s!*hDnFzS16q1oZ2weCFP72b(aY)E8;{Q zX*L2prO~Ubow~@~+%@AKenKwc&7>>2TuOHwdubgOL$Hy2{ZqT4Zt&Cp}Wd}I8MaMCYN(k^FwZt+Eo|YuxVC;JlKK3Rq#dF=$2a) zIq9R|hqfWGCS8El3KfTdvh2kiTcq*u2fr>C?$tZozP!cYW&6iiOj9E*&Rc!>dJXr~@npzI>evH@MRu#51pe_zX@Y9C#TfRT=4lU} zpgL^@-JY@3qT^r>GCw&3?=Z?5*@q;$wOV2?rbdQ$2wb*|v2vW>^TVb*cx&w2s z629=of2>Vk__(3$7*vY}j8--5zLybt%iqifhJ)%(M)O1{E%XE zkRd>qF#m!=WntuNav-0MF@#sF$vl7I$K?@L%Qe}pHa_I~Q3|HYJ-=YLgtaO86(9cj zqF`GDaL$<5!p9YEf_5#|k<^R`Xy5xaRL*ND&_EnZ3ND9%IOJq1Hh=4+Td{hQo*sTu zHjwtZt;+&a&Y->cv0p}9p8(_e=v_o(AJx1KoZJy?g1e=>M?D$1z-mUw#9X*+I58Cj z$#%N5>ZxFL7zuC*>G_Iq-hYp}a~Yxn4yz-q<=lcHbKqXVbu<<+aUfTAb5s6{V$nMP zyw6Yw%Nji>*Gs_c=HN^A6Fc4?xz5X=9;`M&SKOhA0#vgrg<3dkse%c1cXDf~fP{K+ zL`Z|gr}kpb9m;0w!gIR4wreRXe?fCo-_UOF0M(A#$fG$eaXHJmzt^WCtj(Ee*jWyx z@djDA7q#&qqnB|HBo%nkK*wVV+FNj87da&SW1E;#`gfTdi*|>5EHuAKpI&;|ju$6^ z+i?a3|nO6j@ErI#@w3!u9ehQ=BDNScU#vX&8dYDPj%4(N6f+f$PH0d~Jt5 zqrUW(GPQ7TNq_hq3&C!|w{AjUwJ;W^Xt+0k{JELaq2!oh!<-*BnfX(ob^JqZmevi> zCMg*37AEE^VaKn>j}Dx$z=M6DUYX()^47Aiq!o1#%hF0c9>aF+CVcd82Xk<_xlZvJ zhDcN2WWx&6%|1kK=sZN zS!3qw1u!uc+0i$>IIIVd)}gjgNF*4IpuwK^I4+-uk@I?cm|hRnbl& zGQA!kYa@Y2Tyjnr^u>_<%Bbh9J^g-XA@jO~`qC)LIB@s;ikFa|A5Rj0ufzG1Df84E zUH*Aw+3ShY<*!RwcG{f1+niesRlXLx{-)})-UH!mL$Rc-`D3&P*560n{P{m+&kDG* zysc95&t|$&wDOV@tp_~f1!tu2x;xatFdsNm3EK#7O`q~K2W9Z9wyo?_f zSY6S-pRN$D!?CcUEEW4(#nE>pzmxqLyRKp2VHM~*(;abd_Di3}L^9&3k4LeFr;2S< z%8F@?z@6&!%~=?Xht-7Fy!F0vKCC+Il%m_EsjayU*|AI3UpGJHrLO+ui$+GjxO?Ev z$kpfM0EZUSq|%;b?4-i23h)9OX3EK3Vks&fEX$+NNuB{hUc%!2C4bA#I!Et0=^F5y zuJbUWj1+HG_qOWJl$SJtHqNtRHhPXlG2hb)cj1#SA=cqoPv9ieZy$-Q_Aq5{z=GpP z^qJuFvVt%S34Q|%Absj?=%xoecVST-jevNa?jVAy&yz|Cm-O*XSJ}s3p|ugj(WV%T zxl%(c7hNNBT$L`EzkZ4){K*60v{nPyOEC&1W53bXxqQw$9O77JWxH^0KIDSpp9=}#_laUi>+_DGQOp4BgV7TqqoF`D z^{-qEyn1l0DgV;S|9zV2&`ARsl2wH-RC(@7(7W&u5>5e+qx8twv`X=L$craqv=TjY z1e>Xe%aKfMzh0UHuIC6t^BU#E#@dttWEkki{#uzSjW zs+^x4+Mxb|CgfR(#00}rww{+w<=glWx<(RAtHfL;4(0K&_=$MQvQ#+@mX2c-Z1JPk z=U_yRebWXgLX&?{M`)Kl429JEhg6cmgN&PGtVPCi)%^#QUWXeL9DJTSBaJD5?@gQa z{z1k_jI8iMia8B?ZSesz`c}8v5@g%pyg8O^~t$mcGpV1JS>f=xZh7*s?=^NNJuty)~X z{`Iy_!15cjl)@ssrSN5<=V9z><7yBCnAeHx2vcseOwmd(u#_3SF6;Yp6>Ib-K0A1I zN}NEZtpp9Q5z^@Z3^O;92EMk_NY}3*c(rxtk%co5Ism#h0+HUD{r_G{Ry~Pk5q%zQ;*6x|v(rPft}*ss zXby4UfKQ$cdQoFL7^RFZlLif)9-qWp91>*3WxA)lS|5Xv8wmbuvDBxGo;}F~+Gq~~ z$O>IjyWuiBgr`Z1EBIm}m?`_z8vRi$oi=)edvN;(<*Y_^6*G;59lZI9vF^zXgRdQ46(06ZAX)c(9Y%JFPt?UibksJ-rp#jqlFdavw5f0dB^g9UK-jM#LHF;Gq*C2A7)f5J;VbfP#!0vtB&A zp&`I@u4`^&w%{!KPU@%Bv-7A0A|0Rv0iVCy#jEEU)Pg$1%6GYGM1L&!eg{p|D5Z7(CEjM-eU_p)E{@bS zS3@6W=A3}a)%j%(!2RWX(Vxhl8LDD)j#v!aXC|?5%}QBT1n09m0Ge?NvlmW^6Bi2J zh>aJ%FDQQ$OO#I)xxEWNTa7Pxu0)x3ivafVQ2XF}v4%KSth3PS8RnOL(&T9UQ_}a{ z?1YX$X71@}ZPK#wY4NugyguF8f`?+J_{?@{o?`DAYqS}agKVgnCp%oDdhXju!k4dOO6VnM{(jRZ&uQn$nd~hJVfuB@YiqBV!4*q+b5lbny z=Jhp1ux21(PD=(oV|K1dU;M;+{>W5o!m`3yF8P&|%7tanqcSfD48`aD@B3=UgBgEi zUu7sS+A_nPW*osTIzSbU!W<6-x*vI6JRN^-{@1^$>2IGRr{@0I9|k7KKrUDf_3fwt zm?(I%wp(0SmN77&kv%7Vp3cj~PM?*q{RZzZg^u-ISAxM%_gL$zXOT<9zs6<$iTkXv zSlYKro!*=J_Sy&oBt zk>Mf^4HO3_z*RJ!BWiiDQa$OzCR%nY`PWv zqMg2CP-%j$#kw$w(=a$C4!b|NWZ}+u_m8sQ3o4TZQ>}MtopUom*G{MptJw<@NmY`) zmX6Xu`S`+;nH6J$zwerlEyymohEfILg{z_VafO8A8U#3dDe_SItqIqbg$>f;<;TLr zy>qkwNhXLzs4*6VmZW5+Et`8PDKa3>?Jx)EE~gFJXK|U1e{Ua_<%z=%x0gM0omfb( z(H0dh4TiR!!fLIt5bcGVmH^+pqNZ$ezUr^`B-LcWcjWiCr>=$8>%etv_l7mtHacID zzZyM=%bx&K8c#x}Cdz^qDn!a7wI)Mi74AoE6X8NYJ?Tjme78mC3U~%B8#mVZXjFA| zQwXe$II7Kk{dMMWWb)kV*(W`b4bS$tCOghVUNoT9D6jfux3W4b9%>{D`RJp^VfaT-p4D))>J>Y=Ag#-N!U?$xKAIi zGLq=c1lX6PU4q_*`;ihEdh=h#zP)TcDL-Pl`?hN!D4-~MeWd=$73F$EBh-);XlT50 z9o-9dnyvfZ^>86zuj?+8x%RHs|EuiU|Cw&surYIvq0pAip^)?06B$AZMan~~oP{F1 z4<&~$k#%As`kSRz0Mds|C;usjD+xi<0;c+pLCg5mFNkdG<)ZdINp^uHui8Mk#D4o>^9E4!Ja)J6w z(eh)%;X4+^20_YG_Jd)U&D5{y{!8A!S(NV0xRwckHC=>4;4u*(1~&s)0YKgvwT|q- zXG~7(D^gyn|>q;iD>10(6*cCnny3NkopKPFR+GPmh9hT4%+l z0}W!(A1umN`5f0`{oL+m@UF-9fv{GK{1;s@<_m z&Uky-ByxIOXPWiL>IGz{C)|WTLU=^!if3h0a#Gh;-^}+Nz~q8aC!~RV(2v*Jpp1?Y znHO0VJ87ZOlLH|$1^B1tjK8#HW_fnU)O8Y9MH;MwRh}0b2Isi{eakOIW;Jv;x+(l? z2*~1$;}u;z{dr^~K>}N7Fb@`kCSJa~zfZ`bpJdltGZr_} z2IYd7@E+x?Mi@0%ZkpWBGUwz>E@~XMJAyJJFt$H0UZx4-=q>?~pxOr@XpDZFFn|I} zCP2Da!GdofEh&B2!9T-H?1fK^q;Uh3%v@3rmg&L{Im$Y9Xj*uLAQhn{H1y5fTCk}$ ztg?_&Drew;&a*?EiECSGVsXfGW0EyVijUg$Qd0C%H@6bnQP-u4m&R87N(jOcZacH# z7l5tYVSq^s!g21cpyrt53_l3l;fsR$p7Wwt9j9um13c60@(A9@wg#*&uJ7TMPP59@ z+LH6DEgmLuvWmnkhpV=$7UcsB_xd4GXT{9DxjkNqBFT#1q>A#p)>*9Tg@|#acojRh zm-TBxp%z03Sj;sp0e)Iy?y-teIomdC0OSin_&6gR=6l5m<}|dM&J=4lNoC_{dTrVN ziIbY(>3c`-o#{S+@TMI9L$uft+C(l>ls0hpz(^d@ykdxAwMuk_)(bLF+jvfPh&ev8crA_+@8#b zTV*vOh3XP1T`_G1dxP5-##THEd(HN{5F@F;*pqt7EaHlVy+-xMca^muAiPH*OMF?M zvA_Qv4Xj$8l$^P7%if!2gUCA{jsmJfb+4!Q*?%jqP)YuT`#jXY8t9{alqlKVFJF&S zTOe30an036+-Np_y~cL~Aq+k*-rRgP8~v=>&myz_fzGsN_N(*gOP1GMf_x3!Y{f>T zwQz+4lMin~QDJZOY^p3iX(F%@mQ|-2!OSh#7yvYQQI-0wAPIP!|LkS3z_WvP+*S6j zN_$RTzE5`OS<<=Z(jS-uK`iGV5zc02cRcxRIV4olW84S@`Zh=jYPzUD+Ig6`KtVZV z4$mn_%oG<3ZzonDU5hkI?}=B+9@%ftv+wZSdxXDTjnj|eHatg&J8F{$*>!HI z&Rt8z*X^q3{~NnGA(<3lR0Jk8#jk?fKuV6E3HmUpocf`Y#dG Bd>#M* literal 0 HcmV?d00001 diff --git a/vendor/rustdesk/res/mac-tray-dark-x2.png b/vendor/rustdesk/res/mac-tray-dark-x2.png new file mode 100644 index 0000000000000000000000000000000000000000..8b838cb5ea16f38e4d788589a33fe72570f4c394 GIT binary patch literal 612 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bO2Hb0X`wFKw3&lN=8NoNQ#S#OM(HA zjSL`y5)u+X2C_Ju4HTD^m4$GnrKREGKn7=o$sM2_@+Coj!3AlpcXf)#j|)1_>)u9euRkoSVAR!AZu@x7!!`AF^ItM6sQHJ# zDk-XU&-&_dob%gH&A(1}j<4gIeXjHF9J{#1J;CppwhQ-kXDo{3;F7Npf4KFF>Wi5L z#^QTLlcVNOt&TUkc-uSX#R{=EcRyBS#m~5NP^d%D@ZHbI33EO=&MAL4FFWm7hmY`g zn`E>7^Zf)L#k*gS`RJ+X^yi7;CVj@sMH_wM?r-~&yPjX?YvbDZRkI?roO?t953ad4 zPg3aZ%9r{b?*amqGt(a*GUIFhA8%n6x306&d&TFUEj~KWuAlALrT(oZ zExWoqGPU_y*<9h1#aTP#n~lZ%eur==uAeUBZqlTxJCUvJt=_l%9j32WH5I@8md*L4 z>ABbLvkK2PI80f)EnePw>(@uOt`&Wqy3<8x@|_G-yFa$~A78Eve7->slx!G0UHx3v IIVCg!0Cqa~IRF3v literal 0 HcmV?d00001 diff --git a/vendor/rustdesk/res/mac-tray-light-x2.png b/vendor/rustdesk/res/mac-tray-light-x2.png new file mode 100644 index 0000000000000000000000000000000000000000..11e780cada8c209893c71a11ccb4d6a34c7616a8 GIT binary patch literal 586 zcmV-Q0=4~#P)$ zzf(|ltYr7vA3*qr*_e!t(_9)^SlKE-;n~Wgu|fPBJu_?6doptoZRlGloa5lT%V@@2 zp^1Z8oXZza+PIB#xU<*JC{AIK_n4`7h@Tr~J(PwZbW-ot^TGd}gEG!B6A& zj4NY56p-_J#)kQKU#BqFi^z10eXV}x#vd?=cP?wkvoweX+}Hv3-dBi6kUd7U%)`U` z%nD@RqY*>9%5b~!#thjq8Zod_^)23bk`)Gx*JJj7M7TsYkA~dKN*7@)JLpA2x(>2Y zSjc`v3x3KT!b&!d7Tn90;kRrMEx49#L!s&zt?0;J!co>hREh76RlQQfi|M?)6KE+fKP_OomRMRmp@PG?lbdD;}m^6?UZNj z02A%(-NiZFYGdO%&gGjz6NUNby-Fx7^rH=3Q)|>)o9e~C(Kqs7Wor*!Y%M()>BmWb Y0evp6E5lN(aR2}S07*qoM6N<$f};=?a{vGU literal 0 HcmV?d00001 diff --git a/vendor/rustdesk/res/manifest.xml b/vendor/rustdesk/res/manifest.xml new file mode 100644 index 0000000..1f2cf19 --- /dev/null +++ b/vendor/rustdesk/res/manifest.xml @@ -0,0 +1,36 @@ + + + + + true/PM + PerMonitorV2, PerMonitor + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/.gitignore b/vendor/rustdesk/res/msi/.gitignore new file mode 100644 index 0000000..d901aed --- /dev/null +++ b/vendor/rustdesk/res/msi/.gitignore @@ -0,0 +1,13 @@ +.vs + +**/bin +**/obj + +x64 +packages + +CustomActions/x64 +CustomActions/*.user +CustomActions/*.filters + +Package/Resources diff --git a/vendor/rustdesk/res/msi/CustomActions/Common.h b/vendor/rustdesk/res/msi/CustomActions/Common.h new file mode 100644 index 0000000..08302d9 --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/Common.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +bool AddFirewallRule(bool add, LPWSTR exeName, LPWSTR exeFile); + +bool QueryServiceStatusExW(LPCWSTR serviceName, SERVICE_STATUS_PROCESS* status); +bool IsServiceRunningW(LPCWSTR serviceName); +bool MyCreateServiceW(LPCWSTR serviceName, LPCWSTR displayName, LPCWSTR binaryPath); +bool MyDeleteServiceW(LPCWSTR serviceName); +bool MyStartServiceW(LPCWSTR serviceName); +bool MyStopServiceW(LPCWSTR serviceName); + +std::wstring ReadConfig(const std::wstring& filename, const std::wstring& key); + +void UninstallDriver(LPCWSTR hardwareId, BOOL &rebootRequired); + +namespace RemotePrinter +{ + VOID installUpdatePrinter(const std::wstring& installFolder); + VOID uninstallPrinter(); +} diff --git a/vendor/rustdesk/res/msi/CustomActions/CustomActions.cpp b/vendor/rustdesk/res/msi/CustomActions/CustomActions.cpp new file mode 100644 index 0000000..0107929 --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/CustomActions.cpp @@ -0,0 +1,1080 @@ +// CustomAction.cpp : Defines the entry point for the custom action. +#include "pch.h" +#include +#include +#include +#include +#include +#include +#include + +#include "./Common.h" + +#pragma comment(lib, "Shlwapi.lib") + +UINT __stdcall CustomActionHello( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + hr = WcaInitialize(hInstall, "CustomActionHello"); + ExitOnFailure(hr, "Failed to initialize"); + + WcaLog(LOGMSG_STANDARD, "Initialized."); + + // TODO: Add your custom action code here. + WcaLog(LOGMSG_STANDARD, "================= Example CustomAction Hello"); + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +// Helper function to safely delete a file or directory using handle-based deletion. +// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions. +BOOL SafeDeleteItem(LPCWSTR fullPath) +{ + // Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT + // to prevent following symlinks. + // Use shared access to allow deletion even when other processes have the file open. + DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT; + HANDLE hFile = CreateFileW( + fullPath, + DELETE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access + NULL, + OPEN_EXISTING, + flags, + NULL + ); + + if (hFile == INVALID_HANDLE_VALUE) + { + WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to open '%ls'. Error: %lu", fullPath, GetLastError()); + return FALSE; + } + + // Use SetFileInformationByHandle to mark for deletion. + // The file will be deleted when the handle is closed. + FILE_DISPOSITION_INFO dispInfo; + dispInfo.DeleteFile = TRUE; + + BOOL result = SetFileInformationByHandle( + hFile, + FileDispositionInfo, + &dispInfo, + sizeof(dispInfo) + ); + + if (!result) + { + DWORD error = GetLastError(); + WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to mark '%ls' for deletion. Error: %lu", fullPath, error); + } + + CloseHandle(hFile); + return result; +} + +// Helper function to recursively delete a directory's contents with detailed logging. +void RecursiveDelete(LPCWSTR path) +{ + // Ensure the path is not empty or null. + if (path == NULL || path[0] == L'\0') + { + return; + } + + // Extra safety: never operate directly on a root path. + if (PathIsRootW(path)) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path); + return; + } + + // MAX_PATH is enough here since the installer should not be using longer paths. + // No need to handle extended-length paths (\\?\) in this context. + WCHAR searchPath[MAX_PATH]; + HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path); + return; + } + + WIN32_FIND_DATAW findData; + HANDLE hFind = FindFirstFileW(searchPath, &findData); + + if (hFind == INVALID_HANDLE_VALUE) + { + // This can happen if the directory is empty or doesn't exist, which is not an error in our case. + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError()); + return; + } + + do + { + // Skip '.' and '..' directories. + if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0) + { + continue; + } + + // MAX_PATH is enough here since the installer should not be using longer paths. + // No need to handle extended-length paths (\\?\) in this context. + WCHAR fullPath[MAX_PATH]; + hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path); + continue; + } + + // Before acting, ensure the read-only attribute is not set. + if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY) + { + if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY)) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError()); + } + } + + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + // Check for reparse points (symlinks/junctions) to prevent directory traversal attacks. + // Do not follow reparse points, only remove the link itself. + if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath); + SafeDeleteItem(fullPath); + } + else + { + // Recursively delete directory contents first + RecursiveDelete(fullPath); + // Then delete the directory itself + SafeDeleteItem(fullPath); + } + } + else + { + // Delete file using safe handle-based deletion + SafeDeleteItem(fullPath); + } + } while (FindNextFileW(hFind, &findData) != 0); + + DWORD lastError = GetLastError(); + if (lastError != ERROR_NO_MORE_FILES) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError); + } + + FindClose(hFind); +} + +// See `Package.wxs` for the sequence of this custom action. +// +// Upgrade/uninstall sequence: +// 1. InstallInitialize +// 2. RemoveExistingProducts +// ├─ TerminateProcesses +// ├─ TryStopDeleteService +// ├─ RemoveInstallFolder - <-- Here +// └─ RemoveFiles +// 3. InstallValidate +// 4. InstallFiles +// 5. InstallExecute +// 6. InstallFinalize +UINT __stdcall RemoveInstallFolder( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + LPWSTR installFolder = NULL; + LPWSTR pwz = NULL; + LPWSTR pwzData = NULL; + + hr = WcaInitialize(hInstall, "RemoveInstallFolder"); + ExitOnFailure(hr, "Failed to initialize"); + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &installFolder); + ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + + if (installFolder == NULL || installFolder[0] == L'\0') { + WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete."); + goto LExit; + } + + if (PathIsRootW(installFolder)) { + WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder); + goto LExit; + } + + WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder); + + RecursiveDelete(installFolder); + + // The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories. + // We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer. + +LExit: + ReleaseStr(pwzData); + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +// https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess +// **NtQueryInformationProcess** may be altered or unavailable in future versions of Windows. +// Applications should use the alternate functions listed in this topic. +// But I do not find the alternate functions. +// https://github.com/heim-rs/heim/issues/105#issuecomment-683647573 +typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG); +bool TerminateProcessIfNotContainsParam(pfnNtQueryInformationProcess NtQueryInformationProcess, HANDLE process, LPCWSTR excludeParam) +{ + bool processClosed = false; + PROCESS_BASIC_INFORMATION processInfo; + NTSTATUS status = NtQueryInformationProcess(process, ProcessBasicInformation, &processInfo, sizeof(processInfo), NULL); + if (status == 0 && processInfo.PebBaseAddress != NULL) + { + PEB peb; + SIZE_T dwBytesRead; + if (ReadProcessMemory(process, processInfo.PebBaseAddress, &peb, sizeof(peb), &dwBytesRead)) + { + RTL_USER_PROCESS_PARAMETERS pebUpp; + if (ReadProcessMemory(process, + peb.ProcessParameters, + &pebUpp, + sizeof(RTL_USER_PROCESS_PARAMETERS), + &dwBytesRead)) + { + if (pebUpp.CommandLine.Length > 0) + { + // Allocate extra space for null terminator + WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length + sizeof(WCHAR)); + if (commandLine != NULL) + { + // Initialize all bytes to zero for safety + memset(commandLine, 0, pebUpp.CommandLine.Length + sizeof(WCHAR)); + if (ReadProcessMemory(process, pebUpp.CommandLine.Buffer, + commandLine, pebUpp.CommandLine.Length, &dwBytesRead)) + { + if (wcsstr(commandLine, excludeParam) == NULL) + { + WcaLog(LOGMSG_STANDARD, "Terminate process : %ls", commandLine); + TerminateProcess(process, 0); + processClosed = true; + } + } + free(commandLine); + } + } + } + } + } + return processClosed; +} + +// Terminate processes that do not have parameter [excludeParam] +// Note. This function relies on "NtQueryInformationProcess", +// which may not be found. +// Then all processes of [processName] will be terminated. +bool TerminateProcessesByNameW(LPCWSTR processName, LPCWSTR excludeParam) +{ + HMODULE hntdll = GetModuleHandleW(L"ntdll.dll"); + if (hntdll == NULL) + { + WcaLog(LOGMSG_STANDARD, "Failed to load ntdll."); + } + + pfnNtQueryInformationProcess NtQueryInformationProcess = NULL; + if (hntdll != NULL) + { + NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress( + hntdll, "NtQueryInformationProcess"); + } + if (NtQueryInformationProcess == NULL) + { + WcaLog(LOGMSG_STANDARD, "Failed to get address of NtQueryInformationProcess."); + } + + bool processClosed = false; + // Create a snapshot of the current system processes + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot != INVALID_HANDLE_VALUE) + { + PROCESSENTRY32W processEntry; + processEntry.dwSize = sizeof(PROCESSENTRY32W); + if (Process32FirstW(snapshot, &processEntry)) + { + do + { + if (lstrcmpW(processName, processEntry.szExeFile) == 0) + { + HANDLE process = OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processEntry.th32ProcessID); + if (process != NULL) + { + if (NtQueryInformationProcess == NULL) + { + WcaLog(LOGMSG_STANDARD, "Terminate process : %ls, while NtQueryInformationProcess is NULL", processName); + TerminateProcess(process, 0); + processClosed = true; + } + else + { + processClosed = TerminateProcessIfNotContainsParam( + NtQueryInformationProcess, + process, + excludeParam); + } + CloseHandle(process); + } + } + } while (Process32NextW(snapshot, &processEntry)); + } + CloseHandle(snapshot); + } + if (hntdll != NULL) + { + CloseHandle(hntdll); + } + return processClosed; +} + +UINT __stdcall TerminateProcesses( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + int nResult = 0; + wchar_t szProcess[256] = {0}; + DWORD cchProcess = sizeof(szProcess) / sizeof(szProcess[0]); + + hr = WcaInitialize(hInstall, "TerminateProcesses"); + ExitOnFailure(hr, "Failed to initialize"); + + MsiGetPropertyW(hInstall, L"TerminateProcesses", szProcess, &cchProcess); + + WcaLog(LOGMSG_STANDARD, "Try terminate processes : %ls", szProcess); + TerminateProcessesByNameW(szProcess, L"--install"); + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +// No use for now, it can be refer as an example of ShellExecuteW. +void AddFirewallRuleCmdline(LPWSTR exeName, LPWSTR exeFile, LPCWSTR dir) +{ + HRESULT hr = S_OK; + HINSTANCE hi = 0; + WCHAR cmdline[1024] = { 0, }; + WCHAR rulename[500] = { 0, }; + + StringCchPrintfW(rulename, sizeof(rulename) / sizeof(rulename[0]), L"%ls Service", exeName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "Failed to make rulename: %ls", exeName); + return; + } + + StringCchPrintfW(cmdline, sizeof(cmdline) / sizeof(cmdline[0]), L"advfirewall firewall add rule name=\"%ls\" dir=%ls action=allow program=\"%ls\" enable=yes", rulename, dir, exeFile); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "Failed to make cmdline: %ls", exeName); + return; + } + + hi = ShellExecuteW(NULL, L"open", L"netsh", cmdline, NULL, SW_HIDE); + // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew + if ((int)hi <= 32) { + WcaLog(LOGMSG_STANDARD, "Failed to change firewall rule : %d, last error: %d", (int)hi, GetLastError()); + } + else { + WcaLog(LOGMSG_STANDARD, "Firewall rule \"%ls\" (%ls) is added", rulename, dir); + } +} + +// No use for now, it can be refer as an example of ShellExecuteW. +void RemoveFirewallRuleCmdline(LPWSTR exeName) +{ + HRESULT hr = S_OK; + HINSTANCE hi = 0; + WCHAR cmdline[1024] = { 0, }; + WCHAR rulename[500] = { 0, }; + + StringCchPrintfW(rulename, sizeof(rulename) / sizeof(rulename[0]), L"%ls Service", exeName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "Failed to make rulename: %ls", exeName); + return; + } + + StringCchPrintfW(cmdline, sizeof(cmdline) / sizeof(cmdline[0]), L"advfirewall firewall delete rule name=\"%ls\"", rulename); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "Failed to make cmdline: %ls", exeName); + return; + } + + hi = ShellExecuteW(NULL, L"open", L"netsh", cmdline, NULL, SW_HIDE); + // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew + if ((int)hi <= 32) { + WcaLog(LOGMSG_STANDARD, "Failed to change firewall rule \"%ls\" : %d, last error: %d", rulename, (int)hi, GetLastError()); + } + else { + WcaLog(LOGMSG_STANDARD, "Firewall rule \"%ls\" is removed", rulename); + } +} + +UINT __stdcall AddFirewallRules( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + int nResult = 0; + LPWSTR exeFile = NULL; + LPWSTR exeName = NULL; + WCHAR exeNameNoExt[500] = { 0, }; + LPWSTR pwz = NULL; + LPWSTR pwzData = NULL; + size_t szNameLen = 0; + + hr = WcaInitialize(hInstall, "AddFirewallRules"); + ExitOnFailure(hr, "Failed to initialize"); + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &exeFile); + ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + WcaLog(LOGMSG_STANDARD, "Try add firewall exceptions for file : %ls", exeFile); + + exeName = PathFindFileNameW(exeFile + 1); + hr = StringCchPrintfW(exeNameNoExt, 500, exeName); + ExitOnFailure(hr, "Failed to copy exe name: %ls", exeName); + szNameLen = wcslen(exeNameNoExt); + if (szNameLen >= 4 && wcscmp(exeNameNoExt + szNameLen - 4, L".exe") == 0) { + exeNameNoExt[szNameLen - 4] = L'\0'; + } + + //if (exeFile[0] == L'1') { + // AddFirewallRuleCmdline(exeNameNoExt, exeFile, L"in"); + // AddFirewallRuleCmdline(exeNameNoExt, exeFile, L"out"); + //} + //else { + // RemoveFirewallRuleCmdline(exeNameNoExt); + //} + + AddFirewallRule(exeFile[0] == L'1', exeNameNoExt, exeFile + 1); + +LExit: + if (pwzData) { + ReleaseStr(pwzData); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall SetPropertyIsServiceRunning(__in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + wchar_t szAppName[500] = { 0 }; + DWORD cchAppName = sizeof(szAppName) / sizeof(szAppName[0]); + wchar_t szPropertyName[500] = { 0 }; + DWORD cchPropertyName = sizeof(szPropertyName) / sizeof(szPropertyName[0]); + bool isRunning = false; + + hr = WcaInitialize(hInstall, "SetPropertyIsServiceRunning"); + ExitOnFailure(hr, "Failed to initialize"); + + MsiGetPropertyW(hInstall, L"AppName", szAppName, &cchAppName); + WcaLog(LOGMSG_STANDARD, "Try query service of : \"%ls\"", szAppName); + + MsiGetPropertyW(hInstall, L"PropertyName", szPropertyName, &cchPropertyName); + WcaLog(LOGMSG_STANDARD, "Try set is service running, property name : \"%ls\"", szPropertyName); + + isRunning = IsServiceRunningW(szAppName); + MsiSetPropertyW(hInstall, szPropertyName, isRunning ? L"'N'" : L"'Y'"); + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +void TryCreateStartServiceByShell(LPWSTR svcName, LPWSTR svcBinary, LPWSTR szSvcDisplayName); +UINT __stdcall CreateStartService(__in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + LPWSTR svcParams = NULL; + LPWSTR pwz = NULL; + LPWSTR pwzData = NULL; + LPWSTR svcName = NULL; + LPWSTR svcBinary = NULL; + wchar_t szSvcDisplayName[500] = { 0 }; + DWORD cchSvcDisplayName = sizeof(szSvcDisplayName) / sizeof(szSvcDisplayName[0]); + + hr = WcaInitialize(hInstall, "CreateStartService"); + ExitOnFailure(hr, "Failed to initialize"); + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &svcParams); + ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + + WcaLog(LOGMSG_STANDARD, "Try create start service : %ls", svcParams); + + svcName = svcParams; + svcBinary = wcschr(svcParams, L';'); + if (svcBinary == NULL) { + WcaLog(LOGMSG_STANDARD, "Failed to find binary : %ls", svcParams); + goto LExit; + } + svcBinary[0] = L'\0'; + svcBinary += 1; + + hr = StringCchPrintfW(szSvcDisplayName, cchSvcDisplayName, L"%ls Service", svcName); + ExitOnFailure(hr, "Failed to compose a resource identifier string"); + if (MyCreateServiceW(svcName, szSvcDisplayName, svcBinary)) { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is created.", svcName); + if (MyStartServiceW(svcName)) { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is started.", svcName); + } + else { + WcaLog(LOGMSG_STANDARD, "Failed to start service: \"%ls\"", svcName); + } + } + else { + WcaLog(LOGMSG_STANDARD, "Failed to create service: \"%ls\"", svcName); + } + + if (IsServiceRunningW(svcName)) { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is running.", svcName); + } + else { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not running, try create and start service by shell", svcName); + TryCreateStartServiceByShell(svcName, svcBinary, szSvcDisplayName); + } + +LExit: + if (pwzData) { + ReleaseStr(pwzData); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +void TryStopDeleteServiceByShell(LPWSTR svcName); +UINT __stdcall TryStopDeleteService(__in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + int nResult = 0; + LPWSTR svcName = NULL; + LPWSTR pwz = NULL; + LPWSTR pwzData = NULL; + wchar_t szExeFile[500] = { 0 }; + DWORD cchExeFile = sizeof(szExeFile) / sizeof(szExeFile[0]); + SERVICE_STATUS_PROCESS svcStatus; + DWORD lastErrorCode = 0; + + hr = WcaInitialize(hInstall, "TryStopDeleteService"); + ExitOnFailure(hr, "Failed to initialize"); + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &svcName); + ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + WcaLog(LOGMSG_STANDARD, "Try stop and delete service : %ls", svcName); + + if (MyStopServiceW(svcName)) { + for (int i = 0; i < 10; i++) { + if (IsServiceRunningW(svcName)) { + Sleep(100); + } + else { + break; + } + } + } + else { + WcaLog(LOGMSG_STANDARD, "Failed to stop service: \"%ls\", error: 0x%02X.", svcName, GetLastError()); + } + + if (IsServiceRunningW(svcName)) { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not stopped after 1000 ms.", svcName); + } + else { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stopped.", svcName); + } + + if (MyDeleteServiceW(svcName)) { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" deletion is completed without errors.", svcName); + } + else { + WcaLog(LOGMSG_STANDARD, "Failed to delete service: \"%ls\", error: 0x%02X.", svcName, GetLastError()); + } + + if (QueryServiceStatusExW(svcName, &svcStatus)) { + WcaLog(LOGMSG_STANDARD, "Failed to delete service: \"%ls\", current status: %d.", svcName, svcStatus.dwCurrentState); + TryStopDeleteServiceByShell(svcName); + } + else { + lastErrorCode = GetLastError(); + if (lastErrorCode == ERROR_SERVICE_DOES_NOT_EXIST) { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is deleted.", svcName); + } + else { + WcaLog(LOGMSG_STANDARD, "Failed to query service status: \"%ls\", error: 0x%02X.", svcName, lastErrorCode); + TryStopDeleteServiceByShell(svcName); + } + } + + // It's really strange that we need sleep here. + // But the upgrading may be stuck at "copying new files" because the file is in using. + // Steps to reproduce: Install -> stop service in tray --> start service -> upgrade + // Sleep(300); + + // Or we can terminate the process + hr = StringCchPrintfW(szExeFile, cchExeFile, L"%ls.exe", svcName); + ExitOnFailure(hr, "Failed to compose a resource identifier string"); + TerminateProcessesByNameW(szExeFile, L"--not-in-use"); + +LExit: + if (pwzData) { + ReleaseStr(pwzData); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall TryDeleteStartupShortcut(__in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + wchar_t szShortcut[500] = { 0 }; + DWORD cchShortcut = sizeof(szShortcut) / sizeof(szShortcut[0]); + wchar_t szStartupDir[500] = { 0 }; + DWORD cchStartupDir = sizeof(szStartupDir) / sizeof(szStartupDir[0]); + WCHAR pwszTemp[1024] = L""; + + hr = WcaInitialize(hInstall, "DeleteStartupShortcut"); + ExitOnFailure(hr, "Failed to initialize"); + + MsiGetPropertyW(hInstall, L"StartupFolder", szStartupDir, &cchStartupDir); + + MsiGetPropertyW(hInstall, L"ShortcutName", szShortcut, &cchShortcut); + WcaLog(LOGMSG_STANDARD, "Try delete startup shortcut of : \"%ls\"", szShortcut); + + hr = StringCchPrintfW(pwszTemp, 1024, L"%ls%ls.lnk", szStartupDir, szShortcut); + ExitOnFailure(hr, "Failed to compose a resource identifier string"); + + if (DeleteFileW(pwszTemp)) { + WcaLog(LOGMSG_STANDARD, "Failed to delete startup shortcut of : \"%ls\"", pwszTemp); + } + else { + WcaLog(LOGMSG_STANDARD, "Startup shortcut is deleted : \"%ls\"", pwszTemp); + } + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall SetPropertyFromConfig(__in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + wchar_t szConfigFile[1024] = { 0 }; + DWORD cchConfigFile = sizeof(szConfigFile) / sizeof(szConfigFile[0]); + wchar_t szConfigKey[500] = { 0 }; + DWORD cchConfigKey = sizeof(szConfigKey) / sizeof(szConfigKey[0]); + wchar_t szPropertyName[500] = { 0 }; + DWORD cchPropertyName = sizeof(szPropertyName) / sizeof(szPropertyName[0]); + std::wstring configValue; + + hr = WcaInitialize(hInstall, "SetPropertyFromConfig"); + ExitOnFailure(hr, "Failed to initialize"); + + MsiGetPropertyW(hInstall, L"ConfigFile", szConfigFile, &cchConfigFile); + WcaLog(LOGMSG_STANDARD, "Try read config file of : \"%ls\"", szConfigFile); + + MsiGetPropertyW(hInstall, L"ConfigKey", szConfigKey, &cchConfigKey); + WcaLog(LOGMSG_STANDARD, "Try read configuration, config key : \"%ls\"", szConfigKey); + + MsiGetPropertyW(hInstall, L"PropertyName", szPropertyName, &cchPropertyName); + WcaLog(LOGMSG_STANDARD, "Try read configuration, property name : \"%ls\"", szPropertyName); + + configValue = ReadConfig(szConfigFile, szConfigKey); + MsiSetPropertyW(hInstall, szPropertyName, configValue.c_str()); + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall AddRegSoftwareSASGeneration(__in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + LSTATUS result = 0; + HKEY hKey; + LPCWSTR subKey = L"Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"; + LPCWSTR valueName = L"SoftwareSASGeneration"; + DWORD valueType = REG_DWORD; + DWORD valueData = 1; + DWORD valueDataSize = sizeof(DWORD); + + HINSTANCE hi = 0; + + hr = WcaInitialize(hInstall, "AddRegSoftwareSASGeneration"); + ExitOnFailure(hr, "Failed to initialize"); + + hi = ShellExecuteW(NULL, L"open", L"reg", L" add HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System /f /v SoftwareSASGeneration /t REG_DWORD /d 1", NULL, SW_HIDE); + // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew + if ((int)hi <= 32) { + WcaLog(LOGMSG_STANDARD, "Failed to add registry name \"%ls\", %d, %d", valueName, (int)hi, GetLastError()); + } + else { + WcaLog(LOGMSG_STANDARD, "Registry name \"%ls\" is added", valueName); + } + + // Why RegSetValueExW always return 998? + // + result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, subKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL); + if (result != ERROR_SUCCESS) { + WcaLog(LOGMSG_STANDARD, "Failed to create or open registry key: %d", result); + goto LExit; + } + + result = RegSetValueExW(hKey, valueName, 0, valueType, reinterpret_cast(valueData), valueDataSize); + if (result != ERROR_SUCCESS) { + WcaLog(LOGMSG_STANDARD, "Failed to set registry value: %d", result); + RegCloseKey(hKey); + goto LExit; + } + + WcaLog(LOGMSG_STANDARD, "Registry value has been successfully set."); + RegCloseKey(hKey); + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall RemoveAmyuniIdd( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + int nResult = 0; + LPWSTR installFolder = NULL; + LPWSTR pwz = NULL; + LPWSTR pwzData = NULL; + + WCHAR workDir[1024] = L""; + DWORD fileAttributes = 0; + HINSTANCE hi = 0; + + SYSTEM_INFO si; + LPCWSTR exe = L"deviceinstaller64.exe"; + WCHAR exePath[1024] = L""; + + BOOL rebootRequired = FALSE; + + hr = WcaInitialize(hInstall, "RemoveAmyuniIdd"); + ExitOnFailure(hr, "Failed to initialize"); + + UninstallDriver(L"usbmmidd", rebootRequired); + + // Only for x86 app on x64 + GetNativeSystemInfo(&si); + if (si.wProcessorArchitecture != PROCESSOR_ARCHITECTURE_AMD64) { + goto LExit; + } + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &installFolder); + ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + + hr = StringCchPrintfW(workDir, 1024, L"%lsusbmmidd_v2", installFolder); + ExitOnFailure(hr, "Failed to compose a resource identifier string"); + fileAttributes = GetFileAttributesW(workDir); + if (fileAttributes == INVALID_FILE_ATTRIBUTES) { + WcaLog(LOGMSG_STANDARD, "Amyuni idd dir \"%ls\" is not found, %d", workDir, fileAttributes); + goto LExit; + } + + hr = StringCchPrintfW(exePath, 1024, L"%ls\\%ls", workDir, exe); + ExitOnFailure(hr, "Failed to compose a resource identifier string"); + fileAttributes = GetFileAttributesW(exePath); + if (fileAttributes == INVALID_FILE_ATTRIBUTES) { + goto LExit; + } + + WcaLog(LOGMSG_STANDARD, "Remove amyuni idd %ls in %ls", exe, workDir); + hi = ShellExecuteW(NULL, L"open", exe, L"remove usbmmidd", workDir, SW_HIDE); + // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew + if ((int)hi <= 32) { + WcaLog(LOGMSG_STANDARD, "Failed to remove amyuni idd : %d, last error: %d", (int)hi, GetLastError()); + } + else { + WcaLog(LOGMSG_STANDARD, "Amyuni idd is removed"); + } + +LExit: + if (pwzData) { + ReleaseStr(pwzData); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +void TryCreateStartServiceByShell(LPWSTR svcName, LPWSTR svcBinary, LPWSTR szSvcDisplayName) +{ + HRESULT hr = S_OK; + HINSTANCE hi = 0; + wchar_t szNewBin[500] = { 0 }; + DWORD cchNewBin = sizeof(szNewBin) / sizeof(szNewBin[0]); + wchar_t szCmd[800] = { 0 }; + DWORD cchCmd = sizeof(szCmd) / sizeof(szCmd[0]); + SERVICE_STATUS_PROCESS svcStatus; + DWORD lastErrorCode = 0; + int i = 0; + int j = 0; + + WcaLog(LOGMSG_STANDARD, "TryCreateStartServiceByShell, service: %ls", svcName); + + TryStopDeleteServiceByShell(svcName); + // Do not check the result here + + i = 0; + j = 0; + // svcBinary is a string with double quotes, we need to escape it for shell arguments. + // It is original used for `CreateServiceW`. + // eg. "C:\Program Files\MyApp\MyApp.exe" --service -> \"C:\Program Files\MyApp\MyApp.exe\" --service + while (true) { + if (svcBinary[j] == L'"') { + szNewBin[i] = L'\\'; + i += 1; + if (i >= cchNewBin) { + WcaLog(LOGMSG_STANDARD, "Failed to copy bin for service: %ls, buffer is not enough", svcName); + return; + } + szNewBin[i] = L'"'; + } + else { + szNewBin[i] = svcBinary[j]; + } + if (svcBinary[j] == L'\0') { + break; + } + i += 1; + j += 1; + if (i >= cchNewBin) { + WcaLog(LOGMSG_STANDARD, "Failed to copy bin for service: %ls, buffer is not enough", svcName); + return; + } + } + + hr = StringCchPrintfW(szCmd, cchCmd, L"create %ls binpath= \"%ls\" start= auto DisplayName= \"%ls\"", svcName, szNewBin, szSvcDisplayName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "Failed to make command: %ls", svcName); + return; + } + hi = ShellExecuteW(NULL, L"open", L"sc", szCmd, NULL, SW_HIDE); + if ((int)hi <= 32) { + WcaLog(LOGMSG_STANDARD, "Failed to create service with shell : %d, last error: 0x%02X.", (int)hi, GetLastError()); + } + else { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is created with shell.", svcName); + } + + // Query and log if the service is running. + for (int k = 0; k < 10; ++k) { + if (!QueryServiceStatusExW(svcName, &svcStatus)) { + lastErrorCode = GetLastError(); + if (lastErrorCode == ERROR_SERVICE_DOES_NOT_EXIST) { + if (k == 29) { + WcaLog(LOGMSG_STANDARD, "Failed to query service status: \"%ls\", service is not found.", svcName); + return; + } + else { + Sleep(100); + continue; + } + } + // Break if the service exists. + WcaLog(LOGMSG_STANDARD, "Failed to query service status: \"%ls\", error: 0x%02X.", svcName, lastErrorCode); + break; + } + else { + if (svcStatus.dwCurrentState == SERVICE_RUNNING) { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is running.", svcName); + return; + } + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not running.", svcName); + break; + } + } + + hr = StringCchPrintfW(szCmd, cchCmd, L"/c sc start %ls", svcName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "Failed to make command: %ls", svcName); + return; + } + hi = ShellExecuteW(NULL, L"open", L"cmd.exe", szCmd, NULL, SW_HIDE); + if ((int)hi <= 32) { + WcaLog(LOGMSG_STANDARD, "Failed to start service with shell : %d, last error: 0x%02X.", (int)hi, GetLastError()); + } + else { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is started with shell.", svcName); + } +} + +void TryStopDeleteServiceByShell(LPWSTR svcName) +{ + HRESULT hr = S_OK; + HINSTANCE hi = 0; + wchar_t szCmd[800] = { 0 }; + DWORD cchCmd = sizeof(szCmd) / sizeof(szCmd[0]); + SERVICE_STATUS_PROCESS svcStatus; + DWORD lastErrorCode = 0; + + WcaLog(LOGMSG_STANDARD, "TryStopDeleteServiceByShell, service: %ls", svcName); + + hr = StringCchPrintfW(szCmd, cchCmd, L"/c sc stop %ls", svcName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "Failed to make command: %ls", svcName); + return; + } + hi = ShellExecuteW(NULL, L"open", L"cmd.exe", szCmd, NULL, SW_HIDE); + + // Query and log if the service is stopped or deleted. + for (int k = 0; k < 10; ++k) { + if (!IsServiceRunningW(svcName)) { + break; + } + Sleep(100); + } + if (!QueryServiceStatusExW(svcName, &svcStatus)) { + if (GetLastError() == ERROR_SERVICE_DOES_NOT_EXIST) { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is already deleted.", svcName); + return; + } + WcaLog(LOGMSG_STANDARD, "Failed to query service status: \"%ls\" with shell, error: 0x%02X.", svcName, lastErrorCode); + } + else { + WcaLog(LOGMSG_STANDARD, "Status of service: \"%ls\" with shell, current status: %d.", svcName, svcStatus.dwCurrentState); + } + + hr = StringCchPrintfW(szCmd, cchCmd, L"/c sc delete %ls", svcName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "Failed to make command: %ls", svcName); + return; + } + hi = ShellExecuteW(NULL, L"open", L"cmd.exe", szCmd, NULL, SW_HIDE); + if ((int)hi <= 32) { + WcaLog(LOGMSG_STANDARD, "Failed to delete service with shell : %d, last error: 0x%02X.", (int)hi, GetLastError()); + } + else { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" deletion is completed without errors with shell,", svcName); + } + + // Query and log the status of the service after deletion. + for (int k = 0; k < 10; ++k) { + if (!QueryServiceStatusExW(svcName, &svcStatus)) { + if (GetLastError() == ERROR_SERVICE_DOES_NOT_EXIST) { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is deleted with shell.", svcName); + return; + } + } + Sleep(100); + } + if (!QueryServiceStatusExW(svcName, &svcStatus)) { + lastErrorCode = GetLastError(); + if (lastErrorCode == ERROR_SERVICE_DOES_NOT_EXIST) { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is deleted with shell.", svcName); + return; + } + WcaLog(LOGMSG_STANDARD, "Failed to query service status: \"%ls\" with shell, error: 0x%02X.", svcName, lastErrorCode); + } + else { + WcaLog(LOGMSG_STANDARD, "Failed to delete service: \"%ls\" with shell, current status: %d.", svcName, svcStatus.dwCurrentState); + } +} + +UINT __stdcall InstallPrinter( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + int nResult = 0; + LPWSTR installFolder = NULL; + LPWSTR pwz = NULL; + LPWSTR pwzData = NULL; + + hr = WcaInitialize(hInstall, "InstallPrinter"); + ExitOnFailure(hr, "Failed to initialize"); + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &installFolder); + ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + + WcaLog(LOGMSG_STANDARD, "Try to install RD printer in : %ls", installFolder); + RemotePrinter::installUpdatePrinter(installFolder); + WcaLog(LOGMSG_STANDARD, "Install RD printer done"); + +LExit: + if (pwzData) { + ReleaseStr(pwzData); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall UninstallPrinter( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + hr = WcaInitialize(hInstall, "UninstallPrinter"); + ExitOnFailure(hr, "Failed to initialize"); + + WcaLog(LOGMSG_STANDARD, "Try to uninstall RD printer"); + RemotePrinter::uninstallPrinter(); + WcaLog(LOGMSG_STANDARD, "Uninstall RD printer done"); + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} diff --git a/vendor/rustdesk/res/msi/CustomActions/CustomActions.def b/vendor/rustdesk/res/msi/CustomActions/CustomActions.def new file mode 100644 index 0000000..01b0349 --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/CustomActions.def @@ -0,0 +1,16 @@ +LIBRARY "CustomActions" + +EXPORTS + CustomActionHello + RemoveInstallFolder + TerminateProcesses + AddFirewallRules + SetPropertyIsServiceRunning + TryStopDeleteService + CreateStartService + TryDeleteStartupShortcut + SetPropertyFromConfig + AddRegSoftwareSASGeneration + RemoveAmyuniIdd + InstallPrinter + UninstallPrinter diff --git a/vendor/rustdesk/res/msi/CustomActions/CustomActions.vcxproj b/vendor/rustdesk/res/msi/CustomActions/CustomActions.vcxproj new file mode 100644 index 0000000..2e704fb --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/CustomActions.vcxproj @@ -0,0 +1,86 @@ + + + + + + + Release + x64 + + + + Win32Proj + {6b3647e0-b4a3-46ae-8757-a22ee51c1dac} + CustomActions + v143 + 10.0 + + + + DynamicLibrary + false + true + Unicode + + + + + + + + + + + + + Level3 + true + true + true + NDEBUG;EXAMPLECADLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + MultiThreaded + + + msi.lib;version.lib;%(AdditionalDependencies) + Windows + true + true + true + false + CustomActions.def + + + + + + + + + + + + + + Create + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/vendor/rustdesk/res/msi/CustomActions/DeviceUtils.cpp b/vendor/rustdesk/res/msi/CustomActions/DeviceUtils.cpp new file mode 100644 index 0000000..5bc6c10 --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/DeviceUtils.cpp @@ -0,0 +1,84 @@ +#include "pch.h" + +#include +#include +#include +#include + +#pragma comment(lib, "SetupAPI.lib") + + +void UninstallDriver(LPCWSTR hardwareId, BOOL &rebootRequired) +{ + HDEVINFO deviceInfoSet = SetupDiGetClassDevsW(&GUID_DEVCLASS_DISPLAY, NULL, NULL, DIGCF_PRESENT); + if (deviceInfoSet == INVALID_HANDLE_VALUE) + { + WcaLog(LOGMSG_STANDARD, "Failed to get device information set, last error: %d", GetLastError()); + return; + } + + SP_DEVINFO_LIST_DETAIL_DATA devInfoListDetail; + devInfoListDetail.cbSize = sizeof(SP_DEVINFO_LIST_DETAIL_DATA); + if (!SetupDiGetDeviceInfoListDetailW(deviceInfoSet, &devInfoListDetail)) + { + SetupDiDestroyDeviceInfoList(deviceInfoSet); + WcaLog(LOGMSG_STANDARD, "Failed to call SetupDiGetDeviceInfoListDetail, last error: %d", GetLastError()); + return; + } + + SP_DEVINFO_DATA deviceInfoData; + deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA); + + DWORD dataType; + WCHAR deviceId[MAX_DEVICE_ID_LEN] = { 0, }; + + DWORD deviceIndex = 0; + while (SetupDiEnumDeviceInfo(deviceInfoSet, deviceIndex, &deviceInfoData)) + { + if (!SetupDiGetDeviceRegistryPropertyW(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, (PBYTE)deviceId, MAX_DEVICE_ID_LEN, NULL)) + { + WcaLog(LOGMSG_STANDARD, "Failed to get hardware id, last error: %d", GetLastError()); + deviceIndex++; + continue; + } + if (wcscmp(deviceId, hardwareId) != 0) + { + deviceIndex++; + continue; + } + + SP_REMOVEDEVICE_PARAMS remove_device_params; + remove_device_params.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER); + remove_device_params.ClassInstallHeader.InstallFunction = DIF_REMOVE; + remove_device_params.Scope = DI_REMOVEDEVICE_GLOBAL; + remove_device_params.HwProfile = 0; + + if (!SetupDiSetClassInstallParamsW(deviceInfoSet, &deviceInfoData, &remove_device_params.ClassInstallHeader, sizeof(SP_REMOVEDEVICE_PARAMS))) + { + WcaLog(LOGMSG_STANDARD, "Failed to set class install params, last error: %d", GetLastError()); + deviceIndex++; + continue; + } + + if (!SetupDiCallClassInstaller(DIF_REMOVE, deviceInfoSet, &deviceInfoData)) + { + WcaLog(LOGMSG_STANDARD, "ailed to uninstall driver, last error: %d", GetLastError()); + deviceIndex++; + continue; + } + + SP_DEVINSTALL_PARAMS deviceParams; + if (SetupDiGetDeviceInstallParamsW(deviceInfoSet, &deviceInfoData, &deviceParams)) + { + if (deviceParams.Flags & (DI_NEEDRESTART | DI_NEEDREBOOT)) + { + rebootRequired = true; + } + } + + WcaLog(LOGMSG_STANDARD, "Driver uninstalled successfully"); + deviceIndex++; + } + + SetupDiDestroyDeviceInfoList(deviceInfoSet); +} diff --git a/vendor/rustdesk/res/msi/CustomActions/FirewallRules.cpp b/vendor/rustdesk/res/msi/CustomActions/FirewallRules.cpp new file mode 100644 index 0000000..bca2739 --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/FirewallRules.cpp @@ -0,0 +1,413 @@ +// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ics/c-adding-an-application-rule-edge-traversal + +/******************************************************************** +Copyright (C) Microsoft. All Rights Reserved. + +Abstract: + This C++ file includes sample code that adds a firewall rule with + EdgeTraversalOptions (one of the EdgeTraversalOptions values). + +********************************************************************/ + +#include "pch.h" +#include +#include +#include +#include + +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "oleaut32.lib") + +#define STRING_BUFFER_SIZE 500 + + +// Forward declarations +HRESULT WFCOMInitialize(INetFwPolicy2** ppNetFwPolicy2); +void WFCOMCleanup(INetFwPolicy2* pNetFwPolicy2); +HRESULT RemoveFirewallRule( + __in INetFwPolicy2* pNetFwPolicy2, + __in LPWSTR exeName); +HRESULT AddFirewallRuleWithEdgeTraversal(__in INetFwPolicy2* pNetFwPolicy2, + __in bool in, + __in LPWSTR exeName, + __in LPWSTR exeFile); + + +bool AddFirewallRule(bool add, LPWSTR exeName, LPWSTR exeFile) +{ + bool result = false; + HRESULT hrComInit = S_OK; + HRESULT hr = S_OK; + INetFwPolicy2* pNetFwPolicy2 = NULL; + + // Initialize COM. + hrComInit = CoInitializeEx( + 0, + COINIT_APARTMENTTHREADED + ); + + // Ignore RPC_E_CHANGED_MODE; this just means that COM has already been + // initialized with a different mode. Since we don't care what the mode is, + // we'll just use the existing mode. + if (hrComInit != RPC_E_CHANGED_MODE) + { + if (FAILED(hrComInit)) + { + WcaLog(LOGMSG_STANDARD, "CoInitializeEx failed: 0x%08lx\n", hrComInit); + goto Cleanup; + } + } + + // Retrieve INetFwPolicy2 + hr = WFCOMInitialize(&pNetFwPolicy2); + if (FAILED(hr)) + { + goto Cleanup; + } + + if (add) { + // Add firewall rule with EdgeTraversalOption=DeferApp (Windows7+) if available + // else add with Edge=True (Vista and Server 2008). + hr = AddFirewallRuleWithEdgeTraversal(pNetFwPolicy2, true, exeName, exeFile); + hr = AddFirewallRuleWithEdgeTraversal(pNetFwPolicy2, false, exeName, exeFile); + } + else { + hr = RemoveFirewallRule(pNetFwPolicy2, exeName); + } + result = SUCCEEDED(hr); + +Cleanup: + + // Release INetFwPolicy2 + WFCOMCleanup(pNetFwPolicy2); + + // Uninitialize COM. + if (SUCCEEDED(hrComInit)) + { + CoUninitialize(); + } + + return result; +} + +BSTR MakeRuleName(__in LPWSTR exeName) +{ + WCHAR pwszTemp[STRING_BUFFER_SIZE] = L""; + HRESULT hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"%ls Service", exeName); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed to compose a resource identifier string: 0x%08lx\n", hr); + return NULL; + } + return SysAllocString(pwszTemp); +} + +HRESULT RemoveFirewallRule( + __in INetFwPolicy2* pNetFwPolicy2, + __in LPWSTR exeName) +{ + HRESULT hr = S_OK; + INetFwRules* pNetFwRules = NULL; + + WCHAR pwszTemp[STRING_BUFFER_SIZE] = L""; + + BSTR RuleName = NULL; + + RuleName = MakeRuleName(exeName); + if (NULL == RuleName) + { + WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n"); + goto Cleanup; + } + + hr = pNetFwPolicy2->get_Rules(&pNetFwRules); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed to retrieve firewall rules collection : 0x%08lx\n", hr); + goto Cleanup; + } + + // We need to "Remove()" twice, because both "in" and "out" rules are added? + // There's no remarks for this case https://learn.microsoft.com/en-us/windows/win32/api/netfw/nf-netfw-inetfwrules-remove + hr = pNetFwRules->Remove(RuleName); + hr = pNetFwRules->Remove(RuleName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "Failed to remove firewall rule \"%ls\" : 0x%08lx\n", exeName, hr); + } + else { + WcaLog(LOGMSG_STANDARD, "Firewall rule \"%ls\" is removed\n", exeName); + } + +Cleanup: + + SysFreeString(RuleName); + + if (pNetFwRules != NULL) + { + pNetFwRules->Release(); + } + + return hr; +} + +// Add firewall rule with EdgeTraversalOption=DeferApp (Windows7+) if available +// else add with Edge=True (Vista and Server 2008). +HRESULT AddFirewallRuleWithEdgeTraversal( + __in INetFwPolicy2* pNetFwPolicy2, + __in bool in, + __in LPWSTR exeName, + __in LPWSTR exeFile) +{ + HRESULT hr = S_OK; + INetFwRules* pNetFwRules = NULL; + + INetFwRule* pNetFwRule = NULL; + INetFwRule2* pNetFwRule2 = NULL; + + WCHAR pwszTemp[STRING_BUFFER_SIZE] = L""; + + BSTR RuleName = NULL; + BSTR RuleGroupName = NULL; + BSTR RuleDescription = NULL; + BSTR RuleAppPath = NULL; + + long CurrentProfilesBitMask = 0; + + + // For localization purposes, the rule name, description, and group can be + // provided as indirect strings. These indirect strings can be defined in an rc file. + // Examples of the indirect string definitions in the rc file - + // 127 "EdgeTraversalOptions Sample Application" + // 128 "Allow inbound TCP traffic to application EdgeTraversalOptions.exe" + // 129 "Allow EdgeTraversalOptions.exe to receive inbound traffic for TCP protocol + // from remote machines located within your network as well as from + // the Internet (i.e from outside of your Edge device like Firewall or NAT" + + + // Examples of using indirect strings - + // hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"@EdgeTraversalOptions.exe,-128"); + RuleName = MakeRuleName(exeName); + if (NULL == RuleName) + { + WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n"); + goto Cleanup; + } + // Examples of using indirect strings - + // hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"@EdgeTraversalOptions.exe,-127"); + hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, exeName); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed to compose a resource identifier string: 0x%08lx\n", hr); + goto Cleanup; + } + RuleGroupName = SysAllocString(pwszTemp); // Used for grouping together multiple rules + if (NULL == RuleGroupName) + { + WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n"); + goto Cleanup; + } + // Examples of using indirect strings - + // hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"@EdgeTraversalOptions.exe,-129"); + hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"Allow %ls to receive \ + inbound traffic from remote machines located within your network as well as \ + from the Internet", exeName); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed to compose a resource identifier string: 0x%08lx\n", hr); + goto Cleanup; + } + RuleDescription = SysAllocString(pwszTemp); + if (NULL == RuleDescription) + { + WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n"); + goto Cleanup; + } + + RuleAppPath = SysAllocString(exeFile); + if (NULL == RuleAppPath) + { + WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n"); + goto Cleanup; + } + + hr = pNetFwPolicy2->get_Rules(&pNetFwRules); + + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed to retrieve firewall rules collection : 0x%08lx\n", hr); + goto Cleanup; + } + + hr = CoCreateInstance( + __uuidof(NetFwRule), //CLSID of the class whose object is to be created + NULL, + CLSCTX_INPROC_SERVER, + __uuidof(INetFwRule), // Identifier of the Interface used for communicating with the object + (void**)&pNetFwRule); + + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "CoCreateInstance for INetFwRule failed: 0x%08lx\n", hr); + goto Cleanup; + } + + hr = pNetFwRule->put_Name(RuleName); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Name failed with error: 0x %x.\n", hr); + goto Cleanup; + } + + hr = pNetFwRule->put_Grouping(RuleGroupName); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Grouping failed with error: 0x %x.\n", hr); + goto Cleanup; + } + + hr = pNetFwRule->put_Description(RuleDescription); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Description failed with error: 0x %x.\n", hr); + goto Cleanup; + } + + // If you want the rule to avoid public, you can refer to + // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ics/c-adding-an-outbound-rule + CurrentProfilesBitMask = NET_FW_PROFILE2_ALL; + + hr = pNetFwRule->put_Direction(in ? NET_FW_RULE_DIR_IN : NET_FW_RULE_DIR_OUT); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Direction failed with error: 0x %x.\n", hr); + goto Cleanup; + } + + + hr = pNetFwRule->put_Action(NET_FW_ACTION_ALLOW); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Action failed with error: 0x %x.\n", hr); + goto Cleanup; + } + + hr = pNetFwRule->put_ApplicationName(RuleAppPath); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_ApplicationName failed with error: 0x %x.\n", hr); + goto Cleanup; + } + + //hr = pNetFwRule->put_Protocol(6); // TCP + //if (FAILED(hr)) + //{ + // WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Protocol failed with error: 0x %x.\n", hr); + // goto Cleanup; + //} + + hr = pNetFwRule->put_Profiles(CurrentProfilesBitMask); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Profiles failed with error: 0x %x.\n", hr); + goto Cleanup; + } + + hr = pNetFwRule->put_Enabled(VARIANT_TRUE); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Enabled failed with error: 0x %x.\n", hr); + goto Cleanup; + } + + if (in) { + // Check if INetFwRule2 interface is available (i.e Windows7+) + // If supported, then use EdgeTraversalOptions + // Else use the EdgeTraversal boolean flag. + + if (SUCCEEDED(pNetFwRule->QueryInterface(__uuidof(INetFwRule2), (void**)&pNetFwRule2))) + { + hr = pNetFwRule2->put_EdgeTraversalOptions(NET_FW_EDGE_TRAVERSAL_TYPE_DEFER_TO_APP); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_EdgeTraversalOptions failed with error: 0x %x.\n", hr); + goto Cleanup; + } + } + else + { + hr = pNetFwRule->put_EdgeTraversal(VARIANT_TRUE); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_EdgeTraversal failed with error: 0x %x.\n", hr); + goto Cleanup; + } + } + } + + hr = pNetFwRules->Add(pNetFwRule); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Failed to add firewall rule to the firewall rules collection : 0x%08lx\n", hr); + goto Cleanup; + } + + WcaLog(LOGMSG_STANDARD, "Successfully added firewall rule !\n"); + +Cleanup: + + SysFreeString(RuleName); + SysFreeString(RuleGroupName); + SysFreeString(RuleDescription); + SysFreeString(RuleAppPath); + + if (pNetFwRule2 != NULL) + { + pNetFwRule2->Release(); + } + + if (pNetFwRule != NULL) + { + pNetFwRule->Release(); + } + + if (pNetFwRules != NULL) + { + pNetFwRules->Release(); + } + + return hr; +} + + +// Instantiate INetFwPolicy2 +HRESULT WFCOMInitialize(INetFwPolicy2** ppNetFwPolicy2) +{ + HRESULT hr = S_OK; + + hr = CoCreateInstance( + __uuidof(NetFwPolicy2), + NULL, + CLSCTX_INPROC_SERVER, + __uuidof(INetFwPolicy2), + (void**)ppNetFwPolicy2); + + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "CoCreateInstance for INetFwPolicy2 failed: 0x%08lx\n", hr); + goto Cleanup; + } + +Cleanup: + return hr; +} + + +// Release INetFwPolicy2 +void WFCOMCleanup(INetFwPolicy2* pNetFwPolicy2) +{ + // Release the INetFwPolicy2 object (Vista+) + if (pNetFwPolicy2 != NULL) + { + pNetFwPolicy2->Release(); + } +} diff --git a/vendor/rustdesk/res/msi/CustomActions/ReadConfig.cpp b/vendor/rustdesk/res/msi/CustomActions/ReadConfig.cpp new file mode 100644 index 0000000..695ebca --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/ReadConfig.cpp @@ -0,0 +1,36 @@ +#include "pch.h" + +#include +#include +#include +#include + +void trim(std::wstring& str) { + str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](wchar_t ch) { + return !std::iswspace(ch); + })); + str.erase(std::find_if(str.rbegin(), str.rend(), [](wchar_t ch) { + return !std::iswspace(ch); + }).base(), str.end()); +} + +std::wstring ReadConfig(const std::wstring& filename, const std::wstring& key) +{ + std::wstring configValue; + std::wstring line; + std::wifstream file(filename); + while (std::getline(file, line)) { + trim(line); + if (line.find(key) == 0) { + std::size_t position = line.find(L"=", key.size()); + if (position != std::string::npos) { + configValue = line.substr(position + 1); + trim(configValue); + break; + } + } + } + + file.close(); + return configValue; +} diff --git a/vendor/rustdesk/res/msi/CustomActions/RemotePrinter.cpp b/vendor/rustdesk/res/msi/CustomActions/RemotePrinter.cpp new file mode 100644 index 0000000..767c8c8 --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/RemotePrinter.cpp @@ -0,0 +1,517 @@ +#include "pch.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common.h" + +#pragma comment(lib, "setupapi.lib") +#pragma comment(lib, "winspool.lib") + +namespace RemotePrinter +{ +#define HRESULT_ERR_ELEMENT_NOT_FOUND 0x80070490 + + LPCWCH RD_DRIVER_INF_PATH = L"drivers\\RustDeskPrinterDriver\\RustDeskPrinterDriver.inf"; + LPCWCH RD_PRINTER_PORT = L"RustDesk Printer"; + LPCWCH RD_PRINTER_NAME = L"RustDesk Printer"; + LPCWCH RD_PRINTER_DRIVER_NAME = L"RustDesk v4 Printer Driver"; + LPCWCH XCV_MONITOR_LOCAL_PORT = L",XcvMonitor Local Port"; + + using FuncEnum = std::function; + template + using FuncOnData = std::function(const T &)>; + template + using FuncOnNoData = std::function()>; + + template + std::shared_ptr commonEnum(std::wstring funcName, FuncEnum func, DWORD level, FuncOnData onData, FuncOnNoData onNoData) + { + DWORD needed = 0; + DWORD returned = 0; + func(level, NULL, 0, &needed, &returned); + if (needed == 0) + { + return onNoData(); + } + + std::vector buffer(needed); + if (!func(level, buffer.data(), needed, &needed, &returned)) + { + return nullptr; + } + + T *pPortInfo = reinterpret_cast(buffer.data()); + for (DWORD i = 0; i < returned; i++) + { + auto r = onData(pPortInfo[i]); + if (r) + { + return r; + } + } + return onNoData(); + } + + BOOL isNameEqual(LPCWSTR lhs, LPCWSTR rhs) + { + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrcmpiw + // For some locales, the lstrcmpi function may be insufficient. + // If this occurs, use `CompareStringEx` to ensure proper comparison. + // For example, in Japan call with the NORM_IGNORECASE, NORM_IGNOREKANATYPE, and NORM_IGNOREWIDTH values to achieve the most appropriate non-exact string comparison. + // Note that specifying these values slows performance, so use them only when necessary. + // + // No need to consider `CompareStringEx` for now. + return lstrcmpiW(lhs, rhs) == 0 ? TRUE : FALSE; + } + + BOOL enumPrinterPort( + DWORD level, + LPBYTE pPortInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumports + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPortsW(NULL, level, pPortInfo, cbBuf, pcbNeeded, pcReturned); + } + + BOOL isPortExists(LPCWSTR port) + { + auto onData = [port](const PORT_INFO_2 &info) + { + if (isNameEqual(info.pPortName, port) == TRUE) { + return std::shared_ptr(new BOOL(TRUE)); + } + else { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPortsW", enumPrinterPort, 2, onData, onNoData); + if (res == nullptr) + { + return false; + } + else + { + return *res; + } + } + + BOOL executeOnLocalPort(LPCWSTR port, LPCWSTR command) + { + PRINTER_DEFAULTSW dft = {0}; + dft.DesiredAccess = SERVER_WRITE; + HANDLE hMonitor = NULL; + if (OpenPrinterW(const_cast(XCV_MONITOR_LOCAL_PORT), &hMonitor, &dft) == FALSE) + { + return FALSE; + } + + DWORD outputNeeded = 0; + DWORD status = 0; + if (XcvDataW(hMonitor, command, (LPBYTE)port, (lstrlenW(port) + 1) * 2, NULL, 0, &outputNeeded, &status) == FALSE) + { + ClosePrinter(hMonitor); + return FALSE; + } + + ClosePrinter(hMonitor); + return TRUE; + } + + BOOL addLocalPort(LPCWSTR port) + { + return executeOnLocalPort(port, L"AddPort"); + } + + BOOL deleteLocalPort(LPCWSTR port) + { + return executeOnLocalPort(port, L"DeletePort"); + } + + BOOL checkAddLocalPort(LPCWSTR port) + { + if (!isPortExists(port)) + { + return addLocalPort(port); + } + return TRUE; + } + + std::wstring getPrinterInstalledOnPort(LPCWSTR port); + + BOOL checkDeleteLocalPort(LPCWSTR port) + { + if (isPortExists(port)) + { + if (getPrinterInstalledOnPort(port) != L"") + { + WcaLog(LOGMSG_STANDARD, "The printer is installed on the port. Please remove the printer first.\n"); + return FALSE; + } + return deleteLocalPort(port); + } + return TRUE; + } + + BOOL enumPrinterDriver( + DWORD level, + LPBYTE pDriverInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinterdrivers + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPrinterDriversW( + NULL, + NULL, + level, + pDriverInfo, + cbBuf, + pcbNeeded, + pcReturned); + } + + DWORDLONG getInstalledDriverVersion(LPCWSTR name) + { + auto onData = [name](const DRIVER_INFO_6W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new DWORDLONG(info.dwlDriverVersion)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrinterDriversW", enumPrinterDriver, 6, onData, onNoData); + if (res == nullptr) + { + return 0; + } + else + { + return *res; + } + } + + std::wstring findInf(LPCWSTR name) + { + auto onData = [name](const DRIVER_INFO_8W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new std::wstring(info.pszInfPath)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrinterDriversW", enumPrinterDriver, 8, onData, onNoData); + if (res == nullptr) + { + return L""; + } + else + { + return *res; + } + } + + BOOL deletePrinterDriver(LPCWSTR name) + { + // If the printer is used after the spooler service is started. E.g., printing a document through RustDesk Printer. + // `DeletePrinterDriverExW()` may fail with `ERROR_PRINTER_DRIVER_IN_USE`(3001, 0xBB9). + // We can only ignore this error for now. + // Though restarting the spooler service is a solution, it's not a good idea to restart the service. + // + // Deleting the printer driver after deleting the printer is a common practice. + // No idea why `DeletePrinterDriverExW()` fails with `ERROR_UNKNOWN_PRINTER_DRIVER` after using the printer once. + // https://github.com/ChromiumWebApps/chromium/blob/c7361d39be8abd1574e6ce8957c8dbddd4c6ccf7/cloud_print/virtual_driver/win/install/setup.cc#L422 + // AnyDesk printer driver and the simplest printer driver also have the same issue. + BOOL res = DeletePrinterDriverExW(NULL, NULL, const_cast(name), DPD_DELETE_ALL_FILES, 0); + if (res == FALSE) + { + DWORD error = GetLastError(); + if (error == ERROR_UNKNOWN_PRINTER_DRIVER) + { + return TRUE; + } + else + { + WcaLog(LOGMSG_STANDARD, "Failed to delete printer driver. Error (%d)\n", error); + } + } + return res; + } + + BOOL deletePrinterDriverPackage(const std::wstring &inf) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/deleteprinterdriverpackage + // This function is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + int tries = 3; + HRESULT result = S_FALSE; + while ((result = DeletePrinterDriverPackage(NULL, inf.c_str(), NULL)) != S_OK) + { + if (result == HRESULT_ERR_ELEMENT_NOT_FOUND) + { + return TRUE; + } + + WcaLog(LOGMSG_STANDARD, "Failed to delete printer driver package. HRESULT (%d)\n", result); + tries--; + if (tries <= 0) + { + return FALSE; + } + Sleep(2000); + } + return S_OK; + } + + BOOL uninstallDriver(LPCWSTR name) + { + auto infFile = findInf(name); + if (!deletePrinterDriver(name)) + { + return FALSE; + } + if (infFile != L"" && !deletePrinterDriverPackage(infFile)) + { + return FALSE; + } + return TRUE; + } + + BOOL installDriver(LPCWSTR name, LPCWSTR inf) + { + DWORD size = MAX_PATH * 10; + wchar_t package_path[MAX_PATH * 10] = {0}; + HRESULT result = UploadPrinterDriverPackage( + NULL, inf, NULL, + UPDP_SILENT_UPLOAD | UPDP_UPLOAD_ALWAYS, NULL, package_path, &size); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Uploading the printer driver package to the driver cache silently, failed. Will retry with user UI. HRESULT (%d)\n", result); + result = UploadPrinterDriverPackage( + NULL, inf, NULL, UPDP_UPLOAD_ALWAYS, + GetForegroundWindow(), package_path, &size); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Uploading the printer driver package to the driver cache failed with user UI. Aborting...\n"); + return FALSE; + } + } + + result = InstallPrinterDriverFromPackage( + NULL, package_path, name, NULL, IPDFP_COPY_ALL_FILES); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Installing the printer driver failed. HRESULT (%d)\n", result); + } + return result == S_OK; + } + + BOOL enumLocalPrinter( + DWORD level, + LPBYTE pPrinterInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinters + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, level, pPrinterInfo, cbBuf, pcbNeeded, pcReturned); + } + + BOOL isPrinterAdded(LPCWSTR name) + { + auto onData = [name](const PRINTER_INFO_1W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new BOOL(TRUE)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrintersW", enumLocalPrinter, 1, onData, onNoData); + if (res == nullptr) + { + return FALSE; + } + else + { + return *res; + } + } + + std::wstring getPrinterInstalledOnPort(LPCWSTR port) + { + auto onData = [port](const PRINTER_INFO_2W &info) + { + if (isNameEqual(port, info.pPortName) == TRUE) + { + return std::shared_ptr(new std::wstring(info.pPrinterName)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrintersW", enumLocalPrinter, 2, onData, onNoData); + if (res == nullptr) + { + return L""; + } + else + { + return *res; + } + } + + BOOL addPrinter(LPCWSTR name, LPCWSTR driver, LPCWSTR port) + { + PRINTER_INFO_2W printerInfo = {0}; + printerInfo.pPrinterName = const_cast(name); + printerInfo.pPortName = const_cast(port); + printerInfo.pDriverName = const_cast(driver); + printerInfo.pPrintProcessor = const_cast(L"WinPrint"); + printerInfo.pDatatype = const_cast(L"RAW"); + printerInfo.Attributes = PRINTER_ATTRIBUTE_LOCAL; + HANDLE hPrinter = AddPrinterW(NULL, 2, (LPBYTE)&printerInfo); + return hPrinter == NULL ? FALSE : TRUE; + } + + VOID deletePrinter(LPCWSTR name) + { + PRINTER_DEFAULTSW dft = {0}; + dft.DesiredAccess = PRINTER_ALL_ACCESS; + HANDLE hPrinter = NULL; + if (OpenPrinterW(const_cast(name), &hPrinter, &dft) == FALSE) + { + DWORD error = GetLastError(); + if (error == ERROR_INVALID_PRINTER_NAME) + { + return; + } + WcaLog(LOGMSG_STANDARD, "Failed to open printer. error (%d)\n", error); + return; + } + + if (SetPrinterW(hPrinter, 0, NULL, PRINTER_CONTROL_PURGE) == FALSE) + { + ClosePrinter(hPrinter); + WcaLog(LOGMSG_STANDARD, "Failed to purge printer queue. error (%d)\n", GetLastError()); + return; + } + + if (DeletePrinter(hPrinter) == FALSE) + { + ClosePrinter(hPrinter); + WcaLog(LOGMSG_STANDARD, "Failed to delete printer. error (%d)\n", GetLastError()); + return; + } + + ClosePrinter(hPrinter); + } + + bool FileExists(const std::wstring &filePath) + { + DWORD fileAttributes = GetFileAttributes(filePath.c_str()); + return (fileAttributes != INVALID_FILE_ATTRIBUTES && !(fileAttributes & FILE_ATTRIBUTE_DIRECTORY)); + } + + // Steps: + // 1. Add the local port. + // 2. Check if the driver is installed. + // Uninstall the existing driver if it is installed. + // We should not check the driver version because the driver is deployed with the application. + // It's better to uninstall the existing driver and install the driver from the application. + // 3. Add the printer. + VOID installUpdatePrinter(const std::wstring &installFolder) + { + const std::wstring infFile = installFolder + L"\\" + RemotePrinter::RD_DRIVER_INF_PATH; + if (!FileExists(infFile)) + { + WcaLog(LOGMSG_STANDARD, "Printer driver INF file not found, aborting...\n"); + return; + } + + if (!checkAddLocalPort(RD_PRINTER_PORT)) + { + WcaLog(LOGMSG_STANDARD, "Failed to check add local port, error (%d)\n", GetLastError()); + return; + } + else + { + WcaLog(LOGMSG_STANDARD, "Local port added successfully\n"); + } + + if (getInstalledDriverVersion(RD_PRINTER_DRIVER_NAME) > 0) + { + deletePrinter(RD_PRINTER_NAME); + if (FALSE == uninstallDriver(RD_PRINTER_DRIVER_NAME)) + { + WcaLog(LOGMSG_STANDARD, "Failed to uninstall previous printer driver, error (%d)\n", GetLastError()); + } + } + + if (FALSE == installDriver(RD_PRINTER_DRIVER_NAME, infFile.c_str())) + { + WcaLog(LOGMSG_STANDARD, "Driver installation failed, still try to add the printer\n"); + } + else + { + WcaLog(LOGMSG_STANDARD, "Driver installed successfully\n"); + } + + if (FALSE == addPrinter(RD_PRINTER_NAME, RD_PRINTER_DRIVER_NAME, RD_PRINTER_PORT)) + { + WcaLog(LOGMSG_STANDARD, "Failed to add printer, error (%d)\n", GetLastError()); + } + else + { + WcaLog(LOGMSG_STANDARD, "Printer installed successfully\n"); + } + } + + VOID uninstallPrinter() + { + deletePrinter(RD_PRINTER_NAME); + WcaLog(LOGMSG_STANDARD, "Deleted the printer\n"); + uninstallDriver(RD_PRINTER_DRIVER_NAME); + WcaLog(LOGMSG_STANDARD, "Uninstalled the printer driver\n"); + checkDeleteLocalPort(RD_PRINTER_PORT); + WcaLog(LOGMSG_STANDARD, "Deleted the local port\n"); + } +} diff --git a/vendor/rustdesk/res/msi/CustomActions/ServiceUtils.cpp b/vendor/rustdesk/res/msi/CustomActions/ServiceUtils.cpp new file mode 100644 index 0000000..38d0d1d --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/ServiceUtils.cpp @@ -0,0 +1,175 @@ +// https://learn.microsoft.com/en-us/windows/win32/services/installing-a-service + +#include "pch.h" + +#include +#include +#include + +bool MyCreateServiceW(LPCWSTR serviceName, LPCWSTR displayName, LPCWSTR binaryPath) +{ + SC_HANDLE schSCManager; + SC_HANDLE schService; + + // Get a handle to the SCM database. + schSCManager = OpenSCManagerW( + NULL, // local computer + NULL, // ServicesActive database + SC_MANAGER_ALL_ACCESS); // full access rights + + if (NULL == schSCManager) + { + WcaLog(LOGMSG_STANDARD, "OpenSCManager failed (%d)\n", GetLastError()); + return false; + } + + // Create the service + schService = CreateServiceW( + schSCManager, // SCM database + serviceName, // name of service + displayName, // service name to display + SERVICE_ALL_ACCESS, // desired access + SERVICE_WIN32_OWN_PROCESS, // service type + SERVICE_AUTO_START, // start type + SERVICE_ERROR_NORMAL, // error control type + binaryPath, // path to service's binary + NULL, // no load ordering group + NULL, // no tag identifier + NULL, // no dependencies + NULL, // LocalSystem account + NULL); // no password + if (schService == NULL) + { + WcaLog(LOGMSG_STANDARD, "CreateService failed (%d)\n", GetLastError()); + CloseServiceHandle(schSCManager); + return false; + } + else + { + WcaLog(LOGMSG_STANDARD, "Service installed successfully\n"); + } + + CloseServiceHandle(schService); + CloseServiceHandle(schSCManager); + return true; +} + +bool MyDeleteServiceW(LPCWSTR serviceName) +{ + SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT); + if (hSCManager == NULL) { + WcaLog(LOGMSG_STANDARD, "Failed to open Service Control Manager, error: 0x%02X", GetLastError()); + return false; + } + + SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_STOP | DELETE); + if (hService == NULL) { + WcaLog(LOGMSG_STANDARD, "Failed to open service: %ls, error: 0x%02X", serviceName, GetLastError()); + CloseServiceHandle(hSCManager); + return false; + } + + SERVICE_STATUS serviceStatus; + if (ControlService(hService, SERVICE_CONTROL_STOP, &serviceStatus)) { + WcaLog(LOGMSG_STANDARD, "Stopping service: %ls", serviceName); + } + + bool success = DeleteService(hService); + if (!success) { + WcaLog(LOGMSG_STANDARD, "Failed to delete service: %ls, error: 0x%02X", serviceName, GetLastError()); + } + + CloseServiceHandle(hService); + CloseServiceHandle(hSCManager); + + return success; +} + +bool MyStartServiceW(LPCWSTR serviceName) +{ + SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT); + if (hSCManager == NULL) { + WcaLog(LOGMSG_STANDARD, "Failed to open Service Control Manager, error: 0x%02X", GetLastError()); + return false; + } + + SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_START); + if (hService == NULL) { + WcaLog(LOGMSG_STANDARD, "Failed to open service: %ls, error: 0x%02X", serviceName, GetLastError()); + CloseServiceHandle(hSCManager); + return false; + } + + bool success = StartServiceW(hService, 0, NULL); + if (!success) { + WcaLog(LOGMSG_STANDARD, "Failed to start service: %ls, error: 0x%02X", serviceName, GetLastError()); + } + + CloseServiceHandle(hService); + CloseServiceHandle(hSCManager); + + return success; +} + +bool MyStopServiceW(LPCWSTR serviceName) +{ + SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT); + if (hSCManager == NULL) { + WcaLog(LOGMSG_STANDARD, "Failed to open Service Control Manager"); + return false; + } + + SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_STOP); + if (hService == NULL) { + WcaLog(LOGMSG_STANDARD, "Failed to open service: %ls", serviceName); + CloseServiceHandle(hSCManager); + return false; + } + + SERVICE_STATUS serviceStatus; + if (!ControlService(hService, SERVICE_CONTROL_STOP, &serviceStatus)) { + WcaLog(LOGMSG_STANDARD, "Failed to stop service: %ls", serviceName); + CloseServiceHandle(hService); + CloseServiceHandle(hSCManager); + return false; + } + + CloseServiceHandle(hService); + CloseServiceHandle(hSCManager); + + return true; +} + +bool QueryServiceStatusExW(LPCWSTR serviceName, SERVICE_STATUS_PROCESS* status) +{ + SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT); + if (hSCManager == NULL) { + WcaLog(LOGMSG_STANDARD, "Failed to open Service Control Manager"); + return false; + } + + SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_QUERY_STATUS); + if (hService == NULL) { + WcaLog(LOGMSG_STANDARD, "Failed to open service: %ls", serviceName); + CloseServiceHandle(hSCManager); + return false; + } + + DWORD bytesNeeded; + BOOL success = QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, reinterpret_cast(status), sizeof(*status), &bytesNeeded); + if (!success) { + WcaLog(LOGMSG_STANDARD, "Failed to query service: %ls", serviceName); + } + + CloseServiceHandle(hService); + CloseServiceHandle(hSCManager); + + return success; +} + +bool IsServiceRunningW(LPCWSTR serviceName) +{ + SERVICE_STATUS_PROCESS serviceStatus; + QueryServiceStatusExW(serviceName, &serviceStatus); + return (serviceStatus.dwCurrentState == SERVICE_RUNNING); +} diff --git a/vendor/rustdesk/res/msi/CustomActions/dllmain.cpp b/vendor/rustdesk/res/msi/CustomActions/dllmain.cpp new file mode 100644 index 0000000..7288d7c --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/dllmain.cpp @@ -0,0 +1,26 @@ +// dllmain.cpp : Defines the entry point for the DLL application. +#include "pch.h" + +BOOL APIENTRY DllMain( + __in HMODULE hModule, + __in DWORD ulReasonForCall, + __in LPVOID +) +{ + switch (ulReasonForCall) + { + case DLL_PROCESS_ATTACH: + WcaGlobalInitialize(hModule); + break; + + case DLL_PROCESS_DETACH: + WcaGlobalFinalize(); + break; + + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + } + + return TRUE; +} diff --git a/vendor/rustdesk/res/msi/CustomActions/framework.h b/vendor/rustdesk/res/msi/CustomActions/framework.h new file mode 100644 index 0000000..4cd0bc4 --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/framework.h @@ -0,0 +1,10 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +// Windows Header Files +#include +#include +#include + +// WiX Header Files: +#include diff --git a/vendor/rustdesk/res/msi/CustomActions/packages.config b/vendor/rustdesk/res/msi/CustomActions/packages.config new file mode 100644 index 0000000..e25f832 --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/vendor/rustdesk/res/msi/CustomActions/pch.cpp b/vendor/rustdesk/res/msi/CustomActions/pch.cpp new file mode 100644 index 0000000..64b7eef --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/vendor/rustdesk/res/msi/CustomActions/pch.h b/vendor/rustdesk/res/msi/CustomActions/pch.h new file mode 100644 index 0000000..885d5d6 --- /dev/null +++ b/vendor/rustdesk/res/msi/CustomActions/pch.h @@ -0,0 +1,13 @@ +// pch.h: This is a precompiled header file. +// Files listed below are compiled only once, improving build performance for future builds. +// This also affects IntelliSense performance, including code completion and many code browsing features. +// However, files listed here are ALL re-compiled if any one of them is updated between builds. +// Do not add files here that you will be updating frequently as this negates the performance advantage. + +#ifndef PCH_H +#define PCH_H + +// add headers that you want to pre-compile here +#include "framework.h" + +#endif //PCH_H diff --git a/vendor/rustdesk/res/msi/Package/Components/Folders.wxs b/vendor/rustdesk/res/msi/Package/Components/Folders.wxs new file mode 100644 index 0000000..de9edb7 --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Components/Folders.wxs @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/Components/Regs.wxs b/vendor/rustdesk/res/msi/Package/Components/Regs.wxs new file mode 100644 index 0000000..33d587b --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Components/Regs.wxs @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/Components/RustDesk.wxs b/vendor/rustdesk/res/msi/Package/Components/RustDesk.wxs new file mode 100644 index 0000000..337e84e --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Components/RustDesk.wxs @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/Fragments/AddRemoveProperties.wxs b/vendor/rustdesk/res/msi/Package/Fragments/AddRemoveProperties.wxs new file mode 100644 index 0000000..ac1d85a --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Fragments/AddRemoveProperties.wxs @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/Fragments/CustomActions.wxs b/vendor/rustdesk/res/msi/Package/Fragments/CustomActions.wxs new file mode 100644 index 0000000..3727c0d --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Fragments/CustomActions.wxs @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/Fragments/ShortcutProperties.wxs b/vendor/rustdesk/res/msi/Package/Fragments/ShortcutProperties.wxs new file mode 100644 index 0000000..dafd7de --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Fragments/ShortcutProperties.wxs @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/Fragments/Upgrades.wxs b/vendor/rustdesk/res/msi/Package/Fragments/Upgrades.wxs new file mode 100644 index 0000000..8109efd --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Fragments/Upgrades.wxs @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/Includes.wxi b/vendor/rustdesk/res/msi/Package/Includes.wxi new file mode 100644 index 0000000..4b43f74 --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Includes.wxi @@ -0,0 +1,7 @@ + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/Language/Package.en-us.wxl b/vendor/rustdesk/res/msi/Package/Language/Package.en-us.wxl new file mode 100644 index 0000000..c65a512 --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Language/Package.en-us.wxl @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/Language/WixExt_en-us.wxl b/vendor/rustdesk/res/msi/Package/Language/WixExt_en-us.wxl new file mode 100644 index 0000000..fb1da89 --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Language/WixExt_en-us.wxl @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vendor/rustdesk/res/msi/Package/License.rtf b/vendor/rustdesk/res/msi/Package/License.rtf new file mode 100644 index 0000000..8943cca --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/License.rtf @@ -0,0 +1,303 @@ +{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff1\deff0\stshfdbch31505\stshfloch31506\stshfhich31506\stshfbi0\deflang1033\deflangfe2052\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f1\fbidi \fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;} +{\f34\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria Math;}{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;} +{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;} +{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f45\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f46\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\f48\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f49\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f50\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f51\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\f52\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f53\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f55\fbidi \fswiss\fcharset238\fprq2 Arial CE;}{\f56\fbidi \fswiss\fcharset204\fprq2 Arial Cyr;} +{\f58\fbidi \fswiss\fcharset161\fprq2 Arial Greek;}{\f59\fbidi \fswiss\fcharset162\fprq2 Arial Tur;}{\f60\fbidi \fswiss\fcharset177\fprq2 Arial (Hebrew);}{\f61\fbidi \fswiss\fcharset178\fprq2 Arial (Arabic);} +{\f62\fbidi \fswiss\fcharset186\fprq2 Arial Baltic;}{\f63\fbidi \fswiss\fcharset163\fprq2 Arial (Vietnamese);}{\f385\fbidi \froman\fcharset238\fprq2 Cambria Math CE;}{\f386\fbidi \froman\fcharset204\fprq2 Cambria Math Cyr;} +{\f388\fbidi \froman\fcharset161\fprq2 Cambria Math Greek;}{\f389\fbidi \froman\fcharset162\fprq2 Cambria Math Tur;}{\f392\fbidi \froman\fcharset186\fprq2 Cambria Math Baltic;}{\f393\fbidi \froman\fcharset163\fprq2 Cambria Math (Vietnamese);} +{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;}{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;} +{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;}{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;}{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);} +{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);}{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;}{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);} +{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} +{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;} +{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;} +{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}} +{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0; +\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\red0\green0\blue0;\red0\green0\blue0;}{\*\defchp \fs22\kerning2\loch\af31506\hich\af31506\dbch\af31505 }{\*\defpap +\ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\upr{\stylesheet{\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0 +\fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 \snext0 \sqformat \spriority0 Normal;}{\s1\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel0\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0 +\fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 \sbasedon0 \snext0 \slink15 \sqformat heading 1;}{\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 +\ltrch\fcs0 \fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 \sbasedon0 \snext0 \slink16 \sqformat heading 2;}{\s3\ql \li0\ri0\sb240\sa60\keepn\nowidctlpar\wrapdefault\faauto\outlinelevel2\rin0\lin0\itap0 \rtlch\fcs1 +\ab\af0\afs26\alang1025 \ltrch\fcs0 \b\fs26\lang1031\langfe2052\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1031\langfenp2052 \sbasedon0 \snext0 \slink17 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid2828669 heading 3;}{\*\cs10 \additive +\ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* +\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1 +\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe2052\kerning2\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp2052 +\snext11 \ssemihidden \sunhideused Normal Table;}{\*\cs15 \additive \rtlch\fcs1 \ab\af0\afs32 \ltrch\fcs0 \b\fs32\lang1031\langfe0\kerning32\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0 \sbasedon10 \slink1 \spriority9 ?? 1 ??;}{\*\cs16 +\additive \rtlch\fcs1 \ab\ai\af0\afs28 \ltrch\fcs0 \b\i\fs28\lang1031\langfe0\kerning0\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0 \sbasedon10 \slink2 \ssemihidden \spriority9 ?? 2 ??;}{\*\cs17 \additive \rtlch\fcs1 \ab\af0\afs26 +\ltrch\fcs0 \b\fs26\lang1031\langfe0\kerning0\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0 \sbasedon10 \slink3 \ssemihidden \spriority9 \styrsid2828669 ?? 3 ??;}}{\*\ud\uc0{\stylesheet{ +\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0 \fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 \snext0 \sqformat \spriority0 Normal;}{ +\s1\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel0\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0 \fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 \sbasedon0 \snext0 \slink15 \sqformat +heading 1;}{\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0 \fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 +\sbasedon0 \snext0 \slink16 \sqformat heading 2;}{\s3\ql \li0\ri0\sb240\sa60\keepn\nowidctlpar\wrapdefault\faauto\outlinelevel2\rin0\lin0\itap0 \rtlch\fcs1 \ab\af0\afs26\alang1025 \ltrch\fcs0 +\b\fs26\lang1031\langfe2052\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1031\langfenp2052 \sbasedon0 \snext0 \slink17 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid2828669 heading 3;}{\*\cs10 \additive +\ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* +\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1 +\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe2052\kerning2\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp2052 +\snext11 \ssemihidden \sunhideused Normal Table;}{\*\cs15 \additive \rtlch\fcs1 \ab\af0\afs32 \ltrch\fcs0 \b\fs32\lang1031\langfe0\kerning32\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0 \sbasedon10 \slink1 \spriority9 +{\uc1\u26631 ?\u-26472 ? 1 \u23383 ?\u31526 ?};}{\*\cs16 \additive \rtlch\fcs1 \ab\ai\af0\afs28 \ltrch\fcs0 \b\i\fs28\lang1031\langfe0\kerning0\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0 \sbasedon10 \slink2 \ssemihidden \spriority9 +{\uc1\u26631 ?\u-26472 ? 2 \u23383 ?\u31526 ?};}{\*\cs17 \additive \rtlch\fcs1 \ab\af0\afs26 \ltrch\fcs0 \b\fs26\lang1031\langfe0\kerning0\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0 +\sbasedon10 \slink3 \ssemihidden \spriority9 \styrsid2828669 {\uc1\u26631 ?\u-26472 ? 3 \u23383 ?\u31526 ?};}}}}{\*\listtable{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0 +\levelindent0{\leveltext\leveltemplateid67698713\'02\'00.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fbias0 \fi-360\li720\lin720 }{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0 +{\leveltext\leveltemplateid67698713\'02\'01.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li1440\lin1440 }{\listlevel\levelnfc2\levelnfcn2\leveljc2\leveljcn2\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext +\leveltemplateid67698715\'02\'02.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-180\li2160\lin2160 }{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext +\leveltemplateid67698703\'02\'03.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li2880\lin2880 }{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext +\leveltemplateid67698713\'02\'04.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li3600\lin3600 }{\listlevel\levelnfc2\levelnfcn2\leveljc2\leveljcn2\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext +\leveltemplateid67698715\'02\'05.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-180\li4320\lin4320 }{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext +\leveltemplateid67698703\'02\'06.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li5040\lin5040 }{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext +\leveltemplateid67698713\'02\'07.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li5760\lin5760 }{\listlevel\levelnfc2\levelnfcn2\leveljc2\leveljcn2\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext +\leveltemplateid67698715\'02\'08.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-180\li6480\lin6480 }{\listname ;}\listid825630566}}{\*\listoverridetable{\listoverride\listid825630566\listoverridecount0\ls1}}{\*\pgptbl {\pgp\ipgp3\itap0\li0\ri0\sb0 +\sa0}{\pgp\ipgp1\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp2\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}}{\*\rsidtbl \rsid83947\rsid598512\rsid1001100\rsid1384617\rsid1523795\rsid1598568\rsid1917520\rsid2380571 +\rsid2828669\rsid3408922\rsid3425199\rsid3630109\rsid3677587\rsid3958023\rsid4071099\rsid4291156\rsid4471570\rsid5244407\rsid5732487\rsid6045443\rsid6178595\rsid7167559\rsid8404575\rsid8598301\rsid8797129\rsid8979511\rsid9005387\rsid9112532\rsid9119790 +\rsid9381137\rsid9706100\rsid9788126\rsid9898965\rsid10905159\rsid11670652\rsid11828428\rsid12264694\rsid12287738\rsid12650743\rsid12661227\rsid12936610\rsid13186388\rsid13721198\rsid13789023\rsid14101758\rsid14699391\rsid15087333\rsid15155177 +\rsid15274734\rsid15295953\rsid15335801\rsid16412676}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\operator hoosm}{\creatim\yr2024\mo3\dy30\hr15\min56} +{\revtim\yr2024\mo3\dy30\hr16\min44}{\version52}{\edmins17}{\nofpages2}{\nofwords1021}{\nofchars5820}{\nofcharsws6828}{\vern75}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}} +\paperw12240\paperh15840\margl1440\margr1440\margt1440\margb1440\gutter0\ltrsect +\widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont0\relyonvml0\donotembedlingdata1\grfdocevents0\validatexml0\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors0\horzdoc\dghspace120\dgvspace120\dghorigin1701 +\dgvorigin1984\dghshow0\dgvshow3\jcompress\viewkind1\viewscale150\rsidroot598512 \fet0{\*\wgrffmtfilter 2450}\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\sectdefaultcl\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2 +\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6 +\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang +{\pntxtb (}{\pntxta )}}\pard\plain \ltrpar\s2\qc \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0 +\fs24\lang1031\langfe2052\loch\af1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 {\rtlch\fcs1 \ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid2380571\charrsid2380571 \hich\af1\dbch\af31505\loch\f1 Privacy policy}{\rtlch\fcs1 +\ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid1917520 +\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid8979511 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \hich\af1\dbch\af31505\loch\f1 +\hich\f1 This Privacy Policy (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Policy}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 +\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which cStudio GmbH (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 +\b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 us}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 +\hich\f1 or \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 we}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 +\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 +), processes personal data in connection with the activities and services concerning the operation of the website rustdesk.com and other websites or social media profiles run and managed by us (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 +\b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Websites}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94 +\loch\f1 ). +\par \hich\af1\dbch\af31505\loch\f1 We are serious about protecting your personal data and want you to feel safe and comfortable while browsing our Websites. We therefore respect \hich\af1\dbch\af31505\loch\f1 +the confidentiality of your personal data and always proceed in accordance with the provisions of data protection legislation, in particular, Regulation (EU) 2016/679 of the European Parliament and of the Council (General Data Protection Regulation, herei +\hich\af1\dbch\af31505\loch\f1 \hich\f1 nafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 GDPR}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 +\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 ), and follow this Policy. +\par \hich\af1\dbch\af31505\loch\f1 With respect to the above, we use this Policy to inform you about how, for what purposes and to what extent we use your personal data and what information about you as a user of the Websites we may process. +\par }\pard \ltrpar\s2\ql \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1917520 \hich\af1\dbch\af31505\loch\f1 0. Def +\hich\af1\dbch\af31505\loch\f1 initions. +\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid1523795 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1 +\'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Personal data}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 +\fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1 \'94\hich\af1\dbch\af31505\loch\f1 means any information relating to a data subject; +\par \loch\af1\dbch\af31505\hich\f1 \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Controller}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 +\fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 + means the natural or legal person, public authority, agency or other body which, alone or jointly with others, determines the purposes and means of the processing of personal data; +\par \loch\af1\dbch\af31505\hich\f1 \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Data subject}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 +\fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 + means any identified or identifiable person who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number, location data, an online identifier or to one or more factors specific to t +\hich\af1\dbch\af31505\loch\f1 he physical, physiological, genetic, mental, economic, cultural or social identity of that natural person; +\par \loch\af1\dbch\af31505\hich\f1 \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Data processor}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 +\fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 means a natural or legal person, public authority, agency or other body which processes personal data on behalf of the controller; +\par \loch\af1\dbch\af31505\hich\f1 \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Processing}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 +\fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 + means any operation or set of operations which is performed on personal data or on sets of personal data, whether or not by automated means, such as collection, recording, organi}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 +\fs18\lang1033\langfe2052\langnp1033\insrsid14101758 \hich\af1\dbch\af31505\loch\f1 z}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 +ation, structuring, storage, adaptation or alteration, retrieval,\hich\af1\dbch\af31505\loch\f1 consultation, use, disclosure by transmission, dissemination or otherwise making available, alignment or combination, restriction, erasure or destruction. + +\par }\pard \ltrpar\s2\ql \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1917520 \hich\af1\dbch\af31505\loch\f1 1. }{\rtlch\fcs1 +\ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid13186388\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Basic information about personal data processing conducted by us}{\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 +\b\fs18\lang1033\langfe2052\langnp1033\insrsid6045443 .}{\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1917520 +\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid12650743 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid12650743\charrsid12650743 +\hich\af1\dbch\af31505\loch\f1 +We always process your personal data lawfully, fairly, in a transparent manner and for specified, explicit and legitimate purposes. We process personal data only to the minimum necessary extent and we keep them in a form which permits your identification +\hich\af1\dbch\af31505\loch\f1 for no longer than is necessary \hich\af1\dbch\af31505\loch\f1 \hich\f1 vis-\'e0\loch\f1 -vis the purpose of the processing. +\par \hich\af1\dbch\af31505\loch\f1 We process your personal data in a manner that sufficiently ensures their integrity and confidentiality, i.e. by appropriate technical or organi}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 +\fs18\lang1033\langfe2052\langnp1033\insrsid12661227 \hich\af1\dbch\af31505\loch\f1 z}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid12650743\charrsid12650743 \hich\af1\dbch\af31505\loch\f1 +ational measures and appropriate protection against }{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid6178595 \hich\af1\dbch\af31505\loch\f1 u}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 +\fs18\lang1033\langfe2052\langnp1033\insrsid6178595\charrsid6178595 \hich\af1\dbch\af31505\loch\f1 nauthorized }{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid12650743\charrsid12650743 \hich\af1\dbch\af31505\loch\f1 +or unlawful processing and against loss, destruction or damage. We take care to ensure that personal data that are inaccurate, having regard to the purpose for which we process them, are erased or rectified without delay. +\par \hich\af1\dbch\af31505\loch\f1 We respect the principle of refraining \hich\af1\dbch\af31505\loch\f1 from personal data processing and the principle of data minimi}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid12287738 +\hich\af1\dbch\af31505\loch\f1 z}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid12650743\charrsid12650743 \hich\af1\dbch\af31505\loch\f1 +ation. We therefore only retain your personal data if it is necessary in order to achieve the purpose of the processing or for various retention periods specified by law. The relevant data are erased in accordance with the law if the relevant purpose ceas +\hich\af1\dbch\af31505\loch\f1 es to exist as a result of the withdrawal of your consent and/or upon the expiration of the lawful retention period. +\par \hich\af1\dbch\af31505\loch\f1 For the above reasons, we use computer security such as a firewall and data e\hich\af1\dbch\af31505\loch\f1 +ncryption to operate our Websites. We have implemented adequate physical, electronic and procedural safeguards and use reliable IT service providers. However, given the nature of the internet, we would like to bring to your attention the fact that certain +\hich\af1\dbch\af31505\loch\f1 security gaps may exist in the transmission of personal data via the internet (e.g. in communication via e-mail) and that full protection of personal data preventing third party access is impossible.}{\rtlch\fcs1 +\af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1917520\charrsid12650743 +\par }\pard \ltrpar\s2\ql \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1917520 \hich\af1\dbch\af31505\loch\f1 2. }{\rtlch\fcs1 +\ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid11670652\charrsid11670652 \hich\af1\dbch\af31505\loch\f1 Legal ground, purpose and extent of the processing of your personal data}{\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 +\b\fs18\lang1033\langfe2052\langnp1033\insrsid1917520 . +\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid15087333 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid4291156\charrsid4291156 \hich\af1\dbch\af31505\loch\f1 +We may process your personal data for the following legal grounds and for the following purposes:}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid4291156 +\par }\pard \ltrpar\s2\qj \li0\ri0\sb120\sa120\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid9112532 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1384617\charrsid15295953 +\hich\af1\dbch\af31505\loch\f1 a. }{\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid16412676\charrsid15295953 \hich\af1\dbch\af31505\loch\f1 Provision and improvement of and support for our Websites}{\rtlch\fcs1 +\ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid83947\charrsid15295953 +\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid1001100 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid9706100\charrsid1001100 \hich\af1\dbch\af31505\loch\f1 +We process various information about your online activity, e.g. the time of access to our Websites, the time spent on our websites, conversions (i.e. completed activity on our Websites), etc., for the purposes of technical support and improvement of our W +\hich\af1\dbch\af31505\loch\f1 ebsites as well as monitoring of functionalities thereof (for details regarding the extent of the \hich\af1\dbch\af31505\loch\f1 data being processed see Article 4 (c) - d) of this Policy). +\par \hich\af1\dbch\af31505\loch\f1 For this purpose of personal data processing, we process your personal data under the lawful ground of legitimate interest (operation of the Websites, statistical purposes and data security).}{\rtlch\fcs1 \af1\afs18 +\ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid14699391\charrsid1001100 +\par }\pard \ltrpar\s2\qj \li0\ri0\sb120\sa120\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid9112532 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid2828669 +\hich\af1\dbch\af31505\loch\f1 b. Processing of the personal data of the visitors to the Websites +\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid1001100 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid1001100 \hich\af1\dbch\af31505\loch\f1 +If you publish any personal data on our Websites, we may process such personal data to the extent published for the purpose of responding to your post. Usually, we process following personal d\hich\af1\dbch\af31505\loch\f1 +ata categories on our Websites: your name, surname and any personal data which you upload on the Websites or which we receive via personal messages. +\par \hich\af1\dbch\af31505\loch\f1 +For these purposes of personal data processing, we process the above mentioned personal data under lawful ground of negotiation and performance of a contract (customer and technical support under the RustDesk software license agreement concluded between u +\hich\af1\dbch\af31505\loch\f1 s and yourself) and legitimate interest (general communication between us and yourself). +\par }\pard \ltrpar\s2\qj \li0\ri0\sb120\sa120\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid9112532 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid2828669 +\hich\af1\dbch\af31505\loch\f1 c. Cookies +\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid1001100 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid1001100 \hich\af1\dbch\af31505\loch\f1 +We use v\hich\af1\dbch\af31505\loch\f1 +arious cookie files, which may contain your personal data (e.g. your IP address or the configuration of your browser and computer). We use cookies on the basis of your consent that you express via the cookies settings displayed to you in a banner during y +\hich\af1\dbch\af31505\loch\f1 our first visit to our Websites. This consent can be subsequently amended / withdrawn via your web browser settings (to the extended allowed by the respective browser). +\par }\pard \ltrpar\s2\qj \li0\ri0\sb120\sa120\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid9112532 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid2828669 +\hich\af1\dbch\af31505\loch\f1 d. RustDesk +\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid1001100 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid1001100 \hich\af1\dbch\af31505\loch\f1 +To provide you with the RustDesk software application and to constantly impr\hich\af1\dbch\af31505\loch\f1 +ove our services including customer support, we process following personal data about you and your device: start of the RustDesk software application, IP-address of the device, statistical information about your computer (e.g. CPU-type, screen resolution) +\hich\af1\dbch\af31505\loch\f1 , time and duration of RustDesk software sessions and RustDesk-IDs of the RustDesk\hich\f1 \rquote \loch\f1 s session participants. +\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid3630109 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid1001100 \hich\af1\dbch\af31505\loch\f1 +We process the personal data acquired via the RustDesk software under lawful grounds of performance of a contract (performance of RustDesk software license agreement concluded between us and yourself including customer support) and our legitimate interest +\hich\af1\dbch\af31505\loch\f1 (RustDesk software development).}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1917520\charrsid3630109 +\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a +9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad +5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6 +b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0 +0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6 +a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f +c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512 +0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462 +a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865 +6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b +4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b +4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100b9540503a5070000d0200000160000007468656d652f7468656d652f +7468656d65312e786d6cec595f8b1b47127f3fc87718e65dd6bf19fd592c0769247963efdac6921df2d82bb566dadb332da65bbb16c1109ca71008049290870b +1cf7720fc791c005cee41eeebb9c0f9b5cee435c75cf68a65b6ad9bb8b0fccb1bbcba2e9f955f5afabaaab4add373f7c1a53e70ca79cb0a4e7d66fd45c072733 +362749d8731f4dc7958eeb70819239a22cc13d778db9fbe1ad0f7e77131d8808c7d801f9841fa09e1b09b13ca856f90c8611bfc1963881770b96c648c0631a56 +e7293a07bd31ad366ab556354624719d04c5a0f6fe624166d8f9d78bbffffac7effff9d997f0e7dedacc31a2305122b81c98d1742267c086a0c2ce4feb12c1d7 +3ca0a9738668cf85e9e6ec7c8a9f0ad7a1880b78d1736beac7addeba594507b910157b6435b9b1fac9e57281f96943cd998627c5a49ee77bad7ea15f01a8d8c5 +8ddaa3d6a855e85300349bc14a332ea6ce7623f072ac06ca3e5a740fdbc366ddc06bfa9b3b9cfbbefc35f00a94e9f776f0e371005634f00a94e1fd1dbc3fe80e +86a67e05caf0ad1d7cbbd61f7a6d43bf02459424a73be89adf6a069bd5169005a3875678d7f7c6ed46aebc44413414d125a758b044ec8bb5183d61e918001248 +91208923d64bbc403308e60051729212e788841104de12258cc370ad511bd79af05ffe7aea93f2283ac0489396bc8009df19927c1c3e4bc952f4dc3ba0d5d520 +af5ebc78f9fce797cffff6f2f3cf5f3eff319f5ba932e40e5112ea72bffde9ebfffcf099f3efbffee1b76fbecda6dec6731dfffa2f5fbcfee51f6f520f2b2e4d +f1eabb9f5efffcd3abefbffaf5cfdf58b4f75374a2c3a724c6dcb987cf9d872c86055af8e393f47212d308115da29f841c2548ce62d13f129181beb746145970 +036cdaf1710aa9c606bcbd7a62109e44e94a108bc6bb516c008f19a303965aad7057cea59979ba4a42fbe4e94ac73d44e8cc36778012c3cba3d512722cb1a90c +226cd07c4051225088132c1cf98e9d626c59dd278418763d26b39471b610ce27c419206235c9949c18d1540a1d9218fcb2b611047f1bb6397eec0c18b5ad7a88 +cf4c24ec0d442de4a7981a66bc8d5602c53695531453dde0474844369293753ad371232ec0d321a6cc19cd31e73699fb29ac5773fa5d483376b71fd3756c2253 +414e6d3a8f10633a72c84e8308c54b1b76429248c77ec44f214491f380091bfc98993b443e831f50b2d7dd8f0936dcfdf66cf00832ac4ea90c10f966955a7c79 +1b33237e276bba40d8966afa696ca4d87e4aacd131588546681f614cd1399a63ec3cfac8c260c09686cd4bd27722c82a87d81658779019abf239c11c3baab9d9 +cd9347841b213bc121dbc3e778bd9578d6288951ba4ff33df0ba6ef31194bad81600f7e9ec5407de23d00a42bc588d729f830e2db8f76a7d1021a380c9676e8f +d7756af8ef227b0cf6e51383c605f625c8e04bcb4062d765de689b29a2c60465c04c117419b6740b2286fb4b11595c95d8ca2ab730376de906e88e8ca62726c9 +5b3ba0addec7ffdff53ed061bcfafd0f96cdf66efa1dbb6223595db2d3d9974c0eb7fa9b7db8edae2660e99cbcff4dcd10ad920718eac86ec6baee69ae7b1af7 +ffbea7d9b79faf3b997dfdc67527e3428771ddc9e4872befa693299b17e86be4814776d0a38e7de2bda73e0b42e944ac293ee2eae087c3f799f91806a59c3af8 +c4c529e032828fb2ccc104062e4c91927152263e26229a446809a74375572a0979ae3ae4ce9271383452c356dd124f57f1319b67879df5ba3cd8cc2a2b47a21c +aff9c5381c54890cdd6a970778857ac5365407ad1b0252f63224b4c94c124d0b89f666501a491deb82d12c24d4cade098bae854547aadfb86a8705502bbc025f +b81df89ade737d0f444008cee3a0399f4b3f65aede785739f35d7a7a9f318d0880067b1301a5a7bb92ebdee5c9d565a176014f1b24b470334928cba8068f47f0 +35388f4e397a111a97f575b774a9414f9a42cd07a155d26877dec4e2aabe06b9eddc40133d53d0c439efb9ada60f213343cb9ebb804363f8182f2176b8fcce85 +680817303391661bfe2a9965997231443cca0cae924e960d622270ea5012f75cb9fcc20d3451394471ab372021bcb7e4ba9056de3772e074d3c978b1c033a1bb +5d1b9196ce1e21c367b9c2fa56895f1d2c25d90adc3d89e6e7ce095da50f118498dfae4b03ce0987bb837a66cd3981cbb0229195f1b75598f2b4abdf46a918ca +c6115d4628af287a32cfe02a951774d4536103ed295f33185433495e084f42596075a31ad5b4a81a1987bd55f7ed42d2725ad22c6ba6915564d5b4673163864d +19d8b2e5d58abcc66a6362c8697a85cf52f776caed6e72dd569f5054093078613f4bd5bd4041d0a8959319d424e3dd342c73763e6ad68ecd02df42ed224542cb +faad8dda2dbb1535c23a1d0c5ea9f283dc76d4c2d062d3572a4babcb73fd629b9d3c81e431842e77450557ae846beb14414334513d499636608b3c15f9d6804f +ce2a253df7d39adff782861f546a1d7f54f19a5eadd2f1fbcd4adff79bf5915faf0d078d6750584414d7fdece27e0c1718749d5fdfabf19d2bfc7873477363c6 +e22a5357f455455c5de1d71bb62bfca9bc9c771d0249e7d35663dc6d7607ad4ab7d91f57bce1a053e906ad4165d80adac3f130f03bddf133d7395360afdf0cbc +d6a85369d583a0e2b56a927ea75b697b8d46df6bf73b23afff2c6f6360e559fac86d01e655bc6efd170000ffff0300504b0304140006000800000021000dd190 +9fb60000001b010000270000007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f7827708 +6f6fd3ba109126dd88d0add40384e4350d363f2451eced0dae2c082e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64 +b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd500199650 +9affb3fd381a89672f1f165dfe514173d9850528a2c6cce0239baa4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff00 +00001c0200001300000000000000000000000000000000005b436f6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0 +000000360100000b00000000000000000000000000300100005f72656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a000000 +1c00000000000000000000000000190200007468656d652f7468656d652f7468656d654d616e616765722e786d6c504b01022d0014000600080000002100b954 +0503a5070000d02000001600000000000000000000000000d60200007468656d652f7468656d652f7468656d65312e786d6c504b01022d001400060008000000 +21000dd1909fb60000001b0100002700000000000000000000000000af0a00007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000aa0b00000000} +{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d +617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169 +6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363 +656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e} +{\*\latentstyles\lsdstimax376\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdlocked0 heading 1;\lsdqformat1 \lsdlocked0 heading 2; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink; +\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong;\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web); +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text; +\lsdpriority39 \lsdlocked0 Table Grid;\lsdsemihidden1 \lsdlocked0 Placeholder Text;\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid; +\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2;\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2; +\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List;\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1; +\lsdpriority61 \lsdlocked0 Light List Accent 1;\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1; +\lsdsemihidden1 \lsdlocked0 Revision;\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1; +\lsdpriority72 \lsdlocked0 Colorful List Accent 1;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2; +\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3; +\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4;\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4; +\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5;\lsdpriority62 \lsdlocked0 Light Grid Accent 5; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5; +\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6; +\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis; +\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4; +\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2; +\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4; +\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4; +\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6; +\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3; +\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4; +\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4; +\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}} \ No newline at end of file diff --git a/vendor/rustdesk/res/msi/Package/Package.wixproj b/vendor/rustdesk/res/msi/Package/Package.wixproj new file mode 100644 index 0000000..17dbc4f --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Package.wixproj @@ -0,0 +1,22 @@ + + + + + Release + x64 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vendor/rustdesk/res/msi/Package/Package.wxs b/vendor/rustdesk/res/msi/Package/Package.wxs new file mode 100644 index 0000000..e11756a --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/Package.wxs @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/UI/AnotherApp.wxs b/vendor/rustdesk/res/msi/Package/UI/AnotherApp.wxs new file mode 100644 index 0000000..168d1b1 --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/UI/AnotherApp.wxs @@ -0,0 +1,15 @@ + + + +

+ + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/UI/MyInstallDirDlg.wxs b/vendor/rustdesk/res/msi/Package/UI/MyInstallDirDlg.wxs new file mode 100644 index 0000000..e4bad91 --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/UI/MyInstallDirDlg.wxs @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/Package/UI/MyInstallDlg.wxs b/vendor/rustdesk/res/msi/Package/UI/MyInstallDlg.wxs new file mode 100644 index 0000000..bf59d56 --- /dev/null +++ b/vendor/rustdesk/res/msi/Package/UI/MyInstallDlg.wxs @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/rustdesk/res/msi/msi.sln b/vendor/rustdesk/res/msi/msi.sln new file mode 100644 index 0000000..70d28fb --- /dev/null +++ b/vendor/rustdesk/res/msi/msi.sln @@ -0,0 +1,26 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34003.232 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Package", "Package\Package.wixproj", "{F403A403-CEFF-4399-B51C-CC646C8E98CF}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CustomActions", "CustomActions\CustomActions.vcxproj", "{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|x64.ActiveCfg = Release|x64 + {F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|x64.Build.0 = Release|x64 + {6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|x64.ActiveCfg = Release|x64 + {6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {95277884-55F2-4A1F-BFFB-E82EFE847DC2} + EndGlobalSection +EndGlobal diff --git a/vendor/rustdesk/res/msi/preprocess.py b/vendor/rustdesk/res/msi/preprocess.py new file mode 100644 index 0000000..c590549 --- /dev/null +++ b/vendor/rustdesk/res/msi/preprocess.py @@ -0,0 +1,560 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +import sys +import uuid +import argparse +import datetime +import subprocess +import re +import platform +from pathlib import Path +from itertools import chain +import shutil + +g_indent_unit = "\t" +g_version = "" +g_build_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") + +# Replace the following links with your own in the custom arp properties. +# https://learn.microsoft.com/en-us/windows/win32/msi/property-reference +g_arpsystemcomponent = { + "Comments": { + "msi": "ARPCOMMENTS", + "t": "string", + "v": "!(loc.AR_Comment)", + }, + "Contact": { + "msi": "ARPCONTACT", + "v": "https://github.com/rustdesk/rustdesk", + }, + "HelpLink": { + "msi": "ARPHELPLINK", + "v": "https://github.com/rustdesk/rustdesk/issues/", + }, + "ReadMe": { + "msi": "ARPREADME", + "v": "https://github.com/rustdesk/rustdesk", + }, +} + +def default_revision_version(): + return int(datetime.datetime.now().timestamp() / 60) + +def make_parser(): + parser = argparse.ArgumentParser(description="Msi preprocess script.") + parser.add_argument( + "-d", + "--dist-dir", + type=str, + default="../../rustdesk", + help="The dist directory to install.", + ) + parser.add_argument( + "--arp", + action="store_true", + help="Is ARPSYSTEMCOMPONENT", + default=False, + ) + parser.add_argument( + "--custom-arp", + type=str, + default="{}", + help='Custom arp properties, e.g. \'["Comments": {"msi": "ARPCOMMENTS", "v": "Remote control application."}]\'', + ) + parser.add_argument( + "-c", "--custom", action="store_true", help="Is custom client", default=False + ) + parser.add_argument( + "--conn-type", + type=str, + default="", + help='Connection type, e.g. "incoming", "outgoing". Default is empty, means incoming-outgoing', + ) + parser.add_argument( + "--app-name", type=str, default="RustDesk", help="The app name." + ) + parser.add_argument( + "-v", "--version", type=str, default="", help="The app version." + ) + parser.add_argument( + "--revision-version", type=int, default=default_revision_version(), help="The revision version." + ) + parser.add_argument( + "-m", + "--manufacturer", + type=str, + default="PURSLANE", + help="The app manufacturer.", + ) + return parser + + +def read_lines_and_start_index(file_path, tag_start, tag_end): + with open(file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + index_start = -1 + index_end = -1 + for i, line in enumerate(lines): + if tag_start in line: + index_start = i + if tag_end in line: + index_end = i + + if index_start == -1: + print(f'Error: start tag "{tag_start}" not found') + return None, None + if index_end == -1: + print(f'Error: end tag "{tag_end}" not found') + return None, None + return lines, index_start + + +def insert_components_between_tags(lines, index_start, app_name, dist_dir): + indent = g_indent_unit * 3 + path = Path(dist_dir) + idx = 1 + for file_path in path.glob("**/*"): + if file_path.is_file(): + if file_path.name.lower() == f"{app_name}.exe".lower(): + continue + + subdir = str(file_path.parent.relative_to(path)) + dir_attr = "" + if subdir != ".": + dir_attr = f'Subdirectory="{subdir}"' + + # Don't generate Component Id and File Id like 'Component_{idx}' and 'File_{idx}' + # because it will cause error + # "Error WIX0130 The primary key 'xxxx' is duplicated in table 'Directory'" + to_insert_lines = f""" +{indent} +{indent}{g_indent_unit} +{indent} +""" + lines.insert(index_start + 1, to_insert_lines[1:]) + index_start += 1 + idx += 1 + return True + + +def gen_auto_component(app_name, dist_dir): + return gen_content_between_tags( + "Package/Components/RustDesk.wxs", + "", + "", + lambda lines, index_start: insert_components_between_tags( + lines, index_start, app_name, dist_dir + ), + ) + + +def gen_pre_vars(args, dist_dir): + def func(lines, index_start): + upgrade_code = uuid.uuid5(uuid.NAMESPACE_OID, app_name + ".exe") + + indent = g_indent_unit * 1 + to_insert_lines = [ + f'{indent}\n', + f'{indent}\n', + f'{indent}\n', + f'{indent}\n', + f'{indent}\n', + f'{indent}\n', + f'{indent}\n', + f'{indent}\n', + f'{indent}\n', + "\n", + f"{indent}\n" + f'{indent}\n', + ] + + for i, line in enumerate(to_insert_lines): + lines.insert(index_start + i + 1, line) + return lines + + return gen_content_between_tags( + "Package/Includes.wxi", "", "", func + ) + + +def replace_app_name_in_langs(app_name): + langs_dir = Path(sys.argv[0]).parent.joinpath("Package/Language") + for file_path in langs_dir.glob("*.wxl"): + with open(file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + for i, line in enumerate(lines): + lines[i] = line.replace("RustDesk", app_name) + with open(file_path, "w", encoding="utf-8") as f: + f.writelines(lines) + +def replace_app_name_in_custom_actions(app_name): + custion_actions_dir = Path(sys.argv[0]).parent.joinpath("CustomActions") + for file_path in chain(custion_actions_dir.glob("*.cpp"), custion_actions_dir.glob("*.h")): + with open(file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + for i, line in enumerate(lines): + line = re.sub(r"\bRustDesk\b", app_name, line) + line = line.replace(f"{app_name} v4 Printer Driver", "RustDesk v4 Printer Driver") + lines[i] = line + with open(file_path, "w", encoding="utf-8") as f: + f.writelines(lines) + +def gen_upgrade_info(): + def func(lines, index_start): + indent = g_indent_unit * 3 + + vs = g_version.split(".") + major = vs[0] + upgrade_id = uuid.uuid4() + to_insert_lines = [ + f'{indent}\n', + f'{indent}{g_indent_unit}\n', + f"{indent}\n", + ] + + for i, line in enumerate(to_insert_lines): + lines.insert(index_start + i + 1, line) + return lines + + return gen_content_between_tags( + "Package/Fragments/Upgrades.wxs", + "", + "", + func, + ) + + +def gen_custom_dialog_bitmaps(): + def func(lines, index_start): + indent = g_indent_unit * 2 + + # https://wixtoolset.org/docs/tools/wixext/wixui/#customizing-a-dialog-set + vars = [ + "WixUIBannerBmp", + "WixUIDialogBmp", + "WixUIExclamationIco", + "WixUIInfoIco", + "WixUINewIco", + "WixUIUpIco", + ] + to_insert_lines = [] + for var in vars: + if Path(f"Package/Resources/{var}.bmp").exists(): + to_insert_lines.append( + f'{indent}\n' + ) + + for i, line in enumerate(to_insert_lines): + lines.insert(index_start + i + 1, line) + return lines + + return gen_content_between_tags( + "Package/Package.wxs", + "", + "", + func, + ) + + +def gen_custom_ARPSYSTEMCOMPONENT_False(args): + def func(lines, index_start): + indent = g_indent_unit * 2 + + lines_new = [] + lines_new.append( + f"{indent}\n" + ) + lines_new.append( + f'{indent}\n\n' + ) + + lines_new.append( + f"{indent}\n" + ) + for _, v in g_arpsystemcomponent.items(): + if "msi" in v and "v" in v: + lines_new.append( + f'{indent}\n' + ) + + for i, line in enumerate(lines_new): + lines.insert(index_start + i + 1, line) + return lines + + return gen_content_between_tags( + "Package/Fragments/AddRemoveProperties.wxs", + "", + "", + func, + ) + + +def get_folder_size(folder_path): + total_size = 0 + + folder = Path(folder_path) + for file in folder.glob("**/*"): + if file.is_file(): + total_size += file.stat().st_size + + return total_size + + +def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir): + def func(lines, index_start): + indent = g_indent_unit * 5 + + lines_new = [] + lines_new.append( + f"{indent}\n" + ) + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + installDate = datetime.datetime.now().strftime("%Y%m%d") + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + + # EstimatedSize in uninstall registry must be in KB. + estimated_size_bytes = get_folder_size(dist_dir) + estimated_size = max(1, (estimated_size_bytes + 1023) // 1024) + lines_new.append( + f'{indent}\n' + ) + + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + + vs = g_version.split(".") + major, minor, build = vs[0], vs[1], vs[2] + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + lines_new.append( + f'{indent}\n' + ) + + lines_new.append( + f'{indent}\n' + ) + for k, v in g_arpsystemcomponent.items(): + if "v" in v: + t = v["t"] if "t" in v is None else "string" + lines_new.append( + f'{indent}\n' + ) + + for i, line in enumerate(lines_new): + lines.insert(index_start + i + 1, line) + return lines + + return gen_content_between_tags( + "Package/Components/Regs.wxs", + "", + "", + func, + ) + + +def gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir): + try: + custom_arp = json.loads(args.custom_arp) + g_arpsystemcomponent.update(custom_arp) + except json.JSONDecodeError as e: + print(f"Failed to decode custom arp: {e}") + return False + + if args.arp: + return gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir) + else: + return gen_custom_ARPSYSTEMCOMPONENT_False(args) + +def gen_conn_type(args): + def func(lines, index_start): + indent = g_indent_unit * 3 + + lines_new = [] + if args.conn_type != "": + lines_new.append( + f"""{indent}\n""" + ) + + for i, line in enumerate(lines_new): + lines.insert(index_start + i + 1, line) + return lines + + return gen_content_between_tags( + "Package/Fragments/AddRemoveProperties.wxs", + "", + "", + func, + ) + +def gen_content_between_tags(filename, tag_start, tag_end, func): + target_file = Path(sys.argv[0]).parent.joinpath(filename) + lines, index_start = read_lines_and_start_index(target_file, tag_start, tag_end) + if lines is None: + return False + + func(lines, index_start) + + with open(target_file, "w", encoding="utf-8") as f: + f.writelines(lines) + + return True + + +def prepare_resources(): + icon_src = Path(sys.argv[0]).parent.joinpath("../icon.ico") + icon_dst = Path(sys.argv[0]).parent.joinpath("Package/Resources/icon.ico") + if icon_src.exists(): + icon_dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(icon_src, icon_dst) + return True + else: + # unreachable + print(f"Error: icon.ico not found in {icon_src}") + return False + + +def init_global_vars(dist_dir, app_name, args): + dist_app = dist_dir.joinpath(app_name + ".exe") + + def read_process_output(args): + process = subprocess.Popen( + f"{dist_app} {args}", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True, + ) + output, _ = process.communicate() + return output.decode("utf-8").strip() + + global g_version + global g_build_date + g_version = args.version.replace("-", ".") + if g_version == "": + g_version = read_process_output("--version") + version_pattern = re.compile(r"\d+\.\d+\.\d+.*") + if not version_pattern.match(g_version): + print(f"Error: version {g_version} not found in {dist_app}") + return False + if g_version.count(".") == 2: + # https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/Version.cs + if args.revision_version < 0 or args.revision_version > 2147483647: + raise ValueError(f"Invalid revision version: {args.revision_version}") + g_version = f"{g_version}.{args.revision_version}" + + g_build_date = read_process_output("--build-date") + build_date_pattern = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}") + if not build_date_pattern.match(g_build_date): + print(f"Error: build date {g_build_date} not found in {dist_app}") + return False + + return True + + +def update_license_file(app_name): + if app_name == "RustDesk": + return + license_file = Path(sys.argv[0]).parent.joinpath("Package/License.rtf") + with open(license_file, "r", encoding="utf-8") as f: + license_content = f.read() + license_content = license_content.replace("website rustdesk.com and other ", "") + license_content = license_content.replace("RustDesk", app_name) + license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE) + with open(license_file, "w", encoding="utf-8") as f: + f.write(license_content) + + +def replace_component_guids_in_wxs(): + langs_dir = Path(sys.argv[0]).parent.joinpath("Package") + for file_path in langs_dir.glob("**/*.wxs"): + with open(file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + + # + for i, line in enumerate(lines): + match = re.search(r'Component.+Guid="([^"]+)"', line) + if match: + lines[i] = re.sub(r'Guid="[^"]+"', f'Guid="{uuid.uuid4()}"', line) + + with open(file_path, "w", encoding="utf-8") as f: + f.writelines(lines) + + +if __name__ == "__main__": + parser = make_parser() + args = parser.parse_args() + + app_name = args.app_name + dist_dir = Path(sys.argv[0]).parent.joinpath(args.dist_dir).resolve() + + if not prepare_resources(): + sys.exit(-1) + + if not init_global_vars(dist_dir, app_name, args): + sys.exit(-1) + + update_license_file(app_name) + + if not gen_pre_vars(args, dist_dir): + sys.exit(-1) + + if app_name != "RustDesk": + replace_component_guids_in_wxs() + + if not gen_upgrade_info(): + sys.exit(-1) + + if not gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir): + sys.exit(-1) + + if not gen_conn_type(args): + sys.exit(-1) + + if not gen_auto_component(app_name, dist_dir): + sys.exit(-1) + + if not gen_custom_dialog_bitmaps(): + sys.exit(-1) + + replace_app_name_in_langs(args.app_name) + replace_app_name_in_custom_actions(args.app_name) diff --git a/vendor/rustdesk/res/osx-dist.sh b/vendor/rustdesk/res/osx-dist.sh new file mode 100755 index 0000000..fd9c1fa --- /dev/null +++ b/vendor/rustdesk/res/osx-dist.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +echo $MACOS_CODESIGN_IDENTITY +cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid +cd flutter; flutter pub get; cd - +~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h +./build.py --flutter +rm rustdesk-$VERSION.dmg +# security find-identity -v +codesign --force --options runtime -s $MACOS_CODESIGN_IDENTITY --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv +create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-$VERSION.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app +codesign --force --options runtime -s $MACOS_CODESIGN_IDENTITY --deep --strict rustdesk-$VERSION.dmg -vvv +# notarize the rustdesk-${{ env.VERSION }}.dmg +rcodesign notary-submit --api-key-path ~/.p12/api-key.json --staple rustdesk-$VERSION.dmg diff --git a/vendor/rustdesk/res/pacman_install b/vendor/rustdesk/res/pacman_install new file mode 100644 index 0000000..bcd6907 --- /dev/null +++ b/vendor/rustdesk/res/pacman_install @@ -0,0 +1,47 @@ +# arg 1: the new package version +#pre_install() { +#} + +# arg 1: the new package version +post_install() { + # do something here + cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service + cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ + cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ + systemctl daemon-reload + systemctl enable rustdesk + systemctl start rustdesk + update-desktop-database +} + +# arg 1: the new package version +# arg 2: the old package version +pre_upgrade() { + systemctl stop rustdesk || true +} + +# arg 1: the new package version +# arg 2: the old package version +post_upgrade() { + cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service + cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ + cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ + systemctl daemon-reload + systemctl enable rustdesk + systemctl start rustdesk + update-desktop-database +} + +# arg 1: the old package version +pre_remove() { + systemctl stop rustdesk || true + systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service || true +} + +# arg 1: the old package version +post_remove() { + rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true + update-desktop-database +} diff --git a/vendor/rustdesk/res/pam.d/rustdesk.debian b/vendor/rustdesk/res/pam.d/rustdesk.debian new file mode 100644 index 0000000..789ce8f --- /dev/null +++ b/vendor/rustdesk/res/pam.d/rustdesk.debian @@ -0,0 +1,5 @@ +#%PAM-1.0 +@include common-auth +@include common-account +@include common-session +@include common-password diff --git a/vendor/rustdesk/res/pam.d/rustdesk.suse b/vendor/rustdesk/res/pam.d/rustdesk.suse new file mode 100644 index 0000000..a7c7836 --- /dev/null +++ b/vendor/rustdesk/res/pam.d/rustdesk.suse @@ -0,0 +1,5 @@ +#%PAM-1.0 +auth include common-auth +account include common-account +session include common-session +password include common-password diff --git a/vendor/rustdesk/res/rpm-flutter-suse.spec b/vendor/rustdesk/res/rpm-flutter-suse.spec new file mode 100644 index 0000000..bb2b56a --- /dev/null +++ b/vendor/rustdesk/res/rpm-flutter-suse.spec @@ -0,0 +1,98 @@ +Name: rustdesk +Version: 1.4.6 +Release: 0 +Summary: RPM package +License: GPL-3.0 +URL: https://rustdesk.com +Vendor: rustdesk +Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Recommends: libayatana-appindicator3-1 xdotool +Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) + +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + +%description +The best open-source remote desktop client software, written in Rust. + +%prep +# we have no source, so nothing here + +%build +# we have no source, so nothing here + +# %global __python %{__python3} + +%install + +mkdir -p "%{buildroot}/usr/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/share/rustdesk" +mkdir -p "%{buildroot}/usr/bin" +install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png" +install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" + +%files +/usr/share/rustdesk/* +/usr/share/rustdesk/files/rustdesk.service +/usr/share/icons/hicolor/256x256/apps/rustdesk.png +/usr/share/icons/hicolor/scalable/apps/rustdesk.svg +/usr/share/rustdesk/files/rustdesk.desktop +/usr/share/rustdesk/files/rustdesk-link.desktop + +%changelog +# let's skip this for now + +%pre +# can do something for centos7 +case "$1" in + 1) + # for install + ;; + 2) + # for upgrade + systemctl stop rustdesk || true + ;; +esac + +%post +cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service +cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ +cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ +ln -sf /usr/share/rustdesk/rustdesk /usr/bin/rustdesk +systemctl daemon-reload +systemctl enable rustdesk +systemctl start rustdesk +update-desktop-database + +%preun +case "$1" in + 0) + # for uninstall + systemctl stop rustdesk || true + systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service || true + ;; + 1) + # for upgrade + ;; +esac + +%postun +case "$1" in + 0) + # for uninstall + rm /usr/bin/rustdesk || true + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true + rmdir /usr/share/rustdesk || true + rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true + update-desktop-database + ;; + 1) + # for upgrade + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true + ;; +esac diff --git a/vendor/rustdesk/res/rpm-flutter.spec b/vendor/rustdesk/res/rpm-flutter.spec new file mode 100644 index 0000000..1a077ee --- /dev/null +++ b/vendor/rustdesk/res/rpm-flutter.spec @@ -0,0 +1,98 @@ +Name: rustdesk +Version: 1.4.6 +Release: 0 +Summary: RPM package +License: GPL-3.0 +URL: https://rustdesk.com +Vendor: rustdesk +Requires: gtk3 libxcb libXfixes alsa-lib libva pam gstreamer1-plugins-base +Recommends: libayatana-appindicator-gtk3 libxdo +Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) + +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + +%description +The best open-source remote desktop client software, written in Rust. + +%prep +# we have no source, so nothing here + +%build +# we have no source, so nothing here + +# %global __python %{__python3} + +%install + +mkdir -p "%{buildroot}/usr/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/share/rustdesk" +mkdir -p "%{buildroot}/usr/bin" +install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png" +install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" + +%files +/usr/share/rustdesk/* +/usr/share/rustdesk/files/rustdesk.service +/usr/share/icons/hicolor/256x256/apps/rustdesk.png +/usr/share/icons/hicolor/scalable/apps/rustdesk.svg +/usr/share/rustdesk/files/rustdesk.desktop +/usr/share/rustdesk/files/rustdesk-link.desktop + +%changelog +# let's skip this for now + +%pre +# can do something for centos7 +case "$1" in + 1) + # for install + ;; + 2) + # for upgrade + systemctl stop rustdesk || true + ;; +esac + +%post +cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service +cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ +cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ +ln -sf /usr/share/rustdesk/rustdesk /usr/bin/rustdesk +systemctl daemon-reload +systemctl enable rustdesk +systemctl start rustdesk +update-desktop-database + +%preun +case "$1" in + 0) + # for uninstall + systemctl stop rustdesk || true + systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service || true + ;; + 1) + # for upgrade + ;; +esac + +%postun +case "$1" in + 0) + # for uninstall + rm /usr/bin/rustdesk || true + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true + rmdir /usr/share/rustdesk || true + rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true + update-desktop-database + ;; + 1) + # for upgrade + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true + ;; +esac diff --git a/vendor/rustdesk/res/rpm-suse.spec b/vendor/rustdesk/res/rpm-suse.spec new file mode 100644 index 0000000..14364eb --- /dev/null +++ b/vendor/rustdesk/res/rpm-suse.spec @@ -0,0 +1,93 @@ +Name: rustdesk +Version: 1.1.9 +Release: 0 +Summary: RPM package +License: GPL-3.0 +Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Recommends: libayatana-appindicator3-1 xdotool + +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + +%description +The best open-source remote desktop client software, written in Rust. + +%prep +# we have no source, so nothing here + +%build +# we have no source, so nothing here + +%global __python %{__python3} + +%install +mkdir -p %{buildroot}/usr/bin/ +mkdir -p %{buildroot}/usr/share/rustdesk/ +mkdir -p %{buildroot}/usr/share/rustdesk/files/ +mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/ +mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/ +install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk +install $HBB/libsciter-gtk.so %{buildroot}/usr/share/rustdesk/libsciter-gtk.so +install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ +install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png +install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg +install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ +install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ + +%files +/usr/bin/rustdesk +/usr/share/rustdesk/libsciter-gtk.so +/usr/share/rustdesk/files/rustdesk.service +/usr/share/icons/hicolor/256x256/apps/rustdesk.png +/usr/share/icons/hicolor/scalable/apps/rustdesk.svg +/usr/share/rustdesk/files/rustdesk.desktop +/usr/share/rustdesk/files/rustdesk-link.desktop + +%changelog +# let's skip this for now + +%pre +# can do something for centos7 +case "$1" in + 1) + # for install + ;; + 2) + # for upgrade + systemctl stop rustdesk || true + ;; +esac + +%post +cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service +cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ +cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ +systemctl daemon-reload +systemctl enable rustdesk +systemctl start rustdesk +update-desktop-database + +%preun +case "$1" in + 0) + # for uninstall + systemctl stop rustdesk || true + systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service || true + ;; + 1) + # for upgrade + ;; +esac + +%postun +case "$1" in + 0) + # for uninstall + rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true + update-desktop-database + ;; + 1) + # for upgrade + ;; +esac diff --git a/vendor/rustdesk/res/rpm.spec b/vendor/rustdesk/res/rpm.spec new file mode 100644 index 0000000..6a7377b --- /dev/null +++ b/vendor/rustdesk/res/rpm.spec @@ -0,0 +1,96 @@ +Name: rustdesk +Version: 1.4.6 +Release: 0 +Summary: RPM package +License: GPL-3.0 +URL: https://rustdesk.com +Vendor: rustdesk +Requires: gtk3 libxcb libXfixes alsa-lib libva2 pam gstreamer1-plugins-base +Recommends: libayatana-appindicator-gtk3 libxdo + +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + +%description +The best open-source remote desktop client software, written in Rust. + +%prep +# we have no source, so nothing here + +%build +# we have no source, so nothing here + +%global __python %{__python3} + +%install +mkdir -p %{buildroot}/usr/bin/ +mkdir -p %{buildroot}/usr/share/rustdesk/ +mkdir -p %{buildroot}/usr/share/rustdesk/files/ +mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/ +mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/ +install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk +install $HBB/libsciter-gtk.so %{buildroot}/usr/share/rustdesk/libsciter-gtk.so +install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ +install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png +install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg +install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ +install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ + +%files +/usr/bin/rustdesk +/usr/share/rustdesk/libsciter-gtk.so +/usr/share/rustdesk/files/rustdesk.service +/usr/share/icons/hicolor/256x256/apps/rustdesk.png +/usr/share/icons/hicolor/scalable/apps/rustdesk.svg +/usr/share/rustdesk/files/rustdesk.desktop +/usr/share/rustdesk/files/rustdesk-link.desktop +/usr/share/rustdesk/files/__pycache__/* + +%changelog +# let's skip this for now + +%pre +# can do something for centos7 +case "$1" in + 1) + # for install + ;; + 2) + # for upgrade + systemctl stop rustdesk || true + ;; +esac + +%post +cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service +cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ +cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ +systemctl daemon-reload +systemctl enable rustdesk +systemctl start rustdesk +update-desktop-database + +%preun +case "$1" in + 0) + # for uninstall + systemctl stop rustdesk || true + systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service || true + ;; + 1) + # for upgrade + ;; +esac + +%postun +case "$1" in + 0) + # for uninstall + rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true + update-desktop-database + ;; + 1) + # for upgrade + ;; +esac diff --git a/vendor/rustdesk/res/rustdesk-banner.svg b/vendor/rustdesk/res/rustdesk-banner.svg new file mode 100644 index 0000000..fb96307 --- /dev/null +++ b/vendor/rustdesk/res/rustdesk-banner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/rustdesk/res/rustdesk-link.desktop b/vendor/rustdesk/res/rustdesk-link.desktop new file mode 100644 index 0000000..c7a9bd5 --- /dev/null +++ b/vendor/rustdesk/res/rustdesk-link.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=RustDesk +NoDisplay=true +MimeType=x-scheme-handler/rustdesk; +TryExec=rustdesk +Exec=rustdesk %u +Icon=rustdesk +Terminal=false +Type=Application +StartupNotify=false +StartupWMClass=rustdesk diff --git a/vendor/rustdesk/res/rustdesk.desktop b/vendor/rustdesk/res/rustdesk.desktop new file mode 100644 index 0000000..4e7d14f --- /dev/null +++ b/vendor/rustdesk/res/rustdesk.desktop @@ -0,0 +1,19 @@ +[Desktop Entry] +Name=RustDesk +GenericName=Remote Desktop +Comment=Remote Desktop +Exec=rustdesk %u +Icon=rustdesk +Terminal=false +Type=Application +StartupNotify=true +Categories=Network;RemoteAccess;GTK; +Keywords=internet;linux;dart;rust;remote-control;p2p;teamviewer;rust-lang;rdp;remote-desktop;vnc; +Actions=new-window; +StartupWMClass=rustdesk + +X-Desktop-File-Install-Version=0.23 + +[Desktop Action new-window] +Name=Open a New Window +Exec=rustdesk %u diff --git a/vendor/rustdesk/res/rustdesk.service b/vendor/rustdesk/res/rustdesk.service new file mode 100644 index 0000000..1b3feb1 --- /dev/null +++ b/vendor/rustdesk/res/rustdesk.service @@ -0,0 +1,22 @@ +[Unit] +Description=RustDesk +Requires=network.target +After=systemd-user-sessions.service + +[Service] +Type=simple +ExecStart=/usr/bin/rustdesk --service +# kill --tray and --server both +ExecStop=pkill -f "rustdesk --" +# below two lines do not work, have to use above one line +#ExecStop=pkill -f "rustdesk --tray" +#ExecStop=pkill -f "rustdesk --server" +PIDFile=/run/rustdesk.pid +KillMode=mixed +TimeoutStopSec=30 +User=root +LimitNOFILE=100000 +Environment="PULSE_LATENCY_MSEC=60" "PIPEWIRE_LATENCY=1024/48000" + +[Install] +WantedBy=multi-user.target diff --git a/vendor/rustdesk/res/scalable.svg b/vendor/rustdesk/res/scalable.svg new file mode 100644 index 0000000..50cab67 --- /dev/null +++ b/vendor/rustdesk/res/scalable.svg @@ -0,0 +1,88 @@ + + diff --git a/vendor/rustdesk/res/startwm.sh b/vendor/rustdesk/res/startwm.sh new file mode 100755 index 0000000..7cdaf07 --- /dev/null +++ b/vendor/rustdesk/res/startwm.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +# This script is derived from https://github.com/neutrinolabs/xrdp/sesman/startwm.sh. + +# +# This script is an example. You might need to edit this script +# depending on your distro if it doesn't work for you. +# +# Uncomment the following line for debug: +# exec xterm + + +# Execution sequence for interactive login shell - pseudocode +# +# IF /etc/profile is readable THEN +# execute ~/.bash_profile +# END IF +# IF ~/.bash_profile is readable THEN +# execute ~/.bash_profile +# ELSE +# IF ~/.bash_login is readable THEN +# execute ~/.bash_login +# ELSE +# IF ~/.profile is readable THEN +# execute ~/.profile +# END IF +# END IF +# END IF +pre_start() +{ + if [ -r /etc/profile ]; then + . /etc/profile + fi + if [ -r ~/.bash_profile ]; then + . ~/.bash_profile + else + if [ -r ~/.bash_login ]; then + . ~/.bash_login + else + if [ -r ~/.profile ]; then + . ~/.profile + fi + fi + fi + return 0 +} + +# When loging out from the interactive shell, the execution sequence is: +# +# IF ~/.bash_logout exists THEN +# execute ~/.bash_logout +# END IF +post_start() +{ + if [ -r ~/.bash_logout ]; then + . ~/.bash_logout + fi + return 0 +} + +#start the window manager +wm_start() +{ + if [ -r /etc/default/locale ]; then + . /etc/default/locale + export LANG LANGUAGE + fi + + # debian + if [ -r /etc/X11/Xsession ]; then + pre_start + . /etc/X11/Xsession + post_start + exit 0 + fi + + # alpine + # Don't use /etc/X11/xinit/Xsession - it doesn't work + if [ -f /etc/alpine-release ]; then + if [ -f /etc/X11/xinit/xinitrc ]; then + pre_start + /etc/X11/xinit/xinitrc + post_start + else + echo "** xinit package isn't installed" >&2 + exit 1 + fi + fi + + # el + if [ -r /etc/X11/xinit/Xsession ]; then + pre_start + . /etc/X11/xinit/Xsession + post_start + exit 0 + fi + + # suse + if [ -r /etc/X11/xdm/Xsession ]; then + # since the following script run a user login shell, + # do not execute the pseudo login shell scripts + . /etc/X11/xdm/Xsession + exit 0 + elif [ -r /usr/etc/X11/xdm/Xsession ]; then + . /usr/etc/X11/xdm/Xsession + exit 0 + fi + + pre_start + xterm + post_start +} + +#. /etc/environment +#export PATH=$PATH +#export LANG=$LANG + +# change PATH to be what your environment needs usually what is in +# /etc/environment +#PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games" +#export PATH=$PATH + +# for PATH and LANG from /etc/environment +# pam will auto process the environment file if /etc/pam.d/xrdp-sesman +# includes +# auth required pam_env.so readenv=1 + +wm_start + +exit 1 diff --git a/vendor/rustdesk/res/strategies.py b/vendor/rustdesk/res/strategies.py new file mode 100755 index 0000000..178d8d9 --- /dev/null +++ b/vendor/rustdesk/res/strategies.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 + +import requests +import argparse +import json + + +def check_response(response): + """ + Check API response and handle errors. + + Two error cases: + 1. Status code is not 200 -> exit with error + 2. Response contains {"error": "xxx"} -> exit with error + """ + if response.status_code != 200: + print(f"Error: HTTP {response.status_code}: {response.text}") + exit(1) + + # Check for {"error": "xxx"} in response + if response.text and response.text.strip(): + try: + json_data = response.json() + if isinstance(json_data, dict) and "error" in json_data: + print(f"Error: {json_data['error']}") + exit(1) + return json_data + except ValueError: + return response.text + + return None + + +def headers_with(token): + return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + +# ---------- Strategies APIs ---------- + +def list_strategies(url, token): + """List all strategies""" + headers = headers_with(token) + r = requests.get(f"{url}/api/strategies", headers=headers) + return check_response(r) + + +def get_strategy_by_guid(url, token, guid): + """Get strategy by GUID""" + headers = headers_with(token) + r = requests.get(f"{url}/api/strategies/{guid}", headers=headers) + return check_response(r) + + +def get_strategy_by_name(url, token, name): + """Get strategy by name""" + strategies = list_strategies(url, token) + if not strategies: + return None + for s in strategies: + if str(s.get("name")) == name: + return s + return None + + +def enable_strategy(url, token, name): + """Enable a strategy""" + headers = headers_with(token) + strategy = get_strategy_by_name(url, token, name) + if not strategy: + print(f"Error: Strategy '{name}' not found") + exit(1) + guid = strategy.get("guid") + r = requests.put(f"{url}/api/strategies/{guid}/status", headers=headers, json=True) + check_response(r) + return "Success" + + +def disable_strategy(url, token, name): + """Disable a strategy""" + headers = headers_with(token) + strategy = get_strategy_by_name(url, token, name) + if not strategy: + print(f"Error: Strategy '{name}' not found") + exit(1) + guid = strategy.get("guid") + r = requests.put(f"{url}/api/strategies/{guid}/status", headers=headers, json=False) + check_response(r) + return "Success" + + +def get_device_guid_by_id(url, token, device_id): + """Get device GUID by device ID (exact match)""" + headers = headers_with(token) + params = {"id": device_id, "pageSize": 50} + r = requests.get(f"{url}/api/devices", headers=headers, params=params) + res = check_response(r) + if not res: + return None + + devices_data = res.get("data", []) if isinstance(res, dict) else res + for d in devices_data: + if d.get("id") == device_id: + return d.get("guid") + return None + + +def get_user_guid_by_name(url, token, name): + """Get user GUID by exact name match""" + headers = headers_with(token) + params = {"name": name, "pageSize": 50} + r = requests.get(f"{url}/api/users", headers=headers, params=params) + res = check_response(r) + if not res: + return None + + users_data = res.get("data", []) if isinstance(res, dict) else res + for u in users_data: + if u.get("name") == name: + return u.get("guid") + return None + + +def get_device_group_guid_by_name(url, token, name): + """Get device group GUID by exact name match""" + headers = headers_with(token) + params = {"pageSize": 50, "name": name} + r = requests.get(f"{url}/api/device-groups", headers=headers, params=params) + res = check_response(r) + if not res: + return None + + groups_data = res.get("data", []) if isinstance(res, dict) else res + for g in groups_data: + if g.get("name") == name: + return g.get("guid") + return None + + +def assign_strategy(url, token, strategy_name, peers=None, users=None, device_groups=None): + """ + Assign strategy to peers, users, or device groups + + Args: + strategy_name: Name of the strategy (or None to unassign) + peers: List of device IDs or GUIDs + users: List of user names or GUIDs + device_groups: List of device group names or GUIDs + """ + headers = headers_with(token) + + # Get strategy GUID if strategy_name is provided + strategy_guid = None + if strategy_name: + strategy = get_strategy_by_name(url, token, strategy_name) + if not strategy: + print(f"Error: Strategy '{strategy_name}' not found") + exit(1) + strategy_guid = strategy.get("guid") + + # Convert device IDs to GUIDs + peer_guids = [] + if peers: + for peer in peers: + # Check if it's already a GUID format + if len(peer) == 36 and peer.count('-') == 4: + peer_guids.append(peer) + else: + # Treat as device ID, look it up + guid = get_device_guid_by_id(url, token, peer) + if not guid: + print(f"Error: Device '{peer}' not found") + exit(1) + peer_guids.append(guid) + + # Convert user names to GUIDs + user_guids = [] + if users: + for user in users: + # Check if it's already a GUID format + if len(user) == 36 and user.count('-') == 4: + user_guids.append(user) + else: + # Treat as username, look it up + guid = get_user_guid_by_name(url, token, user) + if not guid: + print(f"Error: User '{user}' not found") + exit(1) + user_guids.append(guid) + + # Convert device group names to GUIDs + device_group_guids = [] + if device_groups: + for dg in device_groups: + # Check if it's already a GUID format + if len(dg) == 36 and dg.count('-') == 4: + device_group_guids.append(dg) + else: + # Treat as device group name, look it up + guid = get_device_group_guid_by_name(url, token, dg) + if not guid: + print(f"Error: Device group '{dg}' not found") + exit(1) + device_group_guids.append(guid) + + # Build payload + payload = {} + if strategy_guid: + payload["strategy"] = strategy_guid + + payload["peers"] = peer_guids + payload["users"] = user_guids + payload["groups"] = device_group_guids + + r = requests.post(f"{url}/api/strategies/assign", headers=headers, json=payload) + check_response(r) + + +def main(): + parser = argparse.ArgumentParser(description="Strategy manager") + parser.add_argument("command", choices=[ + "list", "view", "enable", "disable", "assign", "unassign" + ]) + parser.add_argument("--url", required=True, help="Server URL") + parser.add_argument("--token", required=True, help="API token") + + parser.add_argument("--name", help="Strategy name (for view/enable/disable/assign commands)") + parser.add_argument("--guid", help="Strategy GUID (for view command, alternative to --name)") + + # For assign/unassign commands + parser.add_argument("--peers", help="Comma separated device IDs or GUIDs (requires Device Permission:r)") + parser.add_argument("--users", help="Comma separated user names or GUIDs (requires User Permission:r)") + parser.add_argument("--device-groups", help="Comma separated device group names or GUIDs (requires Device Group Permission:r)") + + args = parser.parse_args() + while args.url.endswith("/"): args.url = args.url[:-1] + + if args.command == "list": + res = list_strategies(args.url, args.token) + print(json.dumps(res, indent=2)) + + elif args.command == "view": + if args.guid: + res = get_strategy_by_guid(args.url, args.token, args.guid) + print(json.dumps(res, indent=2)) + elif args.name: + strategy = get_strategy_by_name(args.url, args.token, args.name) + if not strategy: + print(f"Error: Strategy '{args.name}' not found") + exit(1) + # Get full details by GUID + guid = strategy.get("guid") + res = get_strategy_by_guid(args.url, args.token, guid) + print(json.dumps(res, indent=2)) + else: + print("Error: --name or --guid is required for view command") + exit(1) + + elif args.command == "enable": + if not args.name: + print("Error: --name is required") + exit(1) + print(enable_strategy(args.url, args.token, args.name)) + + elif args.command == "disable": + if not args.name: + print("Error: --name is required") + exit(1) + print(disable_strategy(args.url, args.token, args.name)) + + elif args.command == "assign": + if not args.name: + print("Error: --name is required") + exit(1) + if not args.peers and not args.users and not args.device_groups: + print("Error: at least one of --peers, --users, or --device-groups is required") + exit(1) + + peers = [x.strip() for x in args.peers.split(",") if x.strip()] if args.peers else None + users = [x.strip() for x in args.users.split(",") if x.strip()] if args.users else None + device_groups = [x.strip() for x in args.device_groups.split(",") if x.strip()] if args.device_groups else None + + assign_strategy(args.url, args.token, args.name, peers=peers, users=users, device_groups=device_groups) + count = (len(peers) if peers else 0) + (len(users) if users else 0) + (len(device_groups) if device_groups else 0) + print(f"Success: Assigned strategy '{args.name}' to {count} target(s)") + + elif args.command == "unassign": + if not args.peers and not args.users and not args.device_groups: + print("Error: at least one of --peers, --users, or --device-groups is required") + exit(1) + + peers = [x.strip() for x in args.peers.split(",") if x.strip()] if args.peers else None + users = [x.strip() for x in args.users.split(",") if x.strip()] if args.users else None + device_groups = [x.strip() for x in args.device_groups.split(",") if x.strip()] if args.device_groups else None + + assign_strategy(args.url, args.token, None, peers=peers, users=users, device_groups=device_groups) + count = (len(peers) if peers else 0) + (len(users) if users else 0) + (len(device_groups) if device_groups else 0) + print(f"Success: Unassigned strategy from {count} target(s)") + + +if __name__ == "__main__": + main() diff --git a/vendor/rustdesk/res/tray-icon.ico b/vendor/rustdesk/res/tray-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df8bdaccbd9df0c34ca00bb736ba2361f94e1705 GIT binary patch literal 4286 zcmchb`)^c56vwA6G0_-bAi-*46MljCqpxUu(4e3ajs6uz{lUZ-P;4yKVnMVKCB)D+ z_z08`H3$_bExX$^fFRN;E%>CSw7c7FugmV;*L{9HbGLUpclXY27t_w>v@>(gna`a$ zvvcMyV>R??Xkh#=J8&Cg%Nb*sW_A>H6DdC7M$2Edi)sU9FvNfxT`u}_k0 zAaS|;e;dhSmsSFD_6v&*7I=Iefo=k_t+`ed)C3)$Y8$3U~DF7V$hozv=)Zk`wZEw44F2P?KQ|9SOY71 z&P(!dCH%XE-pB~-jdk;6Go*Jgq}mzOowdk@UIR=`80 zcx#>a!{~n##`(`kc0xP(Ix=6ZhAPb4Wyd=h{_Uc5IP;2UJZa;fGZgP7uv+VZ*k!L- zMxp)vr?*dN8RI9A+P4bDY-KNl@>MO2=^q7a|Ek@wer~U|t$4 za)=>v>{h9xzV+sk^W6OPfAIg>4Uru_%n&|ylbqjvcTp~(95eWi)V`8Jy|~F)b3Sj* zp6sC*gjP!3^!@9FJeEIGGy5Nw+E-F2!G|ESCyp{qpMP5FW(>SLn?t?lxfwkx^^qye z^jHb|YA@xav28LJ*wdqwzrAdp$I=HMp!|`Q`uQm=O+obdoz9x`d2{yFK8k1RlAn2@ zFWdciK7SYGl63D&u&aCqJu!rE-_im*HvCM1Bdb{9)0D?*3i-Q`OZG4%zNfqwljjgC zGXdN1UTO5?5GF&*FdnFbmXQ03&yx+j?A#qhHqW8%d4BEagGdd(jqKlD$Xw}!I?@Cs z^oSGhsT1`u)KPIbSb7Xbx{7OQURusc{$q^z79MVDUyXMeC!|zf9G|$A;#*q O@Sg}4xo~EP*#80bVj{T! literal 0 HcmV?d00001 diff --git a/vendor/rustdesk/res/user-groups.py b/vendor/rustdesk/res/user-groups.py new file mode 100755 index 0000000..5df16c3 --- /dev/null +++ b/vendor/rustdesk/res/user-groups.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 + +import requests +import argparse +import json + + +def check_response(response): + """ + Check API response and handle errors. + + Two error cases: + 1. Status code is not 200 -> exit with error + 2. Response contains {"error": "xxx"} -> exit with error + """ + if response.status_code != 200: + print(f"Error: HTTP {response.status_code}: {response.text}") + exit(1) + + # Check for {"error": "xxx"} in response + if response.text and response.text.strip(): + try: + json_data = response.json() + if isinstance(json_data, dict) and "error" in json_data: + print(f"Error: {json_data['error']}") + exit(1) + return json_data + except ValueError: + return response.text + + return None + + +def headers_with(token): + return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + +# ---------- User Group APIs ---------- + +def list_groups(url, token, name=None, page_size=50): + headers = headers_with(token) + params = {"pageSize": page_size} + if name: + params["name"] = name + data, current = [], 0 + while True: + current += 1 + params["current"] = current + r = requests.get(f"{url}/api/user-groups", headers=headers, params=params) + if r.status_code != 200: + print(f"Error: HTTP {r.status_code} - {r.text}") + exit(1) + res = r.json() + if "error" in res: + print(f"Error: {res['error']}") + exit(1) + rows = res.get("data", []) + data.extend(rows) + total = res.get("total", 0) + if len(rows) < page_size or current * page_size >= total: + break + return data + + +def get_group_by_name(url, token, name): + groups = list_groups(url, token, name) + for g in groups: + if str(g.get("name")) == name: + return g + return None + + +def create_group(url, token, name, note=None, accessed_from=None, access_to=None): + headers = headers_with(token) + payload = {"name": name} + if note: + payload["note"] = note + if accessed_from: + payload["allowed_incomings"] = accessed_from + if access_to: + payload["allowed_outgoings"] = access_to + r = requests.post(f"{url}/api/user-groups", headers=headers, json=payload) + return check_response(r) + + +def update_group(url, token, name, new_name=None, note=None, accessed_from=None, access_to=None): + headers = headers_with(token) + g = get_group_by_name(url, token, name) + if not g: + print(f"Error: Group '{name}' not found") + exit(1) + guid = g.get("guid") + payload = {} + if new_name is not None: + payload["name"] = new_name + if note is not None: + payload["note"] = note + if accessed_from is not None: + payload["allowed_incomings"] = accessed_from + if access_to is not None: + payload["allowed_outgoings"] = access_to + r = requests.patch(f"{url}/api/user-groups/{guid}", headers=headers, json=payload) + check_response(r) + return "Success" + + +def delete_groups(url, token, names): + headers = headers_with(token) + if isinstance(names, str): + names = [names] + for n in names: + g = get_group_by_name(url, token, n) + if not g: + print(f"Error: Group '{n}' not found") + exit(1) + guid = g.get("guid") + r = requests.delete(f"{url}/api/user-groups/{guid}", headers=headers) + check_response(r) + return "Success" + + +# ---------- User management in group ---------- + +def view_users(url, token, group_name=None, name=None, page_size=50): + """View users in a user group with filters""" + headers = headers_with(token) + + # Separate exact match and fuzzy match params + params = {} + fuzzy_params = { + "name": name, + } + + # Add group_name without wildcard (exact match) + if group_name: + params["group_name"] = group_name + + # Add wildcard for fuzzy search to other params + for k, v in fuzzy_params.items(): + if v is not None: + params[k] = "%" + v + "%" if (v != "-" and "%" not in v) else v + + params["pageSize"] = page_size + + data, current = [], 0 + while True: + current += 1 + params["current"] = current + r = requests.get(f"{url}/api/users", headers=headers, params=params) + if r.status_code != 200: + return check_response(r) + res = r.json() + rows = res.get("data", []) + data.extend(rows) + total = res.get("total", 0) + if len(rows) < page_size or current * page_size >= total: + break + return data + + +def add_users(url, token, group_name, user_names): + """Add users to a user group""" + headers = headers_with(token) + if isinstance(user_names, str): + user_names = [user_names] + + # Get the user group guid + g = get_group_by_name(url, token, group_name) + if not g: + print(f"Error: Group '{group_name}' not found") + exit(1) + guid = g.get("guid") + + # Get user GUIDs + user_guids = [] + errors = [] + + for user_name in user_names: + # Get user by exact name match + params = {"name": user_name, "pageSize": 50} + r = requests.get(f"{url}/api/users", headers=headers, params=params) + if r.status_code != 200: + errors.append(f"{user_name}: HTTP {r.status_code}") + continue + + users_data = r.json() + users_list = users_data.get("data", []) + user = None + for u in users_list: + if u.get("name") == user_name: + user = u + break + + if not user: + errors.append(f"{user_name}: User not found") + continue + + user_guids.append(user["guid"]) + + if not user_guids: + msg = "Error: No valid users found" + if errors: + msg += ". " + "; ".join(errors) + print(msg) + exit(1) + + # Add users to group using POST /api/user-groups/:guid + r = requests.post(f"{url}/api/user-groups/{guid}", headers=headers, json=user_guids) + check_response(r) + + success_msg = f"Success: Added {len(user_guids)} user(s) to group '{group_name}'" + if errors: + return success_msg + " (with errors: " + "; ".join(errors) + ")" + return success_msg + + +def parse_rules(s): + if not s: + return None + try: + v = json.loads(s) + if isinstance(v, list): + # expect list of {"type": number, "name": string} + return v + except Exception: + pass + return None + + +def main(): + parser = argparse.ArgumentParser(description="User Group manager") + parser.add_argument("command", choices=[ + "view", "add", "update", "delete", + "view-users", "add-users" + ], help=( + "Command to execute. " + "[view/add/update/delete/add-users: require User Group Permission] " + "[view-users: require User Permission]" + )) + parser.add_argument("--url", required=True) + parser.add_argument("--token", required=True) + + parser.add_argument("--name", help="User group name (exact match)") + parser.add_argument("--new-name", help="New user group name (for update)") + parser.add_argument("--note", help="Note") + + parser.add_argument("--accessed-from", help="JSON array: '[{\"type\":0|2,\"name\":\"...\"}]' (0=User Group, 2=User)") + parser.add_argument("--access-to", help="JSON array: '[{\"type\":0|1,\"name\":\"...\"}]' (0=User Group, 1=Device Group)") + + parser.add_argument("--users", help="Comma separated usernames for add-users") + + # Filters for view-users command + parser.add_argument("--user-name", help="User name filter (for view-users, supports fuzzy search)") + + args = parser.parse_args() + while args.url.endswith("/"): args.url = args.url[:-1] + + if args.command == "view": + res = list_groups(args.url, args.token, args.name) + print(json.dumps(res, indent=2)) + elif args.command == "add": + if not args.name: + print("Error: --name is required") + exit(1) + print(create_group( + args.url, args.token, args.name, args.note, + parse_rules(args.accessed_from), + parse_rules(args.access_to) + )) + elif args.command == "update": + if not args.name: + print("Error: --name is required") + exit(1) + print(update_group( + args.url, args.token, args.name, args.new_name, args.note, + parse_rules(args.accessed_from), + parse_rules(args.access_to) + )) + elif args.command == "delete": + if not args.name: + print("Error: --name is required (supports comma separated)") + exit(1) + names = [x.strip() for x in args.name.split(",") if x.strip()] + print(delete_groups(args.url, args.token, names)) + elif args.command == "view-users": + res = view_users( + args.url, + args.token, + group_name=args.name, + name=args.user_name + ) + print(json.dumps(res, indent=2)) + elif args.command == "add-users": + if not args.name or not args.users: + print("Error: --name and --users are required") + exit(1) + users = [x.strip() for x in args.users.split(",") if x.strip()] + print(add_users(args.url, args.token, args.name, users)) + + +if __name__ == "__main__": + main() diff --git a/vendor/rustdesk/res/users.py b/vendor/rustdesk/res/users.py new file mode 100755 index 0000000..02b1147 --- /dev/null +++ b/vendor/rustdesk/res/users.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 + +import requests +import argparse +from datetime import datetime, timedelta + + +def check_response(response): + """ + Check API response and handle errors properly. + Exit with code 1 if there's an error. + """ + if response.status_code != 200: + print(f"Error: HTTP {response.status_code}: {response.text}") + exit(1) + + if response.text and response.text.strip(): + try: + json_data = response.json() + if isinstance(json_data, dict) and "error" in json_data: + print(f"Error: {json_data['error']}") + exit(1) + return json_data + except ValueError: + return response.text + + return None + + +def view( + url, + token, + name=None, + group_name=None, +): + headers = {"Authorization": f"Bearer {token}"} + pageSize = 30 + params = { + "name": name, + "group_name": group_name, + } + + params = { + k: "%" + v + "%" if (v != "-" and "%" not in v) else v + for k, v in params.items() + if v is not None + } + params["pageSize"] = pageSize + + users = [] + + current = 0 + + while True: + current += 1 + params["current"] = current + response = requests.get(f"{url}/api/users", headers=headers, params=params) + if response.status_code != 200: + print(f"Error: HTTP {response.status_code} - {response.text}") + exit(1) + + response_json = response.json() + if "error" in response_json: + print(f"Error: {response_json['error']}") + exit(1) + + data = response_json.get("data", []) + users.extend(data) + + total = response_json.get("total", 0) + if len(data) < pageSize or current * pageSize >= total: + break + + return users + + +def disable(url, token, guid, name): + print("Disable", name) + headers = {"Authorization": f"Bearer {token}"} + response = requests.post(f"{url}/api/users/{guid}/disable", headers=headers) + check_response(response) + + +def enable(url, token, guid, name): + print("Enable", name) + headers = {"Authorization": f"Bearer {token}"} + response = requests.post(f"{url}/api/users/{guid}/enable", headers=headers) + check_response(response) + + +def delete_user(url, token, guid, name): + print("Delete", name) + headers = {"Authorization": f"Bearer {token}"} + response = requests.delete(f"{url}/api/users/{guid}", headers=headers) + check_response(response) + + +def new_user(url, token, name, password, group_name=None, email=None, note=None): + """Create a new user""" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + payload = { + "name": name, + "password": password, + } + if group_name: + payload["group_name"] = group_name + if email: + payload["email"] = email + if note: + payload["note"] = note + response = requests.post(f"{url}/api/users", headers=headers, json=payload) + check_response(response) + + +def invite_user(url, token, email, name, group_name=None, note=None): + """Invite a user by email""" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + payload = { + "email": email, + "name": name, + } + if group_name: + payload["group_name"] = group_name + if note: + payload["note"] = note + response = requests.post(f"{url}/api/users/invite", headers=headers, json=payload) + check_response(response) + + +def enable_2fa_enforce(url, token, user_guids, base_url): + """Enable 2FA enforcement for users""" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + payload = { + "user_guids": user_guids if isinstance(user_guids, list) else [user_guids], + "enforce": True, + "url": base_url + } + response = requests.put(f"{url}/api/users/tfa/totp/enforce", headers=headers, json=payload) + check_response(response) + + +def disable_2fa_enforce(url, token, user_guids, base_url=""): + """Disable 2FA enforcement for users""" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + payload = { + "user_guids": user_guids if isinstance(user_guids, list) else [user_guids], + "enforce": False, + "url": base_url + } + response = requests.put(f"{url}/api/users/tfa/totp/enforce", headers=headers, json=payload) + check_response(response) + + +def disable_email_verification(url, token, user_guids): + """Disable email login verification for users""" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + payload = { + "user_guids": user_guids if isinstance(user_guids, list) else [user_guids], + "type": "email" + } + response = requests.put(f"{url}/api/users/disable_login_verification", headers=headers, json=payload) + check_response(response) + + +def reset_2fa(url, token, user_guids): + """Reset 2FA for users""" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + payload = { + "user_guids": user_guids if isinstance(user_guids, list) else [user_guids], + "type": "2fa" + } + response = requests.put(f"{url}/api/users/disable_login_verification", headers=headers, json=payload) + check_response(response) + + +def force_logout(url, token, user_guids): + """Force logout users""" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + payload = { + "user_guids": user_guids if isinstance(user_guids, list) else [user_guids], + } + response = requests.post(f"{url}/api/users/force-logout", headers=headers, json=payload) + check_response(response) + + +def main(): + parser = argparse.ArgumentParser(description="User manager") + parser.add_argument( + "command", + choices=["view", "disable", "enable", "delete", "new", "invite", + "enable-2fa-enforce", "disable-2fa-enforce", + "disable-email-verification", "reset-2fa", "force-logout"], + help="Command to execute", + ) + parser.add_argument("--url", required=True, help="URL of the API") + parser.add_argument( + "--token", required=True, help="Bearer token for authentication" + ) + parser.add_argument("--name", help="User name") + parser.add_argument("--group_name", help="Group name (for filtering in view, or for new/invite command)") + parser.add_argument("--password", help="User password (for new command)") + parser.add_argument("--email", help="User email (for invite command)") + parser.add_argument("--note", help="User note (for new/invite command)") + parser.add_argument("--web-console-url", help="Web console URL (for 2FA enforce commands)") + + args = parser.parse_args() + + while args.url.endswith("/"): args.url = args.url[:-1] + + if args.command == "new": + if not args.name or not args.password or not args.group_name: + print("Error: --name and --password and --group_name are required for new command") + exit(1) + new_user(args.url, args.token, args.name, args.password, args.group_name, args.email, args.note) + print("Success: User created") + return + + if args.command == "invite": + if not args.email or not args.name or not args.group_name: + print("Error: --email and --name and --group_name are required for invite command") + exit(1) + invite_user(args.url, args.token, args.email, args.name, args.group_name, args.note) + print("Success: Invitation sent") + return + + users = view( + args.url, + args.token, + args.name, + args.group_name, + ) + + if args.command == "view": + if len(users) == 0: + print("Found 0 users") + else: + for user in users: + print(user) + elif args.command in ["disable", "enable", "delete", "enable-2fa-enforce", + "disable-2fa-enforce", "disable-email-verification", "reset-2fa", "force-logout"]: + if len(users) == 0: + print("Found 0 users") + return + + # Check if we need user confirmation for multiple users + if len(users) > 1: + print(f"Found {len(users)} users. Do you want to proceed with {args.command} operation on the users? (Y/N)") + confirmation = input("Type 'Y' to confirm: ").strip() + if confirmation.upper() != 'Y': + print("Operation cancelled.") + return + + if args.command == "disable": + for user in users: + disable(args.url, args.token, user["guid"], user["name"]) + print("Success") + elif args.command == "enable": + for user in users: + enable(args.url, args.token, user["guid"], user["name"]) + print("Success") + elif args.command == "delete": + for user in users: + delete_user(args.url, args.token, user["guid"], user["name"]) + print("Success") + elif args.command == "enable-2fa-enforce": + if not args.web_console_url: + print("Error: --web-console-url is required for enable-2fa-enforce") + exit(1) + user_guids = [user["guid"] for user in users] + enable_2fa_enforce(args.url, args.token, user_guids, args.web_console_url) + print(f"Success: Enabled 2FA enforcement for {len(users)} user(s)") + elif args.command == "disable-2fa-enforce": + user_guids = [user["guid"] for user in users] + web_url = args.web_console_url or "" + disable_2fa_enforce(args.url, args.token, user_guids, web_url) + print(f"Success: Disabled 2FA enforcement for {len(users)} user(s)") + elif args.command == "disable-email-verification": + user_guids = [user["guid"] for user in users] + disable_email_verification(args.url, args.token, user_guids) + print(f"Success: Disabled email verification for {len(users)} user(s)") + elif args.command == "reset-2fa": + user_guids = [user["guid"] for user in users] + reset_2fa(args.url, args.token, user_guids) + print(f"Success: Reset 2FA for {len(users)} user(s)") + elif args.command == "force-logout": + user_guids = [user["guid"] for user in users] + force_logout(args.url, args.token, user_guids) + print(f"Success: Force logout for {len(users)} user(s)") + + +if __name__ == "__main__": + main() diff --git a/vendor/rustdesk/res/vcpkg/aom/aom-avx2.diff b/vendor/rustdesk/res/vcpkg/aom/aom-avx2.diff new file mode 100644 index 0000000..d53cd0a --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/aom/aom-avx2.diff @@ -0,0 +1,60 @@ +diff --git a/build/cmake/cpu.cmake b/build/cmake/cpu.cmake +index acebe20..8c67d89 100644 +--- a/build/cmake/cpu.cmake ++++ b/build/cmake/cpu.cmake +@@ -120,6 +120,19 @@ elseif("${AOM_TARGET_CPU}" MATCHES "^x86") + set(RTCD_ARCH_X86_64 "yes") + endif() + ++ # AVX2 requires __m256i definition starting v3.9.0 ++ ++ if(ENABLE_AVX2) ++ aom_check_source_compiles("x86_64_avx2_m256i_available" " ++#include ++#ifndef __m256i ++#error 1 ++#endif" HAVE_AVX2_M256I) ++ if(HAVE_AVX2_M256I EQUAL 0) ++ set(ENABLE_AVX2 0) ++ endif() ++ endif() ++ + set(X86_FLAVORS "MMX;SSE;SSE2;SSE3;SSSE3;SSE4_1;SSE4_2;AVX;AVX2") + foreach(flavor ${X86_FLAVORS}) + if(ENABLE_${flavor} AND NOT disable_remaining_flavors) +diff --git a/aom_dsp/x86/synonyms.h b/aom_dsp/x86/synonyms.h +index 0d51cdf..6744ec5 100644 +--- a/aom_dsp/x86/synonyms.h ++++ b/aom_dsp/x86/synonyms.h +@@ -46,13 +46,6 @@ static INLINE __m128i xx_loadu_128(const void *a) { + return _mm_loadu_si128((const __m128i *)a); + } + +-// Load 64 bits from each of hi and low, and pack into an SSE register +-// Since directly loading as `int64_t`s and using _mm_set_epi64 may violate +-// the strict aliasing rule, this takes a different approach +-static INLINE __m128i xx_loadu_2x64(const void *hi, const void *lo) { +- return _mm_unpacklo_epi64(_mm_loadu_si64(lo), _mm_loadu_si64(hi)); +-} +- + static INLINE void xx_storel_32(void *const a, const __m128i v) { + const int val = _mm_cvtsi128_si32(v); + memcpy(a, &val, sizeof(val)); +diff --git a/aom_dsp/x86/synonyms_avx2.h b/aom_dsp/x86/synonyms_avx2.h +index d4e8f69..45be17e 100644 +--- a/aom_dsp/x86/synonyms_avx2.h ++++ b/aom_dsp/x86/synonyms_avx2.h +@@ -25,6 +25,13 @@ + * Intrinsics prefixed with yy_ operate on or return 256bit YMM registers. + */ + ++// Load 64 bits from each of hi and low, and pack into an SSE register ++// Since directly loading as `int64_t`s and using _mm_set_epi64 may violate ++// the strict aliasing rule, this takes a different approach ++static INLINE __m128i xx_loadu_2x64(const void *hi, const void *lo) { ++ return _mm_unpacklo_epi64(_mm_loadu_si64(lo), _mm_loadu_si64(hi)); ++} ++ + // Loads and stores to do away with the tedium of casting the address + // to the right type. + static INLINE __m256i yy_load_256(const void *a) { diff --git a/vendor/rustdesk/res/vcpkg/aom/aom-install.diff b/vendor/rustdesk/res/vcpkg/aom/aom-install.diff new file mode 100644 index 0000000..e24b8c5 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/aom/aom-install.diff @@ -0,0 +1,75 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 18190f647..f4b1b359d 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -23,6 +23,9 @@ endif() + + project(AOM C CXX) + ++include(GNUInstallDirs) ++include(CMakePackageConfigHelpers) ++ + # GENERATED source property global visibility. + if(POLICY CMP0118) + cmake_policy(SET CMP0118 NEW) +@@ -302,6 +305,52 @@ if(BUILD_SHARED_LIBS) + set(AOM_LIB_TARGETS ${AOM_LIB_TARGETS} aom_static) + endif() + ++set(PUBLIC_HEADERS ++ aom/aom.h ++ aom/aom_codec.h ++ aom/aom_decoder.h ++ aom/aom_encoder.h ++ aom/aom_frame_buffer.h ++ aom/aom_image.h ++ aom/aom_integer.h ++ aom/aomcx.h ++ aom/aomdx.h ++) ++ ++set_target_properties(aom PROPERTIES ++ PUBLIC_HEADER "${PUBLIC_HEADERS}") ++ ++ ++target_include_directories(aom ++ PUBLIC $ ++ $) ++ ++install(TARGETS aom ++ EXPORT unofficial-aom-targets ++ ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" ++ LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" ++ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" ++ PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/aom") ++ ++install(EXPORT unofficial-aom-targets ++ FILE unofficial-aom-targets.cmake ++ NAMESPACE unofficial:: ++ DESTINATION lib/cmake/aom) ++ ++configure_package_config_file(cmake/aom-config.cmake.in ++ ${CMAKE_CURRENT_BINARY_DIR}/aom-config.cmake ++ INSTALL_DESTINATION lib/cmake/aom ++ NO_SET_AND_CHECK_MACRO ++ NO_CHECK_REQUIRED_COMPONENTS_MACRO) ++ ++write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/aom-config-version.cmake ++ VERSION ${SO_FILE_VERSION} ++ COMPATIBILITY SameMajorVersion) ++ ++install(FILES ${CMAKE_CURRENT_BINARY_DIR}/aom-config.cmake ++ ${CMAKE_CURRENT_BINARY_DIR}/aom-config-version.cmake ++ DESTINATION lib/cmake/aom) ++ + # Setup dependencies. + if(CONFIG_THREE_PASS) + setup_ivf_dec_targets() +diff --git a/cmake/aom-config.cmake.in b/cmake/aom-config.cmake.in +new file mode 100644 +index 000000000..91cac3b5b +--- /dev/null ++++ b/cmake/aom-config.cmake.in +@@ -0,0 +1,2 @@ ++@PACKAGE_INIT@ ++include(${CMAKE_CURRENT_LIST_DIR}/unofficial-aom-targets.cmake) diff --git a/vendor/rustdesk/res/vcpkg/aom/aom-uninitialized-pointer.diff b/vendor/rustdesk/res/vcpkg/aom/aom-uninitialized-pointer.diff new file mode 100644 index 0000000..37a7166 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/aom/aom-uninitialized-pointer.diff @@ -0,0 +1,13 @@ +diff --git a/build/cmake/aom_configure.cmake b/build/cmake/aom_configure.cmake +index aaef2c310..5500ad4a3 100644 +--- a/build/cmake/aom_configure.cmake ++++ b/build/cmake/aom_configure.cmake +@@ -309,6 +309,8 @@ if(MSVC) + + # Disable MSVC warnings that suggest making code non-portable. + add_compiler_flag_if_supported("/wd4996") ++ # Disable MSVC warnings for potentially uninitialized local pointer variable. ++ add_compiler_flag_if_supported("/wd4703") + if(ENABLE_WERROR) + add_compiler_flag_if_supported("/WX") + endif() diff --git a/vendor/rustdesk/res/vcpkg/aom/portfile.cmake b/vendor/rustdesk/res/vcpkg/aom/portfile.cmake new file mode 100644 index 0000000..f7b1e3c --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/aom/portfile.cmake @@ -0,0 +1,79 @@ +# NASM is required to build AOM +vcpkg_find_acquire_program(NASM) +get_filename_component(NASM_EXE_PATH ${NASM} DIRECTORY) +vcpkg_add_to_path(${NASM_EXE_PATH}) + +# Perl is required to build AOM +vcpkg_find_acquire_program(PERL) +get_filename_component(PERL_PATH ${PERL} DIRECTORY) +vcpkg_add_to_path(${PERL_PATH}) + +if(DEFINED ENV{USE_AOM_391}) + vcpkg_from_git( + OUT_SOURCE_PATH SOURCE_PATH + URL "https://aomedia.googlesource.com/aom" + REF 8ad484f8a18ed1853c094e7d3a4e023b2a92df28 # 3.9.1 + PATCHES + aom-uninitialized-pointer.diff + aom-avx2.diff + aom-install.diff + ) +else() + vcpkg_from_git( + OUT_SOURCE_PATH SOURCE_PATH + URL "https://aomedia.googlesource.com/aom" + REF 10aece4157eb79315da205f39e19bf6ab3ee30d0 # 3.12.1 + PATCHES + aom-uninitialized-pointer.diff + # aom-avx2.diff + # Can be dropped when https://bugs.chromium.org/p/aomedia/issues/detail?id=3029 is merged into the upstream + aom-install.diff + ) +endif() + +set(aom_target_cpu "") +if(VCPKG_TARGET_IS_UWP OR (VCPKG_TARGET_IS_WINDOWS AND VCPKG_TARGET_ARCHITECTURE MATCHES "^arm")) + # UWP + aom's assembler files result in weirdness and build failures + # Also, disable assembly on ARM and ARM64 Windows to fix compilation issues. + set(aom_target_cpu "-DAOM_TARGET_CPU=generic") +endif() + +if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm" AND VCPKG_TARGET_IS_LINUX) + set(aom_target_cpu "-DENABLE_NEON=OFF") +endif() + +vcpkg_cmake_configure( + SOURCE_PATH ${SOURCE_PATH} + OPTIONS + ${aom_target_cpu} + -DENABLE_DOCS=OFF + -DENABLE_EXAMPLES=OFF + -DENABLE_TESTDATA=OFF + -DENABLE_TESTS=OFF + -DENABLE_TOOLS=OFF +) + +vcpkg_cmake_install() + +vcpkg_copy_pdbs() + +vcpkg_fixup_pkgconfig() + +if(VCPKG_TARGET_IS_WINDOWS) + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/aom.pc" " -lm" "") + if(NOT VCPKG_BUILD_TYPE) + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/aom.pc" " -lm" "") + endif() +endif() + +# Move cmake configs +vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/${PORT}) + +# Remove duplicate files +file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/debug/include + ${CURRENT_PACKAGES_DIR}/debug/share) + +# Handle copyright +file(INSTALL ${SOURCE_PATH}/LICENSE DESTINATION ${CURRENT_PACKAGES_DIR}/share/${PORT} RENAME copyright) + +vcpkg_fixup_pkgconfig() diff --git a/vendor/rustdesk/res/vcpkg/aom/vcpkg.json b/vendor/rustdesk/res/vcpkg/aom/vcpkg.json new file mode 100644 index 0000000..70a12d8 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/aom/vcpkg.json @@ -0,0 +1,18 @@ +{ + "name": "aom", + "version-semver": "3.12.1", + "port-version": 0, + "description": "AV1 codec library", + "homepage": "https://aomedia.googlesource.com/aom", + "license": "BSD-2-Clause", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ] +} diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0001-create-lib-libraries.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0001-create-lib-libraries.patch new file mode 100644 index 0000000..ced7ba8 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0001-create-lib-libraries.patch @@ -0,0 +1,27 @@ +diff --git a/configure b/configure +index 1f0b9497cb..3243e23021 100644 +--- a/configure ++++ b/configure +@@ -5697,17 +5697,19 @@ case $target_os in + ;; + win32|win64) + disable symver +- if enabled shared; then ++# if enabled shared; then + # Link to the import library instead of the normal static library + # for shared libs. + LD_LIB='%.lib' + # Cannot build both shared and static libs with MSVC or icl. +- disable static +- fi ++# disable static ++# fi + ! enabled small && test_cmd $windres --version && enable gnu_windres + enabled x86_32 && check_ldflags -LARGEADDRESSAWARE + add_cppflags -DWIN32_LEAN_AND_MEAN + shlibdir_default="$bindir_default" ++ LIBPREF="" ++ LIBSUF=".lib" + SLIBPREF="" + SLIBSUF=".dll" + SLIBNAME_WITH_VERSION='$(SLIBPREF)$(FULLNAME)-$(LIBVERSION)$(SLIBSUF)' diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0002-fix-msvc-link.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0002-fix-msvc-link.patch new file mode 100644 index 0000000..c9aa7e7 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0002-fix-msvc-link.patch @@ -0,0 +1,11 @@ +diff --git a/configure b/configure +--- a/configure ++++ b/configure +@@ -6162,6 +6162,7 @@ EOF + test -n "$extern_prefix" && append X86ASMFLAGS "-DPREFIX" + case "$objformat" in + elf*) enabled debug && append X86ASMFLAGS $x86asm_debug ;; ++ win*) enabled debug && append X86ASMFLAGS "-g" ;; + esac + + enabled avx512 && check_x86asm avx512_external "vmovdqa32 [eax]{k1}{z}, zmm0" diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0003-fix-windowsinclude.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0003-fix-windowsinclude.patch new file mode 100644 index 0000000..8b2e22b --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0003-fix-windowsinclude.patch @@ -0,0 +1,13 @@ +diff --git a/fftools/cmdutils.c b/fftools/cmdutils.c +--- a/fftools/cmdutils.c ++++ b/fftools/cmdutils.c +@@ -51,6 +51,8 @@ + #include "fopen_utf8.h" + #include "opt_common.h" + #ifdef _WIN32 ++#define _WIN32_WINNT 0x0502 ++#define WIN32_LEAN_AND_MEAN + #include + #include "compat/w32dlfcn.h" + #endif + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0004-dependencies.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0004-dependencies.patch new file mode 100644 index 0000000..f1f6e72 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0004-dependencies.patch @@ -0,0 +1,65 @@ +diff --git a/configure b/configure +index a8b74e0..c99f41c 100755 +--- a/configure ++++ b/configure +@@ -6633,7 +6633,7 @@ fi + + enabled zlib && { check_pkg_config zlib zlib "zlib.h" zlibVersion || + check_lib zlib zlib.h zlibVersion -lz; } +-enabled bzlib && check_lib bzlib bzlib.h BZ2_bzlibVersion -lbz2 ++enabled bzlib && require_pkg_config bzlib bzip2 bzlib.h BZ2_bzlibVersion + enabled lzma && check_lib lzma lzma.h lzma_version_number -llzma + + enabled zlib && test_exec $zlib_extralibs <= 3.98.3" lame/lame.h lame_set_VBR_quality -lmp3lame $libm_extralibs ++enabled libmp3lame && { check_lib libmp3lame lame/lame.h lame_set_VBR_quality -lmp3lame $libm_extralibs || ++ require libmp3lame lame/lame.h lame_set_VBR_quality -llibmp3lame-static -llibmpghip-static $libm_extralibs; } + enabled libmysofa && { check_pkg_config libmysofa libmysofa mysofa.h mysofa_neighborhood_init_withstepdefine || + require libmysofa mysofa.h mysofa_neighborhood_init_withstepdefine -lmysofa $zlib_extralibs; } + enabled libnpp && { check_lib libnpp npp.h nppGetLibVersion -lnppig -lnppicc -lnppc -lnppidei -lnppif || +@@ -6772,7 +6773,7 @@ require_pkg_config libopencv opencv opencv/cxcore.h cvCreateImageHeader; } + enabled libopenh264 && require_pkg_config libopenh264 "openh264 >= 1.3.0" wels/codec_api.h WelsGetCodecVersion + enabled libopenjpeg && { check_pkg_config libopenjpeg "libopenjp2 >= 2.1.0" openjpeg.h opj_version || + { require_pkg_config libopenjpeg "libopenjp2 >= 2.1.0" openjpeg.h opj_version -DOPJ_STATIC && add_cppflags -DOPJ_STATIC; } } +-enabled libopenmpt && require_pkg_config libopenmpt "libopenmpt >= 0.2.6557" libopenmpt/libopenmpt.h openmpt_module_create -lstdc++ && append libopenmpt_extralibs "-lstdc++" ++enabled libopenmpt && require_pkg_config libopenmpt "libopenmpt >= 0.2.6557" libopenmpt/libopenmpt.h openmpt_module_create + enabled libopenvino && { { check_pkg_config libopenvino openvino openvino/c/openvino.h ov_core_create && enable openvino2; } || + { check_pkg_config libopenvino openvino c_api/ie_c_api.h ie_c_api_version || + require libopenvino c_api/ie_c_api.h ie_c_api_version -linference_engine_c_api; } } +@@ -6796,8 +6797,8 @@ enabled libshaderc && require_pkg_config spirv_compiler "shaderc >= 2019. + enabled libshine && require_pkg_config libshine shine shine/layer3.h shine_encode_buffer + enabled libsmbclient && { check_pkg_config libsmbclient smbclient libsmbclient.h smbc_init || + require libsmbclient libsmbclient.h smbc_init -lsmbclient; } +-enabled libsnappy && require libsnappy snappy-c.h snappy_compress -lsnappy -lstdc++ +-enabled libsoxr && require libsoxr soxr.h soxr_create -lsoxr ++enabled libsnappy && require_pkg_config libsnappy snappy snappy-c.h snappy_compress ++enabled libsoxr && require libsoxr soxr.h soxr_create -lsoxr $libm_extralibs + enabled libssh && require_pkg_config libssh "libssh >= 0.6.0" libssh/sftp.h sftp_init + enabled libspeex && require_pkg_config libspeex speex speex/speex.h speex_decoder_init + enabled libsrt && require_pkg_config libsrt "srt >= 1.3.0" srt/srt.h srt_socket +@@ -6880,6 +6881,8 @@ enabled openal && { check_pkg_config openal "openal >= 1.1" "AL/al.h" + enabled opencl && { check_pkg_config opencl OpenCL CL/cl.h clEnqueueNDRangeKernel || + check_lib opencl OpenCL/cl.h clEnqueueNDRangeKernel "-framework OpenCL" || + check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL || ++ check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL -lAdvapi32 -lOle32 -lCfgmgr32|| ++ check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL -pthread -ldl || + die "ERROR: opencl not found"; } && + { test_cpp_condition "OpenCL/cl.h" "defined(CL_VERSION_1_2)" || + test_cpp_condition "CL/cl.h" "defined(CL_VERSION_1_2)" || +@@ -7204,10 +7207,10 @@ enabled amf && + "(AMF_VERSION_MAJOR << 48 | AMF_VERSION_MINOR << 32 | AMF_VERSION_RELEASE << 16 | AMF_VERSION_BUILD_NUM) >= 0x0001000400210000" + + # Funny iconv installations are not unusual, so check it after all flags have been set +-if enabled libc_iconv; then ++if enabled libc_iconv && disabled iconv; then + check_func_headers iconv.h iconv + elif enabled iconv; then +- check_func_headers iconv.h iconv || check_lib iconv iconv.h iconv -liconv ++ check_func_headers iconv.h iconv || check_lib iconv iconv.h iconv -liconv || check_lib iconv iconv.h iconv -liconv -lcharset + fi + + enabled debug && add_cflags -g"$debuglevel" && add_asflags -g"$debuglevel" diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0005-fix-nasm.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0005-fix-nasm.patch new file mode 100644 index 0000000..68b7503 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0005-fix-nasm.patch @@ -0,0 +1,78 @@ +diff --git a/libavcodec/x86/mlpdsp.asm b/libavcodec/x86/mlpdsp.asm +index 3dc641e..609b834 100644 +--- a/libavcodec/x86/mlpdsp.asm ++++ b/libavcodec/x86/mlpdsp.asm +@@ -23,7 +23,9 @@ + + SECTION .text + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++mlpdsp_placeholder: times 4 db 0 ++%else + + %macro SHLX 2 + %if cpuflag(bmi2) +diff --git a/libavcodec/x86/proresdsp.asm b/libavcodec/x86/proresdsp.asm +index 65c9fad..5ad73f3 100644 +--- a/libavcodec/x86/proresdsp.asm ++++ b/libavcodec/x86/proresdsp.asm +@@ -24,7 +24,10 @@ + + %include "libavutil/x86/x86util.asm" + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++SECTION .rdata ++proresdsp_placeholder: times 4 db 0 ++%else + + SECTION_RODATA + +diff --git a/libavcodec/x86/vvc/vvc_mc.asm b/libavcodec/x86/vvc/vvc_mc.asm +index 30aa97c..3975f98 100644 +--- a/libavcodec/x86/vvc/vvc_mc.asm ++++ b/libavcodec/x86/vvc/vvc_mc.asm +@@ -31,7 +31,9 @@ + + SECTION_RODATA 32 + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++vvc_mc_placeholder: times 4 db 0 ++%else + + %if HAVE_AVX2_EXTERNAL + +diff --git a/libavfilter/x86/vf_atadenoise.asm b/libavfilter/x86/vf_atadenoise.asm +index 4945ad3..748b65a 100644 +--- a/libavfilter/x86/vf_atadenoise.asm ++++ b/libavfilter/x86/vf_atadenoise.asm +@@ -20,7 +20,10 @@ + ;* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + ;****************************************************************************** + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++SECTION .rdata ++vf_atadenoise_placeholder: times 4 db 0 ++%else + + %include "libavutil/x86/x86util.asm" + +diff --git a/libavfilter/x86/vf_nlmeans.asm b/libavfilter/x86/vf_nlmeans.asm +index 8f57801..9aef3a4 100644 +--- a/libavfilter/x86/vf_nlmeans.asm ++++ b/libavfilter/x86/vf_nlmeans.asm +@@ -21,7 +21,10 @@ + + %include "libavutil/x86/x86util.asm" + +-%if HAVE_AVX2_EXTERNAL && ARCH_X86_64 ++%ifn HAVE_AVX2_EXTERNAL && ARCH_X86_64 ++SECTION .rdata ++vf_nlmeans_placeholder: times 4 db 0 ++%else + + SECTION_RODATA 32 + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0007-fix-lib-naming.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0007-fix-lib-naming.patch new file mode 100644 index 0000000..c22f9c1 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0007-fix-lib-naming.patch @@ -0,0 +1,12 @@ +diff --git a/configure b/configure +index d6c4388..75b96c3 100644 +--- a/configure ++++ b/configure +@@ -4781,6 +4781,7 @@ msvc_common_flags(){ + -mfp16-format=*) ;; + -lz) echo zlib.lib ;; + -lx264) echo libx264.lib ;; ++ -lmp3lame) echo libmp3lame.lib ;; + -lstdc++) ;; + -l*) echo ${flag#-l}.lib ;; + -LARGEADDRESSAWARE) echo $flag ;; diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0013-define-WINVER.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0013-define-WINVER.patch new file mode 100644 index 0000000..295a738 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0013-define-WINVER.patch @@ -0,0 +1,15 @@ +diff --color -Naur src_old/libavcodec/mf_utils.c src/libavcodec/mf_utils.c +--- src_old/libavcodec/mf_utils.c 2020-07-11 05:26:17.000000000 +0700 ++++ src/libavcodec/mf_utils.c 2020-11-13 12:55:57.226976400 +0700 +@@ -22,6 +22,11 @@ + #define _WIN32_WINNT 0x0602 + #endif + ++#if !defined(WINVER) || WINVER < 0x0602 ++#undef WINVER ++#define WINVER 0x0602 ++#endif ++ + #include "mf_utils.h" + #include "libavutil/pixdesc.h" + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch new file mode 100644 index 0000000..f47e82e --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch @@ -0,0 +1,28 @@ +diff --git a/libswscale/aarch64/yuv2rgb_neon.S b/libswscale/aarch64/yuv2rgb_neon.S +index 89d69e7f6c..4bc1607a7a 100644 +--- a/libswscale/aarch64/yuv2rgb_neon.S ++++ b/libswscale/aarch64/yuv2rgb_neon.S +@@ -169,19 +169,19 @@ function ff_\ifmt\()_to_\ofmt\()_neon, export=1 + sqdmulh v26.8h, v26.8h, v0.8h // ((Y1*(1<<3) - y_offset) * y_coeff) >> 15 + sqdmulh v27.8h, v27.8h, v0.8h // ((Y2*(1<<3) - y_offset) * y_coeff) >> 15 + +-.ifc \ofmt,argb // 1 2 3 0 ++.ifc \ofmt,argb + compute_rgba v5.8b,v6.8b,v7.8b,v4.8b, v17.8b,v18.8b,v19.8b,v16.8b + .endif + +-.ifc \ofmt,rgba // 0 1 2 3 ++.ifc \ofmt,rgba + compute_rgba v4.8b,v5.8b,v6.8b,v7.8b, v16.8b,v17.8b,v18.8b,v19.8b + .endif + +-.ifc \ofmt,abgr // 3 2 1 0 ++.ifc \ofmt,abgr + compute_rgba v7.8b,v6.8b,v5.8b,v4.8b, v19.8b,v18.8b,v17.8b,v16.8b + .endif + +-.ifc \ofmt,bgra // 2 1 0 3 ++.ifc \ofmt,bgra + compute_rgba v6.8b,v5.8b,v4.8b,v7.8b, v18.8b,v17.8b,v16.8b,v19.8b + .endif + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch new file mode 100644 index 0000000..dbce2f5 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch @@ -0,0 +1,15 @@ +diff --git a/configure b/configure +index 4f5353f84b..dd9147c677 100755 +--- a/configure ++++ b/configure +@@ -5607,8 +5607,8 @@ check_cppflags -D_FILE_OFFSET_BITS=64 + check_cppflags -D_LARGEFILE_SOURCE + + add_host_cppflags -D_ISOC11_SOURCE + check_host_cflags_cc -std=$stdc ctype.h "__STDC_VERSION__ >= 201112L" || +- check_host_cflags_cc -std=c11 ctype.h "__STDC_VERSION__ >= 201112L" || die "Host compiler lacks C11 support" ++ check_host_cflags_cc -std=c11 ctype.h "__STDC_VERSION__ >= 201112L" + + check_host_cflags -Wall + check_host_cflags $host_cflags_speed + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch new file mode 100644 index 0000000..c2e1d8f --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch @@ -0,0 +1,35 @@ +diff --git a/libavformat/avformat.h b/libavformat/avformat.h +index cd7b0d941c..b4a6dce885 100644 +--- a/libavformat/avformat.h ++++ b/libavformat/avformat.h +@@ -1169,7 +1169,11 @@ typedef struct AVStreamGroup { + } AVStreamGroup; + + struct AVCodecParserContext *av_stream_get_parser(const AVStream *s); + ++// Chromium: We use the internal field first_dts vvv ++int64_t av_stream_get_first_dts(const AVStream *st); ++// Chromium: We use the internal field first_dts ^^^ ++ + #define AV_PROGRAM_RUNNING 1 + + /** +diff --git a/libavformat/mux_utils.c b/libavformat/mux_utils.c +index de7580c32d..0ef0fe530e 100644 +--- a/libavformat/mux_utils.c ++++ b/libavformat/mux_utils.c +@@ -29,7 +29,14 @@ #include "avformat.h" + #include "avio.h" + #include "internal.h" + #include "mux.h" + ++// Chromium: We use the internal field first_dts vvv ++int64_t av_stream_get_first_dts(const AVStream *st) ++{ ++ return cffstream(st)->first_dts; ++} ++// Chromium: We use the internal field first_dts ^^^ ++ + int avformat_query_codec(const AVOutputFormat *ofmt, enum AVCodecID codec_id, + int std_compliance) + { diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch new file mode 100644 index 0000000..b22b40d --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch @@ -0,0 +1,13 @@ +diff --git a/libavdevice/opengl_enc.c b/libavdevice/opengl_enc.c +index b2ac6eb..6351614 100644 +--- a/libavdevice/opengl_enc.c ++++ b/libavdevice/opengl_enc.c +@@ -116,7 +116,7 @@ typedef void (APIENTRY *FF_PFNGLATTACHSHADERPROC) (GLuint program, GLuint shad + typedef GLuint (APIENTRY *FF_PFNGLCREATESHADERPROC) (GLenum type); + typedef void (APIENTRY *FF_PFNGLDELETESHADERPROC) (GLuint shader); + typedef void (APIENTRY *FF_PFNGLCOMPILESHADERPROC) (GLuint shader); +-typedef void (APIENTRY *FF_PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const char* *string, const GLint *length); ++typedef void (APIENTRY *FF_PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const char* const *string, const GLint *length); + typedef void (APIENTRY *FF_PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params); + typedef void (APIENTRY *FF_PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, char *infoLog); + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch new file mode 100644 index 0000000..6ff63c3 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch @@ -0,0 +1,9 @@ +diff --git a/ffbuild/libversion.sh b/ffbuild/libversion.sh +index a94ab58..ecaa90c 100644 +--- a/ffbuild/libversion.sh ++++ b/ffbuild/libversion.sh +@@ -1,3 +1,4 @@ ++#!/bin/sh + toupper(){ + echo "$@" | tr abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ + } diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/0043-fix-miss-head.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/0043-fix-miss-head.patch new file mode 100644 index 0000000..bad4279 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/0043-fix-miss-head.patch @@ -0,0 +1,12 @@ +diff --git a/libavfilter/textutils.c b/libavfilter/textutils.c +index ef658d0..c61b0ad 100644 +--- a/libavfilter/textutils.c ++++ b/libavfilter/textutils.c +@@ -31,6 +31,7 @@ + #include "libavutil/file.h" + #include "libavutil/mem.h" + #include "libavutil/time.h" ++#include "libavutil/time_internal.h" + + static int ff_expand_text_function_internal(FFExpandTextContext *expand_text, AVBPrint *bp, + char *name, unsigned argc, char **argv) diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/build.sh.in b/vendor/rustdesk/res/vcpkg/ffmpeg/build.sh.in new file mode 100644 index 0000000..4627375 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/build.sh.in @@ -0,0 +1,152 @@ +#!/usr/bin/env bash + +set -e + +export PATH="$PATH:/usr/bin" + +command -v cygpath >/dev/null && have_cygpath=1 + +cygpath() { + if [ -n "$have_cygpath" ]; then + command cygpath "$@" + else + eval _p='$'$# + printf '%s\n' "$_p" + fi +} + +move_binary() { + SOURCE=$1 + TARGET=$2 + BINARY=$3 + + # run lipo over the command to check whether it really + # is a binary that we need to merge architectures + lipo $SOURCE/$BINARY -info &> /dev/null || return 0 + + # get the directory name the file is in + DIRNAME=$(dirname $BINARY) + + # ensure the directory to move the binary to exists + mkdir -p $TARGET/$DIRNAME + + # now finally move the binary + mv $SOURCE/$BINARY $TARGET/$BINARY +} + +move_binaries() { + SOURCE=$1 + TARGET=$2 + + [ ! -d $SOURCE ] && return 0 + pushd $SOURCE + + for BINARY in $(find . -type f); do + move_binary $SOURCE $TARGET $BINARY + done + + popd +} + +merge_binaries() { + TARGET=$1 + SOURCE=$2 + + shift + shift + + pushd $SOURCE/$1 + BINARIES=$(find . -type f) + popd + + for BINARY in $BINARIES; do + COMMAND="lipo -create -output $TARGET/$BINARY" + + for ARCH in $@; do + COMMAND="$COMMAND -arch $ARCH $SOURCE/$ARCH/$BINARY" + done + + $($COMMAND) + done +} + +export PKG_CONFIG_PATH="$(cygpath -p "${PKG_CONFIG_PATH}")" + +# Export HTTP(S)_PROXY as http(s)_proxy: +[ -n "$HTTP_PROXY" ] && export http_proxy="$HTTP_PROXY" +[ -n "$HTTPS_PROXY" ] && export https_proxy="$HTTPS_PROXY" + +PATH_TO_BUILD_DIR=$( cygpath "@BUILD_DIR@") +PATH_TO_SRC_DIR=$( cygpath "@SOURCE_PATH@") +PATH_TO_PACKAGE_DIR=$(cygpath "@INST_PREFIX@") + +JOBS=@VCPKG_CONCURRENCY@ + +OSX_ARCHS="@OSX_ARCHS@" +OSX_ARCH_COUNT=0@OSX_ARCH_COUNT@ + +# Default to hardware concurrency if unset. +: ${JOBS:=$(nproc)} + +# Disable asm and x86asm on all android targets because they trigger build failures: +# arm64 Android build fails with 'relocation R_AARCH64_ADR_PREL_PG_HI21 cannot be used against symbol ff_cos_32; recompile with -fPIC' +# x86 Android build fails with 'error: inline assembly requires more registers than available'. +# x64 Android build fails with 'relocation R_X86_64_PC32 cannot be used against symbol ff_h264_cabac_tables; recompile with -fPIC' +if [ "@VCPKG_CMAKE_SYSTEM_NAME@" = "Android" ]; then + OPTIONS_arm=" --disable-asm --disable-x86asm" + OPTIONS_arm64=" --disable-asm --disable-x86asm" + OPTIONS_x86=" --disable-asm --disable-x86asm" + OPTIONS_x86_64="${OPTIONS_x86}" +else + OPTIONS_arm=" --disable-asm --disable-x86asm" + OPTIONS_arm64=" --enable-asm --disable-x86asm" + OPTIONS_x86=" --enable-asm --enable-x86asm" + OPTIONS_x86_64="${OPTIONS_x86}" +fi + +build_ffmpeg() { + # extract build architecture + BUILD_ARCH=$1 + shift + + echo "BUILD_ARCH=${BUILD_ARCH}" + + # get architecture-specific options + OPTION_VARIABLE="OPTIONS_${BUILD_ARCH}" + echo "OPTION_VARIABLE=${OPTION_VARIABLE}" + + echo "=== CONFIGURING ===" + + sh "$PATH_TO_SRC_DIR/configure" "--prefix=$PATH_TO_PACKAGE_DIR" @CONFIGURE_OPTIONS@ --arch=${BUILD_ARCH} ${!OPTION_VARIABLE} $@ + + echo "=== BUILDING ===" + + make -j${JOBS} V=1 + + echo "=== INSTALLING ===" + + make install +} + +cd "$PATH_TO_BUILD_DIR" + +if [ $OSX_ARCH_COUNT -gt 0 ]; then + for ARCH in $OSX_ARCHS; do + echo "=== CLEANING FOR $ARCH ===" + + make clean && make distclean + + build_ffmpeg $ARCH --extra-cflags=-arch --extra-cflags=$ARCH --extra-ldflags=-arch --extra-ldflags=$ARCH + + echo "=== COLLECTING BINARIES FOR $ARCH ===" + + move_binaries $PATH_TO_PACKAGE_DIR/lib $PATH_TO_BUILD_DIR/stage/$ARCH/lib + move_binaries $PATH_TO_PACKAGE_DIR/bin $PATH_TO_BUILD_DIR/stage/$ARCH/bin + done + + echo "=== MERGING ARCHITECTURES ===" + + merge_binaries $PATH_TO_PACKAGE_DIR $PATH_TO_BUILD_DIR/stage $OSX_ARCHS +else + build_ffmpeg @BUILD_ARCH@ +fi diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch new file mode 100644 index 0000000..4fbce0d --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch @@ -0,0 +1,71 @@ +From da6921d5bcb50961193526f47aa2dbe71ee5fe81 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Tue, 10 Dec 2024 13:40:46 +0800 +Subject: [PATCH 1/5] avcodec/amfenc: add query_timeout option for h264/hevc + +Signed-off-by: 21pages +--- + libavcodec/amfenc.h | 1 + + libavcodec/amfenc_h264.c | 4 ++++ + libavcodec/amfenc_hevc.c | 4 ++++ + 3 files changed, 9 insertions(+) + +diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h +index d985d01bb1..320c66919e 100644 +--- a/libavcodec/amfenc.h ++++ b/libavcodec/amfenc.h +@@ -91,6 +91,7 @@ typedef struct AmfContext { + int quality; + int b_frame_delta_qp; + int ref_b_frame_delta_qp; ++ int64_t query_timeout; + + // Dynamic options, can be set after Init() call + +diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c +index 8edd39c633..6ad4961b2f 100644 +--- a/libavcodec/amfenc_h264.c ++++ b/libavcodec/amfenc_h264.c +@@ -137,6 +137,7 @@ static const AVOption options[] = { + + + { "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg) , AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, ++ { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE }, + + //Pre Analysis options + { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, +@@ -228,6 +229,9 @@ FF_ENABLE_DEPRECATION_WARNINGS + + AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_FRAMERATE, framerate); + ++ if (ctx->query_timeout >= 0) ++ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout); ++ + switch (avctx->profile) { + case AV_PROFILE_H264_BASELINE: + profile = AMF_VIDEO_ENCODER_PROFILE_BASELINE; +diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c +index 4898824f3a..22cb95c7ce 100644 +--- a/libavcodec/amfenc_hevc.c ++++ b/libavcodec/amfenc_hevc.c +@@ -104,6 +104,7 @@ static const AVOption options[] = { + + + { "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg), AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE }, ++ { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE }, + + //Pre Analysis options + { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, +@@ -194,6 +195,9 @@ FF_ENABLE_DEPRECATION_WARNINGS + + AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_FRAMERATE, framerate); + ++ if (ctx->query_timeout >= 0) ++ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_QUERY_TIMEOUT, ctx->query_timeout); ++ + switch (avctx->profile) { + case AV_PROFILE_HEVC_MAIN: + profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN; +-- +2.43.0.windows.1 + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch new file mode 100644 index 0000000..f2ec5df --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch @@ -0,0 +1,71 @@ +From 8d061adb7b00fc765b8001307c025437ef1cad88 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Thu, 5 Sep 2024 16:32:16 +0800 +Subject: [PATCH 2/5] libavcodec/amfenc: reconfig when bitrate change + +Signed-off-by: 21pages +--- + libavcodec/amfenc.c | 20 ++++++++++++++++++++ + libavcodec/amfenc.h | 1 + + 2 files changed, 21 insertions(+) + +diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c +index a47aea6108..f70f0109f6 100644 +--- a/libavcodec/amfenc.c ++++ b/libavcodec/amfenc.c +@@ -275,6 +275,7 @@ static int amf_init_context(AVCodecContext *avctx) + + ctx->hwsurfaces_in_queue = 0; + ctx->hwsurfaces_in_queue_max = 16; ++ ctx->av_bitrate = avctx->bit_rate; + + // configure AMF logger + // the return of these functions indicates old state and do not affect behaviour +@@ -640,6 +641,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe + frame_ref_storage_buffer->pVtbl->Release(frame_ref_storage_buffer); + } + ++static int reconfig_encoder(AVCodecContext *avctx) ++{ ++ AmfContext *ctx = avctx->priv_data; ++ AMF_RESULT res = AMF_OK; ++ ++ if (ctx->av_bitrate != avctx->bit_rate) { ++ av_log(ctx, AV_LOG_INFO, "change bitrate from %d to %d\n", ctx->av_bitrate, avctx->bit_rate); ++ ctx->av_bitrate = avctx->bit_rate; ++ if (avctx->codec->id == AV_CODEC_ID_H264) { ++ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_TARGET_BITRATE, avctx->bit_rate); ++ } else if (avctx->codec->id == AV_CODEC_ID_HEVC) { ++ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_TARGET_BITRATE, avctx->bit_rate); ++ } ++ } ++ return 0; ++} ++ + int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) + { + AmfContext *ctx = avctx->priv_data; +@@ -653,6 +671,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) + int query_output_data_flag = 0; + AMF_RESULT res_resubmit; + ++ reconfig_encoder(avctx); ++ + if (!ctx->encoder) + return AVERROR(EINVAL); + +diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h +index 320c66919e..481e0fb75d 100644 +--- a/libavcodec/amfenc.h ++++ b/libavcodec/amfenc.h +@@ -115,6 +115,7 @@ typedef struct AmfContext { + int max_b_frames; + int qvbr_quality_level; + int hw_high_motion_quality_boost; ++ int64_t av_bitrate; + + // HEVC - specific options + +-- +2.43.0.windows.1 + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch new file mode 100644 index 0000000..77b41a7 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch @@ -0,0 +1,85 @@ +From d74de94b49efcf7a0b25673ace6016938d1b9272 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Tue, 10 Dec 2024 14:12:01 +0800 +Subject: [PATCH 3/5] videotoolbox changing bitrate + +Signed-off-by: 21pages +--- + libavcodec/videotoolboxenc.c | 40 ++++++++++++++++++++++++++++++++++++ + 1 file changed, 40 insertions(+) + +diff --git a/libavcodec/videotoolboxenc.c b/libavcodec/videotoolboxenc.c +index da7b291b03..3c866177f5 100644 +--- a/libavcodec/videotoolboxenc.c ++++ b/libavcodec/videotoolboxenc.c +@@ -279,6 +279,8 @@ typedef struct VTEncContext { + int max_slice_bytes; + int power_efficient; + int max_ref_frames; ++ ++ int last_bit_rate; + } VTEncContext; + + static void vtenc_free_buf_node(BufNode *info) +@@ -1180,6 +1182,7 @@ static int vtenc_create_encoder(AVCodecContext *avctx, + int64_t one_second_value = 0; + void *nums[2]; + ++ vtctx->last_bit_rate = bit_rate; + int status = VTCompressionSessionCreate(kCFAllocatorDefault, + avctx->width, + avctx->height, +@@ -2638,6 +2641,42 @@ out: + return status; + } + ++static void update_config(AVCodecContext *avctx) ++{ ++ VTEncContext *vtctx = avctx->priv_data; ++ ++ if (avctx->codec_id != AV_CODEC_ID_PRORES) { ++ if (avctx->bit_rate != vtctx->last_bit_rate) { ++ av_log(avctx, AV_LOG_INFO, "Setting bit rate to %d\n", avctx->bit_rate); ++ vtctx->last_bit_rate = avctx->bit_rate; ++ SInt32 bit_rate = avctx->bit_rate; ++ CFNumberRef bit_rate_num = CFNumberCreate(kCFAllocatorDefault, ++ kCFNumberSInt32Type, ++ &bit_rate); ++ if (!bit_rate_num) return; ++ ++ if (vtctx->constant_bit_rate) { ++ int status = VTSessionSetProperty(vtctx->session, ++ compat_keys.kVTCompressionPropertyKey_ConstantBitRate, ++ bit_rate_num); ++ if (status == kVTPropertyNotSupportedErr) { ++ av_log(avctx, AV_LOG_ERROR, "Error: -constant_bit_rate true is not supported by the encoder.\n"); ++ } ++ } else { ++ int status = VTSessionSetProperty(vtctx->session, ++ kVTCompressionPropertyKey_AverageBitRate, ++ bit_rate_num); ++ if (status) { ++ av_log(avctx, AV_LOG_ERROR, "Error: cannot set average bit rate: %d\n", status); ++ } ++ } ++ ++ CFRelease(bit_rate_num); ++ } ++ } ++} ++ ++ + static av_cold int vtenc_frame( + AVCodecContext *avctx, + AVPacket *pkt, +@@ -2650,6 +2689,7 @@ static av_cold int vtenc_frame( + CMSampleBufferRef buf = NULL; + ExtraSEI sei = {0}; + ++ update_config(avctx); + if (frame) { + status = vtenc_send_frame(avctx, vtctx, frame); + +-- +2.43.0.windows.1 + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch new file mode 100644 index 0000000..4a552dd --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch @@ -0,0 +1,246 @@ +From 7323bd68c1b34e9298ea557ff7a3e1883b653957 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Tue, 10 Dec 2024 14:28:16 +0800 +Subject: [PATCH 4/5] mediacodec changing bitrate + +Signed-off-by: 21pages +--- + libavcodec/mediacodec_wrapper.c | 98 +++++++++++++++++++++++++++++++++ + libavcodec/mediacodec_wrapper.h | 7 +++ + libavcodec/mediacodecenc.c | 18 ++++++ + 3 files changed, 123 insertions(+) + +diff --git a/libavcodec/mediacodec_wrapper.c b/libavcodec/mediacodec_wrapper.c +index 96c886666a..06b8504304 100644 +--- a/libavcodec/mediacodec_wrapper.c ++++ b/libavcodec/mediacodec_wrapper.c +@@ -35,6 +35,8 @@ + #include "ffjni.h" + #include "mediacodec_wrapper.h" + ++#define PARAMETER_KEY_VIDEO_BITRATE "video-bitrate" ++ + struct JNIAMediaCodecListFields { + + jclass mediacodec_list_class; +@@ -195,6 +197,8 @@ struct JNIAMediaCodecFields { + jmethodID set_input_surface_id; + jmethodID signal_end_of_input_stream_id; + ++ jmethodID set_parameters_id; ++ + jclass mediainfo_class; + + jmethodID init_id; +@@ -248,6 +252,8 @@ static const struct FFJniField jni_amediacodec_mapping[] = { + { "android/media/MediaCodec", "setInputSurface", "(Landroid/view/Surface;)V", FF_JNI_METHOD, OFFSET(set_input_surface_id), 0 }, + { "android/media/MediaCodec", "signalEndOfInputStream", "()V", FF_JNI_METHOD, OFFSET(signal_end_of_input_stream_id), 0 }, + ++ { "android/media/MediaCodec", "setParameters", "(Landroid/os/Bundle;)V", FF_JNI_METHOD, OFFSET(set_parameters_id), 0 }, ++ + { "android/media/MediaCodec$BufferInfo", NULL, NULL, FF_JNI_CLASS, OFFSET(mediainfo_class), 1 }, + + { "android/media/MediaCodec.BufferInfo", "", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 }, +@@ -292,6 +298,24 @@ typedef struct FFAMediaCodecJni { + + static const FFAMediaCodec media_codec_jni; + ++struct JNIABundleFields ++{ ++ jclass bundle_class; ++ jmethodID init_id; ++ jmethodID put_int_id; ++}; ++ ++#define OFFSET(x) offsetof(struct JNIABundleFields, x) ++static const struct FFJniField jni_abundle_mapping[] = { ++ { "android/os/Bundle", NULL, NULL, FF_JNI_CLASS, OFFSET(bundle_class), 1 }, ++ ++ { "android/os/Bundle", "", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 }, ++ { "android/os/Bundle", "putInt", "(Ljava/lang/String;I)V", FF_JNI_METHOD, OFFSET(put_int_id), 1 }, ++ ++ { NULL } ++}; ++#undef OFFSET ++ + #define JNI_GET_ENV_OR_RETURN(env, log_ctx, ret) do { \ + (env) = ff_jni_get_env(log_ctx); \ + if (!(env)) { \ +@@ -1762,6 +1786,70 @@ static int mediacodec_jni_signalEndOfInputStream(FFAMediaCodec *ctx) + return 0; + } + ++ ++static int mediacodec_jni_setParameter(FFAMediaCodec *ctx, const char* name, int value) ++{ ++ JNIEnv *env = NULL; ++ struct JNIABundleFields jfields = { 0 }; ++ jobject object = NULL; ++ jstring key = NULL; ++ FFAMediaCodecJni *codec = (FFAMediaCodecJni *)ctx; ++ void *log_ctx = codec; ++ int ret = -1; ++ ++ JNI_GET_ENV_OR_RETURN(env, codec, AVERROR_EXTERNAL); ++ ++ if (ff_jni_init_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx) < 0) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to init jfields\n"); ++ goto fail; ++ } ++ ++ object = (*env)->NewObject(env, jfields.bundle_class, jfields.init_id); ++ if (!object) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to create bundle object\n"); ++ goto fail; ++ } ++ ++ key = ff_jni_utf_chars_to_jstring(env, name, log_ctx); ++ if (!key) { ++ av_log(log_ctx, AV_LOG_ERROR, "Failed to convert key to jstring\n"); ++ goto fail; ++ } ++ ++ (*env)->CallVoidMethod(env, object, jfields.put_int_id, key, value); ++ if (ff_jni_exception_check(env, 1, log_ctx) < 0) { ++ goto fail; ++ } ++ ++ if (!codec->jfields.set_parameters_id) { ++ av_log(log_ctx, AV_LOG_ERROR, "System doesn't support setParameters\n"); ++ goto fail; ++ } ++ ++ (*env)->CallVoidMethod(env, codec->object, codec->jfields.set_parameters_id, object); ++ if (ff_jni_exception_check(env, 1, log_ctx) < 0) { ++ goto fail; ++ } ++ ++ ret = 0; ++ ++fail: ++ if (key) { ++ (*env)->DeleteLocalRef(env, key); ++ } ++ if (object) { ++ (*env)->DeleteLocalRef(env, object); ++ } ++ ff_jni_reset_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx); ++ ++ return ret; ++} ++ ++static int mediacodec_jni_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate) ++{ ++ return mediacodec_jni_setParameter(ctx, PARAMETER_KEY_VIDEO_BITRATE, bitrate); ++} ++ + static const FFAMediaFormat media_format_jni = { + .class = &amediaformat_class, + +@@ -1821,6 +1909,8 @@ static const FFAMediaCodec media_codec_jni = { + .getConfigureFlagEncode = mediacodec_jni_getConfigureFlagEncode, + .cleanOutputBuffers = mediacodec_jni_cleanOutputBuffers, + .signalEndOfInputStream = mediacodec_jni_signalEndOfInputStream, ++ ++ .setDynamicBitrate = mediacodec_jni_setDynamicBitrate, + }; + + typedef struct FFAMediaFormatNdk { +@@ -2335,6 +2425,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) + return 0; + } + ++static int mediacodec_ndk_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate) ++{ ++ av_log(ctx, AV_LOG_ERROR, "ndk setDynamicBitrate unavailable\n"); ++ return -1; ++} ++ + static const FFAMediaFormat media_format_ndk = { + .class = &amediaformat_ndk_class, + +@@ -2396,6 +2492,8 @@ static const FFAMediaCodec media_codec_ndk = { + .getConfigureFlagEncode = mediacodec_ndk_getConfigureFlagEncode, + .cleanOutputBuffers = mediacodec_ndk_cleanOutputBuffers, + .signalEndOfInputStream = mediacodec_ndk_signalEndOfInputStream, ++ ++ .setDynamicBitrate = mediacodec_ndk_setDynamicBitrate, + }; + + FFAMediaFormat *ff_AMediaFormat_new(int ndk) +diff --git a/libavcodec/mediacodec_wrapper.h b/libavcodec/mediacodec_wrapper.h +index 11a4260497..86c64556ad 100644 +--- a/libavcodec/mediacodec_wrapper.h ++++ b/libavcodec/mediacodec_wrapper.h +@@ -219,6 +219,8 @@ struct FFAMediaCodec { + + // For encoder with FFANativeWindow as input. + int (*signalEndOfInputStream)(FFAMediaCodec *); ++ ++ int (*setDynamicBitrate)(FFAMediaCodec *codec, int bitrate); + }; + + static inline char *ff_AMediaCodec_getName(FFAMediaCodec *codec) +@@ -343,6 +345,11 @@ static inline int ff_AMediaCodec_signalEndOfInputStream(FFAMediaCodec *codec) + return codec->signalEndOfInputStream(codec); + } + ++static inline int ff_AMediaCodec_setDynamicBitrate(FFAMediaCodec *codec, int bitrate) ++{ ++ return codec->setDynamicBitrate(codec, bitrate); ++} ++ + int ff_Build_SDK_INT(AVCodecContext *avctx); + + enum FFAMediaFormatColorRange { +diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c +index 6ca3968a24..221f7360f4 100644 +--- a/libavcodec/mediacodecenc.c ++++ b/libavcodec/mediacodecenc.c +@@ -76,6 +76,8 @@ typedef struct MediaCodecEncContext { + int level; + int pts_as_dts; + int extract_extradata; ++ ++ int last_bit_rate; + } MediaCodecEncContext; + + enum { +@@ -193,6 +195,8 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) + int ret; + int gop; + ++ s->last_bit_rate = avctx->bit_rate; ++ + if (s->use_ndk_codec < 0) + s->use_ndk_codec = !av_jni_get_java_vm(avctx); + +@@ -542,11 +546,25 @@ static int mediacodec_send(AVCodecContext *avctx, + return 0; + } + ++static void update_config(AVCodecContext *avctx) ++{ ++ MediaCodecEncContext *s = avctx->priv_data; ++ if (avctx->bit_rate != s->last_bit_rate) { ++ s->last_bit_rate = avctx->bit_rate; ++ if (0 != ff_AMediaCodec_setDynamicBitrate(s->codec, avctx->bit_rate)) { ++ av_log(avctx, AV_LOG_ERROR, "Failed to set bitrate to %d\n", avctx->bit_rate); ++ } else { ++ av_log(avctx, AV_LOG_INFO, "Set bitrate to %d\n", avctx->bit_rate); ++ } ++ } ++} ++ + static int mediacodec_encode(AVCodecContext *avctx, AVPacket *pkt) + { + MediaCodecEncContext *s = avctx->priv_data; + int ret; + ++ update_config(avctx); + // Return on three case: + // 1. Serious error + // 2. Got a packet success +-- +2.43.0.windows.1 + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch new file mode 100644 index 0000000..a62be5a --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch @@ -0,0 +1,1883 @@ +From 95ebc0ad912447ba83cacb197f506b881f82179e Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Tue, 10 Dec 2024 15:29:21 +0800 +Subject: [PATCH 1/2] dlopen libva + +Signed-off-by: 21pages +--- + libavcodec/vaapi_decode.c | 96 ++++++----- + libavcodec/vaapi_encode.c | 173 ++++++++++--------- + libavcodec/vaapi_encode_h264.c | 3 +- + libavcodec/vaapi_encode_h265.c | 6 +- + libavutil/hwcontext_vaapi.c | 292 ++++++++++++++++++++++++--------- + libavutil/hwcontext_vaapi.h | 96 +++++++++++ + 6 files changed, 477 insertions(+), 189 deletions(-) + +diff --git a/libavcodec/vaapi_decode.c b/libavcodec/vaapi_decode.c +index a59194340f..e202b673f4 100644 +--- a/libavcodec/vaapi_decode.c ++++ b/libavcodec/vaapi_decode.c +@@ -38,17 +38,18 @@ int ff_vaapi_decode_make_param_buffer(AVCodecContext *avctx, + size_t size) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VABufferID buffer; + + av_assert0(pic->nb_param_buffers + 1 <= MAX_PARAM_BUFFERS); + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + type, size, 1, (void*)data, &buffer); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create parameter " + "buffer (type %d): %d (%s).\n", +- type, vas, vaErrorStr(vas)); ++ type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + +@@ -69,6 +70,7 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, + size_t slice_size) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + int index; + +@@ -88,13 +90,13 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, + + index = 2 * pic->nb_slices; + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VASliceParameterBufferType, + params_size, nb_params, (void*)params_data, + &pic->slice_buffers[index]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create slice " +- "parameter buffer: %d (%s).\n", vas, vaErrorStr(vas)); ++ "parameter buffer: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + +@@ -102,15 +104,15 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, + "is %#x.\n", pic->nb_slices, params_size, + pic->slice_buffers[index]); + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VASliceDataBufferType, + slice_size, 1, (void*)slice_data, + &pic->slice_buffers[index + 1]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create slice " + "data buffer (size %zu): %d (%s).\n", +- slice_size, vas, vaErrorStr(vas)); +- vaDestroyBuffer(ctx->hwctx->display, ++ slice_size, vas, vaf->vaErrorStr(vas)); ++ vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->slice_buffers[index]); + return AVERROR(EIO); + } +@@ -127,26 +129,27 @@ static void ff_vaapi_decode_destroy_buffers(AVCodecContext *avctx, + VAAPIDecodePicture *pic) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + int i; + + for (i = 0; i < pic->nb_param_buffers; i++) { +- vas = vaDestroyBuffer(ctx->hwctx->display, ++ vas = vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->param_buffers[i]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy " + "parameter buffer %#x: %d (%s).\n", +- pic->param_buffers[i], vas, vaErrorStr(vas)); ++ pic->param_buffers[i], vas, vaf->vaErrorStr(vas)); + } + } + + for (i = 0; i < 2 * pic->nb_slices; i++) { +- vas = vaDestroyBuffer(ctx->hwctx->display, ++ vas = vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->slice_buffers[i]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy slice " + "slice buffer %#x: %d (%s).\n", +- pic->slice_buffers[i], vas, vaErrorStr(vas)); ++ pic->slice_buffers[i], vas, vaf->vaErrorStr(vas)); + } + } + } +@@ -155,6 +158,7 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, + VAAPIDecodePicture *pic) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + int err; + +@@ -166,37 +170,37 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, + av_log(avctx, AV_LOG_DEBUG, "Decode to surface %#x.\n", + pic->output_surface); + +- vas = vaBeginPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaBeginPicture(ctx->hwctx->display, ctx->va_context, + pic->output_surface); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to begin picture decode " +- "issue: %d (%s).\n", vas, vaErrorStr(vas)); ++ "issue: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaRenderPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaRenderPicture(ctx->hwctx->display, ctx->va_context, + pic->param_buffers, pic->nb_param_buffers); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to upload decode " +- "parameters: %d (%s).\n", vas, vaErrorStr(vas)); ++ "parameters: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaRenderPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaRenderPicture(ctx->hwctx->display, ctx->va_context, + pic->slice_buffers, 2 * pic->nb_slices); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to upload slices: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to end picture decode " +- "issue: %d (%s).\n", vas, vaErrorStr(vas)); ++ "issue: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + if (CONFIG_VAAPI_1 || ctx->hwctx->driver_quirks & + AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS) +@@ -213,10 +217,10 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, + goto exit; + + fail_with_picture: +- vas = vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to end picture decode " +- "after error: %d (%s).\n", vas, vaErrorStr(vas)); ++ "after error: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + } + fail: + ff_vaapi_decode_destroy_buffers(avctx, pic); +@@ -304,6 +308,7 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, + AVHWFramesContext *frames) + { + AVVAAPIDeviceContext *hwctx = device->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAStatus vas; + VASurfaceAttrib *attr; + enum AVPixelFormat source_format, best_format, format; +@@ -313,11 +318,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, + source_format = avctx->sw_pix_fmt; + av_assert0(source_format != AV_PIX_FMT_NONE); + +- vas = vaQuerySurfaceAttributes(hwctx->display, config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config_id, + NULL, &nb_attr); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(ENOSYS); + } + +@@ -325,11 +330,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, + if (!attr) + return AVERROR(ENOMEM); + +- vas = vaQuerySurfaceAttributes(hwctx->display, config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config_id, + attr, &nb_attr); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + av_freep(&attr); + return AVERROR(ENOSYS); + } +@@ -471,6 +476,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + + AVHWDeviceContext *device = (AVHWDeviceContext*)device_ref->data; + AVVAAPIDeviceContext *hwctx = device->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + + codec_desc = avcodec_descriptor_get(avctx->codec_id); + if (!codec_desc) { +@@ -478,7 +484,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + goto fail; + } + +- profile_count = vaMaxNumProfiles(hwctx->display); ++ profile_count = vaf->vaMaxNumProfiles(hwctx->display); + profile_list = av_malloc_array(profile_count, + sizeof(VAProfile)); + if (!profile_list) { +@@ -486,11 +492,11 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + goto fail; + } + +- vas = vaQueryConfigProfiles(hwctx->display, ++ vas = vaf->vaQueryConfigProfiles(hwctx->display, + profile_list, &profile_count); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query profiles: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(ENOSYS); + goto fail; + } +@@ -550,12 +556,12 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, + } + } + +- vas = vaCreateConfig(hwctx->display, matched_va_profile, ++ vas = vaf->vaCreateConfig(hwctx->display, matched_va_profile, + VAEntrypointVLD, NULL, 0, + va_config); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create decode " +- "configuration: %d (%s).\n", vas, vaErrorStr(vas)); ++ "configuration: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -638,7 +644,7 @@ fail: + av_hwframe_constraints_free(&constraints); + av_freep(&hwconfig); + if (*va_config != VA_INVALID_ID) { +- vaDestroyConfig(hwctx->display, *va_config); ++ vaf->vaDestroyConfig(hwctx->display, *va_config); + *va_config = VA_INVALID_ID; + } + av_freep(&profile_list); +@@ -651,12 +657,14 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, + AVHWFramesContext *hw_frames = (AVHWFramesContext *)hw_frames_ctx->data; + AVHWDeviceContext *device_ctx = hw_frames->device_ctx; + AVVAAPIDeviceContext *hwctx; ++ VAAPIDynLoadFunctions *vaf; + VAConfigID va_config = VA_INVALID_ID; + int err; + + if (device_ctx->type != AV_HWDEVICE_TYPE_VAAPI) + return AVERROR(EINVAL); + hwctx = device_ctx->hwctx; ++ vaf = hwctx->funcs; + + err = vaapi_decode_make_config(avctx, hw_frames->device_ref, &va_config, + hw_frames_ctx); +@@ -664,7 +672,7 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, + return err; + + if (va_config != VA_INVALID_ID) +- vaDestroyConfig(hwctx->display, va_config); ++ vaf->vaDestroyConfig(hwctx->display, va_config); + + return 0; + } +@@ -672,6 +680,7 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, + int ff_vaapi_decode_init(AVCodecContext *avctx) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf; + VAStatus vas; + int err; + +@@ -686,13 +695,18 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) + ctx->hwfc = ctx->frames->hwctx; + ctx->device = ctx->frames->device_ctx; + ctx->hwctx = ctx->device->hwctx; ++ if (!ctx->hwctx || !ctx->hwctx->funcs) { ++ err = AVERROR(EINVAL); ++ goto fail; ++ } ++ vaf = ctx->hwctx->funcs; + + err = vaapi_decode_make_config(avctx, ctx->frames->device_ref, + &ctx->va_config, NULL); + if (err) + goto fail; + +- vas = vaCreateContext(ctx->hwctx->display, ctx->va_config, ++ vas = vaf->vaCreateContext(ctx->hwctx->display, ctx->va_config, + avctx->coded_width, avctx->coded_height, + VA_PROGRESSIVE, + ctx->hwfc->surface_ids, +@@ -700,7 +714,7 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) + &ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create decode " +- "context: %d (%s).\n", vas, vaErrorStr(vas)); ++ "context: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -718,22 +732,28 @@ fail: + int ff_vaapi_decode_uninit(AVCodecContext *avctx) + { + VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; ++ VAAPIDynLoadFunctions *vaf = NULL; + VAStatus vas; + ++ if (ctx->hwctx && ctx->hwctx->funcs) ++ vaf = ctx->hwctx->funcs; ++ if (!vaf) ++ return 0; ++ + if (ctx->va_context != VA_INVALID_ID) { +- vas = vaDestroyContext(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaDestroyContext(ctx->hwctx->display, ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy decode " + "context %#x: %d (%s).\n", +- ctx->va_context, vas, vaErrorStr(vas)); ++ ctx->va_context, vas, vaf->vaErrorStr(vas)); + } + } + if (ctx->va_config != VA_INVALID_ID) { +- vas = vaDestroyConfig(ctx->hwctx->display, ctx->va_config); ++ vas = vaf->vaDestroyConfig(ctx->hwctx->display, ctx->va_config); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy decode " + "configuration %#x: %d (%s).\n", +- ctx->va_config, vas, vaErrorStr(vas)); ++ ctx->va_config, vas, vaf->vaErrorStr(vas)); + } + } + +diff --git a/libavcodec/vaapi_encode.c b/libavcodec/vaapi_encode.c +index 16a9a364f0..ccf6fa59d6 100644 +--- a/libavcodec/vaapi_encode.c ++++ b/libavcodec/vaapi_encode.c +@@ -43,6 +43,7 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, + int type, char *data, size_t bit_len) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VABufferID param_buffer, data_buffer; + VABufferID *tmp; +@@ -57,24 +58,24 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, + return AVERROR(ENOMEM); + pic->param_buffers = tmp; + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VAEncPackedHeaderParameterBufferType, + sizeof(params), 1, ¶ms, ¶m_buffer); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create parameter buffer " + "for packed header (type %d): %d (%s).\n", +- type, vas, vaErrorStr(vas)); ++ type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + pic->param_buffers[pic->nb_param_buffers++] = param_buffer; + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VAEncPackedHeaderDataBufferType, + (bit_len + 7) / 8, 1, data, &data_buffer); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create data buffer " + "for packed header (type %d): %d (%s).\n", +- type, vas, vaErrorStr(vas)); ++ type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + pic->param_buffers[pic->nb_param_buffers++] = data_buffer; +@@ -89,6 +90,7 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, + int type, char *data, size_t len) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VABufferID *tmp; + VABufferID buffer; +@@ -98,11 +100,11 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, + return AVERROR(ENOMEM); + pic->param_buffers = tmp; + +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + type, len, 1, data, &buffer); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create parameter buffer " +- "(type %d): %d (%s).\n", type, vas, vaErrorStr(vas)); ++ "(type %d): %d (%s).\n", type, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + pic->param_buffers[pic->nb_param_buffers++] = buffer; +@@ -141,6 +143,7 @@ static int vaapi_encode_wait(AVCodecContext *avctx, FFHWBaseEncodePicture *base_ + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + #endif + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodePicture *pic = base_pic->priv; + VAStatus vas; + +@@ -156,22 +159,22 @@ static int vaapi_encode_wait(AVCodecContext *avctx, FFHWBaseEncodePicture *base_ + base_pic->encode_order, pic->input_surface); + + #if VA_CHECK_VERSION(1, 9, 0) +- if (base_ctx->async_encode) { +- vas = vaSyncBuffer(ctx->hwctx->display, ++ if (base_ctx->async_encode && vaf->vaSyncBuffer) { ++ vas = vaf->vaSyncBuffer(ctx->hwctx->display, + pic->output_buffer, + VA_TIMEOUT_INFINITE); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to sync to output buffer completion: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + } else + #endif + { // If vaSyncBuffer is not implemented, try old version API. +- vas = vaSyncSurface(ctx->hwctx->display, pic->input_surface); ++ vas = vaf->vaSyncSurface(ctx->hwctx->display, pic->input_surface); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to sync to picture completion: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + } +@@ -270,6 +273,7 @@ static int vaapi_encode_issue(AVCodecContext *avctx, + { + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodePicture *pic = base_pic->priv; + VAAPIEncodeSlice *slice; + VAStatus vas; +@@ -587,28 +591,28 @@ static int vaapi_encode_issue(AVCodecContext *avctx, + } + #endif + +- vas = vaBeginPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaBeginPicture(ctx->hwctx->display, ctx->va_context, + pic->input_surface); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to begin picture encode issue: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaRenderPicture(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaRenderPicture(ctx->hwctx->display, ctx->va_context, + pic->param_buffers, pic->nb_param_buffers); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to upload encode parameters: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_with_picture; + } + +- vas = vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vas = vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to end picture encode issue: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + // vaRenderPicture() has been called here, so we should not destroy + // the parameter buffers unless separate destruction is required. +@@ -622,12 +626,12 @@ static int vaapi_encode_issue(AVCodecContext *avctx, + if (CONFIG_VAAPI_1 || ctx->hwctx->driver_quirks & + AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS) { + for (i = 0; i < pic->nb_param_buffers; i++) { +- vas = vaDestroyBuffer(ctx->hwctx->display, ++ vas = vaf->vaDestroyBuffer(ctx->hwctx->display, + pic->param_buffers[i]); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to destroy " + "param buffer %#x: %d (%s).\n", +- pic->param_buffers[i], vas, vaErrorStr(vas)); ++ pic->param_buffers[i], vas, vaf->vaErrorStr(vas)); + // And ignore. + } + } +@@ -636,10 +640,10 @@ static int vaapi_encode_issue(AVCodecContext *avctx, + return 0; + + fail_with_picture: +- vaEndPicture(ctx->hwctx->display, ctx->va_context); ++ vaf->vaEndPicture(ctx->hwctx->display, ctx->va_context); + fail: + for(i = 0; i < pic->nb_param_buffers; i++) +- vaDestroyBuffer(ctx->hwctx->display, pic->param_buffers[i]); ++ vaf->vaDestroyBuffer(ctx->hwctx->display, pic->param_buffers[i]); + if (pic->slices) { + for (i = 0; i < pic->nb_slices; i++) + av_freep(&pic->slices[i].codec_slice_params); +@@ -657,16 +661,17 @@ fail_at_end: + static int vaapi_encode_get_coded_buffer_size(AVCodecContext *avctx, VABufferID buf_id) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VACodedBufferSegment *buf_list, *buf; + int size = 0; + VAStatus vas; + int err; + +- vas = vaMapBuffer(ctx->hwctx->display, buf_id, ++ vas = vaf->vaMapBuffer(ctx->hwctx->display, buf_id, + (void**)&buf_list); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to map output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -674,10 +679,10 @@ static int vaapi_encode_get_coded_buffer_size(AVCodecContext *avctx, VABufferID + for (buf = buf_list; buf; buf = buf->next) + size += buf->size; + +- vas = vaUnmapBuffer(ctx->hwctx->display, buf_id); ++ vas = vaf->vaUnmapBuffer(ctx->hwctx->display, buf_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to unmap output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -689,15 +694,16 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, + VABufferID buf_id, uint8_t **dst) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VACodedBufferSegment *buf_list, *buf; + VAStatus vas; + int err; + +- vas = vaMapBuffer(ctx->hwctx->display, buf_id, ++ vas = vaf->vaMapBuffer(ctx->hwctx->display, buf_id, + (void**)&buf_list); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to map output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -710,10 +716,10 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, + *dst += buf->size; + } + +- vas = vaUnmapBuffer(ctx->hwctx->display, buf_id); ++ vas = vaf->vaUnmapBuffer(ctx->hwctx->display, buf_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to unmap output buffers: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + return err; + } +@@ -936,6 +942,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + { + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAProfile *va_profiles = NULL; + VAEntrypoint *va_entrypoints = NULL; + VAStatus vas; +@@ -977,16 +984,16 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + av_log(avctx, AV_LOG_VERBOSE, "Input surface format is %s.\n", + desc->name); + +- n = vaMaxNumProfiles(ctx->hwctx->display); ++ n = vaf->vaMaxNumProfiles(ctx->hwctx->display); + va_profiles = av_malloc_array(n, sizeof(VAProfile)); + if (!va_profiles) { + err = AVERROR(ENOMEM); + goto fail; + } +- vas = vaQueryConfigProfiles(ctx->hwctx->display, va_profiles, &n); ++ vas = vaf->vaQueryConfigProfiles(ctx->hwctx->display, va_profiles, &n); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query profiles: %d (%s).\n", +- vas, vaErrorStr(vas)); ++ vas, vaf->vaErrorStr(vas)); + err = AVERROR_EXTERNAL; + goto fail; + } +@@ -1007,7 +1014,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + continue; + + #if VA_CHECK_VERSION(1, 0, 0) +- profile_string = vaProfileStr(profile->va_profile); ++ profile_string = vaf->vaProfileStr(profile->va_profile); + #else + profile_string = "(no profile names)"; + #endif +@@ -1037,18 +1044,18 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + av_log(avctx, AV_LOG_VERBOSE, "Using VAAPI profile %s (%d).\n", + profile_string, ctx->va_profile); + +- n = vaMaxNumEntrypoints(ctx->hwctx->display); ++ n = vaf->vaMaxNumEntrypoints(ctx->hwctx->display); + va_entrypoints = av_malloc_array(n, sizeof(VAEntrypoint)); + if (!va_entrypoints) { + err = AVERROR(ENOMEM); + goto fail; + } +- vas = vaQueryConfigEntrypoints(ctx->hwctx->display, ctx->va_profile, ++ vas = vaf->vaQueryConfigEntrypoints(ctx->hwctx->display, ctx->va_profile, + va_entrypoints, &n); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query entrypoints for " + "profile %s (%d): %d (%s).\n", profile_string, +- ctx->va_profile, vas, vaErrorStr(vas)); ++ ctx->va_profile, vas, vaf->vaErrorStr(vas)); + err = AVERROR_EXTERNAL; + goto fail; + } +@@ -1070,7 +1077,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + + ctx->va_entrypoint = va_entrypoints[i]; + #if VA_CHECK_VERSION(1, 0, 0) +- entrypoint_string = vaEntrypointStr(ctx->va_entrypoint); ++ entrypoint_string = vaf->vaEntrypointStr(ctx->va_entrypoint); + #else + entrypoint_string = "(no entrypoint names)"; + #endif +@@ -1095,12 +1102,12 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) + } + + rt_format_attr = (VAConfigAttrib) { VAConfigAttribRTFormat }; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, ctx->va_entrypoint, + &rt_format_attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query RT format " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR_EXTERNAL; + goto fail; + } +@@ -1157,6 +1164,7 @@ static const VAAPIEncodeRCMode vaapi_encode_rc_modes[] = { + static av_cold int vaapi_encode_init_rate_control(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + uint32_t supported_va_rc_modes; + const VAAPIEncodeRCMode *rc_mode; + int64_t rc_bits_per_second; +@@ -1170,12 +1178,12 @@ static av_cold int vaapi_encode_init_rate_control(AVCodecContext *avctx) + VAStatus vas; + char supported_rc_modes_string[64]; + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, ctx->va_entrypoint, + &rc_attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query rate control " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + if (rc_attr.value == VA_ATTRIB_NOT_SUPPORTED) { +@@ -1516,6 +1524,7 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) + { + #if VA_CHECK_VERSION(1, 5, 0) + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAConfigAttrib attr = { VAConfigAttribMaxFrameSize }; + VAStatus vas; + +@@ -1526,14 +1535,14 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) + return AVERROR(EINVAL); + } + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + ctx->max_frame_size = 0; + av_log(avctx, AV_LOG_ERROR, "Failed to query max frame size " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -1573,18 +1582,19 @@ static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) + { + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VAConfigAttrib attr = { VAConfigAttribEncMaxRefFrames }; + uint32_t ref_l0, ref_l1; + int prediction_pre_only, err; + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query reference frames " +- "attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -1602,13 +1612,13 @@ static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) + if (!(ctx->codec->flags & FF_HW_FLAG_INTRA_ONLY || + avctx->gop_size <= 1)) { + attr = (VAConfigAttrib) { VAConfigAttribPredictionDirection }; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_WARNING, "Failed to query prediction direction " +- "attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } else if (attr.value == VA_ATTRIB_NOT_SUPPORTED) { + av_log(avctx, AV_LOG_VERBOSE, "Driver does not report any additional " +@@ -1758,6 +1768,7 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) + { + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAConfigAttrib attr[3] = { { VAConfigAttribEncMaxSlices }, + { VAConfigAttribEncSliceStructure }, + #if VA_CHECK_VERSION(1, 1, 0) +@@ -1789,13 +1800,13 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) + return 0; + } + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + attr, FF_ARRAY_ELEMS(attr)); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query slice " +- "attributes: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attributes: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + max_slices = attr[0].value; +@@ -1849,16 +1860,17 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) + static av_cold int vaapi_encode_init_packed_headers(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VAConfigAttrib attr = { VAConfigAttribEncPackedHeaders }; + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query packed headers " +- "attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -1910,17 +1922,18 @@ static av_cold int vaapi_encode_init_quality(AVCodecContext *avctx) + { + #if VA_CHECK_VERSION(0, 36, 0) + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VAConfigAttrib attr = { VAConfigAttribEncQualityRange }; + int quality = avctx->compression_level; + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query quality " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -1958,16 +1971,17 @@ static av_cold int vaapi_encode_init_roi(AVCodecContext *avctx) + #if VA_CHECK_VERSION(1, 0, 0) + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAStatus vas; + VAConfigAttrib attr = { VAConfigAttribEncROI }; + +- vas = vaGetConfigAttributes(ctx->hwctx->display, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, + ctx->va_profile, + ctx->va_entrypoint, + &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query ROI " +- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); ++ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR_EXTERNAL; + } + +@@ -1992,10 +2006,11 @@ static void vaapi_encode_free_output_buffer(FFRefStructOpaque opaque, + { + AVCodecContext *avctx = opaque.nc; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VABufferID *buffer_id_ref = obj; + VABufferID buffer_id = *buffer_id_ref; + +- vaDestroyBuffer(ctx->hwctx->display, buffer_id); ++ vaf->vaDestroyBuffer(ctx->hwctx->display, buffer_id); + + av_log(avctx, AV_LOG_DEBUG, "Freed output buffer %#x\n", buffer_id); + } +@@ -2005,6 +2020,7 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) + AVCodecContext *avctx = opaque.nc; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VABufferID *buffer_id = obj; + VAStatus vas; + +@@ -2012,13 +2028,13 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) + // to hold the largest possible compressed frame. We assume here + // that the uncompressed frame plus some header data is an upper + // bound on that. +- vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, ++ vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + VAEncCodedBufferType, + 3 * base_ctx->surface_width * base_ctx->surface_height + + (1 << 16), 1, 0, buffer_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create bitstream " +- "output buffer: %d (%s).\n", vas, vaErrorStr(vas)); ++ "output buffer: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(ENOMEM); + } + +@@ -2092,6 +2108,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + { + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = NULL; + AVVAAPIFramesContext *recon_hwctx = NULL; + VAStatus vas; + int err; +@@ -2107,6 +2124,12 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + + ctx->hwctx = base_ctx->device->hwctx; + ++ if (!ctx->hwctx || !ctx->hwctx->funcs) { ++ err = AVERROR(EINVAL); ++ goto fail; ++ } ++ vaf = ctx->hwctx->funcs; ++ + err = vaapi_encode_profile_entrypoint(avctx); + if (err < 0) + goto fail; +@@ -2157,13 +2180,13 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + goto fail; + } + +- vas = vaCreateConfig(ctx->hwctx->display, ++ vas = vaf->vaCreateConfig(ctx->hwctx->display, + ctx->va_profile, ctx->va_entrypoint, + ctx->config_attributes, ctx->nb_config_attributes, + &ctx->va_config); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create encode pipeline " +- "configuration: %d (%s).\n", vas, vaErrorStr(vas)); ++ "configuration: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -2173,7 +2196,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + goto fail; + + recon_hwctx = base_ctx->recon_frames->hwctx; +- vas = vaCreateContext(ctx->hwctx->display, ctx->va_config, ++ vas = vaf->vaCreateContext(ctx->hwctx->display, ctx->va_config, + base_ctx->surface_width, base_ctx->surface_height, + VA_PROGRESSIVE, + recon_hwctx->surface_ids, +@@ -2181,7 +2204,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + &ctx->va_context); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to create encode pipeline " +- "context: %d (%s).\n", vas, vaErrorStr(vas)); ++ "context: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -2255,14 +2278,16 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + + #if VA_CHECK_VERSION(1, 9, 0) + // check vaSyncBuffer function +- vas = vaSyncBuffer(ctx->hwctx->display, VA_INVALID_ID, 0); +- if (vas != VA_STATUS_ERROR_UNIMPLEMENTED) { +- base_ctx->async_encode = 1; +- base_ctx->encode_fifo = av_fifo_alloc2(base_ctx->async_depth, +- sizeof(VAAPIEncodePicture*), +- 0); +- if (!base_ctx->encode_fifo) +- return AVERROR(ENOMEM); ++ if (vaf->vaSyncBuffer) { ++ vas = vaf->vaSyncBuffer(ctx->hwctx->display, VA_INVALID_ID, 0); ++ if (vas != VA_STATUS_ERROR_UNIMPLEMENTED) { ++ base_ctx->async_encode = 1; ++ base_ctx->encode_fifo = av_fifo_alloc2(base_ctx->async_depth, ++ sizeof(VAAPIEncodePicture*), ++ 0); ++ if (!base_ctx->encode_fifo) ++ return AVERROR(ENOMEM); ++ } + } + #endif + +@@ -2291,14 +2316,14 @@ av_cold int ff_vaapi_encode_close(AVCodecContext *avctx) + ff_refstruct_pool_uninit(&ctx->output_buffer_pool); + + if (ctx->va_context != VA_INVALID_ID) { +- if (ctx->hwctx) +- vaDestroyContext(ctx->hwctx->display, ctx->va_context); ++ if (ctx->hwctx && ctx->hwctx->funcs) ++ ctx->hwctx->funcs->vaDestroyContext(ctx->hwctx->display, ctx->va_context); + ctx->va_context = VA_INVALID_ID; + } + + if (ctx->va_config != VA_INVALID_ID) { +- if (ctx->hwctx) +- vaDestroyConfig(ctx->hwctx->display, ctx->va_config); ++ if (ctx->hwctx && ctx->hwctx->funcs) ++ ctx->hwctx->funcs->vaDestroyConfig(ctx->hwctx->display, ctx->va_config); + ctx->va_config = VA_INVALID_ID; + } + +diff --git a/libavcodec/vaapi_encode_h264.c b/libavcodec/vaapi_encode_h264.c +index fb87b68bec..6d4ce630ce 100644 +--- a/libavcodec/vaapi_encode_h264.c ++++ b/libavcodec/vaapi_encode_h264.c +@@ -868,6 +868,7 @@ static int vaapi_encode_h264_init_slice_params(AVCodecContext *avctx, + static av_cold int vaapi_encode_h264_configure(AVCodecContext *avctx) + { + VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodeH264Context *priv = avctx->priv_data; + int err; + +@@ -919,7 +920,7 @@ static av_cold int vaapi_encode_h264_configure(AVCodecContext *avctx) + vaapi_encode_h264_sei_identifier_uuid, + sizeof(priv->sei_identifier.uuid_iso_iec_11578)); + +- driver = vaQueryVendorString(ctx->hwctx->display); ++ driver = vaf->vaQueryVendorString(ctx->hwctx->display); + if (!driver) + driver = "unknown driver"; + +diff --git a/libavcodec/vaapi_encode_h265.c b/libavcodec/vaapi_encode_h265.c +index 2283bcc0b4..7c624f99a9 100644 +--- a/libavcodec/vaapi_encode_h265.c ++++ b/libavcodec/vaapi_encode_h265.c +@@ -899,6 +899,8 @@ static int vaapi_encode_h265_init_slice_params(AVCodecContext *avctx, + static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) + { + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; ++ VAAPIEncodeContext *ctx = avctx->priv_data; ++ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodeH265Context *priv = avctx->priv_data; + + #if VA_CHECK_VERSION(1, 13, 0) +@@ -909,7 +911,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) + VAStatus vas; + + attr.type = VAConfigAttribEncHEVCFeatures; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ctx->va_profile, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, ctx->va_profile, + ctx->va_entrypoint, &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query encoder " +@@ -923,7 +925,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) + } + + attr.type = VAConfigAttribEncHEVCBlockSizes; +- vas = vaGetConfigAttributes(ctx->hwctx->display, ctx->va_profile, ++ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, ctx->va_profile, + ctx->va_entrypoint, &attr, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "Failed to query encoder " +diff --git a/libavutil/hwcontext_vaapi.c b/libavutil/hwcontext_vaapi.c +index 95aa38d9d2..13451e8ad7 100644 +--- a/libavutil/hwcontext_vaapi.c ++++ b/libavutil/hwcontext_vaapi.c +@@ -48,6 +48,7 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) + # include + #endif + ++#include + + #include "avassert.h" + #include "buffer.h" +@@ -60,6 +61,128 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) + #include "pixdesc.h" + #include "pixfmt.h" + ++//////////////////////////////////////////////////////////// ++/// dynamic load functions ++//////////////////////////////////////////////////////////// ++ ++#define LOAD_SYMBOL(name) do { \ ++ funcs->name = dlsym(funcs->handle_va, #name); \ ++ if (!funcs->name) { \ ++ av_log(NULL, AV_LOG_ERROR, "Failed to load %s\n", #name); \ ++ goto fail; \ ++ } \ ++} while(0) ++ ++static void vaapi_free_functions(VAAPIDynLoadFunctions *funcs) ++{ ++ if (!funcs) ++ return; ++ ++ if (funcs->handle_va_x11) ++ dlclose(funcs->handle_va_x11); ++ if (funcs->handle_va_drm) ++ dlclose(funcs->handle_va_drm); ++ if (funcs->handle_va) ++ dlclose(funcs->handle_va); ++ av_free(funcs); ++} ++ ++static VAAPIDynLoadFunctions *vaapi_load_functions(void) ++{ ++ VAAPIDynLoadFunctions *funcs = av_mallocz(sizeof(*funcs)); ++ if (!funcs) ++ return NULL; ++ ++ // Load libva.so ++ funcs->handle_va = dlopen("libva.so.2", RTLD_NOW | RTLD_LOCAL); ++ if (!funcs->handle_va) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva: %s\n", dlerror()); ++ goto fail; ++ } ++ ++ // Load core functions ++ LOAD_SYMBOL(vaInitialize); ++ LOAD_SYMBOL(vaTerminate); ++ LOAD_SYMBOL(vaCreateConfig); ++ LOAD_SYMBOL(vaDestroyConfig); ++ LOAD_SYMBOL(vaCreateContext); ++ LOAD_SYMBOL(vaDestroyContext); ++ LOAD_SYMBOL(vaCreateBuffer); ++ LOAD_SYMBOL(vaDestroyBuffer); ++ LOAD_SYMBOL(vaMapBuffer); ++ LOAD_SYMBOL(vaUnmapBuffer); ++ LOAD_SYMBOL(vaSyncSurface); ++ LOAD_SYMBOL(vaGetConfigAttributes); ++ LOAD_SYMBOL(vaCreateSurfaces); ++ LOAD_SYMBOL(vaDestroySurfaces); ++ LOAD_SYMBOL(vaBeginPicture); ++ LOAD_SYMBOL(vaRenderPicture); ++ LOAD_SYMBOL(vaEndPicture); ++ LOAD_SYMBOL(vaQueryConfigEntrypoints); ++ LOAD_SYMBOL(vaQueryConfigProfiles); ++ LOAD_SYMBOL(vaGetDisplayAttributes); ++ LOAD_SYMBOL(vaErrorStr); ++ LOAD_SYMBOL(vaMaxNumEntrypoints); ++ LOAD_SYMBOL(vaMaxNumProfiles); ++ LOAD_SYMBOL(vaQueryVendorString); ++ LOAD_SYMBOL(vaQuerySurfaceAttributes); ++ LOAD_SYMBOL(vaDestroyImage); ++ LOAD_SYMBOL(vaDeriveImage); ++ LOAD_SYMBOL(vaPutImage); ++ LOAD_SYMBOL(vaCreateImage); ++ LOAD_SYMBOL(vaGetImage); ++ LOAD_SYMBOL(vaExportSurfaceHandle); ++ LOAD_SYMBOL(vaReleaseBufferHandle); ++ LOAD_SYMBOL(vaAcquireBufferHandle); ++ LOAD_SYMBOL(vaSetErrorCallback); ++ LOAD_SYMBOL(vaSetInfoCallback); ++ LOAD_SYMBOL(vaSetDriverName); ++ LOAD_SYMBOL(vaEntrypointStr); ++ LOAD_SYMBOL(vaQueryImageFormats); ++ LOAD_SYMBOL(vaMaxNumImageFormats); ++ LOAD_SYMBOL(vaProfileStr); ++ ++ // Load libva-x11.so ++ funcs->handle_va_x11 = dlopen("libva-x11.so.2", RTLD_NOW | RTLD_LOCAL); ++ if (!funcs->handle_va_x11) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva-x11: %s\n", dlerror()); ++ goto fail; ++ } ++ ++ funcs->vaGetDisplay = dlsym(funcs->handle_va_x11, "vaGetDisplay"); ++ if (!funcs->vaGetDisplay) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load vaGetDisplay\n"); ++ goto fail; ++ } ++ ++ // Load libva-drm.so ++ funcs->handle_va_drm = dlopen("libva-drm.so.2", RTLD_NOW | RTLD_LOCAL); ++ if (!funcs->handle_va_drm) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva-drm: %s\n", dlerror()); ++ goto fail; ++ } ++ ++ funcs->vaGetDisplayDRM = dlsym(funcs->handle_va_drm, "vaGetDisplayDRM"); ++ if (!funcs->vaGetDisplayDRM) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load vaGetDisplayDRM\n"); ++ goto fail; ++ } ++ ++ // Optional functions ++ funcs->vaSyncBuffer = dlsym(funcs->handle_va, "vaSyncBuffer"); ++ av_log(NULL, AV_LOG_DEBUG, "vaSyncBuffer:%p.\n", funcs->vaSyncBuffer); ++ ++ return funcs; ++ ++fail: ++ vaapi_free_functions(funcs); ++ return NULL; ++} ++ ++//////////////////////////////////////////////////////////// ++/// VAAPI API end ++//////////////////////////////////////////////////////////// ++ + + typedef struct VAAPIDevicePriv { + #if HAVE_VAAPI_X11 +@@ -236,6 +359,7 @@ static int vaapi_frames_get_constraints(AVHWDeviceContext *hwdev, + { + VAAPIDeviceContext *ctx = hwdev->hwctx; + AVVAAPIDeviceContext *hwctx = &ctx->p; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + const AVVAAPIHWConfig *config = hwconfig; + VASurfaceAttrib *attr_list = NULL; + VAStatus vas; +@@ -246,11 +370,11 @@ static int vaapi_frames_get_constraints(AVHWDeviceContext *hwdev, + if (config && + !(hwctx->driver_quirks & AV_VAAPI_DRIVER_QUIRK_SURFACE_ATTRIBUTES)) { + attr_count = 0; +- vas = vaQuerySurfaceAttributes(hwctx->display, config->config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config->config_id, + 0, &attr_count); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwdev, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(ENOSYS); + goto fail; + } +@@ -261,11 +385,11 @@ static int vaapi_frames_get_constraints(AVHWDeviceContext *hwdev, + goto fail; + } + +- vas = vaQuerySurfaceAttributes(hwctx->display, config->config_id, ++ vas = vaf->vaQuerySurfaceAttributes(hwctx->display, config->config_id, + attr_list, &attr_count); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwdev, AV_LOG_ERROR, "Failed to query surface attributes: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + err = AVERROR(ENOSYS); + goto fail; + } +@@ -396,6 +520,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + { + VAAPIDeviceContext *ctx = hwdev->hwctx; + AVVAAPIDeviceContext *hwctx = &ctx->p; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAImageFormat *image_list = NULL; + VAStatus vas; + const char *vendor_string; +@@ -403,7 +528,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + enum AVPixelFormat pix_fmt; + unsigned int fourcc; + +- image_count = vaMaxNumImageFormats(hwctx->display); ++ image_count = vaf->vaMaxNumImageFormats(hwctx->display); + if (image_count <= 0) { + err = AVERROR(EIO); + goto fail; +@@ -413,7 +538,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + err = AVERROR(ENOMEM); + goto fail; + } +- vas = vaQueryImageFormats(hwctx->display, image_list, &image_count); ++ vas = vaf->vaQueryImageFormats(hwctx->display, image_list, &image_count); + if (vas != VA_STATUS_SUCCESS) { + err = AVERROR(EIO); + goto fail; +@@ -440,7 +565,7 @@ static int vaapi_device_init(AVHWDeviceContext *hwdev) + } + } + +- vendor_string = vaQueryVendorString(hwctx->display); ++ vendor_string = vaf->vaQueryVendorString(hwctx->display); + if (vendor_string) + av_log(hwdev, AV_LOG_VERBOSE, "VAAPI driver: %s.\n", vendor_string); + +@@ -493,15 +618,16 @@ static void vaapi_buffer_free(void *opaque, uint8_t *data) + { + AVHWFramesContext *hwfc = opaque; + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + + surface_id = (VASurfaceID)(uintptr_t)data; + +- vas = vaDestroySurfaces(hwctx->display, &surface_id, 1); ++ vas = vaf->vaDestroySurfaces(hwctx->display, &surface_id, 1); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to destroy surface %#x: " +- "%d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + } + +@@ -511,6 +637,7 @@ static AVBufferRef *vaapi_pool_alloc(void *opaque, size_t size) + VAAPIFramesContext *ctx = hwfc->hwctx; + AVVAAPIFramesContext *avfc = &ctx->p; + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + AVBufferRef *ref; +@@ -519,13 +646,13 @@ static AVBufferRef *vaapi_pool_alloc(void *opaque, size_t size) + avfc->nb_surfaces >= hwfc->initial_pool_size) + return NULL; + +- vas = vaCreateSurfaces(hwctx->display, ctx->rt_format, ++ vas = vaf->vaCreateSurfaces(hwctx->display, ctx->rt_format, + hwfc->width, hwfc->height, + &surface_id, 1, + ctx->attributes, ctx->nb_attributes); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to create surface: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + return NULL; + } + av_log(hwfc, AV_LOG_DEBUG, "Created surface %#x.\n", surface_id); +@@ -534,7 +661,7 @@ static AVBufferRef *vaapi_pool_alloc(void *opaque, size_t size) + sizeof(surface_id), &vaapi_buffer_free, + hwfc, AV_BUFFER_FLAG_READONLY); + if (!ref) { +- vaDestroySurfaces(hwctx->display, &surface_id, 1); ++ vaf->vaDestroySurfaces(hwctx->display, &surface_id, 1); + return NULL; + } + +@@ -554,6 +681,7 @@ static int vaapi_frames_init(AVHWFramesContext *hwfc) + VAAPIFramesContext *ctx = hwfc->hwctx; + AVVAAPIFramesContext *avfc = &ctx->p; + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + const VAAPIFormatDescriptor *desc; + VAImageFormat *expected_format; + AVBufferRef *test_surface = NULL; +@@ -669,7 +797,7 @@ static int vaapi_frames_init(AVHWFramesContext *hwfc) + err = vaapi_get_image_format(hwfc->device_ctx, + hwfc->sw_format, &expected_format); + if (err == 0) { +- vas = vaDeriveImage(hwctx->display, test_surface_id, &test_image); ++ vas = vaf->vaDeriveImage(hwctx->display, test_surface_id, &test_image); + if (vas == VA_STATUS_SUCCESS) { + if (expected_format->fourcc == test_image.format.fourcc) { + av_log(hwfc, AV_LOG_DEBUG, "Direct mapping possible.\n"); +@@ -680,11 +808,11 @@ static int vaapi_frames_init(AVHWFramesContext *hwfc) + "expected format %08x.\n", + expected_format->fourcc, test_image.format.fourcc); + } +- vaDestroyImage(hwctx->display, test_image.image_id); ++ vaf->vaDestroyImage(hwctx->display, test_image.image_id); + } else { + av_log(hwfc, AV_LOG_DEBUG, "Direct mapping disabled: " + "deriving image does not work: " +- "%d (%s).\n", vas, vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); + } + } else { + av_log(hwfc, AV_LOG_DEBUG, "Direct mapping disabled: " +@@ -765,33 +893,34 @@ static void vaapi_unmap_frame(AVHWFramesContext *hwfc, + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; + VAAPIMapping *map = hwmap->priv; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + + surface_id = (VASurfaceID)(uintptr_t)hwmap->source->data[3]; + av_log(hwfc, AV_LOG_DEBUG, "Unmap surface %#x.\n", surface_id); + +- vas = vaUnmapBuffer(hwctx->display, map->image.buf); ++ vas = vaf->vaUnmapBuffer(hwctx->display, map->image.buf); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to unmap image from surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + + if ((map->flags & AV_HWFRAME_MAP_WRITE) && + !(map->flags & AV_HWFRAME_MAP_DIRECT)) { +- vas = vaPutImage(hwctx->display, surface_id, map->image.image_id, ++ vas = vaf->vaPutImage(hwctx->display, surface_id, map->image.image_id, + 0, 0, hwfc->width, hwfc->height, + 0, 0, hwfc->width, hwfc->height); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to write image to surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + } + +- vas = vaDestroyImage(hwctx->display, map->image.image_id); ++ vas = vaf->vaDestroyImage(hwctx->display, map->image.image_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to destroy image from surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + } + + av_free(map); +@@ -801,6 +930,7 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + AVFrame *dst, const AVFrame *src, int flags) + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAAPIFramesContext *ctx = hwfc->hwctx; + VASurfaceID surface_id; + const VAAPIFormatDescriptor *desc; +@@ -839,10 +969,10 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + map->flags = flags; + map->image.image_id = VA_INVALID_ID; + +- vas = vaSyncSurface(hwctx->display, surface_id); ++ vas = vaf->vaSyncSurface(hwctx->display, surface_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to sync surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -856,11 +986,11 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + // prefer not to be given direct-mapped memory if they request read access. + if (ctx->derive_works && dst->format == hwfc->sw_format && + ((flags & AV_HWFRAME_MAP_DIRECT) || !(flags & AV_HWFRAME_MAP_READ))) { +- vas = vaDeriveImage(hwctx->display, surface_id, &map->image); ++ vas = vaf->vaDeriveImage(hwctx->display, surface_id, &map->image); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to derive image from " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -873,41 +1003,32 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + } + map->flags |= AV_HWFRAME_MAP_DIRECT; + } else { +- vas = vaCreateImage(hwctx->display, image_format, ++ vas = vaf->vaCreateImage(hwctx->display, image_format, + hwfc->width, hwfc->height, &map->image); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to create image for " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } + if (!(flags & AV_HWFRAME_MAP_OVERWRITE)) { +- vas = vaGetImage(hwctx->display, surface_id, 0, 0, ++ vas = vaf->vaGetImage(hwctx->display, surface_id, 0, 0, + hwfc->width, hwfc->height, map->image.image_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to read image from " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } + } + } + +-#if VA_CHECK_VERSION(1, 21, 0) +- if (flags & AV_HWFRAME_MAP_READ) +- vaflags |= VA_MAPBUFFER_FLAG_READ; +- if (flags & AV_HWFRAME_MAP_WRITE) +- vaflags |= VA_MAPBUFFER_FLAG_WRITE; +- // On drivers not implementing vaMapBuffer2 libva calls vaMapBuffer instead. +- vas = vaMapBuffer2(hwctx->display, map->image.buf, &address, vaflags); +-#else +- vas = vaMapBuffer(hwctx->display, map->image.buf, &address); +-#endif ++ vas = vaf->vaMapBuffer(hwctx->display, map->image.buf, &address); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to map image from surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -936,9 +1057,9 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, + fail: + if (map) { + if (address) +- vaUnmapBuffer(hwctx->display, map->image.buf); ++ vaf->vaUnmapBuffer(hwctx->display, map->image.buf); + if (map->image.image_id != VA_INVALID_ID) +- vaDestroyImage(hwctx->display, map->image.image_id); ++ vaf->vaDestroyImage(hwctx->display, map->image.image_id); + av_free(map); + } + return err; +@@ -1080,12 +1201,12 @@ static void vaapi_unmap_from_drm(AVHWFramesContext *dst_fc, + HWMapDescriptor *hwmap) + { + AVVAAPIDeviceContext *dst_dev = dst_fc->device_ctx->hwctx; +- ++ VAAPIDynLoadFunctions *vaf = dst_dev->funcs; + VASurfaceID surface_id = (VASurfaceID)(uintptr_t)hwmap->priv; + + av_log(dst_fc, AV_LOG_DEBUG, "Destroy surface %#x.\n", surface_id); + +- vaDestroySurfaces(dst_dev->display, &surface_id, 1); ++ vaf->vaDestroySurfaces(dst_dev->display, &surface_id, 1); + } + + static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, +@@ -1100,6 +1221,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, + AVHWFramesContext *dst_fc = + (AVHWFramesContext*)dst->hw_frames_ctx->data; + AVVAAPIDeviceContext *dst_dev = dst_fc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = dst_dev->funcs; + const AVDRMFrameDescriptor *desc; + const VAAPIFormatDescriptor *format_desc; + VASurfaceID surface_id; +@@ -1216,7 +1338,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, + * Gallium seem to do the correct error checks, so lets just try the + * PRIME_2 import first. + */ +- vas = vaCreateSurfaces(dst_dev->display, format_desc->rt_format, ++ vas = vaf->vaCreateSurfaces(dst_dev->display, format_desc->rt_format, + src->width, src->height, &surface_id, 1, + prime_attrs, FF_ARRAY_ELEMS(prime_attrs)); + if (vas != VA_STATUS_SUCCESS) +@@ -1267,7 +1389,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, + FFSWAP(uint32_t, buffer_desc.offsets[1], buffer_desc.offsets[2]); + } + +- vas = vaCreateSurfaces(dst_dev->display, format_desc->rt_format, ++ vas = vaf->vaCreateSurfaces(dst_dev->display, format_desc->rt_format, + src->width, src->height, + &surface_id, 1, + buffer_attrs, FF_ARRAY_ELEMS(buffer_attrs)); +@@ -1298,14 +1420,14 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, + FFSWAP(uint32_t, buffer_desc.offsets[1], buffer_desc.offsets[2]); + } + +- vas = vaCreateSurfaces(dst_dev->display, format_desc->rt_format, ++ vas = vaf->vaCreateSurfaces(dst_dev->display, format_desc->rt_format, + src->width, src->height, + &surface_id, 1, + attrs, FF_ARRAY_ELEMS(attrs)); + #endif + if (vas != VA_STATUS_SUCCESS) { + av_log(dst_fc, AV_LOG_ERROR, "Failed to create surface from DRM " +- "object: %d (%s).\n", vas, vaErrorStr(vas)); ++ "object: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + av_log(dst_fc, AV_LOG_DEBUG, "Create surface %#x.\n", surface_id); +@@ -1343,6 +1465,7 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, + const AVFrame *src, int flags) + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VASurfaceID surface_id; + VAStatus vas; + VADRMPRIMESurfaceDescriptor va_desc; +@@ -1356,10 +1479,10 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, + if (flags & AV_HWFRAME_MAP_READ) { + export_flags |= VA_EXPORT_SURFACE_READ_ONLY; + +- vas = vaSyncSurface(hwctx->display, surface_id); ++ vas = vaf->vaSyncSurface(hwctx->display, surface_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to sync surface " +- "%#x: %d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%#x: %d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + } +@@ -1367,14 +1490,14 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, + if (flags & AV_HWFRAME_MAP_WRITE) + export_flags |= VA_EXPORT_SURFACE_WRITE_ONLY; + +- vas = vaExportSurfaceHandle(hwctx->display, surface_id, ++ vas = vaf->vaExportSurfaceHandle(hwctx->display, surface_id, + VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, + export_flags, &va_desc); + if (vas != VA_STATUS_SUCCESS) { + if (vas == VA_STATUS_ERROR_UNIMPLEMENTED) + return AVERROR(ENOSYS); + av_log(hwfc, AV_LOG_ERROR, "Failed to export surface %#x: " +- "%d (%s).\n", surface_id, vas, vaErrorStr(vas)); ++ "%d (%s).\n", surface_id, vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + +@@ -1437,6 +1560,7 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, + HWMapDescriptor *hwmap) + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAAPIDRMImageBufferMapping *mapping = hwmap->priv; + VASurfaceID surface_id; + VAStatus vas; +@@ -1448,19 +1572,19 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, + // DRM PRIME file descriptors are closed by vaReleaseBufferHandle(), + // so we shouldn't close them separately. + +- vas = vaReleaseBufferHandle(hwctx->display, mapping->image.buf); ++ vas = vaf->vaReleaseBufferHandle(hwctx->display, mapping->image.buf); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to release buffer " + "handle of image %#x (derived from surface %#x): " + "%d (%s).\n", mapping->image.buf, surface_id, +- vas, vaErrorStr(vas)); ++ vas, vaf->vaErrorStr(vas)); + } + +- vas = vaDestroyImage(hwctx->display, mapping->image.image_id); ++ vas = vaf->vaDestroyImage(hwctx->display, mapping->image.image_id); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to destroy image " + "derived from surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + } + + av_free(mapping); +@@ -1470,6 +1594,7 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, + const AVFrame *src, int flags) + { + AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + VAAPIDRMImageBufferMapping *mapping = NULL; + VASurfaceID surface_id; + VAStatus vas; +@@ -1483,12 +1608,12 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, + if (!mapping) + return AVERROR(ENOMEM); + +- vas = vaDeriveImage(hwctx->display, surface_id, ++ vas = vaf->vaDeriveImage(hwctx->display, surface_id, + &mapping->image); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to derive image from " + "surface %#x: %d (%s).\n", +- surface_id, vas, vaErrorStr(vas)); ++ surface_id, vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail; + } +@@ -1543,13 +1668,13 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, + } + } + +- vas = vaAcquireBufferHandle(hwctx->display, mapping->image.buf, ++ vas = vaf->vaAcquireBufferHandle(hwctx->display, mapping->image.buf, + &mapping->buffer_info); + if (vas != VA_STATUS_SUCCESS) { + av_log(hwfc, AV_LOG_ERROR, "Failed to get buffer " + "handle from image %#x (derived from surface %#x): " + "%d (%s).\n", mapping->image.buf, surface_id, +- vas, vaErrorStr(vas)); ++ vas, vaf->vaErrorStr(vas)); + err = AVERROR(EIO); + goto fail_derived; + } +@@ -1578,9 +1703,9 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, + return 0; + + fail_mapped: +- vaReleaseBufferHandle(hwctx->display, mapping->image.buf); ++ vaf->vaReleaseBufferHandle(hwctx->display, mapping->image.buf); + fail_derived: +- vaDestroyImage(hwctx->display, mapping->image.image_id); ++ vaf->vaDestroyImage(hwctx->display, mapping->image.image_id); + fail: + av_freep(&mapping); + return err; +@@ -1634,9 +1759,15 @@ static void vaapi_device_free(AVHWDeviceContext *ctx) + { + AVVAAPIDeviceContext *hwctx = ctx->hwctx; + VAAPIDevicePriv *priv = ctx->user_opaque; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + +- if (hwctx->display) +- vaTerminate(hwctx->display); ++ if (hwctx && hwctx->display && vaf && vaf->vaTerminate) ++ vaf->vaTerminate(hwctx->display); ++ ++ if (hwctx && hwctx->funcs) { ++ vaapi_free_functions(hwctx->funcs); ++ hwctx->funcs = NULL; ++ } + + #if HAVE_VAAPI_X11 + if (priv->x11_display) +@@ -1669,20 +1800,21 @@ static int vaapi_device_connect(AVHWDeviceContext *ctx, + VADisplay display) + { + AVVAAPIDeviceContext *hwctx = ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->funcs; + int major, minor; + VAStatus vas; + + #if CONFIG_VAAPI_1 +- vaSetErrorCallback(display, &vaapi_device_log_error, ctx); +- vaSetInfoCallback (display, &vaapi_device_log_info, ctx); ++ vaf->vaSetErrorCallback(display, &vaapi_device_log_error, ctx); ++ vaf->vaSetInfoCallback (display, &vaapi_device_log_info, ctx); + #endif + + hwctx->display = display; + +- vas = vaInitialize(display, &major, &minor); ++ vas = vaf->vaInitialize(display, &major, &minor); + if (vas != VA_STATUS_SUCCESS) { + av_log(ctx, AV_LOG_ERROR, "Failed to initialise VAAPI " +- "connection: %d (%s).\n", vas, vaErrorStr(vas)); ++ "connection: %d (%s).\n", vas, vaf->vaErrorStr(vas)); + return AVERROR(EIO); + } + av_log(ctx, AV_LOG_VERBOSE, "Initialised VAAPI connection: " +@@ -1698,6 +1830,16 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, + VADisplay display = NULL; + const AVDictionaryEntry *ent; + int try_drm, try_x11, try_win32, try_all; ++ VAAPIDeviceContext *hwctx = ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf; ++ ++ hwctx->p.funcs = vaapi_load_functions(); ++ if (!hwctx->p.funcs) { ++ av_log(NULL, AV_LOG_ERROR, "Failed to load libva: %s\n", dlerror()); ++ return AVERROR_EXTERNAL; ++ } ++ ++ vaf = hwctx->p.funcs; + + priv = av_mallocz(sizeof(*priv)); + if (!priv) +@@ -1843,7 +1985,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, + break; + } + +- display = vaGetDisplayDRM(priv->drm_fd); ++ display = vaf->vaGetDisplayDRM(priv->drm_fd); + if (!display) { + av_log(ctx, AV_LOG_VERBOSE, "Cannot open a VA display " + "from DRM device %s.\n", device); +@@ -1861,7 +2003,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, + av_log(ctx, AV_LOG_VERBOSE, "Cannot open X11 display " + "%s.\n", XDisplayName(device)); + } else { +- display = vaGetDisplay(priv->x11_display); ++ display = vaf->vaGetDisplay(priv->x11_display); + if (!display) { + av_log(ctx, AV_LOG_ERROR, "Cannot open a VA display " + "from X11 display %s.\n", XDisplayName(device)); +@@ -1950,11 +2092,11 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, + if (ent) { + #if VA_CHECK_VERSION(0, 38, 0) + VAStatus vas; +- vas = vaSetDriverName(display, ent->value); ++ vas = vaf->vaSetDriverName(display, ent->value); + if (vas != VA_STATUS_SUCCESS) { + av_log(ctx, AV_LOG_ERROR, "Failed to set driver name to " +- "%s: %d (%s).\n", ent->value, vas, vaErrorStr(vas)); +- vaTerminate(display); ++ "%s: %d (%s).\n", ent->value, vas, vaf->vaErrorStr(vas)); ++ vaf->vaTerminate(display); + return AVERROR_EXTERNAL; + } + #else +@@ -1970,6 +2112,8 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, + AVHWDeviceContext *src_ctx, + AVDictionary *opts, int flags) + { ++ VAAPIDeviceContext *hwctx = ctx->hwctx; ++ VAAPIDynLoadFunctions *vaf = hwctx->p.funcs; + #if HAVE_VAAPI_DRM + if (src_ctx->type == AV_HWDEVICE_TYPE_DRM) { + AVDRMDeviceContext *src_hwctx = src_ctx->hwctx; +@@ -2041,7 +2185,7 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, + ctx->user_opaque = priv; + ctx->free = &vaapi_device_free; + +- display = vaGetDisplayDRM(fd); ++ display = vaf->vaGetDisplayDRM(fd); + if (!display) { + av_log(ctx, AV_LOG_ERROR, "Failed to open a VA display from " + "DRM device.\n"); +diff --git a/libavutil/hwcontext_vaapi.h b/libavutil/hwcontext_vaapi.h +index 0b2e071cb3..2c51223d45 100644 +--- a/libavutil/hwcontext_vaapi.h ++++ b/libavutil/hwcontext_vaapi.h +@@ -20,6 +20,100 @@ + #define AVUTIL_HWCONTEXT_VAAPI_H + + #include ++#include ++#include ++#include ++ ++ ++//////////////////////////////////////////////////////////// ++/// VAAPI dynamic load functions start ++//////////////////////////////////////////////////////////// ++ ++typedef struct VAAPIDynLoadFunctions { ++ // Core VA functions ++ VAStatus (*vaInitialize)(VADisplay dpy, int *major_version, int *minor_version); ++ VAStatus (*vaTerminate)(VADisplay dpy); ++ VAStatus (*vaCreateConfig)(VADisplay dpy, VAProfile profile, VAEntrypoint entrypoint, ++ VAConfigAttrib *attrib_list, int num_attribs, VAConfigID *config_id); ++ VAStatus (*vaDestroyConfig)(VADisplay dpy, VAConfigID config_id); ++ VAStatus (*vaCreateContext)(VADisplay dpy, VAConfigID config_id, int picture_width, ++ int picture_height, int flag, VASurfaceID *render_targets, ++ int num_render_targets, VAContextID *context); ++ VAStatus (*vaDestroyContext)(VADisplay dpy, VAContextID context); ++ VAStatus (*vaCreateBuffer)(VADisplay dpy, VAContextID context, VABufferType type, ++ unsigned int size, unsigned int num_elements, void *data, ++ VABufferID *buf_id); ++ VAStatus (*vaDestroyBuffer)(VADisplay dpy, VABufferID buf_id); ++ VAStatus (*vaMapBuffer)(VADisplay dpy, VABufferID buf_id, void **pbuf); ++ VAStatus (*vaUnmapBuffer)(VADisplay dpy, VABufferID buf_id); ++ VAStatus (*vaSyncSurface)(VADisplay dpy, VASurfaceID render_target); ++ VAStatus (*vaGetConfigAttributes)(VADisplay dpy, VAProfile profile, ++ VAEntrypoint entrypoint, VAConfigAttrib *attrib_list, ++ int num_attribs); ++ VAStatus (*vaCreateSurfaces)(VADisplay dpy, unsigned int format, ++ unsigned int width, unsigned int height, ++ VASurfaceID *surfaces, unsigned int num_surfaces, ++ VASurfaceAttrib *attrib_list, unsigned int num_attribs); ++ VAStatus (*vaDestroySurfaces)(VADisplay dpy, VASurfaceID *surfaces, int num_surfaces); ++ VAStatus (*vaBeginPicture)(VADisplay dpy, VAContextID context, VASurfaceID render_target); ++ VAStatus (*vaRenderPicture)(VADisplay dpy, VAContextID context, ++ VABufferID *buffers, int num_buffers); ++ VAStatus (*vaEndPicture)(VADisplay dpy, VAContextID context); ++ VAStatus (*vaQueryConfigEntrypoints)(VADisplay dpy, VAProfile profile, ++ VAEntrypoint *entrypoint_list, int *num_entrypoints); ++ VAStatus (*vaQueryConfigProfiles)(VADisplay dpy, VAProfile *profile_list, int *num_profiles); ++ VAStatus (*vaGetDisplayAttributes)(VADisplay dpy, VADisplayAttribute *attr_list, int num_attributes); ++ const char *(*vaErrorStr)(VAStatus error_status); ++ int (*vaMaxNumEntrypoints)(VADisplay dpy); ++ int (*vaMaxNumProfiles)(VADisplay dpy); ++ const char *(*vaQueryVendorString)(VADisplay dpy); ++ VAStatus (*vaQuerySurfaceAttributes)(VADisplay dpy, VAConfigID config_id, ++ VASurfaceAttrib *attrib_list, int *num_attribs); ++ VAStatus (*vaDestroyImage)(VADisplay dpy, VAImageID image); ++ VAStatus (*vaDeriveImage)(VADisplay dpy, VASurfaceID surface, VAImage *image); ++ VAStatus (*vaPutImage)(VADisplay dpy, VASurfaceID surface, VAImageID image, ++ int src_x, int src_y, unsigned int src_width, unsigned int src_height, ++ int dest_x, int dest_y, unsigned int dest_width, unsigned int dest_height); ++ VAStatus (*vaCreateImage)(VADisplay dpy, VAImageFormat *format, int width, int height, VAImage *image); ++ VAStatus (*vaGetImage)(VADisplay dpy, VASurfaceID surface, ++ int x, int y, unsigned int width, unsigned int height, ++ VAImageID image); ++ VAStatus (*vaExportSurfaceHandle)(VADisplay dpy, VASurfaceID surface_id, ++ uint32_t mem_type, uint32_t flags, ++ void *descriptor); ++ VAStatus (*vaReleaseBufferHandle)(VADisplay dpy, VABufferID buf_id); ++ VAStatus (*vaAcquireBufferHandle)(VADisplay dpy, VABufferID buf_id, ++ VABufferInfo *buf_info); ++ VAStatus (*vaSetErrorCallback)(VADisplay dpy, VAMessageCallback callback, void *user_context); ++ VAStatus (*vaSetInfoCallback)(VADisplay dpy, VAMessageCallback callback, void *user_context); ++ VAStatus (*vaSetDriverName)(VADisplay dpy, const char *driver_name); ++ const char *(*vaEntrypointStr)(VAEntrypoint entrypoint); ++ VAStatus (*vaQueryImageFormats)(VADisplay dpy, VAImageFormat *format_list, int *num_formats); ++ int (*vaMaxNumImageFormats)(VADisplay dpy); ++ const char *(*vaProfileStr)(VAProfile profile); ++ ++ ++ // Optional functions ++ VAStatus (*vaSyncBuffer)(VADisplay dpy, VABufferID buf_id, uint64_t timeout_ns); ++ ++ // X11 specific functions ++ VADisplay (*vaGetDisplay)(Display *dpy); ++ ++ // DRM specific functions ++ VADisplay (*vaGetDisplayDRM)(int fd); ++ ++ ++ ++ // Library handles ++ void *handle_va; ++ void *handle_va_drm; ++ void *handle_va_x11; ++} VAAPIDynLoadFunctions; ++ ++ ++//////////////////////////////////////////////////////////// ++/// VAAPI API end ++//////////////////////////////////////////////////////////// + + /** + * @file +@@ -78,6 +172,8 @@ typedef struct AVVAAPIDeviceContext { + * operations using VAAPI with the same VADisplay. + */ + unsigned int driver_quirks; ++ ++ VAAPIDynLoadFunctions *funcs; + } AVVAAPIDeviceContext; + + /** +-- +2.34.1 + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch new file mode 100644 index 0000000..21a1f4d --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch @@ -0,0 +1,30 @@ +From 595f0468e127f204741b6c37a479d71daaf571eb Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Tue, 10 Dec 2024 21:17:14 +0800 +Subject: [PATCH] fix linux configure + +Signed-off-by: 21pages +--- + configure | 6 ------ + 1 file changed, 6 deletions(-) + +diff --git a/configure b/configure +index d77a55b653..48ca90ac5e 100755 +--- a/configure ++++ b/configure +@@ -7071,12 +7071,6 @@ enabled mmal && { check_lib mmal interface/mmal/mmal.h mmal_port_co + check_lib mmal interface/mmal/mmal.h mmal_port_connect -lmmal_core -lmmal_util -lmmal_vc_client -lbcm_host; } || + die "ERROR: mmal not found" && + check_func_headers interface/mmal/mmal.h "MMAL_PARAMETER_VIDEO_MAX_NUM_CALLBACKS"; } +-enabled openal && { check_pkg_config openal "openal >= 1.1" "AL/al.h" alGetError || +- { for al_extralibs in "${OPENAL_LIBS}" "-lopenal" "-lOpenAL32"; do +- check_lib openal 'AL/al.h' alGetError "${al_extralibs}" && break; done } || +- die "ERROR: openal not found"; } && +- { test_cpp_condition "AL/al.h" "defined(AL_VERSION_1_1)" || +- die "ERROR: openal must be installed and version must be 1.1 or compatible"; } + enabled opencl && { check_pkg_config opencl OpenCL CL/cl.h clEnqueueNDRangeKernel || + check_lib opencl OpenCL/cl.h clEnqueueNDRangeKernel "-framework OpenCL" || + check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL || +-- +2.34.1 + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch new file mode 100644 index 0000000..fe08aeb --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch @@ -0,0 +1,26 @@ +From 1440f556234d135ce58a2ef38916c6a63b05870e Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Sat, 14 Dec 2024 21:39:44 +0800 +Subject: [PATCH] remove amf loop query + +Signed-off-by: 21pages +--- + libavcodec/amfenc.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c +index f70f0109f6..a53a05b16b 100644 +--- a/libavcodec/amfenc.c ++++ b/libavcodec/amfenc.c +@@ -886,7 +886,7 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) + av_usleep(1000); + } + } +- } while (block_and_wait); ++ } while (false); // already set query timeout + + if (res_query == AMF_EOF) { + ret = AVERROR_EOF; +-- +2.43.0.windows.1 + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch new file mode 100644 index 0000000..2e8aff6 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch @@ -0,0 +1,28 @@ +From bec8d49e75b37806e1cff39c75027860fde0bfa2 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Fri, 27 Dec 2024 08:43:12 +0800 +Subject: [PATCH] fix nvenc reconfigure blur + +Signed-off-by: 21pages +--- + libavcodec/nvenc.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/libavcodec/nvenc.c b/libavcodec/nvenc.c +index 2cce478be0..f4c559b7ce 100644 +--- a/libavcodec/nvenc.c ++++ b/libavcodec/nvenc.c +@@ -2741,8 +2741,8 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame) + } + + if (reconfig_bitrate) { +- params.resetEncoder = 1; +- params.forceIDR = 1; ++ params.resetEncoder = 0; ++ params.forceIDR = 0; + + needs_encode_config = 1; + needs_reconfig = 1; +-- +2.43.0.windows.1 + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch new file mode 100644 index 0000000..18da50b --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch @@ -0,0 +1,31 @@ +diff --git a/compat/w32dlfcn.h b/compat/w32dlfcn.h +index ac20e83..1e83aa6 100644 +--- a/compat/w32dlfcn.h ++++ b/compat/w32dlfcn.h +@@ -76,6 +76,7 @@ static inline HMODULE win32_dlopen(const char *name) + if (!name_w) + goto exit; + namelen = wcslen(name_w); ++ /* + // Try local directory first + path = get_module_filename(NULL); + if (!path) +@@ -91,6 +92,7 @@ static inline HMODULE win32_dlopen(const char *name) + path = new_path; + wcscpy(path + pathlen + 1, name_w); + module = LoadLibraryExW(path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); ++ */ + if (module == NULL) { + // Next try System32 directory + pathlen = GetSystemDirectoryW(path, pathsize); +@@ -131,7 +133,9 @@ exit: + return NULL; + module = LoadPackagedLibrary(name_w, 0); + #else +-#define LOAD_FLAGS (LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32) ++// #define LOAD_FLAGS (LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32) ++// Don't dynamic-link libraries from the application directory. ++ #define LOAD_FLAGS LOAD_LIBRARY_SEARCH_SYSTEM32 + /* filename may be be in CP_ACP */ + if (!name_w) + return LoadLibraryExA(name, NULL, LOAD_FLAGS); diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0011-android-mediacodec-encode-align-64.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0011-android-mediacodec-encode-align-64.patch new file mode 100644 index 0000000..28661cb --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0011-android-mediacodec-encode-align-64.patch @@ -0,0 +1,42 @@ +From a609e1666c79ccce4faf7aa61d509bf202df9149 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Fri, 5 Sep 2025 21:35:37 +0800 +Subject: [PATCH] android mediacodec encode align 64 + +Signed-off-by: 21pages +--- + libavcodec/mediacodecenc.c | 11 ++++++----- + 1 file changed, 6 insertions(+), 5 deletions(-) + +diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c +index 221f7360f4..768c8151df 100644 +--- a/libavcodec/mediacodecenc.c ++++ b/libavcodec/mediacodecenc.c +@@ -242,18 +242,19 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) + ff_AMediaFormat_setString(format, "mime", codec_mime); + // Workaround the alignment requirement of mediacodec. We can't do it + // silently for AV_PIX_FMT_MEDIACODEC. ++ const int align = 64; + if (avctx->pix_fmt != AV_PIX_FMT_MEDIACODEC && + (avctx->codec_id == AV_CODEC_ID_H264 || + avctx->codec_id == AV_CODEC_ID_HEVC)) { +- s->width = FFALIGN(avctx->width, 16); +- s->height = FFALIGN(avctx->height, 16); ++ s->width = FFALIGN(avctx->width, align); ++ s->height = FFALIGN(avctx->height, align); + } else { + s->width = avctx->width; + s->height = avctx->height; +- if (s->width % 16 || s->height % 16) ++ if (s->width % align || s->height % align) + av_log(avctx, AV_LOG_WARNING, +- "Video size %dx%d isn't align to 16, it may have device compatibility issue\n", +- s->width, s->height); ++ "Video size %dx%d isn't align to %d, it may have device compatibility issue\n", ++ s->width, s->height, align); + } + ff_AMediaFormat_setInt32(format, "width", s->width); + ff_AMediaFormat_setInt32(format, "height", s->height); +-- +2.43.0.windows.1 + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0012-fix-macos-big-sur-CVBufferCopyAttachments.patch b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0012-fix-macos-big-sur-CVBufferCopyAttachments.patch new file mode 100644 index 0000000..efcd581 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/patch/0012-fix-macos-big-sur-CVBufferCopyAttachments.patch @@ -0,0 +1,60 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: RustDesk +Date: Fri, 1 Nov 2025 08:00:00 +0000 +Subject: [PATCH] Fix CVBufferCopyAttachments crash on macOS Big Sur + +Use weak linking for CVBufferCopyAttachments to avoid symbol resolution +crash on macOS < 12. The function will be NULL on older systems and the +code will fall back to the deprecated CVBufferGetAttachments. + +This fixes a crash on macOS Big Sur (11.x) where CVBufferCopyAttachments +is not available. The runtime check with __builtin_available is not enough +because the symbol is still resolved at load time, causing a dyld error. + +Fixes: https://github.com/rustdesk/rustdesk/issues/13377 +--- + libavutil/hwcontext_videotoolbox.c | 21 ++++++++++++++++++++- + 1 file changed, 20 insertions(+), 1 deletion(-) + +diff --git a/libavutil/hwcontext_videotoolbox.c b/libavutil/hwcontext_videotoolbox.c +index 0000000000..1111111111 100644 +--- a/libavutil/hwcontext_videotoolbox.c ++++ b/libavutil/hwcontext_videotoolbox.c +@@ -33,6 +33,25 @@ + #include "pixfmt.h" + #include "pixdesc.h" + ++// Weak import CVBufferCopyAttachments to support macOS < 12 ++// The runtime check with __builtin_available is not enough because ++// the symbol is still resolved at load time, causing dyld errors on Big Sur. ++// With weak_import, the function pointer will be NULL on older systems. ++#if TARGET_OS_OSX && defined(__MAC_12_0) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_12_0 ++extern CFDictionaryRef CVBufferCopyAttachments(CVBufferRef buffer, CVAttachmentMode mode) ++ __attribute__((weak_import)); ++#endif ++#if TARGET_OS_IOS && defined(__IPHONE_15_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_15_0 ++extern CFDictionaryRef CVBufferCopyAttachments(CVBufferRef buffer, CVAttachmentMode mode) ++ __attribute__((weak_import)); ++#endif ++#if TARGET_OS_TV && defined(__TVOS_15_0) && __TV_OS_VERSION_MAX_ALLOWED >= __TVOS_15_0 ++extern CFDictionaryRef CVBufferCopyAttachments(CVBufferRef buffer, CVAttachmentMode mode) ++ __attribute__((weak_import)); ++#endif ++ ++// End of weak import section ++ + typedef struct VTFramesContext { + /** + * The public AVVTFramesContext. See hwcontext_videotoolbox.h for it. +@@ -547,7 +566,7 @@ static CFDictionaryRef vt_cv_buffer_copy_attachments(CVBufferRef buffer, + (TARGET_OS_TV && defined(__TVOS_15_0) && __TV_OS_VERSION_MAX_ALLOWED >= __TVOS_15_0) + // On recent enough versions, just use the respective API + if (__builtin_available(macOS 12.0, iOS 15.0, tvOS 15.0, *)) +- return CVBufferCopyAttachments(buffer, attachment_mode); ++ if (CVBufferCopyAttachments != NULL) return CVBufferCopyAttachments(buffer, attachment_mode); + #endif + + // Check that the target is lower than macOS 12 / iOS 15 / tvOS 15 +-- +2.43.0 + diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/portfile.cmake b/vendor/rustdesk/res/vcpkg/ffmpeg/portfile.cmake new file mode 100644 index 0000000..16cef83 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/portfile.cmake @@ -0,0 +1,705 @@ +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO ffmpeg/ffmpeg + REF "n${VERSION}" + SHA512 3b273769ef1a1b63aed0691eef317a760f8c83b1d0e1c232b67bbee26db60b4864aafbc88df0e86d6bebf07185bbd057f33e2d5258fde6d97763b9994cd48b6f + HEAD_REF master + PATCHES + 0001-create-lib-libraries.patch + 0002-fix-msvc-link.patch + 0003-fix-windowsinclude.patch + 0004-dependencies.patch + 0005-fix-nasm.patch + 0007-fix-lib-naming.patch + 0013-define-WINVER.patch + 0020-fix-aarch64-libswscale.patch + 0024-fix-osx-host-c11.patch + 0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch # Do not remove this patch. It is required by chromium + 0041-add-const-for-opengl-definition.patch + 0043-fix-miss-head.patch + patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch + patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch + patch/0004-videotoolbox-changing-bitrate.patch + patch/0005-mediacodec-changing-bitrate.patch + patch/0006-dlopen-libva.patch + patch/0007-fix-linux-configure.patch + patch/0008-remove-amf-loop-query.patch + patch/0009-fix-nvenc-reconfigure-blur.patch + patch/0010.disable-loading-DLLs-from-app-dir.patch + patch/0011-android-mediacodec-encode-align-64.patch + patch/0012-fix-macos-big-sur-CVBufferCopyAttachments.patch +) + +if(SOURCE_PATH MATCHES " ") + message(FATAL_ERROR "Error: ffmpeg will not build with spaces in the path. Please use a directory with no spaces") +endif() + +if(NOT VCPKG_TARGET_ARCHITECTURE STREQUAL "wasm32") + vcpkg_find_acquire_program(NASM) + get_filename_component(NASM_EXE_PATH "${NASM}" DIRECTORY) + vcpkg_add_to_path("${NASM_EXE_PATH}") +endif() + +set(OPTIONS "\ +--disable-shared \ +--enable-static \ +--enable-pic \ +--disable-everything \ +--disable-programs \ +--disable-doc \ +--disable-htmlpages \ +--disable-manpages \ +--disable-podpages \ +--disable-txtpages \ +--disable-network \ +--disable-appkit \ +--disable-coreimage \ +--disable-metal \ +--disable-sdl2 \ +--disable-securetransport \ +--disable-vulkan \ +--disable-audiotoolbox \ +--disable-v4l2-m2m \ +--disable-debug \ +--disable-valgrind-backtrace \ +--disable-large-tests \ +--disable-bzlib \ +--disable-avdevice \ +--enable-avcodec \ +--enable-avformat \ +--disable-avfilter \ +--disable-swresample \ +--disable-swscale \ +--disable-postproc \ +--enable-decoder=h264 \ +--enable-decoder=hevc \ +--enable-parser=h264 \ +--enable-parser=hevc \ +--enable-bsf=h264_mp4toannexb \ +--enable-bsf=hevc_mp4toannexb \ +--enable-bsf=h264_metadata \ +--enable-bsf=hevc_metadata \ +--enable-muxer=mp4 \ +--enable-protocol=file \ +") + +if(VCPKG_HOST_IS_WINDOWS) + vcpkg_acquire_msys(MSYS_ROOT PACKAGES automake1.16) + set(SHELL "${MSYS_ROOT}/usr/bin/bash.exe") + vcpkg_add_to_path("${MSYS_ROOT}/usr/share/automake-1.16") + string(APPEND OPTIONS " --pkg-config=${CURRENT_HOST_INSTALLED_DIR}/tools/pkgconf/pkgconf${VCPKG_HOST_EXECUTABLE_SUFFIX}") +else() + find_program(SHELL bash) +endif() + +if(VCPKG_TARGET_IS_LINUX) + string(APPEND OPTIONS "\ +--target-os=linux \ +--enable-pthreads \ +--disable-vdpau \ +") + + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm") + else() + string(APPEND OPTIONS "\ +--enable-cuda \ +--enable-ffnvcodec \ +--enable-encoder=h264_nvenc \ +--enable-encoder=hevc_nvenc \ +--enable-hwaccel=h264_nvdec \ +--enable-hwaccel=hevc_nvdec \ +--enable-amf \ +--enable-encoder=h264_amf \ +--enable-encoder=hevc_amf \ +--enable-hwaccel=h264_vaapi \ +--enable-hwaccel=hevc_vaapi \ +--enable-encoder=h264_vaapi \ +--enable-encoder=hevc_vaapi \ +") + + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64") + string(APPEND OPTIONS "\ + --enable-cuda_llvm \ +") + endif() + endif() +elseif(VCPKG_TARGET_IS_WINDOWS) + string(APPEND OPTIONS "\ +--target-os=win32 \ +--toolchain=msvc \ +--cc=cl \ +--enable-gpl \ +--enable-d3d11va \ +--enable-cuda \ +--enable-ffnvcodec \ +--enable-hwaccel=h264_nvdec \ +--enable-hwaccel=hevc_nvdec \ +--enable-hwaccel=h264_d3d11va \ +--enable-hwaccel=hevc_d3d11va \ +--enable-hwaccel=h264_d3d11va2 \ +--enable-hwaccel=hevc_d3d11va2 \ +--enable-amf \ +--enable-encoder=h264_amf \ +--enable-encoder=hevc_amf \ +--enable-encoder=h264_nvenc \ +--enable-encoder=hevc_nvenc \ +--enable-libmfx \ +--enable-encoder=h264_qsv \ +--enable-encoder=hevc_qsv \ +") + + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86") + set(LIB_MACHINE_ARG /machine:x86) + string(APPEND OPTIONS " --arch=i686 --enable-cross-compile") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64") + set(LIB_MACHINE_ARG /machine:x64) + string(APPEND OPTIONS " --arch=x86_64") + else() + message(FATAL_ERROR "Unsupported target architecture") + endif() +elseif(VCPKG_TARGET_IS_OSX) + string(APPEND OPTIONS "\ +--disable-autodetect \ +--enable-videotoolbox \ +--enable-encoder=h264_videotoolbox,hevc_videotoolbox \ +--enable-hwaccel=h264_videotoolbox,hevc_videotoolbox \ +") +elseif(VCPKG_TARGET_IS_IOS) + string(APPEND OPTIONS "\ +--arch=arm64 \ +--disable-autodetect \ +--disable-hwaccels \ +--disable-encoders \ +--disable-videotoolbox \ +--extra-cflags=\"-arch arm64 -mios-version-min=8.0 -fembed-bitcode\" \ +--extra-ldflags=\"-arch arm64 -mios-version-min=8.0 -fembed-bitcode\" \ +") +elseif(VCPKG_CMAKE_SYSTEM_NAME STREQUAL "Android") + string(APPEND OPTIONS "\ +--target-os=android \ +--disable-asm \ +--disable-iconv \ +--enable-jni \ +--enable-mediacodec \ +--disable-hwaccels \ +--enable-encoder=h264_mediacodec \ +--enable-encoder=hevc_mediacodec \ +--enable-decoder=h264_mediacodec \ +--enable-decoder=hevc_mediacodec \ +") +endif() + +if(VCPKG_TARGET_IS_OSX) + list(JOIN VCPKG_OSX_ARCHITECTURES " " OSX_ARCHS) + list(LENGTH VCPKG_OSX_ARCHITECTURES OSX_ARCH_COUNT) +endif() + +vcpkg_cmake_get_vars(cmake_vars_file) +include("${cmake_vars_file}") + +if(VCPKG_DETECTED_MSVC) + string(APPEND OPTIONS " --disable-inline-asm") # clang-cl has inline assembly but this leads to undefined symbols. + set(OPTIONS "--toolchain=msvc ${OPTIONS}") + + # This is required because ffmpeg depends upon optimizations to link correctly + string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -O2") + string(REGEX REPLACE "(^| )-RTC1( |$)" " " VCPKG_COMBINED_C_FLAGS_DEBUG "${VCPKG_COMBINED_C_FLAGS_DEBUG}") + string(REGEX REPLACE "(^| )-Od( |$)" " " VCPKG_COMBINED_C_FLAGS_DEBUG "${VCPKG_COMBINED_C_FLAGS_DEBUG}") + string(REGEX REPLACE "(^| )-Ob0( |$)" " " VCPKG_COMBINED_C_FLAGS_DEBUG "${VCPKG_COMBINED_C_FLAGS_DEBUG}") +endif() + +string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include\"") +string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include\"") + +if(VCPKG_TARGET_IS_WINDOWS) + string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"") + string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"") +endif() + +# # Setup vcpkg toolchain +set(prog_env "") + +if(VCPKG_DETECTED_CMAKE_C_COMPILER) + get_filename_component(CC_path "${VCPKG_DETECTED_CMAKE_C_COMPILER}" DIRECTORY) + get_filename_component(CC_filename "${VCPKG_DETECTED_CMAKE_C_COMPILER}" NAME) + set(ENV{CC} "${CC_filename}") + string(APPEND OPTIONS " --cc=${CC_filename}") + + if(VCPKG_HOST_IS_WINDOWS) + string(APPEND OPTIONS " --host_cc=${CC_filename}") + endif() + + list(APPEND prog_env "${CC_path}") +endif() + +if(VCPKG_DETECTED_CMAKE_CXX_COMPILER) + get_filename_component(CXX_path "${VCPKG_DETECTED_CMAKE_CXX_COMPILER}" DIRECTORY) + get_filename_component(CXX_filename "${VCPKG_DETECTED_CMAKE_CXX_COMPILER}" NAME) + set(ENV{CXX} "${CXX_filename}") + string(APPEND OPTIONS " --cxx=${CXX_filename}") + + # string(APPEND OPTIONS " --host_cxx=${CC_filename}") + list(APPEND prog_env "${CXX_path}") +endif() + +if(VCPKG_DETECTED_CMAKE_RC_COMPILER) + get_filename_component(RC_path "${VCPKG_DETECTED_CMAKE_RC_COMPILER}" DIRECTORY) + get_filename_component(RC_filename "${VCPKG_DETECTED_CMAKE_RC_COMPILER}" NAME) + set(ENV{WINDRES} "${RC_filename}") + string(APPEND OPTIONS " --windres=${RC_filename}") + list(APPEND prog_env "${RC_path}") +endif() + +if(VCPKG_DETECTED_CMAKE_LINKER AND VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) + get_filename_component(LD_path "${VCPKG_DETECTED_CMAKE_LINKER}" DIRECTORY) + get_filename_component(LD_filename "${VCPKG_DETECTED_CMAKE_LINKER}" NAME) + set(ENV{LD} "${LD_filename}") + string(APPEND OPTIONS " --ld=${LD_filename}") + + # string(APPEND OPTIONS " --host_ld=${LD_filename}") + list(APPEND prog_env "${LD_path}") +endif() + +if(VCPKG_DETECTED_CMAKE_NM) + get_filename_component(NM_path "${VCPKG_DETECTED_CMAKE_NM}" DIRECTORY) + get_filename_component(NM_filename "${VCPKG_DETECTED_CMAKE_NM}" NAME) + set(ENV{NM} "${NM_filename}") + string(APPEND OPTIONS " --nm=${NM_filename}") + list(APPEND prog_env "${NM_path}") +endif() + +if(VCPKG_DETECTED_CMAKE_AR) + get_filename_component(AR_path "${VCPKG_DETECTED_CMAKE_AR}" DIRECTORY) + get_filename_component(AR_filename "${VCPKG_DETECTED_CMAKE_AR}" NAME) + + if(AR_filename MATCHES [[^(llvm-)?lib\.exe$]]) + set(ENV{AR} "ar-lib ${AR_filename}") + string(APPEND OPTIONS " --ar='ar-lib ${AR_filename}'") + else() + set(ENV{AR} "${AR_filename}") + string(APPEND OPTIONS " --ar='${AR_filename}'") + endif() + + list(APPEND prog_env "${AR_path}") +endif() + +if(VCPKG_DETECTED_CMAKE_RANLIB) + get_filename_component(RANLIB_path "${VCPKG_DETECTED_CMAKE_RANLIB}" DIRECTORY) + get_filename_component(RANLIB_filename "${VCPKG_DETECTED_CMAKE_RANLIB}" NAME) + set(ENV{RANLIB} "${RANLIB_filename}") + string(APPEND OPTIONS " --ranlib=${RANLIB_filename}") + list(APPEND prog_env "${RANLIB_path}") +endif() + +if(VCPKG_DETECTED_CMAKE_STRIP) + get_filename_component(STRIP_path "${VCPKG_DETECTED_CMAKE_STRIP}" DIRECTORY) + get_filename_component(STRIP_filename "${VCPKG_DETECTED_CMAKE_STRIP}" NAME) + set(ENV{STRIP} "${STRIP_filename}") + string(APPEND OPTIONS " --strip=${STRIP_filename}") + list(APPEND prog_env "${STRIP_path}") +endif() + +if(VCPKG_HOST_IS_WINDOWS) + vcpkg_acquire_msys(MSYS_ROOT PACKAGES automake1.16) + set(SHELL "${MSYS_ROOT}/usr/bin/bash.exe") + list(APPEND prog_env "${MSYS_ROOT}/usr/bin" "${MSYS_ROOT}/usr/share/automake-1.16") +else() + # find_program(SHELL bash) +endif() + +list(REMOVE_DUPLICATES prog_env) +vcpkg_add_to_path(PREPEND ${prog_env}) + +# More? OBJCC BIN2C +file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg" "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel") + +set(FFMPEG_PKGCONFIG_MODULES libavutil) + +set(OPTIONS_CROSS "--enable-cross-compile") + +# ffmpeg needs --cross-prefix option to use appropriate tools for cross-compiling. +if(VCPKG_DETECTED_CMAKE_C_COMPILER MATCHES "([^\/]*-)gcc$") + string(APPEND OPTIONS_CROSS " --cross-prefix=${CMAKE_MATCH_1}") +endif() + +if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64") + set(BUILD_ARCH "x86_64") +else() + set(BUILD_ARCH ${VCPKG_TARGET_ARCHITECTURE}) +endif() + +if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm" OR VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64") + if(VCPKG_TARGET_IS_WINDOWS) + vcpkg_find_acquire_program(GASPREPROCESSOR) + + foreach(GAS_PATH ${GASPREPROCESSOR}) + get_filename_component(GAS_ITEM_PATH ${GAS_PATH} DIRECTORY) + vcpkg_add_to_path("${GAS_ITEM_PATH}") + endforeach(GAS_PATH) + endif() +endif() + +set(OPTIONS_DEBUG "--disable-optimizations") +set(OPTIONS_RELEASE "--enable-optimizations") + +set(OPTIONS "${OPTIONS} ${OPTIONS_CROSS}") + +if(VCPKG_TARGET_IS_MINGW) + set(OPTIONS "${OPTIONS} --extra_cflags=-D_WIN32_WINNT=0x0601") +elseif(VCPKG_TARGET_IS_WINDOWS) + set(OPTIONS "${OPTIONS} --extra-cflags=-DHAVE_UNISTD_H=0") +endif() + +vcpkg_find_acquire_program(PKGCONFIG) +set(OPTIONS "${OPTIONS} --pkg-config=${PKGCONFIG}") + +if(VCPKG_LIBRARY_LINKAGE STREQUAL "static") + set(OPTIONS "${OPTIONS} --pkg-config-flags=--static") +endif() + +message(STATUS "Building Options: ${OPTIONS}") + +# Release build +if(NOT VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") + if(VCPKG_DETECTED_MSVC) + set(OPTIONS_RELEASE "${OPTIONS_RELEASE} --extra-ldflags=-libpath:\"${CURRENT_INSTALLED_DIR}/lib\"") + else() + set(OPTIONS_RELEASE "${OPTIONS_RELEASE} --extra-ldflags=-L\"${CURRENT_INSTALLED_DIR}/lib\"") + endif() + + message(STATUS "Building Release Options: ${OPTIONS_RELEASE}") + set(ENV{PKG_CONFIG_PATH} "${CURRENT_INSTALLED_DIR}/lib/pkgconfig") + message(STATUS "Building ${PORT} for Release") + file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel") + + # We use response files here as the only known way to handle spaces in paths + set(crsp "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/cflags.rsp") + string(REGEX REPLACE "-arch [A-Za-z0-9_]+" "" VCPKG_COMBINED_C_FLAGS_RELEASE_SANITIZED "${VCPKG_COMBINED_C_FLAGS_RELEASE}") + file(WRITE "${crsp}" "${VCPKG_COMBINED_C_FLAGS_RELEASE_SANITIZED}") + set(ldrsp "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/ldflags.rsp") + string(REGEX REPLACE "-arch [A-Za-z0-9_]+" "" VCPKG_COMBINED_SHARED_LINKER_FLAGS_RELEASE_SANITIZED "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_RELEASE}") + file(WRITE "${ldrsp}" "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_RELEASE_SANITIZED}") + set(ENV{CFLAGS} "@${crsp}") + + # All tools except the msvc arm{,64} assembler accept @... as response file syntax. + # For that assembler, there is no known way to pass in flags. We must hope that not passing flags will work acceptably. + if(NOT VCPKG_DETECTED_MSVC OR NOT VCPKG_TARGET_ARCHITECTURE MATCHES "^arm") + set(ENV{ASFLAGS} "@${crsp}") + endif() + + set(ENV{LDFLAGS} "@${ldrsp}") + set(ENV{ARFLAGS} "${VCPKG_COMBINED_STATIC_LINKER_FLAGS_RELEASE}") + + set(BUILD_DIR "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel") + set(CONFIGURE_OPTIONS "${OPTIONS} ${OPTIONS_RELEASE}") + set(INST_PREFIX "${CURRENT_PACKAGES_DIR}") + + configure_file("${CMAKE_CURRENT_LIST_DIR}/build.sh.in" "${BUILD_DIR}/build.sh" @ONLY) + + z_vcpkg_setup_pkgconfig_path(CONFIG RELEASE) + + vcpkg_execute_required_process( + COMMAND "${SHELL}" ./build.sh + WORKING_DIRECTORY "${BUILD_DIR}" + LOGNAME "build-${TARGET_TRIPLET}-rel" + SAVE_LOG_FILES ffbuild/config.log + ) + + z_vcpkg_restore_pkgconfig_path() +endif() + +# Debug build +if(NOT VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + if(VCPKG_DETECTED_MSVC) + set(OPTIONS_DEBUG "${OPTIONS_DEBUG} --extra-ldflags=-libpath:\"${CURRENT_INSTALLED_DIR}/debug/lib\"") + else() + set(OPTIONS_DEBUG "${OPTIONS_DEBUG} --extra-ldflags=-L\"${CURRENT_INSTALLED_DIR}/debug/lib\"") + endif() + + message(STATUS "Building Debug Options: ${OPTIONS_DEBUG}") + set(ENV{LDFLAGS} "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_DEBUG}") + set(ENV{PKG_CONFIG_PATH} "${CURRENT_INSTALLED_DIR}/debug/lib/pkgconfig") + message(STATUS "Building ${PORT} for Debug") + file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg") + set(crsp "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg/cflags.rsp") + string(REGEX REPLACE "-arch [A-Za-z0-9_]+" "" VCPKG_COMBINED_C_FLAGS_DEBUG_SANITIZED "${VCPKG_COMBINED_C_FLAGS_DEBUG}") + file(WRITE "${crsp}" "${VCPKG_COMBINED_C_FLAGS_DEBUG_SANITIZED}") + set(ldrsp "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg/ldflags.rsp") + string(REGEX REPLACE "-arch [A-Za-z0-9_]+" "" VCPKG_COMBINED_SHARED_LINKER_FLAGS_DEBUG_SANITIZED "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_DEBUG}") + file(WRITE "${ldrsp}" "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_DEBUG_SANITIZED}") + set(ENV{CFLAGS} "@${crsp}") + + if(NOT VCPKG_DETECTED_MSVC OR NOT VCPKG_TARGET_ARCHITECTURE MATCHES "^arm") + set(ENV{ASFLAGS} "@${crsp}") + endif() + + set(ENV{LDFLAGS} "@${ldrsp}") + set(ENV{ARFLAGS} "${VCPKG_COMBINED_STATIC_LINKER_FLAGS_DEBUG}") + + set(BUILD_DIR "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg") + set(CONFIGURE_OPTIONS "${OPTIONS} ${OPTIONS_DEBUG}") + set(INST_PREFIX "${CURRENT_PACKAGES_DIR}/debug") + + configure_file("${CMAKE_CURRENT_LIST_DIR}/build.sh.in" "${BUILD_DIR}/build.sh" @ONLY) + + z_vcpkg_setup_pkgconfig_path(CONFIG DEBUG) + + vcpkg_execute_required_process( + COMMAND "${SHELL}" ./build.sh + WORKING_DIRECTORY "${BUILD_DIR}" + LOGNAME "build-${TARGET_TRIPLET}-dbg" + SAVE_LOG_FILES ffbuild/config.log + ) + + z_vcpkg_restore_pkgconfig_path() +endif() + +if(VCPKG_TARGET_IS_WINDOWS) + file(GLOB DEF_FILES "${CURRENT_PACKAGES_DIR}/lib/*.def" "${CURRENT_PACKAGES_DIR}/debug/lib/*.def") + + if(NOT VCPKG_TARGET_IS_MINGW) + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm") + set(LIB_MACHINE_ARG /machine:ARM) + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64") + set(LIB_MACHINE_ARG /machine:ARM64) + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86") + set(LIB_MACHINE_ARG /machine:x86) + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64") + set(LIB_MACHINE_ARG /machine:x64) + else() + message(FATAL_ERROR "Unsupported target architecture") + endif() + + foreach(DEF_FILE ${DEF_FILES}) + get_filename_component(DEF_FILE_DIR "${DEF_FILE}" DIRECTORY) + get_filename_component(DEF_FILE_NAME "${DEF_FILE}" NAME) + string(REGEX REPLACE "-[0-9]*\\.def" "${VCPKG_TARGET_STATIC_LIBRARY_SUFFIX}" OUT_FILE_NAME "${DEF_FILE_NAME}") + file(TO_NATIVE_PATH "${DEF_FILE}" DEF_FILE_NATIVE) + file(TO_NATIVE_PATH "${DEF_FILE_DIR}/${OUT_FILE_NAME}" OUT_FILE_NATIVE) + message(STATUS "Generating ${OUT_FILE_NATIVE}") + vcpkg_execute_required_process( + COMMAND lib.exe "/def:${DEF_FILE_NATIVE}" "/out:${OUT_FILE_NATIVE}" ${LIB_MACHINE_ARG} + WORKING_DIRECTORY "${CURRENT_PACKAGES_DIR}" + LOGNAME "libconvert-${TARGET_TRIPLET}" + ) + endforeach() + endif() + + file(GLOB EXP_FILES "${CURRENT_PACKAGES_DIR}/lib/*.exp" "${CURRENT_PACKAGES_DIR}/debug/lib/*.exp") + file(GLOB LIB_FILES "${CURRENT_PACKAGES_DIR}/bin/*${VCPKG_TARGET_STATIC_LIBRARY_SUFFIX}" "${CURRENT_PACKAGES_DIR}/debug/bin/*${VCPKG_TARGET_STATIC_LIBRARY_SUFFIX}") + + if(VCPKG_TARGET_IS_MINGW) + file(GLOB LIB_FILES_2 "${CURRENT_PACKAGES_DIR}/bin/*.lib" "${CURRENT_PACKAGES_DIR}/debug/bin/*.lib") + endif() + + set(files_to_remove ${EXP_FILES} ${LIB_FILES} ${LIB_FILES_2} ${DEF_FILES}) + + if(files_to_remove) + file(REMOVE ${files_to_remove}) + endif() +endif() + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include" "${CURRENT_PACKAGES_DIR}/debug/share") + +if(VCPKG_LIBRARY_LINKAGE STREQUAL "static") + file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/bin" "${CURRENT_PACKAGES_DIR}/debug/bin") +endif() + +vcpkg_copy_pdbs() + +if(VCPKG_TARGET_IS_WINDOWS) + set(_dirs "/") + + if(NOT VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + list(APPEND _dirs "/debug/") + endif() + + foreach(_debug IN LISTS _dirs) + foreach(PKGCONFIG_MODULE IN LISTS FFMPEG_PKGCONFIG_MODULES) + set(PKGCONFIG_FILE "${CURRENT_PACKAGES_DIR}${_debug}lib/pkgconfig/${PKGCONFIG_MODULE}.pc") + + # remove redundant cygwin style -libpath entries + execute_process( + COMMAND "${MSYS_ROOT}/usr/bin/cygpath.exe" -u "${CURRENT_INSTALLED_DIR}" + OUTPUT_VARIABLE CYG_INSTALLED_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + vcpkg_replace_string("${PKGCONFIG_FILE}" "-libpath:${CYG_INSTALLED_DIR}${_debug}lib/pkgconfig/../../lib " "") + + # transform libdir, includedir, and prefix paths from cygwin style to windows style + file(READ "${PKGCONFIG_FILE}" PKGCONFIG_CONTENT) + + foreach(PATH_NAME prefix libdir includedir) + string(REGEX MATCH "${PATH_NAME}=[^\n]*" PATH_VALUE "${PKGCONFIG_CONTENT}") + string(REPLACE "${PATH_NAME}=" "" PATH_VALUE "${PATH_VALUE}") + + if(NOT PATH_VALUE) + message(FATAL_ERROR "failed to find pkgconfig variable ${PATH_NAME}") + endif() + + execute_process( + COMMAND "${MSYS_ROOT}/usr/bin/cygpath.exe" -w "${PATH_VALUE}" + OUTPUT_VARIABLE FIXED_PATH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + file(TO_CMAKE_PATH "${FIXED_PATH}" FIXED_PATH) + vcpkg_replace_string("${PKGCONFIG_FILE}" "${PATH_NAME}=${PATH_VALUE}" "${PATH_NAME}=${FIXED_PATH}") + endforeach() + + # list libraries with -l flag (so pkgconf knows they are libraries and not just linker flags) + foreach(LIBS_ENTRY Libs Libs.private) + string(REGEX MATCH "${LIBS_ENTRY}: [^\n]*" LIBS_VALUE "${PKGCONFIG_CONTENT}") + + if(NOT LIBS_VALUE) + message(FATAL_ERROR "failed to find pkgconfig entry ${LIBS_ENTRY}") + endif() + + string(REPLACE "${LIBS_ENTRY}: " "" LIBS_VALUE "${LIBS_VALUE}") + + if(LIBS_VALUE) + set(LIBS_VALUE_OLD "${LIBS_VALUE}") + string(REGEX REPLACE "([^ ]+)[.]lib" "-l\\1" LIBS_VALUE "${LIBS_VALUE}") + set(LIBS_VALUE_NEW "${LIBS_VALUE}") + vcpkg_replace_string("${PKGCONFIG_FILE}" "${LIBS_ENTRY}: ${LIBS_VALUE_OLD}" "${LIBS_ENTRY}: ${LIBS_VALUE_NEW}") + endif() + endforeach() + endforeach() + endforeach() +endif() + +vcpkg_fixup_pkgconfig() + +# Handle dependencies +x_vcpkg_pkgconfig_get_modules(PREFIX FFMPEG_PKGCONFIG MODULES ${FFMPEG_PKGCONFIG_MODULES} LIBS) + +function(append_dependencies_from_libs out) + cmake_parse_arguments(PARSE_ARGV 1 "arg" "" "LIBS" "") + string(REGEX REPLACE "[ ]+" ";" contents "${arg_LIBS}") + list(FILTER contents EXCLUDE REGEX "^-F.+") + list(FILTER contents EXCLUDE REGEX "^-framework$") + list(FILTER contents EXCLUDE REGEX "^-L.+") + list(FILTER contents EXCLUDE REGEX "^-libpath:.+") + list(TRANSFORM contents REPLACE "^-Wl,-framework," "-l") + list(FILTER contents EXCLUDE REGEX "^-Wl,.+") + list(TRANSFORM contents REPLACE "^-l" "") + list(FILTER contents EXCLUDE REGEX "^avutil$") + list(FILTER contents EXCLUDE REGEX "^avcodec$") + list(FILTER contents EXCLUDE REGEX "^avdevice$") + list(FILTER contents EXCLUDE REGEX "^avfilter$") + list(FILTER contents EXCLUDE REGEX "^avformat$") + list(FILTER contents EXCLUDE REGEX "^postproc$") + list(FILTER contents EXCLUDE REGEX "^swresample$") + list(FILTER contents EXCLUDE REGEX "^swscale$") + + if(VCPKG_TARGET_IS_WINDOWS) + list(TRANSFORM contents TOLOWER) + endif() + + if(contents) + list(APPEND "${out}" "${contents}") + set("${out}" "${${out}}" PARENT_SCOPE) + endif() +endfunction() + +append_dependencies_from_libs(FFMPEG_DEPENDENCIES_RELEASE LIBS "${FFMPEG_PKGCONFIG_LIBS_RELEASE}") +append_dependencies_from_libs(FFMPEG_DEPENDENCIES_DEBUG LIBS "${FFMPEG_PKGCONFIG_LIBS_DEBUG}") + +# must remove duplicates from the front to respect link order so reverse first +list(REVERSE FFMPEG_DEPENDENCIES_RELEASE) +list(REVERSE FFMPEG_DEPENDENCIES_DEBUG) +list(REMOVE_DUPLICATES FFMPEG_DEPENDENCIES_RELEASE) +list(REMOVE_DUPLICATES FFMPEG_DEPENDENCIES_DEBUG) +list(REVERSE FFMPEG_DEPENDENCIES_RELEASE) +list(REVERSE FFMPEG_DEPENDENCIES_DEBUG) + +message(STATUS "Dependencies (release): ${FFMPEG_DEPENDENCIES_RELEASE}") +message(STATUS "Dependencies (debug): ${FFMPEG_DEPENDENCIES_DEBUG}") + +# Handle version strings +function(extract_regex_from_file out) + cmake_parse_arguments(PARSE_ARGV 1 "arg" "MAJOR" "FILE_WITHOUT_EXTENSION;REGEX" "") + file(READ "${arg_FILE_WITHOUT_EXTENSION}.h" contents) + + if(contents MATCHES "${arg_REGEX}") + if(NOT CMAKE_MATCH_COUNT EQUAL 1) + message(FATAL_ERROR "Could not identify match group in regular expression \"${arg_REGEX}\"") + endif() + else() + if(arg_MAJOR) + file(READ "${arg_FILE_WITHOUT_EXTENSION}_major.h" contents) + + if(contents MATCHES "${arg_REGEX}") + if(NOT CMAKE_MATCH_COUNT EQUAL 1) + message(FATAL_ERROR "Could not identify match group in regular expression \"${arg_REGEX}\"") + endif() + else() + message(WARNING "Could not find line matching \"${arg_REGEX}\" in file \"${arg_FILE_WITHOUT_EXTENSION}_major.h\"") + endif() + else() + message(WARNING "Could not find line matching \"${arg_REGEX}\" in file \"${arg_FILE_WITHOUT_EXTENSION}.h\"") + endif() + endif() + + set("${out}" "${CMAKE_MATCH_1}" PARENT_SCOPE) +endfunction() + +function(extract_version_from_component out) + cmake_parse_arguments(PARSE_ARGV 1 "arg" "" "COMPONENT" "") + string(TOLOWER "${arg_COMPONENT}" component_lower) + string(TOUPPER "${arg_COMPONENT}" component_upper) + extract_regex_from_file(major_version + FILE_WITHOUT_EXTENSION "${SOURCE_PATH}/${component_lower}/version" + MAJOR + REGEX "#define ${component_upper}_VERSION_MAJOR[ ]+([0-9]+)" + ) + extract_regex_from_file(minor_version + FILE_WITHOUT_EXTENSION "${SOURCE_PATH}/${component_lower}/version" + REGEX "#define ${component_upper}_VERSION_MINOR[ ]+([0-9]+)" + ) + extract_regex_from_file(micro_version + FILE_WITHOUT_EXTENSION "${SOURCE_PATH}/${component_lower}/version" + REGEX "#define ${component_upper}_VERSION_MICRO[ ]+([0-9]+)" + ) + set("${out}" "${major_version}.${minor_version}.${micro_version}" PARENT_SCOPE) +endfunction() + +extract_regex_from_file(FFMPEG_VERSION + FILE_WITHOUT_EXTENSION "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/libavutil/ffversion" + REGEX "#define FFMPEG_VERSION[ ]+\"(.+)\"" +) + +extract_version_from_component(LIBAVUTIL_VERSION + COMPONENT libavutil) +extract_version_from_component(LIBAVCODEC_VERSION + COMPONENT libavcodec) +extract_version_from_component(LIBAVDEVICE_VERSION + COMPONENT libavdevice) +extract_version_from_component(LIBAVFILTER_VERSION + COMPONENT libavfilter) +extract_version_from_component(LIBAVFORMAT_VERSION + COMPONENT libavformat) +extract_version_from_component(LIBSWRESAMPLE_VERSION + COMPONENT libswresample) +extract_version_from_component(LIBSWSCALE_VERSION + COMPONENT libswscale) + +# Handle copyright +file(STRINGS "${CURRENT_BUILDTREES_DIR}/build-${TARGET_TRIPLET}-rel-out.log" LICENSE_STRING REGEX "License: .*" LIMIT_COUNT 1) + +if(LICENSE_STRING STREQUAL "License: LGPL version 2.1 or later") + set(LICENSE_FILE "COPYING.LGPLv2.1") +elseif(LICENSE_STRING STREQUAL "License: LGPL version 3 or later") + set(LICENSE_FILE "COPYING.LGPLv3") +elseif(LICENSE_STRING STREQUAL "License: GPL version 2 or later") + set(LICENSE_FILE "COPYING.GPLv2") +elseif(LICENSE_STRING STREQUAL "License: GPL version 3 or later") + set(LICENSE_FILE "COPYING.GPLv3") +elseif(LICENSE_STRING STREQUAL "License: nonfree and unredistributable") + set(LICENSE_FILE "COPYING.NONFREE") + file(WRITE "${SOURCE_PATH}/${LICENSE_FILE}" "${LICENSE_STRING}") +else() + message(FATAL_ERROR "Failed to identify license (${LICENSE_STRING})") +endif() + +configure_file("${CMAKE_CURRENT_LIST_DIR}/vcpkg-cmake-wrapper.cmake" "${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-cmake-wrapper.cmake" @ONLY) +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/${LICENSE_FILE}") diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/vcpkg-cmake-wrapper.cmake b/vendor/rustdesk/res/vcpkg/ffmpeg/vcpkg-cmake-wrapper.cmake new file mode 100644 index 0000000..233d613 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/vcpkg-cmake-wrapper.cmake @@ -0,0 +1,47 @@ +set(FFMPEG_PREV_MODULE_PATH ${CMAKE_MODULE_PATH}) +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}) + +include(SelectLibraryConfigurations) + +cmake_policy(SET CMP0012 NEW) + +set(vcpkg_no_avcodec_target ON) +set(vcpkg_no_avformat_target ON) +set(vcpkg_no_avutil_target ON) +if(TARGET FFmpeg::avcodec) + set(vcpkg_no_avcodec_target OFF) +endif() +if(TARGET FFmpeg::avformat) + set(vcpkg_no_avformat_target OFF) +endif() +if(TARGET FFmpeg::avutil) + set(vcpkg_no_avutil_target OFF) +endif() + +_find_package(${ARGS}) + +if(WIN32) + set(PKG_CONFIG_EXECUTABLE "${CMAKE_CURRENT_LIST_DIR}/../../../@_HOST_TRIPLET@/tools/pkgconf/pkgconf.exe" CACHE STRING "" FORCE) +endif() + +set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH ON) # Required for CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 3.1 which otherwise ignores CMAKE_PREFIX_PATH + +if(@WITH_MFX@) + find_package(PkgConfig ) + pkg_check_modules(libmfx IMPORTED_TARGET libmfx) + list(APPEND FFMPEG_LIBRARIES PkgConfig::libmfx) + if(vcpkg_no_avcodec_target AND TARGET FFmpeg::avcodec) + target_link_libraries(FFmpeg::avcodec INTERFACE PkgConfig::libmfx) + endif() + if(vcpkg_no_avutil_target AND TARGET FFmpeg::avutil) + target_link_libraries(FFmpeg::avutil INTERFACE PkgConfig::libmfx) + endif() +endif() + +set(FFMPEG_LIBRARY ${FFMPEG_LIBRARIES}) + +set(CMAKE_MODULE_PATH ${FFMPEG_PREV_MODULE_PATH}) + +unset(vcpkg_no_avformat_target) +unset(vcpkg_no_avcodec_target) +unset(vcpkg_no_avutil_target) diff --git a/vendor/rustdesk/res/vcpkg/ffmpeg/vcpkg.json b/vendor/rustdesk/res/vcpkg/ffmpeg/vcpkg.json new file mode 100644 index 0000000..0346bb5 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/ffmpeg/vcpkg.json @@ -0,0 +1,44 @@ +{ + "name": "ffmpeg", + "version": "7.1", + "port-version": 1, + "description": [ + "a library to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created.", + "FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure FATE across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations." + ], + "homepage": "https://ffmpeg.org", + "license": null, + "dependencies": [ + { + "name": "vcpkg-cmake-get-vars", + "host": true + }, + { + "name": "vcpkg-pkgconfig-get-modules", + "host": true + } + ], + "default-features": [ + ], + "features": { + "amf": { + "description": "AMD AMF codec support", + "dependencies": [ + "amd-amf" + ] + }, + "nvcodec": { + "description": "Nvidia video decoding/encoding acceleration", + "supports": "linux | (!osx & !uwp & !(arm64 & windows))", + "dependencies": [ + "ffnvcodec" + ] + }, + "qsv": { + "description": "Intel QSV Codec", + "dependencies": [ + "mfx-dispatch" + ] + } + } +} \ No newline at end of file diff --git a/vendor/rustdesk/res/vcpkg/libvpx/0003-add-uwp-v142-and-v143-support.patch b/vendor/rustdesk/res/vcpkg/libvpx/0003-add-uwp-v142-and-v143-support.patch new file mode 100644 index 0000000..c9a01b7 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libvpx/0003-add-uwp-v142-and-v143-support.patch @@ -0,0 +1,168 @@ +diff --git a/build/make/configure.sh b/build/make/configure.sh +index cc5bf6ce4..9380e87a7 100644 +--- a/build/make/configure.sh ++++ b/build/make/configure.sh +@@ -1092,7 +1092,7 @@ EOF + # A number of ARM-based Windows platforms are constrained by their + # respective SDKs' limitations. Fortunately, these are all 32-bit ABIs + # and so can be selected as 'win32'. +- if [ ${tgt_os} = "win32" ]; then ++ if [ ${tgt_os} = "win32" ] || [ ${tgt_isa} = "armv7" ]; then + asm_conversion_cmd="${source_path_mk}/build/make/ads2armasm_ms.pl" + AS_SFX=.S + msvs_arch_dir=arm-msvs +@@ -1366,6 +1366,9 @@ EOF + android) + soft_enable realtime_only + ;; ++ uwp) ++ enabled gcc && add_cflags -fno-common ++ ;; + win*) + enabled gcc && add_cflags -fno-common + ;; +@@ -1484,14 +1487,26 @@ EOF + fi + AS_SFX=.asm + case ${tgt_os} in ++ uwp) ++ if [ ${tgt_isa} = "x86" ] || [ ${tgt_isa} = "armv7" ]; then ++ add_asflags -f win32 ++ else ++ add_asflags -f win64 ++ fi ++ enabled debug && [ "${AS}" = yasm ] && add_asflags -g cv8 ++ enabled debug && [ "${AS}" = nasm ] && add_asflags -gcv8 ++ EXE_SFX=.exe ++ ;; + win32) + add_asflags -f win32 +- enabled debug && add_asflags -g cv8 ++ enabled debug && [ "${AS}" = yasm ] && add_asflags -g cv8 ++ enabled debug && [ "${AS}" = nasm ] && add_asflags -gcv8 + EXE_SFX=.exe + ;; + win64) + add_asflags -f win64 +- enabled debug && add_asflags -g cv8 ++ enabled debug && [ "${AS}" = yasm ] && add_asflags -g cv8 ++ enabled debug && [ "${AS}" = nasm ] && add_asflags -gcv8 + EXE_SFX=.exe + ;; + linux*|solaris*|android*) +@@ -1622,6 +1637,8 @@ EOF + # Almost every platform uses pthreads. + if enabled multithread; then + case ${toolchain} in ++ *-uwp-vs*) ++ ;; + *-win*-vs*) + ;; + *-android-gcc) +diff --git a/build/make/gen_msvs_vcxproj.sh b/build/make/gen_msvs_vcxproj.sh +index 1e1db05bb..543eb37b2 100755 +--- a/build/make/gen_msvs_vcxproj.sh ++++ b/build/make/gen_msvs_vcxproj.sh +@@ -310,7 +310,22 @@ generate_vcxproj() { + tag_content ProjectGuid "{${guid}}" + tag_content RootNamespace ${name} + tag_content Keyword ManagedCProj +- if [ $vs_ver -ge 12 ] && [ "${platforms[0]}" = "ARM" ]; then ++ if [ $vs_ver -ge 16 ]; then ++ if [[ $target =~ [^-]*-uwp-.* ]]; then ++ # Universal Windows Applications ++ tag_content AppContainerApplication true ++ tag_content ApplicationType "Windows Store" ++ tag_content ApplicationTypeRevision 10.0 ++ fi ++ if [[ $target =~ [^-]*-uwp-.* ]] || [ "${platforms[0]}" = "ARM" ] || [ "${platforms[0]}" = "ARM64" ]; then ++ # Default to the latest Windows 10 SDK ++ tag_content WindowsTargetPlatformVersion 10.0 ++ else ++ # Minimum supported version of Windows for the desktop ++ tag_content WindowsTargetPlatformVersion 8.1 ++ fi ++ tag_content MinimumVisualStudioVersion 16.0 ++ elif [ $vs_ver -ge 12 ] && [ "${platforms[0]}" = "ARM" ]; then + tag_content AppContainerApplication true + # The application type can be one of "Windows Store", + # "Windows Phone" or "Windows Phone Silverlight". The +@@ -412,7 +427,7 @@ generate_vcxproj() { + Condition="'\$(Configuration)|\$(Platform)'=='$config|$plat'" + if [ "$name" == "vpx" ]; then + hostplat=$plat +- if [ "$hostplat" == "ARM" ]; then ++ if [ "$hostplat" == "ARM" ] && [ $vs_ver -le 15 ]; then + hostplat=Win32 + fi + fi +diff --git a/configure b/configure +index 457bd6b38..fa4bce71b 100755 +--- a/configure ++++ b/configure +@@ -105,6 +105,8 @@ all_platforms="${all_platforms} arm64-darwin22-gcc" + all_platforms="${all_platforms} arm64-darwin23-gcc" + all_platforms="${all_platforms} arm64-darwin24-gcc" + all_platforms="${all_platforms} arm64-linux-gcc" ++all_platforms="${all_platforms} arm64-uwp-vs16" ++all_platforms="${all_platforms} arm64-uwp-vs17" + all_platforms="${all_platforms} arm64-win64-gcc" + all_platforms="${all_platforms} arm64-win64-vs15" + all_platforms="${all_platforms} arm64-win64-vs16" +@@ -116,6 +118,8 @@ all_platforms="${all_platforms} armv7-darwin-gcc" #neon Cortex-A8 + all_platforms="${all_platforms} armv7-linux-rvct" #neon Cortex-A8 + all_platforms="${all_platforms} armv7-linux-gcc" #neon Cortex-A8 + all_platforms="${all_platforms} armv7-none-rvct" #neon Cortex-A8 ++all_platforms="${all_platforms} armv7-uwp-vs16" ++all_platforms="${all_platforms} armv7-uwp-vs17" + all_platforms="${all_platforms} armv7-win32-gcc" + all_platforms="${all_platforms} armv7-win32-vs14" + all_platforms="${all_platforms} armv7-win32-vs15" +@@ -147,6 +151,8 @@ all_platforms="${all_platforms} x86-linux-gcc" + all_platforms="${all_platforms} x86-linux-icc" + all_platforms="${all_platforms} x86-os2-gcc" + all_platforms="${all_platforms} x86-solaris-gcc" ++all_platforms="${all_platforms} x86-uwp-vs16" ++all_platforms="${all_platforms} x86-uwp-vs17" + all_platforms="${all_platforms} x86-win32-gcc" + all_platforms="${all_platforms} x86-win32-vs14" + all_platforms="${all_platforms} x86-win32-vs15" +@@ -173,6 +179,8 @@ all_platforms="${all_platforms} x86_64-iphonesimulator-gcc" + all_platforms="${all_platforms} x86_64-linux-gcc" + all_platforms="${all_platforms} x86_64-linux-icc" + all_platforms="${all_platforms} x86_64-solaris-gcc" ++all_platforms="${all_platforms} x86_64-uwp-vs16" ++all_platforms="${all_platforms} x86_64-uwp-vs17" + all_platforms="${all_platforms} x86_64-win64-gcc" + all_platforms="${all_platforms} x86_64-win64-vs14" + all_platforms="${all_platforms} x86_64-win64-vs15" +@@ -507,11 +515,10 @@ process_targets() { + ! enabled multithread && DIST_DIR="${DIST_DIR}-nomt" + ! enabled install_docs && DIST_DIR="${DIST_DIR}-nodocs" + DIST_DIR="${DIST_DIR}-${tgt_isa}-${tgt_os}" +- case "${tgt_os}" in +- win*) enabled static_msvcrt && DIST_DIR="${DIST_DIR}mt" || DIST_DIR="${DIST_DIR}md" +- DIST_DIR="${DIST_DIR}-${tgt_cc}" +- ;; +- esac ++ if [[ ${tgt_os} =~ win.* ]] || [ "${tgt_os}" = "uwp" ]; then ++ enabled static_msvcrt && DIST_DIR="${DIST_DIR}mt" || DIST_DIR="${DIST_DIR}md" ++ DIST_DIR="${DIST_DIR}-${tgt_cc}" ++ fi + if [ -f "${source_path}/build/make/version.sh" ]; then + ver=`"$source_path/build/make/version.sh" --bare "$source_path"` + DIST_DIR="${DIST_DIR}-${ver}" +@@ -600,6 +607,10 @@ process_detect() { + + # Specialize windows and POSIX environments. + case $toolchain in ++ *-uwp-*) ++ # Don't check for any headers in UWP builds. ++ false ++ ;; + *-win*-*) + # Don't check for any headers in Windows builds. + false +-- +2.49.0 + diff --git a/vendor/rustdesk/res/vcpkg/libvpx/0004-remove-library-suffixes.patch b/vendor/rustdesk/res/vcpkg/libvpx/0004-remove-library-suffixes.patch new file mode 100644 index 0000000..e7f827d --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libvpx/0004-remove-library-suffixes.patch @@ -0,0 +1,13 @@ +diff --git a/build/make/gen_msvs_vcxproj.sh b/build/make/gen_msvs_vcxproj.sh +index 916851662..e60405bc9 100755 +--- a/build/make/gen_msvs_vcxproj.sh ++++ b/build/make/gen_msvs_vcxproj.sh +@@ -394,7 +394,7 @@ generate_vcxproj() { + else + config_suffix="" + fi +- tag_content TargetName "${name}${lib_sfx}${config_suffix}" ++ tag_content TargetName "${name}" + fi + close_tag PropertyGroup + done diff --git a/vendor/rustdesk/res/vcpkg/libvpx/portfile.cmake b/vendor/rustdesk/res/vcpkg/libvpx/portfile.cmake new file mode 100644 index 0000000..fbc60b9 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libvpx/portfile.cmake @@ -0,0 +1,316 @@ +vcpkg_check_linkage(ONLY_STATIC_LIBRARY) + +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO webmproject/libvpx + REF "v${VERSION}" + SHA512 824fe8719e4115ec359ae0642f5e1cea051d458f09eb8c24d60858cf082f66e411215e23228173ab154044bafbdfbb2d93b589bb726f55b233939b91f928aae0 + HEAD_REF master + PATCHES + 0003-add-uwp-v142-and-v143-support.patch + 0004-remove-library-suffixes.patch +) + +if(CMAKE_HOST_WIN32) + vcpkg_acquire_msys(MSYS_ROOT PACKAGES make perl) + set(ENV{PATH} "${MSYS_ROOT}/usr/bin;$ENV{PATH}") +else() + vcpkg_find_acquire_program(PERL) + get_filename_component(PERL_EXE_PATH ${PERL} DIRECTORY) + set(ENV{PATH} "${MSYS_ROOT}/usr/bin:$ENV{PATH}:${PERL_EXE_PATH}") +endif() + +find_program(BASH NAME bash HINTS ${MSYS_ROOT}/usr/bin REQUIRED NO_CACHE) + +vcpkg_find_acquire_program(NASM) +get_filename_component(NASM_EXE_PATH ${NASM} DIRECTORY) +vcpkg_add_to_path(${NASM_EXE_PATH}) + +if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) + + file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-tmp") + + if(VCPKG_CRT_LINKAGE STREQUAL static) + set(LIBVPX_CRT_LINKAGE --enable-static-msvcrt) + set(LIBVPX_CRT_SUFFIX mt) + else() + set(LIBVPX_CRT_SUFFIX md) + endif() + + if(VCPKG_CMAKE_SYSTEM_NAME STREQUAL WindowsStore AND (VCPKG_PLATFORM_TOOLSET STREQUAL v142 OR VCPKG_PLATFORM_TOOLSET STREQUAL v143)) + set(LIBVPX_TARGET_OS "uwp") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL x86 OR VCPKG_TARGET_ARCHITECTURE STREQUAL arm) + set(LIBVPX_TARGET_OS "win32") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL x64 OR VCPKG_TARGET_ARCHITECTURE STREQUAL arm64) + set(LIBVPX_TARGET_OS "win64") + endif() + + if(VCPKG_TARGET_ARCHITECTURE STREQUAL x86) + set(LIBVPX_TARGET_ARCH "x86-${LIBVPX_TARGET_OS}") + set(LIBVPX_ARCH_DIR "Win32") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL x64) + set(LIBVPX_TARGET_ARCH "x86_64-${LIBVPX_TARGET_OS}") + set(LIBVPX_ARCH_DIR "x64") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL arm64) + set(LIBVPX_TARGET_ARCH "arm64-${LIBVPX_TARGET_OS}") + set(LIBVPX_ARCH_DIR "ARM64") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL arm) + set(LIBVPX_TARGET_ARCH "armv7-${LIBVPX_TARGET_OS}") + set(LIBVPX_ARCH_DIR "ARM") + endif() + + if(VCPKG_PLATFORM_TOOLSET STREQUAL v143) + set(LIBVPX_TARGET_VS "vs17") + elseif(VCPKG_PLATFORM_TOOLSET STREQUAL v142) + set(LIBVPX_TARGET_VS "vs16") + else() + set(LIBVPX_TARGET_VS "vs15") + endif() + + set(OPTIONS "--disable-examples --disable-tools --disable-docs --enable-pic") + + if("realtime" IN_LIST FEATURES) + set(OPTIONS "${OPTIONS} --enable-realtime-only") + endif() + + if("highbitdepth" IN_LIST FEATURES) + set(OPTIONS "${OPTIONS} --enable-vp9-highbitdepth") + endif() + + message(STATUS "Generating makefile") + file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-tmp") + vcpkg_execute_required_process( + COMMAND + ${BASH} --noprofile --norc + "${SOURCE_PATH}/configure" + --target=${LIBVPX_TARGET_ARCH}-${LIBVPX_TARGET_VS} + ${LIBVPX_CRT_LINKAGE} + ${OPTIONS} + --as=nasm + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-tmp" + LOGNAME configure-${TARGET_TRIPLET}) + + message(STATUS "Generating MSBuild projects") + vcpkg_execute_required_process( + COMMAND + ${BASH} --noprofile --norc -c "make dist" + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-tmp" + LOGNAME generate-${TARGET_TRIPLET}) + + vcpkg_msbuild_install( + SOURCE_PATH "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-tmp" + PROJECT_SUBPATH vpx.vcxproj + ) + + if (VCPKG_TARGET_ARCHITECTURE STREQUAL arm64) + set(LIBVPX_INCLUDE_DIR "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/vpx-vp8-vp9-nopost-nodocs-${LIBVPX_TARGET_ARCH}${LIBVPX_CRT_SUFFIX}-${LIBVPX_TARGET_VS}-v${VERSION}/include/vpx") + elseif (VCPKG_TARGET_ARCHITECTURE STREQUAL arm) + set(LIBVPX_INCLUDE_DIR "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/vpx-vp8-vp9-nopost-nomt-nodocs-${LIBVPX_TARGET_ARCH}${LIBVPX_CRT_SUFFIX}-${LIBVPX_TARGET_VS}-v${VERSION}/include/vpx") + else() + set(LIBVPX_INCLUDE_DIR "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/vpx-vp8-vp9-nodocs-${LIBVPX_TARGET_ARCH}${LIBVPX_CRT_SUFFIX}-${LIBVPX_TARGET_VS}-v${VERSION}/include/vpx") + endif() + file( + INSTALL + "${LIBVPX_INCLUDE_DIR}" + DESTINATION + "${CURRENT_PACKAGES_DIR}/include" + RENAME + "vpx") + if (NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") + set(LIBVPX_PREFIX "${CURRENT_INSTALLED_DIR}") + configure_file("${CMAKE_CURRENT_LIST_DIR}/vpx.pc.in" "${CURRENT_PACKAGES_DIR}/lib/pkgconfig/vpx.pc" @ONLY) + endif() + + if (NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + set(LIBVPX_PREFIX "${CURRENT_INSTALLED_DIR}/debug") + configure_file("${CMAKE_CURRENT_LIST_DIR}/vpx.pc.in" "${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/vpx.pc" @ONLY) + endif() + +else() + + set(OPTIONS "--disable-examples --disable-tools --disable-docs --disable-unit-tests --enable-pic") + + set(OPTIONS_DEBUG "--enable-debug-libs --enable-debug --prefix=${CURRENT_PACKAGES_DIR}/debug") + set(OPTIONS_RELEASE "--prefix=${CURRENT_PACKAGES_DIR}") + set(AS_NASM "--as=nasm") + + if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic") + set(OPTIONS "${OPTIONS} --disable-static --enable-shared") + else() + set(OPTIONS "${OPTIONS} --enable-static --disable-shared") + endif() + + if("realtime" IN_LIST FEATURES) + set(OPTIONS "${OPTIONS} --enable-realtime-only") + endif() + + if("highbitdepth" IN_LIST FEATURES) + set(OPTIONS "${OPTIONS} --enable-vp9-highbitdepth") + endif() + + if(VCPKG_TARGET_ARCHITECTURE STREQUAL x86) + set(LIBVPX_TARGET_ARCH "x86") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL x64) + set(LIBVPX_TARGET_ARCH "x86_64") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL arm) + set(LIBVPX_TARGET_ARCH "armv7") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL arm64) + set(LIBVPX_TARGET_ARCH "arm64") + else() + message(FATAL_ERROR "libvpx does not support architecture ${VCPKG_TARGET_ARCHITECTURE}") + endif() + + vcpkg_cmake_get_vars(cmake_vars_file) + include("${cmake_vars_file}") + + # Set environment variables for configure + if(VCPKG_DETECTED_CMAKE_C_COMPILER MATCHES "([^\/]*-)gcc$") + message(STATUS "Cross-building for ${TARGET_TRIPLET} with ${CMAKE_MATCH_1}") + set(ENV{CROSS} ${CMAKE_MATCH_1}) + unset(AS_NASM) + else() + set(ENV{CC} ${VCPKG_DETECTED_CMAKE_C_COMPILER}) + set(ENV{CXX} ${VCPKG_DETECTED_CMAKE_CXX_COMPILER}) + set(ENV{AR} ${VCPKG_DETECTED_CMAKE_AR}) + set(ENV{LD} ${VCPKG_DETECTED_CMAKE_LINKER}) + set(ENV{RANLIB} ${VCPKG_DETECTED_CMAKE_RANLIB}) + set(ENV{STRIP} ${VCPKG_DETECTED_CMAKE_STRIP}) + endif() + + if(VCPKG_TARGET_IS_MINGW) + if(LIBVPX_TARGET_ARCH STREQUAL "x86") + set(LIBVPX_TARGET "x86-win32-gcc") + else() + set(LIBVPX_TARGET "x86_64-win64-gcc") + endif() + elseif(VCPKG_TARGET_IS_LINUX) + set(LIBVPX_TARGET "${LIBVPX_TARGET_ARCH}-linux-gcc") + elseif(VCPKG_TARGET_IS_ANDROID) + set(LIBVPX_TARGET "generic-gnu") + # Settings + if(VCPKG_TARGET_ARCHITECTURE STREQUAL x86) + set(OPTIONS "${OPTIONS} --disable-sse4_1 --disable-avx --disable-avx2 --disable-avx512") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL x64) + set(OPTIONS "${OPTIONS} --disable-avx --disable-avx2 --disable-avx512") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL arm) + set(OPTIONS "${OPTIONS} --enable-thumb --disable-neon") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL arm64) + set(OPTIONS "${OPTIONS} --enable-thumb") + endif() + # Set environment variables for configure + set(ENV{AS} ${VCPKG_DETECTED_CMAKE_C_COMPILER}) + set(ENV{LDFLAGS} "${LDFLAGS} --target=${VCPKG_DETECTED_CMAKE_C_COMPILER_TARGET}") + # Set clang target + set(OPTIONS "${OPTIONS} --extra-cflags=--target=${VCPKG_DETECTED_CMAKE_C_COMPILER_TARGET} --extra-cxxflags=--target=${VCPKG_DETECTED_CMAKE_CXX_COMPILER_TARGET}") + # Unset nasm and let AS do its job + unset(AS_NASM) + elseif(VCPKG_TARGET_IS_OSX) + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64") + set(LIBVPX_TARGET "arm64-darwin20-gcc") + if(DEFINED VCPKG_OSX_DEPLOYMENT_TARGET) + set(MAC_OSX_MIN_VERSION_CFLAGS --extra-cflags=-mmacosx-version-min=${VCPKG_OSX_DEPLOYMENT_TARGET} --extra-cxxflags=-mmacosx-version-min=${VCPKG_OSX_DEPLOYMENT_TARGET}) + endif() + else() + set(LIBVPX_TARGET "${LIBVPX_TARGET_ARCH}-darwin17-gcc") # enable latest CPU instructions for best performance and less CPU usage on MacOS + endif() + elseif(VCPKG_TARGET_IS_IOS) + if(VCPKG_TARGET_ARCHITECTURE STREQUAL arm) + set(LIBVPX_TARGET "armv7-darwin-gcc") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL arm64) + set(LIBVPX_TARGET "arm64-darwin-gcc") + else() + message(FATAL_ERROR "libvpx does not support architecture ${VCPKG_TARGET_ARCHITECTURE} on iOS") + endif() + else() + set(LIBVPX_TARGET "generic-gnu") # use default target + endif() + + if (VCPKG_HOST_IS_OPENBSD OR VCPKG_HOST_IS_FREEBSD) + set(MAKE_BINARY "gmake") + else() + set(MAKE_BINARY "make") + endif() + + message(STATUS "Build info. Target: ${LIBVPX_TARGET}; Options: ${OPTIONS}") + + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") + message(STATUS "Configuring libvpx for Release") + file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel") + vcpkg_execute_required_process( + COMMAND + ${BASH} --noprofile --norc + "${SOURCE_PATH}/configure" + --target=${LIBVPX_TARGET} + ${OPTIONS} + ${OPTIONS_RELEASE} + ${MAC_OSX_MIN_VERSION_CFLAGS} + ${AS_NASM} + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel" + LOGNAME configure-${TARGET_TRIPLET}-rel) + + message(STATUS "Building libvpx for Release") + vcpkg_execute_required_process( + COMMAND + ${BASH} --noprofile --norc -c "${MAKE_BINARY} -j${VCPKG_CONCURRENCY}" + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel" + LOGNAME build-${TARGET_TRIPLET}-rel + ) + + message(STATUS "Installing libvpx for Release") + vcpkg_execute_required_process( + COMMAND + ${BASH} --noprofile --norc -c "${MAKE_BINARY} install" + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel" + LOGNAME install-${TARGET_TRIPLET}-rel + ) + endif() + + # --- --- --- + + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + message(STATUS "Configuring libvpx for Debug") + file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg") + vcpkg_execute_required_process( + COMMAND + ${BASH} --noprofile --norc + "${SOURCE_PATH}/configure" + --target=${LIBVPX_TARGET} + ${OPTIONS} + ${OPTIONS_DEBUG} + ${MAC_OSX_MIN_VERSION_CFLAGS} + ${AS_NASM} + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg" + LOGNAME configure-${TARGET_TRIPLET}-dbg) + + message(STATUS "Building libvpx for Debug") + vcpkg_execute_required_process( + COMMAND + ${BASH} --noprofile --norc -c "${MAKE_BINARY} -j${VCPKG_CONCURRENCY}" + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg" + LOGNAME build-${TARGET_TRIPLET}-dbg + ) + + message(STATUS "Installing libvpx for Debug") + vcpkg_execute_required_process( + COMMAND + ${BASH} --noprofile --norc -c "${MAKE_BINARY} install" + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg" + LOGNAME install-${TARGET_TRIPLET}-dbg + ) + + file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") + file(REMOVE "${CURRENT_PACKAGES_DIR}/debug/lib/libvpx_g.a") + endif() +endif() + +vcpkg_fixup_pkgconfig() + +if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + set(LIBVPX_CONFIG_DEBUG ON) +else() + set(LIBVPX_CONFIG_DEBUG OFF) +endif() + +configure_file("${CMAKE_CURRENT_LIST_DIR}/unofficial-libvpx-config.cmake.in" "${CURRENT_PACKAGES_DIR}/share/unofficial-libvpx/unofficial-libvpx-config.cmake" @ONLY) + +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") diff --git a/vendor/rustdesk/res/vcpkg/libvpx/unofficial-libvpx-config.cmake.in b/vendor/rustdesk/res/vcpkg/libvpx/unofficial-libvpx-config.cmake.in new file mode 100644 index 0000000..d3844d3 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libvpx/unofficial-libvpx-config.cmake.in @@ -0,0 +1,49 @@ +if(NOT TARGET unofficial::libvpx::libvpx) + # Compute the installation prefix relative to this file. + get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH) + get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) + get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) + + # Add library target (note: vpx always has a static build in vcpkg). + add_library(unofficial::libvpx::libvpx STATIC IMPORTED) + + # Add interface include directories and link interface languages (applies to all configurations). + set_target_properties(unofficial::libvpx::libvpx PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include" + IMPORTED_LINK_INTERFACE_LANGUAGES "C" + ) + list(APPEND _IMPORT_CHECK_FILES "${_IMPORT_PREFIX}/include/vpx/vpx_codec.h") + + # Add release configuration properties. + find_library(_LIBFILE_RELEASE NAMES vpx PATHS "${_IMPORT_PREFIX}/lib/" NO_DEFAULT_PATH) + set_property(TARGET unofficial::libvpx::libvpx + APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE) + set_target_properties(unofficial::libvpx::libvpx PROPERTIES + IMPORTED_LOCATION_RELEASE ${_LIBFILE_RELEASE}) + list(APPEND _IMPORT_CHECK_FILES ${_LIBFILE_RELEASE}) + unset(_LIBFILE_RELEASE CACHE) + + # Add debug configuration properties. + if(@LIBVPX_CONFIG_DEBUG@) + find_library(_LIBFILE_DEBUG NAMES vpx PATHS "${_IMPORT_PREFIX}/debug/lib/" NO_DEFAULT_PATH) + set_property(TARGET unofficial::libvpx::libvpx + APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG) + set_target_properties(unofficial::libvpx::libvpx PROPERTIES + IMPORTED_LOCATION_DEBUG ${_LIBFILE_DEBUG}) + list(APPEND _IMPORT_CHECK_FILES ${_LIBFILE_DEBUG}) + unset(_LIBFILE_DEBUG CACHE) + endif() + + # Check header and library files are present. + foreach(file ${_IMPORT_CHECK_FILES} ) + if(NOT EXISTS "${file}" ) + message(FATAL_ERROR "unofficial::libvpx::libvpx references the file + \"${file}\" +but this file does not exist. Possible reasons include: +* The file was deleted, renamed, or moved to another location. +* An install or uninstall procedure did not complete successfully. +") + endif() + endforeach() + unset(_IMPORT_CHECK_FILES) +endif() diff --git a/vendor/rustdesk/res/vcpkg/libvpx/vcpkg.json b/vendor/rustdesk/res/vcpkg/libvpx/vcpkg.json new file mode 100644 index 0000000..ac9775e --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libvpx/vcpkg.json @@ -0,0 +1,26 @@ +{ + "name": "libvpx", + "version": "1.15.2", + "description": "The reference software implementation for the video coding formats VP8 and VP9.", + "homepage": "https://github.com/webmproject/libvpx", + "license": "BSD-3-Clause", + "dependencies": [ + { + "name": "vcpkg-cmake-get-vars", + "host": true + }, + { + "name": "vcpkg-msbuild", + "host": true, + "platform": "windows & !mingw" + } + ], + "features": { + "highbitdepth": { + "description": "use VP9 high bit depth (10/12) profiles" + }, + "realtime": { + "description": "enable this option while building for real-time encoding" + } + } +} diff --git a/vendor/rustdesk/res/vcpkg/libvpx/vpx.pc.in b/vendor/rustdesk/res/vcpkg/libvpx/vpx.pc.in new file mode 100644 index 0000000..6df64d4 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libvpx/vpx.pc.in @@ -0,0 +1,12 @@ +prefix=@LIBVPX_PREFIX@ +exec_prefix=${prefix} +libdir=${prefix}/lib +includedir=${prefix}/include + +Name: vpx +Description: WebM Project VPx codec implementation +Version: @VERSION@ +Requires: +Conflicts: +Libs: -L"${libdir}" -lvpx +Cflags: -I"${includedir}" diff --git a/vendor/rustdesk/res/vcpkg/libyuv/fix-cmakelists.patch b/vendor/rustdesk/res/vcpkg/libyuv/fix-cmakelists.patch new file mode 100644 index 0000000..4352057 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libyuv/fix-cmakelists.patch @@ -0,0 +1,80 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index bc641685..42e72a39 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -1,19 +1,22 @@ ++CMAKE_MINIMUM_REQUIRED( VERSION 3.12 ) ++ + # CMakeLists for libyuv + # Originally created for "roxlu build system" to compile libyuv on windows + # Run with -DTEST=ON to build unit tests + + PROJECT ( YUV C CXX ) # "C" is required even for C++ projects +-CMAKE_MINIMUM_REQUIRED( VERSION 2.8.12 ) + OPTION( TEST "Built unit tests" OFF ) + ++SET( CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE ON ) ++ + SET ( ly_base_dir ${PROJECT_SOURCE_DIR} ) + SET ( ly_src_dir ${ly_base_dir}/source ) + SET ( ly_inc_dir ${ly_base_dir}/include ) + SET ( ly_tst_dir ${ly_base_dir}/unit_test ) + SET ( ly_lib_name yuv ) + SET ( ly_lib_static ${ly_lib_name} ) +-SET ( ly_lib_shared ${ly_lib_name}_shared ) + ++FILE ( GLOB_RECURSE ly_include_files ${ly_inc_dir}/libyuv/*.h ) + FILE ( GLOB_RECURSE ly_source_files ${ly_src_dir}/*.cc ) + LIST ( SORT ly_source_files ) + +@@ -28,27 +31,20 @@ endif() + + # this creates the static library (.a) + ADD_LIBRARY ( ${ly_lib_static} STATIC ${ly_source_files} ) +- +-# this creates the shared library (.so) +-ADD_LIBRARY ( ${ly_lib_shared} SHARED ${ly_source_files} ) +-SET_TARGET_PROPERTIES ( ${ly_lib_shared} PROPERTIES OUTPUT_NAME "${ly_lib_name}" ) +-SET_TARGET_PROPERTIES ( ${ly_lib_shared} PROPERTIES PREFIX "lib" ) +-if(WIN32) +- SET_TARGET_PROPERTIES ( ${ly_lib_shared} PROPERTIES IMPORT_PREFIX "lib" ) +-endif() ++SET_TARGET_PROPERTIES ( ${ly_lib_static} PROPERTIES PUBLIC_HEADER include/libyuv.h ) + + # this creates the conversion tool + ADD_EXECUTABLE ( yuvconvert ${ly_base_dir}/util/yuvconvert.cc ) +-TARGET_LINK_LIBRARIES ( yuvconvert ${ly_lib_static} ) ++TARGET_LINK_LIBRARIES ( yuvconvert ${ly_lib_static} ) + + # this creates the yuvconstants tool +-ADD_EXECUTABLE ( yuvconstants ${ly_base_dir}/util/yuvconstants.c ) +-TARGET_LINK_LIBRARIES ( yuvconstants ${ly_lib_static} ) ++ADD_EXECUTABLE ( yuvconstants ${ly_base_dir}/util/yuvconstants.c ) ++TARGET_LINK_LIBRARIES ( yuvconstants ${ly_lib_static} ) + + find_package ( JPEG ) + if (JPEG_FOUND) +- include_directories( ${JPEG_INCLUDE_DIR} ) +- target_link_libraries( ${ly_lib_shared} ${JPEG_LIBRARY} ) ++ include_directories( ${JPEG_INCLUDE_DIR}) ++ target_link_libraries(${ly_lib_static} PUBLIC ${JPEG_LIBRARY}) + add_definitions( -DHAVE_JPEG ) + endif() + +@@ -89,12 +85,11 @@ if(TEST) + endif() + endif() + +- + # install the conversion tool, .so, .a, and all the header files +-INSTALL ( PROGRAMS ${CMAKE_BINARY_DIR}/yuvconvert DESTINATION bin ) +-INSTALL ( TARGETS ${ly_lib_static} DESTINATION lib ) +-INSTALL ( TARGETS ${ly_lib_shared} LIBRARY DESTINATION lib RUNTIME DESTINATION bin ) +-INSTALL ( DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION include ) ++INSTALL ( TARGETS yuvconvert DESTINATION tools ) ++INSTALL ( FILES ${ly_include_files} DESTINATION include/libyuv ) ++INSTALL ( TARGETS ${ly_lib_static} EXPORT libyuv-targets DESTINATION lib INCLUDES DESTINATION include PUBLIC_HEADER DESTINATION include ) ++INSTALL( EXPORT libyuv-targets DESTINATION share/cmake/libyuv/ EXPORT_LINK_INTERFACE_LIBRARIES ) + + # create the .deb and .rpm packages using cpack + INCLUDE ( CM_linux_packages.cmake ) diff --git a/vendor/rustdesk/res/vcpkg/libyuv/libyuv-config.cmake b/vendor/rustdesk/res/vcpkg/libyuv/libyuv-config.cmake new file mode 100644 index 0000000..1e68c15 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libyuv/libyuv-config.cmake @@ -0,0 +1,5 @@ +include(CMakeFindDependencyMacro) +find_dependency(JPEG) + +set(libyuv_INCLUDE_DIRS "${CMAKE_CURRENT_LIST_DIR}/../../include") +include("${CMAKE_CURRENT_LIST_DIR}/libyuv-targets.cmake") diff --git a/vendor/rustdesk/res/vcpkg/libyuv/portfile.cmake b/vendor/rustdesk/res/vcpkg/libyuv/portfile.cmake new file mode 100644 index 0000000..7ff9b07 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libyuv/portfile.cmake @@ -0,0 +1,81 @@ +vcpkg_check_linkage(ONLY_STATIC_LIBRARY) + +vcpkg_from_git( + OUT_SOURCE_PATH SOURCE_PATH + URL https://chromium.googlesource.com/libyuv/libyuv + REF 0faf8dd0e004520a61a603a4d2996d5ecc80dc3f + # Check https://chromium.googlesource.com/libyuv/libyuv/+/refs/heads/main/include/libyuv/version.h for a version! + PATCHES + fix-cmakelists.patch +) + +vcpkg_cmake_get_vars(cmake_vars_file) +include("${cmake_vars_file}") +if (VCPKG_DETECTED_CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND NOT VCPKG_TARGET_IS_UWP) + # Most of libyuv accelerated features need to be compiled by clang/gcc, so force use clang-cl, otherwise the performance is too poor. + # Manually build the port with clang-cl when using MSVC as compiler + + message(STATUS "Set compiler to clang-cl when using MSVC") + + # https://github.com/microsoft/vcpkg/pull/10398 + set(VCPKG_POLICY_SKIP_ARCHITECTURE_CHECK enabled) + + vcpkg_find_acquire_program(CLANG) + if (CLANG MATCHES "-NOTFOUND") + message(FATAL_ERROR "Clang is required.") + endif () + get_filename_component(CLANG "${CLANG}" DIRECTORY) + + if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm") + set(CLANG_TARGET "arm") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64") + set(CLANG_TARGET "aarch64") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86") + set(CLANG_TARGET "i686") + elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64") + set(CLANG_TARGET "x86_64") + else() + message(FATAL_ERROR "Unsupported target architecture") + endif() + + set(CLANG_TARGET "${CLANG_TARGET}-pc-windows-msvc") + + message(STATUS "Using clang target ${CLANG_TARGET}") + string(APPEND VCPKG_DETECTED_CMAKE_CXX_FLAGS --target=${CLANG_TARGET}) + string(APPEND VCPKG_DETECTED_CMAKE_C_FLAGS --target=${CLANG_TARGET}) + + set(BUILD_OPTIONS + -DCMAKE_CXX_COMPILER=${CLANG}/clang-cl.exe + -DCMAKE_C_COMPILER=${CLANG}/clang-cl.exe + -DCMAKE_CXX_FLAGS=${VCPKG_DETECTED_CMAKE_CXX_FLAGS} + -DCMAKE_C_FLAGS=${VCPKG_DETECTED_CMAKE_C_FLAGS}) +endif () + +vcpkg_cmake_configure( + SOURCE_PATH ${SOURCE_PATH} + DISABLE_PARALLEL_CONFIGURE + OPTIONS + ${BUILD_OPTIONS} + OPTIONS_DEBUG + -DCMAKE_DEBUG_POSTFIX=d +) + +vcpkg_cmake_install() +vcpkg_copy_pdbs() + +vcpkg_cmake_config_fixup(CONFIG_PATH share/cmake/libyuv) + +file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/debug/include) +file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/debug/share) + +configure_file(${CMAKE_CURRENT_LIST_DIR}/libyuv-config.cmake ${CURRENT_PACKAGES_DIR}/share/${PORT} COPYONLY) +file(INSTALL ${SOURCE_PATH}/LICENSE DESTINATION ${CURRENT_PACKAGES_DIR}/share/${PORT} RENAME copyright) + +vcpkg_cmake_get_vars(cmake_vars_file) +include("${cmake_vars_file}") +if (VCPKG_DETECTED_CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + message(WARNING "Use MSVC to compile libyuv results in a very slow library. (https://github.com/microsoft/vcpkg/issues/28446)") + file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage-msvc" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME "usage") +else () + file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") +endif () diff --git a/vendor/rustdesk/res/vcpkg/libyuv/usage b/vendor/rustdesk/res/vcpkg/libyuv/usage new file mode 100644 index 0000000..37c2063 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libyuv/usage @@ -0,0 +1,4 @@ +libyuv provides CMake targets: + + find_package(libyuv CONFIG REQUIRED) + target_link_libraries(main PRIVATE yuv) \ No newline at end of file diff --git a/vendor/rustdesk/res/vcpkg/libyuv/usage-msvc b/vendor/rustdesk/res/vcpkg/libyuv/usage-msvc new file mode 100644 index 0000000..4c90c69 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libyuv/usage-msvc @@ -0,0 +1,9 @@ +libyuv provides CMake targets: + + find_package(libyuv CONFIG REQUIRED) + target_link_libraries(main PRIVATE yuv) + + # WARNING + # You are using MSVC to compile libyuv, which results in a very slow library. + # MSVC won't compile any of the acceleration codes. + # See workarounds: https://github.com/microsoft/vcpkg/issues/28446 diff --git a/vendor/rustdesk/res/vcpkg/libyuv/vcpkg.json b/vendor/rustdesk/res/vcpkg/libyuv/vcpkg.json new file mode 100644 index 0000000..46d61c0 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/libyuv/vcpkg.json @@ -0,0 +1,22 @@ +{ + "name": "libyuv", + "version": "1857", + "description": "libyuv is an open source project that includes YUV scaling and conversion functionality", + "homepage": "https://chromium.googlesource.com/libyuv/libyuv", + "license": null, + "dependencies": [ + "libjpeg-turbo", + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + }, + { + "name": "vcpkg-cmake-get-vars", + "host": true + } + ] +} diff --git a/vendor/rustdesk/res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch b/vendor/rustdesk/res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch new file mode 100644 index 0000000..676c0dd --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch @@ -0,0 +1,10 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index a8a3288..7d01d97 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -1,4 +1,4 @@ +-cmake_minimum_required(VERSION 2.6) ++cmake_minimum_required(VERSION 3.14) + + project( libmfx ) + diff --git a/vendor/rustdesk/res/vcpkg/mfx-dispatch/fix-pkgconf.patch b/vendor/rustdesk/res/vcpkg/mfx-dispatch/fix-pkgconf.patch new file mode 100644 index 0000000..c0310e1 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/mfx-dispatch/fix-pkgconf.patch @@ -0,0 +1,39 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 9446bc4..a8a3288 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -3,16 +3,7 @@ cmake_minimum_required(VERSION 2.6) + project( libmfx ) + + # FIXME Adds support for using system/other install of intel media sdk +-find_path ( INTELMEDIASDK_PATH mfx/mfxvideo.h +- HINTS "${CMAKE_SOURCE_DIR}" +-) +- +-if (INTELMEDIASDK_PATH_NOTFOUND) +- message( FATAL_ERROR "Intel MEDIA SDK include not found" ) +-else (INTELMEDIASDK_PATH_NOTFOUND) +- message(STATUS "Intel Media SDK is here: ${INTELMEDIASDK_PATH}") +-endif (INTELMEDIASDK_PATH_NOTFOUND) +- ++set(INTELMEDIASDK_PATH "${CMAKE_CURRENT_LIST_DIR}") + + set(SOURCES + src/main.cpp +diff --git a/libmfx.pc.cmake b/libmfx.pc.cmake +index fabb541..5d248fe 100644 +--- a/libmfx.pc.cmake ++++ b/libmfx.pc.cmake +@@ -6,9 +6,9 @@ Requires.private: + Name: libmfx + Description: Intel Media SDK Dispatched static library +-Version: 2013 ++Version: 1.35 + Requires: + Requires.private: + Conflicts: +-Libs: -L${libdir} -lsupc++ ${libdir}/libmfx.lib ++Libs: -L${libdir} -llibmfx + Libs.private: +-Cflags: -I${includedir} -I@INTELMEDIASDK_PATH@ ++Cflags: -I${includedir} diff --git a/vendor/rustdesk/res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch b/vendor/rustdesk/res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch new file mode 100644 index 0000000..96d9e6d --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch @@ -0,0 +1,66 @@ +Subject: [PATCH] fix for vcpkg +fix missing mfx_driver_store_loader related symbols +--- +Index: CMakeLists.txt +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/CMakeLists.txt b/CMakeLists.txt +--- a/CMakeLists.txt (revision 7e4d221c36c630c1250b23a5dfa15657bc04c10c) ++++ b/CMakeLists.txt (revision 5ebef171699530ca01594a5cef10a68811f4d105) +@@ -40,6 +39,7 @@ + src/mfx_load_plugin.cpp + src/mfx_plugin_hive.cpp + src/mfx_win_reg_key.cpp ++ src/mfx_driver_store_loader.cpp + ) + endif (CMAKE_SYSTEM_NAME MATCHES "Windows") + +@@ -56,6 +56,12 @@ + configure_file (${CMAKE_SOURCE_DIR}/libmfx.pc.cmake ${CMAKE_BINARY_DIR}/libmfx.pc @ONLY) + + add_library( mfx STATIC ${SOURCES} ) ++ ++if (CMAKE_SYSTEM_NAME MATCHES "Windows") ++ set_target_properties(mfx ++ PROPERTIES PREFIX lib) ++endif (CMAKE_SYSTEM_NAME MATCHES "Windows") ++ + install (DIRECTORY ${CMAKE_SOURCE_DIR}/mfx DESTINATION ${CMAKE_INSTALL_PREFIX}/include FILES_MATCHING PATTERN "*.h") + install (FILES ${CMAKE_BINARY_DIR}/libmfx.pc DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/pkgconfig) + install (TARGETS mfx ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX}/lib) +Index: libmfx.pc.cmake +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/libmfx.pc.cmake b/libmfx.pc.cmake +--- a/libmfx.pc.cmake (revision 7e4d221c36c630c1250b23a5dfa15657bc04c10c) ++++ b/libmfx.pc.cmake (revision 388559e9e8234eb0989e1598a9beea4035a04132) +@@ -9,6 +9,6 @@ + Requires: + Requires.private: + Conflicts: +-Libs: -L${libdir} -lsupc++ ${libdir}/libmfx.a ++Libs: -L${libdir} -lsupc++ ${libdir}/libmfx.lib + Libs.private: + Cflags: -I${includedir} -I@INTELMEDIASDK_PATH@ +Index: src/mfx_driver_store_loader.cpp +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/src/mfx_driver_store_loader.cpp b/src/mfx_driver_store_loader.cpp +--- a/src/mfx_driver_store_loader.cpp (revision 388559e9e8234eb0989e1598a9beea4035a04132) ++++ b/src/mfx_driver_store_loader.cpp (revision 5ebef171699530ca01594a5cef10a68811f4d105) +@@ -24,6 +24,9 @@ + #include "mfx_dispatcher_log.h" + #include "mfx_load_dll.h" + ++#pragma comment(lib, "Ole32.lib") ++#pragma comment(lib, "Advapi32.lib") ++ + namespace MFX + { + diff --git a/vendor/rustdesk/res/vcpkg/mfx-dispatch/portfile.cmake b/vendor/rustdesk/res/vcpkg/mfx-dispatch/portfile.cmake new file mode 100644 index 0000000..cb2ad7e --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/mfx-dispatch/portfile.cmake @@ -0,0 +1,40 @@ +vcpkg_download_distfile( + MISSING_CSTDINT_IMPORT_PATCH + URLS https://github.com/lu-zero/mfx_dispatch/commit/d6241243f85a0d947bdfe813006686a930edef24.patch?full_index=1 + FILENAME fix-missing-cstdint-import-d6241243f85a0d947bdfe813006686a930edef24.patch + SHA512 5d2ffc4ec2ba0e5859d01d2e072f75436ebc3e62e0f6580b5bb8b9f82fe588e7558a46a1fdfa0297a782c0eeb8f50322258d0dd9e41d927cc9be496727b61e44 +) + +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO lu-zero/mfx_dispatch + REF "${VERSION}" + SHA512 12517338342d3e653043a57e290eb9cffd190aede0c3a3948956f1c7f12f0ea859361cf3e534ab066b96b1c211f68409c67ef21fd6d76b68cc31daef541941b0 + HEAD_REF master + PATCHES + fix-unresolved-symbol.patch + fix-pkgconf.patch + 0003-upgrade-cmake-3.14.patch + ${MISSING_CSTDINT_IMPORT_PATCH} +) + +if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) + vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + ) + vcpkg_cmake_install() + vcpkg_copy_pdbs() +else() + if(VCPKG_TARGET_IS_MINGW) + vcpkg_check_linkage(ONLY_STATIC_LIBRARY) + endif() + vcpkg_configure_make( + SOURCE_PATH "${SOURCE_PATH}" + AUTOCONFIG + ) + vcpkg_install_make() +endif() +vcpkg_fixup_pkgconfig() + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") diff --git a/vendor/rustdesk/res/vcpkg/mfx-dispatch/vcpkg.json b/vendor/rustdesk/res/vcpkg/mfx-dispatch/vcpkg.json new file mode 100644 index 0000000..e837471 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/mfx-dispatch/vcpkg.json @@ -0,0 +1,16 @@ +{ + "name": "mfx-dispatch", + "version": "1.35.1", + "port-version": 5, + "description": "Open source Intel media sdk dispatcher", + "homepage": "https://github.com/lu-zero/mfx_dispatch", + "license": "BSD-3-Clause", + "supports": "((x86 | x64) & (android | linux)) | (windows & !uwp)", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true, + "platform": "windows & !mingw" + } + ] +} diff --git a/vendor/rustdesk/res/vcpkg/opus/fix-pkgconfig-version.patch b/vendor/rustdesk/res/vcpkg/opus/fix-pkgconfig-version.patch new file mode 100644 index 0000000..ef9f722 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/opus/fix-pkgconfig-version.patch @@ -0,0 +1,15 @@ +diff --git a/cmake/OpusPackageVersion.cmake b/cmake/OpusPackageVersion.cmake +index 447ce3b..15ebd8e 100644 +--- a/cmake/OpusPackageVersion.cmake ++++ b/cmake/OpusPackageVersion.cmake +@@ -4,7 +4,9 @@ endif() + set(__opus_version INCLUDED) + + function(get_package_version PACKAGE_VERSION PROJECT_VERSION) +- ++ set(PACKAGE_VERSION "0" CACHE STRING "opus package version") ++ set(PROJECT_VERSION "0" CACHE STRING "opus project version") ++ return() + find_package(Git) + if(GIT_FOUND AND EXISTS "${CMAKE_CURRENT_LIST_DIR}/.git") + execute_process(COMMAND ${GIT_EXECUTABLE} diff --git a/vendor/rustdesk/res/vcpkg/opus/portfile.cmake b/vendor/rustdesk/res/vcpkg/opus/portfile.cmake new file mode 100644 index 0000000..b4288b2 --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/opus/portfile.cmake @@ -0,0 +1,61 @@ +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO xiph/opus + REF "v${VERSION}" + SHA512 4ffefd9c035671024f9720c5129bfe395dea04f0d6b730041c2804e89b1db6e4d19633ad1ae58855afc355034233537361e707f26dc53adac916554830038fab + HEAD_REF main + PATCHES fix-pkgconfig-version.patch +) + +vcpkg_check_features(OUT_FEATURE_OPTIONS FEATURE_OPTIONS + FEATURES + avx2 AVX2_SUPPORTED +) + +set(ADDITIONAL_OPUS_OPTIONS "") +if(VCPKG_TARGET_IS_MINGW) + set(STACK_PROTECTOR OFF) + string(APPEND VCPKG_C_FLAGS "-D_FORTIFY_SOURCE=0") + string(APPEND VCPKG_CXX_FLAGS "-D_FORTIFY_SOURCE=0") + if(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)64$") + list(APPEND ADDITIONAL_OPUS_OPTIONS "-DOPUS_USE_NEON=OFF") # for version 1.3.1 (remove for future Opus release) + list(APPEND ADDITIONAL_OPUS_OPTIONS "-DOPUS_DISABLE_INTRINSICS=ON") # for HEAD (and future Opus release) + endif() +elseif(VCPKG_TARGET_IS_EMSCRIPTEN) + set(STACK_PROTECTOR OFF) +else() + set(STACK_PROTECTOR ON) +endif() + +if((VCPKG_TARGET_IS_LINUX AND VCPKG_TARGET_ARCHITECTURE STREQUAL "arm") OR + (VCPKG_TARGET_IS_ANDROID AND VCPKG_TARGET_ARCHITECTURE STREQUAL "arm" AND VCPKG_CMAKE_CONFIGURE_OPTIONS MATCHES "ANDROID_ARM_NEON")) + message(STATUS "Disabling ARM NEON and intrinsics on ${TARGET_TRIPLET}") + list(APPEND ADDITIONAL_OPUS_OPTIONS "-DOPUS_DISABLE_INTRINSICS=ON -DCOMPILER_SUPPORTS_NEON=OFF") # for HEAD (and future Opus release) +endif() + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + OPTIONS ${FEATURE_OPTIONS} + -DPACKAGE_VERSION=${VERSION} + -DOPUS_STACK_PROTECTOR=${STACK_PROTECTOR} + -DOPUS_INSTALL_PKG_CONFIG_MODULE=ON + -DOPUS_INSTALL_CMAKE_CONFIG_MODULE=ON + -DOPUS_BUILD_PROGRAMS=OFF + -DOPUS_BUILD_TESTING=OFF + ${ADDITIONAL_OPUS_OPTIONS} + MAYBE_UNUSED_VARIABLES + OPUS_USE_NEON + OPUS_DISABLE_INTRINSICS +) +vcpkg_cmake_install() +vcpkg_copy_pdbs() + +vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/Opus) +vcpkg_fixup_pkgconfig(SYSTEM_LIBRARIES m) + + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/lib/cmake" + "${CURRENT_PACKAGES_DIR}/lib/cmake" + "${CURRENT_PACKAGES_DIR}/debug/include") + +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/COPYING") diff --git a/vendor/rustdesk/res/vcpkg/opus/vcpkg.json b/vendor/rustdesk/res/vcpkg/opus/vcpkg.json new file mode 100644 index 0000000..574879e --- /dev/null +++ b/vendor/rustdesk/res/vcpkg/opus/vcpkg.json @@ -0,0 +1,22 @@ +{ + "name": "opus", + "version": "1.5.2", + "description": "Totally open, royalty-free, highly versatile audio codec", + "homepage": "https://github.com/xiph/opus", + "license": "BSD-3-Clause", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ], + "features": { + "avx2": { + "description": "Builds the library with avx2 instruction set" + } + } +} diff --git a/vendor/rustdesk/res/xorg.conf b/vendor/rustdesk/res/xorg.conf new file mode 100644 index 0000000..fe15399 --- /dev/null +++ b/vendor/rustdesk/res/xorg.conf @@ -0,0 +1,30 @@ +Section "Monitor" + Identifier "Dummy Monitor" + + # Default HorizSync 31.50 - 48.00 kHz + HorizSync 5.0 - 150.0 + # Default VertRefresh 50.00 - 70.00 Hz + VertRefresh 5.0 - 100.0 + + # Taken from https://www.xpra.org/xorg.conf + Modeline "1920x1080" 23.53 1920 1952 2040 2072 1080 1106 1108 1135 + Modeline "1280x720" 27.41 1280 1312 1416 1448 720 737 740 757 +EndSection + +Section "Device" + Identifier "Dummy VideoCard" + Driver "dummy" + # Default VideoRam 4096 + # (1920 * 1080 * 4) / 1024 = 8100 + VideoRam 8100 +EndSection + +Section "Screen" + Identifier "Dummy Screen" + Device "Dummy VideoCard" + Monitor "Dummy Monitor" + SubSection "Display" + Depth 24 + Modes "1920x1080" "1280x720" + EndSubSection +EndSection \ No newline at end of file diff --git a/vendor/rustdesk/src/auth_2fa.rs b/vendor/rustdesk/src/auth_2fa.rs new file mode 100644 index 0000000..1c243bc --- /dev/null +++ b/vendor/rustdesk/src/auth_2fa.rs @@ -0,0 +1,204 @@ +use hbb_common::{ + anyhow::anyhow, + bail, + config::Config, + get_time, + password_security::{decrypt_vec_or_original, encrypt_vec_or_original}, + ResultType, +}; +use serde_derive::{Deserialize, Serialize}; +use std::sync::Mutex; +use totp_rs::{Algorithm, Secret, TOTP}; + +lazy_static::lazy_static! { + static ref CURRENT_2FA: Mutex> = Mutex::new(None); +} + +const ISSUER: &str = "RustDesk"; +const TAG_LOGIN: &str = "Connection"; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct TOTPInfo { + pub name: String, + pub secret: Vec, + pub digits: usize, + pub created_at: i64, +} + +impl TOTPInfo { + fn new_totp(&self) -> ResultType { + let totp = TOTP::new( + Algorithm::SHA1, + self.digits, + 1, + 30, + self.secret.clone(), + Some(format!("{} {}", ISSUER, TAG_LOGIN)), + self.name.clone(), + )?; + Ok(totp) + } + + fn gen_totp_info(name: String, digits: usize) -> ResultType { + let secret = Secret::generate_secret(); + let totp = TOTPInfo { + secret: secret.to_bytes()?, + name, + digits, + created_at: get_time(), + ..Default::default() + }; + Ok(totp) + } + + pub fn into_string(&self) -> ResultType { + let secret = encrypt_vec_or_original(self.secret.as_slice(), "00", 1024); + let totp_info = TOTPInfo { + secret, + ..self.clone() + }; + let s = serde_json::to_string(&totp_info)?; + Ok(s) + } + + pub fn from_str(data: &str) -> ResultType { + let mut totp_info = serde_json::from_str::(data)?; + let (secret, success, _) = decrypt_vec_or_original(&totp_info.secret, "00"); + if success { + totp_info.secret = secret; + return Ok(totp_info.new_totp()?); + } else { + bail!("decrypt_vec_or_original 2fa secret failed") + } + } +} + +pub fn generate2fa() -> String { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let id = crate::ipc::get_id(); + #[cfg(any(target_os = "android", target_os = "ios"))] + let id = Config::get_id(); + if let Ok(info) = TOTPInfo::gen_totp_info(id, 6) { + if let Ok(totp) = info.new_totp() { + let code = totp.get_url(); + *CURRENT_2FA.lock().unwrap() = Some((info, totp)); + return code; + } + } + "".to_owned() +} + +pub fn verify2fa(code: String) -> bool { + if let Some((info, totp)) = CURRENT_2FA.lock().unwrap().as_ref() { + if let Ok(res) = totp.check_current(&code) { + if res { + if let Ok(v) = info.into_string() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::ipc::set_option("2fa", &v); + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_option("2fa".to_owned(), v); + return res; + } + } + } + } + false +} + +pub fn get_2fa(raw: Option) -> Option { + TOTPInfo::from_str(&raw.unwrap_or(Config::get_option("2fa"))) + .map(|x| Some(x)) + .unwrap_or_default() +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct TelegramBot { + #[serde(skip)] + pub token_str: String, + pub token: Vec, + pub chat_id: String, +} + +impl TelegramBot { + fn into_string(&self) -> ResultType { + let token = encrypt_vec_or_original(self.token_str.as_bytes(), "00", 1024); + let bot = TelegramBot { + token, + ..self.clone() + }; + let s = serde_json::to_string(&bot)?; + Ok(s) + } + + fn save(&self) -> ResultType<()> { + let s = self.into_string()?; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::ipc::set_option("bot", &s); + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_option("bot".to_owned(), s); + Ok(()) + } + + pub fn get() -> ResultType> { + let data = Config::get_option("bot"); + if data.is_empty() { + return Ok(None); + } + let mut bot = serde_json::from_str::(&data)?; + let (token, success, _) = decrypt_vec_or_original(&bot.token, "00"); + if success { + bot.token_str = String::from_utf8(token)?; + return Ok(Some(bot)); + } + bail!("decrypt_vec_or_original telegram bot token failed") + } +} + +// https://gist.github.com/dideler/85de4d64f66c1966788c1b2304b9caf1 +pub async fn send_2fa_code_to_telegram(text: &str, bot: TelegramBot) -> ResultType<()> { + let url = format!("https://api.telegram.org/bot{}/sendMessage", bot.token_str); + let params = serde_json::json!({"chat_id": bot.chat_id, "text": text}); + crate::post_request(url, params.to_string(), "").await?; + Ok(()) +} + +pub fn get_chatid_telegram(bot_token: &str) -> ResultType> { + let url = format!("https://api.telegram.org/bot{}/getUpdates", bot_token); + // because caller is in tokio runtime, so we must call post_request_sync in new thread. + let handle = std::thread::spawn(move || crate::post_request_sync(url, "".to_owned(), "")); + let resp = handle.join().map_err(|_| anyhow!("Thread panicked"))??; + let value = serde_json::from_str::(&resp).map_err(|e| anyhow!(e))?; + + // Check for an error_code in the response + if let Some(error_code) = value.get("error_code").and_then(|code| code.as_i64()) { + // If there's an error_code, try to use the description for the error message + let description = value["description"] + .as_str() + .unwrap_or("Unknown error occurred"); + return Err(anyhow!( + "Telegram API error: {} (error_code: {})", + description, + error_code + )); + } + + let chat_id = &value["result"][0]["message"]["chat"]["id"]; + let chat_id = if let Some(id) = chat_id.as_i64() { + Some(id.to_string()) + } else if let Some(id) = chat_id.as_str() { + Some(id.to_owned()) + } else { + None + }; + + if let Some(chat_id) = chat_id.as_ref() { + let bot = TelegramBot { + token_str: bot_token.to_owned(), + chat_id: chat_id.to_owned(), + ..Default::default() + }; + bot.save()?; + } + + Ok(chat_id) +} diff --git a/vendor/rustdesk/src/cli.rs b/vendor/rustdesk/src/cli.rs new file mode 100644 index 0000000..2f3b355 --- /dev/null +++ b/vendor/rustdesk/src/cli.rs @@ -0,0 +1,199 @@ +use crate::client::*; +use async_trait::async_trait; +use hbb_common::{ + config::PeerConfig, + config::READ_TIMEOUT, + futures::{SinkExt, StreamExt}, + log, + message_proto::*, + protobuf::Message as _, + rendezvous_proto::ConnType, + tokio::{self, sync::mpsc}, + Stream, +}; +use std::sync::{Arc, RwLock}; + +#[derive(Clone)] +pub struct Session { + id: String, + lc: Arc>, + sender: mpsc::UnboundedSender, + password: String, +} + +impl Session { + pub fn new(id: &str, sender: mpsc::UnboundedSender) -> Self { + let mut password = "".to_owned(); + if PeerConfig::load(id).password.is_empty() { + match rpassword::prompt_password("Enter password: ") { + Ok(p) => password = p, + Err(e) => { + log::error!("Failed to read password: {:?}", e); + password = "".to_owned(); + } + } + } + let session = Self { + id: id.to_owned(), + sender, + password, + lc: Default::default(), + }; + session.lc.write().unwrap().initialize( + id.to_owned(), + ConnType::PORT_FORWARD, + None, + false, + None, + None, + ); + session + } +} + +#[async_trait] +impl Interface for Session { + fn get_login_config_handler(&self) -> Arc> { + return self.lc.clone(); + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { + match msgtype { + "input-password" => { + self.sender + .send(Data::Login((self.password.clone(), true))) + .ok(); + } + "re-input-password" => { + log::error!("{}: {}", title, text); + match rpassword::prompt_password("Enter password: ") { + Ok(password) => { + let login_data = Data::Login((password, true)); + self.sender.send(login_data).ok(); + } + Err(e) => { + log::error!("reinput password failed, {:?}", e); + } + } + } + msg if msg.contains("error") => { + log::error!("{}: {}: {}", msgtype, title, text); + } + _ => { + log::info!("{}: {}: {}", msgtype, title, text); + } + } + } + + fn handle_login_error(&self, err: &str) -> bool { + handle_login_error(self.lc.clone(), err, self) + } + + fn handle_peer_info(&self, pi: PeerInfo) { + self.lc.write().unwrap().handle_peer_info(&pi); + } + + async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) { + log::info!( + "password={}", + hbb_common::password_security::temporary_password() + ); + handle_hash(self.lc.clone(), &pass, hash, self, peer).await; + } + + async fn handle_login_from_ui( + &self, + os_username: String, + os_password: String, + password: String, + remember: bool, + peer: &mut Stream, + ) { + handle_login_from_ui( + self.lc.clone(), + os_username, + os_password, + password, + remember, + peer, + ) + .await; + } + + async fn handle_test_delay(&self, t: TestDelay, peer: &mut Stream) { + handle_test_delay(t, peer).await; + } + + fn send(&self, data: Data) { + self.sender.send(data).ok(); + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn connect_test(id: &str, key: String, token: String) { + let (sender, mut receiver) = mpsc::unbounded_channel::(); + let handler = Session::new(&id, sender); + match crate::client::Client::start(id, &key, &token, ConnType::PORT_FORWARD, handler).await { + Err(err) => { + log::error!("Failed to connect {}: {}", &id, err); + } + Ok((mut stream, direct)) => { + log::info!("direct: {}", direct); + // rpassword::prompt_password("Input anything to exit").ok(); + loop { + tokio::select! { + res = hbb_common::timeout(READ_TIMEOUT, stream.next()) => match res { + Err(_) => { + log::error!("Timeout"); + break; + } + Ok(Some(Ok(bytes))) => { + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + match msg_in.union { + Some(message::Union::Hash(hash)) => { + log::info!("Got hash"); + break; + } + _ => {} + } + } + } + _ => {} + } + } + } + } + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn start_one_port_forward( + id: String, + port: i32, + remote_host: String, + remote_port: i32, + key: String, + token: String, +) { + crate::common::test_rendezvous_server(); + crate::common::test_nat_type(); + let (sender, mut receiver) = mpsc::unbounded_channel::(); + let handler = Session::new(&id, sender); + if let Err(err) = crate::port_forward::listen( + handler.id.clone(), + handler.password.clone(), + port, + handler.clone(), + receiver, + &key, + &token, + handler.lc.clone(), + remote_host, + remote_port, + ) + .await + { + log::error!("Failed to listen on {}: {}", port, err); + } + log::info!("port forward (:{}) exit", port); +} diff --git a/vendor/rustdesk/src/client.rs b/vendor/rustdesk/src/client.rs new file mode 100644 index 0000000..3ee36b8 --- /dev/null +++ b/vendor/rustdesk/src/client.rs @@ -0,0 +1,4199 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::clipboard_listener; +use async_trait::async_trait; +use bytes::Bytes; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use clipboard_master::CallbackResult; +#[cfg(not(target_os = "linux"))] +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + Device, Host, StreamConfig, +}; +use crossbeam_queue::ArrayQueue; +use magnum_opus::{Channels::*, Decoder as AudioDecoder}; +#[cfg(not(target_os = "linux"))] +use ringbuf::{ring_buffer::RbBase, Rb}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + ffi::c_void, + net::SocketAddr, + ops::Deref, + str::FromStr, + sync::{ + mpsc::{self, RecvTimeoutError}, + Arc, Mutex, RwLock, + }, +}; +use uuid::Uuid; + +use crate::{ + check_port, + common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP}, + create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, + kcp_stream::KcpStream, + secure_tcp, + ui_interface::{get_builtin_option, resolve_avatar_url, use_texture_render}, + ui_session_interface::{InvokeUiSession, Session}, +}; +#[cfg(feature = "unix-file-copy-paste")] +use crate::{clipboard::check_clipboard_files, clipboard_file::unix_file_clip}; +pub use file_trait::FileManager; +#[cfg(not(feature = "flutter"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::tokio::sync::mpsc::UnboundedSender; +use hbb_common::{ + allow_err, + anyhow::{anyhow, Context}, + bail, + config::{ + self, keys, use_ws, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, + CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS, + }, + fs::JobType, + futures::future::{select_ok, FutureExt}, + get_version_number, log, + message_proto::{option_message::BoolOption, *}, + protobuf::{Message as _, MessageField}, + rand, + rendezvous_proto::*, + sha2::{Digest, Sha256}, + socket_client::{connect_tcp, connect_tcp_local, ipv4_to_ipv6, new_direct_udp_for}, + sodiumoxide::{base64, crypto::sign}, + timeout, + tokio::{ + self, + net::UdpSocket, + sync::{ + mpsc::{unbounded_channel, UnboundedReceiver}, + oneshot, + }, + time::{interval, Duration, Instant}, + }, + AddrMangle, ResultType, Stream, +}; +pub use helper::*; +use scrap::{ + codec::Decoder, + record::{Recorder, RecorderContext}, + CodecFormat, ImageFormat, ImageRgb, ImageTexture, +}; + +#[cfg(not(target_os = "ios"))] +use crate::clipboard::CLIPBOARD_INTERVAL; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::{check_clipboard, ClipboardSide}; +#[cfg(not(feature = "flutter"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::ui_session_interface::SessionPermissionConfig; + +pub use super::lang::*; + +pub mod file_trait; +pub mod helper; +pub mod io_loop; +pub mod screenshot; + +pub const MILLI1: Duration = Duration::from_millis(1); +pub const SEC30: Duration = Duration::from_secs(30); +pub const VIDEO_QUEUE_SIZE: usize = 120; +const MAX_DECODE_FAIL_COUNTER: usize = 3; + +#[cfg(target_os = "linux")] +pub const LOGIN_MSG_DESKTOP_NOT_INITED: &str = "Desktop env is not inited"; +pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY: &str = "Desktop session not ready"; +pub const LOGIN_MSG_DESKTOP_XSESSION_FAILED: &str = "Desktop xsession failed"; +pub const LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER: &str = "Desktop session another user login"; +pub const LOGIN_MSG_DESKTOP_XORG_NOT_FOUND: &str = "Desktop xorg not found"; +// ls /usr/share/xsessions/ +pub const LOGIN_MSG_DESKTOP_NO_DESKTOP: &str = "Desktop none"; +pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY: &str = + "Desktop session not ready, password empty"; +pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG: &str = + "Desktop session not ready, password wrong"; +pub const LOGIN_MSG_PASSWORD_EMPTY: &str = "Empty Password"; +pub const LOGIN_MSG_PASSWORD_WRONG: &str = "Wrong Password"; +pub const LOGIN_MSG_2FA_WRONG: &str = "Wrong 2FA Code"; +pub const REQUIRE_2FA: &'static str = "2FA Required"; +pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access"; +pub const LOGIN_MSG_OFFLINE: &str = "Offline"; +pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported"; +#[cfg(target_os = "linux")] +pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "ubuntu-21-04-required"; +#[cfg(target_os = "linux")] +pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = + "wayland-requires-higher-linux-version"; +#[cfg(target_os = "linux")] +pub const SCRAP_XDP_PORTAL_UNAVAILABLE: &str = + "xdp-portal-unavailable"; +pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; +pub const SCRAP_X11_REF_URL: &str = "https://cstudio.ch/hello-agent/docs/en/manual/linux/#x11-required"; + +#[cfg(not(target_os = "linux"))] +pub const AUDIO_BUFFER_MS: usize = 3000; + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub(crate) struct ClientClipboardContext; + +#[cfg(not(feature = "flutter"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub(crate) struct ClientClipboardContext { + pub cfg: SessionPermissionConfig, + pub tx: UnboundedSender, + #[cfg(feature = "unix-file-copy-paste")] + pub is_file_supported: bool, +} + +/// Client of the remote desktop. +pub struct Client; + +#[cfg(not(target_os = "ios"))] +struct ClipboardState { + #[cfg(feature = "flutter")] + is_text_required: bool, + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + is_file_required: bool, + running: bool, +} + +#[cfg(not(target_os = "linux"))] +lazy_static::lazy_static! { + static ref AUDIO_HOST: Host = cpal::default_host(); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); +} + +#[cfg(not(target_os = "ios"))] +lazy_static::lazy_static! { + static ref CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(ClipboardState::new())); +} + +const PUBLIC_SERVER: &str = "public"; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_key_state(key: enigo::Key) -> bool { + use enigo::KeyboardControllable; + #[cfg(target_os = "macos")] + if key == enigo::Key::NumLock { + return true; + } + ENIGO.lock().unwrap().get_key_state(key) +} + +impl Client { + const CLIENT_CLIPBOARD_NAME: &'static str = "client-clipboard"; + + /// Start a new connection. + pub async fn start( + peer: &str, + key: &str, + token: &str, + conn_type: ConnType, + interface: impl Interface, + ) -> ResultType<( + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), + (i32, String), + )> { + debug_assert!(peer == interface.get_id()); + interface.update_direct(None); + interface.update_received(false); + match Self::_start(peer, key, token, conn_type, interface.clone()).await { + Err(err) => { + let err_str = err.to_string(); + if err_str.starts_with("Failed") { + bail!(err_str + ": Please try later"); + } else { + return Err(err); + } + } + Ok(x) => { + // Set x.2 to true only in the connect() function to indicate that direct_failures needs to be updated; everywhere else it should be set to false. + if x.2 { + let direct_failures = interface.get_lch().read().unwrap().direct_failures; + let direct = x.0 .1; + if !interface.is_force_relay() && (direct_failures == 0) != direct { + let n = if direct { 0 } else { 1 }; + log::info!("direct_failures updated to {}", n); + interface.get_lch().write().unwrap().set_direct_failure(n); + } + } + Ok((x.0, x.1)) + } + } + } + + /// Start a new connection. + async fn _start( + peer: &str, + key: &str, + token: &str, + conn_type: ConnType, + interface: impl Interface, + ) -> ResultType<( + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), + (i32, String), + bool, + )> { + if config::is_incoming_only() { + bail!("Incoming only mode"); + } + // to-do: remember the port for each peer, so that we can retry easier + if hbb_common::is_ip_str(peer) { + return Ok(( + ( + connect_tcp_local(check_port(peer, RELAY_PORT + 1), None, CONNECT_TIMEOUT) + .await?, + true, + None, + None, + "TCP", + ), + (0, "".to_owned()), + false, + )); + } + // Allow connect to {domain}:{port} + if hbb_common::is_domain_port_str(peer) { + return Ok(( + ( + connect_tcp_local(peer, None, CONNECT_TIMEOUT).await?, + true, + None, + None, + "TCP", + ), + (0, "".to_owned()), + false, + )); + } + + let other_server = interface.get_lch().read().unwrap().other_server.clone(); + let (peer, other_server, key, token) = if let Some((a, b, c)) = other_server.as_ref() { + (a.as_ref(), b.as_ref(), c.as_ref(), "") + } else { + (peer, "", key, token) + }; + let (rendezvous_server, servers, contained) = if other_server.is_empty() { + crate::get_rendezvous_server(1_000).await + } else { + if other_server == PUBLIC_SERVER { + ( + check_port(RENDEZVOUS_SERVERS[0], RENDEZVOUS_PORT), + RENDEZVOUS_SERVERS[1..] + .iter() + .map(|x| x.to_string()) + .collect(), + true, + ) + } else { + (check_port(other_server, RENDEZVOUS_PORT), Vec::new(), true) + } + }; + + if crate::get_ipv6_punch_enabled() { + crate::test_ipv6().await; + } + + let (stop_udp_tx, stop_udp_rx) = oneshot::channel::<()>(); + let udp = + // no need to care about multiple rendezvous servers case, since it is acutally not used any more. + // Shared state for UDP NAT test result + if crate::get_udp_punch_enabled() && !interface.is_force_relay() { + if let Ok((socket, addr)) = new_direct_udp_for(&rendezvous_server).await { + let udp_port = Arc::new(Mutex::new(0)); + let up_cloned = udp_port.clone(); + let socket_cloned = socket.clone(); + let func = async move { + allow_err!(test_udp_uat(socket_cloned, addr, up_cloned, stop_udp_rx).await); + }; + tokio::spawn(func); + (Some(socket), Some(udp_port)) + } else { + (None, None) + } + } else { + (None, None) + }; + let fut = Self::_start_inner( + peer.to_owned(), + key.to_owned(), + token.to_owned(), + conn_type, + interface.clone(), + udp.clone(), + Some(stop_udp_tx), + rendezvous_server.clone(), + servers.clone(), + contained, + ); + if udp.0.is_none() { + return fut.await; + } + let mut connect_futures = Vec::new(); + connect_futures.push(fut.boxed()); + let fut = Self::_start_inner( + peer.to_owned(), + key.to_owned(), + token.to_owned(), + conn_type, + interface, + (None, None), + None, + rendezvous_server, + servers, + contained, + ); + connect_futures.push(fut.boxed()); + match select_ok(connect_futures).await { + Ok(conn) => Ok((conn.0 .0, conn.0 .1, conn.0 .2)), + Err(e) => Err(e), + } + } + + async fn _start_inner( + peer: String, + key: String, + token: String, + conn_type: ConnType, + interface: impl Interface, + mut udp: (Option>, Option>>), + stop_udp_tx: Option>, + mut rendezvous_server: String, + servers: Vec, + contained: bool, + ) -> ResultType<( + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), + (i32, String), + bool, + )> { + let mut start = Instant::now(); + let mut socket = connect_tcp(&*rendezvous_server, CONNECT_TIMEOUT).await; + debug_assert!(!servers.contains(&rendezvous_server)); + let rtt = start.elapsed(); + log::debug!("TCP connection establishment time used: {:?}", rtt); + if socket.is_err() && !servers.is_empty() { + log::info!("try the other servers: {:?}", servers); + for server in servers { + let server = check_port(server, RENDEZVOUS_PORT); + socket = connect_tcp(&*server, CONNECT_TIMEOUT).await; + if socket.is_ok() { + rendezvous_server = server; + break; + } + } + crate::refresh_rendezvous_server(); + } else if !contained { + crate::refresh_rendezvous_server(); + } + log::info!("rendezvous server: {}", rendezvous_server); + let mut socket = socket?; + let my_addr = socket.local_addr(); + let mut signed_id_pk = Vec::new(); + let mut relay_server = "".to_owned(); + let mut peer_addr = Config::get_any_listen_addr(true); + let mut peer_nat_type = NatType::UNKNOWN_NAT; + let my_nat_type = crate::get_nat_type(100).await; + let mut is_local = false; + let mut feedback = 0; + use hbb_common::protobuf::Enum; + let nat_type = if interface.is_force_relay() { + NatType::SYMMETRIC + } else { + NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT) + }; + + if !key.is_empty() && !token.is_empty() { + // mainly for the security of token + secure_tcp(&mut socket, &key) + .await + .map_err(|e| anyhow!("Failed to secure tcp: {}", e))?; + } else if let Some(udp) = udp.1.as_ref() { + let tm = Instant::now(); + loop { + let port = *udp.lock().unwrap(); + if port > 0 { + break; + } + // await for 0.5 RTT + if tm.elapsed() > rtt / 2 { + break; + } + hbb_common::sleep(0.001).await; + } + } + // Stop UDP NAT test task if still running + stop_udp_tx.map(|tx| tx.send(())); + let mut msg_out = RendezvousMessage::new(); + let mut ipv6 = if crate::get_ipv6_punch_enabled() { + if let Some((socket, addr)) = crate::get_ipv6_socket().await { + (Some(socket), Some(addr)) + } else { + (None, None) + } + } else { + (None, None) + }; + let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0); + let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" }; + msg_out.set_punch_hole_request(PunchHoleRequest { + id: peer.to_owned(), + token: token.to_owned(), + nat_type: nat_type.into(), + licence_key: key.to_owned(), + conn_type: conn_type.into(), + version: crate::VERSION.to_owned(), + udp_port: udp_nat_port as _, + force_relay: interface.is_force_relay(), + socket_addr_v6: ipv6.1.unwrap_or_default(), + ..Default::default() + }); + for i in 1..=3 { + log::info!( + "#{} {} punch attempt with {}, id: {}", + i, + punch_type, + my_addr, + peer + ); + socket.send(&msg_out).await?; + // below timeout should not bigger than hbbs's connection timeout. + if let Some(msg_in) = + crate::get_next_nonkeyexchange_msg(&mut socket, Some(i * 3000)).await + { + match msg_in.union { + Some(rendezvous_message::Union::PunchHoleResponse(ph)) => { + if ph.socket_addr.is_empty() { + if !ph.other_failure.is_empty() { + bail!(ph.other_failure); + } + match ph.failure.enum_value() { + Ok(punch_hole_response::Failure::ID_NOT_EXIST) => { + bail!("ID does not exist"); + } + Ok(punch_hole_response::Failure::OFFLINE) => { + bail!("Remote desktop is offline"); + } + Ok(punch_hole_response::Failure::LICENSE_MISMATCH) => { + bail!("Key mismatch"); + } + Ok(punch_hole_response::Failure::LICENSE_OVERUSE) => { + bail!("Key overuse"); + } + _ => bail!("other punch hole failure"), + } + } else { + peer_nat_type = ph.nat_type(); + is_local = ph.is_local(); + signed_id_pk = ph.pk.into(); + relay_server = ph.relay_server; + peer_addr = AddrMangle::decode(&ph.socket_addr); + feedback = ph.feedback; + let s = udp.0.take(); + if ph.is_udp && s.is_some() { + if let Some(s) = s { + allow_err!(s.connect(peer_addr).await); + udp.0 = Some(s); + } + } + let s = ipv6.0.take(); + if !ph.socket_addr_v6.is_empty() && s.is_some() { + let addr = AddrMangle::decode(&ph.socket_addr_v6); + if addr.port() > 0 { + if let Some(s) = s { + allow_err!(s.connect(addr).await); + ipv6.0 = Some(s); + } + } + } + log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr); + break; + } + } + Some(rendezvous_message::Union::RelayResponse(rr)) => { + log::info!( + "relay requested from peer, time used: {:?}, relay_server: {}", + start.elapsed(), + rr.relay_server + ); + start = Instant::now(); + let mut connect_futures = Vec::new(); + if let Some(s) = ipv6.0 { + let addr = AddrMangle::decode(&rr.socket_addr_v6); + if addr.port() > 0 { + if s.connect(addr).await.is_ok() { + connect_futures + .push(udp_nat_connect(s, "IPv6", CONNECT_TIMEOUT).boxed()); + } + } + } + signed_id_pk = rr.pk().into(); + let fut = Self::create_relay( + &peer, + rr.uuid, + rr.relay_server, + &key, + conn_type, + my_addr.is_ipv4(), + ); + connect_futures.push( + async move { + let conn = fut.await?; + Ok((conn, None, if use_ws() { "WebSocket" } else { "Relay" })) + } + .boxed(), + ); + // Run all connection attempts concurrently, return the first successful one + let (conn, kcp, typ) = match select_ok(connect_futures).await { + Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2), + + Err(e) => (Err(e), None, ""), + }; + let mut conn = conn?; + feedback = rr.feedback; + log::info!("{:?} used to establish {typ} connection", start.elapsed()); + let pk = + Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?; + return Ok(( + (conn, typ == "IPv6", pk, kcp, typ), + (feedback, rendezvous_server), + false, + )); + } + _ => { + log::error!("Unexpected protobuf msg received: {:?}", msg_in); + } + } + } + } + drop(socket); + if peer_addr.port() == 0 { + bail!("Failed to connect via rendezvous server"); + } + let time_used = start.elapsed().as_millis() as u64; + log::info!( + "{} ms used to {} punch hole, relay_server: {}, {}", + time_used, + punch_type, + relay_server, + if is_local { + "is_local: true".to_owned() + } else { + format!("nat_type: {:?}", peer_nat_type) + } + ); + Ok(( + Self::connect( + my_addr, + peer_addr, + &peer, + signed_id_pk, + &relay_server, + &rendezvous_server, + time_used, + peer_nat_type, + my_nat_type, + is_local, + &key, + &token, + conn_type, + interface, + udp.0, + ipv6.0, + punch_type, + ) + .await?, + (feedback, rendezvous_server), + true, + )) + } + + /// Connect to the peer. + async fn connect( + local_addr: SocketAddr, + peer: SocketAddr, + peer_id: &str, + signed_id_pk: Vec, + relay_server: &str, + rendezvous_server: &str, + punch_time_used: u64, + peer_nat_type: NatType, + my_nat_type: i32, + is_local: bool, + key: &str, + token: &str, + conn_type: ConnType, + interface: impl Interface, + udp_socket_nat: Option>, + udp_socket_v6: Option>, + punch_type: &str, + ) -> ResultType<( + Stream, + bool, + Option>, + Option, + &'static str, + )> { + let direct_failures = interface.get_lch().read().unwrap().direct_failures; + let mut connect_timeout = 0; + const MIN: u64 = 1000; + if is_local || peer_nat_type == NatType::SYMMETRIC { + connect_timeout = MIN; + } else { + if relay_server.is_empty() { + connect_timeout = CONNECT_TIMEOUT; + } else { + if peer_nat_type == NatType::ASYMMETRIC { + let mut my_nat_type = my_nat_type; + if my_nat_type == NatType::UNKNOWN_NAT as i32 { + my_nat_type = crate::get_nat_type(100).await; + } + if my_nat_type == NatType::ASYMMETRIC as i32 { + connect_timeout = CONNECT_TIMEOUT; + if direct_failures > 0 { + connect_timeout = punch_time_used * 6; + } + } else if my_nat_type == NatType::SYMMETRIC as i32 { + connect_timeout = MIN; + } + } + if connect_timeout == 0 { + let n = if direct_failures > 0 { 3 } else { 6 }; + connect_timeout = punch_time_used * (n as u64); + } + } + if connect_timeout < MIN { + connect_timeout = MIN; + } + } + log::info!("peer address: {}, timeout: {}", peer, connect_timeout); + let start = std::time::Instant::now(); + + let mut connect_futures = Vec::new(); + let fut = connect_tcp_local(peer, Some(local_addr), connect_timeout); + connect_futures.push( + async move { + let conn = fut.await?; + Ok((conn, None, "TCP")) + } + .boxed(), + ); + if let Some(udp_socket_nat) = udp_socket_nat { + connect_futures.push(udp_nat_connect(udp_socket_nat, "UDP", connect_timeout).boxed()); + } + if let Some(udp_socket_v6) = udp_socket_v6 { + connect_futures.push(udp_nat_connect(udp_socket_v6, "IPv6", connect_timeout).boxed()); + } + // Run all connection attempts concurrently, return the first successful one + let (mut conn, kcp, mut typ) = match select_ok(connect_futures).await { + Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2), + Err(e) => (Err(e), None, ""), + }; + + let mut direct = !conn.is_err(); + if interface.is_force_relay() || conn.is_err() { + if !relay_server.is_empty() { + conn = Self::request_relay( + peer_id, + relay_server.to_owned(), + rendezvous_server, + !signed_id_pk.is_empty(), + key, + token, + conn_type, + ) + .await; + if let Err(e) = conn { + // this direct is mainly used by on_establish_connection_error, so we update it here before bail + interface.update_direct(Some(false)); + bail!("Failed to connect via relay server: {}", e); + } + typ = "Relay"; + direct = false; + } else { + bail!("Failed to make direct connection to remote desktop"); + } + } + let mut conn = conn?; + log::info!( + "{:?} used to establish {typ} connection with {} punch", + start.elapsed(), + punch_type + ); + let res = Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await; + let pk: Option> = match res { + Ok(pk) => pk, + Err(e) => { + // this direct is mainly used by on_establish_connection_error, so we update it here before bail + interface.update_direct(Some(direct)); + bail!(e); + } + }; + log::debug!("{} punch secure_connection ok", punch_type); + Ok((conn, direct, pk, kcp, typ)) + } + + /// Establish secure connection with the server. + async fn secure_connection( + peer_id: &str, + signed_id_pk: Vec, + key: &str, + conn: &mut Stream, + ) -> ResultType>> { + let rs_pk = get_rs_pk(if key.is_empty() { + config::RS_PUB_KEY + } else { + key + }); + let mut sign_pk = None; + let mut option_pk = None; + if !signed_id_pk.is_empty() { + if let Some(rs_pk) = rs_pk { + if let Ok((id, pk)) = decode_id_pk(&signed_id_pk, &rs_pk) { + if id == peer_id { + sign_pk = Some(sign::PublicKey(pk)); + option_pk = Some(pk.to_vec()); + } + } + } + if sign_pk.is_none() { + log::error!("Handshake failed: invalid public key from rendezvous server"); + } + } + let sign_pk = match sign_pk { + Some(v) => v, + None => { + // send an empty message out in case server is setting up secure and waiting for first message + conn.send(&Message::new()).await?; + return Ok(option_pk); + } + }; + match timeout(READ_TIMEOUT, conn.next()).await? { + Some(res) => { + let bytes = res?; + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + if let Some(message::Union::SignedId(si)) = msg_in.union { + if let Ok((id, their_pk_b)) = decode_id_pk(&si.id, &sign_pk) { + if id == peer_id { + let (asymmetric_value, symmetric_value, key) = + create_symmetric_key_msg(their_pk_b); + let mut msg_out = Message::new(); + msg_out.set_public_key(PublicKey { + asymmetric_value, + symmetric_value, + ..Default::default() + }); + timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??; + conn.set_key(key); + } else { + log::error!("Handshake failed: sign failure"); + conn.send(&Message::new()).await?; + } + } else { + // fall back to non-secure connection in case pk mismatch + log::info!("pk mismatch, fall back to non-secure"); + let mut msg_out = Message::new(); + msg_out.set_public_key(PublicKey::new()); + conn.send(&msg_out).await?; + } + } else { + log::error!("Handshake failed: invalid message type"); + conn.send(&Message::new()).await?; + } + } else { + log::error!("Handshake failed: invalid message format"); + conn.send(&Message::new()).await?; + } + } + None => { + bail!("Reset by the peer"); + } + } + Ok(option_pk) + } + + /// Request a relay connection to the server. + async fn request_relay( + peer: &str, + relay_server: String, + rendezvous_server: &str, + secure: bool, + key: &str, + token: &str, + conn_type: ConnType, + ) -> ResultType { + let mut succeed = false; + let mut uuid = "".to_owned(); + let mut ipv4 = true; + + for i in 1..=3 { + // use different socket due to current hbbs implementation requiring different nat address for each attempt + let mut socket = connect_tcp(rendezvous_server, CONNECT_TIMEOUT) + .await + .with_context(|| "Failed to connect to rendezvous server")?; + + if !key.is_empty() && !token.is_empty() { + // mainly for the security of token + secure_tcp(&mut socket, key).await?; + } + + ipv4 = socket.local_addr().is_ipv4(); + let mut msg_out = RendezvousMessage::new(); + uuid = Uuid::new_v4().to_string(); + log::info!( + "#{} request relay attempt, id: {}, uuid: {}, relay_server: {}, secure: {}", + i, + peer, + uuid, + relay_server, + secure, + ); + msg_out.set_request_relay(RequestRelay { + id: peer.to_owned(), + token: token.to_owned(), + uuid: uuid.clone(), + relay_server: relay_server.clone(), + secure, + ..Default::default() + }); + socket.send(&msg_out).await?; + + if let Some(msg_in) = + crate::get_next_nonkeyexchange_msg(&mut socket, Some(CONNECT_TIMEOUT)).await + { + if let Some(rendezvous_message::Union::RelayResponse(rs)) = msg_in.union { + if !rs.refuse_reason.is_empty() { + bail!(rs.refuse_reason); + } + succeed = true; + break; + } + } + } + if !succeed { + bail!("Timeout"); + } + Self::create_relay(peer, uuid, relay_server, key, conn_type, ipv4).await + } + + /// Create a relay connection to the server. + async fn create_relay( + peer: &str, + uuid: String, + relay_server: String, + key: &str, + conn_type: ConnType, + ipv4: bool, + ) -> ResultType { + let mut conn = connect_tcp( + ipv4_to_ipv6(check_port(relay_server, RELAY_PORT), ipv4), + CONNECT_TIMEOUT, + ) + .await + .with_context(|| "Failed to connect to relay server")?; + let mut msg_out = RendezvousMessage::new(); + msg_out.set_request_relay(RequestRelay { + licence_key: key.to_owned(), + id: peer.to_owned(), + uuid, + conn_type: conn_type.into(), + ..Default::default() + }); + conn.send(&msg_out).await?; + Ok(conn) + } + + #[inline] + #[cfg(feature = "flutter")] + #[cfg(not(target_os = "ios"))] + pub fn set_is_text_clipboard_required(b: bool) { + CLIPBOARD_STATE.lock().unwrap().is_text_required = b; + } + + #[inline] + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + pub fn set_is_file_clipboard_required(b: bool) { + CLIPBOARD_STATE.lock().unwrap().is_file_required = b; + } + + #[cfg(not(target_os = "ios"))] + fn try_stop_clipboard() { + // There's a bug here. + // If session is closed by the peer, `has_sessions_running()` will always return true. + // It's better to check if the active session number. + // But it's not a problem, because the clipboard thread does not consume CPU. + // + // If we want to fix it, we can add a flag to indicate if session is active. + // But I think it's not necessary to introduce complexity at this point. + #[cfg(feature = "flutter")] + if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { + return; + } + #[cfg(not(target_os = "android"))] + clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME); + CLIPBOARD_STATE.lock().unwrap().running = false; + #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] + clipboard::platform::unix::fuse::uninit_fuse_context(true); + } + + // `try_start_clipboard` is called by all session when connection is established. (When handling peer info). + // This function only create one thread with a loop, the loop is shared by all sessions. + // After all sessions are end, the loop exists. + // + // If clipboard update is detected, the text will be sent to all sessions by `send_clipboard_msg`. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn try_start_clipboard( + _client_clip_ctx: Option, + ) -> Option> { + let mut clipboard_lock = CLIPBOARD_STATE.lock().unwrap(); + if clipboard_lock.running { + return None; + } + + let (tx_cb_result, rx_cb_result) = mpsc::channel(); + if let Err(e) = + clipboard_listener::subscribe(Self::CLIENT_CLIPBOARD_NAME.to_owned(), tx_cb_result) + { + log::error!("Failed to subscribe clipboard listener: {}", e); + return None; + } + + clipboard_lock.running = true; + let (tx_started, rx_started) = unbounded_channel(); + + log::info!("Start client clipboard loop"); + std::thread::spawn(move || { + let mut handler = ClientClipboardHandler { + ctx: None, + #[cfg(not(feature = "flutter"))] + client_clip_ctx: _client_clip_ctx, + }; + + tx_started.send(()).ok(); + loop { + if !CLIPBOARD_STATE.lock().unwrap().running { + break; + } + match rx_cb_result.recv_timeout(Duration::from_millis(CLIPBOARD_INTERVAL)) { + Ok(CallbackResult::Next) => { + handler.check_clipboard(); + } + Ok(CallbackResult::Stop) => { + log::debug!("Clipboard listener stopped"); + break; + } + Ok(CallbackResult::StopWithError(err)) => { + log::error!("Clipboard listener stopped with error: {}", err); + break; + } + Err(RecvTimeoutError::Timeout) => {} + Err(RecvTimeoutError::Disconnected) => { + log::error!("Clipboard listener disconnected"); + break; + } + } + } + log::info!("Stop client clipboard loop"); + CLIPBOARD_STATE.lock().unwrap().running = false; + }); + + Some(rx_started) + } + + #[cfg(target_os = "android")] + fn try_start_clipboard(_p: Option<()>) -> Option> { + let mut clipboard_lock = CLIPBOARD_STATE.lock().unwrap(); + if clipboard_lock.running { + return None; + } + clipboard_lock.running = true; + + log::info!("Start client clipboard loop"); + std::thread::spawn(move || { + loop { + if !CLIPBOARD_STATE.lock().unwrap().running { + break; + } + if !CLIPBOARD_STATE.lock().unwrap().is_text_required { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + continue; + } + + if let Some(msg) = crate::clipboard::get_clipboards_msg(true) { + crate::flutter::send_clipboard_msg(msg, false); + } + + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + } + log::info!("Stop client clipboard loop"); + CLIPBOARD_STATE.lock().unwrap().running = false; + }); + + None + } +} + +#[cfg(not(target_os = "ios"))] +impl ClipboardState { + fn new() -> Self { + Self { + #[cfg(feature = "flutter")] + is_text_required: true, + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + is_file_required: true, + running: false, + } + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +struct ClientClipboardHandler { + ctx: Option, + #[cfg(not(feature = "flutter"))] + client_clip_ctx: Option, +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl ClientClipboardHandler { + fn is_text_required(&self) -> bool { + #[cfg(feature = "flutter")] + { + CLIPBOARD_STATE.lock().unwrap().is_text_required + } + #[cfg(not(feature = "flutter"))] + { + self.client_clip_ctx + .as_ref() + .map(|ctx| ctx.cfg.is_text_clipboard_required()) + .unwrap_or(false) + } + } + + #[cfg(feature = "unix-file-copy-paste")] + fn is_file_required(&self) -> bool { + #[cfg(feature = "flutter")] + { + CLIPBOARD_STATE.lock().unwrap().is_file_required + } + #[cfg(not(feature = "flutter"))] + { + self.client_clip_ctx + .as_ref() + .map(|ctx| ctx.cfg.is_file_clipboard_required()) + .unwrap_or(false) + } + } + + fn check_clipboard(&mut self) { + if CLIPBOARD_STATE.lock().unwrap().running { + #[cfg(feature = "unix-file-copy-paste")] + if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false) { + if !urls.is_empty() { + #[cfg(target_os = "macos")] + if crate::clipboard::is_file_url_set_by_rustdesk(&urls) { + return; + } + if self.is_file_required() { + match clipboard::platform::unix::serv_files::sync_files(&urls) { + Ok(()) => { + let msg = crate::clipboard_file::clip_2_msg( + unix_file_clip::get_format_list(), + ); + self.send_msg(msg, true); + } + Err(e) => { + log::error!("Failed to sync clipboard files: {}", e); + } + } + return; + } + } + } + + if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) { + if self.is_text_required() { + self.send_msg(msg, false); + } + } + } + } + + #[inline] + #[cfg(feature = "flutter")] + fn send_msg(&self, msg: Message, _is_file: bool) { + crate::flutter::send_clipboard_msg(msg, _is_file); + } + + #[cfg(not(feature = "flutter"))] + fn send_msg(&self, msg: Message, _is_file: bool) { + if let Some(ctx) = &self.client_clip_ctx { + #[cfg(feature = "unix-file-copy-paste")] + if _is_file { + if ctx.is_file_supported { + let _ = ctx.tx.send(Data::Message(msg)); + } + return; + } + + let pi = ctx.cfg.lc.read().unwrap().peer_info.clone(); + if let Some(pi) = pi.as_ref() { + if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip( + &pi.version, + &pi.platform, + multi_clipboards, + ) { + let _ = ctx.tx.send(Data::Message(msg_out)); + return; + } + } + } + let _ = ctx.tx.send(Data::Message(msg)); + } + } +} + +/// Audio handler for the [`Client`]. +#[derive(Default)] +pub struct AudioHandler { + audio_decoder: Option<(AudioDecoder, Vec)>, + #[cfg(target_os = "linux")] + simple: Option, + #[cfg(not(target_os = "linux"))] + audio_buffer: AudioBuffer, + sample_rate: (u32, u32), + #[cfg(not(target_os = "linux"))] + audio_stream: Option>, + channels: u16, + #[cfg(not(target_os = "linux"))] + device_channel: u16, + #[cfg(not(target_os = "linux"))] + ready: Arc>, +} + +#[cfg(not(target_os = "linux"))] +struct AudioBuffer( + pub Arc>>, + usize, + [usize; 30], +); + +#[cfg(not(target_os = "linux"))] +impl Default for AudioBuffer { + fn default() -> Self { + Self( + Arc::new(std::sync::Mutex::new( + ringbuf::HeapRb::::new(48000 * 2 * AUDIO_BUFFER_MS / 1000), // 48000hz, 2 channel + )), + 48000 * 2, + [0; 30], + ) + } +} + +#[cfg(not(target_os = "linux"))] +impl AudioBuffer { + pub fn resize(&mut self, sample_rate: usize, channels: usize) { + let capacity = sample_rate * channels * AUDIO_BUFFER_MS / 1000; + let old_capacity = self.0.lock().unwrap().capacity(); + if capacity != old_capacity { + *self.0.lock().unwrap() = ringbuf::HeapRb::::new(capacity); + self.1 = sample_rate * channels; + log::info!("Audio buffer resized from {old_capacity} to {capacity}"); + } + } + + fn try_shrink(&mut self, having: usize) { + extern crate chrono; + use chrono::prelude::*; + + let mut i = (having * 10) / self.1; + if i > 29 { + i = 29; + } + self.2[i] += 1; + + #[allow(non_upper_case_globals)] + static mut tms: i64 = 0; + let dt = Local::now().timestamp_millis(); + unsafe { + if tms == 0 { + tms = dt; + return; + } else if dt < tms + 12000 { + return; + } + tms = dt; + } + + // the safer water mark to drop + let mut zero = 0; + // the water mark taking most of time + let mut max = 0; + for i in 0..30 { + if self.2[i] == 0 && zero == i { + zero += 1; + } + + if self.2[i] > self.2[max] { + self.2[max] = 0; + max = i; + } else { + self.2[i] = 0; + } + } + zero = zero * 2 / 3; + + // how many data can be dropped: + // 1. will not drop if buffered data is less than 600ms + // 2. choose based on min(zero, max) + const N: usize = 4; + self.2[max] = 0; + if max < 6 { + return; + } else if max > zero * N { + max = zero * N; + } + + let mut lock = self.0.lock().unwrap(); + let cap = lock.capacity(); + let having = lock.occupied_len(); + let skip = (cap * max / (30 * N) + 1) & (!1); + if (having > skip * 3) && (skip > 0) { + lock.skip(skip); + log::info!("skip {skip}, based {max} {zero}"); + } + } + + /// append pcm to audio buffer, if buffered data + /// exceeds AUDIO_BUFFER_MS, only AUDIO_BUFFER_MS + /// will be kept. + fn append_pcm2(&self, buffer: &[f32]) -> usize { + let mut lock = self.0.lock().unwrap(); + let cap = lock.capacity(); + if buffer.len() > cap { + lock.push_slice_overwrite(buffer); + return cap; + } + + let having = lock.occupied_len() + buffer.len(); + if having > cap { + lock.skip(having - cap); + } + lock.push_slice_overwrite(buffer); + lock.occupied_len() + } + + /// append pcm to audio buffer, trying to drop data + /// when data is too much (per 12 seconds) based + /// statistics. + pub fn append_pcm(&mut self, buffer: &[f32]) { + let having = self.append_pcm2(buffer); + self.try_shrink(having); + } +} + +impl AudioHandler { + #[cfg(target_os = "linux")] + fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { + use psimple::Simple; + use pulse::sample::{Format, Spec}; + use pulse::stream::Direction; + + let spec = Spec { + format: Format::F32le, + channels: format0.channels as _, + rate: format0.sample_rate as _, + }; + if !spec.is_valid() { + bail!("Invalid audio format"); + } + + self.simple = Some(Simple::new( + None, // Use the default server + &crate::get_app_name(), // Our application’s name + Direction::Playback, // We want a playback stream + None, // Use the default device + "playback", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + None, // Use default buffering attributes + )?); + self.sample_rate = (format0.sample_rate, format0.sample_rate); + Ok(()) + } + + /// Start the audio playback. + #[cfg(not(target_os = "linux"))] + fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { + let device = AUDIO_HOST + .default_output_device() + .with_context(|| "Failed to get default output device")?; + log::info!( + "Using default output device: \"{}\"", + device.name().unwrap_or("".to_owned()) + ); + let config = device.default_output_config().map_err(|e| anyhow!(e))?; + let sample_format = config.sample_format(); + log::info!("Default output format: {:?}", config); + log::info!("Remote input format: {:?}", format0); + #[allow(unused_mut)] + let mut config: StreamConfig = config.into(); + #[cfg(not(target_os = "ios"))] + { + // this makes ios audio output not work + config.buffer_size = cpal::BufferSize::Fixed(64); + } + + self.sample_rate = (format0.sample_rate, config.sample_rate.0); + let mut build_output_stream = |config: StreamConfig| match sample_format { + cpal::SampleFormat::I8 => self.build_output_stream::(&config, &device), + cpal::SampleFormat::I16 => self.build_output_stream::(&config, &device), + cpal::SampleFormat::I32 => self.build_output_stream::(&config, &device), + cpal::SampleFormat::I64 => self.build_output_stream::(&config, &device), + cpal::SampleFormat::U8 => self.build_output_stream::(&config, &device), + cpal::SampleFormat::U16 => self.build_output_stream::(&config, &device), + cpal::SampleFormat::U32 => self.build_output_stream::(&config, &device), + cpal::SampleFormat::U64 => self.build_output_stream::(&config, &device), + cpal::SampleFormat::F32 => self.build_output_stream::(&config, &device), + cpal::SampleFormat::F64 => self.build_output_stream::(&config, &device), + f => bail!("unsupported audio format: {:?}", f), + }; + if config.channels > format0.channels as _ { + let no_rechannel_config = StreamConfig { + channels: format0.channels as _, + ..config.clone() + }; + if let Err(_) = build_output_stream(no_rechannel_config) { + build_output_stream(config)?; + } + } else { + build_output_stream(config)?; + } + + Ok(()) + } + + /// Handle audio format and create an audio decoder. + pub fn handle_format(&mut self, f: AudioFormat) { + match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) { + Ok(d) => { + let buffer = vec![0.; f.sample_rate as usize * f.channels as usize]; + self.audio_decoder = Some((d, buffer)); + self.channels = f.channels as _; + allow_err!(self.start_audio(f)); + } + Err(err) => { + log::error!("Failed to create audio decoder: {}", err); + } + } + } + + /// Handle audio frame and play it. + #[inline] + pub fn handle_frame(&mut self, frame: AudioFrame) { + #[cfg(not(target_os = "linux"))] + if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() { + return; + } + #[cfg(target_os = "linux")] + if self.simple.is_none() { + log::debug!("PulseAudio simple binding does not exists"); + return; + } + self.audio_decoder.as_mut().map(|(d, buffer)| { + if let Ok(n) = d.decode_float(&frame.data, buffer, false) { + let channels = self.channels; + let n = n * (channels as usize); + #[cfg(not(target_os = "linux"))] + { + let sample_rate0 = self.sample_rate.0; + let sample_rate = self.sample_rate.1; + let mut buffer = buffer[0..n].to_owned(); + if sample_rate != sample_rate0 { + buffer = crate::audio_resample( + &buffer[0..n], + sample_rate0, + sample_rate, + channels, + ); + } + if self.channels != self.device_channel { + buffer = crate::audio_rechannel( + buffer, + sample_rate, + sample_rate, + self.channels, + self.device_channel, + ); + } + self.audio_buffer.append_pcm(&buffer); + } + #[cfg(target_os = "linux")] + { + let data_u8 = + unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; + self.simple.as_mut().map(|x| x.write(data_u8)); + } + } + }); + } + + /// Build audio output stream for current device. + #[cfg(not(target_os = "linux"))] + fn build_output_stream>( + &mut self, + config: &StreamConfig, + device: &Device, + ) -> ResultType<()> { + self.device_channel = config.channels; + let err_fn = move |err| { + // too many errors, will improve later + log::trace!("an error occurred on stream: {}", err); + }; + self.audio_buffer + .resize(config.sample_rate.0 as _, config.channels as _); + let audio_buffer = self.audio_buffer.0.clone(); + let ready = self.ready.clone(); + let timeout = None; + let stream = device.build_output_stream( + config, + move |data: &mut [T], info: &cpal::OutputCallbackInfo| { + if !*ready.lock().unwrap() { + *ready.lock().unwrap() = true; + } + + let mut n = data.len(); + let mut lock = audio_buffer.lock().unwrap(); + let mut having = lock.occupied_len(); + // android two timestamps, one from zero, another not + #[cfg(not(target_os = "android"))] + if having < n { + let tms = info.timestamp(); + let how_long = tms + .playback + .duration_since(&tms.callback) + .unwrap_or(Duration::from_millis(0)); + + // must long enough to fight back scheuler delay + if how_long > Duration::from_millis(6) && how_long < Duration::from_millis(3000) + { + drop(lock); + std::thread::sleep(how_long.div_f32(1.2)); + lock = audio_buffer.lock().unwrap(); + having = lock.occupied_len(); + } + + if having < n { + n = having; + } + } + #[cfg(target_os = "android")] + if having < n { + n = having; + } + let mut elems = vec![0.0f32; n]; + if n > 0 { + lock.pop_slice(&mut elems); + } + drop(lock); + + let mut input = elems.into_iter(); + for sample in data.iter_mut() { + *sample = match input.next() { + Some(x) => T::from_sample(x), + _ => T::from_sample(0.), + }; + } + }, + err_fn, + timeout, + )?; + stream.play()?; + self.audio_stream = Some(Box::new(stream)); + Ok(()) + } +} + +/// Video handler for the [`Client`]. +pub struct VideoHandler { + decoder: Decoder, + pub rgb: ImageRgb, + pub texture: ImageTexture, + recorder: Arc>>, + record: bool, + _display: usize, // useful for debug + fail_counter: usize, + first_frame: bool, +} + +impl VideoHandler { + #[cfg(feature = "flutter")] + pub fn get_adapter_luid() -> Option { + crate::flutter::get_adapter_luid() + } + + #[cfg(not(feature = "flutter"))] + pub fn get_adapter_luid() -> Option { + None + } + + /// Create a new video handler. + pub fn new(format: CodecFormat, _display: usize) -> Self { + let luid = Self::get_adapter_luid(); + log::info!("new video handler for display #{_display}, format: {format:?}, luid: {luid:?}"); + let rgba_format = + if cfg!(feature = "flutter") && (cfg!(windows) || cfg!(target_os = "linux")) { + ImageFormat::ABGR + } else { + ImageFormat::ARGB + }; + VideoHandler { + decoder: Decoder::new(format, luid), + rgb: ImageRgb::new(rgba_format, crate::get_dst_align_rgba()), + texture: Default::default(), + recorder: Default::default(), + record: false, + _display, + fail_counter: 0, + first_frame: true, + } + } + + /// Handle a new video frame. + #[inline] + pub fn handle_frame( + &mut self, + vf: VideoFrame, + pixelbuffer: &mut bool, + chroma: &mut Option, + ) -> ResultType { + let format = CodecFormat::from(&vf); + if format != self.decoder.format() { + self.reset(Some(format)); + } + match &vf.union { + Some(frame) => { + let res = self.decoder.handle_video_frame( + frame, + &mut self.rgb, + &mut self.texture, + pixelbuffer, + chroma, + ); + if res.as_ref().is_ok_and(|x| *x) { + self.fail_counter = 0; + } else { + if self.fail_counter < usize::MAX { + if self.first_frame && self.fail_counter < MAX_DECODE_FAIL_COUNTER { + log::error!("decode first frame failed"); + self.fail_counter = MAX_DECODE_FAIL_COUNTER; + } else { + self.fail_counter += 1; + } + log::error!( + "Failed to handle video frame, fail counter: {}", + self.fail_counter + ); + } + } + self.first_frame = false; + if self.record { + self.recorder.lock().unwrap().as_mut().map(|r| { + let (w, h) = if *pixelbuffer { + (self.rgb.w, self.rgb.h) + } else { + (self.texture.w, self.texture.h) + }; + r.write_frame(frame, w, h).ok(); + }); + } + res + } + _ => Ok(false), + } + } + + /// Reset the decoder, change format if it is Some + pub fn reset(&mut self, format: Option) { + log::info!( + "reset video handler for display #{}, format: {format:?}", + self._display + ); + #[cfg(target_os = "macos")] + self.rgb.set_align(crate::get_dst_align_rgba()); + let luid = Self::get_adapter_luid(); + let format = format.unwrap_or(self.decoder.format()); + self.decoder = Decoder::new(format, luid); + self.fail_counter = 0; + self.first_frame = true; + } + + /// Start or stop screen record. + pub fn record_screen(&mut self, start: bool, id: String, display_idx: usize, camera: bool) { + self.record = false; + if start { + self.recorder = Recorder::new(RecorderContext { + server: false, + id, + dir: crate::ui_interface::video_save_directory(false), + display_idx, + camera, + tx: None, + }) + .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); + } else { + self.recorder = Default::default(); + } + + self.record = start; + } +} + +// The source of sent password +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +enum PasswordSource { + PersonalAb(Vec), + SharedAb(String), + Undefined, +} + +impl Default for PasswordSource { + fn default() -> Self { + PasswordSource::Undefined + } +} + +impl PasswordSource { + // Whether the password is personal ab password + pub fn is_personal_ab(&self, password: &[u8]) -> bool { + if password.is_empty() { + return false; + } + match self { + PasswordSource::PersonalAb(p) => p == password, + _ => false, + } + } + + // Whether the password is shared ab password + pub fn is_shared_ab(&self, password: &[u8], hash: &Hash) -> bool { + if password.is_empty() { + return false; + } + match self { + PasswordSource::SharedAb(p) => Self::equal(p, password, hash), + _ => false, + } + } + + // Whether the password equals to the connected password + fn equal(password: &str, connected_password: &[u8], hash: &Hash) -> bool { + let mut hasher = Sha256::new(); + hasher.update(password); + hasher.update(&hash.salt); + let res = hasher.finalize(); + connected_password[..] == res[..] + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct ConnToken { + password: Vec, + password_source: PasswordSource, + session_id: u64, +} + +/// Login config handler for [`Client`]. +#[derive(Default)] +pub struct LoginConfigHandler { + id: String, + pub conn_type: ConnType, + pub is_terminal_admin: bool, + hash: Hash, + password: Vec, // remember password for reconnect + pub remember: bool, + config: PeerConfig, + pub port_forward: (String, i32), + pub version: i64, + features: Option, + pub session_id: u64, // used for local <-> server communication + pub supported_encoding: SupportedEncoding, + pub restarting_remote_device: bool, + pub force_relay: bool, + pub direct: Option, + pub received: bool, + switch_uuid: Option, + pub save_ab_password_to_recent: bool, // true: connected with ab password + pub other_server: Option<(String, String, String)>, + pub custom_fps: Arc>>, + pub last_auto_fps: Option, + pub adapter_luid: Option, + pub mark_unsupported: Vec, + pub selected_windows_session_id: Option, + pub peer_info: Option, + password_source: PasswordSource, // where the sent password comes from + shared_password: Option, // Store the shared password + pub enable_trusted_devices: bool, + pub record_state: bool, + pub record_permission: bool, +} + +impl Deref for LoginConfigHandler { + type Target = PeerConfig; + + fn deref(&self) -> &Self::Target { + &self.config + } +} + +impl LoginConfigHandler { + /// Initialize the login config handler. + /// + /// # Arguments + /// + /// * `id` - id of peer + /// * `conn_type` - Connection type enum. + pub fn initialize( + &mut self, + id: String, + conn_type: ConnType, + switch_uuid: Option, + mut force_relay: bool, + adapter_luid: Option, + shared_password: Option, + conn_token: Option, + ) { + let mut id = id; + if id.contains("@") { + let mut v = id.split("@"); + let raw_id: &str = v.next().unwrap_or_default(); + let mut server_key = v.next().unwrap_or_default().split('?'); + let server = server_key.next().unwrap_or_default(); + let args = server_key.next().unwrap_or_default(); + let key = if server == PUBLIC_SERVER { + config::RS_PUB_KEY.to_owned() + } else { + let mut args_map: HashMap = HashMap::new(); + for arg in args.split('&') { + if let Some(kv) = arg.find('=') { + let k = arg[0..kv].to_lowercase(); + let v = &arg[kv + 1..]; + args_map.insert(k, v); + } + } + let key = args_map.remove("key").unwrap_or_default(); + key.to_owned() + }; + + // here we can check /r@server + let real_id = crate::ui_interface::handle_relay_id(raw_id).to_string(); + if real_id != raw_id { + force_relay = true; + } + self.other_server = Some((real_id.clone(), server.to_owned(), key)); + id = format!("{real_id}@{server}"); + } else { + let real_id = crate::ui_interface::handle_relay_id(&id); + if real_id != id { + force_relay = true; + id = real_id.to_owned(); + } + } + + self.id = id; + self.conn_type = conn_type; + let config = self.load_config(); + self.remember = !config.password.is_empty(); + self.config = config; + + let conn_token = conn_token + .map(|x| serde_json::from_str::(&x).ok()) + .flatten(); + let mut sid = 0; + if let Some(token) = conn_token { + sid = token.session_id; + self.password = token.password; // use as last password + self.password_source = token.password_source; + } + if sid == 0 { + sid = rand::random(); + if sid == 0 { + // you won the lottery + sid = 1; + } + } + self.session_id = sid; + self.supported_encoding = Default::default(); + self.restarting_remote_device = false; + self.force_relay = + config::option2bool("force-always-relay", &self.get_option("force-always-relay")) + || force_relay + || use_ws() + || Config::is_proxy(); + if let Some((real_id, server, key)) = &self.other_server { + let other_server_key = self.get_option("other-server-key"); + if !other_server_key.is_empty() && key.is_empty() { + self.other_server = Some((real_id.to_owned(), server.to_owned(), other_server_key)); + } + } + + self.direct = None; + self.received = false; + self.switch_uuid = switch_uuid; + self.adapter_luid = adapter_luid; + self.selected_windows_session_id = None; + self.shared_password = shared_password; + self.record_state = false; + self.record_permission = true; + + // `std::env::remove_var("IS_TERMINAL_ADMIN");` is called in `session_add_sync()` - `flutter_ffi.rs`. + let is_terminal_admin = conn_type == ConnType::TERMINAL + && std::env::var("IS_TERMINAL_ADMIN").map_or(false, |v| v == "Y"); + self.is_terminal_admin = is_terminal_admin; + } + + /// Check if the client should auto login. + /// Return password if the client should auto login, otherwise return empty string. + pub fn should_auto_login(&self) -> String { + let l = self.lock_after_session_end.v; + let a = !self.get_option("auto-login").is_empty(); + let p = self.get_option("os-password"); + if !p.is_empty() && l && a { + p + } else { + "".to_owned() + } + } + + /// Load [`PeerConfig`]. + pub fn load_config(&self) -> PeerConfig { + debug_assert!(self.id.len() > 0); + PeerConfig::load(&self.id) + } + + /// Save a [`PeerConfig`] into the handler. + /// + /// # Arguments + /// + /// * `config` - [`PeerConfig`] to save. + pub fn save_config(&mut self, config: PeerConfig) { + config.store(&self.id); + self.config = config; + } + + /// Set an option for handler's [`PeerConfig`]. + /// + /// # Arguments + /// + /// * `k` - key of option + /// * `v` - value of option + pub fn set_option(&mut self, k: String, v: String) { + let mut config = self.load_config(); + if v == self.get_option(&k) { + return; + } + config.options.insert(k, v); + self.save_config(config); + } + + //to-do: too many dup code below. + + /// Save view style to the current config. + /// + /// # Arguments + /// + /// * `value` - The view style to be saved. + pub fn save_view_style(&mut self, value: String) { + let mut config = self.load_config(); + config.view_style = value; + self.save_config(config); + } + + /// Save keyboard mode to the current config. + /// + /// # Arguments + /// + /// * `value` - The view style to be saved. + pub fn save_keyboard_mode(&mut self, value: String) { + let mut config = self.load_config(); + config.keyboard_mode = value; + self.save_config(config); + } + + /// Save reverse mouse wheel ("", "Y") to the current config. + /// + /// # Arguments + /// + /// * `value` - The reverse mouse wheel ("", "Y"). + pub fn save_reverse_mouse_wheel(&mut self, value: String) { + let mut config = self.load_config(); + config.reverse_mouse_wheel = value; + self.save_config(config); + } + + /// Save "displays_as_individual_windows" ("", "Y") to the current config. + /// + /// # Arguments + /// + /// * `value` - The "displays_as_individual_windows" value ("", "Y"). + pub fn save_displays_as_individual_windows(&mut self, value: String) { + let mut config = self.load_config(); + config.displays_as_individual_windows = value; + self.save_config(config); + } + + /// Save "use_all_my_displays_for_the_remote_session" ("", "Y") to the current config. + /// + /// # Arguments + /// + /// * `value` - The "use_all_my_displays_for_the_remote_session" value ("", "Y"). + pub fn save_use_all_my_displays_for_the_remote_session(&mut self, value: String) { + let mut config = self.load_config(); + config.use_all_my_displays_for_the_remote_session = value; + self.save_config(config); + } + + /// Save scroll style to the current config. + /// + /// # Arguments + /// + /// * `value` - The scroll style to be saved. + pub fn save_scroll_style(&mut self, value: String) { + let mut config = self.load_config(); + config.scroll_style = value; + self.save_config(config); + } + + /// Save edge scroll edge thickness to the current config. + /// + /// # Arguments + /// + /// * `value` - The edge thickness to be saved. + pub fn save_edge_scroll_edge_thickness(&mut self, value: i32) { + let mut config = self.load_config(); + config.edge_scroll_edge_thickness = value; + self.save_config(config); + } + + /// Set a ui config of flutter for handler's [`PeerConfig`]. + /// + /// # Arguments + /// + /// * `k` - key of option + /// * `v` - value of option + pub fn save_ui_flutter(&mut self, k: String, v: String) { + let mut config = self.load_config(); + if v.is_empty() { + config.ui_flutter.remove(&k); + } else { + config.ui_flutter.insert(k, v); + } + self.save_config(config); + } + + pub fn set_direct_failure(&mut self, value: i32) { + let mut config = self.load_config(); + config.direct_failures = value; + self.save_config(config); + } + + /// Get a ui config of flutter for handler's [`PeerConfig`]. + /// Return String if the option is found, otherwise return "". + /// + /// # Arguments + /// + /// * `k` - key of option + pub fn get_ui_flutter(&self, k: &str) -> String { + if let Some(v) = self.config.ui_flutter.get(k) { + v.clone() + } else { + "".to_owned() + } + } + + /// Toggle an option in the handler. + /// + /// # Arguments + /// + /// * `name` - The name of the option to toggle. + /// + // It's Ok to check the option empty in this function. + // `toggle_option()` is only called in a session. + // Custom client advanced settings will not effect this function. + pub fn toggle_option(&mut self, name: String) -> Option { + let mut option = OptionMessage::default(); + let mut config = self.load_config(); + if name == "show-remote-cursor" { + config.show_remote_cursor.v = !config.show_remote_cursor.v; + option.show_remote_cursor = (if config.show_remote_cursor.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "follow-remote-cursor" { + config.follow_remote_cursor.v = !config.follow_remote_cursor.v; + option.follow_remote_cursor = (if config.follow_remote_cursor.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "follow-remote-window" { + config.follow_remote_window.v = !config.follow_remote_window.v; + option.follow_remote_window = (if config.follow_remote_window.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "disable-audio" { + config.disable_audio.v = !config.disable_audio.v; + option.disable_audio = (if config.disable_audio.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "disable-clipboard" { + config.disable_clipboard.v = !config.disable_clipboard.v; + option.disable_clipboard = (if config.disable_clipboard.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "lock-after-session-end" { + config.lock_after_session_end.v = !config.lock_after_session_end.v; + option.lock_after_session_end = (if config.lock_after_session_end.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == keys::OPTION_TERMINAL_PERSISTENT { + config.terminal_persistent.v = !config.terminal_persistent.v; + option.terminal_persistent = (if config.terminal_persistent.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "privacy-mode" { + // try toggle privacy mode + option.privacy_mode = (if config.privacy_mode.v { + BoolOption::No + } else { + BoolOption::Yes + }) + .into(); + } else if name == "enable-file-copy-paste" { + config.enable_file_copy_paste.v = !config.enable_file_copy_paste.v; + option.enable_file_transfer = (if config.enable_file_copy_paste.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "block-input" { + option.block_input = BoolOption::Yes.into(); + } else if name == "unblock-input" { + option.block_input = BoolOption::No.into(); + } else if name == "show-quality-monitor" { + config.show_quality_monitor.v = !config.show_quality_monitor.v; + } else if name == "allow_swap_key" { + config.allow_swap_key.v = !config.allow_swap_key.v; + } else if name == "view-only" { + config.view_only.v = !config.view_only.v; + let f = |b: bool| { + if b { + BoolOption::Yes.into() + } else { + BoolOption::No.into() + } + }; + if config.view_only.v { + option.disable_keyboard = f(true); + option.disable_clipboard = f(true); + option.show_remote_cursor = f(true); + option.enable_file_transfer = f(false); + option.lock_after_session_end = f(false); + } else { + option.disable_keyboard = f(false); + option.disable_clipboard = f(self.get_toggle_option("disable-clipboard")); + option.show_remote_cursor = f(self.get_toggle_option("show-remote-cursor")); + option.enable_file_transfer = f(self.config.enable_file_copy_paste.v); + option.lock_after_session_end = f(self.config.lock_after_session_end.v); + if config.show_my_cursor.v { + config.show_my_cursor.v = false; + option.show_my_cursor = BoolOption::No.into(); + } + } + } else if name == "show-my-cursor" { + config.show_my_cursor.v = !config.show_my_cursor.v; + option.show_my_cursor = if config.show_my_cursor.v { + BoolOption::Yes + } else { + BoolOption::No + } + .into(); + } else { + let is_set = self + .options + .get(&name) + .map(|o| !o.is_empty()) + .unwrap_or(false); + if is_set { + self.config.options.remove(&name); + } else { + self.config.options.insert(name, "Y".to_owned()); + } + self.config.store(&self.id); + return None; + } + + #[cfg(feature = "unix-file-copy-paste")] + if option.enable_file_transfer.enum_value() == Ok(BoolOption::No) { + crate::clipboard::try_empty_clipboard_files(crate::clipboard::ClipboardSide::Client, 0); + } + + if !name.contains("block-input") { + self.save_config(config); + } + let mut misc = Misc::new(); + misc.set_option(option); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + Some(msg_out) + } + + /// Get [`PeerConfig`] of the current [`LoginConfigHandler`]. + /// + /// # Arguments + pub fn get_config(&mut self) -> &mut PeerConfig { + &mut self.config + } + + /// Get [`OptionMessage`] of the current [`LoginConfigHandler`]. + /// Return `None` if there's no option, for example, when the session is only for file transfer. + /// + /// # Arguments + /// + /// * `ignore_default` - If `true`, ignore the default value of the option. + fn get_option_message(&self, ignore_default: bool) -> Option { + if self.conn_type.eq(&ConnType::PORT_FORWARD) + || self.conn_type.eq(&ConnType::RDP) + || self.conn_type.eq(&ConnType::FILE_TRANSFER) + { + return None; + } + let mut msg = OptionMessage::new(); + if self.conn_type.eq(&ConnType::TERMINAL) { + if self.get_toggle_option(keys::OPTION_TERMINAL_PERSISTENT) { + msg.terminal_persistent = BoolOption::Yes.into(); + return Some(msg); + } else { + return None; + } + } + let q = self.image_quality.clone(); + if let Some(q) = self.get_image_quality_enum(&q, ignore_default) { + msg.image_quality = q.into(); + } else if q == "custom" { + let config = self.load_config(); + let allow_more = !crate::using_public_server() || self.direct == Some(true); + let quality = if config.custom_image_quality.is_empty() { + 50 + } else { + let mut quality = config.custom_image_quality[0]; + if !allow_more && quality > 100 { + quality = 50; + } + quality + }; + msg.custom_image_quality = quality << 8; + #[cfg(feature = "flutter")] + if let Some(custom_fps) = self.options.get("custom-fps") { + let mut custom_fps = custom_fps.parse().unwrap_or(30); + if !allow_more && custom_fps > 30 { + custom_fps = 30; + } + msg.custom_fps = custom_fps; + *self.custom_fps.lock().unwrap() = Some(custom_fps as _); + } + } + let view_only = self.get_toggle_option("view-only"); + if view_only { + msg.disable_keyboard = BoolOption::Yes.into(); + } + if view_only || self.get_toggle_option("show-remote-cursor") { + msg.show_remote_cursor = BoolOption::Yes.into(); + } + if view_only && self.get_toggle_option("show-my-cursor") { + msg.show_my_cursor = BoolOption::Yes.into(); + } + if self.get_toggle_option("follow-remote-cursor") { + msg.follow_remote_cursor = BoolOption::Yes.into(); + } + if self.get_toggle_option("follow-remote-window") { + msg.follow_remote_window = BoolOption::Yes.into(); + } + if !view_only && self.get_toggle_option("lock-after-session-end") { + msg.lock_after_session_end = BoolOption::Yes.into(); + } + if self.get_toggle_option("disable-audio") { + msg.disable_audio = BoolOption::Yes.into(); + } + if !view_only && self.get_toggle_option(keys::OPTION_ENABLE_FILE_COPY_PASTE) { + msg.enable_file_transfer = BoolOption::Yes.into(); + } + if view_only || self.get_toggle_option("disable-clipboard") { + msg.disable_clipboard = BoolOption::Yes.into(); + } + msg.supported_decoding = MessageField::some(self.get_supported_decoding()); + Some(msg) + } + + pub fn get_supported_decoding(&self) -> SupportedDecoding { + Decoder::supported_decodings( + Some(&self.id), + use_texture_render(), + self.adapter_luid, + &self.mark_unsupported, + ) + } + + /// Parse the image quality option. + /// Return [`ImageQuality`] if the option is valid, otherwise return `None`. + /// + /// # Arguments + /// + /// * `q` - The image quality option. + /// * `ignore_default` - Ignore the default value. + fn get_image_quality_enum(&self, q: &str, ignore_default: bool) -> Option { + if q == "low" { + Some(ImageQuality::Low) + } else if q == "best" { + Some(ImageQuality::Best) + } else if q == "balanced" { + if ignore_default { + None + } else { + Some(ImageQuality::Balanced) + } + } else { + None + } + } + + /// Get the status of a toggle option. + /// + /// # Arguments + /// + /// * `name` - The name of the toggle option. + /// + // It's Ok to check the option empty in this function. + // `get_toggle_option()` is only called in a session. + // Custom client advanced settings will not effect this function. + pub fn get_toggle_option(&self, name: &str) -> bool { + if name == "show-remote-cursor" { + self.config.show_remote_cursor.v + } else if name == "lock-after-session-end" { + self.config.lock_after_session_end.v + } else if name == keys::OPTION_TERMINAL_PERSISTENT { + self.config.terminal_persistent.v + } else if name == "privacy-mode" { + self.config.privacy_mode.v + } else if name == keys::OPTION_ENABLE_FILE_COPY_PASTE { + self.config.enable_file_copy_paste.v + } else if name == "disable-audio" { + self.config.disable_audio.v + } else if name == "disable-clipboard" { + self.config.disable_clipboard.v + } else if name == "show-quality-monitor" { + self.config.show_quality_monitor.v + } else if name == "allow_swap_key" { + self.config.allow_swap_key.v + } else if name == "view-only" { + self.config.view_only.v + } else if name == "show-my-cursor" { + self.config.show_my_cursor.v + } else if name == "follow-remote-cursor" { + self.config.follow_remote_cursor.v + } else if name == "follow-remote-window" { + self.config.follow_remote_window.v + } else { + !self.get_option(name).is_empty() + } + } + + pub fn is_privacy_mode_supported(&self) -> bool { + if let Some(features) = &self.features { + features.privacy_mode + } else { + false + } + } + + /// Create a [`Message`] for refreshing video. + pub fn refresh() -> Message { + let mut misc = Misc::new(); + misc.set_refresh_video(true); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out + } + + /// Create a [`Message`] for refreshing video. + pub fn refresh_display(display: usize) -> Message { + let mut misc = Misc::new(); + misc.set_refresh_video_display(display as _); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out + } + + /// Create a [`Message`] for saving custom image quality. + /// + /// # Arguments + /// + /// * `bitrate` - The given bitrate. + /// * `quantizer` - The given quantizer. + pub fn save_custom_image_quality(&mut self, image_quality: i32) -> Message { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + custom_image_quality: image_quality << 8, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + let mut config = self.load_config(); + config.image_quality = "custom".to_owned(); + config.custom_image_quality = vec![image_quality as _]; + self.save_config(config); + msg_out + } + + /// Save the given image quality to the config. + /// Return a [`Message`] that contains image quality, or `None` if the image quality is not valid. + /// # Arguments + /// + /// * `value` - The image quality. + pub fn save_image_quality(&mut self, value: String) -> Option { + let mut res = None; + if let Some(q) = self.get_image_quality_enum(&value, false) { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + image_quality: q.into(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + res = Some(msg_out); + } + let mut config = self.load_config(); + config.image_quality = value; + self.save_config(config); + res + } + + pub fn save_trackpad_speed(&mut self, speed: i32) { + let mut config = self.load_config(); + config.trackpad_speed = speed; + self.save_config(config); + } + + /// Create a [`Message`] for saving custom fps. + /// + /// # Arguments + /// + /// * `fps` - The given fps. + /// * `save_config` - Save the config. + pub fn set_custom_fps(&mut self, fps: i32, save_config: bool) -> Message { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + custom_fps: fps, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + if save_config { + let mut config = self.load_config(); + config + .options + .insert("custom-fps".to_owned(), fps.to_string()); + self.save_config(config); + } + *self.custom_fps.lock().unwrap() = Some(fps as _); + msg_out + } + + pub fn get_option(&self, k: &str) -> String { + if let Some(v) = self.config.options.get(k) { + v.clone() + } else { + "".to_owned() + } + } + + #[inline] + pub fn get_custom_resolution(&self, display: i32) -> Option<(i32, i32)> { + self.config + .custom_resolutions + .get(&display.to_string()) + .map(|r| (r.w, r.h)) + } + + #[inline] + pub fn set_custom_resolution(&mut self, display: i32, wh: Option<(i32, i32)>) { + let display = display.to_string(); + let mut config = self.load_config(); + match wh { + Some((w, h)) => { + config + .custom_resolutions + .insert(display, Resolution { w, h }); + } + None => { + config.custom_resolutions.remove(&display); + } + } + self.save_config(config); + } + + /// Get user name. + /// Return the name of the given peer. If the peer has no name, return the name in the config. + /// + /// # Arguments + /// + /// * `pi` - peer info. + pub fn get_username(&self, pi: &PeerInfo) -> String { + return if pi.username.is_empty() { + self.info.username.clone() + } else { + pi.username.clone() + }; + } + + /// Handle peer info. + /// + /// # Arguments + /// + /// * `username` - The name of the peer. + /// * `pi` - The peer info. + pub fn handle_peer_info(&mut self, pi: &PeerInfo) { + if !pi.version.is_empty() { + self.version = hbb_common::get_version_number(&pi.version); + } + self.features = pi.features.clone().into_option(); + let serde = PeerInfoSerde { + username: pi.username.clone(), + hostname: pi.hostname.clone(), + platform: pi.platform.clone(), + }; + let mut config = self.load_config(); + config.info = serde; + let password = self.password.clone(); + let password0 = config.password.clone(); + let remember = self.remember; + let hash = self.hash.clone(); + if remember { + // remember is true: use PeerConfig password or ui login + // not sync shared password to recent + if !password.is_empty() + && password != password0 + && !self.password_source.is_shared_ab(&password, &hash) + { + config.password = password.clone(); + log::debug!("remember password of {}", self.id); + } + } else { + if self.password_source.is_personal_ab(&password) { + // sync personal ab password to recent automatically + config.password = password.clone(); + log::debug!("save ab password of {} to recent", self.id); + } else if !password0.is_empty() { + config.password = Default::default(); + log::debug!("remove password of {}", self.id); + } + } + if let Some((_, b, c)) = self.other_server.as_ref() { + if b != PUBLIC_SERVER { + config + .options + .insert("other-server-key".to_owned(), c.clone()); + } + } + if self.force_relay { + config + .options + .insert("force-always-relay".to_owned(), "Y".to_owned()); + } + #[cfg(feature = "flutter")] + { + // sync connected password to personal ab automatically if it is not shared password + if !config.password.is_empty() + && !self.password_source.is_shared_ab(&password, &hash) + && !self.password_source.is_personal_ab(&password) + { + let hash = base64::encode(config.password.clone(), base64::Variant::Original); + let evt: HashMap<&str, String> = HashMap::from([ + ("name", "sync_peer_hash_password_to_personal_ab".to_string()), + ("id", self.id.clone()), + ("hash", hash), + ]); + let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned()); + crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt); + } + } + if config.keyboard_mode.is_empty() { + if is_keyboard_mode_supported( + &KeyboardMode::Map, + get_version_number(&pi.version), + &pi.platform, + ) { + config.keyboard_mode = KeyboardMode::Map.to_string(); + } else { + config.keyboard_mode = KeyboardMode::Legacy.to_string(); + } + } else { + let keyboard_modes = + crate::get_supported_keyboard_modes(get_version_number(&pi.version), &pi.platform); + let current_mode = &KeyboardMode::from_str(&config.keyboard_mode).unwrap_or_default(); + if !keyboard_modes.contains(current_mode) { + config.keyboard_mode = KeyboardMode::Legacy.to_string(); + } + } + // no matter if change, for update file time + self.save_config(config); + self.supported_encoding = pi.encoding.clone().unwrap_or_default(); + log::info!("peer info supported_encoding:{:?}", self.supported_encoding); + } + + pub fn get_remote_dir(&self) -> String { + serde_json::from_str::>(&self.get_option("remote_dir")) + .unwrap_or_default() + .remove(&self.info.username) + .unwrap_or_default() + } + + pub fn get_all_remote_dir(&self, path: String) -> String { + let d = self.get_option("remote_dir"); + let user = self.info.username.clone(); + let mut x = serde_json::from_str::>(&d).unwrap_or_default(); + if path.is_empty() { + x.remove(&user); + } else { + x.insert(user, path); + } + serde_json::to_string::>(&x).unwrap_or_default() + } + + /// Create a [`Message`] for login. + fn create_login_msg( + &self, + os_username: String, + os_password: String, + password: Vec, + ) -> Message { + #[cfg(any(target_os = "android", target_os = "ios"))] + let my_id = Config::get_id_or(crate::DEVICE_ID.lock().unwrap().clone()); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let my_id = Config::get_id(); + let (my_id, pure_id) = if let Some((id, _, _)) = self.other_server.as_ref() { + let server = Config::get_rendezvous_server(); + (format!("{my_id}@{server}"), id.clone()) + } else { + (my_id, self.id.clone()) + }; + let mut avatar = get_builtin_option(keys::OPTION_AVATAR); + if avatar.is_empty() { + avatar = serde_json::from_str::(&LocalConfig::get_option( + "user_info", + )) + .ok() + .and_then(|x| { + x.get("avatar") + .and_then(|x| x.as_str()) + .map(|x| x.trim().to_owned()) + }) + .unwrap_or_default(); + } + avatar = resolve_avatar_url(avatar); + let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME); + if display_name.is_empty() { + display_name = + serde_json::from_str::(&LocalConfig::get_option("user_info")) + .map(|x| { + x.get("display_name") + .and_then(|x| x.as_str()) + .map(|x| x.trim()) + .filter(|x| !x.is_empty()) + .or_else(|| x.get("name").and_then(|x| x.as_str())) + .map(|x| x.to_owned()) + .unwrap_or_default() + }) + .unwrap_or_default(); + } + if display_name.is_empty() { + display_name = crate::username(); + } + let display_name = display_name + .split_whitespace() + .map(|word| { + word.chars() + .enumerate() + .map(|(i, c)| { + if i == 0 { + c.to_uppercase().to_string() + } else { + c.to_string() + } + }) + .collect::() + }) + .collect::>() + .join(" "); + #[cfg(not(target_os = "android"))] + let my_platform = hbb_common::whoami::platform().to_string(); + #[cfg(target_os = "android")] + let my_platform = "Android".into(); + let hwid = if self.get_option("trust-this-device") == "Y" { + crate::get_hwid() + } else { + Bytes::new() + }; + let mut lr = LoginRequest { + username: pure_id, + password: password.into(), + my_id, + my_name: display_name, + my_platform, + option: self.get_option_message(true).into(), + session_id: self.session_id, + version: crate::VERSION.to_string(), + os_login: Some(OSLogin { + username: os_username, + password: os_password, + ..Default::default() + }) + .into(), + hwid, + avatar, + ..Default::default() + }; + match self.conn_type { + ConnType::FILE_TRANSFER => lr.set_file_transfer(FileTransfer { + dir: self.get_remote_dir(), + show_hidden: !self.get_option("remote_show_hidden").is_empty(), + ..Default::default() + }), + ConnType::VIEW_CAMERA => lr.set_view_camera(Default::default()), + ConnType::PORT_FORWARD | ConnType::RDP => lr.set_port_forward(PortForward { + host: self.port_forward.0.clone(), + port: self.port_forward.1, + ..Default::default() + }), + ConnType::TERMINAL => { + let mut terminal = Terminal::new(); + terminal.service_id = self.get_option(self.get_key_terminal_service_id()); + lr.set_terminal(terminal); + } + _ => {} + } + + let mut msg_out = Message::new(); + msg_out.set_login_request(lr); + msg_out + } + + pub fn update_supported_decodings(&self) -> Message { + let decoding = scrap::codec::Decoder::supported_decodings( + Some(&self.id), + use_texture_render(), + self.adapter_luid, + &self.mark_unsupported, + ); + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + supported_decoding: hbb_common::protobuf::MessageField::some(decoding), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out + } + + pub fn restart_remote_device(&self) -> Message { + let mut misc = Misc::new(); + misc.set_restart_remote_device(true); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out + } + + pub fn get_conn_token(&self) -> Option { + if self.password.is_empty() { + return None; + } + serde_json::to_string(&ConnToken { + password: self.password.clone(), + password_source: self.password_source.clone(), + session_id: self.session_id, + }) + .ok() + } + + pub fn get_id(&self) -> &str { + &self.id + } + + pub fn get_key_terminal_service_id(&self) -> &'static str { + if self.is_terminal_admin { + "terminal-admin-service-id" + } else { + "terminal-service-id" + } + } +} + +/// Media data. +pub enum MediaData { + VideoQueue, + VideoFrame(Box), + AudioFrame(Box), + AudioFormat(AudioFormat), + Reset, + RecordScreen(bool), +} + +pub type MediaSender = mpsc::Sender; + +/// Start video thread. +/// +/// # Arguments +/// +/// * `video_callback` - The callback for video frame. Being called when a video frame is ready. +pub fn start_video_thread( + session: Session, + display: usize, + video_receiver: mpsc::Receiver, + video_queue: Arc>>, + fps: Arc>>, + chroma: Arc>>, + discard_queue: Arc>, + video_callback: F, +) where + F: 'static + FnMut(usize, &mut scrap::ImageRgb, *mut c_void, bool) + Send, + T: InvokeUiSession, +{ + let mut video_callback = video_callback; + let mut last_chroma = None; + let is_view_camera = session.is_view_camera(); + + std::thread::spawn(move || { + #[cfg(windows)] + sync_cpu_usage(); + get_hwcodec_config(); + let mut video_handler = None; + let mut count = 0; + let mut duration = std::time::Duration::ZERO; + let mut skip_beginning = 0; + loop { + if let Ok(data) = video_receiver.recv() { + match data { + MediaData::VideoFrame(_) | MediaData::VideoQueue => { + let vf = match data { + MediaData::VideoFrame(vf) => { + *discard_queue.write().unwrap() = false; + *vf + } + MediaData::VideoQueue => { + if let Some(vf) = video_queue.read().unwrap().pop() { + if discard_queue.read().unwrap().clone() { + continue; + } + vf + } else { + continue; + } + } + _ => { + // unreachable!(); + continue; + } + }; + let display = vf.display as usize; + let start = std::time::Instant::now(); + let format = CodecFormat::from(&vf); + if video_handler.is_none() { + let mut handler = VideoHandler::new(format, display); + let record_state = session.lc.read().unwrap().record_state; + let record_permission = session.lc.read().unwrap().record_permission; + let id = session.lc.read().unwrap().id.clone(); + if record_state && record_permission { + handler.record_screen(true, id, display, is_view_camera); + } + video_handler = Some(handler); + } + if let Some(handler) = video_handler.as_mut() { + let mut pixelbuffer = true; + let mut tmp_chroma = None; + let format_changed = handler.decoder.format() != format; + match handler.handle_frame(vf, &mut pixelbuffer, &mut tmp_chroma) { + Ok(true) => { + video_callback( + display, + &mut handler.rgb, + handler.texture.texture, + pixelbuffer, + ); + + // chroma + if tmp_chroma.is_some() && last_chroma != tmp_chroma { + last_chroma = tmp_chroma; + *chroma.write().unwrap() = tmp_chroma; + } + + // fps calculation + fps_calculate( + &mut skip_beginning, + &fps, + format_changed, + start.elapsed(), + &mut count, + &mut duration, + ); + } + Err(e) => { + // This is a simple workaround. + // + // I only see the following error: + // FailedCall("errcode=1 scrap::common::vpxcodec:libs\\scrap\\src\\common\\vpxcodec.rs:433:9") + // When switching from all displays to one display, the error occurs. + // eg: + // 1. Connect to a device with two displays (A and B). + // 2. Switch to display A. The error occurs. + // 3. If the error does not occur. Switch from A to display B. The error occurs. + // + // to-do: fix the error + log::error!("handle video frame error, {}", e); + session.refresh_video(display as _); + } + _ => {} + } + } + + // check invalid decoders + let mut should_update_supported = false; + if let Some(handler) = video_handler.as_mut() { + if !handler.decoder.valid() + || handler.fail_counter >= MAX_DECODE_FAIL_COUNTER + { + let mut lc = session.lc.write().unwrap(); + let format = handler.decoder.format(); + if !lc.mark_unsupported.contains(&format) { + lc.mark_unsupported.push(format); + should_update_supported = true; + log::info!("mark {format:?} decoder as unsupported, valid:{}, fail_counter:{}, all unsupported:{:?}", handler.decoder.valid(), handler.fail_counter, lc.mark_unsupported); + } + } + } + if should_update_supported { + session.send(Data::Message( + session.lc.read().unwrap().update_supported_decodings(), + )); + } + } + MediaData::Reset => { + if let Some(handler) = video_handler.as_mut() { + handler.reset(None); + } + } + MediaData::RecordScreen(start) => { + let id = session.lc.read().unwrap().id.clone(); + if let Some(handler) = video_handler.as_mut() { + handler.record_screen(start, id, display, is_view_camera); + } + } + _ => {} + } + } else { + break; + } + } + log::info!("Video decoder loop exits"); + }); +} + +/// Start an audio thread +/// Return a audio [`MediaSender`] +pub fn start_audio_thread() -> MediaSender { + let (audio_sender, audio_receiver) = mpsc::channel::(); + std::thread::spawn(move || { + let mut audio_handler = AudioHandler::default(); + loop { + if let Ok(data) = audio_receiver.recv() { + match data { + MediaData::AudioFrame(af) => { + audio_handler.handle_frame(*af); + } + MediaData::AudioFormat(f) => { + log::debug!("recved audio format, sample rate={}", f.sample_rate); + audio_handler.handle_format(f); + } + _ => {} + } + } else { + break; + } + } + log::info!("Audio decoder loop exits"); + }); + audio_sender +} + +#[inline] +fn fps_calculate( + skip_beginning: &mut usize, + fps: &Arc>>, + format_changed: bool, + elapsed: std::time::Duration, + count: &mut usize, + duration: &mut std::time::Duration, +) { + if format_changed { + *count = 0; + *duration = std::time::Duration::ZERO; + *skip_beginning = 0; + } + // // The first frame will be very slow + if *skip_beginning < 3 { + *skip_beginning += 1; + return; + } + *duration += elapsed; + *count += 1; + let ms = duration.as_millis(); + if *count % 10 == 0 && ms > 0 { + *fps.write().unwrap() = Some((*count as usize) * 1000 / (ms as usize)); + } + // Clear to get real-time fps + if *count >= 30 { + *count = 0; + *duration = Duration::ZERO; + } +} + +fn get_hwcodec_config() { + // for sciter and unilink + #[cfg(feature = "hwcodec")] + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + use std::sync::Once; + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + let start = std::time::Instant::now(); + if let Err(e) = crate::ipc::get_hwcodec_config_from_server() { + log::error!( + "Failed to get hwcodec config: {e:?}, elapsed: {:?}", + start.elapsed() + ); + } else { + log::info!("{:?} used to get hwcodec config", start.elapsed()); + } + }); + } +} + +#[cfg(windows)] +fn sync_cpu_usage() { + use std::sync::Once; + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + let t = std::thread::spawn(do_sync_cpu_usage); + t.join().ok(); + }); +} + +#[cfg(windows)] +#[tokio::main(flavor = "current_thread")] +async fn do_sync_cpu_usage() { + use crate::ipc::{connect, Data}; + let start = std::time::Instant::now(); + match connect(50, "").await { + Ok(mut conn) => { + if conn.send(&&Data::SyncWinCpuUsage(None)).await.is_ok() { + 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); + } + _ => {} + } + } + } + } + _ => {} + } + log::info!("{:?} used to sync cpu usage", start.elapsed()); +} + +/// Handle latency test. +/// +/// # Arguments +/// +/// * `t` - The latency test message. +/// * `peer` - The peer. +pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) { + if !t.from_client { + let mut msg_out = Message::new(); + msg_out.set_test_delay(t); + allow_err!(peer.send(&msg_out).await); + } +} + +/// Whether is track pad scrolling. +#[inline] +#[cfg(all(target_os = "macos", not(feature = "flutter")))] +fn check_scroll_on_mac(mask: i32, x: i32, y: i32) -> bool { + // flutter version we set mask type bit to 4 when track pad scrolling. + if mask & 7 == crate::input::MOUSE_TYPE_TRACKPAD { + return true; + } + if mask & 3 != crate::input::MOUSE_TYPE_WHEEL { + return false; + } + let btn = mask >> 3; + if y == -1 { + btn != 0xff88 && btn != -0x780000 + } else if y == 1 { + btn != 0x78 && btn != 0x780000 + } else if x != 0 { + // No mouse support horizontal scrolling. + true + } else { + false + } +} + +/// Send mouse data. +/// +/// # Arguments +/// +/// * `mask` - Mouse event. +/// * mask = buttons << 3 | type +/// * type, 1: down, 2: up, 3: wheel, 4: trackpad +/// * buttons, 1: left, 2: right, 4: middle +/// * `x` - X coordinate. +/// * `y` - Y coordinate. +/// * `alt` - Whether the alt key is pressed. +/// * `ctrl` - Whether the ctrl key is pressed. +/// * `shift` - Whether the shift key is pressed. +/// * `command` - Whether the command key is pressed. +/// * `interface` - The interface for sending data. +#[inline] +pub fn send_mouse( + mask: i32, + x: i32, + y: i32, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + interface: &impl Interface, +) { + let mut msg_out = Message::new(); + let mut mouse_event = MouseEvent { + mask, + x, + y, + ..Default::default() + }; + if alt { + mouse_event.modifiers.push(ControlKey::Alt.into()); + } + if shift { + mouse_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl { + mouse_event.modifiers.push(ControlKey::Control.into()); + } + if command { + mouse_event.modifiers.push(ControlKey::Meta.into()); + } + #[cfg(all(target_os = "macos", not(feature = "flutter")))] + if check_scroll_on_mac(mask, x, y) { + let factor = 3; + mouse_event.mask = crate::input::MOUSE_TYPE_TRACKPAD; + mouse_event.x *= factor; + mouse_event.y *= factor; + } + interface.swap_modifier_mouse(&mut mouse_event); + msg_out.set_mouse_event(mouse_event); + interface.send(Data::Message(msg_out)); +} + +#[inline] +pub fn send_pointer_device_event( + mut evt: PointerDeviceEvent, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + interface: &impl Interface, +) { + let mut msg_out = Message::new(); + if alt { + evt.modifiers.push(ControlKey::Alt.into()); + } + if shift { + evt.modifiers.push(ControlKey::Shift.into()); + } + if ctrl { + evt.modifiers.push(ControlKey::Control.into()); + } + if command { + evt.modifiers.push(ControlKey::Meta.into()); + } + msg_out.set_pointer_device_event(evt); + interface.send(Data::Message(msg_out)); +} + +/// Activate OS by sending mouse movement. +/// +/// # Arguments +/// +/// * `interface` - The interface for sending data. +/// * `send_left_click` - Whether to send a click event. +fn activate_os(interface: &impl Interface, send_left_click: bool) { + let left_down = MOUSE_BUTTON_LEFT << 3 | MOUSE_TYPE_DOWN; + let left_up = MOUSE_BUTTON_LEFT << 3 | MOUSE_TYPE_UP; + let right_down = MOUSE_BUTTON_RIGHT << 3 | MOUSE_TYPE_DOWN; + let right_up = MOUSE_BUTTON_RIGHT << 3 | MOUSE_TYPE_UP; + send_mouse(left_up, 0, 0, false, false, false, false, interface); + std::thread::sleep(Duration::from_millis(50)); + send_mouse(0, 0, 0, false, false, false, false, interface); + std::thread::sleep(Duration::from_millis(50)); + send_mouse(0, 3, 3, false, false, false, false, interface); + let (click_down, click_up) = if send_left_click { + (left_down, left_up) + } else { + (right_down, right_up) + }; + std::thread::sleep(Duration::from_millis(50)); + send_mouse(click_down, 0, 0, false, false, false, false, interface); + send_mouse(click_up, 0, 0, false, false, false, false, interface); + /* + let mut key_event = KeyEvent::new(); + // do not use Esc, which has problem with Linux + key_event.set_control_key(ControlKey::RightArrow); + key_event.press = true; + let mut msg_out = Message::new(); + msg_out.set_key_event(key_event.clone()); + interface.send(Data::Message(msg_out.clone())); + */ +} + +/// Input the OS's password. +/// +/// # Arguments +/// +/// * `p` - The password. +/// * `activate` - Whether to activate OS. +/// * `interface` - The interface for sending data. +pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { + std::thread::spawn(move || { + _input_os_password(p, activate, interface); + }); +} + +/// Input the OS's password. +/// +/// # Arguments +/// +/// * `p` - The password. +/// * `activate` - Whether to activate OS. +/// * `interface` - The interface for sending data. +fn _input_os_password(p: String, activate: bool, interface: impl Interface) { + let input_password = !p.is_empty(); + if activate { + // Click event is used to bring up the password input box. + activate_os(&interface, input_password); + std::thread::sleep(Duration::from_millis(1200)); + } + if !input_password { + return; + } + let mut key_event = KeyEvent::new(); + key_event.mode = KeyboardMode::Legacy.into(); + key_event.press = true; + let mut msg_out = Message::new(); + key_event.set_seq(p); + msg_out.set_key_event(key_event.clone()); + interface.send(Data::Message(msg_out.clone())); + key_event.set_control_key(ControlKey::Return); + msg_out.set_key_event(key_event); + interface.send(Data::Message(msg_out)); +} + +#[derive(Copy, Clone)] +struct LoginErrorMsgBox { + msgtype: &'static str, + title: &'static str, + text: &'static str, + link: &'static str, + try_again: bool, +} + +lazy_static::lazy_static! { + static ref LOGIN_ERROR_MAP: Arc> = { + use config::LINK_HEADLESS_LINUX_SUPPORT; + let map = HashMap::from([(LOGIN_SCREEN_WAYLAND, LoginErrorMsgBox{ + msgtype: "error", + title: "Login Error", + text: "Login screen using Wayland is not supported", + link: "https://cstudio.ch/hello-agent/docs/en/manual/linux/#login-screen", + try_again: true, + }), (LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LoginErrorMsgBox{ + msgtype: "session-login", + title: "", + text: "", + link: "", + try_again: true, + }), (LOGIN_MSG_DESKTOP_XSESSION_FAILED, LoginErrorMsgBox{ + msgtype: "session-re-login", + title: "", + text: "", + link: "", + try_again: true, + }), (LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER, LoginErrorMsgBox{ + msgtype: "info-nocancel", + title: "another_user_login_title_tip", + text: "another_user_login_text_tip", + link: "", + try_again: false, + }), (LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, LoginErrorMsgBox{ + msgtype: "info-nocancel", + title: "xorg_not_found_title_tip", + text: "xorg_not_found_text_tip", + link: LINK_HEADLESS_LINUX_SUPPORT, + try_again: true, + }), (LOGIN_MSG_DESKTOP_NO_DESKTOP, LoginErrorMsgBox{ + msgtype: "info-nocancel", + title: "no_desktop_title_tip", + text: "no_desktop_text_tip", + link: LINK_HEADLESS_LINUX_SUPPORT, + try_again: true, + }), (LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY, LoginErrorMsgBox{ + msgtype: "session-login-password", + title: "", + text: "", + link: "", + try_again: true, + }), (LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG, LoginErrorMsgBox{ + msgtype: "session-login-re-password", + title: "", + text: "", + link: "", + try_again: true, + }), (LOGIN_MSG_NO_PASSWORD_ACCESS, LoginErrorMsgBox{ + msgtype: "wait-remote-accept-nook", + title: "Prompt", + text: "Please wait for the remote side to accept your session request...", + link: "", + try_again: true, + })]); + Arc::new(map) + }; +} + +/// Handle login error. +/// Return true if the password is wrong, return false if there's an actual error. +pub fn handle_login_error( + lc: Arc>, + err: &str, + interface: &impl Interface, +) -> bool { + if err == LOGIN_MSG_PASSWORD_EMPTY { + lc.write().unwrap().password = Default::default(); + interface.msgbox("input-password", "Password Required", "", ""); + true + } else if err == LOGIN_MSG_PASSWORD_WRONG { + lc.write().unwrap().password = Default::default(); + interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); + true + } else if err == LOGIN_MSG_2FA_WRONG || err == REQUIRE_2FA { + let enabled = lc.read().unwrap().get_option("trust-this-device") == "Y"; + if enabled { + lc.write() + .unwrap() + .set_option("trust-this-device".to_string(), "".to_string()); + } + interface.msgbox("input-2fa", err, "", ""); + true + } else if LOGIN_ERROR_MAP.contains_key(err) { + if let Some(msgbox_info) = LOGIN_ERROR_MAP.get(err) { + interface.msgbox( + msgbox_info.msgtype, + msgbox_info.title, + msgbox_info.text, + msgbox_info.link, + ); + msgbox_info.try_again + } else { + // unreachable! + false + } + } else { + if err.contains(SCRAP_X11_REQUIRED) { + interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL); + } else { + interface.msgbox("error", "Login Error", err, ""); + } + false + } +} + +/// Handle hash message sent by peer. +/// Hash will be used for login. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `hash` - Hash sent by peer. +/// * `interface` - [`Interface`] for sending data. +/// * `peer` - [`Stream`] for communicating with peer. +pub async fn handle_hash( + lc: Arc>, + password_preset: &str, + hash: Hash, + interface: &impl Interface, + peer: &mut Stream, +) { + lc.write().unwrap().hash = hash.clone(); + // Take care of password application order + + // switch_uuid + let uuid = lc.write().unwrap().switch_uuid.take(); + if let Some(uuid) = uuid { + if let Ok(uuid) = uuid::Uuid::from_str(&uuid) { + send_switch_login_request(lc.clone(), peer, uuid).await; + lc.write().unwrap().password_source = Default::default(); + return; + } + } + // last password + let mut password = lc.read().unwrap().password.clone(); + // preset password + if password.is_empty() { + if !password_preset.is_empty() { + let mut hasher = Sha256::new(); + hasher.update(password_preset); + hasher.update(&hash.salt); + let res = hasher.finalize(); + password = res[..].into(); + lc.write().unwrap().password_source = Default::default(); + } + } + // shared password + // Currently it's used only when click shared ab peer card + let shared_password = lc.write().unwrap().shared_password.take(); + if let Some(shared_password) = shared_password { + if !shared_password.is_empty() { + let mut hasher = Sha256::new(); + hasher.update(shared_password.clone()); + hasher.update(&hash.salt); + let res = hasher.finalize(); + password = res[..].into(); + lc.write().unwrap().password_source = PasswordSource::SharedAb(shared_password); + } + } + // peer config password + if password.is_empty() { + password = lc.read().unwrap().config.password.clone(); + if !password.is_empty() { + lc.write().unwrap().password_source = Default::default(); + } + } + // personal ab password + if password.is_empty() { + try_get_password_from_personal_ab(lc.clone(), &mut password); + } + + if password.is_empty() { + let p = crate::ui_interface::get_builtin_option(keys::OPTION_DEFAULT_CONNECT_PASSWORD); + if !p.is_empty() { + let mut hasher = Sha256::new(); + hasher.update(p.clone()); + hasher.update(&hash.salt); + let res = hasher.finalize(); + password = res[..].into(); + lc.write().unwrap().password_source = PasswordSource::SharedAb(p); // reuse SharedAb here + } + } + + lc.write().unwrap().password = password.clone(); + + let is_terminal_admin = lc.read().unwrap().is_terminal_admin; + let is_terminal = lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL); + if is_terminal && is_terminal_admin { + if password.is_empty() { + interface.msgbox("terminal-admin-login-password", "", "", ""); + } else { + interface.msgbox("terminal-admin-login", "", "", ""); + } + lc.write().unwrap().hash = hash; + return; + } + + let password = if password.is_empty() { + // login without password, the remote side can click accept + interface.msgbox("input-password", "Password Required", "", ""); + Vec::new() + } else { + let mut hasher = Sha256::new(); + hasher.update(&password); + hasher.update(&hash.challenge); + hasher.finalize()[..].into() + }; + + let is_terminal = lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL); + let (os_username, os_password) = if is_terminal { + ("".to_owned(), "".to_owned()) + } else { + ( + lc.read().unwrap().get_option("os-username"), + lc.read().unwrap().get_option("os-password"), + ) + }; + + send_login(lc.clone(), os_username, os_password, password, peer).await; + lc.write().unwrap().hash = hash; +} + +#[inline] +fn try_get_password_from_personal_ab(lc: Arc>, password: &mut Vec) { + let access_token = LocalConfig::get_option("access_token"); + let ab = config::Ab::load(); + if !access_token.is_empty() && access_token == ab.access_token { + let id = lc.read().unwrap().id.clone(); + if let Some(ab) = ab.ab_entries.iter().find(|a| a.personal()) { + if let Some(p) = ab + .peers + .iter() + .find_map(|p| if p.id == id { Some(p) } else { None }) + { + if let Ok(hash_password) = base64::decode(p.hash.clone(), base64::Variant::Original) + { + if !hash_password.is_empty() { + *password = hash_password.clone(); + lc.write().unwrap().password_source = + PasswordSource::PersonalAb(hash_password); + } + } + } + } + } +} + +/// Send login message to peer. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `os_username` - OS username. +/// * `os_password` - OS password. +/// * `password` - Password. +/// * `peer` - [`Stream`] for communicating with peer. +async fn send_login( + lc: Arc>, + os_username: String, + os_password: String, + password: Vec, + peer: &mut Stream, +) { + let msg_out = lc + .read() + .unwrap() + .create_login_msg(os_username, os_password, password); + allow_err!(peer.send(&msg_out).await); +} + +/// Handle login request made from ui. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `os_username` - OS username. +/// * `os_password` - OS password. +/// * `password` - Password. +/// * `remember` - Whether to remember password. +/// * `peer` - [`Stream`] for communicating with peer. +pub async fn handle_login_from_ui( + lc: Arc>, + os_username: String, + os_password: String, + password: String, + remember: bool, + peer: &mut Stream, +) { + let mut hash_password = if password.is_empty() { + let mut password2 = lc.read().unwrap().password.clone(); + if password2.is_empty() { + password2 = lc.read().unwrap().config.password.clone(); + if !password2.is_empty() { + lc.write().unwrap().password_source = Default::default(); + } + } + password2 + } else { + lc.write().unwrap().password_source = Default::default(); + let mut hasher = Sha256::new(); + hasher.update(password); + hasher.update(&lc.read().unwrap().hash.salt); + let res = hasher.finalize(); + lc.write().unwrap().remember = remember; + res[..].into() + }; + lc.write().unwrap().password = hash_password.clone(); + let mut hasher2 = Sha256::new(); + hasher2.update(&hash_password[..]); + hasher2.update(&lc.read().unwrap().hash.challenge); + hash_password = hasher2.finalize()[..].to_vec(); + + send_login(lc.clone(), os_username, os_password, hash_password, peer).await; +} + +async fn send_switch_login_request( + lc: Arc>, + peer: &mut Stream, + uuid: Uuid, +) { + let mut msg_out = Message::new(); + msg_out.set_switch_sides_response(SwitchSidesResponse { + uuid: Bytes::from(uuid.as_bytes().to_vec()), + lr: hbb_common::protobuf::MessageField::some( + lc.read() + .unwrap() + .create_login_msg("".to_owned(), "".to_owned(), vec![]) + .login_request() + .to_owned(), + ), + ..Default::default() + }); + allow_err!(peer.send(&msg_out).await); +} + +/// Interface for client to send data and commands. +#[async_trait] +pub trait Interface: Send + Clone + 'static + Sized { + /// Send message data to remote peer. + fn send(&self, data: Data); + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str); + fn handle_login_error(&self, err: &str) -> bool; + fn handle_peer_info(&self, pi: PeerInfo); + fn set_multiple_windows_session(&self, sessions: Vec); + fn on_error(&self, err: &str) { + self.msgbox("error", "Error", err, ""); + } + async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream); + async fn handle_login_from_ui( + &self, + os_username: String, + os_password: String, + password: String, + remember: bool, + peer: &mut Stream, + ); + async fn handle_test_delay(&self, t: TestDelay, peer: &mut Stream); + + fn get_lch(&self) -> Arc>; + + fn get_id(&self) -> String { + self.get_lch().read().unwrap().id.clone() + } + + fn is_force_relay(&self) -> bool { + self.get_lch().read().unwrap().force_relay + } + + fn swap_modifier_mouse(&self, _msg: &mut hbb_common::protos::message::MouseEvent) {} + + fn update_direct(&self, direct: Option) { + self.get_lch().write().unwrap().direct = direct; + } + + fn update_received(&self, received: bool) { + self.get_lch().write().unwrap().received = received; + } + + fn on_establish_connection_error(&self, err: String) { + let title = "Connection Error"; + let text = err.to_string(); + let lc = self.get_lch(); + let direct = lc.read().unwrap().direct; + let received = lc.read().unwrap().received; + + let mut relay_hint = false; + let mut relay_hint_type = "relay-hint"; + // force relay + let errno = errno::errno().0; + log::error!("Connection closed: {err}({errno})"); + if direct == Some(true) + && ((cfg!(windows) && (errno == 10054 || err.contains("10054"))) + || (!cfg!(windows) && (errno == 104 || err.contains("104"))) + || (!err.contains("Failed") && err.contains("deadline"))) + // deadline: https://github.com/rustdesk/rustdesk-server-pro/discussions/325, most likely comes from secure tcp timeout + { + relay_hint = true; + if !received { + relay_hint_type = "relay-hint2" + } + } + + // relay-hint + if cfg!(feature = "flutter") && relay_hint { + self.msgbox(relay_hint_type, title, &text, ""); + } else { + self.msgbox("error", title, &text, ""); + } + } +} + +/// Data used by the client interface. +#[derive(Clone)] +pub enum Data { + Close, + Login((String, String, String, bool)), + Message(Message), + SendFiles((i32, JobType, String, String, i32, bool, bool)), + RemoveDirAll((i32, String, bool, bool)), + ConfirmDeleteFiles((i32, i32)), + SetNoConfirm(i32), + RemoveDir((i32, String)), + RemoveFile((i32, String, i32, bool)), + CreateDir((i32, String, bool)), + CancelJob(i32), + RemovePortForward(i32), + AddPortForward((i32, String, i32)), + #[cfg(all(target_os = "windows", not(feature = "flutter")))] + ToggleClipboardFile, + NewRDP, + SetConfirmOverrideFile((i32, i32, bool, bool, bool)), + AddJob((i32, JobType, String, String, i32, bool, bool)), + ResumeJob((i32, bool)), + RecordScreen(bool), + ElevateDirect, + ElevateWithLogon(String, String), + NewVoiceCall, + CloseVoiceCall, + ResetDecoder(Option), + RenameFile((i32, String, String, bool)), + TakeScreenshot((i32, String)), +} + +/// Keycode for key events. +#[derive(Clone, Debug)] +pub enum Key { + ControlKey(ControlKey), + Chr(u32), + _Raw(u32), +} + +lazy_static::lazy_static! { + pub static ref KEY_MAP: HashMap<&'static str, Key> = + [ + ("VK_A", Key::Chr('a' as _)), + ("VK_B", Key::Chr('b' as _)), + ("VK_C", Key::Chr('c' as _)), + ("VK_D", Key::Chr('d' as _)), + ("VK_E", Key::Chr('e' as _)), + ("VK_F", Key::Chr('f' as _)), + ("VK_G", Key::Chr('g' as _)), + ("VK_H", Key::Chr('h' as _)), + ("VK_I", Key::Chr('i' as _)), + ("VK_J", Key::Chr('j' as _)), + ("VK_K", Key::Chr('k' as _)), + ("VK_L", Key::Chr('l' as _)), + ("VK_M", Key::Chr('m' as _)), + ("VK_N", Key::Chr('n' as _)), + ("VK_O", Key::Chr('o' as _)), + ("VK_P", Key::Chr('p' as _)), + ("VK_Q", Key::Chr('q' as _)), + ("VK_R", Key::Chr('r' as _)), + ("VK_S", Key::Chr('s' as _)), + ("VK_T", Key::Chr('t' as _)), + ("VK_U", Key::Chr('u' as _)), + ("VK_V", Key::Chr('v' as _)), + ("VK_W", Key::Chr('w' as _)), + ("VK_X", Key::Chr('x' as _)), + ("VK_Y", Key::Chr('y' as _)), + ("VK_Z", Key::Chr('z' as _)), + ("VK_0", Key::Chr('0' as _)), + ("VK_1", Key::Chr('1' as _)), + ("VK_2", Key::Chr('2' as _)), + ("VK_3", Key::Chr('3' as _)), + ("VK_4", Key::Chr('4' as _)), + ("VK_5", Key::Chr('5' as _)), + ("VK_6", Key::Chr('6' as _)), + ("VK_7", Key::Chr('7' as _)), + ("VK_8", Key::Chr('8' as _)), + ("VK_9", Key::Chr('9' as _)), + ("VK_COMMA", Key::Chr(',' as _)), + ("VK_SLASH", Key::Chr('/' as _)), + ("VK_SEMICOLON", Key::Chr(';' as _)), + ("VK_QUOTE", Key::Chr('\'' as _)), + ("VK_LBRACKET", Key::Chr('[' as _)), + ("VK_RBRACKET", Key::Chr(']' as _)), + ("VK_BACKSLASH", Key::Chr('\\' as _)), + ("VK_MINUS", Key::Chr('-' as _)), + ("VK_PLUS", Key::Chr('=' as _)), // it is =, but sciter return VK_PLUS + ("VK_DIVIDE", Key::ControlKey(ControlKey::Divide)), // numpad + ("VK_MULTIPLY", Key::ControlKey(ControlKey::Multiply)), // numpad + ("VK_SUBTRACT", Key::ControlKey(ControlKey::Subtract)), // numpad + ("VK_ADD", Key::ControlKey(ControlKey::Add)), // numpad + ("VK_DECIMAL", Key::ControlKey(ControlKey::Decimal)), // numpad + ("VK_F1", Key::ControlKey(ControlKey::F1)), + ("VK_F2", Key::ControlKey(ControlKey::F2)), + ("VK_F3", Key::ControlKey(ControlKey::F3)), + ("VK_F4", Key::ControlKey(ControlKey::F4)), + ("VK_F5", Key::ControlKey(ControlKey::F5)), + ("VK_F6", Key::ControlKey(ControlKey::F6)), + ("VK_F7", Key::ControlKey(ControlKey::F7)), + ("VK_F8", Key::ControlKey(ControlKey::F8)), + ("VK_F9", Key::ControlKey(ControlKey::F9)), + ("VK_F10", Key::ControlKey(ControlKey::F10)), + ("VK_F11", Key::ControlKey(ControlKey::F11)), + ("VK_F12", Key::ControlKey(ControlKey::F12)), + ("VK_ENTER", Key::ControlKey(ControlKey::Return)), + ("VK_CANCEL", Key::ControlKey(ControlKey::Cancel)), + ("VK_BACK", Key::ControlKey(ControlKey::Backspace)), + ("VK_TAB", Key::ControlKey(ControlKey::Tab)), + ("VK_CLEAR", Key::ControlKey(ControlKey::Clear)), + ("VK_RETURN", Key::ControlKey(ControlKey::Return)), + ("VK_SHIFT", Key::ControlKey(ControlKey::Shift)), + ("VK_CONTROL", Key::ControlKey(ControlKey::Control)), + ("VK_MENU", Key::ControlKey(ControlKey::Alt)), + ("VK_PAUSE", Key::ControlKey(ControlKey::Pause)), + ("VK_CAPITAL", Key::ControlKey(ControlKey::CapsLock)), + ("VK_KANA", Key::ControlKey(ControlKey::Kana)), + ("VK_HANGUL", Key::ControlKey(ControlKey::Hangul)), + ("VK_JUNJA", Key::ControlKey(ControlKey::Junja)), + ("VK_FINAL", Key::ControlKey(ControlKey::Final)), + ("VK_HANJA", Key::ControlKey(ControlKey::Hanja)), + ("VK_KANJI", Key::ControlKey(ControlKey::Kanji)), + ("VK_ESCAPE", Key::ControlKey(ControlKey::Escape)), + ("VK_CONVERT", Key::ControlKey(ControlKey::Convert)), + ("VK_SPACE", Key::ControlKey(ControlKey::Space)), + ("VK_PRIOR", Key::ControlKey(ControlKey::PageUp)), + ("VK_NEXT", Key::ControlKey(ControlKey::PageDown)), + ("VK_END", Key::ControlKey(ControlKey::End)), + ("VK_HOME", Key::ControlKey(ControlKey::Home)), + ("VK_LEFT", Key::ControlKey(ControlKey::LeftArrow)), + ("VK_UP", Key::ControlKey(ControlKey::UpArrow)), + ("VK_RIGHT", Key::ControlKey(ControlKey::RightArrow)), + ("VK_DOWN", Key::ControlKey(ControlKey::DownArrow)), + ("VK_SELECT", Key::ControlKey(ControlKey::Select)), + ("VK_PRINT", Key::ControlKey(ControlKey::Print)), + ("VK_EXECUTE", Key::ControlKey(ControlKey::Execute)), + ("VK_SNAPSHOT", Key::ControlKey(ControlKey::Snapshot)), + ("VK_SCROLL", Key::ControlKey(ControlKey::Scroll)), + ("VK_INSERT", Key::ControlKey(ControlKey::Insert)), + ("VK_DELETE", Key::ControlKey(ControlKey::Delete)), + ("VK_HELP", Key::ControlKey(ControlKey::Help)), + ("VK_SLEEP", Key::ControlKey(ControlKey::Sleep)), + ("VK_SEPARATOR", Key::ControlKey(ControlKey::Separator)), + ("VK_NUMPAD0", Key::ControlKey(ControlKey::Numpad0)), + ("VK_NUMPAD1", Key::ControlKey(ControlKey::Numpad1)), + ("VK_NUMPAD2", Key::ControlKey(ControlKey::Numpad2)), + ("VK_NUMPAD3", Key::ControlKey(ControlKey::Numpad3)), + ("VK_NUMPAD4", Key::ControlKey(ControlKey::Numpad4)), + ("VK_NUMPAD5", Key::ControlKey(ControlKey::Numpad5)), + ("VK_NUMPAD6", Key::ControlKey(ControlKey::Numpad6)), + ("VK_NUMPAD7", Key::ControlKey(ControlKey::Numpad7)), + ("VK_NUMPAD8", Key::ControlKey(ControlKey::Numpad8)), + ("VK_NUMPAD9", Key::ControlKey(ControlKey::Numpad9)), + ("Apps", Key::ControlKey(ControlKey::Apps)), + ("Meta", Key::ControlKey(ControlKey::Meta)), + ("RAlt", Key::ControlKey(ControlKey::RAlt)), + ("RWin", Key::ControlKey(ControlKey::RWin)), + ("RControl", Key::ControlKey(ControlKey::RControl)), + ("RShift", Key::ControlKey(ControlKey::RShift)), + ("CTRL_ALT_DEL", Key::ControlKey(ControlKey::CtrlAltDel)), + ("LOCK_SCREEN", Key::ControlKey(ControlKey::LockScreen)), + ].iter().cloned().collect(); +} + +/// Check if the given message is an error and can be retried. +/// +/// # Arguments +/// +/// * `msgtype` - The message type. +/// * `title` - The title of the message. +/// * `text` - The text of the message. +#[inline] +pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: bool) -> bool { + msgtype == "error" + && title == "Connection Error" + && ((text.contains("10054") || text.contains("104")) && retry_for_relay + || (!text.to_lowercase().contains("offline") + && !text.to_lowercase().contains("not exist") + && (!text.to_lowercase().contains("handshake") + // https://github.com/snapview/tungstenite-rs/blob/e7e060a89a72cb08e31c25a6c7284dc1bd982e23/src/error.rs#L248 + || text + .to_lowercase() + .contains("connection reset without closing handshake") && use_ws()) + && !text.to_lowercase().contains("failed") + && !text.to_lowercase().contains("resolve") + && !text.to_lowercase().contains("mismatch") + && !text.to_lowercase().contains("manually") + && !text.to_lowercase().contains("restricted") + && !text.to_lowercase().contains("not allowed"))) +} + +pub async fn hc_connection( + feedback: i32, + rendezvous_server: String, + token: &str, +) -> Option> { + if feedback == 0 || rendezvous_server.is_empty() || token.is_empty() { + return None; + } + let (tx, rx) = unbounded_channel::<()>(); + let token = token.to_owned(); + tokio::spawn(async move { + allow_err!(hc_connection_(rendezvous_server, rx, token).await); + }); + Some(tx) +} + +async fn hc_connection_( + rendezvous_server: String, + mut rx: UnboundedReceiver<()>, + token: String, +) -> ResultType<()> { + let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT)); + let mut last_recv_msg = Instant::now(); + let mut keep_alive = crate::DEFAULT_KEEP_ALIVE; + + let host = check_port(&rendezvous_server, RENDEZVOUS_PORT); + let mut conn = connect_tcp(host.clone(), CONNECT_TIMEOUT).await?; + let key = crate::get_key(true).await; + crate::secure_tcp(&mut conn, &key).await?; + let mut msg_out = RendezvousMessage::new(); + msg_out.set_hc(HealthCheck { + token, + ..Default::default() + }); + conn.send(&msg_out).await?; + loop { + tokio::select! { + res = rx.recv() => { + if res.is_none() { + log::debug!("HC connection is closed as controlling connection exits"); + break; + } + } + res = conn.next() => { + last_recv_msg = Instant::now(); + let bytes = res.ok_or_else(|| anyhow!("Rendezvous connection is reset by the peer"))??; + if bytes.is_empty() { + conn.send_bytes(bytes::Bytes::new()).await?; + continue; // heartbeat + } + let msg = RendezvousMessage::parse_from_bytes(&bytes)?; + match msg.union { + Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => { + if rpr.keep_alive > 0 { + keep_alive = rpr.keep_alive * 1000; + log::info!("keep_alive: {}ms", keep_alive); + } + } + _ => {} + } + } + _ = timer.tick() => { + // https://www.emqx.com/en/blog/mqtt-keep-alive + if last_recv_msg.elapsed().as_millis() as u64 > keep_alive as u64 * 3 / 2 { + bail!("HC connection is timeout"); + } + } + } + } + Ok(()) +} + +pub mod peer_online { + use hbb_common::{ + anyhow::bail, + config::{Config, CONNECT_TIMEOUT, READ_TIMEOUT}, + log, + rendezvous_proto::*, + sleep, + socket_client::connect_tcp, + ResultType, Stream, + }; + + pub async fn query_online_states, Vec)>(ids: Vec, f: F) { + let test = false; + if test { + sleep(1.5).await; + let mut onlines = ids; + let offlines = onlines.drain((onlines.len() / 2)..).collect(); + f(onlines, offlines) + } else { + let query_timeout = std::time::Duration::from_millis(3_000); + match query_online_states_(&ids, query_timeout).await { + Ok((onlines, offlines)) => { + f(onlines, offlines); + } + Err(e) => { + log::debug!("query onlines, {}", &e); + } + } + } + } + + async fn create_online_stream() -> ResultType { + let (rendezvous_server, _servers, _contained) = + crate::get_rendezvous_server(READ_TIMEOUT).await; + let tmp: Vec<&str> = rendezvous_server.split(":").collect(); + if tmp.len() != 2 { + bail!("Invalid server address: {}", rendezvous_server); + } + let port: u16 = tmp[1].parse()?; + if port == 0 { + bail!("Invalid server address: {}", rendezvous_server); + } + let online_server = format!("{}:{}", tmp[0], port - 1); + connect_tcp(online_server, CONNECT_TIMEOUT).await + } + + async fn query_online_states_( + ids: &Vec, + timeout: std::time::Duration, + ) -> ResultType<(Vec, Vec)> { + let mut msg_out = RendezvousMessage::new(); + msg_out.set_online_request(OnlineRequest { + id: Config::get_id(), + peers: ids.clone(), + ..Default::default() + }); + + let mut socket = match create_online_stream().await { + Ok(s) => s, + Err(e) => { + log::debug!("Failed to create peers online stream, {e}"); + return Ok((vec![], ids.clone())); + } + }; + // TODO: Use long connections to avoid socket creation + // If we use a Arc>> to hold and reuse the previous socket, + // we may face the following error: + // An established connection was aborted by the software in your host machine. (os error 10053) + if let Err(e) = socket.send(&msg_out).await { + log::debug!("Failed to send peers online states query, {e}"); + return Ok((vec![], ids.clone())); + } + // Retry for 2 times to get the online response + for _ in 0..2 { + if let Some(msg_in) = + crate::get_next_nonkeyexchange_msg(&mut socket, Some(timeout.as_millis() as _)) + .await + { + match msg_in.union { + Some(rendezvous_message::Union::OnlineResponse(online_response)) => { + let states = online_response.states; + let mut onlines = Vec::new(); + let mut offlines = Vec::new(); + for i in 0..ids.len() { + // bytes index from left to right + let bit_value = 0x01 << (7 - i % 8); + if (states[i / 8] & bit_value) == bit_value { + onlines.push(ids[i].clone()); + } else { + offlines.push(ids[i].clone()); + } + } + return Ok((onlines, offlines)); + } + _ => { + // ignore + } + } + } else { + // TODO: Make sure socket closed? + bail!("Online stream receives None"); + } + } + + bail!("Failed to query online states, no online response"); + } + + #[cfg(test)] + mod tests { + use hbb_common::tokio; + + #[tokio::test] + async fn test_query_onlines() { + super::query_online_states( + vec![ + "152183996".to_owned(), + "165782066".to_owned(), + "155323351".to_owned(), + "460952777".to_owned(), + ], + |onlines: Vec, offlines: Vec| { + println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines); + }, + ) + .await; + } + } +} + +async fn test_udp_uat( + udp_socket: Arc, + server_addr: SocketAddr, + udp_port: Arc>, + mut stop_udp_rx: oneshot::Receiver<()>, +) -> ResultType<()> { + let (tx, mut rx) = oneshot::channel::<_>(); + tokio::spawn(async { + if let Ok(v) = crate::test_nat_ipv4().await { + tx.send(v).ok(); + } + }); + + let start = Instant::now(); + let mut msg_out = RendezvousMessage::new(); + msg_out.set_test_nat_request(TestNatRequest { + ..Default::default() + }); + // Adaptive retry strategy that works within TCP RTT constraints + // Start with aggressive sending, then back off + let mut retry_interval = Duration::from_millis(20); // Start fast + const MAX_INTERVAL: Duration = Duration::from_millis(200); + let mut packets_sent = 0; + + // Send initial burst to improve reliability + let data = msg_out.write_to_bytes()?; + for _ in 0..2 { + if let Err(e) = udp_socket.send_to(&data, server_addr).await { + log::warn!("Failed to send initial UDP NAT test packet: {}", e); + } else { + packets_sent += 1; + } + } + let mut last_send_time = Instant::now(); + let mut buf = [0u8; 1500]; + + loop { + tokio::select! { + Ok((addr, server)) = &mut rx => { + *udp_port.lock().unwrap() = addr.port(); + log::debug!("UDP NAT test received response from {}: {}", addr, server); + break; + } + _ = &mut stop_udp_rx => { + log::debug!("UDP NAT test received stop signal after {} packets", packets_sent); + break; + } + _ = hbb_common::sleep(retry_interval.as_secs_f32()) => { + // Adaptive retry: send fewer packets as time goes on + let elapsed = last_send_time.elapsed(); + + if elapsed >= retry_interval { + // Send single packet (not double) to reduce network load + if let Err(e) = udp_socket.send_to(&data, server_addr).await { + log::warn!("Failed to send UDP NAT test retry packet: {}", e); + } else { + packets_sent += 1; + } + + // Exponentially increase interval to reduce network pressure + retry_interval = std::cmp::min( + Duration::from_millis((retry_interval.as_millis() as f64 * 1.5) as u64), + MAX_INTERVAL + ); + last_send_time = Instant::now(); + } + } + res = udp_socket.recv(&mut buf[..]) => { + match res { + Ok(n) => { + match RendezvousMessage::parse_from_bytes(&buf[0..n]) { + Ok(msg_in) => { + if let Some(rendezvous_message::Union::TestNatResponse(response)) = msg_in.union { + *udp_port.lock().unwrap() = response.port as u16; + break; + } + } + Err(e) => { + log::warn!("Failed to parse UDP NAT test response: {}", e); + } + } + } + Err(e) => { + log::warn!("UDP NAT test socket error: {}", e); + } + } + } + } + } + + let final_port = *udp_port.lock().unwrap(); + log::debug!( + "UDP NAT test to {:?} finished: time={:?}, port={}, packets_sent={}, success={}", + server_addr, + start.elapsed(), + final_port, + packets_sent, + final_port > 0 + ); + Ok(()) +} + +#[inline] +async fn udp_nat_connect( + socket: Arc, + typ: &'static str, + ms_timeout: u64, +) -> ResultType<(Stream, Option, &'static str)> { + crate::punch_udp(socket.clone(), false) + .await + .map_err(|err| { + log::debug!("{err}"); + anyhow!(err) + })?; + let res = KcpStream::connect(socket, Duration::from_millis(ms_timeout)) + .await + .map_err(|err| { + log::debug!("Failed to connect KCP stream: {}", err); + anyhow!(err) + })?; + Ok((res.1, Some(res.0), typ)) +} diff --git a/vendor/rustdesk/src/client/file_trait.rs b/vendor/rustdesk/src/client/file_trait.rs new file mode 100644 index 0000000..003767b --- /dev/null +++ b/vendor/rustdesk/src/client/file_trait.rs @@ -0,0 +1,193 @@ +use hbb_common::{fs, log, message_proto::*}; + +use super::{Data, Interface}; + +pub trait FileManager: Interface { + #[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + )))] + fn get_home_dir(&self) -> String { + fs::get_home_as_string() + } + + #[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + )))] + fn get_next_job_id(&self) -> i32 { + fs::get_next_job_id() + } + + #[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + )))] + fn update_next_job_id(&self, id: i32) { + fs::update_next_job_id(id); + } + + #[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + )))] + fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value { + match fs::read_dir(&fs::get_path(&path), include_hidden) { + Err(_) => sciter::Value::null(), + Ok(fd) => { + use crate::ui::remote::make_fd; + let mut m = make_fd(0, &fd.entries.to_vec(), false); + m.set_item("path", path); + m + } + } + } + + fn cancel_job(&self, id: i32) { + self.send(Data::CancelJob(id)); + } + + fn read_empty_dirs(&self, path: String, include_hidden: bool) { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_read_empty_dirs(ReadEmptyDirs { + path, + include_hidden, + ..Default::default() + }); + msg_out.set_file_action(file_action); + self.send(Data::Message(msg_out)); + } + + fn read_remote_dir(&self, path: String, include_hidden: bool) { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_read_dir(ReadDir { + path, + include_hidden, + ..Default::default() + }); + msg_out.set_file_action(file_action); + self.send(Data::Message(msg_out)); + } + + fn remove_file(&self, id: i32, path: String, file_num: i32, is_remote: bool) { + self.send(Data::RemoveFile((id, path, file_num, is_remote))); + } + + fn remove_dir_all(&self, id: i32, path: String, is_remote: bool, include_hidden: bool) { + self.send(Data::RemoveDirAll((id, path, is_remote, include_hidden))); + } + + #[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + )))] + fn confirm_delete_files(&self, id: i32, file_num: i32) { + self.send(Data::ConfirmDeleteFiles((id, file_num))); + } + + #[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" + )))] + fn set_no_confirm(&self, id: i32) { + self.send(Data::SetNoConfirm(id)); + } + + fn remove_dir(&self, id: i32, path: String, is_remote: bool) { + if is_remote { + self.send(Data::RemoveDir((id, path))); + } else { + fs::remove_all_empty_dir(&fs::get_path(&path)).ok(); + } + } + + fn create_dir(&self, id: i32, path: String, is_remote: bool) { + self.send(Data::CreateDir((id, path, is_remote))); + } + + fn send_files( + &self, + id: i32, + r#type: i32, + path: String, + to: String, + file_num: i32, + include_hidden: bool, + is_remote: bool, + ) { + self.send(Data::SendFiles(( + id, + r#type.into(), + path, + to, + file_num, + include_hidden, + is_remote, + ))); + } + + fn add_job( + &self, + id: i32, + r#type: i32, + path: String, + to: String, + file_num: i32, + include_hidden: bool, + is_remote: bool, + ) { + self.send(Data::AddJob(( + id, + r#type.into(), + path, + to, + file_num, + include_hidden, + is_remote, + ))); + } + + fn resume_job(&self, id: i32, is_remote: bool) { + self.send(Data::ResumeJob((id, is_remote))); + } + + fn set_confirm_override_file( + &self, + id: i32, + file_num: i32, + need_override: bool, + remember: bool, + is_upload: bool, + ) { + log::info!( + "confirm file transfer, job: {}, need_override: {}", + id, + need_override + ); + self.send(Data::SetConfirmOverrideFile(( + id, + file_num, + need_override, + remember, + is_upload, + ))); + } + + fn rename_file(&self, act_id: i32, path: String, new_name: String, is_remote: bool) { + self.send(Data::RenameFile((act_id, path, new_name, is_remote))); + } +} diff --git a/vendor/rustdesk/src/client/helper.rs b/vendor/rustdesk/src/client/helper.rs new file mode 100644 index 0000000..98d1239 --- /dev/null +++ b/vendor/rustdesk/src/client/helper.rs @@ -0,0 +1,37 @@ +use hbb_common::{ + get_time, + message_proto::{Message, VoiceCallRequest, VoiceCallResponse}, +}; +use scrap::CodecFormat; +use std::collections::HashMap; + +#[derive(Debug, Default)] +pub struct QualityStatus { + pub speed: Option, + pub fps: HashMap, + pub delay: Option, + pub target_bitrate: Option, + pub codec_format: Option, + pub chroma: Option, +} + +#[inline] +pub fn new_voice_call_request(is_connect: bool) -> Message { + let mut req = VoiceCallRequest::new(); + req.is_connect = is_connect; + req.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(req); + msg +} + +#[inline] +pub fn new_voice_call_response(request_timestamp: i64, accepted: bool) -> Message { + let mut resp = VoiceCallResponse::new(); + resp.accepted = accepted; + resp.req_timestamp = request_timestamp; + resp.ack_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_response(resp); + msg +} diff --git a/vendor/rustdesk/src/client/io_loop.rs b/vendor/rustdesk/src/client/io_loop.rs new file mode 100644 index 0000000..78ba9eb --- /dev/null +++ b/vendor/rustdesk/src/client/io_loop.rs @@ -0,0 +1,2492 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::{update_clipboard, ClipboardSide}; +#[cfg(not(any(target_os = "ios")))] +use crate::{audio_service, clipboard::CLIPBOARD_INTERVAL, ConnInner, CLIENT_SERVER}; +use crate::{ + client::{ + self, new_voice_call_request, Client, Data, Interface, MediaData, MediaSender, + QualityStatus, MILLI1, SEC30, + }, + common::get_default_sound_input, + ui_session_interface::{InvokeUiSession, Session}, +}; +#[cfg(feature = "unix-file-copy-paste")] +use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip}; +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] +use clipboard::ContextSend; +use crossbeam_queue::ArrayQueue; +#[cfg(not(target_os = "ios"))] +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, + tokio::{ + self, + sync::mpsc, + time::{self, Duration, Instant}, + }, + Stream, +}; +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] +use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType}; +use scrap::CodecFormat; +use std::{ + collections::HashMap, + ffi::c_void, + num::NonZeroI64, + path::PathBuf, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, RwLock, + }, +}; + +pub struct Remote { + handler: Session, + audio_sender: MediaSender, + receiver: mpsc::UnboundedReceiver, + sender: mpsc::UnboundedSender, + // Stop sending local audio to remote client. + stop_voice_call_sender: Option>, + voice_call_request_timestamp: Option, + read_jobs: Vec, + write_jobs: Vec, + remove_jobs: HashMap, + timer: crate::RustDeskInterval, + last_update_jobs_status: (Instant, HashMap), + is_connected: bool, + first_frame: bool, + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + client_conn_id: i32, // used for file clipboard + data_count: Arc, + video_format: CodecFormat, + elevation_requested: bool, + peer_info: ParsedPeerInfo, + video_threads: HashMap, + chroma: Arc>>, + last_record_state: bool, + sent_close_reason: bool, +} + +#[derive(Default)] +struct ParsedPeerInfo { + platform: String, + is_installed: bool, + idd_impl: String, + support_view_camera: bool, + support_terminal: bool, +} + +impl ParsedPeerInfo { + fn is_support_virtual_display(&self) -> bool { + self.is_installed + && self.platform == "Windows" + && (self.idd_impl == "rustdesk_idd" || self.idd_impl == "amyuni_idd") + } +} + +impl Remote { + pub fn new( + handler: Session, + receiver: mpsc::UnboundedReceiver, + sender: mpsc::UnboundedSender, + ) -> Self { + Self { + handler, + audio_sender: crate::client::start_audio_thread(), + receiver, + sender, + read_jobs: Vec::new(), + write_jobs: Vec::new(), + remove_jobs: Default::default(), + timer: crate::rustdesk_interval(time::interval(SEC30)), + last_update_jobs_status: (Instant::now(), Default::default()), + is_connected: false, + first_frame: false, + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + client_conn_id: 0, + data_count: Arc::new(AtomicUsize::new(0)), + video_format: CodecFormat::Unknown, + stop_voice_call_sender: None, + voice_call_request_timestamp: None, + elevation_requested: false, + peer_info: Default::default(), + video_threads: Default::default(), + chroma: Default::default(), + last_record_state: false, + sent_close_reason: false, + } + } + + pub async fn io_loop(&mut self, key: &str, token: &str, round: u32) { + #[cfg(target_os = "windows")] + let _file_clip_context_holder = { + // `is_port_forward()` will not reach here, but we still check it for clarity. + if self.handler.is_default() { + // It is ok to call this function multiple times. + ContextSend::enable(true); + Some(crate::SimpleCallOnReturn { + b: true, + f: Box::new(|| { + // No need to call `enable(false)` for sciter version, because each client of sciter version is a new process. + // It's better to check if the peers are windows(support file copy&paste), but it's not necessary. + #[cfg(feature = "flutter")] + if !crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { + ContextSend::enable(false); + }; + }), + }) + } else { + None + } + }; + + let mut last_recv_time = Instant::now(); + let mut received = false; + let conn_type = if self.handler.is_file_transfer() { + ConnType::FILE_TRANSFER + } else if self.handler.is_view_camera() { + ConnType::VIEW_CAMERA + } else if self.handler.is_terminal() { + ConnType::TERMINAL + } else { + ConnType::default() + }; + + match Client::start( + &self.handler.get_id(), + key, + token, + conn_type, + self.handler.clone(), + ) + .await + { + Ok(((mut peer, direct, pk, kcp, stream_type), (feedback, rendezvous_server))) => { + self.handler + .connection_round_state + .lock() + .unwrap() + .set_connected(); + self.handler + .set_connection_type(peer.is_secured(), direct, stream_type); // flutter -> connection_ready + self.handler.update_direct(Some(direct)); + if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA { + self.handler + .set_fingerprint(crate::common::pk_to_fingerprint(pk.unwrap_or_default())); + } + + // just build for now + #[cfg(not(any(target_os = "windows", feature = "unix-file-copy-paste")))] + let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); + + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + let (_tx_holder, rx) = mpsc::unbounded_channel(); + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + let mut rx_clip_client_holder = (Arc::new(TokioMutex::new(rx)), None); + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + { + if self.handler.is_default() { + (self.client_conn_id, rx_clip_client_holder.0) = + clipboard::get_rx_cliprdr_client(&self.handler.get_id()); + log::debug!("get cliprdr client for conn_id {}", self.client_conn_id); + let client_conn_id = self.client_conn_id; + rx_clip_client_holder.1 = Some(crate::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + clipboard::remove_channel_by_conn_id(client_conn_id); + }), + }); + }; + } + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + let mut rx_clip_client = rx_clip_client_holder.0.lock().await; + + let mut status_timer = + crate::rustdesk_interval(time::interval(Duration::new(1, 0))); + let mut fps_instant = Instant::now(); + + let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await; + + loop { + tokio::select! { + res = peer.next() => { + if let Some(res) = res { + match res { + Err(err) => { + self.handler.on_establish_connection_error(err.to_string()); + break; + } + Ok(ref bytes) => { + last_recv_time = Instant::now(); + if !received { + received = true; + self.handler.update_received(true); + } + self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); + if !self.handle_msg_from_peer(bytes, &mut peer).await { + break + } + } + } + } else { + if self.handler.is_restarting_remote_device() { + log::info!("Restart remote device"); + self.handler.msgbox("restarting", "Restarting remote device", "remote_restarting_tip", ""); + } else { + log::info!("Reset by the peer"); + self.handler.msgbox("error", "Connection Error", "Reset by the peer", ""); + } + break; + } + } + d = self.receiver.recv() => { + if let Some(d) = d { + if !self.handle_msg_from_ui(d, &mut peer).await { + break; + } + } + } + _msg = rx_clip_client.recv() => { + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + self.handle_local_clipboard_msg(&mut peer, _msg).await; + } + _ = self.timer.tick() => { + if last_recv_time.elapsed() >= SEC30 { + self.handler.msgbox("error", "Connection Error", "Timeout", ""); + break; + } + if !self.read_jobs.is_empty() { + if let Err(err) = fs::handle_read_jobs(&mut self.read_jobs, &mut peer).await { + self.handler.msgbox("error", "Connection Error", &err.to_string(), ""); + break; + } + self.update_jobs_status(); + } else { + self.timer = crate::rustdesk_interval(time::interval_at(Instant::now() + SEC30, SEC30)); + } + } + _ = status_timer.tick() => { + let elapsed = fps_instant.elapsed().as_millis(); + if elapsed < 1000 { + continue; + } + fps_instant = Instant::now(); + let mut speed = self.data_count.swap(0, Ordering::Relaxed); + speed = speed * 1000 / elapsed as usize; + let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); + + let fps = self.video_threads.iter().map(|(k, v)| { + // Correcting the inaccuracy of status_timer + (k.clone(), (*v.frame_count.read().unwrap() as i32) * 1000 / elapsed as i32) + }).collect::>(); + self.video_threads.iter().for_each(|(_, v)| { + *v.frame_count.write().unwrap() = 0; + }); + self.fps_control(direct, fps.clone()); + let chroma = self.chroma.read().unwrap().clone(); + let chroma = match chroma { + Some(Chroma::I444) => "4:4:4", + Some(Chroma::I420) => "4:2:0", + None => "-", + }; + let chroma = Some(chroma.to_string()); + let codec_format = if self.video_format == CodecFormat::Unknown { + None + } else { + Some(self.video_format.clone()) + }; + self.handler.update_quality_status(QualityStatus { + speed: Some(speed), + fps, + chroma, + codec_format, + ..Default::default() + }); + } + } + } + log::debug!("Exit io_loop of id={}", self.handler.get_id()); + // Stop client audio server. + if let Some(s) = self.stop_voice_call_sender.take() { + s.send(()).ok(); + } + if kcp.is_some() { + // Send the close reason if it hasn't been sent yet, as KCP cannot detect the socket close event. + self.send_close_reason(&mut peer, "kcp").await; + // KCP does not send messages immediately, so wait to ensure the last message is sent. + // 1ms works in my test, but 30ms is more reliable. + tokio::time::sleep(Duration::from_millis(30)).await; + } + } + Err(err) => { + self.handler.on_establish_connection_error(err.to_string()); + } + } + // set_disconnected_ok is used to check if new connection round is started. + let _set_disconnected_ok = self + .handler + .connection_round_state + .lock() + .unwrap() + .set_disconnected(round); + + #[cfg(not(target_os = "ios"))] + if self.handler.is_default() && _set_disconnected_ok { + Client::try_stop_clipboard(); + } + + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + if self.handler.is_default() && _set_disconnected_ok { + crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id); + } + } + + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + async fn handle_local_clipboard_msg( + &self, + peer: &mut Stream, + msg: Option, + ) { + match msg { + Some(clip) => match clip { + clipboard::ClipboardFile::NotifyCallback { + r#type, + title, + text, + } => { + self.handler.msgbox(&r#type, &title, &text, ""); + } + _ => { + let is_stopping_allowed = clip.is_stopping_allowed(); + let server_file_transfer_enabled = + *self.handler.server_file_transfer_enabled.read().unwrap(); + let file_transfer_enabled = + self.handler.lc.read().unwrap().enable_file_copy_paste.v; + let view_only = self.handler.lc.read().unwrap().view_only.v; + let stop = is_stopping_allowed + && (view_only + || !self.is_connected + || !(server_file_transfer_enabled && file_transfer_enabled)); + log::debug!( + "Process clipboard message from system, stop: {}, is_stopping_allowed: {}, view_only: {}, server_file_transfer_enabled: {}, file_transfer_enabled: {}", + view_only, stop, is_stopping_allowed, server_file_transfer_enabled, file_transfer_enabled + ); + if stop { + #[cfg(target_os = "windows")] + { + ContextSend::set_is_stopped(); + } + } else { + #[cfg(target_os = "windows")] + if let Err(e) = ContextSend::make_sure_enabled() { + log::error!("failed to restart clipboard context: {}", e); + // to-do: Show msgbox with "Don't show again" option + }; + log::debug!("Send system clipboard message to remote"); + let msg = crate::clipboard_file::clip_2_msg(clip); + allow_err!(peer.send(&msg).await); + } + } + }, + None => { + // unreachable!() + } + } + } + + fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { + if let Some(job) = self.remove_jobs.get_mut(&id) { + if job.no_confirm { + let file_num = (file_num + 1) as usize; + if file_num < job.files.len() { + let path = format!("{}{}{}", job.path, job.sep, job.files[file_num].name); + self.sender + .send(Data::RemoveFile((id, path, file_num as i32, job.is_remote))) + .ok(); + let elapsed = job.last_update_job_status.elapsed().as_millis() as i32; + if elapsed >= 1000 { + job.last_update_job_status = Instant::now(); + } else { + return; + } + } else { + self.remove_jobs.remove(&id); + } + } + } + if let Some(err) = err { + self.handler.job_error(id, err, file_num); + } else { + self.handler.job_done(id, file_num); + } + } + + fn stop_voice_call(&mut self) { + let voice_call_sender = std::mem::replace(&mut self.stop_voice_call_sender, None); + if let Some(stopper) = voice_call_sender { + let _ = stopper.send(()); + } + } + + // Start a voice call recorder, records audio and send to remote + fn start_voice_call(&mut self) -> Option> { + if self.handler.is_file_transfer() + || self.handler.is_port_forward() + || self.handler.is_terminal() + { + return None; + } + // iOS does not have this server. + #[cfg(not(any(target_os = "ios")))] + { + // NOTE: + // The client server and --server both use the same sound input device. + // It's better to distinguish the server side and client side. + // But it' not necessary for now, because it's not a common case. + // And it is immediately known when the input device is changed. + crate::audio_service::set_voice_call_input_device(get_default_sound_input(), false); + // Create a channel to receive error or closed message + let (tx, rx) = std::sync::mpsc::channel(); + let (tx_audio_data, mut rx_audio_data) = + hbb_common::tokio::sync::mpsc::unbounded_channel(); + // Create a stand-alone inner, add subscribe to audio service + let conn_id = CLIENT_SERVER.write().unwrap().get_new_id(); + let client_conn_inner = ConnInner::new(conn_id.clone(), Some(tx_audio_data), None); + // now we subscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner.clone(), + true, + ); + let tx_audio = self.sender.clone(); + std::thread::spawn(move || { + loop { + // check if client is closed + match rx.try_recv() { + Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { + log::debug!("Exit voice call audio service of client"); + // unsubscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner, + false, + ); + crate::audio_service::set_voice_call_input_device(None, true); + break; + } + _ => {} + } + match rx_audio_data.try_recv() { + Ok((_instant, msg)) => match &msg.union { + Some(message::Union::AudioFrame(frame)) => { + let mut msg = Message::new(); + msg.set_audio_frame(frame.clone()); + tx_audio.send(Data::Message(msg)).ok(); + } + Some(message::Union::Misc(misc)) => { + let mut msg = Message::new(); + msg.set_misc(misc.clone()); + tx_audio.send(Data::Message(msg)).ok(); + } + _ => {} + }, + Err(err) => { + if err == TryRecvError::Empty { + // ignore + } else { + log::debug!("Failed to record local audio channel: {}", err); + } + } + } + } + }); + return Some(tx); + } + #[cfg(target_os = "ios")] + { + None + } + } + + async fn send_close_reason(&mut self, peer: &mut Stream, reason: &str) { + if self.sent_close_reason { + return; + } + let mut misc = Misc::new(); + misc.set_close_reason(reason.to_owned()); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + self.sent_close_reason = true; + } + + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { + match data { + Data::Close => { + self.send_close_reason(peer, "").await; + return false; + } + Data::Login((os_username, os_password, password, remember)) => { + self.handler + .handle_login_from_ui(os_username, os_password, password, remember, peer) + .await; + } + #[cfg(all(target_os = "windows", not(feature = "flutter")))] + Data::ToggleClipboardFile => { + self.check_clipboard_file_context(); + } + Data::Message(msg) => { + match &msg.union { + Some(message::Union::Misc(misc)) => match misc.union { + Some(misc::Union::RefreshVideo(_)) => { + self.video_threads.iter().for_each(|(_, v)| { + *v.discard_queue.write().unwrap() = true; + }); + } + Some(misc::Union::RefreshVideoDisplay(display)) => { + if let Some(v) = self.video_threads.get_mut(&(display as usize)) { + *v.discard_queue.write().unwrap() = true; + } + } + _ => {} + }, + _ => {} + } + allow_err!(peer.send(&msg).await); + } + Data::SendFiles((id, r#type, path, to, file_num, include_hidden, is_remote)) => { + log::info!("send files, is remote {}", is_remote); + let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); + if is_remote { + log::debug!("New job {}, write to {} from remote {}", id, to, path); + let to = match r#type { + fs::JobType::Generic => fs::DataSource::FilePath(PathBuf::from(&to)), + fs::JobType::Printer => { + fs::DataSource::MemoryCursor(std::io::Cursor::new(Vec::new())) + } + }; + self.write_jobs.push(fs::TransferJob::new_write( + id, + r#type, + path.clone(), + to, + file_num, + include_hidden, + is_remote, + od, + )); + allow_err!( + peer.send(&fs::new_send(id, r#type, path, file_num, include_hidden)) + .await + ); + } else { + match fs::TransferJob::new_read( + id, + r#type, + to.clone(), + fs::DataSource::FilePath(PathBuf::from(&path)), + file_num, + include_hidden, + is_remote, + od, + ) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(job) => { + log::debug!( + "New job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + self.handler.update_folder_files( + job.id(), + job.files(), + path, + !is_remote, + true, + ); + #[cfg(not(windows))] + let files = job.files().clone(); + #[cfg(windows)] + let mut files = job.files().clone(); + #[cfg(windows)] + if self.handler.peer_platform() != "Windows" { + // peer is not windows, need transform \ to / + fs::transform_windows_path(&mut files); + } + let total_size = job.total_size(); + self.read_jobs.push(job); + self.timer = crate::rustdesk_interval(time::interval(MILLI1)); + allow_err!( + peer.send(&fs::new_receive(id, to, file_num, files, total_size)) + .await + ); + } + } + } + } + Data::AddJob((id, r#type, path, to, file_num, include_hidden, is_remote)) => { + let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); + if is_remote { + log::debug!( + "new write waiting job {}, write to {} from remote {}", + id, + to, + path + ); + let mut job = fs::TransferJob::new_write( + id, + r#type, + path.clone(), + fs::DataSource::FilePath(PathBuf::from(&to)), + file_num, + include_hidden, + is_remote, + od, + ); + job.is_last_job = true; + self.write_jobs.push(job); + } else { + match fs::TransferJob::new_read( + id, + r#type, + to.clone(), + fs::DataSource::FilePath(PathBuf::from(&path)), + file_num, + include_hidden, + is_remote, + od, + ) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(mut job) => { + log::debug!( + "new read waiting job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + self.handler.update_folder_files( + job.id(), + job.files(), + path, + !is_remote, + true, + ); + job.is_last_job = true; + self.read_jobs.push(job); + self.timer = crate::rustdesk_interval(time::interval(MILLI1)); + } + } + } + } + Data::ResumeJob((id, is_remote)) => { + if is_remote { + if let Some(job) = get_job(id, &mut self.write_jobs) { + job.is_last_job = false; + job.is_resume = true; + allow_err!( + peer.send(&fs::new_send( + id, + fs::JobType::Generic, + job.remote.clone(), + job.file_num, + job.show_hidden + )) + .await + ); + } + } else { + if let Some(job) = get_job(id, &mut self.read_jobs) { + match &job.data_source { + fs::DataSource::FilePath(_p) => { + job.is_last_job = false; + job.is_resume = true; + job.set_finished_size_on_resume(); + #[cfg(not(windows))] + let files = job.files().clone(); + #[cfg(windows)] + let mut files = job.files().clone(); + #[cfg(windows)] + if self.handler.peer_platform() != "Windows" { + // peer is not windows, need transform \ to / + fs::transform_windows_path(&mut files); + } + allow_err!( + peer.send(&fs::new_receive( + id, + job.remote.clone(), + job.file_num, + files, + job.total_size(), + )) + .await + ); + } + fs::DataSource::MemoryCursor(_) => { + // unreachable!() + log::error!("Resume job with memory cursor"); + } + } + } + } + } + Data::SetNoConfirm(id) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + job.no_confirm = true; + } + } + Data::ConfirmDeleteFiles((id, file_num)) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + let i = file_num as usize; + if i < job.files.len() { + self.handler.ui_handler.confirm_delete_files( + id, + file_num, + job.files[i].name.clone(), + ); + } + } + } + Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { + if is_upload { + if let Some(job) = fs::get_job(id, &mut self.read_jobs) { + if remember { + job.set_overwrite_strategy(Some(need_override)); + } + job.confirm(&FileTransferSendConfirmRequest { + id, + file_num, + union: if need_override { + Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) + } else { + Some(file_transfer_send_confirm_request::Union::Skip(true)) + }, + ..Default::default() + }) + .await; + } + } else { + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + if remember { + job.set_overwrite_strategy(Some(need_override)); + } + let mut msg = Message::new(); + let mut file_action = FileAction::new(); + let req = FileTransferSendConfirmRequest { + id, + file_num, + union: if need_override { + Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) + } else { + Some(file_transfer_send_confirm_request::Union::Skip(true)) + }, + ..Default::default() + }; + job.confirm(&req).await; + file_action.set_send_confirm(req); + msg.set_file_action(file_action); + allow_err!(peer.send(&msg).await); + } + } + } + Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { + let sep = self.handler.get_path_sep(is_remote); + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_all_files(ReadAllFiles { + id, + path: path.clone(), + include_hidden, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + self.remove_jobs + .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); + } else { + match fs::get_recursive_files(&path, include_hidden) { + Ok(entries) => { + self.handler.update_folder_files( + id, + &entries, + path.clone(), + !is_remote, + false, + ); + self.remove_jobs + .insert(id, RemoveJob::new(entries, path, sep, is_remote)); + } + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + } + } + } + Data::CancelJob(id) => { + self.cancel_transfer_job(id, peer).await; + } + Data::RemoveDir((id, path)) => { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_dir(FileRemoveDir { + id, + path, + recursive: true, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } + Data::RemoveFile((id, path, file_num, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_file(FileRemoveFile { + id, + path, + file_num, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::remove_file(&path) { + Err(err) => { + self.handle_job_status(id, file_num, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, file_num, None); + } + } + } + } + Data::CreateDir((id, path, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_create(FileDirCreate { + id, + path, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::create_dir(&path) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, -1, None); + } + } + } + } + Data::RenameFile((id, path, new_name, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_rename(FileRename { + id, + path, + new_name, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + let err = fs::rename_file(&path, &new_name) + .err() + .map(|e| e.to_string()); + self.handle_job_status(id, -1, err); + } + } + Data::RecordScreen(start) => { + self.handler.lc.write().unwrap().record_state = start; + self.update_record_state(); + } + Data::ElevateDirect => { + let mut request = ElevationRequest::new(); + request.set_direct(true); + let mut misc = Misc::new(); + misc.set_elevation_request(request); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + self.elevation_requested = true; + } + Data::ElevateWithLogon(username, password) => { + let mut request = ElevationRequest::new(); + request.set_logon(ElevationRequestWithLogon { + username, + password, + ..Default::default() + }); + let mut misc = Misc::new(); + misc.set_elevation_request(request); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + self.elevation_requested = true; + } + Data::NewVoiceCall => { + let msg = new_voice_call_request(true); + // Save the voice call request timestamp for the further validation. + self.voice_call_request_timestamp = Some( + NonZeroI64::new(msg.voice_call_request().req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); + allow_err!(peer.send(&msg).await); + self.handler.on_voice_call_waiting(); + } + Data::CloseVoiceCall => { + self.stop_voice_call(); + let msg = new_voice_call_request(false); + self.handler + .on_voice_call_closed("Closed manually by the peer"); + allow_err!(peer.send(&msg).await); + } + Data::ResetDecoder(display) => match display { + Some(display) => { + if let Some(v) = self.video_threads.get_mut(&display) { + v.video_sender.send(MediaData::Reset).ok(); + } + } + None => { + for (_, v) in self.video_threads.iter_mut() { + v.video_sender.send(MediaData::Reset).ok(); + } + } + }, + Data::TakeScreenshot((display, sid)) => { + let mut msg = Message::new(); + msg.set_screenshot_request(ScreenshotRequest { + display, + sid, + ..Default::default() + }); + allow_err!(peer.send(&msg).await); + } + _ => {} + } + true + } + + #[inline] + fn update_job_status( + job: &fs::TransferJob, + elapsed: i32, + last_update_jobs_status: &mut (Instant, HashMap), + handler: &Session, + ) { + if elapsed <= 0 { + return; + } + let transferred = job.transferred(); + let last_transferred = { + if let Some(v) = last_update_jobs_status.1.get(&job.id()) { + v.to_owned() + } else { + 0 + } + }; + last_update_jobs_status.1.insert(job.id(), transferred); + let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); + let file_num = job.file_num() - 1; + handler.job_progress(job.id(), file_num, speed, job.finished_size() as f64); + } + + fn update_jobs_status(&mut self) { + let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; + if elapsed >= 1000 { + for job in self.read_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &self.handler, + ); + } + for job in self.write_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &mut self.handler, + ); + } + self.last_update_jobs_status.0 = Instant::now(); + } + } + + async fn cancel_transfer_job(&mut self, id: i32, peer: &mut Stream) { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_cancel(FileTransferCancel { + id, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + if let Some(job) = fs::remove_job(id, &mut self.write_jobs) { + job.remove_download_file(); + } + let _ = fs::remove_job(id, &mut self.read_jobs); + self.remove_jobs.remove(&id); + } + + pub async fn sync_jobs_status_to_local(&mut self) -> bool { + if !self.is_connected { + return false; + } + let mut config: PeerConfig = self.handler.load_config(); + let mut transfer_metas = TransferSerde::default(); + for job in self.read_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); + transfer_metas.read_jobs.push(json_str); + } + for job in self.write_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); + transfer_metas.write_jobs.push(json_str); + } + log::info!("meta: {:?}", transfer_metas); + if config.transfer != transfer_metas { + config.transfer = transfer_metas; + self.handler.save_config(config); + } + true + } + + async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) { + if !self.peer_info.is_support_virtual_display() { + return; + } + let lc = self.handler.lc.read().unwrap(); + let displays = lc.get_option("virtual-display"); + for d in displays.split(',') { + if let Ok(index) = d.parse::() { + let mut misc = Misc::new(); + misc.set_toggle_virtual_display(ToggleVirtualDisplay { + display: index, + on: true, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } + } + } + + async fn send_toggle_privacy_mode_msg(&self, peer: &mut Stream) { + let lc = self.handler.lc.read().unwrap(); + if lc.version >= hbb_common::get_version_number("1.2.4") + && lc.get_toggle_option("privacy-mode") + { + let impl_key = lc.get_option("privacy-mode-impl-key"); + if impl_key == crate::privacy_mode::PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY + && !self.peer_info.is_support_virtual_display() + { + return; + } + let mut misc = Misc::new(); + misc.set_toggle_privacy_mode(TogglePrivacyMode { + impl_key, + on: true, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } + } + + fn contains_key_frame(vf: &VideoFrame) -> bool { + use video_frame::Union::*; + match &vf.union { + Some(vf) => match vf { + Vp8s(f) | Vp9s(f) | Av1s(f) | H264s(f) | H265s(f) => f.frames.iter().any(|e| e.key), + _ => false, + }, + None => false, + } + } + + // Currently, this function only considers decoding speed and queue length, not network delay. + // The controlled end can consider auto fps as the maximum decoding fps. + #[inline] + fn fps_control(&mut self, direct: bool, real_fps_map: HashMap) { + self.video_threads.iter_mut().for_each(|(k, v)| { + let real_fps = real_fps_map.get(k).cloned().unwrap_or_default(); + if real_fps == 0 { + v.fps_control.inactive_counter += 1; + } else { + v.fps_control.inactive_counter = 0; + } + }); + let custom_fps = self.handler.lc.read().unwrap().custom_fps.clone(); + let custom_fps = custom_fps.lock().unwrap().clone(); + let mut custom_fps = custom_fps.unwrap_or(30); + if custom_fps < 5 || custom_fps > 120 { + custom_fps = 30; + } + let inactive_threshold = 15; + let max_queue_len = self + .video_threads + .iter() + .map(|v| v.1.video_queue.read().unwrap().len()) + .max() + .unwrap_or_default(); + let min_decode_fps = self + .video_threads + .iter() + .filter(|v| v.1.fps_control.inactive_counter < inactive_threshold) + .map(|v| *v.1.decode_fps.read().unwrap()) + .min() + .flatten(); + let Some(min_decode_fps) = min_decode_fps else { + return; + }; + let mut limited_fps = if direct { + min_decode_fps * 9 / 10 // 30 got 27 + } else { + min_decode_fps * 4 / 5 // 30 got 24 + }; + if limited_fps > custom_fps { + limited_fps = custom_fps; + } + let last_auto_fps = self.handler.lc.read().unwrap().last_auto_fps.clone(); + let displays = self.video_threads.keys().cloned().collect::>(); + let mut fps_trending = |display: usize| { + let thread = self.video_threads.get_mut(&display)?; + let ctl = &mut thread.fps_control; + let len = thread.video_queue.read().unwrap().len(); + let decode_fps = thread.decode_fps.read().unwrap().clone()?; + let last_auto_fps = last_auto_fps.clone().unwrap_or(custom_fps as _); + if ctl.inactive_counter > inactive_threshold { + return None; + } + if len > 1 && last_auto_fps > limited_fps || len > std::cmp::max(1, decode_fps / 2) { + ctl.idle_counter = 0; + return Some(false); + } + if len <= 1 { + ctl.idle_counter += 1; + if ctl.idle_counter > 3 && last_auto_fps + 3 <= limited_fps { + return Some(true); + } + } + if len > 1 { + ctl.idle_counter = 0; + } + None + }; + let trendings: Vec<_> = displays.iter().map(|k| fps_trending(*k)).collect(); + let should_decrease = trendings.iter().any(|v| *v == Some(false)); + let should_increase = !should_decrease && trendings.iter().any(|v| *v == Some(true)); + if last_auto_fps.is_none() || should_decrease || should_increase { + // limited_fps to ensure decoding is faster than encoding + let mut auto_fps = limited_fps; + if should_decrease && limited_fps < max_queue_len { + auto_fps = limited_fps / 2; + } + if auto_fps < 1 { + auto_fps = 1; + } + if Some(auto_fps) != last_auto_fps { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + custom_fps: auto_fps as _, + ..Default::default() + }); + let mut msg = Message::new(); + msg.set_misc(misc); + self.sender.send(Data::Message(msg)).ok(); + log::info!("Set fps to {}", auto_fps); + self.handler.lc.write().unwrap().last_auto_fps = Some(auto_fps); + } + } + // send refresh + for (display, thread) in self.video_threads.iter_mut() { + let ctl = &mut thread.fps_control; + let video_queue = thread.video_queue.read().unwrap(); + let tolerable = std::cmp::min(min_decode_fps, video_queue.capacity() / 2); + if ctl.refresh_times < 20 // enough + && (video_queue.len() > tolerable + && (ctl.refresh_times == 0 || ctl.last_refresh_instant.map(|t|t.elapsed().as_secs() > 10).unwrap_or(false))) + { + // Refresh causes client set_display, left frames cause flickering. + drop(video_queue); + self.handler.refresh_video(*display as _); + log::info!("Refresh display {} to reduce delay", display); + ctl.refresh_times += 1; + ctl.last_refresh_instant = Some(Instant::now()); + } + } + } + + fn check_view_camera_support(&self, peer_version: &str, peer_platform: &str) -> bool { + if self.peer_info.support_view_camera { + return true; + } + if hbb_common::get_version_number(&peer_version) < hbb_common::get_version_number("1.3.9") + && (peer_platform == "Windows" || peer_platform == "Linux") + { + self.handler.msgbox( + "error", + "Download new version", + "upgrade_remote_rustdesk_client_to_{1.3.9}_tip", + "", + ); + } else { + self.handler.on_error("view_camera_unsupported_tip"); + } + return false; + } + + fn check_terminal_support(&self, peer_version: &str) -> bool { + if self.peer_info.support_terminal { + return true; + } + if hbb_common::get_version_number(&peer_version) < hbb_common::get_version_number("1.4.1") { + self.handler.msgbox( + "error", + "Remote terminal not supported", + "Remote terminal is not supported by the remote side. Please upgrade to version 1.4.1 or higher.", + "", + ); + } else { + self.handler + .on_error("Remote terminal is not supported by the remote side"); + } + return false; + } + + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { + if let Ok(msg_in) = Message::parse_from_bytes(&data) { + match msg_in.union { + Some(message::Union::VideoFrame(vf)) => { + if !self.first_frame { + self.first_frame = true; + self.handler.close_success(); + self.handler.adapt_size(); + self.send_toggle_virtual_display_msg(peer).await; + self.send_toggle_privacy_mode_msg(peer).await; + } + self.video_format = CodecFormat::from(&vf); + + let display = vf.display as usize; + if !self.video_threads.contains_key(&display) { + self.new_video_thread(display); + } + let Some(thread) = self.video_threads.get_mut(&display) else { + return true; + }; + if Self::contains_key_frame(&vf) { + thread + .video_sender + .send(MediaData::VideoFrame(Box::new(vf))) + .ok(); + } else { + let video_queue = thread.video_queue.read().unwrap(); + if video_queue.force_push(vf).is_some() { + drop(video_queue); + self.handler.refresh_video(display as _); + } else { + thread.video_sender.send(MediaData::VideoQueue).ok(); + } + } + } + Some(message::Union::Hash(hash)) => { + self.handler + .handle_hash(&self.handler.password.clone(), hash, peer) + .await; + } + Some(message::Union::LoginResponse(lr)) => match lr.union { + Some(login_response::Union::Error(err)) => { + if err == client::REQUIRE_2FA { + self.handler.lc.write().unwrap().enable_trusted_devices = + lr.enable_trusted_devices; + } + if !self.handler.handle_login_error(&err) { + return false; + } + } + Some(login_response::Union::PeerInfo(pi)) => { + let peer_version = pi.version.clone(); + let peer_platform = pi.platform.clone(); + self.set_peer_info(&pi); + if self.handler.is_view_camera() { + if !self.check_view_camera_support(&peer_version, &peer_platform) { + self.handler.lc.write().unwrap().handle_peer_info(&pi); + return false; + } + } + if self.handler.is_terminal() { + if !self.check_terminal_support(&peer_version) { + self.handler.lc.write().unwrap().handle_peer_info(&pi); + return false; + } + } + self.handler.handle_peer_info(pi); + #[cfg(all(target_os = "windows", not(feature = "flutter")))] + self.check_clipboard_file_context(); + if self.handler.is_default() { + #[cfg(feature = "flutter")] + #[cfg(not(target_os = "ios"))] + let rx = Client::try_start_clipboard(None); + #[cfg(not(feature = "flutter"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let rx = Client::try_start_clipboard(Some( + crate::client::ClientClipboardContext { + cfg: self.handler.get_permission_config(), + tx: self.sender.clone(), + #[cfg(feature = "unix-file-copy-paste")] + is_file_supported: crate::is_support_file_copy_paste( + &peer_version, + ), + }, + )); + // To make sure current text clipboard data is updated. + #[cfg(not(target_os = "ios"))] + if let Some(mut rx) = rx { + timeout(CLIPBOARD_INTERVAL, rx.recv()).await.ok(); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.handler.lc.read().unwrap().sync_init_clipboard.v { + if let Some(msg_out) = crate::clipboard::get_current_clipboard_msg( + &peer_version, + &peer_platform, + crate::clipboard::ClipboardSide::Client, + ) { + let sender = self.sender.clone(); + let permission_config = self.handler.get_permission_config(); + tokio::spawn(async move { + if permission_config.is_text_clipboard_required() { + sender.send(Data::Message(msg_out)).ok(); + } + }); + } + } + // to-do: Android, is `sync_init_clipboard` really needed? + // https://github.com/rustdesk/rustdesk/discussions/9010 + + #[cfg(feature = "flutter")] + #[cfg(not(target_os = "ios"))] + crate::flutter::update_text_clipboard_required(); + + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); + + // on connection established client + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::plugin::handle_listen_event( + crate::plugin::EVENT_ON_CONN_CLIENT.to_owned(), + self.handler.get_id(), + ); + } + + if self.handler.is_file_transfer() { + self.handler.load_last_jobs(); + } + + self.is_connected = true; + } + _ => {} + }, + Some(message::Union::CursorData(cd)) => { + self.handler.set_cursor_data(cd); + } + Some(message::Union::CursorId(id)) => { + self.handler.set_cursor_id(id.to_string()); + } + Some(message::Union::CursorPosition(cp)) => { + self.handler.set_cursor_position(cp); + } + Some(message::Union::Clipboard(cb)) => { + if !self.handler.lc.read().unwrap().disable_clipboard.v { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_clipboard(vec![cb], ClipboardSide::Client); + #[cfg(target_os = "ios")] + { + let content = if cb.compress { + hbb_common::compress::decompress(&cb.content) + } else { + cb.content.into() + }; + if let Ok(content) = String::from_utf8(content) { + self.handler.clipboard(content); + } + } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_clipboard(cb); + } + } + Some(message::Union::MultiClipboards(_mcb)) => { + if !self.handler.lc.read().unwrap().disable_clipboard.v { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_clipboard(_mcb.clipboards, ClipboardSide::Client); + #[cfg(target_os = "ios")] + { + if let Some(cb) = _mcb + .clipboards + .iter() + .find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text)) + { + let content = if cb.compress { + hbb_common::compress::decompress(&cb.content) + } else { + cb.content.to_vec() + }; + if let Ok(content) = String::from_utf8(content) { + self.handler.clipboard(content); + } + } + } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_multi_clipboards(_mcb); + } + } + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + Some(message::Union::Cliprdr(clip)) => { + self.handle_cliprdr_msg(clip, peer).await; + } + Some(message::Union::FileResponse(fr)) => { + match fr.union { + Some(file_response::Union::EmptyDirs(res)) => { + self.handler.update_empty_dirs(res); + } + Some(file_response::Union::Dir(fd)) => { + #[cfg(windows)] + let entries = fd.entries.to_vec(); + #[cfg(not(windows))] + let mut entries = fd.entries.to_vec(); + #[cfg(not(windows))] + { + if self.handler.peer_platform() == "Windows" { + fs::transform_windows_path(&mut entries); + } + } + // We cannot call cancel_transfer_job/handle_job_status while holding + // a mutable borrow from fs::get_job(&mut self.write_jobs), so defer + // the error handling until after the borrow scope ends. + let mut set_files_err = None; + if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { + log::info!("job set_files: {:?}", entries); + if let Err(err) = job.set_files(entries) { + set_files_err = Some(err.to_string()); + } else { + job.set_finished_size_on_resume(); + self.handler.update_folder_files( + fd.id, + job.files(), + fd.path, + false, + false, + ); + } + } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { + // Intentionally keep raw entries here: + // - remote remove flow executes deletions on peer side; + // - local remove flow is populated from local get_recursive_files(). + job.files = entries; + self.handler + .update_folder_files(fd.id, &job.files, fd.path, false, false); + } else { + self.handler + .update_folder_files(fd.id, &entries, fd.path, false, false); + } + if let Some(err) = set_files_err { + log::warn!( + "Rejected unsafe file list from remote peer for job {}: {}", + fd.id, + err + ); + self.cancel_transfer_job(fd.id, peer).await; + self.handle_job_status(fd.id, -1, Some(err)); + } + } + Some(file_response::Union::Digest(digest)) => { + if digest.is_upload { + if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + if let fs::DataSource::FilePath(p) = &job.data_source { + let read_path = + get_string(&fs::TransferJob::join(p, &file.name)); + let mut overwrite_strategy = + job.default_overwrite_strategy(); + let mut offset = 0; + if digest.is_identical && job.is_resume { + if digest.transferred_size > 0 { + overwrite_strategy = Some(true); + offset = digest.transferred_size as _; + } + } + if let Some(overwrite) = overwrite_strategy { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(offset) + } else { + file_transfer_send_confirm_request::Union::Skip( + true, + ) + }), + ..Default::default() + }; + job.confirm(&req).await; + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, + digest.is_identical, + ); + } + } + } + } + } else { + if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + if let fs::DataSource::FilePath(p) = &job.data_source { + let write_path = + get_string(&fs::TransferJob::join(p, &file.name)); + job.set_digest(digest.file_size, digest.last_modified); + let peer_ver = self.handler.lc.read().unwrap().version; + let is_support_resume = + crate::is_support_file_transfer_resume_num( + peer_ver, + ); + match fs::is_write_need_confirmation( + is_support_resume && job.is_resume, + &write_path, + &digest, + ) { + Ok(res) => match res { + DigestCheckResult::IsSame => { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(file_transfer_send_confirm_request::Union::Skip(true)), + ..Default::default() + }; + job.confirm(&req).await; + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } + DigestCheckResult::NeedConfirm(digest) => { + let mut overwrite_strategy = + job.default_overwrite_strategy(); + let mut offset = 0; + if digest.is_identical + && job.is_resume + && digest.transferred_size > 0 + { + overwrite_strategy = Some(true); + offset = digest.transferred_size as _; + } + if let Some(overwrite) = overwrite_strategy + { + let req = + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(offset) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }; + job.confirm(&req).await; + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + write_path, + false, + digest.is_identical, + ); + } + } + DigestCheckResult::NoSuchFile => { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), + ..Default::default() + }; + job.confirm(&req).await; + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } + }, + Err(err) => { + println!("error receiving digest: {}", err); + } + } + } + } + } + } + } + Some(file_response::Union::Block(block)) => { + if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { + if let Err(_err) = job.write(block).await { + // to-do: add "skip" for writing job + } + if job.r#type == fs::JobType::Generic { + self.update_jobs_status(); + } + } + } + Some(file_response::Union::Done(d)) => { + let mut err: Option = None; + let mut job_type = fs::JobType::Generic; + let mut printer_data = None; + if let Some(job) = fs::remove_job(d.id, &mut self.write_jobs) { + job.modify_time(); + err = job.job_error(); + job_type = job.r#type; + printer_data = match job.get_buf_data().await { + Ok(d) => d, + Err(e) => { + log::error!("Failed to get the printer data: {}", e); + None + } + }; + } + match job_type { + fs::JobType::Generic => { + self.handle_job_status(d.id, d.file_num, err); + } + fs::JobType::Printer => { + if let Some(err) = err { + log::error!("Receive print job failed, error {err}"); + } else { + log::info!( + "Receive print job done, data len: {:?}", + printer_data.as_ref().map(|d| d.len()).unwrap_or(0) + ); + #[cfg(target_os = "windows")] + if let Some(data) = printer_data { + let printer_name = self + .handler + .printer_names + .write() + .unwrap() + .remove(&d.id); + // Spawn a new thread to handle the print job. + // Or print job will block the ui thread. + std::thread::spawn(move || { + if let Err(e) = + crate::platform::send_raw_data_to_printer( + printer_name, + data, + ) + { + log::error!("Print job error: {}", e); + } + }); + } + } + } + } + } + Some(file_response::Union::Error(e)) => { + let job_type = fs::remove_job(e.id, &mut self.write_jobs) + .or_else(|| fs::remove_job(e.id, &mut self.read_jobs)) + .map(|j| j.r#type) + .unwrap_or(fs::JobType::Generic); + match job_type { + fs::JobType::Generic => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + fs::JobType::Printer => { + log::error!("Printer job error: {}", e.error); + } + } + } + _ => {} + } + } + Some(message::Union::Misc(misc)) => match misc.union { + Some(misc::Union::AudioFormat(f)) => { + self.audio_sender.send(MediaData::AudioFormat(f)).ok(); + } + Some(misc::Union::ChatMessage(c)) => { + self.handler.new_message(c.text); + } + Some(misc::Union::PermissionInfo(p)) => { + log::info!("Change permission {:?} -> {}", p.permission, p.enabled); + // https://github.com/rustdesk/rustdesk/issues/3703#issuecomment-1474734754 + match p.permission.enum_value() { + Ok(Permission::Keyboard) => { + *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; + #[cfg(feature = "flutter")] + #[cfg(not(target_os = "ios"))] + crate::flutter::update_text_clipboard_required(); + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); + self.handler.set_permission("keyboard", p.enabled); + } + Ok(Permission::Clipboard) => { + *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; + #[cfg(feature = "flutter")] + #[cfg(not(target_os = "ios"))] + crate::flutter::update_text_clipboard_required(); + self.handler.set_permission("clipboard", p.enabled); + } + Ok(Permission::Audio) => { + self.handler.set_permission("audio", p.enabled); + } + Ok(Permission::File) => { + *self.handler.server_file_transfer_enabled.write().unwrap() = + p.enabled; + if !p.enabled && self.handler.is_file_transfer() { + return true; + } + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); + self.handler.set_permission("file", p.enabled); + #[cfg(feature = "unix-file-copy-paste")] + if !p.enabled { + try_empty_clipboard_files( + ClipboardSide::Client, + self.client_conn_id, + ); + } + } + Ok(Permission::Restart) => { + self.handler.set_permission("restart", p.enabled); + } + Ok(Permission::Recording) => { + self.handler.lc.write().unwrap().record_permission = p.enabled; + self.update_record_state(); + self.handler.set_permission("recording", p.enabled); + } + Ok(Permission::BlockInput) => { + self.handler.set_permission("block_input", p.enabled); + } + Ok(Permission::PrivacyMode) => { + self.handler.set_permission("privacy_mode", p.enabled); + } + _ => {} + } + } + Some(misc::Union::SwitchDisplay(s)) => { + self.handler.handle_peer_switch_display(&s); + if let Some(thread) = self.video_threads.get_mut(&(s.display as usize)) { + thread.video_sender.send(MediaData::Reset).ok(); + } + + let mut scale = 1.0; + if let Some(pi) = &self.handler.lc.read().unwrap().peer_info { + if let Some(d) = pi.displays.get(s.display as usize) { + scale = d.scale; + } + } + + if s.width > 0 && s.height > 0 { + self.handler.set_display( + s.x, + s.y, + s.width, + s.height, + s.cursor_embedded, + scale, + ); + } + } + Some(misc::Union::CloseReason(c)) => { + self.sent_close_reason = true; // The controlled end will close, no need to send close reason + self.handler.msgbox("error", "Connection Error", &c, ""); + return false; + } + Some(misc::Union::BackNotification(notification)) => { + if !self.handle_back_notification(notification).await { + return false; + } + } + Some(misc::Union::Uac(uac)) => { + let keyboard = self.handler.server_keyboard_enabled.read().unwrap().clone(); + #[cfg(feature = "flutter")] + { + if uac && keyboard { + self.handler.msgbox( + "on-uac", + "Prompt", + "Please wait for confirmation of UAC...", + "", + ); + } else { + self.handler.cancel_msgbox("on-uac"); + self.handler.cancel_msgbox("wait-uac"); + self.handler.cancel_msgbox("elevation-error"); + } + } + #[cfg(not(feature = "flutter"))] + { + let msgtype = "custom-uac-nocancel"; + let title = "Prompt"; + let text = "Please wait for confirmation of UAC..."; + let link = ""; + if uac && keyboard { + self.handler.msgbox(msgtype, title, text, link); + } else { + self.handler.cancel_msgbox(&format!( + "{}-{}-{}-{}", + msgtype, title, text, link, + )); + } + } + } + Some(misc::Union::ForegroundWindowElevated(elevated)) => { + let keyboard = self.handler.server_keyboard_enabled.read().unwrap().clone(); + #[cfg(feature = "flutter")] + { + if elevated && keyboard { + self.handler.msgbox( + "on-foreground-elevated", + "Prompt", + "elevated_foreground_window_tip", + "", + ); + } else { + self.handler.cancel_msgbox("on-foreground-elevated"); + self.handler.cancel_msgbox("wait-uac"); + self.handler.cancel_msgbox("elevation-error"); + } + } + #[cfg(not(feature = "flutter"))] + { + let msgtype = "custom-elevated-foreground-nocancel"; + let title = "Prompt"; + let text = "elevated_foreground_window_tip"; + let link = ""; + if elevated && keyboard { + self.handler.msgbox(msgtype, title, text, link); + } else { + self.handler.cancel_msgbox(&format!( + "{}-{}-{}-{}", + msgtype, title, text, link, + )); + } + } + } + Some(misc::Union::ElevationResponse(err)) => { + if err.is_empty() { + self.handler.msgbox("wait-uac", "", "", ""); + } else { + self.handler.cancel_msgbox("wait-uac"); + self.handler + .msgbox("elevation-error", "Elevation Error", &err, ""); + } + } + Some(misc::Union::PortableServiceRunning(b)) => { + self.handler.portable_service_running(b); + if self.elevation_requested && b { + self.handler.msgbox( + "custom-nocancel-success", + "Successful", + "Elevate successfully", + "", + ); + } + } + Some(misc::Union::SwitchBack(_)) => { + #[cfg(feature = "flutter")] + self.handler.switch_back(&self.handler.get_id()); + } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::PluginRequest(p)) => { + allow_err!(crate::plugin::handle_server_event( + &p.id, + &self.handler.get_id(), + &p.content + )); + // to-do: show message box on UI when error occurs? + } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::PluginFailure(p)) => { + let name = if p.name.is_empty() { + "plugin".to_string() + } else { + p.name + }; + self.handler.msgbox("custom-nocancel", &name, &p.msg, ""); + } + Some(misc::Union::SupportedEncoding(e)) => { + log::info!("update supported encoding:{:?}", e); + self.handler.lc.write().unwrap().supported_encoding = e; + } + Some(misc::Union::FollowCurrentDisplay(d_idx)) => { + self.handler.set_current_display(d_idx); + } + _ => {} + }, + Some(message::Union::TestDelay(t)) => { + self.handler.handle_test_delay(t, peer).await; + } + Some(message::Union::AudioFrame(frame)) => { + if !self.handler.lc.read().unwrap().disable_audio.v { + self.audio_sender + .send(MediaData::AudioFrame(Box::new(frame))) + .ok(); + } + } + Some(message::Union::FileAction(action)) => match action.union { + Some(file_action::Union::Send(_s)) => match _s.file_type.enum_value() { + #[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, + ); + #[cfg(not(feature = "flutter"))] + let action = ""; + if action == "dismiss" { + // Just ignore the incoming print job. + } else { + 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, + ); + #[cfg(not(feature = "flutter"))] + let allow_auto_print = false; + if allow_auto_print { + let printer_name = if action == "" { + "".to_string() + } else { + LocalConfig::get_option( + config::keys::OPTION_PRINTER_SELECTED_NAME, + ) + }; + self.handler.printer_response(id, _s.path, printer_name); + } else { + self.handler.printer_request(id, _s.path); + } + } + } + _ => {} + }, + Some(file_action::Union::SendConfirm(c)) => { + if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { + job.confirm(&c).await; + } + } + _ => {} + }, + Some(message::Union::MessageBox(msgbox)) => { + let mut link = msgbox.link; + if let Some(v) = config::HELPER_URL.get(&link as &str) { + link = v.to_string(); + } else { + log::warn!("Message box ignore link {} for security", &link); + link = "".to_string(); + } + self.handler + .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); + } + Some(message::Union::VoiceCallRequest(request)) => { + if request.is_connect { + // TODO: maybe we will do a voice call from the peer in the future. + } else { + log::debug!("The remote has requested to close the voice call"); + if let Some(sender) = self.stop_voice_call_sender.take() { + allow_err!(sender.send(())); + self.handler.on_voice_call_closed(""); + } + } + } + Some(message::Union::VoiceCallResponse(response)) => { + let ts = std::mem::replace(&mut self.voice_call_request_timestamp, None); + if let Some(ts) = ts { + if response.req_timestamp != ts.get() { + log::debug!("Possible encountering a voice call attack."); + } else { + if response.accepted { + // The peer accepted the voice call. + self.handler.on_voice_call_started(); + self.stop_voice_call_sender = self.start_voice_call(); + } else { + // The peer refused the voice call. + self.handler.on_voice_call_closed(""); + } + } + } + } + Some(message::Union::PeerInfo(pi)) => { + self.handler.set_displays(&pi.displays); + self.handler.set_platform_additions(&pi.platform_additions); + } + Some(message::Union::ScreenshotResponse(response)) => { + crate::client::screenshot::set_screenshot(response.data); + self.handler + .handle_screenshot_resp(response.sid, response.msg); + } + Some(message::Union::TerminalResponse(response)) => { + use hbb_common::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(); + let key = lc.get_key_terminal_service_id().to_owned(); + lc.set_option(key, opened.service_id.clone()); + } + } + self.handler.handle_terminal_response(response); + } + _ => {} + } + } + true + } + + fn set_peer_info(&mut self, pi: &PeerInfo) { + self.peer_info.platform = pi.platform.clone(); + + // Check features field for terminal support + if let Some(features) = pi.features.as_ref() { + self.peer_info.support_terminal = features.terminal; + } + + if let Ok(platform_additions) = + serde_json::from_str::>(&pi.platform_additions) + { + self.peer_info.is_installed = platform_additions + .get("is_installed") + .map(|v| v.as_bool()) + .flatten() + .unwrap_or(false); + self.peer_info.idd_impl = platform_additions + .get("idd_impl") + .map(|v| v.as_str()) + .flatten() + .unwrap_or_default() + .to_string(); + self.peer_info.support_view_camera = platform_additions + .get("support_view_camera") + .map(|v| v.as_bool()) + .flatten() + .unwrap_or(false); + } + } + + async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { + match notification.union { + Some(back_notification::Union::BlockInputState(state)) => { + self.handle_back_msg_block_input( + state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), + notification.details, + ) + .await; + } + Some(back_notification::Union::PrivacyModeState(state)) => { + if !self + .handle_back_msg_privacy_mode( + state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), + notification.details, + notification.impl_key, + ) + .await + { + return false; + } + } + _ => {} + } + true + } + + #[inline(always)] + fn update_block_input_state(&mut self, on: bool) { + self.handler.update_block_input_state(on); + } + + async fn handle_back_msg_block_input( + &mut self, + state: back_notification::BlockInputState, + details: String, + ) { + match state { + back_notification::BlockInputState::BlkOnSucceeded => { + self.update_block_input_state(true); + } + back_notification::BlockInputState::BlkOnFailed => { + self.handler.msgbox( + "custom-error", + "Block user input", + if details.is_empty() { + "Failed" + } else { + &details + }, + "", + ); + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffSucceeded => { + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffFailed => { + self.handler.msgbox( + "custom-error", + "Unblock user input", + if details.is_empty() { + "Failed" + } else { + &details + }, + "", + ); + } + _ => {} + } + } + + #[inline(always)] + fn update_privacy_mode(&mut self, impl_key: String, on: bool) { + let mut config = self.handler.load_config(); + config.privacy_mode.v = on; + if on { + // For compatibility, version < 1.2.4, the default value is 'privacy_mode_impl_mag'. + let impl_key = if impl_key.is_empty() { + "privacy_mode_impl_mag".to_string() + } else { + impl_key + }; + config + .options + .insert("privacy-mode-impl-key".to_string(), impl_key); + } + self.handler.save_config(config); + + self.handler.update_privacy_mode(); + } + + async fn handle_back_msg_privacy_mode( + &mut self, + state: back_notification::PrivacyModeState, + details: String, + impl_key: String, + ) -> bool { + match state { + back_notification::PrivacyModeState::PrvOnByOther => { + self.handler.msgbox( + "error", + "Connecting...", + "Someone turns on privacy mode, exit", + "", + ); + return false; + } + back_notification::PrivacyModeState::PrvNotSupported => { + self.handler + .msgbox("custom-error", "Privacy mode", "Unsupported", ""); + self.update_privacy_mode(impl_key, false); + } + back_notification::PrivacyModeState::PrvOnSucceeded => { + self.handler + .msgbox("custom-nocancel", "Privacy mode", "Enter privacy mode", ""); + self.update_privacy_mode(impl_key, true); + } + back_notification::PrivacyModeState::PrvOnFailedDenied => { + self.handler + .msgbox("custom-error", "Privacy mode", "Peer denied", ""); + self.update_privacy_mode(impl_key, false); + } + back_notification::PrivacyModeState::PrvOnFailedPlugin => { + self.handler + .msgbox("custom-error", "Privacy mode", "Please install plugins", ""); + self.update_privacy_mode(impl_key, false); + } + back_notification::PrivacyModeState::PrvOnFailed => { + self.handler.msgbox( + "custom-error", + "Privacy mode", + if details.is_empty() { + "Failed" + } else { + &details + }, + "", + ); + self.update_privacy_mode(impl_key, false); + } + back_notification::PrivacyModeState::PrvOffSucceeded => { + self.handler + .msgbox("custom-nocancel", "Privacy mode", "Exit privacy mode", ""); + self.update_privacy_mode(impl_key, false); + } + back_notification::PrivacyModeState::PrvOffByPeer => { + self.handler + .msgbox("custom-error", "Privacy mode", "Peer exit", ""); + self.update_privacy_mode(impl_key, false); + } + back_notification::PrivacyModeState::PrvOffFailed => { + self.handler.msgbox( + "custom-error", + "Privacy mode", + if details.is_empty() { + "Failed to turn off" + } else { + &details + }, + "", + ); + } + back_notification::PrivacyModeState::PrvOffUnknown => { + self.handler + .msgbox("custom-error", "Privacy mode", "Turned off", ""); + // log::error!("Privacy mode is turned off with unknown reason"); + self.update_privacy_mode(impl_key, false); + } + _ => {} + } + true + } + + #[cfg(all(target_os = "windows", not(feature = "flutter")))] + fn check_clipboard_file_context(&self) { + let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() + && self.handler.lc.read().unwrap().enable_file_copy_paste.v; + ContextSend::enable(enabled); + } + + #[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, + ) { + log::debug!("handling cliprdr msg from server peer"); + #[cfg(feature = "flutter")] + if let Some(hbb_common::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) + { + return; + } + } + + let Some(clip) = crate::clipboard_file::msg_2_clip(clip) else { + log::warn!("failed to decode cliprdr msg from server peer"); + return; + }; + + let is_stopping_allowed = clip.is_beginning_message(); + let file_transfer_enabled = self.handler.is_file_clipboard_required(); + let stop = is_stopping_allowed && !file_transfer_enabled; + log::debug!( + "Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", + stop, is_stopping_allowed, file_transfer_enabled); + if !stop { + #[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") + ))] + if let Err(e) = ContextSend::make_sure_enabled() { + log::error!("failed to restart clipboard context: {}", e); + }; + #[cfg(target_os = "windows")] + { + let _ = ContextSend::proc(|context| -> ResultType<()> { + context + .server_clip_file(self.client_conn_id, clip) + .map_err(|e| e.into()) + }); + } + #[cfg(feature = "unix-file-copy-paste")] + if crate::is_support_file_copy_paste_num(self.handler.lc.read().unwrap().version) { + let mut out_msgs = vec![]; + + #[cfg(target_os = "macos")] + if clipboard::platform::unix::macos::should_handle_msg(&clip) { + if let Err(e) = ContextSend::proc(|context| -> ResultType<()> { + context + .server_clip_file(self.client_conn_id, clip) + .map_err(|e| e.into()) + }) { + log::error!("failed to handle cliprdr msg: {}", e); + } + } else { + out_msgs = unix_file_clip::serve_clip_messages( + ClipboardSide::Client, + clip, + self.client_conn_id, + ); + } + + #[cfg(not(target_os = "macos"))] + { + out_msgs = unix_file_clip::serve_clip_messages( + ClipboardSide::Client, + clip, + self.client_conn_id, + ); + } + + for msg in out_msgs.into_iter() { + allow_err!(_peer.send(&msg).await); + } + } + } + } + + fn new_video_thread(&mut self, display: usize) { + let video_queue = Arc::new(RwLock::new(ArrayQueue::new(client::VIDEO_QUEUE_SIZE))); + let (video_sender, video_receiver) = std::sync::mpsc::channel::(); + let decode_fps = Arc::new(RwLock::new(None)); + let frame_count = Arc::new(RwLock::new(0)); + let discard_queue = Arc::new(RwLock::new(false)); + let video_thread = VideoThread { + video_queue: video_queue.clone(), + video_sender, + decode_fps: decode_fps.clone(), + frame_count: frame_count.clone(), + fps_control: Default::default(), + discard_queue: discard_queue.clone(), + }; + let handler = self.handler.ui_handler.clone(); + crate::client::start_video_thread( + self.handler.clone(), + display, + video_receiver, + video_queue, + decode_fps, + self.chroma.clone(), + discard_queue, + move |display: usize, + data: &mut scrap::ImageRgb, + _texture: *mut c_void, + pixelbuffer: bool| { + *frame_count.write().unwrap() += 1; + if pixelbuffer { + handler.on_rgba(display, data); + } else { + #[cfg(all(feature = "vram", feature = "flutter"))] + handler.on_texture(display, _texture); + } + }, + ); + 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); + self.handler.lc.write().unwrap().record_state = auto_record; + self.update_record_state(); + } + } + + fn update_record_state(&mut self) { + // state + let permission = self.handler.lc.read().unwrap().record_permission; + if !permission { + self.handler.lc.write().unwrap().record_state = false; + } + let state = self.handler.lc.read().unwrap().record_state; + let start = state && permission; + if self.last_record_state == start { + return; + } + self.last_record_state = start; + log::info!("record screen start: {start}"); + // update local + for (_, v) in self.video_threads.iter_mut() { + v.video_sender.send(MediaData::RecordScreen(start)).ok(); + } + self.handler.update_record_status(start); + // update remote + let mut misc = Misc::new(); + misc.set_client_record_status(start); + let mut msg = Message::new(); + msg.set_misc(misc); + self.sender.send(Data::Message(msg)).ok(); + } +} + +struct RemoveJob { + files: Vec, + path: String, + sep: &'static str, + is_remote: bool, + no_confirm: bool, + last_update_job_status: Instant, +} + +impl RemoveJob { + fn new(files: Vec, path: String, sep: &'static str, is_remote: bool) -> Self { + Self { + files, + path, + sep, + is_remote, + no_confirm: false, + last_update_job_status: Instant::now(), + } + } + + pub fn _gen_meta(&self) -> RemoveJobMeta { + RemoveJobMeta { + path: self.path.clone(), + is_remote: self.is_remote, + no_confirm: self.no_confirm, + } + } +} + +#[derive(Debug, Default)] +struct FpsControl { + refresh_times: usize, + last_refresh_instant: Option, + idle_counter: usize, + inactive_counter: usize, +} + +struct VideoThread { + video_queue: Arc>>, + video_sender: MediaSender, + decode_fps: Arc>>, + frame_count: Arc>, + discard_queue: Arc>, + fps_control: FpsControl, +} + +impl Drop for VideoThread { + fn drop(&mut self) { + // since channels are buffered, messages sent before the disconnect will still be properly received. + *self.discard_queue.write().unwrap() = true; + } +} diff --git a/vendor/rustdesk/src/client/screenshot.rs b/vendor/rustdesk/src/client/screenshot.rs new file mode 100644 index 0000000..82a95be --- /dev/null +++ b/vendor/rustdesk/src/client/screenshot.rs @@ -0,0 +1,99 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::{update_clipboard, ClipboardSide}; +use hbb_common::{message_proto::*, ResultType}; +use std::sync::Mutex; + +lazy_static::lazy_static! { + static ref SCREENSHOT: Mutex = Default::default(); +} + +pub enum ScreenshotAction { + SaveAs(String), + CopyToClipboard, + Discard, +} + +impl Default for ScreenshotAction { + fn default() -> Self { + Self::Discard + } +} + +impl From<&str> for ScreenshotAction { + fn from(value: &str) -> Self { + match value.chars().next() { + Some('0') => { + if let Some((pos, _)) = value.char_indices().nth(2) { + let substring = &value[pos..]; + Self::SaveAs(substring.to_string()) + } else { + Self::default() + } + } + Some('1') => Self::CopyToClipboard, + Some('2') => Self::default(), + _ => Self::default(), + } + } +} + +impl Into for ScreenshotAction { + fn into(self) -> String { + match self { + Self::SaveAs(p) => format!("0:{p}"), + Self::CopyToClipboard => "1".to_owned(), + Self::Discard => "2".to_owned(), + } + } +} + +#[derive(Default)] +pub struct Screenshot { + data: Option, +} + +impl Screenshot { + fn set_screenshot(&mut self, data: bytes::Bytes) { + self.data.replace(data); + } + + fn handle_screenshot(&mut self, action: String) -> String { + let Some(data) = self.data.take() else { + return "No cached screenshot".to_owned(); + }; + match Self::handle_screenshot_(data, action) { + Ok(()) => "".to_owned(), + Err(e) => e.to_string(), + } + } + + fn handle_screenshot_(data: bytes::Bytes, action: String) -> ResultType<()> { + match ScreenshotAction::from(&action as &str) { + ScreenshotAction::SaveAs(p) => { + std::fs::write(p, data)?; + } + ScreenshotAction::CopyToClipboard => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let clips = vec![Clipboard { + compress: false, + content: data, + format: ClipboardFormat::ImagePng.into(), + ..Default::default() + }]; + update_clipboard(clips, ClipboardSide::Client); + } + } + ScreenshotAction::Discard => {} + } + Ok(()) + } +} + +pub fn set_screenshot(data: bytes::Bytes) { + SCREENSHOT.lock().unwrap().set_screenshot(data); +} + +pub fn handle_screenshot(action: String) -> String { + SCREENSHOT.lock().unwrap().handle_screenshot(action) +} diff --git a/vendor/rustdesk/src/clipboard.rs b/vendor/rustdesk/src/clipboard.rs new file mode 100644 index 0000000..cae7d03 --- /dev/null +++ b/vendor/rustdesk/src/clipboard.rs @@ -0,0 +1,885 @@ +#[cfg(not(target_os = "android"))] +use arboard::{ClipboardData, ClipboardFormat}; +use hbb_common::{bail, log, message_proto::*, ResultType}; +use std::{ + sync::{Arc, Mutex}, + time::Duration, +}; + +pub const CLIPBOARD_NAME: &'static str = "clipboard"; +#[cfg(feature = "unix-file-copy-paste")] +pub const FILE_CLIPBOARD_NAME: &'static str = "file-clipboard"; +pub const CLIPBOARD_INTERVAL: u64 = 333; + +// This format is used to store the flag in the clipboard. +const RUSTDESK_CLIPBOARD_OWNER_FORMAT: &'static str = "dyn.com.rustdesk.owner"; + +// Add special format for Excel XML Spreadsheet +const CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET: &'static str = "XML Spreadsheet"; + +#[cfg(not(target_os = "android"))] +lazy_static::lazy_static! { + static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); + // cache the clipboard msg + static ref LAST_MULTI_CLIPBOARDS: Arc> = Arc::new(Mutex::new(MultiClipboards::new())); + // For updating in server and getting content in cm. + // Clipboard on Linux is "server--clients" mode. + // The clipboard content is owned by the server and passed to the clients when requested. + // Plain text is the only exception, it does not require the server to be present. + static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); +} + +#[cfg(not(target_os = "android"))] +const CLIPBOARD_GET_MAX_RETRY: usize = 3; +#[cfg(not(target_os = "android"))] +const CLIPBOARD_GET_RETRY_INTERVAL_DUR: Duration = Duration::from_millis(33); + +#[cfg(not(target_os = "android"))] +const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ + ClipboardFormat::Text, + ClipboardFormat::Html, + ClipboardFormat::Rtf, + ClipboardFormat::ImageRgba, + ClipboardFormat::ImagePng, + ClipboardFormat::ImageSvg, + #[cfg(feature = "unix-file-copy-paste")] + ClipboardFormat::FileUrl, + ClipboardFormat::Special(CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET), + ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), +]; + +#[cfg(not(target_os = "android"))] +pub fn check_clipboard( + ctx: &mut Option, + side: ClipboardSide, + force: bool, +) -> Option { + if ctx.is_none() { + *ctx = ClipboardContext::new().ok(); + } + let ctx2 = ctx.as_mut()?; + match ctx2.get(side, force) { + Ok(content) => { + if !content.is_empty() { + let mut msg = Message::new(); + let clipboards = proto::create_multi_clipboards(content); + msg.set_multi_clipboards(clipboards.clone()); + *LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; + return Some(msg); + } + } + Err(e) => { + log::error!("Failed to get clipboard content. {}", e); + } + } + None +} + +#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))] +pub fn is_file_url_set_by_rustdesk(url: &Vec) -> bool { + if url.len() != 1 { + return false; + } + url.iter() + .next() + .map(|s| { + for prefix in &["file:///tmp/.rustdesk_", "//tmp/.rustdesk_"] { + if s.starts_with(prefix) { + return s[prefix.len()..].parse::().is_ok(); + } + } + false + }) + .unwrap_or(false) +} + +#[cfg(feature = "unix-file-copy-paste")] +pub fn check_clipboard_files( + ctx: &mut Option, + side: ClipboardSide, + force: bool, +) -> Option> { + if ctx.is_none() { + *ctx = ClipboardContext::new().ok(); + } + let ctx2 = ctx.as_mut()?; + match ctx2.get_files(side, force) { + Ok(Some(urls)) => { + if !urls.is_empty() { + return Some(urls); + } + } + Err(e) => { + log::error!("Failed to get clipboard file urls. {}", e); + } + _ => {} + } + None +} + +#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] +pub fn update_clipboard_files(files: Vec, side: ClipboardSide) { + if !files.is_empty() { + std::thread::spawn(move || { + do_update_clipboard_(vec![ClipboardData::FileUrl(files)], side); + }); + } +} + +#[cfg(feature = "unix-file-copy-paste")] +pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) { + std::thread::spawn(move || { + let mut ctx = CLIPBOARD_CTX.lock().unwrap(); + if ctx.is_none() { + match ClipboardContext::new() { + Ok(x) => { + *ctx = Some(x); + } + Err(e) => { + log::error!("Failed to create clipboard context: {}", e); + return; + } + } + } + #[allow(unused_mut)] + if let Some(mut ctx) = ctx.as_mut() { + #[cfg(target_os = "linux")] + { + use clipboard::platform::unix; + if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) { + ctx.try_empty_clipboard_files(_side); + } + } + #[cfg(target_os = "macos")] + { + ctx.try_empty_clipboard_files(_side); + // No need to make sure the context is enabled. + clipboard::ContextSend::proc(|context| -> ResultType<()> { + context.empty_clipboard(_conn_id).ok(); + Ok(()) + }) + .ok(); + } + } + }); +} + +#[cfg(target_os = "windows")] +pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) { + log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id); + let _ = clipboard::ContextSend::proc(|context| -> ResultType<()> { + context.empty_clipboard(conn_id)?; + Ok(()) + }); +} + +#[cfg(target_os = "windows")] +pub fn check_clipboard_cm() -> ResultType { + let mut ctx = CLIPBOARD_CTX.lock().unwrap(); + if ctx.is_none() { + match ClipboardContext::new() { + Ok(x) => { + *ctx = Some(x); + } + Err(e) => { + hbb_common::bail!("Failed to create clipboard context: {}", e); + } + } + } + if let Some(ctx) = ctx.as_mut() { + let content = ctx.get(ClipboardSide::Host, false)?; + let clipboards = proto::create_multi_clipboards(content); + Ok(clipboards) + } else { + hbb_common::bail!("Failed to create clipboard context"); + } +} + +#[cfg(not(target_os = "android"))] +fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { + let to_update_data = proto::from_multi_clipboards(multi_clipboards); + if to_update_data.is_empty() { + return; + } + do_update_clipboard_(to_update_data, side); +} + +#[cfg(not(target_os = "android"))] +fn do_update_clipboard_(mut to_update_data: Vec, side: ClipboardSide) { + let mut ctx = CLIPBOARD_CTX.lock().unwrap(); + if ctx.is_none() { + match ClipboardContext::new() { + Ok(x) => { + *ctx = Some(x); + } + Err(e) => { + log::error!("Failed to create clipboard context: {}", e); + return; + } + } + } + if let Some(ctx) = ctx.as_mut() { + to_update_data.push(ClipboardData::Special(( + RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(), + side.get_owner_data(), + ))); + if let Err(e) = ctx.set(&to_update_data) { + log::debug!("Failed to set clipboard: {}", e); + } else { + log::debug!("{} updated on {}", CLIPBOARD_NAME, side); + } + } +} + +#[cfg(not(target_os = "android"))] +pub fn update_clipboard(multi_clipboards: Vec, side: ClipboardSide) { + std::thread::spawn(move || { + update_clipboard_(multi_clipboards, side); + }); +} + +#[cfg(not(target_os = "android"))] +pub struct ClipboardContext { + inner: arboard::Clipboard, +} + +#[cfg(not(target_os = "android"))] +#[allow(unreachable_code)] +impl ClipboardContext { + pub fn new() -> ResultType { + let board; + #[cfg(not(target_os = "linux"))] + { + board = arboard::Clipboard::new()?; + } + #[cfg(target_os = "linux")] + { + let mut i = 1; + loop { + // Try 5 times to create clipboard + // Arboard::new() connect to X server or Wayland compositor, which should be OK most times + // But sometimes, the connection may fail, so we retry here. + match arboard::Clipboard::new() { + Ok(x) => { + board = x; + break; + } + Err(e) => { + if i == 5 { + return Err(e.into()); + } else { + std::thread::sleep(std::time::Duration::from_millis(30 * i)); + } + } + } + i += 1; + } + } + + Ok(ClipboardContext { inner: board }) + } + + fn get_formats(&mut self, formats: &[ClipboardFormat]) -> ResultType> { + // If there're multiple threads or processes trying to access the clipboard at the same time, + // the previous clipboard owner will fail to access the clipboard. + // `GetLastError()` will return `ERROR_CLIPBOARD_NOT_OPEN` (OSError(1418): Thread does not have a clipboard open) at this time. + // See https://github.com/rustdesk-org/arboard/blob/747ab2d9b40a5c9c5102051cf3b0bb38b4845e60/src/platform/windows.rs#L34 + // + // This is a common case on Windows, so we retry here. + // Related issues: + // https://github.com/rustdesk/rustdesk/issues/9263 + // https://github.com/rustdesk/rustdesk/issues/9222#issuecomment-2329233175 + for i in 0..CLIPBOARD_GET_MAX_RETRY { + match self.inner.get_formats(formats) { + Ok(data) => { + return Ok(data + .into_iter() + .filter(|c| !matches!(c, arboard::ClipboardData::None)) + .collect()) + } + Err(e) => match e { + arboard::Error::ClipboardOccupied => { + log::debug!("Failed to get clipboard formats, clipboard is occupied, retrying... {}", i + 1); + std::thread::sleep(CLIPBOARD_GET_RETRY_INTERVAL_DUR); + } + _ => { + log::error!("Failed to get clipboard formats, {}", e); + return Err(e.into()); + } + }, + } + } + bail!("Failed to get clipboard formats, clipboard is occupied, {CLIPBOARD_GET_MAX_RETRY} retries failed"); + } + + pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType> { + let data = self.get_formats_filter(SUPPORTED_FORMATS, side, force)?; + // We have a separate service named `file-clipboard` to handle file copy-paste. + // We need to read the file urls because file copy may set the other clipboard formats such as text. + #[cfg(feature = "unix-file-copy-paste")] + { + if data.iter().any(|c| matches!(c, ClipboardData::FileUrl(_))) { + return Ok(vec![]); + } + } + Ok(data) + } + + fn get_formats_filter( + &mut self, + formats: &[ClipboardFormat], + side: ClipboardSide, + force: bool, + ) -> ResultType> { + let _lock = ARBOARD_MTX.lock().unwrap(); + let data = self.get_formats(formats)?; + if data.is_empty() { + return Ok(data); + } + if !force { + for c in data.iter() { + if let ClipboardData::Special((s, d)) = c { + if s == RUSTDESK_CLIPBOARD_OWNER_FORMAT && side.is_owner(d) { + return Ok(vec![]); + } + } + } + } + Ok(data + .into_iter() + .filter(|c| match c { + ClipboardData::Special((s, _)) => s != RUSTDESK_CLIPBOARD_OWNER_FORMAT, + // Skip synchronizing empty text to the remote clipboard + ClipboardData::Text(text) => !text.is_empty(), + _ => true, + }) + .collect()) + } + + #[cfg(feature = "unix-file-copy-paste")] + pub fn get_files( + &mut self, + side: ClipboardSide, + force: bool, + ) -> ResultType>> { + let data = self.get_formats_filter( + &[ + ClipboardFormat::FileUrl, + ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), + ], + side, + force, + )?; + Ok(data.into_iter().find_map(|c| match c { + ClipboardData::FileUrl(urls) => Some(urls), + _ => None, + })) + } + + fn set(&mut self, data: &[ClipboardData]) -> ResultType<()> { + let _lock = ARBOARD_MTX.lock().unwrap(); + self.inner.set_formats(data)?; + Ok(()) + } + + #[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))] + fn get_file_urls_set_by_rustdesk( + data: Vec, + _side: ClipboardSide, + ) -> Vec { + for item in data.into_iter() { + if let ClipboardData::FileUrl(urls) = item { + if is_file_url_set_by_rustdesk(&urls) { + return urls; + } + } + } + vec![] + } + + #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] + fn get_file_urls_set_by_rustdesk(data: Vec, side: ClipboardSide) -> Vec { + let exclude_path = + clipboard::platform::unix::fuse::get_exclude_paths(side == ClipboardSide::Client); + data.into_iter() + .filter_map(|c| match c { + ClipboardData::FileUrl(urls) => Some( + urls.into_iter() + .filter(|s| s.starts_with(&*exclude_path)) + .collect::>(), + ), + _ => None, + }) + .flatten() + .collect::>() + } + + #[cfg(feature = "unix-file-copy-paste")] + fn try_empty_clipboard_files(&mut self, side: ClipboardSide) { + let _lock = ARBOARD_MTX.lock().unwrap(); + if let Ok(data) = self.get_formats(&[ClipboardFormat::FileUrl]) { + let urls = Self::get_file_urls_set_by_rustdesk(data, side); + if !urls.is_empty() { + // FIXME: + // 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. + // It's not correct in the server process. + #[cfg(target_os = "linux")] + let is_kde_x11 = hbb_common::platform::linux::is_kde_session() + && crate::platform::linux::is_x11(); + #[cfg(target_os = "macos")] + let is_kde_x11 = false; + let clear_holder_text = if is_kde_x11 { + "RustDesk placeholder to clear the file clipboard" + } else { + "" + } + .to_string(); + self.inner + .set_formats(&[ + ClipboardData::Text(clear_holder_text), + ClipboardData::Special(( + RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(), + side.get_owner_data(), + )), + ]) + .ok(); + } + } + } +} + +pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool { + use hbb_common::get_version_number; + if get_version_number(peer_version) < get_version_number("1.3.0") { + return false; + } + if ["", &hbb_common::whoami::Platform::Ios.to_string()].contains(&peer_platform) { + return false; + } + if "Android" == peer_platform && get_version_number(peer_version) < get_version_number("1.3.3") + { + return false; + } + true +} + +#[cfg(not(target_os = "android"))] +pub fn get_current_clipboard_msg( + peer_version: &str, + peer_platform: &str, + side: ClipboardSide, +) -> Option { + let mut multi_clipboards = LAST_MULTI_CLIPBOARDS.lock().unwrap(); + if multi_clipboards.clipboards.is_empty() { + let mut ctx = ClipboardContext::new().ok()?; + *multi_clipboards = proto::create_multi_clipboards(ctx.get(side, true).ok()?); + } + if multi_clipboards.clipboards.is_empty() { + return None; + } + + if is_support_multi_clipboard(peer_version, peer_platform) { + let mut msg = Message::new(); + msg.set_multi_clipboards(multi_clipboards.clone()); + Some(msg) + } else { + // Find the first text clipboard and send it. + multi_clipboards + .clipboards + .iter() + .find(|c| c.format.enum_value() == Ok(hbb_common::message_proto::ClipboardFormat::Text)) + .map(|c| { + let mut msg = Message::new(); + msg.set_clipboard(c.clone()); + msg + }) + } +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum ClipboardSide { + Host, + Client, +} + +impl ClipboardSide { + // 01: the clipboard is owned by the host + // 10: the clipboard is owned by the client + fn get_owner_data(&self) -> Vec { + match self { + ClipboardSide::Host => vec![0b01], + ClipboardSide::Client => vec![0b10], + } + } + + fn is_owner(&self, data: &[u8]) -> bool { + if data.len() == 0 { + return false; + } + data[0] & 0b11 != 0 + } +} + +impl std::fmt::Display for ClipboardSide { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ClipboardSide::Host => write!(f, "host"), + ClipboardSide::Client => write!(f, "client"), + } + } +} + +pub use proto::get_msg_if_not_support_multi_clip; +mod proto { + #[cfg(not(target_os = "android"))] + use arboard::ClipboardData; + use hbb_common::{ + compress::{compress as compress_func, decompress}, + message_proto::{Clipboard, ClipboardFormat, Message, MultiClipboards}, + }; + + fn plain_to_proto(s: String, format: ClipboardFormat) -> Clipboard { + let compressed = compress_func(s.as_bytes()); + let compress = compressed.len() < s.as_bytes().len(); + let content = if compress { + compressed + } else { + s.bytes().collect::>() + }; + Clipboard { + compress, + content: content.into(), + format: format.into(), + ..Default::default() + } + } + + #[cfg(not(target_os = "android"))] + fn image_to_proto(a: arboard::ImageData) -> Clipboard { + match &a { + arboard::ImageData::Rgba(rgba) => { + let compressed = compress_func(&a.bytes()); + let compress = compressed.len() < a.bytes().len(); + let content = if compress { + compressed + } else { + a.bytes().to_vec() + }; + Clipboard { + compress, + content: content.into(), + width: rgba.width as _, + height: rgba.height as _, + format: ClipboardFormat::ImageRgba.into(), + ..Default::default() + } + } + arboard::ImageData::Png(png) => Clipboard { + compress: false, + content: png.to_owned().to_vec().into(), + format: ClipboardFormat::ImagePng.into(), + ..Default::default() + }, + arboard::ImageData::Svg(_) => { + let compressed = compress_func(&a.bytes()); + let compress = compressed.len() < a.bytes().len(); + let content = if compress { + compressed + } else { + a.bytes().to_vec() + }; + Clipboard { + compress, + content: content.into(), + format: ClipboardFormat::ImageSvg.into(), + ..Default::default() + } + } + } + } + + fn special_to_proto(d: Vec, s: String) -> Clipboard { + let compressed = compress_func(&d); + let compress = compressed.len() < d.len(); + let content = if compress { + compressed + } else { + s.bytes().collect::>() + }; + Clipboard { + compress, + content: content.into(), + format: ClipboardFormat::Special.into(), + special_name: s, + ..Default::default() + } + } + + #[cfg(not(target_os = "android"))] + fn clipboard_data_to_proto(data: ClipboardData) -> Option { + let d = match data { + ClipboardData::Text(s) => plain_to_proto(s, ClipboardFormat::Text), + ClipboardData::Rtf(s) => plain_to_proto(s, ClipboardFormat::Rtf), + ClipboardData::Html(s) => plain_to_proto(s, ClipboardFormat::Html), + ClipboardData::Image(a) => image_to_proto(a), + ClipboardData::Special((s, d)) => special_to_proto(d, s), + _ => return None, + }; + Some(d) + } + + #[cfg(not(target_os = "android"))] + pub fn create_multi_clipboards(vec_data: Vec) -> MultiClipboards { + MultiClipboards { + clipboards: vec_data + .into_iter() + .filter_map(clipboard_data_to_proto) + .collect(), + ..Default::default() + } + } + + #[cfg(not(target_os = "android"))] + fn from_clipboard(clipboard: Clipboard) -> Option { + let data = if clipboard.compress { + decompress(&clipboard.content) + } else { + clipboard.content.into() + }; + match clipboard.format.enum_value() { + Ok(ClipboardFormat::Text) => String::from_utf8(data).ok().map(ClipboardData::Text), + Ok(ClipboardFormat::Rtf) => String::from_utf8(data).ok().map(ClipboardData::Rtf), + Ok(ClipboardFormat::Html) => String::from_utf8(data).ok().map(ClipboardData::Html), + Ok(ClipboardFormat::ImageRgba) => Some(ClipboardData::Image(arboard::ImageData::rgba( + clipboard.width as _, + clipboard.height as _, + data.into(), + ))), + Ok(ClipboardFormat::ImagePng) => { + Some(ClipboardData::Image(arboard::ImageData::png(data.into()))) + } + Ok(ClipboardFormat::ImageSvg) => Some(ClipboardData::Image(arboard::ImageData::svg( + std::str::from_utf8(&data).unwrap_or_default(), + ))), + Ok(ClipboardFormat::Special) => { + Some(ClipboardData::Special((clipboard.special_name, data))) + } + _ => None, + } + } + + #[cfg(not(target_os = "android"))] + pub fn from_multi_clipboards(multi_clipboards: Vec) -> Vec { + multi_clipboards + .into_iter() + .filter_map(from_clipboard) + .collect() + } + + pub fn get_msg_if_not_support_multi_clip( + version: &str, + platform: &str, + multi_clipboards: &MultiClipboards, + ) -> Option { + if crate::clipboard::is_support_multi_clipboard(version, platform) { + return None; + } + + // Find the first text clipboard and send it. + multi_clipboards + .clipboards + .iter() + .find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text)) + .map(|c| { + let mut msg = Message::new(); + msg.set_clipboard(c.clone()); + msg + }) + } +} + +#[cfg(target_os = "android")] +pub fn handle_msg_clipboard(mut cb: Clipboard) { + use hbb_common::protobuf::Message; + + if cb.compress { + cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content)); + } + let multi_clips = MultiClipboards { + clipboards: vec![cb], + ..Default::default() + }; + if let Ok(bytes) = multi_clips.write_to_bytes() { + let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes); + } +} + +#[cfg(target_os = "android")] +pub fn handle_msg_multi_clipboards(mut mcb: MultiClipboards) { + use hbb_common::protobuf::Message; + + for cb in mcb.clipboards.iter_mut() { + if cb.compress { + cb.content = bytes::Bytes::from(hbb_common::compress::decompress(&cb.content)); + } + } + if let Ok(bytes) = mcb.write_to_bytes() { + let _ = scrap::android::ffi::call_clipboard_manager_update_clipboard(&bytes); + } +} + +#[cfg(target_os = "android")] +pub fn get_clipboards_msg(client: bool) -> Option { + let mut clipboards = scrap::android::ffi::get_clipboards(client)?; + let mut msg = Message::new(); + for c in &mut clipboards.clipboards { + let compressed = hbb_common::compress::compress(&c.content); + let compress = compressed.len() < c.content.len(); + if compress { + c.content = compressed.into(); + } + c.compress = compress; + } + msg.set_multi_clipboards(clipboards); + Some(msg) +} + +// We need this mod to notify multiple subscribers when the clipboard changes. +// Because only one clipboard master(listener) can trigger the clipboard change event multiple listeners are created on Linux(x11). +// https://github.com/rustdesk-org/clipboard-master/blob/4fb62e5b62fb6350d82b571ec7ba94b3cd466695/src/master/x11.rs#L226 +#[cfg(not(target_os = "android"))] +pub mod clipboard_listener { + use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown}; + use hbb_common::{bail, log, ResultType}; + use std::{ + collections::HashMap, + io, + sync::mpsc::{channel, Sender}, + sync::{Arc, Mutex}, + thread::JoinHandle, + }; + + lazy_static::lazy_static! { + pub static ref CLIPBOARD_LISTENER: Arc> = Default::default(); + } + + struct Handler { + subscribers: Arc>>>, + } + + impl ClipboardHandler for Handler { + fn on_clipboard_change(&mut self) -> CallbackResult { + let sub_lock = self.subscribers.lock().unwrap(); + for tx in sub_lock.values() { + tx.send(CallbackResult::Next).ok(); + } + CallbackResult::Next + } + + fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { + let msg = format!("Clipboard listener error: {}", error); + let sub_lock = self.subscribers.lock().unwrap(); + for tx in sub_lock.values() { + tx.send(CallbackResult::StopWithError(io::Error::new( + io::ErrorKind::Other, + msg.clone(), + ))) + .ok(); + } + CallbackResult::Next + } + } + + #[derive(Default)] + pub struct ClipboardListener { + subscribers: Arc>>>, + handle: Option<(Shutdown, JoinHandle<()>)>, + } + + pub fn subscribe(name: String, tx: Sender) -> ResultType<()> { + log::info!("Subscribe clipboard listener: {}", &name); + let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap(); + listener_lock + .subscribers + .lock() + .unwrap() + .insert(name.clone(), tx); + + if listener_lock.handle.is_none() { + log::info!("Start clipboard listener thread"); + let handler = Handler { + subscribers: listener_lock.subscribers.clone(), + }; + let (tx_start_res, rx_start_res) = channel(); + let h = start_clipboard_master_thread(handler, tx_start_res); + let shutdown = match rx_start_res.recv() { + Ok((Some(s), _)) => s, + Ok((None, err)) => { + bail!(err); + } + + Err(e) => { + bail!("Failed to create clipboard listener: {}", e); + } + }; + listener_lock.handle = Some((shutdown, h)); + log::info!("Clipboard listener thread started"); + } + + log::info!("Clipboard listener subscribed: {}", name); + Ok(()) + } + + pub fn unsubscribe(name: &str) { + log::info!("Unsubscribe clipboard listener: {}", name); + let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap(); + let is_empty = { + let mut sub_lock = listener_lock.subscribers.lock().unwrap(); + if let Some(tx) = sub_lock.remove(name) { + tx.send(CallbackResult::Stop).ok(); + } + sub_lock.is_empty() + }; + if is_empty { + if let Some((shutdown, h)) = listener_lock.handle.take() { + log::info!("Stop clipboard listener thread"); + shutdown.signal(); + h.join().ok(); + log::info!("Clipboard listener thread stopped"); + } + } + log::info!("Clipboard listener unsubscribed: {}", name); + } + + fn start_clipboard_master_thread( + handler: impl ClipboardHandler + Send + 'static, + tx_start_res: Sender<(Option, String)>, + ) -> JoinHandle<()> { + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage#:~:text=The%20window%20must%20belong%20to%20the%20current%20thread. + let h = std::thread::spawn(move || match Master::new(handler) { + Ok(mut master) => { + tx_start_res + .send((Some(master.shutdown_channel()), "".to_owned())) + .ok(); + log::debug!("Clipboard listener started"); + if let Err(err) = master.run() { + log::error!("Failed to run clipboard listener: {}", err); + } else { + log::debug!("Clipboard listener stopped"); + } + } + Err(err) => { + tx_start_res + .send(( + None, + format!("Failed to create clipboard listener: {}", err), + )) + .ok(); + } + }); + h + } +} diff --git a/vendor/rustdesk/src/clipboard_file.rs b/vendor/rustdesk/src/clipboard_file.rs new file mode 100644 index 0000000..724d8ae --- /dev/null +++ b/vendor/rustdesk/src/clipboard_file.rs @@ -0,0 +1,427 @@ +use clipboard::ClipboardFile; +use hbb_common::message_proto::*; + +pub fn clip_2_msg(clip: ClipboardFile) -> Message { + match clip { + ClipboardFile::NotifyCallback { + r#type, + title, + text, + } => Message { + union: Some(message::Union::MessageBox(MessageBox { + msgtype: r#type, + title, + text, + link: "".to_string(), + ..Default::default() + })), + ..Default::default() + }, + ClipboardFile::MonitorReady => Message { + union: Some(message::Union::Cliprdr(Cliprdr { + union: Some(cliprdr::Union::Ready(CliprdrMonitorReady { + ..Default::default() + })), + ..Default::default() + })), + ..Default::default() + }, + ClipboardFile::FormatList { format_list } => { + let mut formats: Vec = Vec::new(); + for v in format_list.iter() { + formats.push(CliprdrFormat { + id: v.0, + format: v.1.clone(), + ..Default::default() + }); + } + Message { + union: Some(message::Union::Cliprdr(Cliprdr { + union: Some(cliprdr::Union::FormatList(CliprdrServerFormatList { + formats, + ..Default::default() + })), + ..Default::default() + })), + ..Default::default() + } + } + ClipboardFile::FormatListResponse { msg_flags } => Message { + union: Some(message::Union::Cliprdr(Cliprdr { + union: Some(cliprdr::Union::FormatListResponse( + CliprdrServerFormatListResponse { + msg_flags, + ..Default::default() + }, + )), + ..Default::default() + })), + ..Default::default() + }, + ClipboardFile::FormatDataRequest { + requested_format_id, + } => Message { + union: Some(message::Union::Cliprdr(Cliprdr { + union: Some(cliprdr::Union::FormatDataRequest( + CliprdrServerFormatDataRequest { + requested_format_id, + ..Default::default() + }, + )), + ..Default::default() + })), + ..Default::default() + }, + ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + } => Message { + union: Some(message::Union::Cliprdr(Cliprdr { + union: Some(cliprdr::Union::FormatDataResponse( + CliprdrServerFormatDataResponse { + msg_flags, + format_data: format_data.into(), + ..Default::default() + }, + )), + ..Default::default() + })), + ..Default::default() + }, + ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id, + clip_data_id, + } => Message { + union: Some(message::Union::Cliprdr(Cliprdr { + union: Some(cliprdr::Union::FileContentsRequest( + CliprdrFileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + have_clip_data_id, + clip_data_id, + ..Default::default() + }, + )), + ..Default::default() + })), + ..Default::default() + }, + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + requested_data, + } => Message { + union: Some(message::Union::Cliprdr(Cliprdr { + union: Some(cliprdr::Union::FileContentsResponse( + CliprdrFileContentsResponse { + msg_flags, + stream_id, + requested_data: requested_data.into(), + ..Default::default() + }, + )), + ..Default::default() + })), + ..Default::default() + }, + ClipboardFile::TryEmpty => Message { + union: Some(message::Union::Cliprdr(Cliprdr { + union: Some(cliprdr::Union::TryEmpty(CliprdrTryEmpty { + ..Default::default() + })), + ..Default::default() + })), + ..Default::default() + }, + ClipboardFile::Files { files } => { + let files = files + .iter() + .filter_map(|(f, s)| { + if *s == 0 { + if let Ok(meta) = std::fs::metadata(f) { + Some(CliprdrFile { + name: f.to_owned(), + size: meta.len(), + ..Default::default() + }) + } else { + None + } + } else { + Some(CliprdrFile { + name: f.to_owned(), + size: *s, + ..Default::default() + }) + } + }) + .collect::>(); + Message { + union: Some(message::Union::Cliprdr(Cliprdr { + union: Some(cliprdr::Union::Files(CliprdrFiles { + files, + ..Default::default() + })), + ..Default::default() + })), + ..Default::default() + } + } + } +} + +pub fn msg_2_clip(msg: Cliprdr) -> Option { + match msg.union { + Some(cliprdr::Union::Ready(_)) => Some(ClipboardFile::MonitorReady), + Some(cliprdr::Union::FormatList(data)) => { + let mut format_list: Vec<(i32, String)> = Vec::new(); + for v in data.formats.iter() { + format_list.push((v.id, v.format.clone())); + } + Some(ClipboardFile::FormatList { format_list }) + } + Some(cliprdr::Union::FormatListResponse(data)) => Some(ClipboardFile::FormatListResponse { + msg_flags: data.msg_flags, + }), + Some(cliprdr::Union::FormatDataRequest(data)) => Some(ClipboardFile::FormatDataRequest { + requested_format_id: data.requested_format_id, + }), + Some(cliprdr::Union::FormatDataResponse(data)) => Some(ClipboardFile::FormatDataResponse { + msg_flags: data.msg_flags, + format_data: data.format_data.into(), + }), + Some(cliprdr::Union::FileContentsRequest(data)) => { + Some(ClipboardFile::FileContentsRequest { + stream_id: data.stream_id, + list_index: data.list_index, + dw_flags: data.dw_flags, + n_position_low: data.n_position_low, + n_position_high: data.n_position_high, + cb_requested: data.cb_requested, + have_clip_data_id: data.have_clip_data_id, + clip_data_id: data.clip_data_id, + }) + } + Some(cliprdr::Union::FileContentsResponse(data)) => { + Some(ClipboardFile::FileContentsResponse { + msg_flags: data.msg_flags, + stream_id: data.stream_id, + requested_data: data.requested_data.into(), + }) + } + Some(cliprdr::Union::TryEmpty(_)) => Some(ClipboardFile::TryEmpty), + _ => None, + } +} + +#[cfg(feature = "unix-file-copy-paste")] +pub mod unix_file_clip { + use super::*; + #[cfg(target_os = "linux")] + use crate::clipboard::update_clipboard_files; + use crate::clipboard::{try_empty_clipboard_files, ClipboardSide}; + #[cfg(target_os = "linux")] + use clipboard::platform::unix::fuse; + use clipboard::platform::unix::{ + get_local_format, serv_files, FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME, + FILEDESCRIPTORW_FORMAT_NAME, FILEDESCRIPTOR_FORMAT_ID, + }; + use hbb_common::log; + use std::sync::{Arc, Mutex}; + + lazy_static::lazy_static! { + static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); + } + + pub fn get_format_list() -> ClipboardFile { + let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID) + .unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string()); + let fc_format_name = get_local_format(FILECONTENTS_FORMAT_ID) + .unwrap_or(FILECONTENTS_FORMAT_NAME.to_string()); + ClipboardFile::FormatList { + format_list: vec![ + (FILEDESCRIPTOR_FORMAT_ID, fd_format_name), + (FILECONTENTS_FORMAT_ID, fc_format_name), + ], + } + } + + #[inline] + fn msg_resp_format_data_failure() -> Message { + clip_2_msg(ClipboardFile::FormatDataResponse { + msg_flags: 0x2, + format_data: vec![], + }) + } + + #[inline] + fn resp_file_contents_fail(stream_id: i32) -> Message { + clip_2_msg(ClipboardFile::FileContentsResponse { + msg_flags: 0x2, + stream_id, + requested_data: vec![], + }) + } + + pub fn serve_clip_messages( + side: ClipboardSide, + clip: ClipboardFile, + conn_id: i32, + ) -> Vec { + log::debug!("got clipfile from client peer"); + match clip { + ClipboardFile::MonitorReady => { + log::debug!("client is ready for clipboard"); + } + ClipboardFile::FormatList { format_list } => { + if !format_list + .iter() + .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) + .map(|(id, _)| *id) + .is_some() + { + log::error!("no file contents format found"); + return vec![]; + }; + let Some(file_descriptor_id) = format_list + .iter() + .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) + .map(|(id, _)| *id) + else { + log::error!("no file descriptor format found"); + return vec![]; + }; + // sync file system from peer + let data = ClipboardFile::FormatDataRequest { + requested_format_id: file_descriptor_id, + }; + return vec![clip_2_msg(data)]; + } + ClipboardFile::FormatListResponse { + msg_flags: _msg_flags, + } => {} + ClipboardFile::FormatDataRequest { + requested_format_id: _requested_format_id, + } => { + log::debug!("requested format id: {}", _requested_format_id); + let format_data = serv_files::get_file_list_pdu(); + if !format_data.is_empty() { + return vec![clip_2_msg(ClipboardFile::FormatDataResponse { + msg_flags: 1, + format_data, + })]; + } + // empty file list, send failure message + return vec![msg_resp_format_data_failure()]; + } + #[cfg(target_os = "linux")] + ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + } => { + log::debug!("format data response: msg_flags: {}", msg_flags); + + if msg_flags != 0x1 { + // return failure message? + } + + log::debug!("parsing file descriptors"); + if fuse::init_fuse_context(true).is_ok() { + match fuse::format_data_response_to_urls( + side == ClipboardSide::Client, + format_data, + conn_id, + ) { + Ok(files) => { + update_clipboard_files(files, side); + } + Err(e) => { + log::error!("failed to parse file descriptors: {:?}", e); + } + } + } else { + // send error message to server + } + } + ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + .. + } => { + log::debug!("file contents request: stream_id: {}, list_index: {}, dw_flags: {}, n_position_low: {}, n_position_high: {}, cb_requested: {}", stream_id, list_index, dw_flags, n_position_low, n_position_high, cb_requested); + return serv_files::read_file_contents( + conn_id, + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + ) + .into_iter() + .map(|res| match res { + Ok(data) => clip_2_msg(data), + Err(e) => { + log::error!("failed to read file contents: {:?}", e); + resp_file_contents_fail(stream_id) + } + }) + .collect::<_>(); + } + #[cfg(target_os = "linux")] + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + .. + } => { + log::debug!( + "file contents response: msg_flags: {}, stream_id: {}", + msg_flags, + stream_id, + ); + if fuse::init_fuse_context(true).is_ok() { + hbb_common::allow_err!(fuse::handle_file_content_response( + side == ClipboardSide::Client, + clip + )); + } else { + // send error message to server + } + } + ClipboardFile::NotifyCallback { + r#type, + title, + text, + } => { + // unreachable, but still log it + log::debug!( + "notify callback: type: {}, title: {}, text: {}", + r#type, + title, + text + ); + } + ClipboardFile::TryEmpty => { + try_empty_clipboard_files(side, conn_id); + } + _ => { + log::error!("unsupported clipboard file type"); + } + } + vec![] + } +} diff --git a/vendor/rustdesk/src/common.rs b/vendor/rustdesk/src/common.rs new file mode 100644 index 0000000..69e3ec3 --- /dev/null +++ b/vendor/rustdesk/src/common.rs @@ -0,0 +1,3007 @@ +use std::{ + collections::HashMap, + future::Future, + net::{SocketAddr, ToSocketAddrs}, + sync::{Arc, Mutex, RwLock}, + task::Poll, +}; + +use serde_json::{json, Map, Value}; + +#[cfg(not(target_os = "ios"))] +use hbb_common::whoami; +use hbb_common::{ + allow_err, + anyhow::{anyhow, Context}, + async_recursion::async_recursion, + bail, base64, + bytes::Bytes, + config::{ + self, keys, 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, + sodiumoxide::crypto::{box_, secretbox, sign}, + timeout, + tls::{get_cached_tls_accept_invalid_cert, get_cached_tls_type, upsert_tls_cache, TlsType}, + tokio::{ + self, + net::UdpSocket, + time::{Duration, Instant, Interval}, + }, + ResultType, Stream, +}; + +use crate::{ + hbbs_http::{create_http_client_async, get_url_for_tls}, + ui_interface::{get_api_server as ui_get_api_server, get_option, is_installed, set_option}, +}; + +#[derive(Debug, Eq, PartialEq)] +pub enum GrabState { + Ready, + Run, + Wait, + Exit, +} + +pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; + +// the executable name of the portable version +pub const PORTABLE_APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME"; + +pub const PLATFORM_WINDOWS: &str = "Windows"; +pub const PLATFORM_LINUX: &str = "Linux"; +pub const PLATFORM_MACOS: &str = "Mac OS"; +pub const PLATFORM_ANDROID: &str = "Android"; + +pub const TIMER_OUT: Duration = Duration::from_secs(1); +pub const DEFAULT_KEEP_ALIVE: i32 = 60_000; + +const MIN_VER_MULTI_UI_SESSION: &str = "1.2.4"; + +pub mod input { + pub const MOUSE_TYPE_MOVE: i32 = 0; + pub const MOUSE_TYPE_DOWN: i32 = 1; + pub const MOUSE_TYPE_UP: i32 = 2; + pub const MOUSE_TYPE_WHEEL: i32 = 3; + pub const MOUSE_TYPE_TRACKPAD: i32 = 4; + /// Relative mouse movement type for gaming/3D applications. + /// This type sends delta (dx, dy) values instead of absolute coordinates. + /// NOTE: This is only supported by the Flutter client. The Sciter client (deprecated) + /// does not support relative mouse mode due to: + /// 1. Fixed send_mouse() function signature that doesn't allow type differentiation + /// 2. Lack of pointer lock API in Sciter/TIS + /// 3. No OS cursor control (hide/show/clip) FFI bindings in Sciter UI + pub const MOUSE_TYPE_MOVE_RELATIVE: i32 = 5; + + /// Mask to extract the mouse event type from the mask field. + /// The lower 3 bits contain the event type (MOUSE_TYPE_*), giving a valid range of 0-7. + /// Currently defined types use values 0-5; values 6 and 7 are reserved for future use. + pub const MOUSE_TYPE_MASK: i32 = 0x7; + + pub const MOUSE_BUTTON_LEFT: i32 = 0x01; + pub const MOUSE_BUTTON_RIGHT: i32 = 0x02; + pub const MOUSE_BUTTON_WHEEL: i32 = 0x04; + pub const MOUSE_BUTTON_BACK: i32 = 0x08; + pub const MOUSE_BUTTON_FORWARD: i32 = 0x10; +} + +lazy_static::lazy_static! { + pub static ref SOFTWARE_UPDATE_URL: Arc> = Default::default(); + pub static ref DEVICE_ID: Arc> = Default::default(); + pub static ref DEVICE_NAME: Arc> = Default::default(); + static ref PUBLIC_IPV6_ADDR: Arc, Option)>> = Default::default(); +} + +lazy_static::lazy_static! { + // Is server process, with "--server" args + static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); + // Is server logic running. The server code can invoked to run by the main process if --server is not running. + static ref SERVER_RUNNING: Arc> = Default::default(); + static ref IS_MAIN: bool = std::env::args().nth(1).map_or(true, |arg| !arg.starts_with("--")); + static ref IS_CM: bool = std::env::args().nth(1) == Some("--cm".to_owned()) || std::env::args().nth(1) == Some("--cm-no-ui".to_owned()); +} + +pub struct SimpleCallOnReturn { + pub b: bool, + pub f: Box, +} + +impl Drop for SimpleCallOnReturn { + fn drop(&mut self) { + if self.b { + (self.f)(); + } + } +} + +pub fn global_init() -> bool { + #[cfg(target_os = "linux")] + { + if !crate::platform::linux::is_x11() { + crate::server::wayland::init(); + } + } + true +} + +pub fn global_clean() {} + +#[inline] +pub fn set_server_running(b: bool) { + *SERVER_RUNNING.write().unwrap() = b; +} + +#[inline] +pub fn is_support_multi_ui_session(ver: &str) -> bool { + is_support_multi_ui_session_num(hbb_common::get_version_number(ver)) +} + +#[inline] +pub fn is_support_multi_ui_session_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number(MIN_VER_MULTI_UI_SESSION) +} + +#[inline] +#[cfg(feature = "unix-file-copy-paste")] +pub fn is_support_file_copy_paste(ver: &str) -> bool { + is_support_file_copy_paste_num(hbb_common::get_version_number(ver)) +} + +#[inline] +#[cfg(feature = "unix-file-copy-paste")] +pub fn is_support_file_copy_paste_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number("1.3.8") +} + +pub fn is_support_remote_print(ver: &str) -> bool { + hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9") +} + +pub fn is_support_file_paste_if_macos(ver: &str) -> bool { + hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9") +} + +#[inline] +pub fn is_support_screenshot(ver: &str) -> bool { + is_support_multi_ui_session_num(hbb_common::get_version_number(ver)) +} + +#[inline] +pub fn is_support_screenshot_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number("1.4.0") +} + +#[inline] +pub fn is_support_file_transfer_resume(ver: &str) -> bool { + is_support_file_transfer_resume_num(hbb_common::get_version_number(ver)) +} + +#[inline] +pub fn is_support_file_transfer_resume_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number("1.4.2") +} + +/// Minimum server version required for relative mouse mode support. +/// This constant must mirror Flutter's `kMinVersionForRelativeMouseMode` in `consts.dart`. +const MIN_VERSION_RELATIVE_MOUSE_MODE: &str = "1.4.5"; + +#[inline] +pub fn is_support_relative_mouse_mode(ver: &str) -> bool { + is_support_relative_mouse_mode_num(hbb_common::get_version_number(ver)) +} + +#[inline] +pub fn is_support_relative_mouse_mode_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number(MIN_VERSION_RELATIVE_MOUSE_MODE) +} + +// is server process, with "--server" args +#[inline] +pub fn is_server() -> bool { + *IS_SERVER +} + +#[inline] +pub fn need_fs_cm_send_files() -> bool { + #[cfg(windows)] + { + is_server() + } + #[cfg(not(windows))] + { + false + } +} + +#[inline] +pub fn is_main() -> bool { + *IS_MAIN +} + +#[inline] +pub fn is_cm() -> bool { + *IS_CM +} + +// Is server logic running. +#[inline] +pub fn is_server_running() -> bool { + *SERVER_RUNNING.read().unwrap() +} + +#[inline] +pub fn valid_for_numlock(evt: &KeyEvent) -> bool { + if let Some(key_event::Union::ControlKey(ck)) = evt.union { + let v = ck.value(); + (v >= ControlKey::Numpad0.value() && v <= ControlKey::Numpad9.value()) + || v == ControlKey::Decimal.value() + } else { + false + } +} + +/// Set sound input device. +pub fn set_sound_input(device: String) { + let prior_device = get_option("audio-input".to_owned()); + if prior_device != device { + log::info!("switch to audio input device {}", device); + std::thread::spawn(move || { + set_option("audio-input".to_owned(), device); + }); + } else { + log::info!("audio input is already set to {}", device); + } +} + +/// Get system's default sound input device name. +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_default_sound_input() -> Option { + #[cfg(not(target_os = "linux"))] + { + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + let dev = host.default_input_device(); + return if let Some(dev) = dev { + match dev.name() { + Ok(name) => Some(name), + Err(_) => None, + } + } else { + None + }; + } + #[cfg(target_os = "linux")] + { + let input = crate::platform::linux::get_default_pa_source(); + return if let Some(input) = input { + Some(input.1) + } else { + None + }; + } +} + +#[inline] +#[cfg(any(target_os = "android", target_os = "ios"))] +pub fn get_default_sound_input() -> Option { + None +} + +#[cfg(feature = "use_rubato")] +pub fn resample_channels( + data: &[f32], + sample_rate0: u32, + sample_rate: u32, + channels: u16, +) -> Vec { + use rubato::{ + InterpolationParameters, InterpolationType, Resampler, SincFixedIn, WindowFunction, + }; + let params = InterpolationParameters { + sinc_len: 256, + f_cutoff: 0.95, + interpolation: InterpolationType::Nearest, + oversampling_factor: 160, + window: WindowFunction::BlackmanHarris2, + }; + let mut resampler = SincFixedIn::::new( + sample_rate as f64 / sample_rate0 as f64, + params, + data.len() / (channels as usize), + channels as _, + ); + let mut waves_in = Vec::new(); + if channels == 2 { + waves_in.push( + data.iter() + .step_by(2) + .map(|x| *x as f64) + .collect::>(), + ); + waves_in.push( + data.iter() + .skip(1) + .step_by(2) + .map(|x| *x as f64) + .collect::>(), + ); + } else { + waves_in.push(data.iter().map(|x| *x as f64).collect::>()); + } + if let Ok(x) = resampler.process(&waves_in) { + if x.is_empty() { + Vec::new() + } else if x.len() == 2 { + x[0].chunks(1) + .zip(x[1].chunks(1)) + .flat_map(|(a, b)| a.into_iter().chain(b)) + .map(|x| *x as f32) + .collect() + } else { + x[0].iter().map(|x| *x as f32).collect() + } + } else { + Vec::new() + } +} + +#[cfg(feature = "use_dasp")] +pub fn audio_resample( + data: &[f32], + sample_rate0: u32, + sample_rate: u32, + channels: u16, +) -> Vec { + use dasp::{interpolate::linear::Linear, signal, Signal}; + let n = data.len() / (channels as usize); + let n = n * sample_rate as usize / sample_rate0 as usize; + if channels == 2 { + let mut source = signal::from_interleaved_samples_iter::<_, [_; 2]>(data.iter().cloned()); + let a = source.next(); + let b = source.next(); + let interp = Linear::new(a, b); + let mut data = Vec::with_capacity(n << 1); + for x in source + .from_hz_to_hz(interp, sample_rate0 as _, sample_rate as _) + .take(n) + { + data.push(x[0]); + data.push(x[1]); + } + data + } else { + let mut source = signal::from_iter(data.iter().cloned()); + let a = source.next(); + let b = source.next(); + let interp = Linear::new(a, b); + source + .from_hz_to_hz(interp, sample_rate0 as _, sample_rate as _) + .take(n) + .collect() + } +} + +#[cfg(feature = "use_samplerate")] +pub fn audio_resample( + data: &[f32], + sample_rate0: u32, + sample_rate: u32, + channels: u16, +) -> Vec { + use samplerate::{convert, ConverterType}; + convert( + sample_rate0 as _, + sample_rate as _, + channels as _, + ConverterType::SincBestQuality, + data, + ) + .unwrap_or_default() +} + +pub fn audio_rechannel( + input: Vec, + in_hz: u32, + out_hz: u32, + in_chan: u16, + output_chan: u16, +) -> Vec { + if in_chan == output_chan { + return input; + } + let mut input = input; + input.truncate(input.len() / in_chan as usize * in_chan as usize); + match (in_chan, output_chan) { + (1, 2) => audio_rechannel_1_2(&input, in_hz, out_hz), + (1, 3) => audio_rechannel_1_3(&input, in_hz, out_hz), + (1, 4) => audio_rechannel_1_4(&input, in_hz, out_hz), + (1, 5) => audio_rechannel_1_5(&input, in_hz, out_hz), + (1, 6) => audio_rechannel_1_6(&input, in_hz, out_hz), + (1, 7) => audio_rechannel_1_7(&input, in_hz, out_hz), + (1, 8) => audio_rechannel_1_8(&input, in_hz, out_hz), + (2, 1) => audio_rechannel_2_1(&input, in_hz, out_hz), + (2, 3) => audio_rechannel_2_3(&input, in_hz, out_hz), + (2, 4) => audio_rechannel_2_4(&input, in_hz, out_hz), + (2, 5) => audio_rechannel_2_5(&input, in_hz, out_hz), + (2, 6) => audio_rechannel_2_6(&input, in_hz, out_hz), + (2, 7) => audio_rechannel_2_7(&input, in_hz, out_hz), + (2, 8) => audio_rechannel_2_8(&input, in_hz, out_hz), + (3, 1) => audio_rechannel_3_1(&input, in_hz, out_hz), + (3, 2) => audio_rechannel_3_2(&input, in_hz, out_hz), + (3, 4) => audio_rechannel_3_4(&input, in_hz, out_hz), + (3, 5) => audio_rechannel_3_5(&input, in_hz, out_hz), + (3, 6) => audio_rechannel_3_6(&input, in_hz, out_hz), + (3, 7) => audio_rechannel_3_7(&input, in_hz, out_hz), + (3, 8) => audio_rechannel_3_8(&input, in_hz, out_hz), + (4, 1) => audio_rechannel_4_1(&input, in_hz, out_hz), + (4, 2) => audio_rechannel_4_2(&input, in_hz, out_hz), + (4, 3) => audio_rechannel_4_3(&input, in_hz, out_hz), + (4, 5) => audio_rechannel_4_5(&input, in_hz, out_hz), + (4, 6) => audio_rechannel_4_6(&input, in_hz, out_hz), + (4, 7) => audio_rechannel_4_7(&input, in_hz, out_hz), + (4, 8) => audio_rechannel_4_8(&input, in_hz, out_hz), + (5, 1) => audio_rechannel_5_1(&input, in_hz, out_hz), + (5, 2) => audio_rechannel_5_2(&input, in_hz, out_hz), + (5, 3) => audio_rechannel_5_3(&input, in_hz, out_hz), + (5, 4) => audio_rechannel_5_4(&input, in_hz, out_hz), + (5, 6) => audio_rechannel_5_6(&input, in_hz, out_hz), + (5, 7) => audio_rechannel_5_7(&input, in_hz, out_hz), + (5, 8) => audio_rechannel_5_8(&input, in_hz, out_hz), + (6, 1) => audio_rechannel_6_1(&input, in_hz, out_hz), + (6, 2) => audio_rechannel_6_2(&input, in_hz, out_hz), + (6, 3) => audio_rechannel_6_3(&input, in_hz, out_hz), + (6, 4) => audio_rechannel_6_4(&input, in_hz, out_hz), + (6, 5) => audio_rechannel_6_5(&input, in_hz, out_hz), + (6, 7) => audio_rechannel_6_7(&input, in_hz, out_hz), + (6, 8) => audio_rechannel_6_8(&input, in_hz, out_hz), + (7, 1) => audio_rechannel_7_1(&input, in_hz, out_hz), + (7, 2) => audio_rechannel_7_2(&input, in_hz, out_hz), + (7, 3) => audio_rechannel_7_3(&input, in_hz, out_hz), + (7, 4) => audio_rechannel_7_4(&input, in_hz, out_hz), + (7, 5) => audio_rechannel_7_5(&input, in_hz, out_hz), + (7, 6) => audio_rechannel_7_6(&input, in_hz, out_hz), + (7, 8) => audio_rechannel_7_8(&input, in_hz, out_hz), + (8, 1) => audio_rechannel_8_1(&input, in_hz, out_hz), + (8, 2) => audio_rechannel_8_2(&input, in_hz, out_hz), + (8, 3) => audio_rechannel_8_3(&input, in_hz, out_hz), + (8, 4) => audio_rechannel_8_4(&input, in_hz, out_hz), + (8, 5) => audio_rechannel_8_5(&input, in_hz, out_hz), + (8, 6) => audio_rechannel_8_6(&input, in_hz, out_hz), + (8, 7) => audio_rechannel_8_7(&input, in_hz, out_hz), + _ => input, + } +} + +macro_rules! audio_rechannel { + ($name:ident, $in_channels:expr, $out_channels:expr) => { + fn $name(input: &[f32], in_hz: u32, out_hz: u32) -> Vec { + use fon::{chan::Ch32, Audio, Frame}; + let mut in_audio = + Audio::::with_silence(in_hz, input.len() / $in_channels); + for (x, y) in input.chunks_exact($in_channels).zip(in_audio.iter_mut()) { + let mut f = Frame::::default(); + let mut i = 0; + for c in f.channels_mut() { + *c = x[i].into(); + i += 1; + } + *y = f; + } + Audio::::with_audio(out_hz, &in_audio) + .as_f32_slice() + .to_owned() + } + }; +} + +audio_rechannel!(audio_rechannel_1_2, 1, 2); +audio_rechannel!(audio_rechannel_1_3, 1, 3); +audio_rechannel!(audio_rechannel_1_4, 1, 4); +audio_rechannel!(audio_rechannel_1_5, 1, 5); +audio_rechannel!(audio_rechannel_1_6, 1, 6); +audio_rechannel!(audio_rechannel_1_7, 1, 7); +audio_rechannel!(audio_rechannel_1_8, 1, 8); +audio_rechannel!(audio_rechannel_2_1, 2, 1); +audio_rechannel!(audio_rechannel_2_3, 2, 3); +audio_rechannel!(audio_rechannel_2_4, 2, 4); +audio_rechannel!(audio_rechannel_2_5, 2, 5); +audio_rechannel!(audio_rechannel_2_6, 2, 6); +audio_rechannel!(audio_rechannel_2_7, 2, 7); +audio_rechannel!(audio_rechannel_2_8, 2, 8); +audio_rechannel!(audio_rechannel_3_1, 3, 1); +audio_rechannel!(audio_rechannel_3_2, 3, 2); +audio_rechannel!(audio_rechannel_3_4, 3, 4); +audio_rechannel!(audio_rechannel_3_5, 3, 5); +audio_rechannel!(audio_rechannel_3_6, 3, 6); +audio_rechannel!(audio_rechannel_3_7, 3, 7); +audio_rechannel!(audio_rechannel_3_8, 3, 8); +audio_rechannel!(audio_rechannel_4_1, 4, 1); +audio_rechannel!(audio_rechannel_4_2, 4, 2); +audio_rechannel!(audio_rechannel_4_3, 4, 3); +audio_rechannel!(audio_rechannel_4_5, 4, 5); +audio_rechannel!(audio_rechannel_4_6, 4, 6); +audio_rechannel!(audio_rechannel_4_7, 4, 7); +audio_rechannel!(audio_rechannel_4_8, 4, 8); +audio_rechannel!(audio_rechannel_5_1, 5, 1); +audio_rechannel!(audio_rechannel_5_2, 5, 2); +audio_rechannel!(audio_rechannel_5_3, 5, 3); +audio_rechannel!(audio_rechannel_5_4, 5, 4); +audio_rechannel!(audio_rechannel_5_6, 5, 6); +audio_rechannel!(audio_rechannel_5_7, 5, 7); +audio_rechannel!(audio_rechannel_5_8, 5, 8); +audio_rechannel!(audio_rechannel_6_1, 6, 1); +audio_rechannel!(audio_rechannel_6_2, 6, 2); +audio_rechannel!(audio_rechannel_6_3, 6, 3); +audio_rechannel!(audio_rechannel_6_4, 6, 4); +audio_rechannel!(audio_rechannel_6_5, 6, 5); +audio_rechannel!(audio_rechannel_6_7, 6, 7); +audio_rechannel!(audio_rechannel_6_8, 6, 8); +audio_rechannel!(audio_rechannel_7_1, 7, 1); +audio_rechannel!(audio_rechannel_7_2, 7, 2); +audio_rechannel!(audio_rechannel_7_3, 7, 3); +audio_rechannel!(audio_rechannel_7_4, 7, 4); +audio_rechannel!(audio_rechannel_7_5, 7, 5); +audio_rechannel!(audio_rechannel_7_6, 7, 6); +audio_rechannel!(audio_rechannel_7_8, 7, 8); +audio_rechannel!(audio_rechannel_8_1, 8, 1); +audio_rechannel!(audio_rechannel_8_2, 8, 2); +audio_rechannel!(audio_rechannel_8_3, 8, 3); +audio_rechannel!(audio_rechannel_8_4, 8, 4); +audio_rechannel!(audio_rechannel_8_5, 8, 5); +audio_rechannel!(audio_rechannel_8_6, 8, 6); +audio_rechannel!(audio_rechannel_8_7, 8, 7); + +pub struct CheckTestNatType { + is_direct: bool, +} + +impl CheckTestNatType { + pub fn new() -> Self { + Self { + is_direct: Config::get_socks().is_none() && !config::use_ws(), + } + } +} + +impl Drop for CheckTestNatType { + fn drop(&mut self) { + let is_direct = Config::get_socks().is_none() && !config::use_ws(); + if self.is_direct != is_direct { + test_nat_type(); + } + } +} + +pub fn test_nat_type() { + test_ipv6_sync(); + use std::sync::atomic::{AtomicBool, Ordering}; + std::thread::spawn(move || { + static IS_RUNNING: AtomicBool = AtomicBool::new(false); + if IS_RUNNING.load(Ordering::SeqCst) { + return; + } + IS_RUNNING.store(true, Ordering::SeqCst); + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::ipc::get_socks_ws(); + let is_direct = Config::get_socks().is_none() && !config::use_ws(); + if !is_direct { + Config::set_nat_type(NatType::SYMMETRIC as _); + IS_RUNNING.store(false, Ordering::SeqCst); + return; + } + + let mut i = 0; + loop { + match test_nat_type_() { + Ok(true) => break, + Err(err) => { + log::error!("test nat: {}", err); + } + _ => {} + } + if Config::get_nat_type() != 0 { + break; + } + i = i * 2 + 1; + if i > 300 { + i = 300; + } + std::thread::sleep(std::time::Duration::from_secs(i)); + } + + IS_RUNNING.store(false, Ordering::SeqCst); + }); +} + +#[tokio::main(flavor = "current_thread")] +async fn test_nat_type_() -> ResultType { + log::info!("Testing nat ..."); + let start = std::time::Instant::now(); + let server1 = Config::get_rendezvous_server(); + let server2 = crate::increase_port(&server1, -1); + let mut msg_out = RendezvousMessage::new(); + let serial = Config::get_serial(); + msg_out.set_test_nat_request(TestNatRequest { + serial, + ..Default::default() + }); + let mut port1 = 0; + let mut port2 = 0; + let mut local_addr = None; + for i in 0..2 { + let server = if i == 0 { &*server1 } else { &*server2 }; + let mut socket = + socket_client::connect_tcp_local(server, local_addr, CONNECT_TIMEOUT).await?; + if i == 0 { + // reuse the local addr is required for nat test + local_addr = Some(socket.local_addr()); + Config::set_option( + "local-ip-addr".to_owned(), + socket.local_addr().ip().to_string(), + ); + } + socket.send(&msg_out).await?; + if let Some(msg_in) = get_next_nonkeyexchange_msg(&mut socket, None).await { + if let Some(rendezvous_message::Union::TestNatResponse(tnr)) = msg_in.union { + log::debug!("Got nat response from {}: port={}", server, tnr.port); + if i == 0 { + port1 = tnr.port; + } else { + port2 = tnr.port; + } + if let Some(cu) = tnr.cu.as_ref() { + Config::set_option( + "rendezvous-servers".to_owned(), + cu.rendezvous_servers.join(","), + ); + Config::set_serial(cu.serial); + } + } + } else { + break; + } + } + let ok = port1 > 0 && port2 > 0; + if ok { + let t = if port1 == port2 { + NatType::ASYMMETRIC + } else { + NatType::SYMMETRIC + }; + Config::set_nat_type(t as _); + log::info!("Tested nat type: {:?} in {:?}", t, start.elapsed()); + } + Ok(ok) +} + +pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec, bool) { + #[cfg(any(target_os = "android", target_os = "ios"))] + let (mut a, mut b) = get_rendezvous_server_(ms_timeout); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let (mut a, mut b) = get_rendezvous_server_(ms_timeout).await; + #[cfg(windows)] + if let Ok(lic) = crate::platform::get_license_from_exe_name() { + if !lic.host.is_empty() { + a = lic.host; + } + } + let mut b: Vec = b + .drain(..) + .map(|x| socket_client::check_port(x, config::RENDEZVOUS_PORT)) + .collect(); + let c = if b.contains(&a) { + b = b.drain(..).filter(|x| x != &a).collect(); + true + } else { + a = b.pop().unwrap_or(a); + false + }; + (a, b, c) +} + +#[inline] +#[cfg(any(target_os = "android", target_os = "ios"))] +fn get_rendezvous_server_(_ms_timeout: u64) -> (String, Vec) { + ( + Config::get_rendezvous_server(), + Config::get_rendezvous_servers(), + ) +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +async fn get_rendezvous_server_(ms_timeout: u64) -> (String, Vec) { + crate::ipc::get_rendezvous_server(ms_timeout).await +} + +#[inline] +#[cfg(any(target_os = "android", target_os = "ios"))] +pub async fn get_nat_type(_ms_timeout: u64) -> i32 { + Config::get_nat_type() +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub async fn get_nat_type(ms_timeout: u64) -> i32 { + crate::ipc::get_nat_type(ms_timeout).await +} + +// used for client to test which server is faster in case stop-servic=Y +#[tokio::main(flavor = "current_thread")] +async fn test_rendezvous_server_() { + let servers = Config::get_rendezvous_servers(); + if servers.len() <= 1 { + return; + } + let mut futs = Vec::new(); + for host in servers { + futs.push(tokio::spawn(async move { + let tm = std::time::Instant::now(); + if socket_client::connect_tcp( + crate::check_port(&host, RENDEZVOUS_PORT), + CONNECT_TIMEOUT, + ) + .await + .is_ok() + { + let elapsed = tm.elapsed().as_micros(); + Config::update_latency(&host, elapsed as _); + } else { + Config::update_latency(&host, -1); + } + })); + } + join_all(futs).await; + Config::reset_online(); +} + +// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] +pub fn test_rendezvous_server() { + std::thread::spawn(test_rendezvous_server_); +} + +pub fn refresh_rendezvous_server() { + #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] + test_rendezvous_server(); + #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + std::thread::spawn(|| { + if crate::ipc::test_rendezvous_server().is_err() { + test_rendezvous_server(); + } + }); +} + +pub fn run_me>(args: Vec) -> std::io::Result { + #[cfg(target_os = "linux")] + if let Ok(appdir) = std::env::var("APPDIR") { + let appimage_cmd = std::path::Path::new(&appdir).join("AppRun"); + if appimage_cmd.exists() { + log::info!("path: {:?}", appimage_cmd); + return std::process::Command::new(appimage_cmd).args(&args).spawn(); + } + } + let cmd = std::env::current_exe()?; + let mut cmd = std::process::Command::new(cmd); + #[cfg(windows)] + let mut force_foreground = false; + #[cfg(windows)] + { + let arg_strs = args + .iter() + .map(|x| x.as_ref().to_string_lossy()) + .collect::>(); + if arg_strs == vec!["--install"] || arg_strs == &["--noinstall"] { + cmd.env(crate::platform::SET_FOREGROUND_WINDOW, "1"); + force_foreground = true; + } + } + let result = cmd.args(&args).spawn(); + match result.as_ref() { + Ok(_child) => + { + #[cfg(windows)] + if force_foreground { + unsafe { winapi::um::winuser::AllowSetForegroundWindow(_child.id() as u32) }; + } + } + Err(err) => log::error!("run_me: {err:?}"), + } + result +} + +#[inline] +pub fn username() -> String { + // fix bug of whoami + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return whoami::username().trim_end_matches('\0').to_owned(); + #[cfg(any(target_os = "android", target_os = "ios"))] + return DEVICE_NAME.lock().unwrap().clone(); +} + +// Exactly the implementation of "whoami::hostname()". +// This wrapper is to suppress warnings. +#[inline(always)] +#[cfg(not(target_os = "ios"))] +pub fn whoami_hostname() -> String { + let mut hostname = whoami::fallible::hostname().unwrap_or_else(|_| "localhost".to_string()); + hostname.make_ascii_lowercase(); + hostname +} + +#[inline] +pub fn hostname() -> String { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + #[allow(unused_mut)] + let mut name = whoami_hostname(); + // some time, there is .local, some time not, so remove it for osx + #[cfg(target_os = "macos")] + if name.ends_with(".local") { + name = name.trim_end_matches(".local").to_owned(); + } + name + } + #[cfg(any(target_os = "android", target_os = "ios"))] + return DEVICE_NAME.lock().unwrap().clone(); +} + +#[inline] +pub fn get_sysinfo() -> serde_json::Value { + use hbb_common::sysinfo::System; + let mut system = System::new(); + system.refresh_memory(); + system.refresh_cpu(); + let memory = system.total_memory(); + let memory = (memory as f64 / 1024. / 1024. / 1024. * 100.).round() / 100.; + let cpus = system.cpus(); + let cpu_name = cpus.first().map(|x| x.brand()).unwrap_or_default(); + let cpu_name = cpu_name.trim_end(); + let cpu_freq = cpus.first().map(|x| x.frequency()).unwrap_or_default(); + let cpu_freq = (cpu_freq as f64 / 1024. * 100.).round() / 100.; + let cpu = if cpu_freq > 0. { + format!("{}, {}GHz, ", cpu_name, cpu_freq) + } else { + "".to_owned() // android + }; + let num_cpus = num_cpus::get(); + let num_pcpus = num_cpus::get_physical(); + let mut os = system.distribution_id(); + os = format!("{} / {}", os, system.long_os_version().unwrap_or_default()); + #[cfg(windows)] + { + os = format!("{os} - {}", system.os_version().unwrap_or_default()); + } + let hostname = hostname(); // sys.hostname() return localhost on android in my test + #[cfg(any(target_os = "android", target_os = "ios"))] + let out; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let mut out; + out = json!({ + "cpu": format!("{cpu}{num_cpus}/{num_pcpus} cores"), + "memory": format!("{memory}GB"), + "os": os, + "hostname": hostname, + }); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let username = crate::platform::get_active_username(); + if !username.is_empty() && (!cfg!(windows) || username != "SYSTEM") { + out["username"] = json!(username); + } + } + out +} + +#[inline] +pub fn check_port(host: T, port: i32) -> String { + hbb_common::socket_client::check_port(host, port) +} + +#[inline] +pub fn increase_port(host: T, offset: i32) -> String { + hbb_common::socket_client::increase_port(host, offset) +} + +pub const POSTFIX_SERVICE: &'static str = "_service"; + +#[inline] +pub fn is_control_key(evt: &KeyEvent, key: &ControlKey) -> bool { + if let Some(key_event::Union::ControlKey(ck)) = evt.union { + ck.value() == key.value() + } else { + false + } +} + +#[inline] +pub fn is_modifier(evt: &KeyEvent) -> bool { + if let Some(key_event::Union::ControlKey(ck)) = evt.union { + let v = ck.value(); + v == ControlKey::Alt.value() + || v == ControlKey::Shift.value() + || v == ControlKey::Control.value() + || v == ControlKey::Meta.value() + || v == ControlKey::RAlt.value() + || v == ControlKey::RShift.value() + || v == ControlKey::RControl.value() + || v == ControlKey::RWin.value() + } else { + false + } +} + +pub fn check_software_update() { + if is_custom_client() { + return; + } + let opt = LocalConfig::get_option(keys::OPTION_ENABLE_CHECK_UPDATE); + if config::option2bool(keys::OPTION_ENABLE_CHECK_UPDATE, &opt) { + std::thread::spawn(move || allow_err!(do_check_software_update())); + } +} + +// No need to check `danger_accept_invalid_cert` for now. +// Because the url is always `https://api.rustdesk.com/version/latest`. +#[tokio::main(flavor = "current_thread")] +pub async fn do_check_software_update() -> hbb_common::ResultType<()> { + let (request, url) = + hbb_common::version_check_request(hbb_common::VER_TYPE_RUSTDESK_CLIENT.to_string()); + let proxy_conf = Config::get_socks(); + let tls_url = get_url_for_tls(&url, &proxy_conf); + let tls_type = get_cached_tls_type(tls_url); + let is_tls_not_cached = tls_type.is_none(); + let tls_type = tls_type.unwrap_or(TlsType::Rustls); + let client = create_http_client_async(tls_type, false); + let latest_release_response = match client.post(&url).json(&request).send().await { + Ok(resp) => { + upsert_tls_cache(tls_url, tls_type, false); + resp + } + Err(err) => { + if is_tls_not_cached && err.is_request() { + let tls_type = TlsType::NativeTls; + let client = create_http_client_async(tls_type, false); + let resp = client.post(&url).json(&request).send().await?; + upsert_tls_cache(tls_url, tls_type, false); + resp + } else { + return Err(err.into()); + } + } + }; + let bytes = latest_release_response.bytes().await?; + let resp: hbb_common::VersionCheckResponse = serde_json::from_slice(&bytes)?; + let response_url = resp.url; + let latest_release_version = response_url.rsplit('/').next().unwrap_or_default(); + + if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) { + #[cfg(feature = "flutter")] + { + let mut m = HashMap::new(); + m.insert("name", "check_software_update_finish"); + m.insert("url", &response_url); + if let Ok(data) = serde_json::to_string(&m) { + let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data); + } + } + *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; + } else { + *SOFTWARE_UPDATE_URL.lock().unwrap() = "".to_string(); + } + Ok(()) +} + +#[inline] +pub fn get_app_name() -> String { + hbb_common::config::APP_NAME.read().unwrap().clone() +} + +#[inline] +pub fn is_rustdesk() -> bool { + hbb_common::config::APP_NAME.read().unwrap().eq("RustDesk") +} + +#[inline] +pub fn get_uri_prefix() -> String { + format!("{}://", get_app_name().to_lowercase()) +} + +#[cfg(target_os = "macos")] +pub fn get_full_name() -> String { + format!( + "{}.{}", + hbb_common::config::ORG.read().unwrap(), + hbb_common::config::APP_NAME.read().unwrap(), + ) +} + +pub fn is_setup(name: &str) -> bool { + name.to_lowercase().ends_with("install.exe") +} + +pub fn get_custom_rendezvous_server(custom: String) -> String { + #[cfg(windows)] + if let Ok(lic) = crate::platform::windows::get_license_from_exe_name() { + if !lic.host.is_empty() { + return lic.host.clone(); + } + } + if !custom.is_empty() { + return custom; + } + if !config::PROD_RENDEZVOUS_SERVER.read().unwrap().is_empty() { + return config::PROD_RENDEZVOUS_SERVER.read().unwrap().clone(); + } + "".to_owned() +} + +#[inline] +pub fn get_api_server(api: String, custom: String) -> String { + if Config::no_register_device() { + return "".to_owned(); + } + let mut res = get_api_server_(api, custom); + if res.ends_with('/') { + res.pop(); + } + if res.starts_with("https") + && res.ends_with(":21114") + && get_builtin_option(keys::OPTION_ALLOW_HTTPS_21114) != "Y" + { + return res.replace(":21114", ""); + } + res +} + +fn get_api_server_(api: String, custom: String) -> String { + #[cfg(windows)] + if let Ok(lic) = crate::platform::windows::get_license_from_exe_name() { + if !lic.api.is_empty() { + return lic.api.clone(); + } + } + if !api.is_empty() { + return api.to_owned(); + } + let s0 = get_custom_rendezvous_server(custom); + if !s0.is_empty() { + let s = crate::increase_port(&s0, -2); + if s == s0 { + return format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); + } else { + return format!("http://{}", s); + } + } + "https://admin.rustdesk.com".to_owned() +} + +#[inline] +pub fn is_public(url: &str) -> bool { + let url = url.to_ascii_lowercase(); + url.contains("rustdesk.com/") || url.ends_with("rustdesk.com") +} + +pub fn get_udp_punch_enabled() -> bool { + config::option2bool( + keys::OPTION_ENABLE_UDP_PUNCH, + &get_local_option(keys::OPTION_ENABLE_UDP_PUNCH), + ) +} + +pub fn get_ipv6_punch_enabled() -> bool { + config::option2bool( + keys::OPTION_ENABLE_IPV6_PUNCH, + &get_local_option(keys::OPTION_ENABLE_IPV6_PUNCH), + ) +} + +pub fn get_local_option(key: &str) -> String { + let v = LocalConfig::get_option(key); + if key == keys::OPTION_ENABLE_UDP_PUNCH || key == keys::OPTION_ENABLE_IPV6_PUNCH { + if v.is_empty() { + if !is_public(&Config::get_rendezvous_server()) { + return "N".to_owned(); + } + } + } + v +} + +pub fn get_audit_server(api: String, custom: String, typ: String) -> String { + let url = get_api_server(api, custom); + if url.is_empty() || is_public(&url) { + return "".to_owned(); + } + format!("{}/api/audit/{}", url, typ) +} + +/// Check if we should use raw TCP proxy for API calls. +/// Returns true if USE_RAW_TCP_FOR_API builtin option is "Y", WebSocket is off, +/// and the target URL belongs to the configured non-public API host. +#[inline] +fn should_use_raw_tcp_for_api(url: &str) -> bool { + get_builtin_option(keys::OPTION_USE_RAW_TCP_FOR_API) == "Y" + && !use_ws() + && is_tcp_proxy_api_target(url) +} + +/// Check if we can attempt raw TCP proxy fallback for this target URL. +#[inline] +fn can_fallback_to_raw_tcp(url: &str) -> bool { + !use_ws() && is_tcp_proxy_api_target(url) +} + +#[inline] +fn should_use_tcp_proxy_for_api_url(url: &str, api_url: &str) -> bool { + if api_url.is_empty() || is_public(api_url) { + return false; + } + + let target_host = url::Url::parse(url) + .ok() + .and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase())); + let api_host = url::Url::parse(api_url) + .ok() + .and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase())); + + matches!((target_host, api_host), (Some(target), Some(api)) if target == api) +} + +#[inline] +fn is_tcp_proxy_api_target(url: &str) -> bool { + should_use_tcp_proxy_for_api_url(url, &ui_get_api_server()) +} + +fn tcp_proxy_log_target(url: &str) -> String { + url::Url::parse(url) + .ok() + .map(|parsed| { + let mut redacted = format!("{}://", parsed.scheme()); + let Some(host) = parsed.host() else { + return "".to_owned(); + }; + redacted.push_str(&host.to_string()); + if let Some(port) = parsed.port() { + redacted.push(':'); + redacted.push_str(&port.to_string()); + } + redacted.push_str(parsed.path()); + redacted + }) + .unwrap_or_else(|| "".to_owned()) +} + +#[inline] +fn get_tcp_proxy_addr() -> String { + check_port(Config::get_rendezvous_server(), RENDEZVOUS_PORT) +} + +/// Send an HTTP request via the rendezvous server's TCP proxy using protobuf. +/// Connects with `connect_tcp` + `secure_tcp`, sends `HttpProxyRequest`, +/// receives `HttpProxyResponse`. +/// +/// The entire operation (connect + handshake + send + receive) is wrapped in +/// an overall timeout of `CONNECT_TIMEOUT + READ_TIMEOUT` so that a stall at +/// any stage cannot block the caller indefinitely. +async fn tcp_proxy_request( + method: &str, + url: &str, + body: &[u8], + headers: Vec, +) -> ResultType { + let tcp_addr = get_tcp_proxy_addr(); + if tcp_addr.is_empty() { + bail!("No rendezvous server configured for TCP proxy"); + } + + let parsed = url::Url::parse(url)?; + let path = if let Some(query) = parsed.query() { + format!("{}?{}", parsed.path(), query) + } else { + parsed.path().to_string() + }; + + log::debug!( + "Sending {} {} via TCP proxy to {}", + method, + parsed.path(), + tcp_addr + ); + + let overall_timeout = CONNECT_TIMEOUT + READ_TIMEOUT; + timeout(overall_timeout, async { + let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?; + let key = crate::get_key(true).await; + secure_tcp_silent(&mut conn, &key).await?; + + let mut req = HttpProxyRequest::new(); + req.method = method.to_uppercase(); + req.path = path; + req.headers = headers.into(); + req.body = Bytes::from(body.to_vec()); + + let mut msg_out = RendezvousMessage::new(); + msg_out.set_http_proxy_request(req); + conn.send(&msg_out).await?; + + match conn.next().await { + Some(Ok(bytes)) => { + let msg_in = RendezvousMessage::parse_from_bytes(&bytes)?; + match msg_in.union { + Some(rendezvous_message::Union::HttpProxyResponse(resp)) => Ok(resp), + _ => bail!("Unexpected response from TCP proxy"), + } + } + Some(Err(e)) => bail!("TCP proxy read error: {}", e), + None => bail!("TCP proxy connection closed without response"), + } + }) + .await? +} + +/// Build HeaderEntry list from "Key: Value" style header string (used by post_request). +/// If the caller supplies a Content-Type header it overrides the default `application/json`. +fn parse_simple_header(header: &str) -> Vec { + let mut entries = Vec::new(); + let mut has_content_type = false; + if !header.is_empty() { + let tmp: Vec<&str> = header.splitn(2, ": ").collect(); + if tmp.len() == 2 { + if tmp[0].eq_ignore_ascii_case("Content-Type") { + has_content_type = true; + } + entries.push(HeaderEntry { + name: tmp[0].into(), + value: tmp[1].into(), + ..Default::default() + }); + } + } + if !has_content_type { + entries.insert( + 0, + HeaderEntry { + name: "Content-Type".into(), + value: "application/json".into(), + ..Default::default() + }, + ); + } + entries +} + +/// POST request via TCP proxy. +async fn post_request_via_tcp_proxy(url: &str, body: &str, header: &str) -> ResultType { + let headers = parse_simple_header(header); + let resp = tcp_proxy_request("POST", url, body.as_bytes(), headers).await?; + if !resp.error.is_empty() { + bail!("TCP proxy error: {}", resp.error); + } + Ok(String::from_utf8_lossy(&resp.body).to_string()) +} + +fn http_proxy_response_to_json(resp: HttpProxyResponse) -> ResultType { + if !resp.error.is_empty() { + bail!("TCP proxy error: {}", resp.error); + } + + let mut response_headers = Map::new(); + for entry in resp.headers.iter() { + response_headers.insert(entry.name.to_lowercase(), json!(entry.value)); + } + + let mut result = Map::new(); + result.insert("status_code".to_string(), json!(resp.status)); + result.insert("headers".to_string(), Value::Object(response_headers)); + result.insert( + "body".to_string(), + json!(String::from_utf8_lossy(&resp.body)), + ); + + serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e)) +} + +fn parse_json_header_entries(header: &str) -> ResultType> { + let v: Value = serde_json::from_str(header)?; + if let Value::Object(obj) = v { + Ok(obj + .iter() + .map(|(key, value)| HeaderEntry { + name: key.clone(), + value: value.as_str().unwrap_or_default().into(), + ..Default::default() + }) + .collect()) + } else { + Err(anyhow!("HTTP header information parsing failed!")) + } +} + +/// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback. +async fn post_request_http(url: &str, body: &str, header: &str) -> ResultType<(u16, String)> { + let proxy_conf = Config::get_socks(); + let tls_url = get_url_for_tls(url, &proxy_conf); + let tls_type = get_cached_tls_type(tls_url); + let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); + let response = post_request_( + url, + tls_url, + body.to_owned(), + header, + tls_type, + danger_accept_invalid_cert, + danger_accept_invalid_cert, + ) + .await?; + let status = response.status().as_u16(); + let text = response.text().await?; + Ok((status, text)) +} + +/// Try `http_fn` first; on connection failure or 5xx, fall back to `tcp_fn` +/// if the URL is eligible. 4xx responses are returned as-is. +async fn with_tcp_proxy_fallback( + url: &str, + method: &str, + http_fn: HttpFut, + tcp_fn: TcpFut, +) -> ResultType +where + HttpFut: Future>, + TcpFut: Future>, +{ + if should_use_raw_tcp_for_api(url) { + return tcp_fn.await; + } + + let http_result = http_fn.await; + let should_fallback = match &http_result { + Err(_) => true, + Ok((status, _)) => *status >= 500, + }; + + if should_fallback && can_fallback_to_raw_tcp(url) { + log::warn!( + "HTTP {} to {} failed or 5xx (result: {:?}), trying TCP proxy fallback", + method, + tcp_proxy_log_target(url), + http_result + .as_ref() + .map(|(s, _)| *s) + .map_err(|e| e.to_string()), + ); + match tcp_fn.await { + Ok(resp) => return Ok(resp), + Err(tcp_err) => { + log::warn!("TCP proxy fallback also failed: {:?}", tcp_err); + } + } + } + + http_result.map(|(_status, text)| text) +} + +/// POST request with raw TCP proxy support. +/// - If `USE_RAW_TCP_FOR_API` is "Y" and WS is off, goes directly through TCP proxy. +/// - Otherwise tries HTTP first; on connection failure or 5xx status, +/// falls back to TCP proxy if WS is off. +/// - 4xx responses are returned as-is (server is reachable, business logic error). +/// - If fallback also fails, returns the original HTTP result (text or error). +pub async fn post_request(url: String, body: String, header: &str) -> ResultType { + with_tcp_proxy_fallback( + &url, + "POST", + post_request_http(&url, &body, header), + post_request_via_tcp_proxy(&url, &body, header), + ) + .await +} + +#[async_recursion] +async fn post_request_( + url: &str, + tls_url: &str, + body: String, + header: &str, + tls_type: Option, + danger_accept_invalid_cert: Option, + original_danger_accept_invalid_cert: Option, +) -> ResultType { + let mut req = create_http_client_async( + tls_type.unwrap_or(TlsType::Rustls), + danger_accept_invalid_cert.unwrap_or(false), + ) + .post(url); + if !header.is_empty() { + let tmp: Vec<&str> = header.split(": ").collect(); + if tmp.len() == 2 { + req = req.header(tmp[0], tmp[1]); + } + } + req = req.header("Content-Type", "application/json"); + let to = std::time::Duration::from_secs(12); + if tls_type.is_some() && danger_accept_invalid_cert.is_some() { + // This branch is used to reduce a `clone()` when both `tls_type` and + // `danger_accept_invalid_cert` are cached. + match req.body(body.clone()).timeout(to).send().await { + Ok(resp) => { + upsert_tls_cache( + tls_url, + tls_type.unwrap_or(TlsType::Rustls), + danger_accept_invalid_cert.unwrap_or(false), + ); + Ok(resp) + } + Err(e) => Err(anyhow!("{:?}", e)), + } + } else { + match req.body(body.clone()).timeout(to).send().await { + Ok(resp) => { + upsert_tls_cache( + tls_url, + tls_type.unwrap_or(TlsType::Rustls), + danger_accept_invalid_cert.unwrap_or(false), + ); + Ok(resp) + } + Err(e) => { + if (tls_type.is_none() || danger_accept_invalid_cert.is_none()) && e.is_request() { + if danger_accept_invalid_cert.is_none() { + log::warn!( + "HTTP request failed: {:?}, try again, danger accept invalid cert", + e + ); + post_request_( + url, + tls_url, + body, + header, + tls_type, + Some(true), + original_danger_accept_invalid_cert, + ) + .await + } else { + log::warn!("HTTP request failed: {:?}, try again with native-tls", e); + post_request_( + url, + tls_url, + body, + header, + Some(TlsType::NativeTls), + original_danger_accept_invalid_cert, + original_danger_accept_invalid_cert, + ) + .await + } + } else { + Err(anyhow!("{:?}", e)) + } + } + } + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn post_request_sync(url: String, body: String, header: &str) -> ResultType { + post_request(url, body, header).await +} + +#[async_recursion] +async fn get_http_response_async( + url: &str, + tls_url: &str, + method: &str, + body: Option, + header: &str, + tls_type: Option, + danger_accept_invalid_cert: Option, + original_danger_accept_invalid_cert: Option, +) -> ResultType { + let http_client = create_http_client_async( + tls_type.unwrap_or(TlsType::Rustls), + danger_accept_invalid_cert.unwrap_or(false), + ); + let normalized_method = method.to_ascii_lowercase(); + let mut http_client = match normalized_method.as_str() { + "get" => http_client.get(url), + "post" => http_client.post(url), + "put" => http_client.put(url), + "delete" => http_client.delete(url), + _ => return Err(anyhow!("The HTTP request method is not supported!")), + }; + for entry in parse_json_header_entries(header)? { + http_client = http_client.header(entry.name, entry.value); + } + + if tls_type.is_some() && danger_accept_invalid_cert.is_some() { + if let Some(b) = body { + http_client = http_client.body(b); + } + match http_client + .timeout(std::time::Duration::from_secs(12)) + .send() + .await + { + Ok(resp) => { + upsert_tls_cache( + tls_url, + tls_type.unwrap_or(TlsType::Rustls), + danger_accept_invalid_cert.unwrap_or(false), + ); + Ok(resp) + } + Err(e) => Err(anyhow!("{:?}", e)), + } + } else { + if let Some(b) = body.clone() { + http_client = http_client.body(b); + } + + match http_client + .timeout(std::time::Duration::from_secs(12)) + .send() + .await + { + Ok(resp) => { + upsert_tls_cache( + tls_url, + tls_type.unwrap_or(TlsType::Rustls), + danger_accept_invalid_cert.unwrap_or(false), + ); + Ok(resp) + } + Err(e) => { + if (tls_type.is_none() || danger_accept_invalid_cert.is_none()) && e.is_request() { + if danger_accept_invalid_cert.is_none() { + log::warn!( + "HTTP request failed: {:?}, try again, danger accept invalid cert", + e + ); + get_http_response_async( + url, + tls_url, + method, + body, + header, + tls_type, + Some(true), + original_danger_accept_invalid_cert, + ) + .await + } else { + log::warn!("HTTP request failed: {:?}, try again with native-tls", e); + get_http_response_async( + url, + tls_url, + method, + body, + header, + Some(TlsType::NativeTls), + original_danger_accept_invalid_cert, + original_danger_accept_invalid_cert, + ) + .await + } + } else { + Err(anyhow!("{:?}", e)) + } + } + } + } +} + +/// Returns (status_code, json_string) so the caller can inspect the status +/// without re-parsing the serialized JSON. +async fn http_request_http( + url: &str, + method: &str, + body: Option, + header: &str, +) -> ResultType<(u16, String)> { + let proxy_conf = Config::get_socks(); + let tls_url = get_url_for_tls(url, &proxy_conf); + let tls_type = get_cached_tls_type(tls_url); + let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); + let response = get_http_response_async( + url, + tls_url, + method, + body, + header, + tls_type, + danger_accept_invalid_cert, + danger_accept_invalid_cert, + ) + .await?; + // Serialize response headers + let mut response_headers = Map::new(); + for (key, value) in response.headers() { + response_headers.insert(key.to_string(), json!(value.to_str().unwrap_or(""))); + } + + let status_code = response.status().as_u16(); + let response_body = response.text().await?; + + // Construct the JSON object + let mut result = Map::new(); + result.insert("status_code".to_string(), json!(status_code)); + result.insert("headers".to_string(), Value::Object(response_headers)); + result.insert("body".to_string(), json!(response_body)); + + // Convert map to JSON string + let json_str = serde_json::to_string(&result) + .map_err(|e| anyhow!("Failed to serialize response: {}", e))?; + Ok((status_code, json_str)) +} + +/// HTTP request with raw TCP proxy support. +#[tokio::main(flavor = "current_thread")] +pub async fn http_request_sync( + url: String, + method: String, + body: Option, + header: String, +) -> ResultType { + with_tcp_proxy_fallback( + &url, + &method, + http_request_http(&url, &method, body.clone(), &header), + http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header), + ) + .await +} + +/// General HTTP request via TCP proxy. Header is a JSON string (used by http_request_sync). +/// Returns a JSON string with status_code, headers, body (same format as http_request_sync). +async fn http_request_via_tcp_proxy( + url: &str, + method: &str, + body: Option<&str>, + header: &str, +) -> ResultType { + let headers = parse_json_header_entries(header)?; + let body_bytes = body.unwrap_or("").as_bytes(); + + let resp = tcp_proxy_request(method, url, body_bytes, headers).await?; + http_proxy_response_to_json(resp) +} + +#[inline] +pub fn make_privacy_mode_msg_with_details( + state: back_notification::PrivacyModeState, + details: String, + impl_key: String, +) -> Message { + let mut misc = Misc::new(); + let mut back_notification = BackNotification { + details, + impl_key, + ..Default::default() + }; + back_notification.set_privacy_mode_state(state); + misc.set_back_notification(back_notification); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out +} + +#[inline] +pub fn make_privacy_mode_msg( + state: back_notification::PrivacyModeState, + impl_key: String, +) -> Message { + make_privacy_mode_msg_with_details(state, "".to_owned(), impl_key) +} + +pub fn is_keyboard_mode_supported( + keyboard_mode: &KeyboardMode, + version_number: i64, + peer_platform: &str, +) -> bool { + match keyboard_mode { + KeyboardMode::Legacy => true, + KeyboardMode::Map => { + if peer_platform.to_lowercase() == crate::PLATFORM_ANDROID.to_lowercase() { + false + } else { + version_number >= hbb_common::get_version_number("1.2.0") + } + } + KeyboardMode::Translate => version_number >= hbb_common::get_version_number("1.2.0"), + KeyboardMode::Auto => version_number >= hbb_common::get_version_number("1.2.0"), + } +} + +pub fn get_supported_keyboard_modes(version: i64, peer_platform: &str) -> Vec { + KeyboardMode::iter() + .filter(|&mode| is_keyboard_mode_supported(mode, version, peer_platform)) + .map(|&mode| mode) + .collect::>() +} + +pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { + let fd_json = _make_fd_to_json(id, path, entries); + serde_json::to_string(&fd_json).unwrap_or("".into()) +} + +pub fn _make_fd_to_json(id: i32, path: String, entries: &Vec) -> Map { + let mut fd_json = serde_json::Map::new(); + fd_json.insert("id".into(), json!(id)); + fd_json.insert("path".into(), json!(path)); + + let mut entries_out = vec![]; + for entry in entries { + let mut entry_map = serde_json::Map::new(); + entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); + entry_map.insert("name".into(), json!(entry.name)); + entry_map.insert("size".into(), json!(entry.size)); + entry_map.insert("modified_time".into(), json!(entry.modified_time)); + entries_out.push(entry_map); + } + fd_json.insert("entries".into(), json!(entries_out)); + fd_json +} + +pub fn make_vec_fd_to_json(fds: &[FileDirectory]) -> String { + let mut fd_jsons = vec![]; + + for fd in fds.iter() { + let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries); + fd_jsons.push(fd_json); + } + + serde_json::to_string(&fd_jsons).unwrap_or("".into()) +} + +pub fn make_empty_dirs_response_to_json(res: &ReadEmptyDirsResponse) -> String { + let mut map: Map = serde_json::Map::new(); + map.insert("path".into(), json!(res.path)); + + let mut fd_jsons = vec![]; + + for fd in res.empty_dirs.iter() { + let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries); + fd_jsons.push(fd_json); + } + map.insert("empty_dirs".into(), fd_jsons.into()); + + serde_json::to_string(&map).unwrap_or("".into()) +} + +/// The function to handle the url scheme sent by the system. +/// +/// 1. Try to send the url scheme from ipc. +/// 2. If failed to send the url scheme, we open a new main window to handle this url scheme. +pub fn handle_url_scheme(url: String) { + #[cfg(not(target_os = "ios"))] + if let Err(err) = crate::ipc::send_url_scheme(url.clone()) { + log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); + let _ = crate::run_me(vec![url]); + } +} + +#[inline] +pub fn encode64>(input: T) -> String { + #[allow(deprecated)] + base64::encode(input) +} + +#[inline] +pub fn decode64>(input: T) -> Result, base64::DecodeError> { + #[allow(deprecated)] + base64::decode(input) +} + +pub async fn get_key(sync: bool) -> String { + #[cfg(windows)] + if let Ok(lic) = crate::platform::windows::get_license_from_exe_name() { + if !lic.key.is_empty() { + return lic.key; + } + } + #[cfg(target_os = "ios")] + let mut key = Config::get_option("key"); + #[cfg(not(target_os = "ios"))] + let mut key = if sync { + Config::get_option("key") + } else { + let mut options = crate::ipc::get_options_async().await; + options.remove("key").unwrap_or_default() + }; + if key.is_empty() { + key = config::RS_PUB_KEY.to_owned(); + } + key +} + +pub fn pk_to_fingerprint(pk: Vec) -> String { + let s: String = pk.iter().map(|u| format!("{:02x}", u)).collect(); + s.chars() + .enumerate() + .map(|(i, c)| { + if i > 0 && i % 4 == 0 { + format!(" {}", c) + } else { + format!("{}", c) + } + }) + .collect() +} + +#[inline] +pub async fn get_next_nonkeyexchange_msg( + conn: &mut Stream, + timeout: Option, +) -> Option { + let timeout = timeout.unwrap_or(READ_TIMEOUT); + for _ in 0..2 { + if let Some(Ok(bytes)) = conn.next_timeout(timeout).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match &msg_in.union { + Some(rendezvous_message::Union::KeyExchange(_)) => { + continue; + } + _ => { + return Some(msg_in); + } + } + } + } + break; + } + None +} + +#[cfg(all(target_os = "windows", not(target_pointer_width = "64")))] +pub fn check_process(arg: &str, same_session_id: bool) -> bool { + let mut path = std::env::current_exe().unwrap_or_default(); + if let Ok(linked) = path.read_link() { + path = linked; + } + let Some(filename) = path.file_name() else { + return false; + }; + let filename = filename.to_string_lossy().to_string(); + match crate::platform::windows::get_pids_with_first_arg_check_session( + &filename, + arg, + same_session_id, + ) { + Ok(pids) => { + let self_pid = hbb_common::sysinfo::Pid::from_u32(std::process::id()); + pids.into_iter().filter(|pid| *pid != self_pid).count() > 0 + } + Err(e) => { + log::error!("Failed to check process with arg: \"{}\", {}", arg, e); + false + } + } +} + +#[allow(unused_mut)] +#[cfg(not(all(target_os = "windows", not(target_pointer_width = "64"))))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn check_process(arg: &str, mut same_uid: bool) -> bool { + #[cfg(target_os = "macos")] + if !crate::platform::is_root() && !same_uid { + log::warn!("Can not get other process's command line arguments on macos without root"); + same_uid = true; + } + use hbb_common::sysinfo::System; + let mut sys = System::new(); + sys.refresh_processes(); + let mut path = std::env::current_exe().unwrap_or_default(); + if let Ok(linked) = path.read_link() { + path = linked; + } + let path = path.to_string_lossy().to_lowercase(); + let my_uid = sys + .process((std::process::id() as usize).into()) + .map(|x| x.user_id()) + .unwrap_or_default(); + for (_, p) in sys.processes().iter() { + let mut cur_path = p.exe().to_path_buf(); + if let Ok(linked) = cur_path.read_link() { + cur_path = linked; + } + if cur_path.to_string_lossy().to_lowercase() != path { + continue; + } + if p.pid().to_string() == std::process::id().to_string() { + continue; + } + if same_uid && p.user_id() != my_uid { + continue; + } + // on mac, p.cmd() get "/Applications/RustDesk.app/Contents/MacOS/RustDesk", "XPC_SERVICE_NAME=com.carriez.RustDesk_server" + let parg = if p.cmd().len() <= 1 { "" } else { &p.cmd()[1] }; + if arg.is_empty() { + if !parg.starts_with("--") { + return true; + } + } else if arg == parg { + return true; + } + } + false +} + +async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) -> ResultType<()> { + // Skip additional encryption when using WebSocket connections (wss://) + // as WebSocket Secure (wss://) already provides transport layer encryption. + // This doesn't affect the end-to-end encryption between clients, + // it only avoids redundant encryption between client and server. + if use_ws() { + return Ok(()); + } + let rs_pk = get_rs_pk(key); + let Some(rs_pk) = rs_pk else { + bail!("Handshake failed: invalid public key from rendezvous server"); + }; + match timeout(READ_TIMEOUT, conn.next()).await? { + Some(Ok(bytes)) => { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::KeyExchange(ex)) => { + if ex.keys.len() != 1 { + bail!("Handshake failed: invalid key exchange message"); + } + let their_pk_b = sign::verify(&ex.keys[0], &rs_pk) + .map_err(|_| anyhow!("Signature mismatch in key exchange"))?; + let (asymmetric_value, symmetric_value, key) = create_symmetric_key_msg( + get_pk(&their_pk_b) + .context("Wrong their public length in key exchange")?, + ); + let mut msg_out = RendezvousMessage::new(); + msg_out.set_key_exchange(KeyExchange { + keys: vec![asymmetric_value, symmetric_value], + ..Default::default() + }); + timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??; + conn.set_key(key); + if log_on_success { + log::info!("Connection secured"); + } + } + _ => {} + } + } + } + _ => {} + } + Ok(()) +} + +pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> { + secure_tcp_impl(conn, key, true).await +} + +async fn secure_tcp_silent(conn: &mut Stream, key: &str) -> ResultType<()> { + secure_tcp_impl(conn, key, false).await +} + +#[inline] +fn get_pk(pk: &[u8]) -> Option<[u8; 32]> { + if pk.len() == 32 { + let mut tmp = [0u8; 32]; + tmp[..].copy_from_slice(&pk); + Some(tmp) + } else { + None + } +} + +#[inline] +pub fn get_rs_pk(str_base64: &str) -> Option { + if let Ok(pk) = crate::decode64(str_base64) { + get_pk(&pk).map(|x| sign::PublicKey(x)) + } else { + None + } +} + +pub fn decode_id_pk(signed: &[u8], key: &sign::PublicKey) -> ResultType<(String, [u8; 32])> { + let res = IdPk::parse_from_bytes( + &sign::verify(signed, key).map_err(|_| anyhow!("Signature mismatch"))?, + )?; + if let Some(pk) = get_pk(&res.pk) { + Ok((res.id, pk)) + } else { + bail!("Wrong their public length"); + } +} + +pub fn create_symmetric_key_msg(their_pk_b: [u8; 32]) -> (Bytes, Bytes, secretbox::Key) { + let their_pk_b = box_::PublicKey(their_pk_b); + let (our_pk_b, out_sk_b) = box_::gen_keypair(); + let key = secretbox::gen_key(); + let nonce = box_::Nonce([0u8; box_::NONCEBYTES]); + let sealed_key = box_::seal(&key.0, &nonce, &their_pk_b, &out_sk_b); + (Vec::from(our_pk_b.0).into(), sealed_key.into(), key) +} + +#[inline] +pub fn using_public_server() -> bool { + crate::get_custom_rendezvous_server(get_option("custom-rendezvous-server")).is_empty() +} + +pub struct ThrottledInterval { + interval: Interval, + next_tick: Instant, + min_interval: Duration, +} + +impl ThrottledInterval { + pub fn new(i: Interval) -> ThrottledInterval { + let period = i.period(); + ThrottledInterval { + interval: i, + next_tick: Instant::now(), + min_interval: Duration::from_secs_f64(period.as_secs_f64() * 0.9), + } + } + + pub async fn tick(&mut self) -> Instant { + let instant = poll_fn(|cx| self.poll_tick(cx)); + instant.await + } + + pub fn poll_tick(&mut self, cx: &mut std::task::Context<'_>) -> Poll { + match self.interval.poll_tick(cx) { + Poll::Ready(instant) => { + let now = Instant::now(); + if self.next_tick <= now { + self.next_tick = now + self.min_interval; + Poll::Ready(instant) + } else { + // This call is required since tokio 1.27 + cx.waker().wake_by_ref(); + Poll::Pending + } + } + Poll::Pending => Poll::Pending, + } + } +} + +pub type RustDeskInterval = ThrottledInterval; + +#[inline] +pub fn rustdesk_interval(i: Interval) -> ThrottledInterval { + ThrottledInterval::new(i) +} + +pub fn load_custom_client() { + #[cfg(debug_assertions)] + if let Ok(data) = std::fs::read_to_string("./custom.txt") { + read_custom_client(data.trim()); + return; + } + let Some(path) = std::env::current_exe().map_or(None, |x| x.parent().map(|x| x.to_path_buf())) + else { + return; + }; + #[cfg(target_os = "macos")] + let path = path.join("../Resources"); + let path = path.join("custom.txt"); + if path.is_file() { + let Ok(data) = std::fs::read_to_string(&path) else { + log::error!("Failed to read custom client config"); + return; + }; + read_custom_client(&data.trim()); + } +} + +fn read_custom_client_advanced_settings( + settings: serde_json::Value, + map_display_settings: &HashMap, + map_local_settings: &HashMap, + map_settings: &HashMap, + map_buildin_settings: &HashMap, + is_override: bool, +) { + let mut display_settings = if is_override { + config::OVERWRITE_DISPLAY_SETTINGS.write().unwrap() + } else { + config::DEFAULT_DISPLAY_SETTINGS.write().unwrap() + }; + let mut local_settings = if is_override { + config::OVERWRITE_LOCAL_SETTINGS.write().unwrap() + } else { + config::DEFAULT_LOCAL_SETTINGS.write().unwrap() + }; + let mut server_settings = if is_override { + config::OVERWRITE_SETTINGS.write().unwrap() + } else { + config::DEFAULT_SETTINGS.write().unwrap() + }; + let mut buildin_settings = config::BUILTIN_SETTINGS.write().unwrap(); + + if let Some(settings) = settings.as_object() { + for (k, v) in settings { + let Some(v) = v.as_str() else { + continue; + }; + if let Some(k2) = map_display_settings.get(k) { + display_settings.insert(k2.to_string(), v.to_owned()); + } else if let Some(k2) = map_local_settings.get(k) { + local_settings.insert(k2.to_string(), v.to_owned()); + } else if let Some(k2) = map_settings.get(k) { + server_settings.insert(k2.to_string(), v.to_owned()); + } else if let Some(k2) = map_buildin_settings.get(k) { + buildin_settings.insert(k2.to_string(), v.to_owned()); + } else { + let k2 = k.replace("_", "-"); + let k = k2.replace("-", "_"); + // display + display_settings.insert(k.clone(), v.to_owned()); + display_settings.insert(k2.clone(), v.to_owned()); + // local + local_settings.insert(k.clone(), v.to_owned()); + local_settings.insert(k2.clone(), v.to_owned()); + // server + server_settings.insert(k.clone(), v.to_owned()); + server_settings.insert(k2.clone(), v.to_owned()); + // buildin + buildin_settings.insert(k.clone(), v.to_owned()); + buildin_settings.insert(k2.clone(), v.to_owned()); + } + } + } +} + +#[inline] +#[cfg(target_os = "macos")] +pub fn get_dst_align_rgba() -> usize { + // https://developer.apple.com/forums/thread/712709 + // Memory alignment should be multiple of 64. + if crate::ui_interface::use_texture_render() { + 64 + } else { + 1 + } +} + +#[inline] +#[cfg(not(target_os = "macos"))] +pub fn get_dst_align_rgba() -> usize { + 1 +} + +pub fn read_custom_client(config: &str) { + let Ok(data) = decode64(config) else { + log::error!("Failed to decode custom client config"); + return; + }; + const KEY: &str = "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM="; + let Some(pk) = get_rs_pk(KEY) else { + log::error!("Failed to parse public key of custom client"); + return; + }; + let Ok(data) = sign::verify(&data, &pk) else { + log::error!("Failed to dec custom client config"); + return; + }; + let Ok(mut data) = + serde_json::from_slice::>(&data) + else { + log::error!("Failed to parse custom client config"); + return; + }; + + if let Some(app_name) = data.remove("app-name") { + if let Some(app_name) = app_name.as_str() { + *config::APP_NAME.write().unwrap() = app_name.to_owned(); + } + } + + let mut map_display_settings = HashMap::new(); + for s in keys::KEYS_DISPLAY_SETTINGS { + map_display_settings.insert(s.replace("_", "-"), s); + } + let mut map_local_settings = HashMap::new(); + for s in keys::KEYS_LOCAL_SETTINGS { + map_local_settings.insert(s.replace("_", "-"), s); + } + let mut map_settings = HashMap::new(); + for s in keys::KEYS_SETTINGS { + map_settings.insert(s.replace("_", "-"), s); + } + let mut buildin_settings = HashMap::new(); + for s in keys::KEYS_BUILDIN_SETTINGS { + buildin_settings.insert(s.replace("_", "-"), s); + } + if let Some(default_settings) = data.remove("default-settings") { + read_custom_client_advanced_settings( + default_settings, + &map_display_settings, + &map_local_settings, + &map_settings, + &buildin_settings, + false, + ); + } + if let Some(overwrite_settings) = data.remove("override-settings") { + read_custom_client_advanced_settings( + overwrite_settings, + &map_display_settings, + &map_local_settings, + &map_settings, + &buildin_settings, + true, + ); + } + for (k, v) in data { + if let Some(v) = v.as_str() { + config::HARD_SETTINGS + .write() + .unwrap() + .insert(k, v.to_owned()); + }; + } +} + +#[inline] +pub fn is_empty_uni_link(arg: &str) -> bool { + let prefix = crate::get_uri_prefix(); + if !arg.starts_with(&prefix) { + return false; + } + arg[prefix.len()..].chars().all(|c| c == '/') +} + +pub fn get_hwid() -> Bytes { + use hbb_common::sha2::{Digest, Sha256}; + + let uuid = hbb_common::get_uuid(); + let mut hasher = Sha256::new(); + hasher.update(&uuid); + Bytes::from(hasher.finalize().to_vec()) +} + +#[inline] +pub fn get_builtin_option(key: &str) -> String { + config::BUILTIN_SETTINGS + .read() + .unwrap() + .get(key) + .cloned() + .unwrap_or_default() +} + +#[inline] +pub fn is_custom_client() -> bool { + get_app_name() != "RustDesk" +} + +pub fn verify_login(_raw: &str, _id: &str) -> bool { + true + /* + if is_custom_client() { + return true; + } + #[cfg(debug_assertions)] + return true; + let Ok(pk) = crate::decode64("IycjQd4TmWvjjLnYd796Rd+XkK+KG+7GU1Ia7u4+vSw=") else { + return false; + }; + let Some(key) = get_pk(&pk).map(|x| sign::PublicKey(x)) else { + return false; + }; + let Ok(v) = crate::decode64(raw) else { + return false; + }; + let raw = sign::verify(&v, &key).unwrap_or_default(); + let v_str = std::str::from_utf8(&raw) + .unwrap_or_default() + .split(":") + .next() + .unwrap_or_default(); + v_str == id + */ +} + +#[inline] +pub fn is_udp_disabled() -> bool { + Config::get_option(keys::OPTION_DISABLE_UDP) == "Y" +} + +// this crate https://github.com/yoshd/stun-client supports nat type +async fn stun_ipv6_test(stun_server: &str) -> ResultType<(SocketAddr, String)> { + use std::net::ToSocketAddrs; + use stunclient::StunClient; + let local_addr = SocketAddr::from(([0u16; 8], 0)); // [::]:0 + let socket = UdpSocket::bind(&local_addr).await?; + let Some(stun_addr) = stun_server + .to_socket_addrs()? + .filter(|x| x.is_ipv6()) + .next() + else { + bail!( + "Failed to resolve STUN ipv6 server address: {}", + stun_server + ); + }; + let client = StunClient::new(stun_addr); + let addr = client.query_external_address_async(&socket).await?; + Ok(if addr.ip().is_ipv6() { + (addr, stun_server.to_owned()) + } else { + bail!("STUN server returned non-IPv6 address: {}", addr) + }) +} + +async fn stun_ipv4_test(stun_server: &str) -> ResultType<(SocketAddr, String)> { + use std::net::ToSocketAddrs; + use stunclient::StunClient; + let local_addr = SocketAddr::from(([0u8; 4], 0)); + let socket = UdpSocket::bind(&local_addr).await?; + let Some(stun_addr) = stun_server + .to_socket_addrs()? + .filter(|x| x.is_ipv4()) + .next() + else { + bail!( + "Failed to resolve STUN ipv4 server address: {}", + stun_server + ); + }; + let client = StunClient::new(stun_addr); + let addr = client.query_external_address_async(&socket).await?; + Ok(if addr.ip().is_ipv4() { + (addr, stun_server.to_owned()) + } else { + bail!("STUN server returned non-IPv6 address: {}", addr) + }) +} + +static STUNS_V4: [&str; 3] = [ + "stun.l.google.com:19302", + "stun.cloudflare.com:3478", + "stun.nextcloud.com:3478", +]; + +static STUNS_V6: [&str; 3] = [ + "stun.l.google.com:19302", + "stun.cloudflare.com:3478", + "stun.nextcloud.com:3478", +]; + +pub async fn test_nat_ipv4() -> ResultType<(SocketAddr, String)> { + use hbb_common::futures::future::{select_ok, FutureExt}; + let tests = STUNS_V4 + .iter() + .map(|&stun| stun_ipv4_test(stun).boxed()) + .collect::>(); + + match select_ok(tests).await { + Ok(res) => { + return Ok(res.0); + } + Err(e) => { + bail!( + "Failed to get public IPv4 address via public STUN servers: {}", + e + ); + } + }; +} + +async fn test_bind_ipv6() -> ResultType { + let local_addr = SocketAddr::from(([0u16; 8], 0)); // [::]:0 + let socket = UdpSocket::bind(local_addr).await?; + let addr = STUNS_V6[0] + .to_socket_addrs()? + .filter(|x| x.is_ipv6()) + .next() + .ok_or_else(|| { + anyhow!( + "Failed to resolve STUN ipv6 server address: {}", + STUNS_V6[0] + ) + })?; + socket.connect(addr).await?; + Ok(socket.local_addr()?) +} + +pub async fn test_ipv6() -> Option> { + if PUBLIC_IPV6_ADDR + .lock() + .unwrap() + .1 + .map(|x| x.elapsed().as_secs() < 60) + .unwrap_or(false) + { + return None; + } + PUBLIC_IPV6_ADDR.lock().unwrap().1 = Some(Instant::now()); + + match test_bind_ipv6().await { + Ok(mut addr) => { + if let std::net::IpAddr::V6(ip) = addr.ip() { + if !ip.is_loopback() + && !ip.is_unspecified() + && !ip.is_multicast() + && (ip.segments()[0] & 0xe000) == 0x2000 + { + addr.set_port(0); + PUBLIC_IPV6_ADDR.lock().unwrap().0 = Some(addr); + log::debug!("Found public IPv6 address locally: {}", addr); + } + } + } + Err(e) => { + log::warn!("Failed to bind IPv6 socket: {}", e); + } + } + // Interestingly, on my macOS, sometimes my ipv6 works, sometimes not (test with ping6 or https://test-ipv6.com/). + // I checked ifconfig, could not see any difference. Both secure ipv6 and temporary ipv6 are there. + // So we can not rely on the local ipv6 address queries with if_addrs. + // above test_bind_ipv6 is safer, because it can fail in this case. + /* + std::thread::spawn(|| { + if let Ok(ifaces) = if_addrs::get_if_addrs() { + for iface in ifaces { + if let if_addrs::IfAddr::V6(v6) = iface.addr { + let ip = v6.ip; + if !ip.is_loopback() + && !ip.is_unspecified() + && !ip.is_multicast() + && !ip.is_unique_local() + && !ip.is_unicast_link_local() + && (ip.segments()[0] & 0xe000) == 0x2000 + { + // only use the first one, on mac, the first one is the stable + // one, the last one is the temporary one. The middle ones are deperecated. + *PUBLIC_IPV6_ADDR.lock().unwrap() = + Some((SocketAddr::from((ip, 0)), Instant::now())); + log::debug!("Found public IPv6 address locally: {}", ip); + break; + } + } + } + } + }); + */ + + Some(tokio::spawn(async { + use hbb_common::futures::future::{select_ok, FutureExt}; + let tests = STUNS_V6 + .iter() + .map(|&stun| stun_ipv6_test(stun).boxed()) + .collect::>(); + + match select_ok(tests).await { + Ok(res) => { + let mut addr = res.0 .0; + addr.set_port(0); // Set port to 0 to avoid conflicts + PUBLIC_IPV6_ADDR.lock().unwrap().0 = Some(addr); + log::debug!( + "Found public IPv6 address via STUN server {}: {}", + res.0 .1, + addr + ); + } + Err(e) => { + log::error!("Failed to get public IPv6 address: {}", e); + } + }; + })) +} + +pub async fn punch_udp( + socket: Arc, + listen: bool, +) -> ResultType> { + let mut retry_interval = Duration::from_millis(20); + const MAX_INTERVAL: Duration = Duration::from_millis(200); + const MAX_TIME: Duration = Duration::from_secs(20); + let mut packets_sent = 0; + socket.send(&[]).await.ok(); + packets_sent += 1; + let mut last_send_time = Instant::now(); + let tm = Instant::now(); + let mut data = [0u8; 1500]; + + loop { + tokio::select! { + _ = hbb_common::sleep(retry_interval.as_secs_f32()) => { + if tm.elapsed() > MAX_TIME { + bail!("UDP punch is timed out, stop sending packets after {:?} packets", packets_sent); + } + let elapsed = last_send_time.elapsed(); + + if elapsed >= retry_interval { + socket.send(&[]).await.ok(); + packets_sent += 1; + + // Exponentially increase interval to reduce network pressure + retry_interval = std::cmp::min( + Duration::from_millis((retry_interval.as_millis() as f64 * 1.5) as u64), + MAX_INTERVAL + ); + last_send_time = Instant::now(); + } + } + res = socket.recv(&mut data) => match res { + Err(e) => bail!("UDP punch failed, {packets_sent} packets sent: {e}"), + Ok(n) => { + // log::debug!("UDP punch succeeded after sending {} packets after {:?}", packets_sent, tm.elapsed()); + if listen { + if n == 0 { + continue; + } + return Ok(Some(bytes::BytesMut::from(&data[..n]))); + } + return Ok(None); + } + } + } + } +} + +fn test_ipv6_sync() { + #[tokio::main(flavor = "current_thread")] + async fn func() { + if let Some(job) = test_ipv6().await { + job.await.ok(); + } + } + std::thread::spawn(func); +} + +pub async fn get_ipv6_socket() -> Option<(Arc, bytes::Bytes)> { + let Some(addr) = PUBLIC_IPV6_ADDR.lock().unwrap().0 else { + return None; + }; + + match UdpSocket::bind(addr).await { + Err(err) => { + log::warn!("Failed to create UDP socket for IPv6: {err}"); + } + Ok(socket) => { + if let Ok(local_addr_v6) = socket.local_addr() { + return Some(( + Arc::new(socket), + hbb_common::AddrMangle::encode(local_addr_v6).into(), + )); + } + } + } + None +} + +// The color is the same to `str2color()` in flutter. +pub fn str2color(s: &str, alpha: u8) -> u32 { + let bytes = s.as_bytes(); + // dart code `160 << 16 + 114 << 8 + 91` results `0`. + let mut hash: u32 = 0; + for &byte in bytes { + let code = byte as u32; + hash = code.wrapping_add((hash << 5).wrapping_sub(hash)); + } + + hash = hash % 16777216; + let rgb = hash & 0xFF7FFF; + + (alpha as u32) << 24 | rgb +} + +/// Check control permission state from a u64 bitmap. +/// Each permission uses 2 bits: 0 = not set, 1 = disable, 2 = enable, 3 = invalid (treated as not set) +/// Returns: Some(true) = enabled, Some(false) = disabled, None = not set or invalid +pub fn get_control_permission( + permissions: u64, + permission: hbb_common::rendezvous_proto::control_permissions::Permission, +) -> Option { + use hbb_common::protobuf::Enum; + let index = permission.value(); + if index >= 0 && index < 32 { + let shift = index * 2; + let value = (permissions >> shift) & 0b11; + match value { + 1 => Some(false), // disable + 2 => Some(true), // enable + _ => None, // 0 = not set, 3 = invalid + } + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hbb_common::tokio::{ + self, + time::{interval, interval_at, sleep, Duration, Instant, Interval}, + }; + use std::collections::HashSet; + + #[inline] + fn get_timestamp_secs() -> u128 { + (std::time::SystemTime::UNIX_EPOCH + .elapsed() + .unwrap() + .as_millis() + + 500) + / 1000 + } + + fn interval_maker() -> Interval { + interval(Duration::from_secs(1)) + } + + fn interval_at_maker() -> Interval { + interval_at( + Instant::now() + Duration::from_secs(1), + Duration::from_secs(1), + ) + } + + // ThrottledInterval tick at the same time as tokio interval, if no sleeps + #[allow(non_snake_case)] + #[tokio::test] + async fn test_RustDesk_interval() { + let base_intervals = [interval_maker, interval_at_maker]; + for maker in base_intervals.into_iter() { + let mut tokio_timer = maker(); + let mut tokio_times = Vec::new(); + let mut timer = rustdesk_interval(maker()); + let mut times = Vec::new(); + loop { + tokio::select! { + _ = timer.tick() => { + if tokio_times.len() >= 10 && times.len() >= 10 { + break; + } + times.push(get_timestamp_secs()); + } + _ = tokio_timer.tick() => { + if tokio_times.len() >= 10 && times.len() >= 10 { + break; + } + tokio_times.push(get_timestamp_secs()); + } + } + } + assert_eq!(times, tokio_times); + } + } + + #[tokio::test] + async fn test_tokio_time_interval_sleep() { + let mut timer = interval_maker(); + let mut times = Vec::new(); + sleep(Duration::from_secs(3)).await; + loop { + tokio::select! { + _ = timer.tick() => { + times.push(get_timestamp_secs()); + if times.len() == 5 { + break; + } + } + } + } + let times2: HashSet = HashSet::from_iter(times.clone()); + assert_eq!(times.len(), times2.len() + 3); + } + + // ThrottledInterval tick less times than tokio interval, if there're sleeps + #[allow(non_snake_case)] + #[tokio::test] + async fn test_RustDesk_interval_sleep() { + let base_intervals = [interval_maker, interval_at_maker]; + for (i, maker) in base_intervals.into_iter().enumerate() { + let mut timer = rustdesk_interval(maker()); + let mut times = Vec::new(); + sleep(Duration::from_secs(3)).await; + loop { + tokio::select! { + _ = timer.tick() => { + times.push(get_timestamp_secs()); + if times.len() == 5 { + break; + } + } + } + } + // No multiple ticks in the `interval` time. + // Values in "times" are unique and are less than normal tokio interval. + // See previous test (test_tokio_time_interval_sleep) for comparison. + let times2: HashSet = HashSet::from_iter(times.clone()); + assert_eq!(times.len(), times2.len(), "test: {}", i); + } + } + + #[test] + fn test_duration_multiplication() { + let dur = Duration::from_secs(1); + + assert_eq!(dur * 2, Duration::from_secs(2)); + assert_eq!( + Duration::from_secs_f64(dur.as_secs_f64() * 0.9), + Duration::from_millis(900) + ); + assert_eq!( + Duration::from_secs_f64(dur.as_secs_f64() * 0.923), + Duration::from_millis(923) + ); + assert_eq!( + Duration::from_secs_f64(dur.as_secs_f64() * 0.923 * 1e-3), + Duration::from_micros(923) + ); + assert_eq!( + Duration::from_secs_f64(dur.as_secs_f64() * 0.923 * 1e-6), + Duration::from_nanos(923) + ); + assert_eq!( + Duration::from_secs_f64(dur.as_secs_f64() * 0.923 * 1e-9), + Duration::from_nanos(1) + ); + assert_eq!( + Duration::from_secs_f64(dur.as_secs_f64() * 0.5 * 1e-9), + Duration::from_nanos(1) + ); + assert_eq!( + Duration::from_secs_f64(dur.as_secs_f64() * 0.499 * 1e-9), + Duration::from_nanos(0) + ); + } + + #[test] + fn test_is_public() { + // Test URLs containing "rustdesk.com/" + assert!(is_public("https://rustdesk.com/")); + assert!(is_public("https://www.rustdesk.com/")); + assert!(is_public("https://api.rustdesk.com/v1")); + assert!(is_public("https://API.RUSTDESK.COM/v1")); + assert!(is_public("https://rustdesk.com/path")); + + // Test URLs ending with "rustdesk.com" + assert!(is_public("rustdesk.com")); + assert!(is_public("https://rustdesk.com")); + assert!(is_public("https://RustDesk.com")); + assert!(is_public("http://www.rustdesk.com")); + assert!(is_public("https://api.rustdesk.com")); + + // Test non-public URLs + assert!(!is_public("https://example.com")); + assert!(!is_public("https://custom-server.com")); + assert!(!is_public("http://192.168.1.1")); + assert!(!is_public("localhost")); + assert!(!is_public("https://rustdesk.computer.com")); + assert!(!is_public("rustdesk.comhello.com")); + } + + #[test] + fn test_should_use_tcp_proxy_for_api_url() { + assert!(should_use_tcp_proxy_for_api_url( + "https://admin.example.com/api/login", + "https://admin.example.com" + )); + assert!(should_use_tcp_proxy_for_api_url( + "https://admin.example.com:21114/api/login", + "https://admin.example.com" + )); + assert!(!should_use_tcp_proxy_for_api_url( + "https://api.telegram.org/bot123/sendMessage", + "https://admin.example.com" + )); + assert!(!should_use_tcp_proxy_for_api_url( + "https://admin.rustdesk.com/api/login", + "https://admin.rustdesk.com" + )); + assert!(!should_use_tcp_proxy_for_api_url( + "https://admin.example.com/api/login", + "not a url" + )); + assert!(!should_use_tcp_proxy_for_api_url( + "not a url", + "https://admin.example.com" + )); + } + + #[test] + fn test_get_tcp_proxy_addr_normalizes_bare_ipv6_host() { + struct RestoreCustomRendezvousServer(String); + + impl Drop for RestoreCustomRendezvousServer { + fn drop(&mut self) { + Config::set_option( + keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(), + self.0.clone(), + ); + } + } + + let _restore = RestoreCustomRendezvousServer(Config::get_option( + keys::OPTION_CUSTOM_RENDEZVOUS_SERVER, + )); + Config::set_option( + keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(), + "1:2".to_string(), + ); + + assert_eq!(get_tcp_proxy_addr(), format!("[1:2]:{RENDEZVOUS_PORT}")); + } + + #[tokio::test] + async fn test_http_request_via_tcp_proxy_rejects_invalid_header_json() { + let result = http_request_via_tcp_proxy("not a url", "get", None, "{").await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_http_request_via_tcp_proxy_rejects_non_object_header_json() { + let err = http_request_via_tcp_proxy("not a url", "get", None, "[]") + .await + .unwrap_err() + .to_string(); + assert!(err.contains("HTTP header information parsing failed!")); + } + + #[test] + fn test_parse_json_header_entries_preserves_single_content_type() { + let headers = parse_json_header_entries( + r#"{"Content-Type":"text/plain","Authorization":"Bearer token"}"#, + ) + .unwrap(); + + assert_eq!( + headers + .iter() + .filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .count(), + 1 + ); + assert_eq!( + headers + .iter() + .find(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .map(|entry| entry.value.as_str()), + Some("text/plain") + ); + } + + #[test] + fn test_parse_json_header_entries_does_not_add_default_content_type() { + let headers = parse_json_header_entries(r#"{"Authorization":"Bearer token"}"#).unwrap(); + + assert!(!headers + .iter() + .any(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))); + } + + #[test] + fn test_parse_simple_header_respects_custom_content_type() { + let headers = parse_simple_header("Content-Type: text/plain"); + + assert_eq!( + headers + .iter() + .filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .count(), + 1 + ); + assert_eq!( + headers + .iter() + .find(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .map(|entry| entry.value.as_str()), + Some("text/plain") + ); + } + + #[test] + fn test_parse_simple_header_preserves_non_content_type_header() { + let headers = parse_simple_header("Authorization: Bearer token"); + + assert!(headers.iter().any(|entry| { + entry.name.eq_ignore_ascii_case("Authorization") + && entry.value.as_str() == "Bearer token" + })); + assert_eq!( + headers + .iter() + .filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .count(), + 1 + ); + assert_eq!( + headers + .iter() + .find(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .map(|entry| entry.value.as_str()), + Some("application/json") + ); + } + + #[test] + fn test_tcp_proxy_log_target_redacts_query_only() { + assert_eq!( + tcp_proxy_log_target("https://example.com/api/heartbeat?token=secret"), + "https://example.com/api/heartbeat" + ); + } + + #[test] + fn test_tcp_proxy_log_target_brackets_ipv6_host_with_port() { + assert_eq!( + tcp_proxy_log_target("https://[2001:db8::1]:21114/api/heartbeat?token=secret"), + "https://[2001:db8::1]:21114/api/heartbeat" + ); + } + + #[test] + fn test_http_proxy_response_to_json() { + let mut resp = HttpProxyResponse { + status: 200, + body: br#"{"ok":true}"#.to_vec().into(), + ..Default::default() + }; + resp.headers.push(HeaderEntry { + name: "Content-Type".into(), + value: "application/json".into(), + ..Default::default() + }); + + let json = http_proxy_response_to_json(resp).unwrap(); + let value: Value = serde_json::from_str(&json).unwrap(); + assert_eq!(value["status_code"], 200); + assert_eq!(value["headers"]["content-type"], "application/json"); + assert_eq!(value["body"], r#"{"ok":true}"#); + + let err = http_proxy_response_to_json(HttpProxyResponse { + error: "dial failed".into(), + ..Default::default() + }) + .unwrap_err() + .to_string(); + assert!(err.contains("TCP proxy error: dial failed")); + } + + #[test] + fn test_mouse_event_constants_and_mask_layout() { + use super::input::*; + + // Verify MOUSE_TYPE constants are unique and within the mask range. + let types = [ + MOUSE_TYPE_MOVE, + MOUSE_TYPE_DOWN, + MOUSE_TYPE_UP, + MOUSE_TYPE_WHEEL, + MOUSE_TYPE_TRACKPAD, + MOUSE_TYPE_MOVE_RELATIVE, + ]; + + let mut seen = std::collections::HashSet::new(); + for t in types.iter() { + assert!(seen.insert(*t), "Duplicate mouse type: {}", t); + assert_eq!( + *t & MOUSE_TYPE_MASK, + *t, + "Mouse type {} exceeds mask {}", + t, + MOUSE_TYPE_MASK + ); + } + + // The mask layout is: lower 3 bits for type, upper bits for buttons (shifted by 3). + let combined_mask = MOUSE_TYPE_DOWN | ((MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT) << 3); + assert_eq!(combined_mask & MOUSE_TYPE_MASK, MOUSE_TYPE_DOWN); + assert_eq!(combined_mask >> 3, MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT); + } +} diff --git a/vendor/rustdesk/src/core_main.rs b/vendor/rustdesk/src/core_main.rs new file mode 100644 index 0000000..e270919 --- /dev/null +++ b/vendor/rustdesk/src/core_main.rs @@ -0,0 +1,850 @@ +#[cfg(any(target_os = "windows", target_os = "macos"))] +use crate::client::translate; +#[cfg(not(debug_assertions))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::platform::breakdown_callback; +#[cfg(not(debug_assertions))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::platform::register_breakdown_handler; +use hbb_common::{config, log}; +#[cfg(windows)] +use tauri_winrt_notification::{Duration, Sound, Toast}; + +#[macro_export] +macro_rules! my_println{ + ($($arg:tt)*) => { + #[cfg(not(windows))] + println!("{}", format_args!($($arg)*)); + #[cfg(windows)] + crate::platform::message_box( + &format!("{}", format_args!($($arg)*)) + ); + }; +} + +/// shared by flutter and sciter main function +/// +/// [Note] +/// If it returns [`None`], then the process will terminate, and flutter gui will not be started. +/// If it returns [`Some`], then the process will continue, and flutter gui will be started. +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn core_main() -> Option> { + if !crate::common::global_init() { + return None; + } + crate::load_custom_client(); + #[cfg(windows)] + if !crate::platform::windows::bootstrap() { + // return None to terminate the process + return None; + } + let mut args = Vec::new(); + let mut flutter_args = Vec::new(); + let mut i = 0; + let mut _is_elevate = false; + let mut _is_run_as_system = false; + let mut _is_quick_support = false; + let mut _is_flutter_invoke_new_connection = false; + let mut no_server = false; + let mut arg_exe = Default::default(); + for arg in std::env::args() { + if i == 0 { + arg_exe = arg; + } else if i > 0 { + #[cfg(feature = "flutter")] + if [ + "--connect", + "--play", + "--file-transfer", + "--view-camera", + "--port-forward", + "--terminal", + "--rdp", + ] + .contains(&arg.as_str()) + { + _is_flutter_invoke_new_connection = true; + } + if arg == "--elevate" { + _is_elevate = true; + } else if arg == "--run-as-system" { + _is_run_as_system = true; + } else if arg == "--quick_support" { + _is_quick_support = true; + } else if arg == "--no-server" { + no_server = true; + } else { + args.push(arg); + } + } + i += 1; + } + #[cfg(any(target_os = "linux", target_os = "windows"))] + if args.is_empty() { + #[cfg(target_os = "linux")] + let should_check_start_tray = crate::check_process("--server", false); + // We can use `crate::check_process("--server", false)` on Windows. + // Because `--server` process is the System user's process. We can't get the arguments in `check_process()`. + // We can assume that self service running means the server is also running on Windows. + #[cfg(target_os = "windows")] + let should_check_start_tray = crate::platform::is_self_service_running() + && crate::platform::is_cur_exe_the_installed(); + if should_check_start_tray && !crate::check_process("--tray", true) { + #[cfg(target_os = "linux")] + hbb_common::allow_err!(crate::platform::check_autostart_config()); + hbb_common::allow_err!(crate::run_me(vec!["--tray"])); + } + } + #[cfg(not(debug_assertions))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + register_breakdown_handler(breakdown_callback); + #[cfg(target_os = "linux")] + #[cfg(feature = "flutter")] + { + let (k, v) = ("LIBGL_ALWAYS_SOFTWARE", "1"); + if config::option2bool( + "allow-always-software-render", + &config::Config::get_option("allow-always-software-render"), + ) { + std::env::set_var(k, v); + } else { + std::env::remove_var(k); + } + } + #[cfg(windows)] + if args.contains(&"--connect".to_string()) || args.contains(&"--view-camera".to_string()) { + hbb_common::platform::windows::start_cpu_performance_monitor(); + } + #[cfg(feature = "flutter")] + if _is_flutter_invoke_new_connection { + return core_main_invoke_new_connection(std::env::args()); + } + let click_setup = cfg!(windows) && args.is_empty() && crate::common::is_setup(&arg_exe); + if click_setup && !config::is_disable_installation() { + args.push("--install".to_owned()); + flutter_args.push("--install".to_string()); + } + if args.contains(&"--noinstall".to_string()) { + args.clear(); + } + if args.len() > 0 { + if args[0] == "--version" { + println!("{}", crate::VERSION); + return None; + } else if args[0] == "--build-date" { + println!("{}", crate::BUILD_DATE); + return None; + } + } + #[cfg(windows)] + { + _is_quick_support |= !crate::platform::is_installed() + && args.is_empty() + && (is_quick_support_exe(&arg_exe) + || config::LocalConfig::get_option("pre-elevate-service") == "Y" + || (!click_setup && crate::platform::is_elevated(None).unwrap_or(false))); + crate::portable_service::client::set_quick_support(_is_quick_support); + } + let mut log_name = "".to_owned(); + if args.len() > 0 && args[0].starts_with("--") { + let name = args[0].replace("--", ""); + if !name.is_empty() { + log_name = name; + } + } + hbb_common::init_log(false, &log_name); + + // linux uni (url) go here. + #[cfg(all(target_os = "linux", feature = "flutter"))] + if args.len() > 0 && args[0].starts_with(&crate::get_uri_prefix()) { + return try_send_by_dbus(args[0].clone()); + } + + #[cfg(windows)] + if !crate::platform::is_installed() + && args.is_empty() + && _is_quick_support + && !_is_elevate + && !_is_run_as_system + { + use crate::portable_service::client; + if let Err(e) = client::start_portable_service(client::StartPara::Direct) { + log::error!("Failed to start portable service: {:?}", e); + } + } + #[cfg(windows)] + if !crate::platform::is_installed() && (_is_elevate || _is_run_as_system) { + crate::platform::elevate_or_run_as_system(click_setup, _is_elevate, _is_run_as_system); + return None; + } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + init_plugins(&args); + if args.is_empty() || crate::common::is_empty_uni_link(&args[0]) { + #[cfg(target_os = "macos")] + { + crate::platform::macos::try_remove_temp_update_dir(None); + } + + #[cfg(windows)] + { + crate::platform::try_remove_temp_update_files(); + hbb_common::config::PeerConfig::preload_peers(); + } + std::thread::spawn(move || crate::start_server(false, no_server)); + } else { + #[cfg(windows)] + { + use crate::platform; + if args[0] == "--uninstall" { + if let Err(err) = platform::uninstall_me(true) { + log::error!("Failed to uninstall: {}", err); + } + return None; + } else if args[0] == "--update" { + if config::is_disable_installation() { + return None; + } + + let text = match crate::platform::prepare_custom_client_update() { + Err(e) => { + log::error!("Error preparing custom client update: {}", e); + "Update failed!".to_string() + } + Ok(false) => "Update failed!".to_string(), + Ok(true) => match platform::update_me(false) { + Ok(_) => "Updated successfully!".to_string(), + Err(err) => { + log::error!("Failed with error: {err}"); + "Update failed!".to_string() + } + }, + }; + Toast::new(Toast::POWERSHELL_APP_ID) + .title(&config::APP_NAME.read().unwrap()) + .text1(&translate(text)) + .sound(Some(Sound::Default)) + .duration(Duration::Short) + .show() + .ok(); + return None; + } else if args[0] == "--after-install" { + if let Err(err) = platform::run_after_install() { + log::error!("Failed to after-install: {}", err); + } + return None; + } else if args[0] == "--before-uninstall" { + if let Err(err) = platform::run_before_uninstall() { + log::error!("Failed to before-uninstall: {}", err); + } + return None; + } else if args[0] == "--silent-install" { + if config::is_disable_installation() { + return None; + } + #[cfg(not(windows))] + let options = "desktopicon startmenu"; + #[cfg(windows)] + let options = "desktopicon startmenu printer"; + let res = platform::install_me(options, "".to_owned(), true, args.len() > 1); + let text = match res { + Ok(_) => translate("Installation Successful!".to_string()), + Err(err) => { + println!("Failed with error: {err}"); + translate("Installation failed!".to_string()) + } + }; + Toast::new(Toast::POWERSHELL_APP_ID) + .title(&config::APP_NAME.read().unwrap()) + .text1(&text) + .sound(Some(Sound::Default)) + .duration(Duration::Short) + .show() + .ok(); + return None; + } else if args[0] == "--uninstall-cert" { + #[cfg(windows)] + hbb_common::allow_err!(crate::platform::windows::uninstall_cert()); + return None; + } else if args[0] == "--install-idd" { + #[cfg(windows)] + if crate::virtual_display_manager::is_virtual_display_supported() { + hbb_common::allow_err!( + crate::virtual_display_manager::rustdesk_idd::install_update_driver() + ); + } + return None; + } else if args[0] == "--portable-service" { + crate::platform::elevate_or_run_as_system( + click_setup, + _is_elevate, + _is_run_as_system, + ); + return None; + } else if args[0] == "--uninstall-amyuni-idd" { + #[cfg(windows)] + hbb_common::allow_err!( + crate::virtual_display_manager::amyuni_idd::uninstall_driver() + ); + return None; + } else if args[0] == "--install-remote-printer" { + #[cfg(windows)] + if crate::platform::is_win_10_or_greater() { + match remote_printer::install_update_printer(&crate::get_app_name()) { + Ok(_) => { + log::info!("Remote printer installed/updated successfully"); + } + Err(e) => { + log::error!("Failed to install/update the remote printer: {}", e); + } + } + } else { + log::error!("Win10 or greater required!"); + } + return None; + } else if args[0] == "--uninstall-remote-printer" { + #[cfg(windows)] + if crate::platform::is_win_10_or_greater() { + remote_printer::uninstall_printer(&crate::get_app_name()); + log::info!("Remote printer uninstalled"); + } + return None; + } + } + #[cfg(target_os = "macos")] + { + use crate::platform; + if args[0] == "--update" { + if args.len() > 1 && args[1].ends_with(".dmg") { + // Version check is unnecessary unless downgrading to an older version + // that lacks "update dmg" support. This is a special case since we cannot + // detect the version before extracting the DMG, so we skip the check. + let dmg_path = &args[1]; + println!("Updating from DMG: {}", dmg_path); + match platform::update_from_dmg(dmg_path) { + Ok(_) => { + println!("Update process from DMG started successfully."); + // The new process will handle the rest. We can exit. + } + Err(err) => { + eprintln!("Failed to start update from DMG: {}", err); + } + } + } else { + println!("Starting update process..."); + log::info!("Starting update process..."); + let _text = match platform::update_me() { + Ok(_) => { + println!("{}", translate("Updated successfully!".to_string())); + log::info!("Updated successfully!"); + } + Err(err) => { + eprintln!("Update failed with error: {}", err); + log::error!("Update failed with error: {err}"); + } + }; + } + return None; + } + } + if args[0] == "--remove" { + if args.len() == 2 { + // sleep a while so that process of removed exe exit + std::thread::sleep(std::time::Duration::from_secs(1)); + std::fs::remove_file(&args[1]).ok(); + return None; + } + } else if args[0] == "--tray" { + if !crate::check_process("--tray", true) { + crate::tray::start_tray(); + } + return None; + } else if args[0] == "--install-service" { + log::info!("start --install-service"); + crate::platform::install_service(); + return None; + } else if args[0] == "--uninstall-service" { + log::info!("start --uninstall-service"); + crate::platform::uninstall_service(false, true); + return None; + } else if args[0] == "--service" { + log::info!("start --service"); + crate::start_os_service(); + return None; + } else if args[0] == "--server" { + log::info!("start --server with user {}", crate::username()); + #[cfg(target_os = "linux")] + { + hbb_common::allow_err!(crate::platform::check_autostart_config()); + std::process::Command::new("pkill") + .arg("-f") + .arg(&format!("{} --tray", crate::get_app_name().to_lowercase())) + .status() + .ok(); + hbb_common::allow_err!(crate::run_me(vec!["--tray"])); + } + #[cfg(windows)] + crate::privacy_mode::restore_reg_connectivity(true, false); + #[cfg(any(target_os = "linux", target_os = "windows"))] + { + crate::start_server(true, false); + } + #[cfg(target_os = "macos")] + { + let handler = std::thread::spawn(move || crate::start_server(true, false)); + crate::tray::start_tray(); + // prevent server exit when encountering errors from tray + hbb_common::allow_err!(handler.join()); + } + return None; + } else if args[0] == "--import-config" { + if args.len() == 2 { + let filepath; + let path = std::path::Path::new(&args[1]); + if !path.is_absolute() { + let mut cur = std::env::current_dir().unwrap(); + cur.push(path); + filepath = cur.to_str().unwrap().to_string(); + } else { + filepath = path.to_str().unwrap().to_string(); + } + import_config(&filepath); + } + return None; + } else if args[0] == "--password" { + if config::is_disable_settings() { + println!("Settings are disabled!"); + return None; + } + if config::Config::is_disable_change_permanent_password() { + println!("Changing permanent password is disabled!"); + return None; + } + if args.len() == 2 { + if crate::platform::is_installed() && is_root() { + if let Err(err) = crate::ipc::set_permanent_password(args[1].to_owned()) { + println!("{err}"); + } else { + println!("Done!"); + } + } else { + println!("Installation and administrative privileges required!"); + } + } + return None; + } else if args[0] == "--set-unlock-pin" { + if config::Config::is_disable_unlock_pin() { + println!("Unlock PIN is disabled!"); + return None; + } + #[cfg(feature = "flutter")] + if args.len() == 2 { + if crate::platform::is_installed() && is_root() { + if let Err(err) = crate::ipc::set_unlock_pin(args[1].to_owned(), false) { + println!("{err}"); + } else { + println!("Done!"); + } + } else { + println!("Installation and administrative privileges required!"); + } + } + return None; + } else if args[0] == "--get-id" { + println!("{}", crate::ipc::get_id()); + return None; + } else if args[0] == "--set-id" { + if config::is_disable_settings() { + println!("Settings are disabled!"); + return None; + } + if config::Config::is_disable_change_id() { + println!("Changing ID is disabled!"); + return None; + } + if args.len() == 2 { + if crate::platform::is_installed() && is_root() { + let old_id = crate::ipc::get_id(); + let mut res = crate::ui_interface::change_id_shared(args[1].to_owned(), old_id); + if res.is_empty() { + res = "Done!".to_owned(); + } + println!("{}", res); + } else { + println!("Installation and administrative privileges required!"); + } + } + return None; + } else if args[0] == "--config" { + if args.len() == 2 && !args[0].contains("host=") { + if crate::platform::is_installed() && is_root() { + // encrypted string used in renaming exe. + let name = if args[1].ends_with(".exe") { + args[1].to_owned() + } else { + format!("{}.exe", args[1]) + }; + if let Ok(lic) = crate::custom_server::get_custom_server_from_string(&name) { + if !lic.host.is_empty() { + crate::ui_interface::set_option("key".into(), lic.key); + crate::ui_interface::set_option( + "custom-rendezvous-server".into(), + lic.host, + ); + crate::ui_interface::set_option("api-server".into(), lic.api); + crate::ui_interface::set_option("relay-server".into(), lic.relay); + } + } + } else { + println!("Installation and administrative privileges required!"); + } + } + return None; + } else if args[0] == "--option" { + if config::is_disable_settings() { + println!("Settings are disabled!"); + return None; + } + if crate::platform::is_installed() && is_root() { + if args.len() == 2 { + let options = crate::ipc::get_options(); + println!("{}", options.get(&args[1]).unwrap_or(&"".to_owned())); + } else if args.len() == 3 { + crate::ipc::set_option(&args[1], &args[2]); + } + } else { + println!("Installation and administrative privileges required!"); + } + return None; + } else if args[0] == "--assign" { + if config::Config::no_register_device() { + println!("Cannot assign an unregistrable device!"); + } else if crate::platform::is_installed() && is_root() { + let max = args.len() - 1; + let pos = args.iter().position(|x| x == "--token").unwrap_or(max); + if pos < max { + let token = args[pos + 1].to_owned(); + let id = crate::ipc::get_id(); + let uuid = crate::encode64(hbb_common::get_uuid()); + let get_value = |c: &str| { + let pos = args.iter().position(|x| x == c).unwrap_or(max); + if pos < max { + Some(args[pos + 1].to_owned()) + } else { + None + } + }; + let user_name = get_value("--user_name"); + let strategy_name = get_value("--strategy_name"); + let address_book_name = get_value("--address_book_name"); + let address_book_tag = get_value("--address_book_tag"); + let address_book_alias = get_value("--address_book_alias"); + let address_book_password = get_value("--address_book_password"); + let address_book_note = get_value("--address_book_note"); + let device_group_name = get_value("--device_group_name"); + let note = get_value("--note"); + let device_username = get_value("--device_username"); + let device_name = get_value("--device_name"); + let mut body = serde_json::json!({ + "id": id, + "uuid": uuid, + }); + let header = "Authorization: Bearer ".to_owned() + &token; + if user_name.is_none() + && strategy_name.is_none() + && address_book_name.is_none() + && device_group_name.is_none() + && note.is_none() + && device_username.is_none() + && device_name.is_none() + { + println!( + r#"At least one of the following options is required: + --user_name + --strategy_name + --address_book_name + --device_group_name + --note + --device_username + --device_name"# + ); + } else { + if let Some(name) = user_name { + body["user_name"] = serde_json::json!(name); + } + if let Some(name) = strategy_name { + body["strategy_name"] = serde_json::json!(name); + } + if let Some(name) = address_book_name { + body["address_book_name"] = serde_json::json!(name); + if let Some(name) = address_book_tag { + body["address_book_tag"] = serde_json::json!(name); + } + if let Some(name) = address_book_alias { + body["address_book_alias"] = serde_json::json!(name); + } + if let Some(name) = address_book_password { + body["address_book_password"] = serde_json::json!(name); + } + if let Some(name) = address_book_note { + body["address_book_note"] = serde_json::json!(name); + } + } + if let Some(name) = device_group_name { + body["device_group_name"] = serde_json::json!(name); + } + if let Some(name) = note { + body["note"] = serde_json::json!(name); + } + if let Some(name) = device_username { + body["device_username"] = serde_json::json!(name); + } + if let Some(name) = device_name { + body["device_name"] = serde_json::json!(name); + } + let url = crate::ui_interface::get_api_server() + "/api/devices/cli"; + match crate::post_request_sync(url, body.to_string(), &header) { + Err(err) => println!("{}", err), + Ok(text) => { + if text.is_empty() { + println!("Done!"); + } else { + println!("{}", text); + } + } + } + } + } else { + println!("--token is required!"); + } + } else { + println!("Installation and administrative privileges required!"); + } + return None; + } else if args[0] == "--check-hwcodec-config" { + #[cfg(feature = "hwcodec")] + crate::ipc::hwcodec_process(); + return None; + } else if args[0] == "--terminal-helper" { + // Terminal helper process - runs as user to create ConPTY + // This is needed because ConPTY has compatibility issues with CreateProcessAsUserW + #[cfg(target_os = "windows")] + { + let helper_args: Vec = args[1..].to_vec(); + if let Err(e) = crate::server::terminal_helper::run_terminal_helper(&helper_args) { + log::error!("Terminal helper failed: {}", e); + } + } + return None; + } else if args[0] == "--cm" { + // call connection manager to establish connections + // meanwhile, return true to call flutter window to show control panel + crate::ui_interface::start_option_status_sync(); + } else if args[0] == "--cm-no-ui" { + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + crate::ui_interface::start_option_status_sync(); + crate::flutter::connection_manager::start_cm_no_ui(); + } + return None; + } else if args[0] == "--whiteboard" { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + crate::whiteboard::run(); + } + return None; + } else if args[0] == "-gtk-sudo" { + // rustdesk service kill `rustdesk --` processes + #[cfg(target_os = "linux")] + if args.len() > 2 { + crate::platform::gtk_sudo::exec(); + } + return None; + } else { + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if args[0] == "--plugin-install" { + if args.len() == 2 { + crate::plugin::change_uninstall_plugin(&args[1], false); + } else if args.len() == 3 { + crate::plugin::install_plugin_with_url(&args[1], &args[2]); + } + return None; + } else if args[0] == "--plugin-uninstall" { + if args.len() == 2 { + crate::plugin::change_uninstall_plugin(&args[1], true); + } + return None; + } + } + } + //_async_logger_holder.map(|x| x.flush()); + #[cfg(feature = "flutter")] + return Some(flutter_args); + #[cfg(not(feature = "flutter"))] + return Some(args); +} + +#[inline] +#[cfg(all(feature = "flutter", feature = "plugin_framework"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn init_plugins(args: &Vec) { + if args.is_empty() || "--server" == (&args[0] as &str) { + #[cfg(debug_assertions)] + let load_plugins = true; + #[cfg(not(debug_assertions))] + let load_plugins = crate::platform::is_installed(); + if load_plugins { + crate::plugin::init(); + } + } else if "--service" == (&args[0] as &str) { + hbb_common::allow_err!(crate::plugin::remove_uninstalled()); + } +} + +fn import_config(path: &str) { + use hbb_common::{config::*, get_exe_time, get_modified_time}; + let path2 = path.replace(".toml", "2.toml"); + let path2 = std::path::Path::new(&path2); + let path = std::path::Path::new(path); + log::info!("import config from {:?} and {:?}", path, path2); + let config: Config = load_path(path.into()); + if config.is_empty() { + log::info!("Empty source config, skipped"); + return; + } + if get_modified_time(&path) > get_modified_time(&Config::file()) + && get_modified_time(&path) < get_exe_time() + { + if store_path(Config::file(), config).is_err() { + log::info!("config written"); + } + } + let config2: Config2 = load_path(path2.into()); + if get_modified_time(&path2) > get_modified_time(&Config2::file()) { + if store_path(Config2::file(), config2).is_err() { + log::info!("config2 written"); + } + } +} + +/// invoke a new connection +/// +/// [Note] +/// this is for invoke new connection from dbus. +/// If it returns [`None`], then the process will terminate, and flutter gui will not be started. +/// If it returns [`Some`], then the process will continue, and flutter gui will be started. +#[cfg(feature = "flutter")] +fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option> { + let mut authority = None; + let mut id = None; + let mut param_array = vec![]; + while let Some(arg) = args.next() { + match arg.as_str() { + "--connect" | "--play" | "--file-transfer" | "--view-camera" | "--port-forward" + | "--terminal" | "--rdp" => { + authority = Some((&arg.to_string()[2..]).to_owned()); + id = args.next(); + } + "--password" => { + if let Some(password) = args.next() { + param_array.push(format!("password={password}")); + } + } + "--relay" => { + param_array.push(format!("relay=true")); + } + // inner + "--switch_uuid" => { + if let Some(switch_uuid) = args.next() { + param_array.push(format!("switch_uuid={switch_uuid}")); + } + } + _ => {} + } + } + let mut uni_links = Default::default(); + if let Some(authority) = authority { + if let Some(mut id) = id { + let app_name = crate::get_app_name(); + let ext = format!(".{}", app_name.to_lowercase()); + if id.ends_with(&ext) { + id = id.replace(&ext, ""); + } + let params = param_array.join("&"); + let params_flag = if params.is_empty() { "" } else { "?" }; + uni_links = format!( + "{}{}/{}{}{}", + crate::get_uri_prefix(), + authority, + id, + params_flag, + params + ); + } + } + if uni_links.is_empty() { + return None; + } + + #[cfg(target_os = "linux")] + return try_send_by_dbus(uni_links); + + #[cfg(windows)] + { + use winapi::um::winuser::WM_USER; + let res = crate::platform::send_message_to_hnwd( + &crate::platform::FLUTTER_RUNNER_WIN32_WINDOW_CLASS, + &crate::get_app_name(), + (WM_USER + 2) as _, // referred from unilinks desktop pub + uni_links.as_str(), + false, + ); + return if res { None } else { Some(Vec::new()) }; + } + #[cfg(target_os = "macos")] + { + return if let Err(_) = crate::ipc::send_url_scheme(uni_links) { + Some(Vec::new()) + } else { + None + }; + } +} + +#[cfg(all(target_os = "linux", feature = "flutter"))] +fn try_send_by_dbus(uni_links: String) -> Option> { + use crate::dbus::invoke_new_connection; + + match invoke_new_connection(uni_links) { + Ok(()) => { + return None; + } + Err(err) => { + log::error!("{}", err.as_ref()); + // return Some to invoke this url by self + return Some(Vec::new()); + } + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn is_root() -> bool { + #[cfg(windows)] + { + return crate::platform::is_elevated(None).unwrap_or_default() + || crate::platform::is_root(); + } + #[allow(unreachable_code)] + crate::platform::is_root() +} + +/// Check if the executable is a Quick Support version. +/// Note: This function must be kept in sync with `libs/portable/src/main.rs`. +#[cfg(windows)] +#[inline] +fn is_quick_support_exe(exe: &str) -> bool { + let exe = exe.to_lowercase(); + exe.contains("-qs-") || exe.contains("-qs.exe") || exe.contains("_qs.exe") +} diff --git a/vendor/rustdesk/src/custom_server.rs b/vendor/rustdesk/src/custom_server.rs new file mode 100644 index 0000000..1811878 --- /dev/null +++ b/vendor/rustdesk/src/custom_server.rs @@ -0,0 +1,219 @@ +use hbb_common::{ + bail, + base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}, + sodiumoxide::crypto::sign, + ResultType, +}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] +pub struct CustomServer { + #[serde(default)] + pub key: String, + #[serde(default)] + pub host: String, + #[serde(default)] + pub api: String, + #[serde(default)] + pub relay: String, +} + +fn get_custom_server_from_config_string(s: &str) -> ResultType { + let tmp: String = s.chars().rev().collect(); + const PK: &[u8; 32] = &[ + 88, 168, 68, 104, 60, 5, 163, 198, 165, 38, 12, 85, 114, 203, 96, 163, 70, 48, 0, 131, 57, + 12, 46, 129, 83, 17, 84, 193, 119, 197, 130, 103, + ]; + let pk = sign::PublicKey(*PK); + let data = URL_SAFE_NO_PAD.decode(tmp)?; + if let Ok(lic) = serde_json::from_slice::(&data) { + return Ok(lic); + } + if let Ok(data) = sign::verify(&data, &pk) { + Ok(serde_json::from_slice::(&data)?) + } else { + bail!("sign:verify failed"); + } +} + +pub fn get_custom_server_from_string(s: &str) -> ResultType { + let s = if s.to_lowercase().ends_with(".exe.exe") { + &s[0..s.len() - 8] + } else if s.to_lowercase().ends_with(".exe") { + &s[0..s.len() - 4] + } else { + s + }; + /* + * The following code tokenizes the file name based on commas and + * extracts relevant parts sequentially. + * + * host= is expected to be the first part. + * + * Since Windows renames files adding (1), (2) etc. before the .exe + * in case of duplicates, which causes the host or key values to be + * garbled. + * + * This allows using a ',' (comma) symbol as a final delimiter. + */ + if s.to_lowercase().contains("host=") { + let stripped = &s[s.to_lowercase().find("host=").unwrap_or(0)..s.len()]; + let strs: Vec<&str> = stripped.split(",").collect(); + let mut host = String::default(); + let mut key = String::default(); + let mut api = String::default(); + let mut relay = String::default(); + let strs_iter = strs.iter(); + for el in strs_iter { + let el_lower = el.to_lowercase(); + if el_lower.starts_with("host=") { + host = el.chars().skip(5).collect(); + } + if el_lower.starts_with("key=") { + key = el.chars().skip(4).collect(); + } + if el_lower.starts_with("api=") { + api = el.chars().skip(4).collect(); + } + if el_lower.starts_with("relay=") { + relay = el.chars().skip(6).collect(); + } + } + return Ok(CustomServer { + host, + key, + api, + relay, + }); + } else { + let s = s + .replace("-licensed---", "--") + .replace("-licensed--", "--") + .replace("-licensed-", "--"); + let strs = s.split("--"); + for s in strs { + if let Ok(lic) = get_custom_server_from_config_string(s.trim()) { + return Ok(lic); + } else if s.contains("(") { + // https://github.com/rustdesk/rustdesk/issues/4162 + for s in s.split("(") { + if let Ok(lic) = get_custom_server_from_config_string(s.trim()) { + return Ok(lic); + } + } + } + } + } + bail!("Failed to parse"); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_filename_license_string() { + assert!(get_custom_server_from_string("rustdesk.exe").is_err()); + assert!(get_custom_server_from_string("rustdesk").is_err()); + assert_eq!( + get_custom_server_from_string("rustdesk-host=server.example.net.exe").unwrap(), + CustomServer { + host: "server.example.net".to_owned(), + key: "".to_owned(), + api: "".to_owned(), + relay: "".to_owned(), + } + ); + assert_eq!( + get_custom_server_from_string("rustdesk-host=server.example.net,.exe").unwrap(), + CustomServer { + host: "server.example.net".to_owned(), + key: "".to_owned(), + api: "".to_owned(), + relay: "".to_owned(), + } + ); + // key in these tests is "foobar.,2" base64 encoded + assert_eq!( + get_custom_server_from_string( + "rustdesk-host=server.example.net,api=abc,key=Zm9vYmFyLiwyCg==.exe" + ) + .unwrap(), + CustomServer { + host: "server.example.net".to_owned(), + key: "Zm9vYmFyLiwyCg==".to_owned(), + api: "abc".to_owned(), + relay: "".to_owned(), + } + ); + assert_eq!( + get_custom_server_from_string( + "rustdesk-host=server.example.net,key=Zm9vYmFyLiwyCg==,.exe" + ) + .unwrap(), + CustomServer { + host: "server.example.net".to_owned(), + key: "Zm9vYmFyLiwyCg==".to_owned(), + api: "".to_owned(), + relay: "".to_owned(), + } + ); + assert_eq!( + get_custom_server_from_string( + "rustdesk-host=server.example.net,key=Zm9vYmFyLiwyCg==,relay=server.example.net.exe" + ) + .unwrap(), + CustomServer { + host: "server.example.net".to_owned(), + key: "Zm9vYmFyLiwyCg==".to_owned(), + api: "".to_owned(), + relay: "server.example.net".to_owned(), + } + ); + assert_eq!( + get_custom_server_from_string( + "rustdesk-Host=server.example.net,Key=Zm9vYmFyLiwyCg==,RELAY=server.example.net.exe" + ) + .unwrap(), + CustomServer { + host: "server.example.net".to_owned(), + key: "Zm9vYmFyLiwyCg==".to_owned(), + api: "".to_owned(), + relay: "server.example.net".to_owned(), + } + ); + let lic = CustomServer { + host: "1.1.1.1".to_owned(), + key: "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=".to_owned(), + api: "".to_owned(), + relay: "".to_owned(), + }; + assert_eq!( + get_custom_server_from_string("rustdesk-licensed-0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye.exe") + .unwrap(), lic); + assert_eq!( + get_custom_server_from_string("rustdesk-licensed-0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye(1).exe") + .unwrap(), lic); + assert_eq!( + get_custom_server_from_string("rustdesk--0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye(1).exe") + .unwrap(), lic); + assert_eq!( + get_custom_server_from_string("rustdesk-licensed-0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye (1).exe") + .unwrap(), lic); + assert_eq!( + get_custom_server_from_string("rustdesk-licensed-0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye (1) (2).exe") + .unwrap(), lic); + assert_eq!( + get_custom_server_from_string("rustdesk-licensed-0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye--abc.exe") + .unwrap(), lic); + assert_eq!( + get_custom_server_from_string("rustdesk-licensed--0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye--.exe") + .unwrap(), lic); + assert_eq!( + get_custom_server_from_string("rustdesk-licensed---0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye--.exe") + .unwrap(), lic); + assert_eq!( + get_custom_server_from_string("rustdesk-licensed--0nI900VsFHZVBVdIlncwpHS4V0bOZ0dtVldrpVO4JHdCp0YV5WdzUGZzdnYRVjI6ISeltmIsISMuEjLx4SMiojI0N3boJye--.exe") + .unwrap(), lic); + } +} diff --git a/vendor/rustdesk/src/flutter.rs b/vendor/rustdesk/src/flutter.rs new file mode 100644 index 0000000..c7e07f8 --- /dev/null +++ b/vendor/rustdesk/src/flutter.rs @@ -0,0 +1,2363 @@ +use crate::{ + client::*, + flutter_ffi::{EventToUI, SessionID}, + ui_session_interface::{io_loop, InvokeUiSession, Session}, +}; +use flutter_rust_bridge::StreamSink; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::dlopen::{ + symbor::{Library, Symbol}, + Error as LibError, +}; +use hbb_common::{ + anyhow::anyhow, bail, config::LocalConfig, get_version_number, log, message_proto::*, + rendezvous_proto::ConnType, ResultType, +}; +use serde::Serialize; +use serde_json::json; +#[cfg(target_os = "windows")] +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; +use std::{ + collections::{HashMap, HashSet}, + ffi::CString, + os::raw::{c_char, c_int, c_void}, + str::FromStr, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, RwLock, + }, +}; + +/// tag "main" for [Desktop Main Page] and [Mobile (Client and Server)] (the mobile don't need multiple windows, only one global event stream is needed) +/// tag "cm" only for [Desktop CM Page] +pub(crate) const APP_TYPE_MAIN: &str = "main"; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub(crate) const APP_TYPE_CM: &str = "cm"; +#[cfg(any(target_os = "android", target_os = "ios"))] +pub(crate) const APP_TYPE_CM: &str = "main"; + +// Do not remove the following constants. +// Uncomment them when they are used. +// pub(crate) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; +// pub(crate) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; +// pub(crate) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward"; + +pub type FlutterSession = Arc>; + +lazy_static::lazy_static! { + pub(crate) static ref CUR_SESSION_ID: RwLock = Default::default(); // For desktop only + static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel +} + +#[cfg(target_os = "windows")] +lazy_static::lazy_static! { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = load_plugin_in_app_path("texture_rgba_renderer_plugin.dll"); +} + +#[cfg(target_os = "linux")] +lazy_static::lazy_static! { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open("libtexture_rgba_renderer_plugin.so"); +} + +#[cfg(target_os = "macos")] +lazy_static::lazy_static! { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open_self(); +} + +#[cfg(target_os = "windows")] +lazy_static::lazy_static! { + pub static ref TEXTURE_GPU_RENDERER_PLUGIN: Result = load_plugin_in_app_path("flutter_gpu_texture_renderer_plugin.dll"); +} + +// Move this function into `src/platform/windows.rs` if there're more calls to load plugins. +// Load dll with full path. +#[cfg(target_os = "windows")] +fn load_plugin_in_app_path(dll_name: &str) -> Result { + match std::env::current_exe() { + Ok(exe_file) => { + if let Some(cur_dir) = exe_file.parent() { + let full_path = cur_dir.join(dll_name); + if !full_path.exists() { + Err(LibError::OpeningLibraryError(IoError::new( + IoErrorKind::NotFound, + format!("{} not found", dll_name), + ))) + } else { + Library::open(full_path) + } + } else { + Err(LibError::OpeningLibraryError(IoError::new( + IoErrorKind::Other, + format!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ), + ))) + } + } + Err(e) => Err(LibError::OpeningLibraryError(e)), + } +} + +/// FFI for rustdesk core's main entry. +/// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. +#[cfg(not(windows))] +#[no_mangle] +pub extern "C" fn rustdesk_core_main() -> bool { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if crate::core_main::core_main().is_some() { + return true; + } else { + #[cfg(target_os = "macos")] + std::process::exit(0); + } + #[cfg(not(target_os = "macos"))] + false +} + +#[cfg(target_os = "macos")] +#[no_mangle] +pub extern "C" fn handle_applicationShouldOpenUntitledFile() { + crate::platform::macos::handle_application_should_open_untitled_file(); +} + +#[cfg(windows)] +#[no_mangle] +pub extern "C" fn rustdesk_core_main_args(args_len: *mut c_int) -> *mut *mut c_char { + unsafe { std::ptr::write(args_len, 0) }; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if let Some(args) = crate::core_main::core_main() { + return rust_args_to_c_args(args, args_len); + } + return std::ptr::null_mut() as _; + } + #[cfg(any(target_os = "android", target_os = "ios"))] + return std::ptr::null_mut() as _; +} + +// https://gist.github.com/iskakaushik/1c5b8aa75c77479c33c4320913eebef6 +#[cfg(windows)] +fn rust_args_to_c_args(args: Vec, outlen: *mut c_int) -> *mut *mut c_char { + let mut v = vec![]; + + // Let's fill a vector with null-terminated strings + for s in args { + match CString::new(s) { + Ok(s) => v.push(s), + Err(_) => return std::ptr::null_mut() as _, + } + } + + // Turning each null-terminated string into a pointer. + // `into_raw` takes ownershop, gives us the pointer and does NOT drop the data. + let mut out = v.into_iter().map(|s| s.into_raw()).collect::>(); + + // Make sure we're not wasting space. + out.shrink_to_fit(); + debug_assert!(out.len() == out.capacity()); + + // Get the pointer to our vector. + let len = out.len(); + let ptr = out.as_mut_ptr(); + std::mem::forget(out); + + // Let's write back the length the caller can expect + unsafe { std::ptr::write(outlen, len as c_int) }; + + // Finally return the data + ptr +} + +#[no_mangle] +pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { + let len = len as usize; + + // Get back our vector. + // Previously we shrank to fit, so capacity == length. + let v = Vec::from_raw_parts(ptr, len, len); + + // Now drop one string at a time. + for elem in v { + let s = CString::from_raw(elem); + std::mem::drop(s); + } + + // Afterwards the vector will be dropped and thus freed. +} + +#[cfg(windows)] +#[no_mangle] +pub unsafe extern "C" fn get_rustdesk_app_name(buffer: *mut u16, length: i32) -> i32 { + let name = crate::platform::wide_string(&crate::get_app_name()); + if length > name.len() as i32 { + std::ptr::copy_nonoverlapping(name.as_ptr(), buffer, name.len()); + return 0; + } + -1 +} + +#[derive(Default)] +struct SessionHandler { + event_stream: Option>, + // displays of current session. + // We need this variable to check if the display is in use before pushing rgba to flutter. + displays: Vec, + renderer: VideoRenderer, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum RenderType { + PixelBuffer, + #[cfg(feature = "vram")] + Texture, +} + +#[derive(Clone)] +pub struct FlutterHandler { + // ui session id -> display handler data + session_handlers: Arc>>, + display_rgbas: Arc>>, + peer_info: Arc>, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + hooks: Arc>>, + use_texture_render: Arc, +} + +impl Default for FlutterHandler { + fn default() -> Self { + Self { + session_handlers: Default::default(), + display_rgbas: Default::default(), + peer_info: Default::default(), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + hooks: Default::default(), + use_texture_render: Arc::new( + AtomicBool::new(crate::ui_interface::use_texture_render()), + ), + } + } +} + +#[derive(Default, Clone)] +struct RgbaData { + // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. + // We must check the `rgba_valid` before reading [rgba]. + data: Vec, + valid: bool, +} + +pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn( + texture_rgba: *mut c_void, + buffer: *const u8, + len: c_int, + width: c_int, + height: c_int, + dst_rgba_stride: c_int, +); + +#[cfg(feature = "vram")] +pub type FlutterGpuTextureRendererPluginCApiSetTexture = + unsafe extern "C" fn(output: *mut c_void, texture: *mut c_void); + +#[cfg(feature = "vram")] +pub type FlutterGpuTextureRendererPluginCApiGetAdapterLuid = unsafe extern "C" fn() -> i64; + +pub(super) type TextureRgbaPtr = usize; + +struct DisplaySessionInfo { + // TextureRgba pointer in flutter native. + texture_rgba_ptr: TextureRgbaPtr, + size: (usize, usize), + #[cfg(feature = "vram")] + gpu_output_ptr: usize, + notify_render_type: Option, +} + +// Video Texture Renderer in Flutter +#[derive(Clone)] +struct VideoRenderer { + is_support_multi_ui_session: bool, + map_display_sessions: Arc>>, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + on_rgba_func: Option>, + #[cfg(feature = "vram")] + on_texture_func: Option>, +} + +impl Default for VideoRenderer { + fn default() -> Self { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let on_rgba_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN { + Ok(lib) => { + let find_sym_res = unsafe { + lib.symbol::("FlutterRgbaRendererPluginOnRgba") + }; + match find_sym_res { + Ok(sym) => Some(sym), + Err(e) => { + log::error!("Failed to find symbol FlutterRgbaRendererPluginOnRgba, {e}"); + None + } + } + } + Err(e) => { + log::error!("Failed to load texture rgba renderer plugin, {e}"); + None + } + }; + #[cfg(feature = "vram")] + let on_texture_func = match &*TEXTURE_GPU_RENDERER_PLUGIN { + Ok(lib) => { + let find_sym_res = unsafe { + lib.symbol::( + "FlutterGpuTextureRendererPluginCApiSetTexture", + ) + }; + match find_sym_res { + Ok(sym) => Some(sym), + Err(e) => { + log::error!("Failed to find symbol FlutterGpuTextureRendererPluginCApiSetTexture, {e}"); + None + } + } + } + Err(e) => { + log::error!("Failed to load texture gpu renderer plugin, {e}"); + None + } + }; + + Self { + map_display_sessions: Default::default(), + is_support_multi_ui_session: false, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + on_rgba_func, + #[cfg(feature = "vram")] + on_texture_func, + } + } +} + +impl VideoRenderer { + #[inline] + fn set_size(&mut self, display: usize, width: usize, height: usize) { + let mut sessions_lock = self.map_display_sessions.write().unwrap(); + if let Some(info) = sessions_lock.get_mut(&display) { + info.size = (width, height); + info.notify_render_type = None; + } else { + sessions_lock.insert( + display, + DisplaySessionInfo { + texture_rgba_ptr: usize::default(), + size: (width, height), + #[cfg(feature = "vram")] + gpu_output_ptr: usize::default(), + notify_render_type: None, + }, + ); + } + } + + fn register_pixelbuffer_texture(&self, display: usize, ptr: usize) { + let mut sessions_lock = self.map_display_sessions.write().unwrap(); + if ptr == 0 { + if let Some(info) = sessions_lock.get_mut(&display) { + if info.texture_rgba_ptr != usize::default() { + info.texture_rgba_ptr = usize::default(); + } + #[cfg(feature = "vram")] + if info.gpu_output_ptr != usize::default() { + return; + } + } + sessions_lock.remove(&display); + } else { + if let Some(info) = sessions_lock.get_mut(&display) { + if info.texture_rgba_ptr != usize::default() + && info.texture_rgba_ptr != ptr as TextureRgbaPtr + { + log::warn!( + "texture_rgba_ptr is not null and not equal to ptr, replace {} to {}", + info.texture_rgba_ptr, + ptr + ); + } + info.texture_rgba_ptr = ptr as _; + info.notify_render_type = None; + } else { + if ptr != 0 { + sessions_lock.insert( + display, + DisplaySessionInfo { + texture_rgba_ptr: ptr as _, + size: (0, 0), + #[cfg(feature = "vram")] + gpu_output_ptr: usize::default(), + notify_render_type: None, + }, + ); + } + } + } + } + + #[cfg(feature = "vram")] + pub fn register_gpu_output(&self, display: usize, ptr: usize) { + let mut sessions_lock = self.map_display_sessions.write().unwrap(); + if ptr == 0 { + if let Some(info) = sessions_lock.get_mut(&display) { + if info.gpu_output_ptr != usize::default() { + info.gpu_output_ptr = usize::default(); + } + if info.texture_rgba_ptr != usize::default() { + return; + } + } + sessions_lock.remove(&display); + } else { + if let Some(info) = sessions_lock.get_mut(&display) { + if info.gpu_output_ptr != usize::default() && info.gpu_output_ptr != ptr { + log::error!( + "gpu_output_ptr is not null and not equal to ptr, relace {} to {}", + info.gpu_output_ptr, + ptr + ); + } + info.gpu_output_ptr = ptr as _; + info.notify_render_type = None; + } else { + if ptr != usize::default() { + sessions_lock.insert( + display, + DisplaySessionInfo { + texture_rgba_ptr: usize::default(), + size: (0, 0), + gpu_output_ptr: ptr, + notify_render_type: None, + }, + ); + } + } + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) -> bool { + let mut write_lock = self.map_display_sessions.write().unwrap(); + let opt_info = if !self.is_support_multi_ui_session { + write_lock.values_mut().next() + } else { + write_lock.get_mut(&display) + }; + let Some(info) = opt_info else { + return false; + }; + if info.texture_rgba_ptr == usize::default() { + return false; + } + + if info.size.0 != rgba.w || info.size.1 != rgba.h { + log::error!( + "width/height mismatch: ({},{}) != ({},{})", + info.size.0, + info.size.1, + rgba.w, + rgba.h + ); + // Peer info's handling is async and may be late than video frame's handling + // Allow peer info not set, but not allow wrong width/height for correct local cursor position + if info.size != (0, 0) { + return false; + } + } + if let Some(func) = &self.on_rgba_func { + unsafe { + func( + info.texture_rgba_ptr as _, + rgba.raw.as_ptr() as _, + rgba.raw.len() as _, + rgba.w as _, + rgba.h as _, + rgba.align() as _, + ) + }; + } + if info.notify_render_type != Some(RenderType::PixelBuffer) { + info.notify_render_type = Some(RenderType::PixelBuffer); + true + } else { + false + } + } + + #[cfg(feature = "vram")] + pub fn on_texture(&self, display: usize, texture: *mut c_void) -> bool { + let mut write_lock = self.map_display_sessions.write().unwrap(); + let opt_info = if !self.is_support_multi_ui_session { + write_lock.values_mut().next() + } else { + write_lock.get_mut(&display) + }; + let Some(info) = opt_info else { + return false; + }; + if info.gpu_output_ptr == usize::default() { + return false; + } + if let Some(func) = &self.on_texture_func { + unsafe { func(info.gpu_output_ptr as _, texture) }; + } + if info.notify_render_type != Some(RenderType::Texture) { + info.notify_render_type = Some(RenderType::Texture); + true + } else { + false + } + } + + pub fn reset_all_display_render_type(&self) { + let mut write_lock = self.map_display_sessions.write().unwrap(); + write_lock + .values_mut() + .map(|v| v.notify_render_type = None) + .count(); + } +} + +impl SessionHandler { + pub fn on_waiting_for_image_dialog_show(&self) { + self.renderer.reset_all_display_render_type(); + // rgba array render will notify every frame + } +} + +impl FlutterHandler { + /// Push an event to all the event queues. + /// An event is stored as json in the event queues. + /// + /// # Arguments + /// + /// * `name` - The name of the event. + /// * `event` - Fields of the event content. + pub fn push_event(&self, name: &str, event: &[(&str, V)], excludes: &[&SessionID]) + where + V: Sized + Serialize + Clone, + { + self.push_event_(name, event, &[], excludes); + } + + pub fn push_event_to(&self, name: &str, event: &[(&str, V)], include: &[&SessionID]) + where + V: Sized + Serialize + Clone, + { + self.push_event_(name, event, include, &[]); + } + + pub fn push_event_( + &self, + name: &str, + event: &[(&str, V)], + includes: &[&SessionID], + excludes: &[&SessionID], + ) where + V: Sized + Serialize + Clone, + { + let mut h: HashMap<&str, serde_json::Value> = + event.iter().map(|(k, v)| (*k, json!(*v))).collect(); + debug_assert!(h.get("name").is_none()); + h.insert("name", json!(name)); + let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); + for (sid, session) in self.session_handlers.read().unwrap().iter() { + let mut push = false; + if includes.is_empty() { + if !excludes.contains(&sid) { + push = true; + } + } else { + if includes.contains(&sid) { + push = true; + } + } + if push { + if let Some(stream) = &session.event_stream { + stream.add(EventToUI::Event(out.clone())); + } + } + } + } + + pub(crate) fn close_event_stream(&self, session_id: SessionID) { + // to-do: Make sure the following logic is correct. + // No need to remove the display handler, because it will be removed when the connection is closed. + if let Some(session) = self.session_handlers.write().unwrap().get_mut(&session_id) { + try_send_close_event(&session.event_stream); + } + } + + fn make_displays_msg(displays: &Vec) -> String { + let mut msg_vec = Vec::new(); + for ref d in displays.iter() { + let mut h: HashMap<&str, i32> = Default::default(); + h.insert("x", d.x); + h.insert("y", d.y); + h.insert("width", d.width); + h.insert("height", d.height); + h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); + if let Some(original_resolution) = d.original_resolution.as_ref() { + h.insert("original_width", original_resolution.width); + h.insert("original_height", original_resolution.height); + } + // Don't convert scale (x 100) to i32 directly. + // (d.scale * 100.0f64) as i32 may produces inaccuracies. + // + // Example: GNOME Wayland with Fractional Scaling enabled: + // - Physical resolution: 2560x1600 + // - Logical resolution: 1074x1065 + // - Scale factor: 150% + // Passing physical dimensions and scale factor prevents accurate logical resolution calculation + // since 2560/1.5 = 1706.666... (rounded to 1706.67) and 1600/1.5 = 1066.666... (rounded to 1066.67) + // h.insert("scale", (d.scale * 100.0f64) as i32); + + // Send scaled_width for accurate logical scale calculation. + if d.scale > 0.0 { + let scaled_width = (d.width as f64 / d.scale).round() as i32; + h.insert("scaled_width", scaled_width); + } + msg_vec.push(h); + } + serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) + } + + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub(crate) fn add_session_hook(&self, key: String, hook: SessionHook) -> bool { + let mut hooks = self.hooks.write().unwrap(); + if hooks.contains_key(&key) { + // Already has the hook with this key. + return false; + } + let _ = hooks.insert(key, hook); + true + } + + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub(crate) fn remove_session_hook(&self, key: &String) -> bool { + let mut hooks = self.hooks.write().unwrap(); + if !hooks.contains_key(key) { + // The hook with this key does not found. + return false; + } + let _ = hooks.remove(key); + true + } + + pub fn update_use_texture_render(&self) { + self.use_texture_render + .store(crate::ui_interface::use_texture_render(), Ordering::Relaxed); + self.display_rgbas.write().unwrap().clear(); + } +} + +impl InvokeUiSession for FlutterHandler { + fn set_cursor_data(&self, cd: CursorData) { + let colors = hbb_common::compress::decompress(&cd.colors); + self.push_event( + "cursor_data", + &[ + ("id", &cd.id.to_string()), + ("hotx", &cd.hotx.to_string()), + ("hoty", &cd.hoty.to_string()), + ("width", &cd.width.to_string()), + ("height", &cd.height.to_string()), + ( + "colors", + &serde_json::ser::to_string(&colors).unwrap_or("".to_owned()), + ), + ], + &[], + ); + } + + fn set_cursor_id(&self, id: String) { + self.push_event("cursor_id", &[("id", &id.to_string())], &[]); + } + + fn set_cursor_position(&self, cp: CursorPosition) { + self.push_event( + "cursor_position", + &[("x", &cp.x.to_string()), ("y", &cp.y.to_string())], + &[], + ); + } + + /// unused in flutter, use switch_display or set_peer_info + fn set_display(&self, _x: i32, _y: i32, _w: i32, _h: i32, _cursor_embedded: bool, _scale: f64) {} + + fn update_privacy_mode(&self) { + self.push_event::<&str>("update_privacy_mode", &[], &[]); + } + + fn set_permission(&self, name: &str, value: bool) { + self.push_event("permission", &[(name, &value.to_string())], &[]); + } + + // unused in flutter + fn close_success(&self) {} + + fn update_quality_status(&self, status: QualityStatus) { + const NULL: String = String::new(); + self.push_event( + "update_quality_status", + &[ + ("speed", &status.speed.map_or(NULL, |it| it)), + ( + "fps", + &serde_json::ser::to_string(&status.fps).unwrap_or(NULL.to_owned()), + ), + ("delay", &status.delay.map_or(NULL, |it| it.to_string())), + ( + "target_bitrate", + &status.target_bitrate.map_or(NULL, |it| it.to_string()), + ), + ( + "codec_format", + &status.codec_format.map_or(NULL, |it| it.to_string()), + ), + ("chroma", &status.chroma.map_or(NULL, |it| it.to_string())), + ], + &[], + ); + } + + fn set_connection_type(&self, is_secured: bool, direct: bool, stream_type: &str) { + self.push_event( + "connection_ready", + &[ + ("secure", &is_secured.to_string()), + ("direct", &direct.to_string()), + ("stream_type", &stream_type.to_string()), + ], + &[], + ); + } + + fn set_fingerprint(&self, fingerprint: String) { + self.push_event("fingerprint", &[("fingerprint", &fingerprint)], &[]); + } + + fn job_error(&self, id: i32, err: String, file_num: i32) { + self.push_event( + "job_error", + &[ + ("id", &id.to_string()), + ("err", &err), + ("file_num", &file_num.to_string()), + ], + &[], + ); + } + + fn job_done(&self, id: i32, file_num: i32) { + self.push_event( + "job_done", + &[("id", &id.to_string()), ("file_num", &file_num.to_string())], + &[], + ); + } + + // unused in flutter + fn clear_all_jobs(&self) {} + + fn load_last_job(&self, _cnt: i32, job_json: &str, _auto_start: bool) { + self.push_event("load_last_job", &[("value", job_json)], &[]); + } + + fn update_folder_files( + &self, + id: i32, + entries: &Vec, + path: String, + #[allow(unused_variables)] is_local: bool, + only_count: bool, + ) { + // TODO opt + if only_count { + self.push_event( + "update_folder_files", + &[("info", &make_fd_flutter(id, entries, only_count))], + &[], + ); + } else { + self.push_event( + "file_dir", + &[ + ("is_local", "false"), + ("value", &crate::common::make_fd_to_json(id, path, entries)), + ], + &[], + ); + } + } + + fn update_empty_dirs(&self, res: ReadEmptyDirsResponse) { + self.push_event( + "empty_dirs", + &[ + ("is_local", "false"), + ( + "value", + &crate::common::make_empty_dirs_response_to_json(&res), + ), + ], + &[], + ); + } + + // unused in flutter + fn update_transfer_list(&self) {} + + // unused in flutter // TEST flutter + fn confirm_delete_files(&self, _id: i32, _i: i32, _name: String) {} + + fn override_file_confirm( + &self, + id: i32, + file_num: i32, + to: String, + is_upload: bool, + is_identical: bool, + ) { + self.push_event( + "override_file_confirm", + &[ + ("id", &id.to_string()), + ("file_num", &file_num.to_string()), + ("read_path", &to), + ("is_upload", &is_upload.to_string()), + ("is_identical", &is_identical.to_string()), + ], + &[], + ); + } + + fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64) { + self.push_event( + "job_progress", + &[ + ("id", &id.to_string()), + ("file_num", &file_num.to_string()), + ("speed", &speed.to_string()), + ("finished_size", &finished_size.to_string()), + ], + &[], + ); + } + + // unused in flutter + fn adapt_size(&self) {} + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb) { + let use_texture_render = self.use_texture_render.load(Ordering::Relaxed); + self.on_rgba_flutter_texture_render(use_texture_render, display, rgba); + if !use_texture_render { + self.on_rgba_soft_render(display, rgba); + } + } + + #[inline] + #[cfg(any(target_os = "android", target_os = "ios"))] + fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb) { + self.on_rgba_soft_render(display, rgba); + } + + #[inline] + #[cfg(feature = "vram")] + fn on_texture(&self, display: usize, texture: *mut c_void) { + if !self.use_texture_render.load(Ordering::Relaxed) { + return; + } + for (_, session) in self.session_handlers.read().unwrap().iter() { + if session.renderer.on_texture(display, texture) { + if let Some(stream) = &session.event_stream { + stream.add(EventToUI::Texture(display, true)); + } + } + } + } + + fn set_peer_info(&self, pi: &PeerInfo) { + let displays = Self::make_displays_msg(&pi.displays); + let mut features: HashMap<&str, bool> = Default::default(); + for ref f in pi.features.iter() { + features.insert("privacy_mode", f.privacy_mode); + } + // compatible with 1.1.9 + if get_version_number(&pi.version) < get_version_number("1.2.0") { + features.insert("privacy_mode", false); + } + let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); + let resolutions = serialize_resolutions(&pi.resolutions.resolutions); + *self.peer_info.write().unwrap() = pi.clone(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let is_support_multi_ui_session = crate::common::is_support_multi_ui_session(&pi.version); + #[cfg(any(target_os = "android", target_os = "ios"))] + let is_support_multi_ui_session = false; + self.session_handlers + .write() + .unwrap() + .values_mut() + .for_each(|h| { + h.renderer.is_support_multi_ui_session = is_support_multi_ui_session; + }); + self.push_event( + "peer_info", + &[ + ("username", &pi.username), + ("hostname", &pi.hostname), + ("platform", &pi.platform), + ("sas_enabled", &pi.sas_enabled.to_string()), + ("displays", &displays), + ("version", &pi.version), + ("features", &features), + ("current_display", &pi.current_display.to_string()), + ("resolutions", &resolutions), + ("platform_additions", &pi.platform_additions), + ], + &[], + ); + } + + fn set_displays(&self, displays: &Vec) { + self.peer_info.write().unwrap().displays = displays.clone(); + self.push_event( + "sync_peer_info", + &[("displays", &Self::make_displays_msg(displays))], + &[], + ); + } + + fn set_platform_additions(&self, data: &str) { + self.push_event( + "sync_platform_additions", + &[("platform_additions", &data)], + &[], + ) + } + + fn set_multiple_windows_session(&self, sessions: Vec) { + let mut msg_vec = Vec::new(); + let mut sessions = sessions; + for d in sessions.drain(..) { + let mut h: HashMap<&str, String> = Default::default(); + h.insert("sid", d.sid.to_string()); + h.insert("name", d.name); + msg_vec.push(h); + } + self.push_event( + "set_multiple_windows_session", + &[( + "windows_sessions", + &serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()), + )], + &[], + ); + } + + fn is_multi_ui_session(&self) -> bool { + self.session_handlers.read().unwrap().len() > 1 + } + + fn set_current_display(&self, disp_idx: i32) { + if self.is_multi_ui_session() { + return; + } + self.push_event( + "follow_current_display", + &[("display_idx", &disp_idx.to_string())], + &[], + ); + } + + fn on_connected(&self, _conn_type: ConnType) {} + + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { + let has_retry = if retry { "true" } else { "" }; + self.push_event( + "msgbox", + &[ + ("type", msgtype), + ("title", title), + ("text", text), + ("link", link), + ("hasRetry", has_retry), + ], + &[], + ); + } + + fn cancel_msgbox(&self, tag: &str) { + self.push_event("cancel_msgbox", &[("tag", tag)], &[]); + } + + fn new_message(&self, msg: String) { + self.push_event("chat_client_mode", &[("text", &msg)], &[]); + } + + fn switch_display(&self, display: &SwitchDisplay) { + let resolutions = serialize_resolutions(&display.resolutions.resolutions); + self.push_event( + "switch_display", + &[ + ("display", &display.display.to_string()), + ("x", &display.x.to_string()), + ("y", &display.y.to_string()), + ("width", &display.width.to_string()), + ("height", &display.height.to_string()), + ( + "cursor_embedded", + &{ + if display.cursor_embedded { + 1 + } else { + 0 + } + } + .to_string(), + ), + ("resolutions", &resolutions), + ( + "original_width", + &display.original_resolution.width.to_string(), + ), + ( + "original_height", + &display.original_resolution.height.to_string(), + ), + ], + &[], + ); + } + + fn update_block_input_state(&self, on: bool) { + self.push_event( + "update_block_input_state", + &[("input_state", if on { "on" } else { "off" })], + &[], + ); + } + + #[cfg(any(target_os = "android", target_os = "ios"))] + fn clipboard(&self, content: String) { + self.push_event("clipboard", &[("content", &content)], &[]); + } + + fn switch_back(&self, peer_id: &str) { + self.push_event("switch_back", &[("peer_id", peer_id)], &[]); + } + + fn portable_service_running(&self, running: bool) { + self.push_event( + "portable_service_running", + &[("running", running.to_string().as_str())], + &[], + ); + } + + fn on_voice_call_started(&self) { + self.push_event::<&str>("on_voice_call_started", &[], &[]); + } + + fn on_voice_call_closed(&self, reason: &str) { + let _res = self.push_event("on_voice_call_closed", &[("reason", reason)], &[]); + } + + fn on_voice_call_waiting(&self) { + self.push_event::<&str>("on_voice_call_waiting", &[], &[]); + } + + fn on_voice_call_incoming(&self) { + self.push_event::<&str>("on_voice_call_incoming", &[], &[]); + } + + #[inline] + fn get_rgba(&self, _display: usize) -> *const u8 { + if let Some(rgba_data) = self.display_rgbas.read().unwrap().get(&_display) { + if rgba_data.valid { + return rgba_data.data.as_ptr(); + } + } + std::ptr::null_mut() + } + + #[inline] + fn next_rgba(&self, _display: usize) { + if let Some(rgba_data) = self.display_rgbas.write().unwrap().get_mut(&_display) { + rgba_data.valid = false; + } + } + + fn update_record_status(&self, start: bool) { + self.push_event("record_status", &[("start", &start.to_string())], &[]); + } + + fn printer_request(&self, id: i32, path: String) { + self.push_event( + "printer_request", + &[("id", json!(id)), ("path", json!(path))], + &[], + ); + } + + fn handle_screenshot_resp(&self, sid: String, msg: String) { + match SessionID::from_str(&sid) { + Ok(sid) => self.push_event_to("screenshot", &[("msg", json!(msg))], &[&sid]), + Err(e) => { + // Unreachable! + log::error!("Failed to parse sid \"{}\", {}", sid, e); + } + } + } + + fn handle_terminal_response(&self, response: TerminalResponse) { + use hbb_common::message_proto::terminal_response::Union; + + match response.union { + Some(Union::Opened(opened)) => { + let mut event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("opened")), + ("terminal_id", json!(opened.terminal_id)), + ("success", json!(opened.success)), + ("message", json!(&opened.message)), + ("pid", json!(opened.pid)), + ("service_id", json!(&opened.service_id)), + ]; + if !opened.persistent_sessions.is_empty() { + event_data.push(("persistent_sessions", json!(opened.persistent_sessions))); + } + self.push_event_("terminal_response", &event_data, &[], &[]); + } + Some(Union::Data(data)) => { + // Decompress data if needed + let output_data = if data.compressed { + hbb_common::compress::decompress(&data.data) + } else { + data.data.to_vec() + }; + + let encoded = crate::encode64(&output_data); + let event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("data")), + ("terminal_id", json!(data.terminal_id)), + ("data", json!(&encoded)), + ]; + self.push_event_("terminal_response", &event_data, &[], &[]); + } + Some(Union::Closed(closed)) => { + let event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("closed")), + ("terminal_id", json!(closed.terminal_id)), + ("exit_code", json!(closed.exit_code)), + ]; + self.push_event_("terminal_response", &event_data, &[], &[]); + } + Some(Union::Error(error)) => { + let event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("error")), + ("terminal_id", json!(error.terminal_id)), + ("message", json!(&error.message)), + ]; + self.push_event_("terminal_response", &event_data, &[], &[]); + } + None => {} + Some(_) => { + log::warn!("Unhandled terminal response type"); + } + } + } +} + +impl FlutterHandler { + #[inline] + fn on_rgba_soft_render(&self, display: usize, rgba: &mut scrap::ImageRgb) { + // Give a chance for plugins or etc to hook a rgba data. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + for (key, hook) in self.hooks.read().unwrap().iter() { + match hook { + SessionHook::OnSessionRgba(cb) => { + cb(key.to_owned(), rgba); + } + } + } + // If the current rgba is not fetched by flutter, i.e., is valid. + // We give up sending a new event to flutter. + let mut rgba_write_lock = self.display_rgbas.write().unwrap(); + if let Some(rgba_data) = rgba_write_lock.get_mut(&display) { + if rgba_data.valid { + return; + } else { + rgba_data.valid = true; + } + // Return the rgba buffer to the video handler for reusing allocated rgba buffer. + std::mem::swap::>(&mut rgba.raw, &mut rgba_data.data); + } else { + let mut rgba_data = RgbaData::default(); + std::mem::swap::>(&mut rgba.raw, &mut rgba_data.data); + rgba_data.valid = true; + rgba_write_lock.insert(display, rgba_data); + } + drop(rgba_write_lock); + + let mut is_sent = false; + let is_multi_sessions = self.is_multi_ui_session(); + for h in self.session_handlers.read().unwrap().values() { + // The soft renderer does not support multi-displays session for now. + if h.displays.len() > 1 { + continue; + } + // If there're multiple ui sessions, we only notify the ui session that has the display. + if is_multi_sessions { + if !h.displays.contains(&display) { + continue; + } + } + if let Some(stream) = &h.event_stream { + stream.add(EventToUI::Rgba(display)); + is_sent = true; + } + } + // We need `is_sent` here. Because we use texture render for multi-displays session. + // + // Eg. We have two windows, one is display 1, the other is displays 0&1. + // When image of display 0 is received, we will not send the event. + // + // 1. "display 1" will not send the event. + // 2. "displays 0&1" will not send the event. Because it uses texutre render for now. + if !is_sent { + if let Some(rgba_data) = self.display_rgbas.write().unwrap().get_mut(&display) { + rgba_data.valid = false; + } + } + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn on_rgba_flutter_texture_render( + &self, + use_texture_render: bool, + display: usize, + rgba: &mut scrap::ImageRgb, + ) { + for (_, session) in self.session_handlers.read().unwrap().iter() { + if use_texture_render || session.displays.len() > 1 { + if session.renderer.on_rgba(display, rgba) { + if let Some(stream) = &session.event_stream { + stream.add(EventToUI::Texture(display, false)); + } + } + } + } + } +} + +// This function is only used for the default connection session. +pub fn session_add_existed( + peer_id: String, + session_id: SessionID, + displays: Vec, + is_view_camera: bool, +) -> ResultType<()> { + let conn_type = if is_view_camera { + ConnType::VIEW_CAMERA + } else { + ConnType::DEFAULT_CONN + }; + sessions::insert_peer_session_id(peer_id, conn_type, session_id, displays); + Ok(()) +} + +/// Create a new remote session with the given id. +/// +/// # Arguments +/// +/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ +/// * `is_file_transfer` - If the session is used for file transfer. +/// * `is_view_camera` - If the session is used for view camera. +/// * `is_port_forward` - If the session is used for port forward. +pub fn session_add( + session_id: &SessionID, + id: &str, + is_file_transfer: bool, + is_view_camera: bool, + is_port_forward: bool, + is_rdp: bool, + is_terminal: bool, + switch_uuid: &str, + force_relay: bool, + password: String, + is_shared_password: bool, + conn_token: Option, +) -> ResultType { + let conn_type = if is_file_transfer { + ConnType::FILE_TRANSFER + } else if is_view_camera { + ConnType::VIEW_CAMERA + } else if is_terminal { + ConnType::TERMINAL + } else if is_port_forward { + if is_rdp { + ConnType::RDP + } else { + ConnType::PORT_FORWARD + } + } else { + ConnType::DEFAULT_CONN + }; + + // to-do: check the same id session. + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + if session.lc.read().unwrap().conn_type != conn_type { + bail!("same session id is found with different conn type?"); + } + // The same session is added before? + bail!("same session id is found"); + } + + LocalConfig::set_remote_id(&id); + + let mut preset_password = password.clone(); + let shared_password = if is_shared_password { + // To achieve a flexible password application order, we don't treat shared password as a preset password. + preset_password = Default::default(); + Some(password) + } else { + None + }; + + let session: Session = Session { + password: preset_password, + server_keyboard_enabled: Arc::new(RwLock::new(true)), + server_file_transfer_enabled: Arc::new(RwLock::new(true)), + server_clipboard_enabled: Arc::new(RwLock::new(true)), + reconnect_count: Arc::new(AtomicUsize::new(0)), + ..Default::default() + }; + + let switch_uuid = if switch_uuid.is_empty() { + None + } else { + Some(switch_uuid.to_string()) + }; + + session.lc.write().unwrap().initialize( + id.to_owned(), + conn_type, + switch_uuid, + force_relay, + get_adapter_luid(), + shared_password, + conn_token, + ); + + let session = Arc::new(session.clone()); + sessions::insert_session(session_id.to_owned(), conn_type, session.clone()); + + Ok(session) +} + +/// start a session with the given id. +/// +/// # Arguments +/// +/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ +/// * `events2ui` - The events channel to ui. +pub fn session_start_( + session_id: &SessionID, + id: &str, + event_stream: StreamSink, +) -> ResultType<()> { + // is_connected is used to indicate whether to start a peer connection. For two cases: + // 1. "Move tab to new window" + // 2. multi ui session within the same peer connection. + let mut is_connected = false; + let mut is_found = false; + for s in sessions::get_sessions() { + if let Some(h) = s.session_handlers.write().unwrap().get_mut(session_id) { + is_connected = h.event_stream.is_some(); + try_send_close_event(&h.event_stream); + h.event_stream = Some(event_stream); + is_found = true; + break; + } + } + if !is_found { + bail!( + "No session with peer id {}, session id: {}", + id, + session_id.to_string() + ); + } + + if let Some(session) = sessions::get_session_by_session_id(session_id) { + let is_first_ui_session = session.session_handlers.read().unwrap().len() == 1; + if !is_connected && is_first_ui_session { + log::info!( + "Session {} start, use texture render: {}", + id, + session.use_texture_render.load(Ordering::Relaxed) + ); + let session = (*session).clone(); + std::thread::spawn(move || { + let round = session.connection_round_state.lock().unwrap().new_round(); + io_loop(session, round); + }); + } + Ok(()) + } else { + bail!("No session with peer id {}", id) + } +} + +#[inline] +fn try_send_close_event(event_stream: &Option>) { + if let Some(stream) = &event_stream { + stream.add(EventToUI::Event("close".to_owned())); + } +} + +#[cfg(not(target_os = "ios"))] +pub fn update_text_clipboard_required() { + let is_required = sessions::get_sessions() + .iter() + .any(|s| s.is_text_clipboard_required()); + #[cfg(target_os = "android")] + let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required); + Client::set_is_text_clipboard_required(is_required); +} + +#[cfg(feature = "unix-file-copy-paste")] +pub fn update_file_clipboard_required() { + let is_required = sessions::get_sessions() + .iter() + .any(|s| s.is_file_clipboard_required()); + Client::set_is_file_clipboard_required(is_required); +} + +#[cfg(not(target_os = "ios"))] +pub fn send_clipboard_msg(msg: Message, _is_file: bool) { + for s in sessions::get_sessions() { + #[cfg(feature = "unix-file-copy-paste")] + if _is_file { + if crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version) + && s.is_file_clipboard_required() + { + s.send(Data::Message(msg.clone())); + } + continue; + } + if s.is_text_clipboard_required() { + // Check if the client supports multi clipboards + if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { + let version = s.ui_handler.peer_info.read().unwrap().version.clone(); + let platform = s.ui_handler.peer_info.read().unwrap().platform.clone(); + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip( + &version, + &platform, + multi_clipboards, + ) { + s.send(Data::Message(msg_out)); + continue; + } + } + s.send(Data::Message(msg.clone())); + } + } +} + +// Server Side +#[cfg(not(any(target_os = "ios")))] +pub mod connection_manager { + use std::collections::HashMap; + + #[cfg(any(target_os = "android"))] + use hbb_common::log; + #[cfg(any(target_os = "android"))] + use scrap::android::call_main_service_set_by_name; + use serde_json::json; + + use crate::ui_cm_interface::InvokeUiCM; + + use super::GLOBAL_EVENT_STREAM; + + #[derive(Clone)] + struct FlutterHandler {} + + impl InvokeUiCM for FlutterHandler { + //TODO port_forward + fn add_connection(&self, client: &crate::ui_cm_interface::Client) { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + // send to Android service, active notification no matter UI is shown or not. + #[cfg(target_os = "android")] + if let Err(e) = + call_main_service_set_by_name("add_connection", Some(&client_json), None) + { + log::debug!("call_main_service_set_by_name fail,{}", e); + } + // send to UI, refresh widget + self.push_event("add_connection", &[("client", &client_json)]); + } + + fn remove_connection(&self, id: i32, close: bool) { + self.push_event( + "on_client_remove", + &[("id", &id.to_string()), ("close", &close.to_string())], + ); + } + + fn new_message(&self, id: i32, text: String) { + self.push_event( + "chat_server_mode", + &[("id", &id.to_string()), ("text", &text)], + ); + } + + fn change_theme(&self, dark: String) { + self.push_event("theme", &[("dark", &dark)]); + } + + fn change_language(&self) { + self.push_event::<&str>("language", &[]); + } + + fn show_elevation(&self, show: bool) { + self.push_event("show_elevation", &[("show", &show.to_string())]); + } + + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + // send to Android service, active notification no matter UI is shown or not. + #[cfg(target_os = "android")] + if let Err(e) = + call_main_service_set_by_name("update_voice_call_state", Some(&client_json), None) + { + log::debug!("call_main_service_set_by_name fail,{}", e); + } + self.push_event("update_voice_call_state", &[("client", &client_json)]); + } + + fn file_transfer_log(&self, action: &str, log: &str) { + self.push_event("cm_file_transfer_log", &[(action, log)]); + } + } + + impl FlutterHandler { + fn push_event(&self, name: &str, event: &[(&str, V)]) + where + V: Sized + serde::Serialize + Clone, + { + let mut h: HashMap<&str, serde_json::Value> = + event.iter().map(|(k, v)| (*k, json!(*v))).collect(); + debug_assert!(h.get("name").is_none()); + h.insert("name", json!(name)); + + if let Some(s) = GLOBAL_EVENT_STREAM.read().unwrap().get(super::APP_TYPE_CM) { + s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); + } else { + println!( + "Push event {} failed. No {} event stream found.", + name, + super::APP_TYPE_CM + ); + }; + } + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn start_cm_no_ui() { + start_listen_ipc(false); + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn start_listen_ipc_thread() { + start_listen_ipc(true); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn start_listen_ipc(new_thread: bool) { + use crate::ui_cm_interface::{start_ipc, ConnectionManager}; + + #[cfg(target_os = "linux")] + std::thread::spawn(crate::ipc::start_pa); + + let cm = ConnectionManager { + ui_handler: FlutterHandler {}, + }; + if new_thread { + std::thread::spawn(move || start_ipc(cm)); + } else { + start_ipc(cm); + } + } + + #[inline] + pub fn cm_init() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + start_listen_ipc_thread(); + } + + #[cfg(target_os = "android")] + use hbb_common::tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + + #[cfg(target_os = "android")] + pub fn start_channel( + rx: UnboundedReceiver, + tx: UnboundedSender, + ) { + use crate::ui_cm_interface::start_listen; + let cm = crate::ui_cm_interface::ConnectionManager { + ui_handler: FlutterHandler {}, + }; + std::thread::spawn(move || start_listen(cm, rx, tx)); + } +} + +pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> String { + let mut m = serde_json::Map::new(); + m.insert("id".into(), json!(id)); + let mut a = vec![]; + let mut n: u64 = 0; + for entry in entries { + n += entry.size; + if only_count { + continue; + } + let mut e = serde_json::Map::new(); + e.insert("name".into(), json!(entry.name.to_owned())); + let tmp = entry.entry_type.value(); + e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp })); + e.insert("time".into(), json!(entry.modified_time as f64)); + e.insert("size".into(), json!(entry.size as f64)); + a.push(e); + } + if only_count { + m.insert("num_entries".into(), json!(entries.len() as i32)); + } else { + m.insert("entries".into(), json!(a)); + } + m.insert("total_size".into(), json!(n as f64)); + serde_json::to_string(&m).unwrap_or("".into()) +} + +pub fn get_cur_session_id() -> SessionID { + CUR_SESSION_ID.read().unwrap().clone() +} + +pub fn get_cur_peer_id() -> String { + sessions::get_peer_id_by_session_id(&get_cur_session_id(), ConnType::DEFAULT_CONN) + .unwrap_or("".to_string()) +} + +pub fn set_cur_session_id(session_id: SessionID) { + if get_cur_session_id() != session_id { + *CUR_SESSION_ID.write().unwrap() = session_id; + } +} + +#[inline] +fn serialize_resolutions(resolutions: &Vec) -> String { + #[derive(Debug, serde::Serialize)] + struct ResolutionSerde { + width: i32, + height: i32, + } + + let mut v = vec![]; + resolutions + .iter() + .map(|r| { + v.push(ResolutionSerde { + width: r.width, + height: r.height, + }) + }) + .count(); + serde_json::ser::to_string(&v).unwrap_or("".to_string()) +} + +fn char_to_session_id(c: *const char) -> ResultType { + if c.is_null() { + bail!("Session id ptr is null"); + } + let cstr = unsafe { std::ffi::CStr::from_ptr(c as _) }; + let str = cstr.to_str()?; + SessionID::from_str(str).map_err(|e| anyhow!("{:?}", e)) +} + +pub fn session_get_rgba_size(session_id: SessionID, display: usize) -> usize { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + return session + .display_rgbas + .read() + .unwrap() + .get(&display) + .map_or(0, |rgba| rgba.data.len()); + } + 0 +} + +#[no_mangle] +pub extern "C" fn session_get_rgba(session_uuid_str: *const char, display: usize) -> *const u8 { + if let Ok(session_id) = char_to_session_id(session_uuid_str) { + if let Some(s) = sessions::get_session_by_session_id(&session_id) { + return s.ui_handler.get_rgba(display); + } + } + + std::ptr::null() +} + +pub fn session_next_rgba(session_id: SessionID, display: usize) { + if let Some(s) = sessions::get_session_by_session_id(&session_id) { + return s.ui_handler.next_rgba(display); + } +} + +#[inline] +pub fn session_set_size(session_id: SessionID, display: usize, width: usize, height: usize) { + for s in sessions::get_sessions() { + if let Some(h) = s + .ui_handler + .session_handlers + .write() + .unwrap() + .get_mut(&session_id) + { + // If the session is the first connection, displays is not set yet. + // `displays`` is set while switching displays or adding a new session. + if !h.displays.contains(&display) { + h.displays.push(display); + } + h.renderer.set_size(display, width, height); + break; + } + } +} + +#[inline] +pub fn session_register_pixelbuffer_texture(session_id: SessionID, display: usize, ptr: usize) { + for s in sessions::get_sessions() { + if let Some(h) = s + .ui_handler + .session_handlers + .read() + .unwrap() + .get(&session_id) + { + h.renderer.register_pixelbuffer_texture(display, ptr); + break; + } + } +} + +#[inline] +pub fn session_register_gpu_texture(_session_id: SessionID, _display: usize, _output_ptr: usize) { + #[cfg(feature = "vram")] + for s in sessions::get_sessions() { + if let Some(h) = s + .ui_handler + .session_handlers + .read() + .unwrap() + .get(&_session_id) + { + h.renderer.register_gpu_output(_display, _output_ptr); + break; + } + } +} + +#[inline] +#[cfg(not(feature = "vram"))] +pub fn get_adapter_luid() -> Option { + None +} + +#[cfg(feature = "vram")] +pub fn get_adapter_luid() -> Option { + if !crate::ui_interface::use_texture_render() { + return None; + } + let get_adapter_luid_func = match &*TEXTURE_GPU_RENDERER_PLUGIN { + Ok(lib) => { + let find_sym_res = unsafe { + lib.symbol::( + "FlutterGpuTextureRendererPluginCApiGetAdapterLuid", + ) + }; + match find_sym_res { + Ok(sym) => Some(sym), + Err(e) => { + log::error!("Failed to find symbol FlutterGpuTextureRendererPluginCApiGetAdapterLuid, {e}"); + None + } + } + } + Err(e) => { + log::error!("Failed to load texture gpu renderer plugin, {e}"); + None + } + }; + let adapter_luid = match get_adapter_luid_func { + Some(get_adapter_luid_func) => unsafe { Some(get_adapter_luid_func()) }, + None => Default::default(), + }; + return adapter_luid; +} + +#[inline] +pub fn push_session_event(session_id: &SessionID, name: &str, event: Vec<(&str, &str)>) { + if let Some(s) = sessions::get_session_by_session_id(session_id) { + s.push_event(name, &event, &[]); + } +} + +#[inline] +pub fn push_global_event(channel: &str, event: String) -> Option { + Some(GLOBAL_EVENT_STREAM.read().unwrap().get(channel)?.add(event)) +} + +#[inline] +pub fn get_global_event_channels() -> Vec { + GLOBAL_EVENT_STREAM + .read() + .unwrap() + .keys() + .cloned() + .collect() +} + +pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { + let app_type_values = app_type.split(",").collect::>(); + let mut lock = GLOBAL_EVENT_STREAM.write().unwrap(); + if !lock.contains_key(app_type_values[0]) { + lock.insert(app_type_values[0].to_string(), s); + } else { + if let Some(_) = lock.insert(app_type.clone(), s) { + log::warn!( + "Global event stream of type {} is started before, but now removed", + app_type + ); + } + } + Ok(()) +} + +pub fn stop_global_event_stream(app_type: String) { + let _ = GLOBAL_EVENT_STREAM.write().unwrap().remove(&app_type); +} + +#[inline] +fn session_send_touch_scale( + session_id: SessionID, + v: &serde_json::Value, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, +) { + match v.get("v").and_then(|s| s.as_i64()) { + Some(scale) => { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.send_touch_scale(scale as _, alt, ctrl, shift, command); + } + } + None => {} + } +} + +#[inline] +fn session_send_touch_pan( + session_id: SessionID, + v: &serde_json::Value, + pan_event: &str, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, +) { + match v.get("v") { + Some(v) => match ( + v.get("x").and_then(|x| x.as_i64()), + v.get("y").and_then(|y| y.as_i64()), + ) { + (Some(x), Some(y)) => { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session + .send_touch_pan_event(pan_event, x as _, y as _, alt, ctrl, shift, command); + } + } + _ => {} + }, + _ => {} + } +} + +fn session_send_touch_event( + session_id: SessionID, + v: &serde_json::Value, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, +) { + match v.get("t").and_then(|t| t.as_str()) { + Some("scale") => session_send_touch_scale(session_id, v, alt, ctrl, shift, command), + Some(pan_event) => { + session_send_touch_pan(session_id, v, pan_event, alt, ctrl, shift, command) + } + _ => {} + } +} + +pub fn session_send_pointer(session_id: SessionID, msg: String) { + if let Ok(m) = serde_json::from_str::>(&msg) { + let alt = m.get("alt").is_some(); + let ctrl = m.get("ctrl").is_some(); + let shift = m.get("shift").is_some(); + let command = m.get("command").is_some(); + match (m.get("k"), m.get("v")) { + (Some(k), Some(v)) => match k.as_str() { + Some("touch") => session_send_touch_event(session_id, v, alt, ctrl, shift, command), + _ => {} + }, + _ => {} + } + } +} + +#[inline] +pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) { + for s in sessions::get_sessions() { + if let Some(h) = s.session_handlers.write().unwrap().get_mut(&session_id) { + h.on_waiting_for_image_dialog_show(); + } + } +} + +/// Hooks for session. +#[derive(Clone)] +pub enum SessionHook { + OnSessionRgba(fn(String, &mut scrap::ImageRgb)), +} + +#[inline] +pub fn get_cur_session() -> Option { + sessions::get_session_by_session_id(&*CUR_SESSION_ID.read().unwrap()) +} + +#[inline] +pub fn try_sync_peer_option( + session: &FlutterSession, + cur_id: &SessionID, + key: &str, + _value: Option, +) { + let mut event = Vec::new(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if key == "view-only" { + event = vec![ + ("k", json!(key.to_string())), + ("v", json!(session.lc.read().unwrap().view_only.v)), + ]; + } + if ["keyboard_mode", "input_source"].contains(&key) { + event = vec![("k", json!(key.to_string())), ("v", json!(""))]; + } + if !event.is_empty() { + session.push_event("sync_peer_option", &event, &[cur_id]); + } +} + +pub(super) fn session_update_virtual_display(session: &FlutterSession, index: i32, on: bool) { + let virtual_display_key = "virtual-display"; + let displays = session.get_option(virtual_display_key.to_owned()); + if !on { + if index == -1 { + if !displays.is_empty() { + session.set_option(virtual_display_key.to_owned(), "".to_owned()); + } + } else { + let mut vdisplays = displays.split(',').collect::>(); + let len = vdisplays.len(); + if index == 0 { + // 0 means we can't toggle the virtual display by index. + vdisplays.remove(vdisplays.len() - 1); + } else { + if let Some(i) = vdisplays.iter().position(|&x| x == index.to_string()) { + vdisplays.remove(i); + } + } + if vdisplays.len() != len { + session.set_option( + virtual_display_key.to_owned(), + vdisplays.join(",").to_owned(), + ); + } + } + } else { + let mut vdisplays = displays + .split(',') + .map(|x| x.to_string()) + .collect::>(); + let len = vdisplays.len(); + if index == 0 { + vdisplays.push(index.to_string()); + } else { + if !vdisplays.iter().any(|x| *x == index.to_string()) { + vdisplays.push(index.to_string()); + } + } + if vdisplays.len() != len { + session.set_option( + virtual_display_key.to_owned(), + vdisplays.join(",").to_owned(), + ); + } + } +} + +// sessions mod is used to avoid the big lock of sessions' map. +pub mod sessions { + + use super::*; + + lazy_static::lazy_static! { + // peer -> peer session, peer session -> ui sessions + static ref SESSIONS: RwLock> = Default::default(); + } + + #[inline] + pub fn get_session_count(peer_id: String, conn_type: ConnType) -> usize { + SESSIONS + .read() + .unwrap() + .get(&(peer_id, conn_type)) + .map(|s| s.ui_handler.session_handlers.read().unwrap().len()) + .unwrap_or(0) + } + + #[inline] + pub fn get_peer_id_by_session_id(id: &SessionID, conn_type: ConnType) -> Option { + SESSIONS + .read() + .unwrap() + .iter() + .find_map(|((peer_id, t), s)| { + if *t == conn_type + && s.ui_handler + .session_handlers + .read() + .unwrap() + .contains_key(id) + { + Some(peer_id.clone()) + } else { + None + } + }) + } + + #[inline] + pub fn get_session_by_session_id(id: &SessionID) -> Option { + SESSIONS + .read() + .unwrap() + .values() + .find(|s| { + s.ui_handler + .session_handlers + .read() + .unwrap() + .contains_key(id) + }) + .cloned() + } + + #[inline] + pub fn get_session_by_peer_id(peer_id: String, conn_type: ConnType) -> Option { + SESSIONS.read().unwrap().get(&(peer_id, conn_type)).cloned() + } + + #[inline] + pub fn remove_session_by_session_id(id: &SessionID) -> Option { + let mut remove_peer_key = None; + for (peer_key, s) in SESSIONS.write().unwrap().iter_mut() { + let mut write_lock = s.ui_handler.session_handlers.write().unwrap(); + let remove_ret = write_lock.remove(id); + match remove_ret { + Some(_) => { + if write_lock.is_empty() { + remove_peer_key = Some(peer_key.clone()); + } else { + check_remove_unused_displays(None, id, s, &write_lock); + } + break; + } + None => {} + } + } + let s = SESSIONS.write().unwrap().remove(&remove_peer_key?); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_session_count_to_server(); + s + } + + /// Check if removing a session by session_id would result in removing the entire peer. + /// + /// Returns: + /// - `true`: The session exists and removing it would leave the peer with no other sessions, + /// so the entire peer would be removed (equivalent to `remove_session_by_session_id` returning `Some`) + /// - `false`: The session doesn't exist, or it exists but the peer has other sessions, + /// so the peer would not be removed (equivalent to `remove_session_by_session_id` returning `None`) + #[inline] + pub fn would_remove_peer_by_session_id(id: &SessionID) -> bool { + for (_peer_key, s) in SESSIONS.read().unwrap().iter() { + let read_lock = s.ui_handler.session_handlers.read().unwrap(); + if read_lock.contains_key(id) { + // Found the session, check if it's the only one for this peer + return read_lock.len() == 1; + } + } + // Session not found + false + } + + fn check_remove_unused_displays( + current: Option, + session_id: &SessionID, + session: &FlutterSession, + handlers: &HashMap, + ) { + // Set capture displays if some are not used any more. + let mut remains_displays = HashSet::new(); + if let Some(current) = current { + remains_displays.insert(current); + } + for (k, h) in handlers.iter() { + if k == session_id { + continue; + } + remains_displays.extend( + h.renderer + .map_display_sessions + .read() + .unwrap() + .keys() + .cloned(), + ); + } + if !remains_displays.is_empty() { + session.capture_displays( + vec![], + vec![], + remains_displays.iter().map(|d| *d as i32).collect(), + ); + } + } + + pub fn session_switch_display(is_desktop: bool, session_id: SessionID, value: Vec) { + for s in SESSIONS.read().unwrap().values() { + let mut write_lock = s.ui_handler.session_handlers.write().unwrap(); + if let Some(h) = write_lock.get_mut(&session_id) { + h.displays = value.iter().map(|x| *x as usize).collect::<_>(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let displays_refresh = value.clone(); + if value.len() == 1 { + // Switch display. + // This operation will also cause the peer to send a switch display message. + // The switch display message will contain `SupportedResolutions`, which is useful when changing resolutions. + s.switch_display(value[0]); + // Reset the valid flag of the display. + s.next_rgba(value[0] as usize); + + if !is_desktop { + s.capture_displays(vec![], vec![], value); + } else { + // Check if other displays are needed. + if value.len() == 1 { + check_remove_unused_displays( + Some(value[0] as _), + &session_id, + &s, + &write_lock, + ); + } + } + } else { + // Try capture all displays. + s.capture_displays(vec![], vec![], value); + } + // When switching display, we also need to send "Refresh display" message. + // On the controlled side: + // 1. If this display is not currently captured -> Refresh -> Message "Refresh display" is not required. + // One more key frame (first frame) will be sent because the refresh message. + // 2. If this display is currently captured -> Not refresh -> Message "Refresh display" is required. + // Without the message, the control side cannot see the latest display image. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let is_support_multi_ui_session = crate::common::is_support_multi_ui_session( + &s.ui_handler.peer_info.read().unwrap().version, + ); + if is_support_multi_ui_session { + for display in displays_refresh.iter() { + s.refresh_video(*display); + } + } + } + break; + } + } + } + + #[inline] + pub fn insert_session(session_id: SessionID, conn_type: ConnType, session: FlutterSession) { + SESSIONS + .write() + .unwrap() + .entry((session.get_id(), conn_type)) + .or_insert(session) + .ui_handler + .session_handlers + .write() + .unwrap() + .insert(session_id, Default::default()); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_session_count_to_server(); + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn update_session_count_to_server() { + crate::ipc::update_controlling_session_count(SESSIONS.read().unwrap().len()).ok(); + } + + #[inline] + pub fn insert_peer_session_id( + peer_id: String, + conn_type: ConnType, + session_id: SessionID, + displays: Vec, + ) -> bool { + if let Some(s) = SESSIONS.read().unwrap().get(&(peer_id, conn_type)) { + let mut h = SessionHandler::default(); + h.displays = displays.iter().map(|x| *x as usize).collect::<_>(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let is_support_multi_ui_session = crate::common::is_support_multi_ui_session( + &s.ui_handler.peer_info.read().unwrap().version, + ); + #[cfg(any(target_os = "android", target_os = "ios"))] + let is_support_multi_ui_session = false; + h.renderer.is_support_multi_ui_session = is_support_multi_ui_session; + let _ = s + .ui_handler + .session_handlers + .write() + .unwrap() + .insert(session_id, h); + // If the session is a single display session, it may be a software rgba rendered display. + // If this is the second time the display is opened, the old valid flag may be true. + if displays.len() == 1 { + s.ui_handler.next_rgba(displays[0] as usize); + } + true + } else { + false + } + } + + #[inline] + pub fn get_sessions() -> Vec { + SESSIONS.read().unwrap().values().cloned().collect() + } + + #[inline] + #[cfg(not(target_os = "ios"))] + pub fn has_sessions_running(conn_type: ConnType) -> bool { + SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| { + *r#type == conn_type && s.session_handlers.read().unwrap().len() != 0 + }) + } +} + +pub(super) mod async_tasks { + use hbb_common::{bail, tokio, ResultType}; + use std::{ + collections::HashMap, + sync::{ + mpsc::{sync_channel, SyncSender}, + Arc, Mutex, + }, + }; + + type TxQueryOnlines = SyncSender>; + lazy_static::lazy_static! { + static ref TX_QUERY_ONLINES: Arc>> = Default::default(); + } + + #[inline] + pub fn start_flutter_async_runner() { + std::thread::spawn(start_flutter_async_runner_); + } + + #[allow(dead_code)] + pub fn stop_flutter_async_runner() { + let _ = TX_QUERY_ONLINES.lock().unwrap().take(); + } + + #[tokio::main(flavor = "current_thread")] + async fn start_flutter_async_runner_() { + // Only one task is allowed to run at the same time. + let (tx_onlines, rx_onlines) = sync_channel::>(1); + TX_QUERY_ONLINES.lock().unwrap().replace(tx_onlines); + + loop { + match rx_onlines.recv() { + Ok(ids) => { + crate::client::peer_online::query_online_states(ids, handle_query_onlines).await + } + _ => { + // unreachable! + break; + } + } + } + } + + pub fn query_onlines(ids: Vec) -> ResultType<()> { + if let Some(tx) = TX_QUERY_ONLINES.lock().unwrap().as_ref() { + // Ignore if the channel is full. + let _ = tx.try_send(ids)?; + } else { + bail!("No tx_query_onlines"); + } + Ok(()) + } + + fn handle_query_onlines(onlines: Vec, offlines: Vec) { + let data = HashMap::from([ + ("name", "callback_query_onlines".to_owned()), + ("onlines", onlines.join(",")), + ("offlines", offlines.join(",")), + ]); + let _res = super::push_global_event( + super::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + } +} diff --git a/vendor/rustdesk/src/flutter_ffi.rs b/vendor/rustdesk/src/flutter_ffi.rs new file mode 100644 index 0000000..3f97df0 --- /dev/null +++ b/vendor/rustdesk/src/flutter_ffi.rs @@ -0,0 +1,3134 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::keyboard::input_source::{change_input_source, get_cur_session_input_source}; +#[cfg(target_os = "linux")] +use crate::platform::linux::is_x11; +use crate::{ + client::file_trait::FileManager, + common::{make_fd_to_json, make_vec_fd_to_json}, + flutter::{ + self, session_add, session_add_existed, session_start_, sessions, try_sync_peer_option, + }, + input::*, + ui_interface::{self, *}, +}; +use flutter_rust_bridge::{StreamSink, SyncReturn}; +#[cfg(feature = "plugin_framework")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::allow_err; +use hbb_common::{ + config::{self, LocalConfig, PeerConfig, PeerInfoSerde}, + fs, lazy_static, log, + rendezvous_proto::ConnType, + ResultType, +}; +use std::{ + collections::HashMap, + path::PathBuf, + sync::{ + atomic::{AtomicI32, Ordering}, + Arc, + }, + time::{Duration, SystemTime}, +}; + +pub type SessionID = uuid::Uuid; + +lazy_static::lazy_static! { + static ref TEXTURE_RENDER_KEY: Arc = Arc::new(AtomicI32::new(0)); +} + +fn initialize(app_dir: &str, custom_client_config: &str) { + flutter::async_tasks::start_flutter_async_runner(); + // `APP_DIR` is set in `main_get_data_dir_ios()` on iOS. + #[cfg(not(target_os = "ios"))] + { + *config::APP_DIR.write().unwrap() = app_dir.to_owned(); + } + // core_main's load_custom_client does not work for flutter since it is only applied to its load_library in main.c + if custom_client_config.is_empty() { + crate::load_custom_client(); + } else { + crate::read_custom_client(custom_client_config); + } + #[cfg(target_os = "android")] + { + // flexi_logger can't work when android_logger initialized. + #[cfg(debug_assertions)] + android_logger::init_once( + android_logger::Config::default() + .with_max_level(log::LevelFilter::Debug) // limit log level + .with_tag("ffi"), // logs will show under mytag tag + ); + #[cfg(not(debug_assertions))] + hbb_common::init_log(false, ""); + #[cfg(feature = "mediacodec")] + scrap::mediacodec::check_mediacodec(); + crate::common::test_rendezvous_server(); + crate::common::test_nat_type(); + } + #[cfg(target_os = "ios")] + { + use hbb_common::env_logger::*; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")); + crate::common::test_nat_type(); + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + let _ = crate::common::global_init(); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + // core_main's init_log does not work for flutter since it is only applied to its load_library in main.c + hbb_common::init_log(false, "flutter_ffi"); + } +} + +#[inline] +pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { + super::flutter::start_global_event_stream(s, app_type) +} + +#[inline] +pub fn stop_global_event_stream(app_type: String) { + super::flutter::stop_global_event_stream(app_type) +} +pub enum EventToUI { + Event(String), + Rgba(usize), + Texture(usize, bool), // (display, gpu_texture) +} + +pub fn host_stop_system_key_propagate(_stopped: bool) { + #[cfg(windows)] + crate::platform::windows::stop_system_key_propagate(_stopped); +} + +// This function is only used to count the number of control sessions. +pub fn peer_get_sessions_count(id: String, conn_type: i32) -> SyncReturn { + let conn_type = if conn_type == ConnType::VIEW_CAMERA as i32 { + ConnType::VIEW_CAMERA + } else if conn_type == ConnType::FILE_TRANSFER as i32 { + ConnType::FILE_TRANSFER + } else if conn_type == ConnType::PORT_FORWARD as i32 { + ConnType::PORT_FORWARD + } else if conn_type == ConnType::RDP as i32 { + ConnType::RDP + } else if conn_type == ConnType::TERMINAL as i32 { + ConnType::TERMINAL + } else { + ConnType::DEFAULT_CONN + }; + SyncReturn(sessions::get_session_count(id, conn_type)) +} + +pub fn session_add_existed_sync( + id: String, + session_id: SessionID, + displays: Vec, + is_view_camera: bool, +) -> SyncReturn { + if let Err(e) = session_add_existed(id.clone(), session_id, displays, is_view_camera) { + SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) + } else { + SyncReturn("".to_owned()) + } +} + +pub fn session_add_sync( + session_id: SessionID, + id: String, + is_file_transfer: bool, + is_view_camera: bool, + is_port_forward: bool, + is_rdp: bool, + is_terminal: bool, + switch_uuid: String, + force_relay: bool, + password: String, + is_shared_password: bool, + conn_token: Option, +) -> SyncReturn { + let add_res = session_add( + &session_id, + &id, + is_file_transfer, + is_view_camera, + is_port_forward, + is_rdp, + is_terminal, + &switch_uuid, + force_relay, + password, + is_shared_password, + conn_token, + ); + // We can't put the remove call together with `std::env::var("IS_TERMINAL_ADMIN")`. + // Because there are some `bail!` in `session_add()`, we must make sure `IS_TERMINAL_ADMIN` is removed at last. + if is_terminal { + std::env::remove_var("IS_TERMINAL_ADMIN"); + } + + if let Err(e) = add_res { + SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) + } else { + SyncReturn("".to_owned()) + } +} + +pub fn session_start( + events2ui: StreamSink, + session_id: SessionID, + id: String, +) -> ResultType<()> { + session_start_(&session_id, &id, events2ui) +} + +pub fn session_start_with_displays( + events2ui: StreamSink, + session_id: SessionID, + id: String, + displays: Vec, +) -> ResultType<()> { + session_start_(&session_id, &id, events2ui)?; + + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.capture_displays(displays.clone(), vec![], vec![]); + for display in displays { + session.refresh_video(display as _); + } + } + Ok(()) +} + +pub fn session_get_remember(session_id: SessionID) -> Option { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_remember()) + } else { + None + } +} + +pub fn session_get_toggle_option(session_id: SessionID, arg: String) -> Option { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_toggle_option(arg)) + } else { + None + } +} + +pub fn session_get_toggle_option_sync(session_id: SessionID, arg: String) -> SyncReturn { + let res = session_get_toggle_option(session_id, arg) == Some(true); + SyncReturn(res) +} + +pub fn session_get_option(session_id: SessionID, arg: String) -> Option { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_option(arg)) + } else { + None + } +} + +pub fn session_login( + session_id: SessionID, + os_username: String, + os_password: String, + password: String, + remember: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.login(os_username, os_password, password, remember); + } +} + +pub fn session_send2fa(session_id: SessionID, code: String, trust_this_device: bool) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.send2fa(code, trust_this_device); + } +} + +pub fn session_get_enable_trusted_devices(session_id: SessionID) -> SyncReturn { + let v = if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.get_enable_trusted_devices() + } else { + false + }; + SyncReturn(v) +} + +pub fn will_session_close_close_session(session_id: SessionID) -> SyncReturn { + SyncReturn(sessions::would_remove_peer_by_session_id(&session_id)) +} + +pub fn session_close(session_id: SessionID) { + if let Some(session) = sessions::remove_session_by_session_id(&session_id) { + // `release_remote_keys` is not required for mobile platforms in common cases. + // But we still call it to make the code more stable. + #[cfg(any(target_os = "android", target_os = "ios"))] + crate::keyboard::release_remote_keys("map"); + session.close_event_stream(session_id); + session.close(); + } +} + +pub fn session_refresh(session_id: SessionID, display: usize) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.refresh_video(display as _); + } +} + +pub fn session_take_screenshot(session_id: SessionID, display: usize) { + if let Some(s) = sessions::get_session_by_session_id(&session_id) { + s.take_screenshot(display as _, session_id.to_string()); + } +} + +pub fn session_handle_screenshot( + #[allow(unused_variables)] session_id: SessionID, + action: String, +) -> String { + crate::client::screenshot::handle_screenshot(action) +} + +pub fn session_is_multi_ui_session(session_id: SessionID) -> SyncReturn { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.is_multi_ui_session()) + } else { + SyncReturn(false) + } +} + +pub fn session_record_screen(session_id: SessionID, start: bool) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.record_screen(start); + } +} + +pub fn session_get_is_recording(session_id: SessionID) -> SyncReturn { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.is_recording()) + } else { + SyncReturn(false) + } +} + +pub fn session_reconnect(session_id: SessionID, force_relay: bool) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.reconnect(force_relay); + } + session_on_waiting_for_image_dialog_show(session_id); +} + +pub fn session_toggle_option(session_id: SessionID, value: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + log::warn!("toggle option {}", &value); + session.toggle_option(value.clone()); + try_sync_peer_option(&session, &session_id, &value, None); + } + #[cfg(not(target_os = "ios"))] + if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" { + crate::flutter::update_text_clipboard_required(); + } + #[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 + { + crate::flutter::update_file_clipboard_required(); + } +} + +pub fn session_toggle_privacy_mode(session_id: SessionID, impl_key: String, on: bool) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.toggle_privacy_mode(impl_key, on); + } +} + +pub fn session_get_flutter_option(session_id: SessionID, k: String) -> Option { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_flutter_option(k)) + } else { + None + } +} + +pub fn session_set_flutter_option(session_id: SessionID, k: String, v: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_flutter_option(k, v); + } +} + +pub fn get_next_texture_key() -> SyncReturn { + let k = TEXTURE_RENDER_KEY.fetch_add(1, Ordering::SeqCst) + 1; + SyncReturn(k) +} + +pub fn get_local_flutter_option(k: String) -> SyncReturn { + SyncReturn(ui_interface::get_local_flutter_option(k)) +} + +pub fn set_local_flutter_option(k: String, v: String) { + ui_interface::set_local_flutter_option(k, v); +} + +pub fn get_local_kb_layout_type() -> SyncReturn { + SyncReturn(ui_interface::get_kb_layout_type()) +} + +pub fn set_local_kb_layout_type(kb_layout_type: String) { + ui_interface::set_kb_layout_type(kb_layout_type) +} + +pub fn session_get_view_style(session_id: SessionID) -> Option { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_view_style()) + } else { + None + } +} + +pub fn session_set_view_style(session_id: SessionID, value: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_view_style(value); + } +} + +pub fn session_get_scroll_style(session_id: SessionID) -> Option { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_scroll_style()) + } else { + None + } +} + +pub fn session_set_scroll_style(session_id: SessionID, value: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_scroll_style(value); + } +} + +pub fn session_get_edge_scroll_edge_thickness(session_id: SessionID) -> Option { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_edge_scroll_edge_thickness()) + } else { + None + } +} + +pub fn session_set_edge_scroll_edge_thickness(session_id: SessionID, value: i32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_edge_scroll_edge_thickness(value); + } +} + +pub fn session_get_image_quality(session_id: SessionID) -> Option { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_image_quality()) + } else { + None + } +} + +pub fn session_set_image_quality(session_id: SessionID, value: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_image_quality(value); + } +} + +pub fn session_get_keyboard_mode(session_id: SessionID) -> Option { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_keyboard_mode()) + } else { + None + } +} + +pub fn session_set_keyboard_mode(session_id: SessionID, value: String) { + let mut _mode_updated = false; + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_keyboard_mode(value.clone()); + _mode_updated = true; + try_sync_peer_option(&session, &session_id, "keyboard_mode", None); + } + #[cfg(windows)] + if _mode_updated { + crate::keyboard::update_grab_get_key_name(&value); + } +} + +pub fn session_get_reverse_mouse_wheel_sync(session_id: SessionID) -> SyncReturn> { + let res = if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_reverse_mouse_wheel()) + } else { + None + }; + SyncReturn(res) +} + +pub fn session_set_reverse_mouse_wheel(session_id: SessionID, value: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_reverse_mouse_wheel(value); + } +} + +pub fn session_get_displays_as_individual_windows( + session_id: SessionID, +) -> SyncReturn> { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(Some(session.get_displays_as_individual_windows())) + } else { + SyncReturn(None) + } +} + +pub fn session_set_displays_as_individual_windows(session_id: SessionID, value: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_displays_as_individual_windows(value); + } +} + +pub fn session_get_use_all_my_displays_for_the_remote_session( + session_id: SessionID, +) -> SyncReturn> { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(Some( + session.get_use_all_my_displays_for_the_remote_session(), + )) + } else { + SyncReturn(None) + } +} + +pub fn session_set_use_all_my_displays_for_the_remote_session( + session_id: SessionID, + value: String, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_use_all_my_displays_for_the_remote_session(value); + } +} + +pub fn session_get_custom_image_quality(session_id: SessionID) -> Option> { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_custom_image_quality()) + } else { + None + } +} + +pub fn session_is_keyboard_mode_supported(session_id: SessionID, mode: String) -> SyncReturn { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.is_keyboard_mode_supported(mode)) + } else { + SyncReturn(false) + } +} + +pub fn session_set_custom_image_quality(session_id: SessionID, value: i32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_custom_image_quality(value); + } +} + +pub fn session_set_custom_fps(session_id: SessionID, fps: i32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.set_custom_fps(fps); + } +} + +pub fn session_get_trackpad_speed(session_id: SessionID) -> Option { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_trackpad_speed()) + } else { + None + } +} + +pub fn session_set_trackpad_speed(session_id: SessionID, value: i32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_trackpad_speed(value); + } +} + +pub fn session_lock_screen(session_id: SessionID) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.lock_screen(); + } +} + +pub fn session_ctrl_alt_del(session_id: SessionID) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.ctrl_alt_del(); + } +} + +pub fn session_switch_display(is_desktop: bool, session_id: SessionID, value: Vec) { + sessions::session_switch_display(is_desktop, session_id, value); +} + +pub fn session_handle_flutter_key_event( + session_id: SessionID, + character: String, + usb_hid: i32, + lock_modes: i32, + down_or_up: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + let keyboard_mode = session.get_keyboard_mode(); + session.handle_flutter_key_event( + &keyboard_mode, + &character, + usb_hid, + lock_modes, + down_or_up, + ); + } +} + +pub fn session_handle_flutter_raw_key_event( + session_id: SessionID, + name: String, + platform_code: i32, + position_code: i32, + lock_modes: i32, + down_or_up: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + let keyboard_mode = session.get_keyboard_mode(); + session.handle_flutter_raw_key_event( + &keyboard_mode, + &name, + platform_code, + position_code, + lock_modes, + down_or_up, + ); + } +} + +// If the cursor jumps between remote page of two connections, leave view and enter view will be called. +// session_enter_or_leave() will be called then. +// As Rust is multi-threaded, enter() can be called before leave(). +// The Rust-side grab ownership state filters stale transitions. +pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(session) = sessions::get_session_by_session_id(&_session_id) { + let keyboard_mode = session.get_keyboard_mode(); + // Use the full per-window UUID (not lc.session_id which is per-connection) + // so that two windows viewing the same peer get distinct grab owners. + let window_id = _session_id.as_u128(); + if _enter { + set_cur_session_id_(_session_id, &keyboard_mode); + crate::keyboard::client::change_grab_status( + crate::common::GrabState::Run, + &keyboard_mode, + window_id, + ); + } else { + crate::keyboard::client::change_grab_status( + crate::common::GrabState::Wait, + &keyboard_mode, + window_id, + ); + } + } + SyncReturn(()) +} + +pub fn session_input_key( + session_id: SessionID, + name: String, + down: bool, + press: bool, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + // #[cfg(any(target_os = "android", target_os = "ios"))] + session.input_key(&name, down, press, alt, ctrl, shift, command); + } +} + +pub fn session_input_string(session_id: SessionID, value: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + // #[cfg(any(target_os = "android", target_os = "ios"))] + session.input_string(&value); + } +} + +// chat_client_mode +pub fn session_send_chat(session_id: SessionID, text: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.send_chat(text); + } +} + +// Terminal functions +pub fn session_open_terminal(session_id: SessionID, terminal_id: i32, rows: u32, cols: u32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.open_terminal(terminal_id, rows, cols); + } else { + log::error!( + "[flutter_ffi] Session not found for session_id: {}", + session_id + ); + } +} + +pub fn session_send_terminal_input(session_id: SessionID, terminal_id: i32, data: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.send_terminal_input(terminal_id, data); + } +} + +pub fn session_resize_terminal(session_id: SessionID, terminal_id: i32, rows: u32, cols: u32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.resize_terminal(terminal_id, rows, cols); + } +} + +pub fn session_close_terminal(session_id: SessionID, terminal_id: i32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.close_terminal(terminal_id); + } +} + +pub fn session_peer_option(session_id: SessionID, name: String, value: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.set_option(name, value); + } +} + +pub fn session_get_peer_option(session_id: SessionID, name: String) -> String { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + return session.get_option(name); + } + "".to_string() +} + +pub fn session_input_os_password(session_id: SessionID, value: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.input_os_password(value, true); + } +} + +// File Action +pub fn session_read_remote_dir(session_id: SessionID, path: String, include_hidden: bool) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.read_remote_dir(path, include_hidden); + } +} + +pub fn session_send_files( + session_id: SessionID, + act_id: i32, + path: String, + to: String, + file_num: i32, + include_hidden: bool, + is_remote: bool, + _is_dir: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.send_files( + act_id, + fs::JobType::Generic.into(), + path, + to, + file_num, + include_hidden, + is_remote, + ); + } +} + +pub fn session_set_confirm_override_file( + session_id: SessionID, + act_id: i32, + file_num: i32, + need_override: bool, + remember: bool, + is_upload: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.set_confirm_override_file(act_id, file_num, need_override, remember, is_upload); + } +} + +pub fn session_remove_file( + session_id: SessionID, + act_id: i32, + path: String, + file_num: i32, + is_remote: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.remove_file(act_id, path, file_num, is_remote); + } +} + +pub fn session_read_dir_to_remove_recursive( + session_id: SessionID, + act_id: i32, + path: String, + is_remote: bool, + show_hidden: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.remove_dir_all(act_id, path, is_remote, show_hidden); + } +} + +pub fn session_remove_all_empty_dirs( + session_id: SessionID, + act_id: i32, + path: String, + is_remote: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.remove_dir(act_id, path, is_remote); + } +} + +pub fn session_cancel_job(session_id: SessionID, act_id: i32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.cancel_job(act_id); + } +} + +pub fn session_create_dir(session_id: SessionID, act_id: i32, path: String, is_remote: bool) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.create_dir(act_id, path, is_remote); + } +} + +pub fn session_read_local_dir_sync( + _session_id: SessionID, + path: String, + show_hidden: bool, +) -> String { + if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) { + return make_fd_to_json(fd.id, path, &fd.entries); + } + "".to_string() +} + +pub fn session_read_local_empty_dirs_recursive_sync( + _session_id: SessionID, + path: String, + include_hidden: bool, +) -> String { + if let Ok(fds) = fs::get_empty_dirs_recursive(&path, include_hidden) { + return make_vec_fd_to_json(&fds); + } + "".to_string() +} + +pub fn session_read_remote_empty_dirs_recursive_sync( + session_id: SessionID, + path: String, + include_hidden: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.read_empty_dirs(path, include_hidden); + } +} + +pub fn session_get_platform(session_id: SessionID, is_remote: bool) -> String { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + return session.get_platform(is_remote); + } + "".to_string() +} + +pub fn session_load_last_transfer_jobs(session_id: SessionID) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + return session.load_last_jobs(); + } else { + // a tip for flutter dev + eprintln!( + "cannot load last transfer job from non-existed session. Please ensure session \ + is connected before calling load last transfer jobs." + ); + } +} + +pub fn session_add_job( + session_id: SessionID, + act_id: i32, + path: String, + to: String, + file_num: i32, + include_hidden: bool, + is_remote: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.add_job( + act_id, + fs::JobType::Generic.into(), + path, + to, + file_num, + include_hidden, + is_remote, + ); + } +} + +pub fn session_resume_job(session_id: SessionID, act_id: i32, is_remote: bool) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.resume_job(act_id, is_remote); + } +} + +pub fn session_rename_file( + session_id: SessionID, + act_id: i32, + path: String, + new_name: String, + is_remote: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.rename_file(act_id, path, new_name, is_remote); + } +} + +pub fn session_elevate_direct(session_id: SessionID) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.elevate_direct(); + } +} + +pub fn session_elevate_with_logon(session_id: SessionID, username: String, password: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.elevate_with_logon(username, password); + } +} + +pub fn session_switch_sides(session_id: SessionID) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.switch_sides(); + } +} + +pub fn session_change_resolution(session_id: SessionID, display: i32, width: i32, height: i32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.change_resolution(display, width, height); + } +} + +pub fn session_set_size(session_id: SessionID, display: usize, width: usize, height: usize) { + super::flutter::session_set_size(session_id, display, width, height) +} + +pub fn session_send_selected_session_id(session_id: SessionID, sid: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.send_selected_session_id(sid); + } +} + +pub fn main_get_sound_inputs() -> Vec { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return get_sound_inputs(); + #[cfg(any(target_os = "android", target_os = "ios"))] + vec![String::from("")] +} + +pub fn main_get_login_device_info() -> SyncReturn { + SyncReturn(get_login_device_info_json()) +} + +pub fn main_change_id(new_id: String) { + change_id(new_id) +} + +pub fn main_get_async_status() -> String { + get_async_job_status() +} + +pub fn main_get_http_status(url: String) -> Option { + get_async_http_status(url) +} + +pub fn main_get_option(key: String) -> String { + get_option(key) +} + +pub fn main_get_option_sync(key: String) -> SyncReturn { + SyncReturn(get_option(key)) +} + +pub fn main_get_error() -> String { + get_error() +} + +pub fn main_show_option(_key: String) -> SyncReturn { + #[cfg(target_os = "linux")] + if _key.eq(config::keys::OPTION_ALLOW_LINUX_HEADLESS) { + return SyncReturn(true); + } + SyncReturn(false) +} + +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 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), + ); + if is_permission_option + && !allow_perm_change_in_accept_window + && crate::ui_cm_interface::has_active_clients() + { + log::info!( + "blocked main_set_option by policy, key={}, value={}", + key, + value + ); + return; + } + } + #[cfg(target_os = "android")] + if key.eq(config::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) { + crate::ui_cm_interface::switch_permission_all( + "clipboard".to_owned(), + config::option2bool(&key, &value), + ); + } + + // 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); + 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("api-server") + { + if is_allow_tls_fallback { + hbb_common::tls::reset_tls_cache(); + } + set_option(key, value.clone()); + #[cfg(target_os = "android")] + crate::rendezvous_mediator::RendezvousMediator::restart(); + #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] + crate::common::test_rendezvous_server(); + } else { + set_option(key, value.clone()); + } +} + +pub fn main_get_options() -> String { + get_options() +} + +pub fn main_get_options_sync() -> SyncReturn { + SyncReturn(get_options()) +} + +pub fn main_set_options(json: String) { + let mut map: HashMap = serde_json::from_str(&json).unwrap_or(HashMap::new()); + #[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), + ); + 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, + ] { + if let Some(value) = map.remove(key) { + log::info!( + "blocked main_set_options item by policy, key={}, value={}", + key, + value + ); + } + } + } + } + if !map.is_empty() { + set_options(map) + } +} + +pub fn main_test_if_valid_server(server: String, test_with_proxy: bool) -> String { + test_if_valid_server(server, test_with_proxy) +} + +pub fn main_set_socks(proxy: String, username: String, password: String) { + set_socks(proxy, username, password) +} + +pub fn main_get_proxy_status() -> bool { + get_proxy_status() +} + +pub fn main_get_socks() -> Vec { + get_socks() +} + +pub fn main_get_app_name() -> String { + get_app_name() +} + +pub fn main_get_app_name_sync() -> SyncReturn { + SyncReturn(get_app_name()) +} + +pub fn main_uri_prefix_sync() -> SyncReturn { + SyncReturn(crate::get_uri_prefix()) +} + +pub fn main_get_license() -> String { + get_license() +} + +pub fn main_get_version() -> String { + get_version() +} + +pub fn main_get_fav() -> Vec { + get_fav() +} + +pub fn main_store_fav(favs: Vec) { + store_fav(favs) +} + +pub fn main_get_peer_sync(id: String) -> SyncReturn { + let conf = get_peer(id); + SyncReturn(serde_json::to_string(&conf).unwrap_or("".to_string())) +} + +pub fn main_get_lan_peers() -> String { + serde_json::to_string(&get_lan_peers()).unwrap_or_default() +} + +pub fn main_get_connect_status() -> String { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + serde_json::to_string(&get_connect_status()).unwrap_or("".to_string()) + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + let mut state = hbb_common::config::get_online_state(); + if state > 0 { + state = 1; + } + serde_json::json!({ "status_num": state }).to_string() + } +} + +pub fn main_check_connect_status() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + start_option_status_sync(); // avoid multi calls +} + +pub fn main_is_using_public_server() -> bool { + crate::using_public_server() +} + +pub fn main_discover() { + discover(); +} + +pub fn main_get_api_server() -> String { + get_api_server() +} + +pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn { + SyncReturn(resolve_avatar_url(avatar)) +} + +pub fn main_http_request(url: String, method: String, body: Option, header: String) { + http_request(url, method, body, header) +} + +pub fn main_get_local_option(key: String) -> SyncReturn { + SyncReturn(get_local_option(key)) +} + +pub fn main_get_use_texture_render() -> SyncReturn { + SyncReturn(use_texture_render()) +} + +pub fn main_get_env(key: String) -> SyncReturn { + SyncReturn(std::env::var(key).unwrap_or_default()) +} + +// Dart does not support changing environment variables. +// `Platform.environment['MY_VAR'] = 'VAR';` will throw an error +// `Unsupported operation: Cannot modify unmodifiable map`. +// +// And we need to share the environment variables between rust and dart isolates sometimes. +pub fn main_set_env(key: String, value: Option) -> SyncReturn<()> { + let is_valid_key = !key.is_empty() && !key.contains('=') && !key.contains('\0'); + debug_assert!(is_valid_key, "Invalid environment variable key: {}", key); + if !is_valid_key { + log::error!("Invalid environment variable key: {}", key); + return SyncReturn(()); + } + + match value { + Some(v) => { + let is_valid_value = !v.contains('\0'); + debug_assert!(is_valid_value, "Invalid environment variable value: {}", v); + if !is_valid_value { + log::error!("Invalid environment variable value: {}", v); + return SyncReturn(()); + } + std::env::set_var(key, v); + } + None => std::env::remove_var(key), + } + + 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); + set_local_option(key, value.clone()); + if is_texture_render_key { + let session_event = [("v", &value)]; + for session in sessions::get_sessions() { + session.push_event("use_texture_render", &session_event, &[]); + session.use_texture_render_changed(); + session.ui_handler.update_use_texture_render(); + } + } + if is_d3d_render_key { + for session in sessions::get_sessions() { + session.update_supported_decodings(); + } + } +} + +// We do use use `main_get_local_option` and `main_set_local_option`. +// +// 1. For get, the value is stored in the server process. +// 2. For clear, we need to need to return the error mmsg from the server process to flutter. +pub fn main_handle_wayland_screencast_restore_token(_key: String, _value: String) -> String { + #[cfg(not(target_os = "linux"))] + { + return "".to_owned(); + } + #[cfg(target_os = "linux")] + if _value == "get" { + match crate::ipc::get_wayland_screencast_restore_token(_key) { + Ok(v) => v, + Err(e) => { + log::error!("Failed to get wayland screencast restore token, {}", e); + "".to_owned() + } + } + } else if _value == "clear" { + match crate::ipc::clear_wayland_screencast_restore_token(_key.clone()) { + Ok(true) => { + set_local_option(_key, "".to_owned()); + "".to_owned() + } + Ok(false) => "Failed to clear, please try again.".to_owned(), + Err(e) => format!("Failed to clear, {}", e), + } + } else { + "".to_owned() + } +} + +pub fn main_get_input_source() -> SyncReturn { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let input_source = get_cur_session_input_source(); + #[cfg(any(target_os = "android", target_os = "ios"))] + let input_source = "".to_owned(); + SyncReturn(input_source) +} + +pub fn main_set_input_source(session_id: SessionID, value: String) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + change_input_source(session_id, value); + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + try_sync_peer_option(&session, &session_id, "input_source", None); + } + } +} + +/// Set cursor position (for pointer lock re-centering). +/// +/// # Returns +/// - `true`: cursor position was successfully set +/// - `false`: operation failed or not supported +/// +/// # Platform behavior +/// - Windows/macOS/Linux: attempts to move the cursor to (x, y) +/// - Android/iOS: no-op, always returns `false` +pub fn main_set_cursor_position(x: i32, y: i32) -> SyncReturn { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + SyncReturn(crate::set_cursor_pos(x, y)) + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + let _ = (x, y); + SyncReturn(false) + } +} + +/// Clip cursor to a rectangle (for pointer lock). +/// +/// When `enable` is true, the cursor is clipped to the rectangle defined by +/// `left`, `top`, `right`, `bottom`. When `enable` is false, the rectangle +/// values are ignored and the cursor is unclipped. +/// +/// # Returns +/// - `true`: operation succeeded or no-op completed +/// - `false`: operation failed +/// +/// # Platform behavior +/// - Windows: uses ClipCursor API to confine cursor to the specified rectangle +/// - macOS: uses CGAssociateMouseAndMouseCursorPosition for pointer lock effect; +/// the rect coordinates are ignored (only Some/None matters) +/// - Linux: no-op, always returns `true`; use pointer warping for similar effect +/// - Android/iOS: no-op, always returns `false` +pub fn main_clip_cursor( + left: i32, + top: i32, + right: i32, + bottom: i32, + enable: bool, +) -> SyncReturn { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let rect = if enable { + Some((left, top, right, bottom)) + } else { + None + }; + SyncReturn(crate::clip_cursor(rect)) + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + let _ = (left, top, right, bottom, enable); + SyncReturn(false) + } +} + +pub fn main_get_my_id() -> String { + get_id() +} + +pub fn main_get_uuid() -> String { + get_uuid() +} + +pub fn main_get_peer_option(id: String, key: String) -> String { + get_peer_option(id, key) +} + +pub fn main_get_peer_option_sync(id: String, key: String) -> SyncReturn { + SyncReturn(get_peer_option(id, key)) +} + +// Sometimes we need to get the flutter option of a peer by reading the file. +// Because the session may not be established yet. +pub fn main_get_peer_flutter_option_sync(id: String, k: String) -> SyncReturn { + SyncReturn(get_peer_flutter_option(id, k)) +} + +pub fn main_set_peer_flutter_option_sync(id: String, k: String, v: String) -> SyncReturn<()> { + set_peer_flutter_option(id, k, v); + SyncReturn(()) +} + +pub fn main_set_peer_option(id: String, key: String, value: String) { + set_peer_option(id, key, value) +} + +pub fn main_set_peer_option_sync(id: String, key: String, value: String) -> SyncReturn { + set_peer_option(id, key, value); + SyncReturn(true) +} + +pub fn main_set_peer_alias(id: String, alias: String) { + set_peer_option(id, "alias".to_owned(), alias) +} + +pub fn main_get_new_stored_peers() -> String { + let peers: Vec = config::NEW_STORED_PEER_CONFIG + .lock() + .unwrap() + .drain() + .collect(); + serde_json::to_string(&peers).unwrap_or_default() +} + +pub fn main_forget_password(id: String) { + forget_password(id) +} + +pub fn main_peer_has_password(id: String) -> bool { + peer_has_password(id) +} + +pub fn main_peer_exists(id: String) -> bool { + peer_exists(&id) +} + +fn load_recent_peers( + vec_id_modified_time_path: &Vec<(String, SystemTime, std::path::PathBuf)>, + to_end: bool, + all_peers: &mut Vec>, + from: usize, +) -> usize { + let to = if to_end { + Some(vec_id_modified_time_path.len()) + } else { + None + }; + let mut peers_next = PeerConfig::batch_peers(vec_id_modified_time_path, from, to); + // There may be less peers than the batch size. + // But no need to consider this case, because it is a rare case. + let peers = peers_next.0.drain(..).map(|(id, _, p)| peer_to_map(id, p)); + all_peers.extend(peers); + peers_next.1 +} + +pub fn main_load_recent_peers() { + let push_to_flutter = |peers, ids| { + let mut data = HashMap::from([("name", "load_recent_peers".to_owned()), ("peers", peers)]); + if let Some(ids) = ids { + data.insert("ids", ids); + } + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }; + + if !config::APP_DIR.read().unwrap().is_empty() { + let vec_id_modified_time_path = PeerConfig::get_vec_id_modified_time_path(&None); + if vec_id_modified_time_path.is_empty() { + push_to_flutter("".to_owned(), None); + return; + } + + let load_two_times = vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT + && cfg!(target_os = "windows"); + let mut all_peers = vec![]; + if load_two_times { + let next_from = load_recent_peers(&vec_id_modified_time_path, false, &mut all_peers, 0); + let rest_ids = if next_from < vec_id_modified_time_path.len() { + Some( + vec_id_modified_time_path[next_from..] + .iter() + .map(|(id, _, _)| id.clone()) + .collect::>() + .join(", "), + ) + } else { + None + }; + push_to_flutter( + serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned()), + rest_ids, + ); + let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, next_from); + } else { + let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, 0); + } + // Don't check if `all_peers` is empty, because we need this message to update the state in the flutter side. + push_to_flutter( + serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned()), + None, + ); + } else { + push_to_flutter("".to_owned(), None) + } +} + +pub fn main_load_recent_peers_for_ab(filter: String) -> String { + let id_filters = serde_json::from_str::>(&filter).unwrap_or_default(); + let id_filters = if id_filters.is_empty() { + None + } else { + Some(id_filters) + }; + if !config::APP_DIR.read().unwrap().is_empty() { + let peers: Vec> = PeerConfig::peers(id_filters) + .drain(..) + .map(|(id, _, p)| peer_to_map(id, p)) + .collect(); + return serde_json::ser::to_string(&peers).unwrap_or("".to_owned()); + } + "".to_string() +} + +pub fn main_load_fav_peers() { + let push_to_flutter = |peers| { + let data = HashMap::from([("name", "load_fav_peers".to_owned()), ("peers", peers)]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }; + if !config::APP_DIR.read().unwrap().is_empty() { + let favs = get_fav(); + let mut recent = PeerConfig::peers(Some(favs.clone())); + let mut lan = config::LanPeers::load() + .peers + .iter() + .filter(|d| favs.contains(&d.id) && recent.iter().all(|r| r.0 != d.id)) + .map(|d| { + ( + d.id.clone(), + SystemTime::UNIX_EPOCH, + PeerConfig { + info: PeerInfoSerde { + username: d.username.clone(), + hostname: d.hostname.clone(), + platform: d.platform.clone(), + }, + ..Default::default() + }, + ) + }) + .collect(); + recent.append(&mut lan); + let peers: Vec> = recent + .into_iter() + .map(|(id, _, p)| peer_to_map(id, p)) + .collect(); + + push_to_flutter(serde_json::ser::to_string(&peers).unwrap_or("".to_owned())); + } else { + push_to_flutter("".to_owned()); + } +} + +pub fn main_load_lan_peers() { + let data = HashMap::from([ + ("name", "load_lan_peers".to_owned()), + ( + "peers", + serde_json::to_string(&get_lan_peers()).unwrap_or_default(), + ), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); +} + +pub fn main_remove_discovered(id: String) { + remove_discovered(id); +} + +fn main_broadcast_message(data: &HashMap<&str, &str>) { + let event = serde_json::ser::to_string(&data).unwrap_or("".to_owned()); + for app in flutter::get_global_event_channels() { + if app == flutter::APP_TYPE_MAIN || app == flutter::APP_TYPE_CM { + continue; + } + let _res = flutter::push_global_event(&app, event.clone()); + } +} + +pub fn main_change_theme(dark: String) { + main_broadcast_message(&HashMap::from([("name", "theme"), ("dark", &dark)])); + #[cfg(not(any(target_os = "ios")))] + send_to_cm(&crate::ipc::Data::Theme(dark)); +} + +pub fn main_change_language(lang: String) { + main_broadcast_message(&HashMap::from([("name", "language"), ("lang", &lang)])); + #[cfg(not(any(target_os = "ios")))] + send_to_cm(&crate::ipc::Data::Language(lang)); +} + +pub fn main_video_save_directory(root: bool) -> SyncReturn { + SyncReturn(video_save_directory(root)) +} + +pub fn main_set_user_default_option(key: String, value: String) { + set_user_default_option(key, value); +} + +pub fn main_get_user_default_option(key: String) -> SyncReturn { + SyncReturn(get_user_default_option(key)) +} + +pub fn main_handle_relay_id(id: String) -> String { + handle_relay_id(&id).to_owned() +} + +pub fn main_is_option_fixed(key: String) -> SyncReturn { + SyncReturn(is_option_fixed(&key)) +} + +pub fn main_get_main_display() -> SyncReturn { + #[cfg(target_os = "ios")] + let display_info = "".to_owned(); + #[cfg(not(target_os = "ios"))] + let mut display_info = "".to_owned(); + #[cfg(not(target_os = "ios"))] + { + #[cfg(not(target_os = "linux"))] + let is_linux_wayland = false; + #[cfg(target_os = "linux")] + let is_linux_wayland = !is_x11(); + + if !is_linux_wayland { + if let Ok(displays) = crate::display_service::try_get_displays() { + // to-do: Need to detect current display index. + if let Some(display) = displays.iter().next() { + display_info = serde_json::to_string(&HashMap::from([ + ("w", display.width()), + ("h", display.height()), + ])) + .unwrap_or_default(); + } + } + } + + #[cfg(target_os = "linux")] + if is_linux_wayland { + let displays = scrap::wayland::display::get_displays(); + if let Some(display) = displays.displays.get(displays.primary) { + let logical_size = display + .logical_size + .unwrap_or((display.width, display.height)); + display_info = serde_json::to_string(&HashMap::from([ + ("w", logical_size.0), + ("h", logical_size.1), + ])) + .unwrap_or_default(); + } + } + } + SyncReturn(display_info) +} + +// No need to check if is on Wayland in this function. +// The Flutter side gets display information on Wayland using a different method. +pub fn main_get_displays() -> SyncReturn { + #[cfg(target_os = "ios")] + let display_info = "".to_owned(); + #[cfg(not(target_os = "ios"))] + let mut display_info = "".to_owned(); + #[cfg(not(target_os = "ios"))] + if let Ok(displays) = crate::display_service::try_get_displays() { + let displays = displays + .iter() + .map(|d| { + HashMap::from([ + ("x", d.origin().0), + ("y", d.origin().1), + ("w", d.width() as i32), + ("h", d.height() as i32), + ]) + }) + .collect::>(); + display_info = serde_json::to_string(&displays).unwrap_or_default(); + } + SyncReturn(display_info) +} + +pub fn session_add_port_forward( + session_id: SessionID, + local_port: i32, + remote_host: String, + remote_port: i32, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.add_port_forward(local_port, remote_host, remote_port); + } +} + +pub fn session_remove_port_forward(session_id: SessionID, local_port: i32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.remove_port_forward(local_port); + } +} + +pub fn session_new_rdp(session_id: SessionID) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.new_rdp(); + } +} + +pub fn session_request_voice_call(session_id: SessionID) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.request_voice_call(); + } +} + +pub fn session_close_voice_call(session_id: SessionID) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.close_voice_call(); + } +} + +pub fn session_get_conn_token(session_id: SessionID) -> SyncReturn> { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.get_conn_token()) + } else { + SyncReturn(None) + } +} + +pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) { + crate::ui_cm_interface::handle_incoming_voice_call(id, accept); +} + +pub fn cm_close_voice_call(id: i32) { + crate::ui_cm_interface::close_voice_call(id); +} + +pub fn set_voice_call_input_device(_is_cm: bool, _device: String) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if _is_cm { + let _ = crate::ipc::set_config("voice-call-input", _device); + } else { + crate::audio_service::set_voice_call_input_device(Some(_device), true); + } +} + +pub fn get_voice_call_input_device(_is_cm: bool) -> String { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if _is_cm { + match crate::ipc::get_config("voice-call-input") { + Ok(Some(device)) => device, + _ => "".to_owned(), + } + } else { + crate::audio_service::get_voice_call_input_device().unwrap_or_default() + } + #[cfg(any(target_os = "android", target_os = "ios"))] + "".to_owned() +} + +pub fn main_get_last_remote_id() -> String { + LocalConfig::get_remote_id() +} + +pub fn main_get_software_update_url() { + crate::common::check_software_update(); +} + +pub fn main_get_home_dir() -> String { + fs::get_home_as_string() +} + +pub fn main_get_langs() -> String { + get_langs() +} + +pub fn main_get_temporary_password() -> String { + ui_interface::temporary_password() +} + +pub fn main_set_permanent_password_with_result(password: String) -> bool { + ui_interface::set_permanent_password_with_result(password) +} + +pub fn main_get_fingerprint() -> String { + get_fingerprint() +} + +pub fn cm_get_clients_state() -> String { + crate::ui_cm_interface::get_clients_state() +} + +pub fn cm_check_clients_length(length: usize) -> Option { + if length != crate::ui_cm_interface::get_clients_length() { + Some(crate::ui_cm_interface::get_clients_state()) + } else { + None + } +} + +pub fn cm_get_clients_length() -> usize { + crate::ui_cm_interface::get_clients_length() +} + +pub fn main_init(app_dir: String, custom_client_config: String) { + initialize(&app_dir, &custom_client_config); +} + +pub fn main_device_id(id: String) { + *crate::common::DEVICE_ID.lock().unwrap() = id; +} + +pub fn main_device_name(name: String) { + *crate::common::DEVICE_NAME.lock().unwrap() = name; +} + +pub fn main_remove_peer(id: String) { + PeerConfig::remove(&id); +} + +pub fn main_has_hwcodec() -> SyncReturn { + SyncReturn(has_hwcodec()) +} + +pub fn main_has_vram() -> SyncReturn { + SyncReturn(has_vram()) +} + +pub fn main_supported_hwdecodings() -> SyncReturn { + let decoding = supported_hwdecodings(); + let msg = HashMap::from([("h264", decoding.0), ("h265", decoding.1)]); + + SyncReturn(serde_json::ser::to_string(&msg).unwrap_or("".to_owned())) +} + +pub fn main_is_root() -> bool { + is_root() +} + +pub fn get_double_click_time() -> SyncReturn { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + return SyncReturn(crate::platform::get_double_click_time() as _); + } + #[cfg(any(target_os = "android", target_os = "ios"))] + SyncReturn(500i32) +} + +pub fn main_start_dbus_server() { + #[cfg(target_os = "linux")] + { + use crate::dbus::start_dbus_server; + // spawn new thread to start dbus server + std::thread::spawn(|| { + let _ = start_dbus_server(); + }); + } +} + +pub fn main_save_ab(json: String) { + if json.len() > 1024 { + std::thread::spawn(|| { + config::Ab::store(json); + }); + } else { + config::Ab::store(json); + } +} + +pub fn main_clear_ab() { + config::Ab::remove(); +} + +pub fn main_load_ab() -> String { + serde_json::to_string(&config::Ab::load()).unwrap_or_default() +} + +pub fn main_save_group(json: String) { + if json.len() > 1024 { + std::thread::spawn(|| { + config::Group::store(json); + }); + } else { + config::Group::store(json); + } +} + +pub fn main_clear_group() { + config::Group::remove(); +} + +pub fn main_load_group() -> String { + serde_json::to_string(&config::Group::load()).unwrap_or_default() +} + +pub fn session_send_pointer(session_id: SessionID, msg: String) { + super::flutter::session_send_pointer(session_id, msg); +} + +/// Send mouse event from Flutter to the remote peer. +/// +/// # Relative Mouse Mode Message Contract +/// +/// When the message contains a `relative_mouse_mode` field, this function validates +/// and filters activation/deactivation markers. +/// +/// **Mode Authority:** +/// The Flutter InputModel is authoritative for relative mouse mode activation/deactivation. +/// The server (via `input_service.rs`) only consumes forwarded delta movements and tracks +/// relative movement processing state, but does NOT control mode activation/deactivation. +/// +/// **Deactivation Markers are Local-Only:** +/// Deactivation markers (`relative_mouse_mode: "0"`) are NEVER forwarded to the server. +/// They are handled entirely on the client side to reset local UI state (cursor visibility, +/// pointer lock, etc.). The server does not rely on deactivation markers and should not +/// expect to receive them. +/// +/// **Contract (Flutter side MUST adhere to):** +/// 1. `relative_mouse_mode` field is ONLY present on activation/deactivation marker messages, +/// NEVER on normal pointer events (move, button, scroll). +/// 2. Deactivation marker: `{"relative_mouse_mode": "0"}` - local-only, never forwarded. +/// 3. Activation marker: `{"relative_mouse_mode": "1", "type": "move_relative", "x": "0", "y": "0"}` +/// - MUST use `type="move_relative"` with `x="0"` and `y="0"` (safe no-op). +/// - Any other combination is dropped to prevent accidental cursor movement. +/// +/// If these assumptions are violated (e.g., `relative_mouse_mode` is added to normal events), +/// legitimate mouse events may be silently dropped by the early-return logic below. +pub fn session_send_mouse(session_id: SessionID, msg: String) { + if let Ok(m) = serde_json::from_str::>(&msg) { + // Relative mouse mode marker validation (Flutter-only). + // This only validates and filters markers; the server tracks per-connection + // relative-movement processing state but not mode activation/deactivation. + // See doc comment above for the message contract. + if let Some(v) = m.get("relative_mouse_mode") { + let active = matches!(v.as_str(), "1" | "Y" | "on"); + + // Disable marker: local-only, never forwarded to the server. + // The server does not track mode deactivation; it simply stops receiving + // relative move events when the client exits relative mouse mode. + if !active { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::keyboard::set_relative_mouse_mode_state(false); + return; + } + + // Enable marker: validate BEFORE setting state to avoid desync. + // This ensures we only mark as active if the marker will actually be forwarded. + + // Enable marker is allowed to go through only if it's a safe no-op relative move. + // This avoids accidentally moving the remote cursor (e.g. if type/x/y are missing). + let msg_type = m.get("type").map(|t| t.as_str()); + if msg_type != Some("move_relative") { + log::warn!( + "relative_mouse_mode activation marker has invalid type: {:?}, expected 'move_relative'. Dropping.", + msg_type + ); + return; + } + let x_marker = m + .get("x") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let y_marker = m + .get("y") + .map(|y| y.parse::().unwrap_or(0)) + .unwrap_or(0); + if x_marker != 0 || y_marker != 0 { + log::warn!( + "relative_mouse_mode activation marker has non-zero coordinates: x={}, y={}. Dropping.", + x_marker, y_marker + ); + return; + } + + // Guard against unexpected fields that could turn this no-op into a real event. + if m.contains_key("buttons") + || m.contains_key("alt") + || m.contains_key("ctrl") + || m.contains_key("shift") + || m.contains_key("command") + { + log::warn!( + "relative_mouse_mode activation marker contains unexpected fields (buttons/alt/ctrl/shift/command). Dropping." + ); + return; + } + + // All validation passed - marker will be forwarded as a no-op relative move. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::keyboard::set_relative_mouse_mode_state(true); + } + + let alt = m.get("alt").is_some(); + let ctrl = m.get("ctrl").is_some(); + let shift = m.get("shift").is_some(); + let command = m.get("command").is_some(); + let x = m + .get("x") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let y = m + .get("y") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let mut mask = 0; + if let Some(_type) = m.get("type") { + mask = match _type.as_str() { + "down" => MOUSE_TYPE_DOWN, + "up" => MOUSE_TYPE_UP, + "wheel" => MOUSE_TYPE_WHEEL, + "trackpad" => MOUSE_TYPE_TRACKPAD, + "move_relative" => MOUSE_TYPE_MOVE_RELATIVE, + _ => 0, + }; + } + if let Some(buttons) = m.get("buttons") { + mask |= match buttons.as_str() { + "left" => MOUSE_BUTTON_LEFT, + "right" => MOUSE_BUTTON_RIGHT, + "wheel" => MOUSE_BUTTON_WHEEL, + "back" => MOUSE_BUTTON_BACK, + "forward" => MOUSE_BUTTON_FORWARD, + _ => 0, + } << 3; + } + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.send_mouse(mask, x, y, alt, ctrl, shift, command); + } + } +} + +pub fn session_restart_remote_device(session_id: SessionID) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.restart_remote_device(); + } +} + +pub fn session_get_audit_server_sync(session_id: SessionID, typ: String) -> SyncReturn { + let res = if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.get_audit_server(typ) + } else { + "".to_owned() + }; + SyncReturn(res) +} + +pub fn session_send_note(session_id: SessionID, note: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.send_note(note) + } +} + +pub fn session_get_last_audit_note(session_id: SessionID) -> SyncReturn { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.last_audit_note.lock().unwrap().clone()) + } else { + SyncReturn("".to_owned()) + } +} + +pub fn session_set_audit_guid(session_id: SessionID, guid: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + *session.audit_guid.lock().unwrap() = guid; + } +} + +pub fn session_get_audit_guid(session_id: SessionID) -> SyncReturn { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.audit_guid.lock().unwrap().clone()) + } else { + SyncReturn("".to_owned()) + } +} + +pub fn session_get_conn_session_id(session_id: SessionID) -> SyncReturn { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.lc.read().unwrap().session_id.to_string()) + } else { + SyncReturn("".to_owned()) + } +} + +pub fn session_alternative_codecs(session_id: SessionID) -> String { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + let (vp8, av1, h264, h265) = session.alternative_codecs(); + let msg = HashMap::from([("vp8", vp8), ("av1", av1), ("h264", h264), ("h265", h265)]); + serde_json::ser::to_string(&msg).unwrap_or("".to_owned()) + } else { + String::new() + } +} + +pub fn session_change_prefer_codec(session_id: SessionID) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.update_supported_decodings(); + } +} + +pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) { + super::flutter::session_on_waiting_for_image_dialog_show(session_id); +} + +pub fn session_toggle_virtual_display(session_id: SessionID, index: i32, on: bool) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.toggle_virtual_display(index, on); + flutter::session_update_virtual_display(&session, index, on); + } +} + +pub fn session_printer_response( + session_id: SessionID, + id: i32, + path: String, + printer_name: String, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.printer_response(id, path, printer_name); + } +} + +pub fn main_set_home_dir(_home: String) { + #[cfg(any(target_os = "android", target_os = "ios"))] + { + *config::APP_HOME_DIR.write().unwrap() = _home; + } +} + +// This is a temporary method to get data dir for ios +pub fn main_get_data_dir_ios(app_dir: String) -> SyncReturn { + *config::APP_DIR.write().unwrap() = app_dir; + let data_dir = config::Config::path("data"); + if !data_dir.exists() { + if let Err(e) = std::fs::create_dir_all(&data_dir) { + log::warn!("Failed to create data dir {}", e); + } + } + SyncReturn(data_dir.to_string_lossy().to_string()) +} + +pub fn main_stop_service() { + #[cfg(target_os = "android")] + { + config::Config::set_option("stop-service".into(), "Y".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } +} + +pub fn main_start_service() { + #[cfg(target_os = "android")] + { + config::Config::set_option("stop-service".into(), "".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } +} + +pub fn main_update_temporary_password() { + update_temporary_password(); +} + +pub fn main_check_super_user_permission() -> bool { + check_super_user_permission() +} + +pub fn main_get_unlock_pin() -> SyncReturn { + SyncReturn(get_unlock_pin()) +} + +pub fn main_set_unlock_pin(pin: String) -> SyncReturn { + SyncReturn(set_unlock_pin(pin)) +} + +pub fn main_check_mouse_time() { + check_mouse_time(); +} + +pub fn main_get_mouse_time() -> f64 { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + get_mouse_time() + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + 0.0 + } +} + +pub fn main_wol(id: String) { + // TODO: move send_wol outside. + #[cfg(not(any(target_os = "ios")))] + crate::lan::send_wol(id) +} + +pub fn main_create_shortcut(_id: String) { + #[cfg(windows)] + create_shortcut(_id); +} + +pub fn cm_send_chat(conn_id: i32, msg: String) { + #[cfg(not(any(target_os = "ios")))] + crate::ui_cm_interface::send_chat(conn_id, msg); +} + +pub fn cm_login_res(conn_id: i32, res: bool) { + #[cfg(not(any(target_os = "ios")))] + if res { + crate::ui_cm_interface::authorize(conn_id); + } else { + crate::ui_cm_interface::close(conn_id); + } +} + +pub fn cm_close_connection(conn_id: i32) { + #[cfg(not(any(target_os = "ios")))] + crate::ui_cm_interface::close(conn_id); +} + +pub fn cm_remove_disconnected_connection(conn_id: i32) { + #[cfg(not(any(target_os = "ios")))] + crate::ui_cm_interface::remove(conn_id); +} + +pub fn cm_check_click_time(conn_id: i32) { + #[cfg(not(any(target_os = "ios")))] + crate::ui_cm_interface::check_click_time(conn_id) +} + +pub fn cm_get_click_time() -> f64 { + #[cfg(not(any(target_os = "ios")))] + return crate::ui_cm_interface::get_click_time() as _; + #[cfg(any(target_os = "ios"))] + return 0 as _; +} + +pub fn cm_switch_permission(conn_id: i32, name: String, enabled: bool) { + #[cfg(not(any(target_os = "ios")))] + crate::ui_cm_interface::switch_permission(conn_id, name, enabled) +} + +pub fn cm_can_elevate() -> SyncReturn { + SyncReturn(crate::ui_cm_interface::can_elevate()) +} + +pub fn cm_elevate_portable(conn_id: i32) { + #[cfg(not(any(target_os = "ios")))] + crate::ui_cm_interface::elevate_portable(conn_id); +} + +pub fn cm_switch_back(conn_id: i32) { + #[cfg(not(any(target_os = "ios")))] + crate::ui_cm_interface::switch_back(conn_id); +} + +pub fn cm_get_config(name: String) -> String { + #[cfg(not(target_os = "ios"))] + { + if let Ok(Some(v)) = crate::ipc::get_config(&name) { + v + } else { + "".to_string() + } + } + #[cfg(target_os = "ios")] + { + "".to_string() + } +} + +pub fn main_get_build_date() -> String { + crate::BUILD_DATE.to_string() +} + +pub fn translate(name: String, locale: String) -> SyncReturn { + SyncReturn(crate::client::translate_locale(name, &locale)) +} + +pub fn session_get_rgba_size(session_id: SessionID, display: usize) -> SyncReturn { + SyncReturn(super::flutter::session_get_rgba_size(session_id, display)) +} + +pub fn session_next_rgba(session_id: SessionID, display: usize) -> SyncReturn<()> { + SyncReturn(super::flutter::session_next_rgba(session_id, display)) +} + +pub fn session_register_pixelbuffer_texture( + session_id: SessionID, + display: usize, + ptr: usize, +) -> SyncReturn<()> { + SyncReturn(super::flutter::session_register_pixelbuffer_texture( + session_id, display, ptr, + )) +} + +pub fn session_register_gpu_texture( + session_id: SessionID, + display: usize, + ptr: usize, +) -> SyncReturn<()> { + SyncReturn(super::flutter::session_register_gpu_texture( + session_id, display, ptr, + )) +} + +pub fn query_onlines(ids: Vec) { + let _ = flutter::async_tasks::query_onlines(ids); +} + +pub fn version_to_number(v: String) -> SyncReturn { + SyncReturn(hbb_common::get_version_number(&v)) +} + +pub fn option_synced() -> bool { + crate::ui_interface::option_synced() +} + +pub fn main_is_installed() -> SyncReturn { + SyncReturn(is_installed()) +} + +pub fn main_init_input_source() -> SyncReturn<()> { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::keyboard::input_source::init_input_source(); + SyncReturn(()) +} + +pub fn main_is_installed_lower_version() -> SyncReturn { + SyncReturn(is_installed_lower_version()) +} + +pub fn main_is_installed_daemon(prompt: bool) -> SyncReturn { + SyncReturn(is_installed_daemon(prompt)) +} + +pub fn main_is_process_trusted(prompt: bool) -> SyncReturn { + SyncReturn(is_process_trusted(prompt)) +} + +pub fn main_is_can_screen_recording(prompt: bool) -> SyncReturn { + SyncReturn(is_can_screen_recording(prompt)) +} + +pub fn main_is_can_input_monitoring(prompt: bool) -> SyncReturn { + SyncReturn(is_can_input_monitoring(prompt)) +} + +pub fn main_is_share_rdp() -> SyncReturn { + SyncReturn(is_share_rdp()) +} + +pub fn main_set_share_rdp(enable: bool) { + set_share_rdp(enable) +} + +pub fn main_goto_install() -> SyncReturn { + goto_install(); + SyncReturn(true) +} + +pub fn main_get_new_version() -> SyncReturn { + SyncReturn(get_new_version()) +} + +pub fn main_update_me() -> SyncReturn { + update_me("".to_owned()); + SyncReturn(true) +} + +pub fn set_cur_session_id(session_id: SessionID) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + set_cur_session_id_(session_id, &session.get_keyboard_mode()) + } +} + +fn set_cur_session_id_(session_id: SessionID, _keyboard_mode: &str) { + super::flutter::set_cur_session_id(session_id); + #[cfg(windows)] + crate::keyboard::update_grab_get_key_name(_keyboard_mode); +} + +pub fn install_show_run_without_install() -> SyncReturn { + SyncReturn(show_run_without_install()) +} + +pub fn install_run_without_install() { + run_without_install(); +} + +pub fn install_install_me(options: String, path: String) { + install_me(options, path, false, false); +} + +pub fn install_install_path() -> SyncReturn { + SyncReturn(install_path()) +} + +pub fn install_install_options() -> SyncReturn { + SyncReturn(install_options()) +} + +pub fn main_account_auth(op: String, remember_me: bool) { + let id = get_id(); + let uuid = get_uuid(); + account_auth(op, id, uuid, remember_me); +} + +pub fn main_account_auth_cancel() { + account_auth_cancel() +} + +pub fn main_account_auth_result() -> String { + account_auth_result() +} + +pub fn main_on_main_window_close() { + // may called more than one times + #[cfg(windows)] + crate::portable_service::client::drop_portable_service_shared_memory(); +} + +pub fn main_current_is_wayland() -> SyncReturn { + SyncReturn(current_is_wayland()) +} + +pub fn main_is_login_wayland() -> SyncReturn { + SyncReturn(is_login_wayland()) +} + +pub fn main_hide_dock() -> SyncReturn { + #[cfg(target_os = "macos")] + crate::platform::macos::hide_dock(); + SyncReturn(true) +} + +pub fn main_has_file_clipboard() -> SyncReturn { + let ret = cfg!(any(target_os = "windows", feature = "unix-file-copy-paste",)); + SyncReturn(ret) +} + +pub fn main_has_gpu_texture_render() -> SyncReturn { + SyncReturn(cfg!(feature = "vram")) +} + +pub fn cm_init() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::flutter::connection_manager::cm_init(); +} + +/// Start an ipc server for receiving the url scheme. +/// +/// * Should only be called in the main flutter window. +/// * macOS only +pub fn main_start_ipc_url_server() { + #[cfg(target_os = "macos")] + std::thread::spawn(move || crate::server::start_ipc_url_server()); +} + +pub fn main_test_wallpaper(_second: u64) { + #[cfg(any(target_os = "windows", target_os = "linux"))] + std::thread::spawn(move || match crate::platform::WallPaperRemover::new() { + Ok(_remover) => { + std::thread::sleep(std::time::Duration::from_secs(_second)); + } + Err(e) => { + log::info!("create wallpaper remover failed: {:?}", e); + } + }); +} + +pub fn main_support_remove_wallpaper() -> bool { + support_remove_wallpaper() +} + +pub fn is_incoming_only() -> SyncReturn { + SyncReturn(config::is_incoming_only()) +} + +pub fn is_outgoing_only() -> SyncReturn { + SyncReturn(config::is_outgoing_only()) +} + +pub fn is_custom_client() -> SyncReturn { + SyncReturn(crate::common::is_custom_client()) +} + +pub fn is_disable_settings() -> SyncReturn { + SyncReturn(config::is_disable_settings()) +} + +pub fn is_disable_ab() -> SyncReturn { + SyncReturn(config::is_disable_ab()) +} + +pub fn is_disable_account() -> SyncReturn { + SyncReturn(config::is_disable_account()) +} + +pub fn is_disable_group_panel() -> SyncReturn { + SyncReturn(LocalConfig::get_option("disable-group-panel") == "Y") +} + +// windows only +pub fn is_disable_installation() -> SyncReturn { + SyncReturn(config::is_disable_installation()) +} + +pub fn is_preset_password() -> bool { + let hard = config::HARD_SETTINGS + .read() + .unwrap() + .get("password") + .cloned() + .unwrap_or_default(); + if hard.is_empty() { + return false; + } + + // On desktop, service owns the authoritative config; query it via IPC and return only a boolean. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return crate::ipc::is_permanent_password_preset(); + + // On mobile, we have no service IPC; verify against local storage. + #[cfg(any(target_os = "android", target_os = "ios"))] + return config::Config::matches_permanent_password_plain(&hard); +} + +// Don't call this function for desktop version. +// We need this function because we want a sync return for mobile version. +pub fn is_preset_password_mobile_only() -> SyncReturn { + SyncReturn(is_preset_password()) +} + +/// Send a url scheme through the ipc. +/// +/// * macOS only +#[allow(unused_variables)] +pub fn send_url_scheme(_url: String) { + #[cfg(target_os = "macos")] + std::thread::spawn(move || crate::handle_url_scheme(_url)); +} + +#[inline] +pub fn plugin_event(_id: String, _peer: String, _event: Vec) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + allow_err!(crate::plugin::handle_ui_event(&_id, &_peer, &_event)); + } +} + +pub fn plugin_register_event_stream(_id: String, _event2ui: StreamSink) { + #[cfg(feature = "plugin_framework")] + { + crate::plugin::native_handlers::session::session_register_event_stream(_id, _event2ui); + } +} + +#[inline] +pub fn plugin_get_session_option( + _id: String, + _peer: String, + _key: String, +) -> SyncReturn> { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + SyncReturn(crate::plugin::PeerConfig::get(&_id, &_peer, &_key)) + } + #[cfg(any( + not(feature = "plugin_framework"), + target_os = "android", + target_os = "ios" + ))] + { + SyncReturn(None) + } +} + +#[inline] +pub fn plugin_set_session_option(_id: String, _peer: String, _key: String, _value: String) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let _res = crate::plugin::PeerConfig::set(&_id, &_peer, &_key, &_value); + } +} + +#[inline] +pub fn plugin_get_shared_option(_id: String, _key: String) -> SyncReturn> { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + SyncReturn(crate::plugin::ipc::get_config(&_id, &_key).unwrap_or(None)) + } + #[cfg(any( + not(feature = "plugin_framework"), + target_os = "android", + target_os = "ios" + ))] + { + SyncReturn(None) + } +} + +#[inline] +pub fn plugin_set_shared_option(_id: String, _key: String, _value: String) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + allow_err!(crate::plugin::ipc::set_config(&_id, &_key, _value)); + } +} + +#[inline] +pub fn plugin_reload(_id: String) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + allow_err!(crate::plugin::ipc::reload_plugin(&_id,)); + allow_err!(crate::plugin::reload_plugin(&_id)); + } +} + +#[inline] +pub fn plugin_enable(_id: String, _v: bool) -> SyncReturn<()> { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + allow_err!(crate::plugin::ipc::set_manager_plugin_config( + &_id, + "enabled", + _v.to_string() + )); + if _v { + allow_err!(crate::plugin::load_plugin(&_id)); + } else { + crate::plugin::unload_plugin(&_id); + } + } + SyncReturn(()) +} + +pub fn plugin_is_enabled(_id: String) -> SyncReturn { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + SyncReturn( + match crate::plugin::ipc::get_manager_plugin_config(&_id, "enabled") { + Ok(Some(enabled)) => bool::from_str(&enabled).unwrap_or(false), + _ => false, + }, + ) + } + #[cfg(any( + not(feature = "plugin_framework"), + target_os = "android", + target_os = "ios" + ))] + { + SyncReturn(false) + } +} + +pub fn plugin_feature_is_enabled() -> SyncReturn { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + #[cfg(debug_assertions)] + let enabled = true; + #[cfg(not(debug_assertions))] + let enabled = is_installed(); + SyncReturn(enabled) + } + #[cfg(any( + not(feature = "plugin_framework"), + target_os = "android", + target_os = "ios" + ))] + { + SyncReturn(false) + } +} + +pub fn plugin_sync_ui(_sync_to: String) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if plugin_feature_is_enabled().0 { + crate::plugin::sync_ui(_sync_to); + } + } +} + +pub fn plugin_list_reload() { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + crate::plugin::load_plugin_list(); + } +} + +pub fn plugin_install(_id: String, _b: bool) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if _b { + if let Err(e) = crate::plugin::install_plugin(&_id) { + log::error!("Failed to install plugin '{}': {}", _id, e); + } + } else { + crate::plugin::uninstall_plugin(&_id, true); + } + } +} + +pub fn is_support_multi_ui_session(version: String) -> SyncReturn { + SyncReturn(crate::common::is_support_multi_ui_session(&version)) +} + +pub fn is_selinux_enforcing() -> SyncReturn { + #[cfg(target_os = "linux")] + { + SyncReturn(crate::platform::linux::is_selinux_enforcing()) + } + #[cfg(not(target_os = "linux"))] + { + SyncReturn(false) + } +} + +pub fn main_default_privacy_mode_impl() -> SyncReturn { + SyncReturn(crate::privacy_mode::DEFAULT_PRIVACY_MODE_IMPL.to_owned()) +} + +pub fn main_supported_privacy_mode_impls() -> SyncReturn { + SyncReturn( + serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl()) + .unwrap_or_default(), + ) +} + +pub fn main_supported_input_source() -> SyncReturn { + #[cfg(any(target_os = "android", target_os = "ios"))] + { + SyncReturn("".to_owned()) + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + SyncReturn( + serde_json::to_string(&crate::keyboard::input_source::get_supported_input_source()) + .unwrap_or_default(), + ) + } +} + +pub fn main_generate2fa() -> String { + generate2fa() +} + +pub fn main_verify2fa(code: String) -> bool { + verify2fa(code) +} + +pub fn main_has_valid_2fa_sync() -> SyncReturn { + SyncReturn(has_valid_2fa()) +} + +pub fn main_verify_bot(token: String) -> String { + verify_bot(token) +} + +pub fn main_has_valid_bot_sync() -> SyncReturn { + SyncReturn(has_valid_bot()) +} + +pub fn main_get_hard_option(key: String) -> SyncReturn { + SyncReturn(get_hard_option(key)) +} + +pub fn main_get_buildin_option(key: String) -> SyncReturn { + SyncReturn(get_builtin_option(&key)) +} + +pub fn main_check_hwcodec() { + check_hwcodec() +} + +pub fn main_get_trusted_devices() -> String { + get_trusted_devices() +} + +pub fn main_remove_trusted_devices(json: String) { + remove_trusted_devices(&json) +} + +pub fn main_clear_trusted_devices() { + clear_trusted_devices() +} + +pub fn main_max_encrypt_len() -> SyncReturn { + SyncReturn(max_encrypt_len()) +} + +pub fn session_request_new_display_init_msgs(session_id: SessionID, display: usize) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.request_init_msgs(display); + } +} + +pub fn main_audio_support_loopback() -> SyncReturn { + #[cfg(target_os = "windows")] + let is_surpport = true; + #[cfg(feature = "screencapturekit")] + let is_surpport = crate::audio_service::is_screen_capture_kit_available(); + #[cfg(not(any(target_os = "windows", feature = "screencapturekit")))] + let is_surpport = false; + SyncReturn(is_surpport) +} + +pub fn main_get_printer_names() -> SyncReturn { + #[cfg(target_os = "windows")] + return SyncReturn( + serde_json::to_string(&crate::platform::windows::get_printer_names().unwrap_or_default()) + .unwrap_or_default(), + ); + #[cfg(not(target_os = "windows"))] + return SyncReturn("".to_owned()); +} + +pub fn main_get_common(key: String) -> String { + if key == "is-printer-installed" { + #[cfg(target_os = "windows")] + { + return match remote_printer::is_rd_printer_installed(&get_app_name()) { + Ok(r) => r.to_string(), + Err(e) => e.to_string(), + }; + } + #[cfg(not(target_os = "windows"))] + return false.to_string(); + } else if key == "is-support-printer-driver" { + #[cfg(target_os = "windows")] + return crate::platform::is_win_10_or_greater().to_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(); + } else if key == "is-remote-modify-enabled-by-control-permissions" { + return match is_remote_modify_enabled_by_control_permissions() { + Some(true) => "true", + Some(false) => "false", + None => "", + } + .to_string(); + } else if key == "has-gnome-shortcuts-inhibitor-permission" { + #[cfg(target_os = "linux")] + return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string(); + #[cfg(not(target_os = "linux"))] + return false.to_string(); + } else if key == "permanent-password-set" { + return ui_interface::is_permanent_password_set().to_string(); + } else if key == "local-permanent-password-set" { + return ui_interface::is_local_permanent_password_set().to_string(); + } else { + if key.starts_with("download-data-") { + let id = key.replace("download-data-", ""); + match crate::hbbs_http::downloader::get_download_data(&id) { + Ok(data) => serde_json::to_string(&data).unwrap_or_default(), + Err(e) => { + format!("error:{}", e) + } + } + } else if key.starts_with("download-file-") { + let _version = key.replace("download-file-", ""); + #[cfg(target_os = "windows")] + return match ( + crate::platform::windows::is_msi_installed(), + crate::common::is_custom_client(), + ) { + (Ok(true), false) => format!("rustdesk-{_version}-x86_64.msi"), + (Ok(true), true) | (Ok(false), _) => format!("rustdesk-{_version}-x86_64.exe"), + (Err(e), _) => { + log::error!("Failed to check if is msi: {}", e); + format!("error:update-failed-check-msi-tip") + } + }; + #[cfg(target_os = "macos")] + { + return if cfg!(target_arch = "x86_64") { + format!("rustdesk-{_version}-x86_64.dmg") + } else if cfg!(target_arch = "aarch64") { + format!("rustdesk-{_version}-aarch64.dmg") + } else { + "error:unsupported".to_owned() + }; + } + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + { + "error:unsupported".to_owned() + } + } else { + "".to_owned() + } + } +} + +pub fn main_get_common_sync(key: String) -> SyncReturn { + SyncReturn(main_get_common(key)) +} + +pub fn main_set_common(_key: String, _value: String) { + #[cfg(target_os = "windows")] + if _key == "install-printer" && crate::platform::is_win_10_or_greater() { + std::thread::spawn(move || { + let (success, msg) = match remote_printer::install_update_printer(&get_app_name()) { + Ok(_) => (true, "".to_owned()), + Err(e) => { + let err = e.to_string(); + log::error!("Failed to install/update rd printer: {}", &err); + (false, err) + } + }; + if success { + // Use `ipc` to notify the server process to update the install option in the registry. + // Because `install_update_printer()` may prompt for permissions, there is no need to prompt again here. + if let Err(e) = crate::ipc::set_install_option( + crate::platform::REG_NAME_INSTALL_PRINTER.to_string(), + "1".to_string(), + ) { + log::error!("Failed to set install printer option: {}", e); + } + } + let data = HashMap::from([ + ("name", serde_json::json!("install-printer-res")), + ("success", serde_json::json!(success)), + ("msg", serde_json::json!(msg)), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }); + } + #[cfg(any(target_os = "windows", target_os = "macos"))] + { + use crate::updater::get_download_file_from_url; + if _key == "download-new-version" { + let download_url = _value.clone(); + let event_key = "download-new-version".to_owned(); + let data = if let Some(download_file) = get_download_file_from_url(&download_url) { + std::fs::remove_file(&download_file).ok(); + match crate::hbbs_http::downloader::download_file( + download_url, + Some(PathBuf::from(download_file)), + Some(Duration::from_secs(3)), + ) { + Ok(id) => HashMap::from([("name", event_key), ("id", id)]), + Err(e) => HashMap::from([("name", event_key), ("error", e.to_string())]), + } + } else { + HashMap::from([ + ("name", event_key), + ("error", "Invalid download url".to_string()), + ]) + }; + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + } else if _key == "update-me" { + if let Some(new_version_file) = get_download_file_from_url(&_value) { + log::debug!( + "New version file is downloaded, update begin, {:?}", + new_version_file.to_str() + ); + if let Some(f) = new_version_file.to_str() { + // 1.4.0 does not support "--update" + // But we can assume that the new version supports it. + + #[cfg(any(target_os = "windows", target_os = "macos"))] + match crate::platform::update_to(f) { + Ok(_) => { + log::info!("Update process is launched successfully!"); + } + Err(e) => { + log::error!("Failed to update to new version, {}", e); + fs::remove_file(f).ok(); + } + } + } + } + } else if _key == "extract-update-dmg" { + #[cfg(target_os = "macos")] + { + if let Some(new_version_file) = get_download_file_from_url(&_value) { + if let Some(f) = new_version_file.to_str() { + crate::platform::macos::extract_update_dmg(f); + } else { + // unreachable!() + log::error!("Failed to get the new version file path"); + } + } else { + // unreachable!() + log::error!("Failed to get the new version file from url: {}", _value); + } + } + } + } + + if _key == "remove-downloader" { + crate::hbbs_http::downloader::remove(&_value); + } else if _key == "cancel-downloader" { + crate::hbbs_http::downloader::cancel(&_value); + } + + #[cfg(target_os = "linux")] + if _key == "clear-gnome-shortcuts-inhibitor-permission" { + std::thread::spawn(move || { + let (success, msg) = + match crate::platform::linux::clear_gnome_shortcuts_inhibitor_permission() { + Ok(_) => (true, "".to_owned()), + Err(e) => (false, e.to_string()), + }; + let data = HashMap::from([ + ( + "name", + serde_json::json!("clear-gnome-shortcuts-inhibitor-permission-res"), + ), + ("success", serde_json::json!(success)), + ("msg", serde_json::json!(msg)), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }); + } +} + +pub fn session_get_common_sync( + session_id: SessionID, + key: String, + param: String, +) -> SyncReturn> { + SyncReturn(session_get_common(session_id, key, param)) +} + +pub fn session_get_common( + session_id: SessionID, + key: String, + #[allow(unused_variables)] param: String, +) -> Option { + if let Some(s) = sessions::get_session_by_session_id(&session_id) { + let v = if key == "is_screenshot_supported" { + s.is_screenshot_supported().to_string() + } else { + "".to_owned() + }; + Some(v) + } else { + None + } +} + +#[cfg(target_os = "android")] +pub mod server_side { + use hbb_common::{config, log}; + use jni::{ + errors::{Error as JniError, Result as JniResult}, + objects::{JClass, JObject, JString}, + sys::{jboolean, jstring}, + JNIEnv, + }; + + use crate::start_server; + + #[no_mangle] + pub unsafe extern "system" fn Java_ffi_FFI_startServer( + env: JNIEnv, + _class: JClass, + app_dir: JString, + custom_client_config: JString, + ) { + log::debug!("startServer from jvm"); + let mut env = env; + if let Ok(app_dir) = env.get_string(&app_dir) { + *config::APP_DIR.write().unwrap() = app_dir.into(); + } + if let Ok(custom_client_config) = env.get_string(&custom_client_config) { + if !custom_client_config.is_empty() { + let custom_client_config: String = custom_client_config.into(); + crate::read_custom_client(&custom_client_config); + } + } + std::thread::spawn(move || start_server(true)); + } + + #[no_mangle] + pub unsafe extern "system" fn Java_ffi_FFI_startService(_env: JNIEnv, _class: JClass) { + log::debug!("startService from jvm"); + config::Config::set_option("stop-service".into(), "".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } + + #[no_mangle] + pub unsafe extern "system" fn Java_ffi_FFI_translateLocale( + env: JNIEnv, + _class: JClass, + locale: JString, + input: JString, + ) -> jstring { + let mut env = env; + let res = if let (Ok(input), Ok(locale)) = (env.get_string(&input), env.get_string(&locale)) + { + let input: String = input.into(); + let locale: String = locale.into(); + crate::client::translate_locale(input, &locale) + } else { + "".into() + }; + return env.new_string(res).unwrap_or(input).into_raw(); + } + + #[no_mangle] + pub unsafe extern "system" fn Java_ffi_FFI_refreshScreen(_env: JNIEnv, _class: JClass) { + crate::server::video_service::refresh() + } + + #[no_mangle] + pub unsafe extern "system" fn Java_ffi_FFI_getLocalOption( + env: JNIEnv, + _class: JClass, + key: JString, + ) -> jstring { + let mut env = env; + let res = if let Ok(key) = env.get_string(&key) { + let key: String = key.into(); + super::get_local_option(key) + } else { + "".into() + }; + return env.new_string(res).unwrap_or_default().into_raw(); + } + + #[no_mangle] + pub unsafe extern "system" fn Java_ffi_FFI_getBuildinOption( + env: JNIEnv, + _class: JClass, + key: JString, + ) -> jstring { + let mut env = env; + let res = if let Ok(key) = env.get_string(&key) { + let key: String = key.into(); + super::get_builtin_option(&key) + } else { + "".into() + }; + return env.new_string(res).unwrap_or_default().into_raw(); + } + + #[no_mangle] + pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled( + env: JNIEnv, + _class: JClass, + ) -> jboolean { + jboolean::from(crate::server::is_clipboard_service_ok()) + } +} diff --git a/vendor/rustdesk/src/hbbs_http.rs b/vendor/rustdesk/src/hbbs_http.rs new file mode 100644 index 0000000..9e45386 --- /dev/null +++ b/vendor/rustdesk/src/hbbs_http.rs @@ -0,0 +1,40 @@ +use hbb_common::ResultType; +use serde::de::DeserializeOwned; +use serde_json::{Map, Value}; + +#[cfg(feature = "flutter")] +pub mod account; +pub mod downloader; +mod http_client; +pub mod record_upload; +pub mod sync; +pub use http_client::{ + create_http_client_async, create_http_client_async_with_url, create_http_client_with_url, + get_url_for_tls, +}; + +#[derive(Debug)] +pub enum HbbHttpResponse { + ErrorFormat, + Error(String), + DataTypeFormat, + Data(T), +} + +impl HbbHttpResponse { + pub fn parse(body: &str) -> ResultType { + let map = serde_json::from_str::>(body)?; + if let Some(error) = map.get("error") { + if let Some(err) = error.as_str() { + Ok(Self::Error(err.to_owned())) + } else { + Ok(Self::ErrorFormat) + } + } else { + match serde_json::from_value(Value::Object(map)) { + Ok(v) => Ok(Self::Data(v)), + Err(_) => Ok(Self::DataTypeFormat), + } + } + } +} diff --git a/vendor/rustdesk/src/hbbs_http/account.rs b/vendor/rustdesk/src/hbbs_http/account.rs new file mode 100644 index 0000000..3f82411 --- /dev/null +++ b/vendor/rustdesk/src/hbbs_http/account.rs @@ -0,0 +1,366 @@ +use super::HbbHttpResponse; +use crate::hbbs_http::create_http_client_with_url; +use hbb_common::{config::LocalConfig, log, ResultType}; +use serde_derive::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, + time::{Duration, Instant}, +}; +use url::Url; + +lazy_static::lazy_static! { + static ref OIDC_SESSION: Arc> = Arc::new(RwLock::new(OidcSession::new())); +} + +const QUERY_INTERVAL_SECS: f32 = 1.0; +const QUERY_TIMEOUT_SECS: u64 = 60 * 3; + +const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth"; +const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth"; +const LOGIN_ACCOUNT_AUTH: &str = "Login account auth"; + +#[derive(Deserialize, Clone, Debug)] +pub struct OidcAuthUrl { + code: String, + url: Url, +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct DeviceInfo { + /// Linux , Windows , Android ... + #[serde(default)] + pub os: String, + + /// `browser` or `client` + #[serde(default)] + pub r#type: String, + + /// device name from rustdesk client, + /// browser info(name + version) from browser + #[serde(default)] + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct WhitelistItem { + data: String, // ip / device uuid + info: DeviceInfo, + exp: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct UserInfo { + #[serde(default, flatten)] + pub settings: UserSettings, + #[serde(default)] + pub login_device_whitelist: Vec, + #[serde(default)] + pub other: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct UserSettings { + #[serde(default)] + pub email_verification: bool, + #[serde(default)] + pub email_alarm_notification: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize_repr, Deserialize_repr)] +#[repr(i64)] +pub enum UserStatus { + Disabled = 0, + Normal = 1, + Unverified = -1, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserPayload { + pub name: String, + #[serde(default)] + pub display_name: Option, + #[serde(default)] + pub avatar: Option, + #[serde(default)] + pub email: Option, + #[serde(default)] + pub note: Option, + #[serde(default)] + pub status: UserStatus, + pub info: UserInfo, + #[serde(default)] + pub is_admin: bool, + #[serde(default)] + pub third_auth_type: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthBody { + pub access_token: String, + pub r#type: String, + #[serde(default)] + pub tfa_type: String, + #[serde(default)] + pub secret: String, + pub user: UserPayload, +} + +pub struct OidcSession { + warmed_api_server: Option, + state_msg: &'static str, + failed_msg: String, + code_url: Option, + auth_body: Option, + keep_querying: bool, + running: bool, + query_timeout: Duration, +} + +#[derive(Serialize)] +pub struct AuthResult { + pub state_msg: String, + pub failed_msg: String, + pub url: Option, + pub auth_body: Option, +} + +impl Default for UserStatus { + fn default() -> Self { + UserStatus::Normal + } +} + +impl OidcSession { + fn new() -> Self { + Self { + warmed_api_server: None, + state_msg: REQUESTING_ACCOUNT_AUTH, + failed_msg: "".to_owned(), + code_url: None, + auth_body: None, + keep_querying: false, + running: false, + query_timeout: Duration::from_secs(QUERY_TIMEOUT_SECS), + } + } + + fn ensure_client(api_server: &str) { + let mut write_guard = OIDC_SESSION.write().unwrap(); + if write_guard.warmed_api_server.as_deref() == Some(api_server) { + return; + } + // This URL is used to detect the appropriate TLS implementation for the server. + let login_option_url = format!("{}/api/login-options", api_server); + let _ = create_http_client_with_url(&login_option_url); + write_guard.warmed_api_server = Some(api_server.to_owned()); + } + + fn auth( + api_server: &str, + op: &str, + id: &str, + uuid: &str, + ) -> ResultType> { + Self::ensure_client(api_server); + let body = serde_json::json!({ + "op": op, + "id": id, + "uuid": uuid, + "deviceInfo": crate::ui_interface::get_login_device_info(), + }) + .to_string(); + let resp = crate::post_request_sync(format!("{}/api/oidc/auth", api_server), body, "")?; + HbbHttpResponse::parse(&resp) + } + + fn query( + api_server: &str, + code: &str, + id: &str, + uuid: &str, + ) -> ResultType> { + let url = Url::parse_with_params( + &format!("{}/api/oidc/auth-query", api_server), + &[("code", code), ("id", id), ("uuid", uuid)], + )?; + Self::ensure_client(api_server); + #[derive(Deserialize)] + struct HttpResponseBody { + body: String, + } + + let resp = crate::http_request_sync( + url.to_string(), + "GET".to_owned(), + None, + "{}".to_owned(), + )?; + let resp = serde_json::from_str::(&resp)?; + HbbHttpResponse::parse(&resp.body) + } + + fn reset(&mut self) { + self.state_msg = REQUESTING_ACCOUNT_AUTH; + self.failed_msg = "".to_owned(); + self.keep_querying = true; + self.running = false; + self.code_url = None; + self.auth_body = None; + } + + fn before_task(&mut self) { + self.reset(); + self.running = true; + } + + fn after_task(&mut self) { + self.running = false; + } + + fn sleep(secs: f32) { + std::thread::sleep(std::time::Duration::from_secs_f32(secs)); + } + + fn auth_task(api_server: String, op: String, id: String, uuid: String, remember_me: bool) { + let auth_request_res = Self::auth(&api_server, &op, &id, &uuid); + log::info!("Request oidc auth result: {:?}", &auth_request_res); + let code_url = match auth_request_res { + Ok(HbbHttpResponse::<_>::Data(code_url)) => code_url, + Ok(HbbHttpResponse::<_>::Error(err)) => { + OIDC_SESSION + .write() + .unwrap() + .set_state(REQUESTING_ACCOUNT_AUTH, err); + return; + } + Ok(_) => { + OIDC_SESSION + .write() + .unwrap() + .set_state(REQUESTING_ACCOUNT_AUTH, "Invalid auth response".to_owned()); + return; + } + Err(err) => { + OIDC_SESSION + .write() + .unwrap() + .set_state(REQUESTING_ACCOUNT_AUTH, err.to_string()); + return; + } + }; + + OIDC_SESSION + .write() + .unwrap() + .set_state(WAITING_ACCOUNT_AUTH, "".to_owned()); + OIDC_SESSION.write().unwrap().code_url = Some(code_url.clone()); + + let begin = Instant::now(); + let query_timeout = OIDC_SESSION.read().unwrap().query_timeout; + while OIDC_SESSION.read().unwrap().keep_querying && begin.elapsed() < query_timeout { + match Self::query(&api_server, &code_url.code, &id, &uuid) { + Ok(HbbHttpResponse::<_>::Data(auth_body)) => { + if auth_body.r#type == "access_token" { + if remember_me { + LocalConfig::set_option( + "access_token".to_owned(), + auth_body.access_token.clone(), + ); + LocalConfig::set_option( + "user_info".to_owned(), + serde_json::json!({ + "name": auth_body.user.name, + "display_name": auth_body.user.display_name, + "avatar": auth_body.user.avatar, + "status": auth_body.user.status + }) + .to_string(), + ); + } + } + OIDC_SESSION + .write() + .unwrap() + .set_state(LOGIN_ACCOUNT_AUTH, "".to_owned()); + OIDC_SESSION.write().unwrap().auth_body = Some(auth_body); + return; + } + Ok(HbbHttpResponse::<_>::Error(err)) => { + if err.contains("No authed oidc is found") { + // ignore, keep querying + } else { + OIDC_SESSION + .write() + .unwrap() + .set_state(WAITING_ACCOUNT_AUTH, err); + return; + } + } + Ok(_) => { + // ignore + } + Err(err) => { + log::trace!("Failed query oidc {}", err); + // ignore + } + } + Self::sleep(QUERY_INTERVAL_SECS); + } + + if begin.elapsed() >= query_timeout { + OIDC_SESSION + .write() + .unwrap() + .set_state(WAITING_ACCOUNT_AUTH, "timeout".to_owned()); + } + + // no need to handle "keep_querying == false" + } + + fn set_state(&mut self, state_msg: &'static str, failed_msg: String) { + self.state_msg = state_msg; + self.failed_msg = failed_msg; + } + + fn wait_stop_querying() { + let wait_secs = 0.3; + while OIDC_SESSION.read().unwrap().running { + Self::sleep(wait_secs); + } + } + + pub fn account_auth( + api_server: String, + op: String, + id: String, + uuid: String, + remember_me: bool, + ) { + Self::auth_cancel(); + Self::wait_stop_querying(); + OIDC_SESSION.write().unwrap().before_task(); + std::thread::spawn(move || { + Self::auth_task(api_server, op, id, uuid, remember_me); + OIDC_SESSION.write().unwrap().after_task(); + }); + } + + fn get_result_(&self) -> AuthResult { + AuthResult { + state_msg: self.state_msg.to_string(), + failed_msg: self.failed_msg.clone(), + url: self.code_url.as_ref().map(|x| x.url.to_string()), + auth_body: self.auth_body.clone(), + } + } + + pub fn auth_cancel() { + OIDC_SESSION.write().unwrap().keep_querying = false; + } + + pub fn get_result() -> AuthResult { + OIDC_SESSION.read().unwrap().get_result_() + } +} diff --git a/vendor/rustdesk/src/hbbs_http/downloader.rs b/vendor/rustdesk/src/hbbs_http/downloader.rs new file mode 100644 index 0000000..573e7e7 --- /dev/null +++ b/vendor/rustdesk/src/hbbs_http/downloader.rs @@ -0,0 +1,309 @@ +use super::create_http_client_async_with_url; +use hbb_common::{ + bail, + lazy_static::lazy_static, + log, + tokio::{ + self, + fs::File, + io::AsyncWriteExt, + sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + }, + ResultType, +}; +use serde_derive::Serialize; +use std::{collections::HashMap, path::PathBuf, sync::Mutex, time::Duration}; + +lazy_static! { + static ref DOWNLOADERS: Mutex> = Default::default(); +} + +/// This struct is used to return the download data to the caller. +/// The caller should check if the file is downloaded successfully and remove the job from the map. +/// If the file is not downloaded successfully, the `data` field will be empty. +/// If the file is downloaded successfully, the `data` field will contain the downloaded data if `path` is None. +#[derive(Serialize, Debug)] +pub struct DownloadData { + #[serde(skip_serializing_if = "Vec::is_empty")] + pub data: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub total_size: Option, + pub downloaded_size: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +struct Downloader { + data: Vec, + path: Option, + // Some file may be empty, so we use Option to indicate if the size is known + total_size: Option, + downloaded_size: u64, + error: Option, + finished: bool, + tx_cancel: UnboundedSender<()>, +} + +// The caller should check if the file is downloaded successfully and remove the job from the map. +pub fn download_file( + url: String, + path: Option, + auto_del_dur: Option, +) -> ResultType { + let id = url.clone(); + // First pass: if a non-error downloader exists for this URL, reuse it. + // If an errored downloader exists, remove it so this call can retry. + let mut stale_path = None; + { + let mut downloaders = DOWNLOADERS.lock().unwrap(); + if let Some(downloader) = downloaders.get(&id) { + if downloader.error.is_none() { + return Ok(id); + } + stale_path = downloader.path.clone(); + downloaders.remove(&id); + } + } + if let Some(p) = stale_path { + if p.exists() { + if let Err(e) = std::fs::remove_file(&p) { + log::warn!("Failed to remove stale download file {}: {}", p.display(), e); + } + } + } + + if let Some(path) = path.as_ref() { + if path.exists() { + bail!("File {} already exists", path.display()); + } + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + } + let (tx, rx) = unbounded_channel(); + let downloader = Downloader { + data: Vec::new(), + path: path.clone(), + total_size: None, + downloaded_size: 0, + error: None, + tx_cancel: tx, + finished: false, + }; + // Second pass (atomic with insert) to avoid race with another concurrent caller. + let mut stale_path_after_check = None; + { + let mut downloaders = DOWNLOADERS.lock().unwrap(); + if let Some(existing) = downloaders.get(&id) { + if existing.error.is_none() { + return Ok(id); + } + stale_path_after_check = existing.path.clone(); + downloaders.remove(&id); + } + downloaders.insert(id.clone(), downloader); + } + if let Some(p) = stale_path_after_check { + if p.exists() { + if let Err(e) = std::fs::remove_file(&p) { + log::warn!("Failed to remove stale download file {}: {}", p.display(), e); + } + } + } + + let id2 = id.clone(); + std::thread::spawn( + move || match do_download(&id2, url, path, auto_del_dur, rx) { + Ok(is_all_downloaded) => { + let mut downloaded_size = 0; + let mut total_size = 0; + DOWNLOADERS.lock().unwrap().get_mut(&id2).map(|downloader| { + downloaded_size = downloader.downloaded_size; + total_size = downloader.total_size.unwrap_or(0); + }); + log::info!( + "Download {} end, {}/{}, {:.2} %", + &id2, + downloaded_size, + total_size, + if total_size == 0 { + 0.0 + } else { + downloaded_size as f64 / total_size as f64 * 100.0 + } + ); + + let is_canceled = !is_all_downloaded; + if is_canceled { + if let Some(downloader) = DOWNLOADERS.lock().unwrap().remove(&id2) { + if let Some(p) = downloader.path { + if p.exists() { + std::fs::remove_file(p).ok(); + } + } + } + } + } + Err(e) => { + let err = e.to_string(); + log::error!("Download {}, failed: {}", &id2, &err); + DOWNLOADERS.lock().unwrap().get_mut(&id2).map(|downloader| { + downloader.error = Some(err); + }); + } + }, + ); + + Ok(id) +} + +#[tokio::main(flavor = "current_thread")] +async fn do_download( + id: &str, + url: String, + path: Option, + auto_del_dur: Option, + mut rx_cancel: UnboundedReceiver<()>, +) -> ResultType { + let client = create_http_client_async_with_url(&url).await; + + let mut is_all_downloaded = false; + tokio::select! { + _ = rx_cancel.recv() => { + return Ok(is_all_downloaded); + } + head_resp = client.head(&url).send() => { + match head_resp { + Ok(resp) => { + if resp.status().is_success() { + let total_size = resp + .headers() + .get(reqwest::header::CONTENT_LENGTH) + .and_then(|ct_len| ct_len.to_str().ok()) + .and_then(|ct_len| ct_len.parse::().ok()); + let Some(total_size) = total_size else { + bail!("Failed to get content length"); + }; + DOWNLOADERS.lock().unwrap().get_mut(id).map(|downloader| { + downloader.total_size = Some(total_size); + }); + } else { + bail!("Failed to get content length: {}", resp.status()); + } + } + Err(e) => { + return Err(e.into()); + } + } + } + } + + let mut response; + tokio::select! { + _ = rx_cancel.recv() => { + return Ok(is_all_downloaded); + } + resp = client.get(url).send() => { + response = resp?; + } + } + + let mut dest: Option = None; + if let Some(p) = path { + dest = Some(File::create(p).await?); + } + + loop { + tokio::select! { + _ = rx_cancel.recv() => { + break; + } + chunk = response.chunk() => { + match chunk { + Ok(Some(chunk)) => { + match dest { + Some(ref mut f) => { + f.write_all(&chunk).await?; + f.flush().await?; + DOWNLOADERS.lock().unwrap().get_mut(id).map(|downloader| { + downloader.downloaded_size += chunk.len() as u64; + }); + } + None => { + DOWNLOADERS.lock().unwrap().get_mut(id).map(|downloader| { + downloader.data.extend_from_slice(&chunk); + downloader.downloaded_size += chunk.len() as u64; + }); + } + } + } + Ok(None) => { + is_all_downloaded = true; + break; + }, + Err(e) => { + log::error!("Download {} failed: {}", id, e); + return Err(e.into()); + } + } + } + } + } + + if let Some(mut f) = dest.take() { + f.flush().await?; + } + + if let Some(ref mut downloader) = DOWNLOADERS.lock().unwrap().get_mut(id) { + downloader.finished = true; + } + if is_all_downloaded { + let id_del = id.to_string(); + if let Some(dur) = auto_del_dur { + tokio::spawn(async move { + tokio::time::sleep(dur).await; + DOWNLOADERS.lock().unwrap().remove(&id_del); + }); + } + } + Ok(is_all_downloaded) +} + +pub fn get_download_data(id: &str) -> ResultType { + let downloaders = DOWNLOADERS.lock().unwrap(); + if let Some(downloader) = downloaders.get(id) { + let downloaded_size = downloader.downloaded_size; + let total_size = downloader.total_size.clone(); + let error = downloader.error.clone(); + let data = if total_size.unwrap_or(0) == downloaded_size && downloader.path.is_none() { + downloader.data.clone() + } else { + Vec::new() + }; + let path = downloader.path.clone(); + let download_data = DownloadData { + data, + path, + total_size, + downloaded_size, + error, + }; + Ok(download_data) + } else { + bail!("Downloader not found") + } +} + +pub fn cancel(id: &str) { + if let Some(downloader) = DOWNLOADERS.lock().unwrap().get(id) { + // downloader.is_canceled.store(true, Ordering::SeqCst); + // The receiver may not be able to receive the cancel signal, so we also set the atomic bool to true + let _ = downloader.tx_cancel.send(()); + } +} + +pub fn remove(id: &str) { + let _ = DOWNLOADERS.lock().unwrap().remove(id); +} diff --git a/vendor/rustdesk/src/hbbs_http/http_client.rs b/vendor/rustdesk/src/hbbs_http/http_client.rs new file mode 100644 index 0000000..432e5fa --- /dev/null +++ b/vendor/rustdesk/src/hbbs_http/http_client.rs @@ -0,0 +1,336 @@ +use hbb_common::{ + async_recursion::async_recursion, + config::{Config, Socks5Server}, + log::{self, info}, + proxy::{Proxy, ProxyScheme}, + tls::{ + get_cached_tls_accept_invalid_cert, get_cached_tls_type, is_plain, upsert_tls_cache, + TlsType, + }, +}; +use reqwest::{blocking::Client as SyncClient, Client as AsyncClient}; + +macro_rules! configure_http_client { + ($builder:expr, $tls_type:expr, $danger_accept_invalid_cert:expr, $Client: ty) => {{ + // https://github.com/rustdesk/rustdesk/issues/11569 + // https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.no_proxy + let mut builder = $builder.no_proxy(); + + match $tls_type { + TlsType::Plain => {} + TlsType::NativeTls => { + builder = builder.use_native_tls(); + if $danger_accept_invalid_cert { + builder = builder.danger_accept_invalid_certs(true); + } + } + TlsType::Rustls => { + #[cfg(any(target_os = "android", target_os = "ios"))] + match hbb_common::verifier::client_config($danger_accept_invalid_cert) { + Ok(client_config) => { + builder = builder.use_preconfigured_tls(client_config); + } + Err(e) => { + hbb_common::log::error!("Failed to get client config: {}", e); + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + builder = builder.use_rustls_tls(); + if $danger_accept_invalid_cert { + builder = builder.danger_accept_invalid_certs(true); + } + } + } + } + + let client = if let Some(conf) = Config::get_socks() { + let proxy_result = Proxy::from_conf(&conf, None); + + match proxy_result { + Ok(proxy) => { + let proxy_setup = match &proxy.intercept { + ProxyScheme::Http { host, .. } => { + reqwest::Proxy::all(format!("http://{}", host)) + } + ProxyScheme::Https { host, .. } => { + reqwest::Proxy::all(format!("https://{}", host)) + } + ProxyScheme::Socks5 { addr, .. } => { + reqwest::Proxy::all(&format!("socks5://{}", addr)) + } + }; + + match proxy_setup { + Ok(mut p) => { + if let Some(auth) = proxy.intercept.maybe_auth() { + if !auth.username().is_empty() && !auth.password().is_empty() { + p = p.basic_auth(auth.username(), auth.password()); + } + } + builder = builder.proxy(p); + builder.build().unwrap_or_else(|e| { + info!("Failed to create a proxied client: {}", e); + <$Client>::new() + }) + } + Err(e) => { + info!("Failed to set up proxy: {}", e); + <$Client>::new() + } + } + } + Err(e) => { + info!("Failed to configure proxy: {}", e); + <$Client>::new() + } + } + } else { + builder.build().unwrap_or_else(|e| { + info!("Failed to create a client: {}", e); + <$Client>::new() + }) + }; + + client + }}; +} + +pub fn create_http_client(tls_type: TlsType, danger_accept_invalid_cert: bool) -> SyncClient { + let builder = SyncClient::builder(); + configure_http_client!(builder, tls_type, danger_accept_invalid_cert, SyncClient) +} + +pub fn create_http_client_async( + tls_type: TlsType, + danger_accept_invalid_cert: bool, +) -> AsyncClient { + let builder = AsyncClient::builder(); + configure_http_client!(builder, tls_type, danger_accept_invalid_cert, AsyncClient) +} + +pub fn get_url_for_tls<'a>(url: &'a str, proxy_conf: &'a Option) -> &'a str { + if is_plain(url) { + if let Some(conf) = proxy_conf { + if conf.proxy.starts_with("https://") { + return &conf.proxy; + } + } + } + url +} + +pub fn create_http_client_with_url(url: &str) -> SyncClient { + let proxy_conf = Config::get_socks(); + let tls_url = get_url_for_tls(url, &proxy_conf); + let tls_type = get_cached_tls_type(tls_url); + let is_tls_type_cached = tls_type.is_some(); + let tls_type = tls_type.unwrap_or(TlsType::Rustls); + let tls_danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); + create_http_client_with_url_( + url, + tls_url, + tls_type, + is_tls_type_cached, + tls_danger_accept_invalid_cert, + tls_danger_accept_invalid_cert, + ) +} + +fn create_http_client_with_url_( + url: &str, + tls_url: &str, + tls_type: TlsType, + is_tls_type_cached: bool, + danger_accept_invalid_cert: Option, + original_danger_accept_invalid_cert: Option, +) -> SyncClient { + let mut client = create_http_client(tls_type, danger_accept_invalid_cert.unwrap_or(false)); + if is_tls_type_cached && original_danger_accept_invalid_cert.is_some() { + return client; + } + if let Err(e) = client.head(url).send() { + if e.is_request() { + match (tls_type, is_tls_type_cached, danger_accept_invalid_cert) { + (TlsType::Rustls, _, None) => { + log::warn!( + "Failed to connect to server {} with rustls-tls: {:?}, trying accept invalid cert", + tls_url, + e + ); + client = create_http_client_with_url_( + url, + tls_url, + tls_type, + is_tls_type_cached, + Some(true), + original_danger_accept_invalid_cert, + ); + } + (TlsType::Rustls, false, Some(_)) => { + log::warn!( + "Failed to connect to server {} with rustls-tls: {:?}, trying native-tls", + tls_url, + e + ); + client = create_http_client_with_url_( + url, + tls_url, + TlsType::NativeTls, + is_tls_type_cached, + original_danger_accept_invalid_cert, + original_danger_accept_invalid_cert, + ); + } + (TlsType::NativeTls, _, None) => { + log::warn!( + "Failed to connect to server {} with native-tls: {:?}, trying accept invalid cert", + tls_url, + e + ); + client = create_http_client_with_url_( + url, + tls_url, + tls_type, + is_tls_type_cached, + Some(true), + original_danger_accept_invalid_cert, + ); + } + _ => { + log::error!( + "Failed to connect to server {} with {:?}, err: {:?}.", + tls_url, + tls_type, + e + ); + } + } + } else { + log::warn!( + "Failed to connect to server {} with {:?}, err: {}.", + tls_url, + tls_type, + e + ); + } + } else { + log::info!( + "Successfully connected to server {} with {:?}", + tls_url, + tls_type + ); + upsert_tls_cache( + tls_url, + tls_type, + danger_accept_invalid_cert.unwrap_or(false), + ); + } + client +} + +pub async fn create_http_client_async_with_url(url: &str) -> AsyncClient { + let proxy_conf = Config::get_socks(); + let tls_url = get_url_for_tls(url, &proxy_conf); + let tls_type = get_cached_tls_type(tls_url); + let is_tls_type_cached = tls_type.is_some(); + let tls_type = tls_type.unwrap_or(TlsType::Rustls); + let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); + create_http_client_async_with_url_( + url, + tls_url, + tls_type, + is_tls_type_cached, + danger_accept_invalid_cert, + danger_accept_invalid_cert, + ) + .await +} + +#[async_recursion] +async fn create_http_client_async_with_url_( + url: &str, + tls_url: &str, + tls_type: TlsType, + is_tls_type_cached: bool, + danger_accept_invalid_cert: Option, + original_danger_accept_invalid_cert: Option, +) -> AsyncClient { + let mut client = + create_http_client_async(tls_type, danger_accept_invalid_cert.unwrap_or(false)); + if is_tls_type_cached && original_danger_accept_invalid_cert.is_some() { + return client; + } + if let Err(e) = client.head(url).send().await { + match (tls_type, is_tls_type_cached, danger_accept_invalid_cert) { + (TlsType::Rustls, _, None) => { + log::warn!( + "Failed to connect to server {} with rustls-tls: {:?}, trying accept invalid cert", + tls_url, + e + ); + client = create_http_client_async_with_url_( + url, + tls_url, + tls_type, + is_tls_type_cached, + Some(true), + original_danger_accept_invalid_cert, + ) + .await; + } + (TlsType::Rustls, false, Some(_)) => { + log::warn!( + "Failed to connect to server {} with rustls-tls: {:?}, trying native-tls", + tls_url, + e + ); + client = create_http_client_async_with_url_( + url, + tls_url, + TlsType::NativeTls, + is_tls_type_cached, + original_danger_accept_invalid_cert, + original_danger_accept_invalid_cert, + ) + .await; + } + (TlsType::NativeTls, _, None) => { + log::warn!( + "Failed to connect to server {} with native-tls: {:?}, trying accept invalid cert", + tls_url, + e + ); + client = create_http_client_async_with_url_( + url, + tls_url, + tls_type, + is_tls_type_cached, + Some(true), + original_danger_accept_invalid_cert, + ) + .await; + } + _ => { + log::error!( + "Failed to connect to server {} with {:?}, err: {:?}.", + tls_url, + tls_type, + e + ); + } + } + } else { + log::info!( + "Successfully connected to server {} with {:?}", + tls_url, + tls_type + ); + upsert_tls_cache( + tls_url, + tls_type, + danger_accept_invalid_cert.unwrap_or(false), + ); + } + client +} diff --git a/vendor/rustdesk/src/hbbs_http/record_upload.rs b/vendor/rustdesk/src/hbbs_http/record_upload.rs new file mode 100644 index 0000000..ac51d5c --- /dev/null +++ b/vendor/rustdesk/src/hbbs_http/record_upload.rs @@ -0,0 +1,211 @@ +use crate::hbbs_http::create_http_client_with_url; +use bytes::Bytes; +use hbb_common::{bail, config::Config, lazy_static, log, ResultType}; +use reqwest::blocking::{Body, Client}; +use scrap::record::RecordState; +use serde::Serialize; +use serde_json::Map; +use std::{ + fs::File, + io::{prelude::*, SeekFrom}, + sync::{mpsc::Receiver, Arc, Mutex}, + time::{Duration, Instant}, +}; + +const MAX_HEADER_LEN: usize = 1024; +const SHOULD_SEND_TIME: Duration = Duration::from_secs(1); +const SHOULD_SEND_SIZE: u64 = 1024 * 1024; + +lazy_static::lazy_static! { + static ref ENABLE: Arc> = Default::default(); +} + +pub fn is_enable() -> bool { + ENABLE.lock().unwrap().clone() +} + +pub fn run(rx: Receiver) { + std::thread::spawn(move || { + let api_server = crate::get_api_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ); + // This URL is used for TLS connectivity testing and fallback detection. + let login_option_url = format!("{}/api/login-options", &api_server); + let client = create_http_client_with_url(&login_option_url); + let mut uploader = RecordUploader { + client, + api_server, + filepath: Default::default(), + filename: Default::default(), + upload_size: Default::default(), + running: Default::default(), + last_send: Instant::now(), + }; + loop { + if let Err(e) = match rx.recv() { + Ok(state) => match state { + RecordState::NewFile(filepath) => uploader.handle_new_file(filepath), + RecordState::NewFrame => { + if uploader.running { + uploader.handle_frame(false) + } else { + Ok(()) + } + } + RecordState::WriteTail => { + if uploader.running { + uploader.handle_tail() + } else { + Ok(()) + } + } + RecordState::RemoveFile => { + if uploader.running { + uploader.handle_remove() + } else { + Ok(()) + } + } + }, + Err(e) => { + log::trace!("upload thread stop: {}", e); + break; + } + } { + uploader.running = false; + log::error!("upload stop: {}", e); + } + } + }); +} + +struct RecordUploader { + client: Client, + api_server: String, + filepath: String, + filename: String, + upload_size: u64, + running: bool, + last_send: Instant, +} +impl RecordUploader { + fn send(&self, query: &Q, body: B) -> ResultType<()> + where + Q: Serialize + ?Sized, + B: Into, + { + match self + .client + .post(format!("{}/api/record", self.api_server)) + .query(query) + .body(body) + .send() + { + Ok(resp) => { + if let Ok(m) = resp.json::>() { + if let Some(e) = m.get("error") { + bail!(e.to_string()); + } + } + Ok(()) + } + Err(e) => bail!(e.to_string()), + } + } + + fn handle_new_file(&mut self, filepath: String) -> ResultType<()> { + match std::path::PathBuf::from(&filepath).file_name() { + Some(filename) => match filename.to_owned().into_string() { + Ok(filename) => { + self.filename = filename.clone(); + self.filepath = filepath.clone(); + self.upload_size = 0; + self.running = true; + self.last_send = Instant::now(); + self.send(&[("type", "new"), ("file", &filename)], Bytes::new())?; + Ok(()) + } + Err(_) => bail!("can't parse filename:{:?}", filename), + }, + None => bail!("can't parse filepath:{}", filepath), + } + } + + fn handle_frame(&mut self, flush: bool) -> ResultType<()> { + if !flush && self.last_send.elapsed() < SHOULD_SEND_TIME { + return Ok(()); + } + match File::open(&self.filepath) { + Ok(mut file) => match file.metadata() { + Ok(m) => { + let len = m.len(); + if len <= self.upload_size { + return Ok(()); + } + if !flush && len - self.upload_size < SHOULD_SEND_SIZE { + return Ok(()); + } + let mut buf = Vec::new(); + match file.seek(SeekFrom::Start(self.upload_size)) { + Ok(_) => match file.read_to_end(&mut buf) { + Ok(length) => { + self.send( + &[ + ("type", "part"), + ("file", &self.filename), + ("offset", &self.upload_size.to_string()), + ("length", &length.to_string()), + ], + buf, + )?; + self.upload_size = len; + self.last_send = Instant::now(); + Ok(()) + } + Err(e) => bail!(e.to_string()), + }, + Err(e) => bail!(e.to_string()), + } + } + Err(e) => bail!(e.to_string()), + }, + Err(e) => bail!(e.to_string()), + } + } + + fn handle_tail(&mut self) -> ResultType<()> { + self.handle_frame(true)?; + match File::open(&self.filepath) { + Ok(mut file) => { + let mut buf = vec![0u8; MAX_HEADER_LEN]; + match file.read(&mut buf) { + Ok(length) => { + buf.truncate(length); + self.send( + &[ + ("type", "tail"), + ("file", &self.filename), + ("offset", "0"), + ("length", &length.to_string()), + ], + buf, + )?; + log::info!("upload success, file: {}", self.filename); + Ok(()) + } + Err(e) => bail!(e.to_string()), + } + } + Err(e) => bail!(e.to_string()), + } + } + + fn handle_remove(&mut self) -> ResultType<()> { + self.send( + &[("type", "remove"), ("file", &self.filename)], + Bytes::new(), + )?; + Ok(()) + } +} diff --git a/vendor/rustdesk/src/hbbs_http/sync.rs b/vendor/rustdesk/src/hbbs_http/sync.rs new file mode 100644 index 0000000..8460b98 --- /dev/null +++ b/vendor/rustdesk/src/hbbs_http/sync.rs @@ -0,0 +1,325 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::Duration, +}; + +#[cfg(not(any(target_os = "ios")))] +use crate::{ui_interface::get_builtin_option, Connection}; +use hbb_common::{ + config::{self, keys, Config, LocalConfig}, + log, + tokio::{self, sync::broadcast, time::Instant}, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +// hello-agent local patch: lowered from 15s to 5s so device-online status in +// the admin UI reacts faster. Re-apply on vendor resync. +const TIME_HEARTBEAT: Duration = Duration::from_secs(1); +const UPLOAD_SYSINFO_TIMEOUT: Duration = Duration::from_secs(120); +const TIME_CONN: Duration = Duration::from_secs(3); + +#[cfg(not(any(target_os = "ios")))] +lazy_static::lazy_static! { + static ref SENDER : Mutex>> = Mutex::new(start_hbbs_sync()); + static ref PRO: Arc> = Default::default(); +} + +#[cfg(not(any(target_os = "ios")))] +pub fn start() { + let _sender = SENDER.lock().unwrap(); +} + +#[cfg(not(target_os = "ios"))] +pub fn signal_receiver() -> broadcast::Receiver> { + SENDER.lock().unwrap().subscribe() +} + +#[cfg(not(any(target_os = "ios")))] +fn start_hbbs_sync() -> broadcast::Sender> { + let (tx, _rx) = broadcast::channel::>(16); + std::thread::spawn(move || start_hbbs_sync_async()); + return tx; +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StrategyOptions { + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub config_options: HashMap, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub extra: HashMap, +} + +struct InfoUploaded { + uploaded: bool, + url: String, + last_uploaded: Option, + id: String, + username: Option, +} + +impl Default for InfoUploaded { + fn default() -> Self { + Self { + uploaded: false, + url: "".to_owned(), + last_uploaded: None, + id: "".to_owned(), + username: None, + } + } +} + +impl InfoUploaded { + fn uploaded(url: String, id: String, username: String) -> Self { + Self { + uploaded: true, + url, + last_uploaded: None, + id, + username: Some(username), + } + } +} + +#[cfg(not(any(target_os = "ios")))] +#[tokio::main(flavor = "current_thread")] +async fn start_hbbs_sync_async() { + let mut interval = crate::rustdesk_interval(tokio::time::interval_at( + Instant::now() + TIME_CONN, + TIME_CONN, + )); + let mut last_sent: Option = None; + let mut info_uploaded = InfoUploaded::default(); + let mut sysinfo_ver = "".to_owned(); + loop { + tokio::select! { + _ = interval.tick() => { + let url = heartbeat_url(); + let id = Config::get_id(); + if url.is_empty() { + *PRO.lock().unwrap() = false; + continue; + } + if config::option2bool("stop-service", &Config::get_option("stop-service")) { + continue; + } + let conns = Connection::alive_conns(); + if info_uploaded.uploaded && (url != info_uploaded.url || id != info_uploaded.id) { + info_uploaded.uploaded = false; + *PRO.lock().unwrap() = false; + } + // For Windows: + // We can't skip uploading sysinfo when the username is empty, because the username may + // always be empty before login. We also need to upload the other sysinfo info. + // + // https://github.com/rustdesk/rustdesk/discussions/8031 + // We still need to check the username after uploading sysinfo, because + // 1. The username may be empty when logining in, and it can be fetched after a while. + // In this case, we need to upload sysinfo again. + // 2. The username may be changed after uploading sysinfo, and we need to upload sysinfo again. + // + // The Windows session will switch to the last user session before the restart, + // so it may be able to get the username before login. + // But strangely, sometimes we can get the username before login, + // we may not be able to get the username before login after the next restart. + let mut v = crate::get_sysinfo(); + let sys_username = v["username"].as_str().unwrap_or_default().to_string(); + // Though the username comparison is only necessary on Windows, + // we still keep the comparison on other platforms for consistency. + let need_upload = (!info_uploaded.uploaded || info_uploaded.username.as_ref() != Some(&sys_username)) && + info_uploaded.last_uploaded.map(|x| x.elapsed() >= UPLOAD_SYSINFO_TIMEOUT).unwrap_or(true); + if need_upload { + v["version"] = json!(crate::VERSION); + v["id"] = json!(id); + v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); + // Optional rebrand identity: `AGENT_NAME` / `AGENT_VERSION` + // are empty by default (vanilla rustdesk) and populated by + // OEM shells like hello-agent. We only stamp the field + // when set so older servers parsing the payload don't see + // empty strings they have to special-case. + let agent_name = config::AGENT_NAME.read().unwrap().clone(); + if !agent_name.is_empty() { + v["agent_name"] = json!(agent_name); + } + let agent_version = config::AGENT_VERSION.read().unwrap().clone(); + if !agent_version.is_empty() { + v["agent_version"] = json!(agent_version); + } + let ab_name = Config::get_option(keys::OPTION_PRESET_ADDRESS_BOOK_NAME); + if !ab_name.is_empty() { + v[keys::OPTION_PRESET_ADDRESS_BOOK_NAME] = json!(ab_name); + } + let ab_tag = Config::get_option(keys::OPTION_PRESET_ADDRESS_BOOK_TAG); + if !ab_tag.is_empty() { + v[keys::OPTION_PRESET_ADDRESS_BOOK_TAG] = json!(ab_tag); + } + let ab_alias = Config::get_option(keys::OPTION_PRESET_ADDRESS_BOOK_ALIAS); + if !ab_alias.is_empty() { + v[keys::OPTION_PRESET_ADDRESS_BOOK_ALIAS] = json!(ab_alias); + } + let ab_password = Config::get_option(keys::OPTION_PRESET_ADDRESS_BOOK_PASSWORD); + if !ab_password.is_empty() { + v[keys::OPTION_PRESET_ADDRESS_BOOK_PASSWORD] = json!(ab_password); + } + let ab_note = Config::get_option(keys::OPTION_PRESET_ADDRESS_BOOK_NOTE); + if !ab_note.is_empty() { + v[keys::OPTION_PRESET_ADDRESS_BOOK_NOTE] = json!(ab_note); + } + let username = get_builtin_option(keys::OPTION_PRESET_USERNAME); + if !username.is_empty() { + v[keys::OPTION_PRESET_USERNAME] = json!(username); + } + let strategy_name = get_builtin_option(keys::OPTION_PRESET_STRATEGY_NAME); + if !strategy_name.is_empty() { + v[keys::OPTION_PRESET_STRATEGY_NAME] = json!(strategy_name); + } + let device_group_name = get_builtin_option(keys::OPTION_PRESET_DEVICE_GROUP_NAME); + if !device_group_name.is_empty() { + v[keys::OPTION_PRESET_DEVICE_GROUP_NAME] = json!(device_group_name); + } + let device_username = Config::get_option(keys::OPTION_PRESET_DEVICE_USERNAME); + if !device_username.is_empty() { + v["username"] = json!(device_username); + } + let device_name = Config::get_option(keys::OPTION_PRESET_DEVICE_NAME); + if !device_name.is_empty() { + v["hostname"] = json!(device_name); + } + let note = Config::get_option(keys::OPTION_PRESET_NOTE); + if !note.is_empty() { + v[keys::OPTION_PRESET_NOTE] = json!(note); + } + let v = v.to_string(); + let mut hash = "".to_owned(); + if crate::is_public(&url) { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(url.as_bytes()); + hasher.update(&v.as_bytes()); + let res = hasher.finalize(); + hash = hbb_common::base64::encode(&res[..]); + let old_hash = config::Status::get("sysinfo_hash"); + let ver = config::Status::get("sysinfo_ver"); // sysinfo_ver is the version of sysinfo on server's side + if hash == old_hash { + // When the api doesn't exist, Ok("") will be returned in test. + let samever = match crate::post_request(url.replace("heartbeat", "sysinfo_ver"), "".to_owned(), "").await { + Ok(x) => { + sysinfo_ver = x.clone(); + *PRO.lock().unwrap() = true; + x == ver + } + _ => { + false // to make sure Pro can be assigned in below post for old + // hbbs pro not supporting sysinfo_ver, use false for ensuring + } + }; + if samever { + info_uploaded = InfoUploaded::uploaded(url.clone(), id.clone(), sys_username); + log::info!("sysinfo not changed, skip upload"); + continue; + } + } + } + match crate::post_request(url.replace("heartbeat", "sysinfo"), v, "").await { + Ok(x) => { + if x == "SYSINFO_UPDATED" { + info_uploaded = InfoUploaded::uploaded(url.clone(), id.clone(), sys_username); + log::info!("sysinfo updated"); + if !hash.is_empty() { + config::Status::set("sysinfo_hash", hash); + config::Status::set("sysinfo_ver", sysinfo_ver.clone()); + } + *PRO.lock().unwrap() = true; + } else if x == "ID_NOT_FOUND" { + info_uploaded.last_uploaded = None; // next heartbeat will upload sysinfo again + } else { + info_uploaded.last_uploaded = Some(Instant::now()); + } + } + _ => { + info_uploaded.last_uploaded = Some(Instant::now()); + } + } + } + if conns.is_empty() && last_sent.map(|x| x.elapsed() < TIME_HEARTBEAT).unwrap_or(false) { + continue; + } + last_sent = Some(Instant::now()); + let mut v = Value::default(); + v["id"] = json!(id); + v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); + v["ver"] = json!(hbb_common::get_version_number(crate::VERSION)); + if !conns.is_empty() { + v["conns"] = json!(conns); + } + let modified_at = LocalConfig::get_option("strategy_timestamp").parse::().unwrap_or(0); + v["modified_at"] = json!(modified_at); + if let Ok(s) = crate::post_request(url.clone(), v.to_string(), "").await { + if let Ok(mut rsp) = serde_json::from_str::>(&s) { + if rsp.remove("sysinfo").is_some() { + info_uploaded.uploaded = false; + config::Status::set("sysinfo_hash", "".to_owned()); + log::info!("sysinfo required to forcely update"); + } + if let Some(conns) = rsp.remove("disconnect") { + if let Ok(conns) = serde_json::from_value::>(conns) { + SENDER.lock().unwrap().send(conns).ok(); + } + } + if let Some(rsp_modified_at) = rsp.remove("modified_at") { + if let Ok(rsp_modified_at) = serde_json::from_value::(rsp_modified_at) { + if rsp_modified_at != modified_at { + LocalConfig::set_option("strategy_timestamp".to_string(), rsp_modified_at.to_string()); + } + } + } + if let Some(strategy) = rsp.remove("strategy") { + if let Ok(strategy) = serde_json::from_value::(strategy) { + log::info!("strategy updated"); + handle_config_options(strategy.config_options); + } + } + } + } + } + } + } +} + +fn heartbeat_url() -> String { + let url = crate::common::get_api_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ); + if url.is_empty() || crate::is_public(&url) { + return "".to_owned(); + } + format!("{}/api/heartbeat", url) +} + +fn handle_config_options(config_options: HashMap) { + let mut options = Config::get_options(); + let default_settings = config::DEFAULT_SETTINGS.read().unwrap().clone(); + config_options + .iter() + .map(|(k, v)| { + // Priority: user config > default advanced options. + // Only when default advanced options are also empty, remove user option (fallback to built-in default); + // otherwise insert an empty value so user config remains present. + if v.is_empty() && default_settings.get(k).map_or("", |v| v).is_empty() { + options.remove(k); + } else { + options.insert(k.to_string(), v.to_string()); + } + }) + .count(); + Config::set_options(options); +} + +#[allow(unused)] +#[cfg(not(any(target_os = "ios")))] +pub fn is_pro() -> bool { + PRO.lock().unwrap().clone() +} diff --git a/vendor/rustdesk/src/ipc.rs b/vendor/rustdesk/src/ipc.rs new file mode 100644 index 0000000..e6d4fc8 --- /dev/null +++ b/vendor/rustdesk/src/ipc.rs @@ -0,0 +1,1701 @@ +use crate::{ + common::CheckTestNatType, + privacy_mode::PrivacyModeState, + ui_interface::{get_local_option, set_local_option}, +}; +use bytes::Bytes; +use parity_tokio_ipc::{ + Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, +}; +use serde_derive::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + sync::atomic::{AtomicBool, Ordering}, +}; +#[cfg(not(windows))] +use std::{fs::File, io::prelude::*}; + +#[cfg(all(feature = "flutter", feature = "plugin_framework"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::plugin::ipc::Plugin; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use clipboard::ClipboardFile; +use hbb_common::{ + allow_err, bail, bytes, + bytes_codec::BytesCodec, + config::{ + self, + keys::{self, OPTION_ALLOW_WEBSOCKET}, + Config, Config2, + }, + futures::StreamExt as _, + futures_util::sink::SinkExt, + log, password_security as password, timeout, + tokio::{ + self, + io::{AsyncRead, AsyncWrite}, + }, + tokio_util::codec::Framed, + ResultType, +}; + +use crate::{common::is_server, privacy_mode, rendezvous_mediator::RendezvousMediator}; + +// IPC actions here. +pub const IPC_ACTION_CLOSE: &str = "close"; +pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true); + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum FS { + ReadEmptyDirs { + dir: String, + include_hidden: bool, + }, + ReadDir { + dir: String, + include_hidden: bool, + }, + RemoveDir { + path: String, + id: i32, + recursive: bool, + }, + RemoveFile { + path: String, + id: i32, + file_num: i32, + }, + CreateDir { + path: String, + id: i32, + }, + NewWrite { + path: String, + id: i32, + file_num: i32, + files: Vec<(String, u64)>, + overwrite_detection: bool, + total_size: u64, + conn_id: i32, + }, + CancelWrite { + id: i32, + }, + WriteBlock { + id: i32, + file_num: i32, + data: Bytes, + compressed: bool, + }, + WriteDone { + id: i32, + file_num: i32, + }, + WriteError { + id: i32, + file_num: i32, + err: String, + }, + WriteOffset { + id: i32, + file_num: i32, + offset_blk: u32, + }, + CheckDigest { + id: i32, + file_num: i32, + file_size: u64, + last_modified: u64, + is_upload: bool, + is_resume: bool, + }, + SendConfirm(Vec), + Rename { + id: i32, + path: String, + new_name: String, + }, + // CM-side file reading operations (Windows only) + // These enable Connection Manager to read files and stream them back to Connection + ReadFile { + path: String, + id: i32, + file_num: i32, + include_hidden: bool, + conn_id: i32, + overwrite_detection: bool, + }, + CancelRead { + id: i32, + conn_id: i32, + }, + SendConfirmForRead { + id: i32, + file_num: i32, + skip: bool, + offset_blk: u32, + conn_id: i32, + }, + ReadAllFiles { + path: String, + id: i32, + include_hidden: bool, + conn_id: i32, + }, +} + +#[cfg(target_os = "windows")] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t")] +pub struct ClipboardNonFile { + pub compress: bool, + pub content: bytes::Bytes, + pub content_len: usize, + pub next_raw: bool, + pub width: i32, + pub height: i32, + // message.proto: ClipboardFormat + pub format: i32, + pub special_name: String, +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum DataKeyboard { + Sequence(String), + KeyDown(enigo::Key), + KeyUp(enigo::Key), + KeyClick(enigo::Key), + GetKeyState(enigo::Key), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum DataKeyboardResponse { + GetKeyState(bool), +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum DataMouse { + MoveTo(i32, i32), + MoveRelative(i32, i32), + Down(enigo::MouseButton), + Up(enigo::MouseButton), + Click(enigo::MouseButton), + ScrollX(i32), + ScrollY(i32), + Refresh, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum DataControl { + Resolution { + minx: i32, + maxx: i32, + miny: i32, + maxy: i32, + }, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum DataPortableService { + Ping, + Pong, + ConnCount(Option), + Mouse((Vec, i32, String, u32, bool, bool)), + Pointer((Vec, i32)), + Key(Vec), + RequestStart, + WillClose, + CmShowElevation(bool), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum Data { + Login { + id: i32, + is_file_transfer: bool, + is_view_camera: bool, + is_terminal: bool, + peer_id: String, + name: String, + avatar: String, + authorized: bool, + port_forward: String, + keyboard: bool, + clipboard: bool, + audio: bool, + file: bool, + file_transfer_enabled: bool, + restart: bool, + recording: bool, + block_input: bool, + privacy_mode: bool, + from_switch: bool, + }, + ChatMessage { + text: String, + }, + SwitchPermission { + name: String, + enabled: bool, + }, + SystemInfo(Option), + ClickTime(i64), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + MouseMoveTime(i64), + Authorize, + Close, + #[cfg(windows)] + SAS, + UserSid(Option), + OnlineStatus(Option<(i64, bool)>), + Config((String, Option)), + Options(Option>), + NatType(Option), + ConfirmedKey(Option<(Vec, Vec)>), + RawMessage(Vec), + Socks(Option), + FS(FS), + Test, + SyncConfig(Option>), + #[cfg(target_os = "windows")] + ClipboardFile(ClipboardFile), + ClipboardFileEnabled(bool), + #[cfg(target_os = "windows")] + ClipboardNonFile(Option<(String, Vec)>), + PrivacyModeState((i32, PrivacyModeState, String)), + TestRendezvousServer, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Keyboard(DataKeyboard), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + KeyboardResponse(DataKeyboardResponse), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Mouse(DataMouse), + Control(DataControl), + Theme(String), + Language(String), + Empty, + Disconnected, + DataPortableService(DataPortableService), + SwitchSidesRequest(String), + SwitchSidesBack, + UrlLink(String), + VoiceCallIncoming, + StartVoiceCall, + VoiceCallResponse(bool), + CloseVoiceCall(String), + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Plugin(Plugin), + #[cfg(windows)] + SyncWinCpuUsage(Option), + FileTransferLog((String, String)), + #[cfg(windows)] + ControlledSessionCount(usize), + CmErr(String), + // CM-side file reading responses (Windows only) + // These are sent from CM back to Connection when CM handles file reading + /// Response to ReadFile: contains initial file list or error + ReadJobInitResult { + id: i32, + file_num: i32, + include_hidden: bool, + conn_id: i32, + /// Serialized protobuf bytes of FileDirectory, or error string + result: Result, String>, + }, + /// File data block read by CM. + /// + /// The actual data is sent separately via `send_raw()` after this message to avoid + /// JSON encoding overhead for large binary data. This mirrors the `WriteBlock` pattern. + /// + /// **Protocol:** + /// - Sender: `send(FileBlockFromCM{...})` then `send_raw(data)` + /// - Receiver: `next()` returns `FileBlockFromCM`, then `next_raw()` returns data bytes + /// + /// **Note on empty data (e.g., empty files):** + /// Empty data is supported. The IPC connection uses `BytesCodec` with `raw=false` (default), + /// which prefixes each frame with a length header. So `send_raw(Bytes::new())` sends a + /// 1-byte frame (length=0), and `next_raw()` correctly returns an empty `BytesMut`. + /// See `libs/hbb_common/src/bytes_codec.rs` test `test_codec2` for verification. + FileBlockFromCM { + id: i32, + file_num: i32, + /// Data is sent separately via `send_raw()` to avoid JSON encoding overhead. + /// This field is skipped during serialization; sender must call `send_raw()` after sending. + /// Receiver must call `next_raw()` and populate this field manually. + #[serde(skip)] + data: bytes::Bytes, + compressed: bool, + conn_id: i32, + }, + /// File read completed successfully + FileReadDone { + id: i32, + file_num: i32, + conn_id: i32, + }, + /// File read failed with error + FileReadError { + id: i32, + file_num: i32, + err: String, + conn_id: i32, + }, + /// Digest info from CM for overwrite detection + FileDigestFromCM { + id: i32, + file_num: i32, + last_modified: u64, + file_size: u64, + is_resume: bool, + conn_id: i32, + }, + /// Response to ReadAllFiles: recursive directory listing + AllFilesResult { + id: i32, + conn_id: i32, + path: String, + /// Serialized protobuf bytes of FileDirectory, or error string + result: Result, String>, + }, + CheckHwcodec, + #[cfg(feature = "flutter")] + VideoConnCount(Option), + // Although the key is not necessary, it is used to avoid hardcoding the key. + WaylandScreencastRestoreToken((String, String)), + HwCodecConfig(Option), + RemoveTrustedDevices(Vec), + ClearTrustedDevices, + #[cfg(all(target_os = "windows", feature = "flutter"))] + PrinterData(Vec), + InstallOption(Option<(String, String)>), + #[cfg(all( + feature = "flutter", + not(any(target_os = "android", target_os = "ios")) + ))] + ControllingSessionCount(usize), + #[cfg(target_os = "linux")] + TerminalSessionCount(usize), + #[cfg(target_os = "windows")] + PortForwardSessionCount(Option), + SocksWs(Option, String)>>), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Whiteboard((String, crate::whiteboard::CustomEvent)), + ControlPermissionsRemoteModify(Option), + #[cfg(target_os = "windows")] + FileTransferEnabledState(Option), +} + +#[tokio::main(flavor = "current_thread")] +pub async fn start(postfix: &str) -> ResultType<()> { + let mut incoming = new_listener(postfix).await?; + loop { + if let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + let mut stream = Connection::new(stream); + let postfix = postfix.to_owned(); + tokio::spawn(async move { + loop { + match stream.next().await { + Err(err) => { + log::trace!("ipc '{}' connection closed: {}", postfix, err); + break; + } + Ok(Some(data)) => { + handle(data, &mut stream).await; + } + _ => {} + } + } + }); + } + Err(err) => { + log::error!("Couldn't get client: {:?}", err); + } + } + } + } +} + +pub async fn new_listener(postfix: &str) -> ResultType { + let path = Config::ipc_path(postfix); + #[cfg(not(any(windows, target_os = "android", target_os = "ios")))] + check_pid(postfix).await; + let mut endpoint = Endpoint::new(path.clone()); + match SecurityAttributes::allow_everyone_create() { + Ok(attr) => endpoint.set_security_attributes(attr), + Err(err) => log::error!("Failed to set ipc{} security: {}", postfix, err), + }; + match endpoint.incoming() { + Ok(incoming) => { + log::info!("Started ipc{} server at path: {}", postfix, &path); + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); + write_pid(postfix); + } + Ok(incoming) + } + Err(err) => { + log::error!( + "Failed to start ipc{} server at path {}: {}", + postfix, + path, + err + ); + Err(err.into()) + } + } +} + +pub struct CheckIfRestart { + stop_service: String, + rendezvous_servers: Vec, + audio_input: String, + voice_call_input: String, + ws: String, + disable_udp: String, + allow_insecure_tls_fallback: String, + api_server: String, +} + +impl CheckIfRestart { + pub fn new() -> CheckIfRestart { + CheckIfRestart { + stop_service: Config::get_option("stop-service"), + rendezvous_servers: Config::get_rendezvous_servers(), + 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), + allow_insecure_tls_fallback: Config::get_option( + config::keys::OPTION_ALLOW_INSECURE_TLS_FALLBACK, + ), + api_server: Config::get_option("api-server"), + } + } +} +impl Drop for CheckIfRestart { + fn drop(&mut self) { + // If 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 allow_insecure_tls_fallback_changed = self.allow_insecure_tls_fallback + != Config::get_option(config::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.api_server != Config::get_option("api-server") + { + if allow_insecure_tls_fallback_changed { + hbb_common::tls::reset_tls_cache(); + } + RendezvousMediator::restart(); + } + if self.audio_input != Config::get_option("audio-input") { + crate::audio_service::restart(); + } + if self.voice_call_input != Config::get_option("voice-call-input") { + crate::audio_service::set_voice_call_input_device( + Some(Config::get_option("voice-call-input")), + true, + ) + } + } +} + +async fn handle(data: Data, stream: &mut Connection) { + match data { + Data::SystemInfo(_) => { + let info = format!( + "log_path: {}, config: {}, username: {}", + Config::log_path().to_str().unwrap_or(""), + Config::file().to_str().unwrap_or(""), + crate::username(), + ); + allow_err!(stream.send(&Data::SystemInfo(Some(info))).await); + } + Data::ClickTime(_) => { + let t = crate::server::CLICK_TIME.load(Ordering::SeqCst); + allow_err!(stream.send(&Data::ClickTime(t)).await); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Data::MouseMoveTime(_) => { + let t = crate::server::MOUSE_MOVE_TIME.load(Ordering::SeqCst); + allow_err!(stream.send(&Data::MouseMoveTime(t)).await); + } + Data::Close => { + log::info!("Receive close message"); + if EXIT_RECV_CLOSE.load(Ordering::SeqCst) { + #[cfg(not(target_os = "android"))] + crate::server::input_service::fix_key_down_timeout_at_exit(); + if is_server() { + let _ = privacy_mode::turn_off_privacy(0, Some(PrivacyModeState::OffByPeer)); + } + #[cfg(any(target_os = "macos", target_os = "linux"))] + if crate::is_main() { + // below part is for main windows can be reopen during rustdesk installation and installing service from UI + // this make new ipc server (domain socket) can be created. + std::fs::remove_file(&Config::ipc_path("")).ok(); + #[cfg(target_os = "linux")] + { + hbb_common::sleep((crate::platform::SERVICE_INTERVAL * 2) as f32 / 1000.0) + .await; + // https://github.com/rustdesk/rustdesk/discussions/9254 + crate::run_me::<&str>(vec!["--no-server"]).ok(); + } + #[cfg(target_os = "macos")] + { + // our launchagent interval is 1 second + hbb_common::sleep(1.5).await; + std::process::Command::new("open") + .arg("-n") + .arg(&format!("/Applications/{}.app", crate::get_app_name())) + .spawn() + .ok(); + } + // leave above open a little time + hbb_common::sleep(0.3).await; + // in case below exit failed + crate::platform::quit_gui(); + } + std::process::exit(-1); // to make sure --server luauchagent process can restart because SuccessfulExit used + } + } + Data::OnlineStatus(_) => { + let x = config::get_online_state(); + let confirmed = Config::get_key_confirmed(); + allow_err!(stream.send(&Data::OnlineStatus(Some((x, confirmed)))).await); + } + Data::ConfirmedKey(None) => { + let out = if Config::get_key_confirmed() { + Some(Config::get_key_pair()) + } else { + None + }; + allow_err!(stream.send(&Data::ConfirmedKey(out)).await); + } + Data::Socks(s) => match s { + None => { + allow_err!(stream.send(&Data::Socks(Config::get_socks())).await); + } + Some(data) => { + let _nat = CheckTestNatType::new(); + if data.proxy.is_empty() { + Config::set_socks(None); + } else { + Config::set_socks(Some(data)); + } + RendezvousMediator::restart(); + log::info!("socks updated"); + } + }, + Data::SocksWs(s) => match s { + None => { + allow_err!( + stream + .send(&Data::SocksWs(Some(Box::new(( + Config::get_socks(), + Config::get_option(OPTION_ALLOW_WEBSOCKET) + ))))) + .await + ); + } + _ => {} + }, + #[cfg(feature = "flutter")] + Data::VideoConnCount(None) => { + let n = crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|x| x.conn_type == crate::server::AuthConnType::Remote) + .count(); + allow_err!(stream.send(&Data::VideoConnCount(Some(n))).await); + } + Data::Config((name, value)) => match value { + None => { + let value; + if name == "id" { + value = Some(Config::get_id()); + } else if name == "temporary-password" { + value = Some(password::temporary_password()); + } else if name == "permanent-password-storage-and-salt" { + let (storage, salt) = Config::get_local_permanent_password_storage_and_salt(); + value = Some(storage + "\n" + &salt); + } else if name == "permanent-password-set" { + value = Some(if Config::has_permanent_password() { + "Y".to_owned() + } else { + "N".to_owned() + }); + } else if name == "permanent-password-is-preset" { + let hard = config::HARD_SETTINGS + .read() + .unwrap() + .get("password") + .cloned() + .unwrap_or_default(); + let is_preset = + !hard.is_empty() && Config::matches_permanent_password_plain(&hard); + value = Some(if is_preset { + "Y".to_owned() + } else { + "N".to_owned() + }); + } else if name == "salt" { + value = Some(Config::get_salt()); + } else if name == "rendezvous_server" { + value = Some(format!( + "{},{}", + Config::get_rendezvous_server(), + Config::get_rendezvous_servers().join(",") + )); + } else if name == "rendezvous_servers" { + value = Some(Config::get_rendezvous_servers().join(",")); + } else if name == "fingerprint" { + value = if Config::get_key_confirmed() { + Some(crate::common::pk_to_fingerprint(Config::get_key_pair().1)) + } else { + None + }; + } else if name == "hide_cm" { + value = if crate::hbbs_http::sync::is_pro() || crate::common::is_custom_client() + { + Some(hbb_common::password_security::hide_cm().to_string()) + } else { + None + }; + } else if name == "voice-call-input" { + value = crate::audio_service::get_voice_call_input_device(); + } else if name == "unlock-pin" { + value = Some(Config::get_unlock_pin()); + } else if name == "trusted-devices" { + value = Some(Config::get_trusted_devices_json()); + } else { + value = None; + } + allow_err!(stream.send(&Data::Config((name, value))).await); + } + Some(value) => { + let mut updated = true; + if name == "id" { + Config::set_key_confirmed(false); + Config::set_id(&value); + } else if name == "temporary-password" { + password::update_temporary_password(); + } else if name == "permanent-password" { + if Config::is_disable_change_permanent_password() { + log::warn!("Changing permanent password is disabled"); + updated = false; + } else { + Config::set_permanent_password(&value); + } + // Explicitly ACK/NACK permanent-password writes. This allows UIs/FFI to + // distinguish "accepted by daemon" vs "IPC send succeeded" without + // reading back any secret. + let ack = if updated { "Y" } else { "N" }.to_owned(); + allow_err!(stream.send(&Data::Config((name.clone(), Some(ack)))).await); + } else if name == "salt" { + Config::set_salt(&value); + } else if name == "voice-call-input" { + crate::audio_service::set_voice_call_input_device(Some(value), true); + } else if name == "unlock-pin" { + Config::set_unlock_pin(&value); + } else { + return; + } + if updated { + log::info!("{} updated", name); + } + } + }, + Data::Options(value) => match value { + None => { + let v = Config::get_options(); + allow_err!(stream.send(&Data::Options(Some(v))).await); + } + Some(value) => { + let _chk = CheckIfRestart::new(); + let _nat = CheckTestNatType::new(); + if let Some(v) = value.get("privacy-mode-impl-key") { + crate::privacy_mode::switch(v); + } + Config::set_options(value); + allow_err!(stream.send(&Data::Options(None)).await); + } + }, + Data::NatType(_) => { + let t = Config::get_nat_type(); + allow_err!(stream.send(&Data::NatType(Some(t))).await); + } + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; + let _chk = CheckIfRestart::new(); + Config::set(config); + Config2::set(config2); + allow_err!(stream.send(&Data::SyncConfig(None)).await); + } + Data::SyncConfig(None) => { + allow_err!( + stream + .send(&Data::SyncConfig(Some( + (Config::get(), Config2::get()).into() + ))) + .await + ); + } + #[cfg(windows)] + Data::SyncWinCpuUsage(None) => { + allow_err!( + stream + .send(&Data::SyncWinCpuUsage( + hbb_common::platform::windows::cpu_uage_one_minute() + )) + .await + ); + } + Data::TestRendezvousServer => { + crate::test_rendezvous_server(); + } + Data::SwitchSidesRequest(id) => { + let uuid = uuid::Uuid::new_v4(); + crate::server::insert_switch_sides_uuid(id, uuid.clone()); + allow_err!( + stream + .send(&Data::SwitchSidesRequest(uuid.to_string())) + .await + ); + } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Data::Plugin(plugin) => crate::plugin::ipc::handle_plugin(plugin, stream).await, + #[cfg(windows)] + Data::ControlledSessionCount(_) => { + allow_err!( + stream + .send(&Data::ControlledSessionCount( + crate::Connection::alive_conns().len() + )) + .await + ); + } + #[cfg(all( + feature = "flutter", + not(any(target_os = "android", target_os = "ios")) + ))] + Data::ControllingSessionCount(count) => { + crate::updater::update_controlling_session_count(count); + } + #[cfg(target_os = "linux")] + Data::TerminalSessionCount(_) => { + let count = crate::terminal_service::get_terminal_session_count(true); + allow_err!(stream.send(&Data::TerminalSessionCount(count)).await); + } + #[cfg(feature = "hwcodec")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Data::CheckHwcodec => { + scrap::hwcodec::start_check_process(); + } + #[cfg(feature = "hwcodec")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Data::HwCodecConfig(c) => { + match c { + None => { + let v = match scrap::hwcodec::HwCodecConfig::get_set_value() { + Some(v) => Some(serde_json::to_string(&v).unwrap_or_default()), + None => None, + }; + allow_err!(stream.send(&Data::HwCodecConfig(v)).await); + } + Some(v) => { + // --server and portable + scrap::hwcodec::HwCodecConfig::set(v); + } + } + } + Data::WaylandScreencastRestoreToken((key, value)) => { + let v = if value == "get" { + let opt = get_local_option(key.clone()); + #[cfg(not(target_os = "linux"))] + { + Some(opt) + } + #[cfg(target_os = "linux")] + { + let v = if opt.is_empty() { + if scrap::wayland::pipewire::is_rdp_session_hold() { + "fake token".to_string() + } else { + "".to_owned() + } + } else { + opt + }; + Some(v) + } + } else if value == "clear" { + set_local_option(key.clone(), "".to_owned()); + #[cfg(target_os = "linux")] + scrap::wayland::pipewire::close_session(); + Some("".to_owned()) + } else { + None + }; + if let Some(v) = v { + allow_err!( + stream + .send(&Data::WaylandScreencastRestoreToken((key, v))) + .await + ); + } + } + Data::RemoveTrustedDevices(v) => { + Config::remove_trusted_devices(&v); + } + Data::ClearTrustedDevices => { + Config::clear_trusted_devices(); + } + Data::InstallOption(opt) => match opt { + Some((_k, _v)) => { + #[cfg(target_os = "windows")] + if let Err(e) = crate::platform::windows::update_install_option(&_k, &_v) { + log::error!( + "Failed to update install option \"{}\" to \"{}\", error: {}", + &_k, + &_v, + e + ); + } + } + None => { + // `None` is usually used to get values. + // This branch is left blank for unification and further use. + } + }, + #[cfg(target_os = "windows")] + Data::PortForwardSessionCount(c) => match c { + None => { + let count = crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.conn_type == crate::server::AuthConnType::PortForward) + .count(); + allow_err!( + stream + .send(&Data::PortForwardSessionCount(Some(count))) + .await + ); + } + _ => { + // Port forward session count is only a get value. + } + }, + Data::ControlPermissionsRemoteModify(_) => { + use hbb_common::rendezvous_proto::control_permissions::Permission; + let state = + crate::server::get_control_permission_state(Permission::remote_modify, true); + allow_err!( + stream + .send(&Data::ControlPermissionsRemoteModify(state)) + .await + ); + } + #[cfg(target_os = "windows")] + Data::FileTransferEnabledState(_) => { + use hbb_common::rendezvous_proto::control_permissions::Permission; + 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, + ) + }); + allow_err!( + stream + .send(&Data::FileTransferEnabledState(Some(enabled))) + .await + ); + } + _ => {} + } +} + +pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType> { + let path = Config::ipc_path(postfix); + let client = timeout(ms_timeout, Endpoint::connect(&path)).await??; + Ok(ConnectionTmpl::new(client)) +} + +#[cfg(target_os = "linux")] +#[tokio::main(flavor = "current_thread")] +pub async fn start_pa() { + use crate::audio_service::AUDIO_DATA_SIZE_U8; + + match new_listener("_pa").await { + Ok(mut incoming) => { + loop { + if let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + let mut stream = Connection::new(stream); + let mut device: String = "".to_owned(); + if let Some(Ok(Some(Data::Config((_, Some(x)))))) = + stream.next_timeout2(1000).await + { + device = x; + } + if !device.is_empty() { + device = crate::platform::linux::get_pa_source_name(&device); + } + if device.is_empty() { + device = crate::platform::linux::get_pa_monitor(); + } + if device.is_empty() { + continue; + } + let spec = pulse::sample::Spec { + format: pulse::sample::Format::F32le, + channels: 2, + rate: crate::platform::PA_SAMPLE_RATE, + }; + log::info!("pa monitor: {:?}", device); + // systemctl --user status pulseaudio.service + let mut buf: Vec = vec![0; AUDIO_DATA_SIZE_U8]; + match psimple::Simple::new( + None, // Use the default server + &crate::get_app_name(), // Our application’s name + pulse::stream::Direction::Record, // We want a record stream + Some(&device), // Use the default device + "record", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + None, // Use default buffering attributes + ) { + Ok(s) => loop { + if let Ok(_) = s.read(&mut buf) { + let out = + if buf.iter().filter(|x| **x != 0).next().is_none() { + vec![] + } else { + buf.clone() + }; + if let Err(err) = stream.send_raw(out.into()).await { + log::error!("Failed to send audio data:{}", err); + break; + } + } + }, + Err(err) => { + log::error!("Could not create simple pulse: {}", err); + } + } + } + Err(err) => { + log::error!("Couldn't get pa client: {:?}", err); + } + } + } + } + } + Err(err) => { + log::error!("Failed to start pa ipc server: {}", err); + } + } +} + +#[inline] +#[cfg(not(windows))] +fn get_pid_file(postfix: &str) -> String { + let path = Config::ipc_path(postfix); + format!("{}.pid", path) +} + +#[cfg(not(any(windows, target_os = "android", target_os = "ios")))] +async fn check_pid(postfix: &str) { + let pid_file = get_pid_file(postfix); + if let Ok(mut file) = File::open(&pid_file) { + let mut content = String::new(); + file.read_to_string(&mut content).ok(); + let pid = content.parse::().unwrap_or(0); + if pid > 0 { + use hbb_common::sysinfo::System; + let mut sys = System::new(); + sys.refresh_processes(); + if let Some(p) = sys.process(pid.into()) { + if let Some(current) = sys.process((std::process::id() as usize).into()) { + if current.name() == p.name() { + // double check with connect + if connect(1000, postfix).await.is_ok() { + return; + } + } + } + } + } + } + // if not remove old ipc file, the new ipc creation will fail + // if we remove a ipc file, but the old ipc process is still running, + // new connection to the ipc will connect to new ipc, old connection to old ipc still keep alive + std::fs::remove_file(&Config::ipc_path(postfix)).ok(); +} + +#[inline] +#[cfg(not(windows))] +fn write_pid(postfix: &str) { + let path = get_pid_file(postfix); + if let Ok(mut file) = File::create(&path) { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); + file.write_all(&std::process::id().to_string().into_bytes()) + .ok(); + } +} + +pub struct ConnectionTmpl { + inner: Framed, +} + +pub type Connection = ConnectionTmpl; + +impl ConnectionTmpl +where + T: AsyncRead + AsyncWrite + std::marker::Unpin, +{ + pub fn new(conn: T) -> Self { + Self { + inner: Framed::new(conn, BytesCodec::new()), + } + } + + pub async fn send(&mut self, data: &Data) -> ResultType<()> { + let v = serde_json::to_vec(data)?; + self.inner.send(bytes::Bytes::from(v)).await?; + Ok(()) + } + + async fn send_config(&mut self, name: &str, value: String) -> ResultType<()> { + self.send(&Data::Config((name.to_owned(), Some(value)))) + .await + } + + pub async fn next_timeout(&mut self, ms_timeout: u64) -> ResultType> { + Ok(timeout(ms_timeout, self.next()).await??) + } + + pub async fn next_timeout2(&mut self, ms_timeout: u64) -> Option>> { + if let Ok(x) = timeout(ms_timeout, self.next()).await { + Some(x) + } else { + None + } + } + + pub async fn next(&mut self) -> ResultType> { + match self.inner.next().await { + Some(res) => { + let bytes = res?; + if let Ok(s) = std::str::from_utf8(&bytes) { + if let Ok(data) = serde_json::from_str::(s) { + return Ok(Some(data)); + } + } + return Ok(None); + } + _ => { + bail!("reset by the peer"); + } + } + } + + pub async fn send_raw(&mut self, data: Bytes) -> ResultType<()> { + self.inner.send(data).await?; + Ok(()) + } + + pub async fn next_raw(&mut self) -> ResultType { + match self.inner.next().await { + Some(Ok(res)) => Ok(res), + _ => { + bail!("reset by the peer"); + } + } + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_config(name: &str) -> ResultType> { + get_config_async(name, 1_000).await +} + +async fn get_config_async(name: &str, ms_timeout: u64) -> ResultType> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::Config((name.to_owned(), None))).await?; + if let Some(Data::Config((name2, value))) = c.next_timeout(ms_timeout).await? { + if name == name2 { + return Ok(value); + } + } + return Ok(None); +} + +pub async fn set_config_async(name: &str, value: String) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send_config(name, value).await?; + Ok(()) +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_data(data: &Data) -> ResultType<()> { + set_data_async(data).await +} + +async fn set_data_async(data: &Data) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(data).await?; + Ok(()) +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_config(name: &str, value: String) -> ResultType<()> { + set_config_async(name, value).await +} + +pub fn update_temporary_password() -> ResultType<()> { + set_config("temporary-password", "".to_owned()) +} + +fn apply_permanent_password_storage_and_salt_payload(payload: Option<&str>) -> ResultType<()> { + let Some(payload) = payload else { + return Ok(()); + }; + let Some((storage, salt)) = payload.split_once('\n') else { + bail!("Invalid permanent-password-storage-and-salt payload"); + }; + + if storage.is_empty() { + Config::set_permanent_password_storage_for_sync("", "")?; + return Ok(()); + } + + Config::set_permanent_password_storage_for_sync(storage, salt)?; + Ok(()) +} + +pub fn sync_permanent_password_storage_from_daemon() -> ResultType<()> { + let v = get_config("permanent-password-storage-and-salt")?; + apply_permanent_password_storage_and_salt_payload(v.as_deref()) +} + +async fn sync_permanent_password_storage_from_daemon_async() -> ResultType<()> { + let ms_timeout = 1_000; + let v = get_config_async("permanent-password-storage-and-salt", ms_timeout).await?; + apply_permanent_password_storage_and_salt_payload(v.as_deref()) +} + +pub fn is_permanent_password_set() -> bool { + match get_config("permanent-password-set") { + Ok(Some(v)) => { + let v = v.trim(); + return v == "Y"; + } + Ok(None) => { + // No response/value (timeout). + } + Err(_) => { + // Connection error. + } + } + log::warn!("Failed to query permanent password state from daemon"); + false +} + +pub fn is_permanent_password_preset() -> bool { + if let Ok(Some(v)) = get_config("permanent-password-is-preset") { + let v = v.trim(); + return v == "Y"; + } + false +} + +pub fn get_fingerprint() -> String { + get_config("fingerprint") + .unwrap_or_default() + .unwrap_or_default() +} + +pub fn set_permanent_password(v: String) -> ResultType<()> { + if Config::is_disable_change_permanent_password() { + bail!("Changing permanent password is disabled"); + } + if set_permanent_password_with_ack(v)? { + Ok(()) + } else { + bail!("Changing permanent password was rejected by daemon"); + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_permanent_password_with_ack(v: String) -> ResultType { + set_permanent_password_with_ack_async(v).await +} + +async fn set_permanent_password_with_ack_async(v: String) -> ResultType { + // The daemon ACK/NACK is expected quickly since it applies the config in-process. + let ms_timeout = 1_000; + let mut c = connect(ms_timeout, "").await?; + c.send_config("permanent-password", v).await?; + if let Some(Data::Config((name2, Some(v)))) = c.next_timeout(ms_timeout).await? { + if name2 == "permanent-password" { + let v = v.trim(); + let ok = v == "Y"; + if ok { + // Ensure the hashed permanent password storage is written to the user config file. + // This sync must not affect the daemon ACK outcome. + if let Err(err) = sync_permanent_password_storage_from_daemon_async().await { + log::warn!("Failed to sync permanent password storage from daemon: {err}"); + } + } + return Ok(ok); + } + } + Ok(false) +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn set_unlock_pin(v: String, translate: bool) -> ResultType<()> { + let v = v.trim().to_owned(); + let min_len = 4; + let max_len = crate::ui_interface::max_encrypt_len(); + let len = v.chars().count(); + if !v.is_empty() { + if len < min_len { + let err = if translate { + crate::lang::translate( + "Requires at least {".to_string() + &format!("{min_len}") + "} characters", + ) + } else { + // Sometimes, translated can't show normally in command line + format!("Requires at least {} characters", min_len) + }; + bail!(err); + } + if len > max_len { + bail!("No more than {max_len} characters"); + } + } + Config::set_unlock_pin(&v); + set_config("unlock-pin", v) +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_unlock_pin() -> String { + if let Ok(Some(v)) = get_config("unlock-pin") { + Config::set_unlock_pin(&v); + v + } else { + Config::get_unlock_pin() + } +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_trusted_devices() -> String { + if let Ok(Some(v)) = get_config("trusted-devices") { + v + } else { + Config::get_trusted_devices_json() + } +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn remove_trusted_devices(hwids: Vec) { + Config::remove_trusted_devices(&hwids); + allow_err!(set_data(&Data::RemoveTrustedDevices(hwids))); +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn clear_trusted_devices() { + Config::clear_trusted_devices(); + allow_err!(set_data(&Data::ClearTrustedDevices)); +} + +pub fn get_id() -> String { + if let Ok(Some(v)) = get_config("id") { + // update salt also, so that next time reinstallation not causing first-time auto-login failure + if let Ok(Some(v2)) = get_config("salt") { + Config::set_salt(&v2); + } + if v != Config::get_id() { + Config::set_key_confirmed(false); + Config::set_id(&v); + } + v + } else { + Config::get_id() + } +} + +pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec) { + if let Ok(Some(v)) = get_config_async("rendezvous_server", ms_timeout).await { + let mut urls = v.split(","); + let a = urls.next().unwrap_or_default().to_owned(); + let b: Vec = urls.map(|x| x.to_owned()).collect(); + (a, b) + } else { + ( + Config::get_rendezvous_server(), + Config::get_rendezvous_servers(), + ) + } +} + +async fn get_options_(ms_timeout: u64) -> ResultType> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::Options(None)).await?; + if let Some(Data::Options(Some(value))) = c.next_timeout(ms_timeout).await? { + Config::set_options(value.clone()); + Ok(value) + } else { + Ok(Config::get_options()) + } +} + +pub async fn get_options_async() -> HashMap { + get_options_(1000).await.unwrap_or(Config::get_options()) +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_options() -> HashMap { + get_options_async().await +} + +pub async fn get_option_async(key: &str) -> String { + if let Some(v) = get_options_async().await.get(key) { + v.clone() + } else { + "".to_owned() + } +} + +pub fn set_option(key: &str, value: &str) { + let mut options = get_options(); + if value.is_empty() { + options.remove(key); + } else { + options.insert(key.to_owned(), value.to_owned()); + } + set_options(options).ok(); +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_options(value: HashMap) -> ResultType<()> { + let _nat = CheckTestNatType::new(); + if let Ok(mut c) = connect(1000, "").await { + c.send(&Data::Options(Some(value.clone()))).await?; + // do not put below before connect, because we need to check should_exit + c.next_timeout(1000).await.ok(); + } + Config::set_options(value); + Ok(()) +} + +#[inline] +async fn get_nat_type_(ms_timeout: u64) -> ResultType { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::NatType(None)).await?; + if let Some(Data::NatType(Some(value))) = c.next_timeout(ms_timeout).await? { + Config::set_nat_type(value); + Ok(value) + } else { + Ok(Config::get_nat_type()) + } +} + +pub async fn get_nat_type(ms_timeout: u64) -> i32 { + get_nat_type_(ms_timeout) + .await + .unwrap_or(Config::get_nat_type()) +} + +pub async fn get_rendezvous_servers(ms_timeout: u64) -> Vec { + if let Ok(Some(v)) = get_config_async("rendezvous_servers", ms_timeout).await { + return v.split(',').map(|x| x.to_owned()).collect(); + } + return Config::get_rendezvous_servers(); +} + +#[inline] +async fn get_socks_(ms_timeout: u64) -> ResultType> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::Socks(None)).await?; + if let Some(Data::Socks(value)) = c.next_timeout(ms_timeout).await? { + Config::set_socks(value.clone()); + Ok(value) + } else { + Ok(Config::get_socks()) + } +} + +pub async fn get_socks_async(ms_timeout: u64) -> Option { + get_socks_(ms_timeout).await.unwrap_or(Config::get_socks()) +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_socks() -> Option { + get_socks_async(1_000).await +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> { + let _nat = CheckTestNatType::new(); + Config::set_socks(if value.proxy.is_empty() { + None + } else { + Some(value.clone()) + }); + connect(1_000, "") + .await? + .send(&Data::Socks(Some(value))) + .await?; + Ok(()) +} + +async fn get_socks_ws_(ms_timeout: u64) -> ResultType<(Option, String)> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::SocksWs(None)).await?; + if let Some(Data::SocksWs(Some(value))) = c.next_timeout(ms_timeout).await? { + Config::set_socks(value.0.clone()); + Config::set_option(OPTION_ALLOW_WEBSOCKET.to_string(), value.1.clone()); + Ok(*value) + } else { + Ok(( + Config::get_socks(), + Config::get_option(OPTION_ALLOW_WEBSOCKET), + )) + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_socks_ws() -> (Option, String) { + get_socks_ws_(1_000).await.unwrap_or(( + Config::get_socks(), + Config::get_option(OPTION_ALLOW_WEBSOCKET), + )) +} + +pub fn get_proxy_status() -> bool { + Config::get_socks().is_some() +} +#[tokio::main(flavor = "current_thread")] +pub async fn test_rendezvous_server() -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::TestRendezvousServer).await?; + Ok(()) +} + +#[tokio::main(flavor = "current_thread")] +pub async fn send_url_scheme(url: String) -> ResultType<()> { + connect(1_000, "_url") + .await? + .send(&Data::UrlLink(url)) + .await?; + Ok(()) +} + +// Emit `close` events to ipc. +pub fn close_all_instances() -> ResultType { + match crate::ipc::send_url_scheme(IPC_ACTION_CLOSE.to_owned()) { + Ok(_) => Ok(true), + Err(err) => Err(err), + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn connect_to_user_session(usid: Option) -> ResultType<()> { + let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; + timeout(1000, stream.send(&crate::ipc::Data::UserSid(usid))).await??; + Ok(()) +} + +#[tokio::main(flavor = "current_thread")] +pub async fn notify_server_to_check_hwcodec() -> ResultType<()> { + connect(1_000, "").await?.send(&&Data::CheckHwcodec).await?; + Ok(()) +} + +#[cfg(target_os = "windows")] +pub async fn get_port_forward_session_count(ms_timeout: u64) -> ResultType { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::PortForwardSessionCount(None)).await?; + if let Some(Data::PortForwardSessionCount(Some(count))) = c.next_timeout(ms_timeout).await? { + return Ok(count); + } + bail!("Failed to get port forward session count"); +} + +#[cfg(feature = "hwcodec")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[tokio::main(flavor = "current_thread")] +pub async fn get_hwcodec_config_from_server() -> ResultType<()> { + if !scrap::codec::enable_hwcodec_option() || scrap::hwcodec::HwCodecConfig::already_set() { + return Ok(()); + } + let mut c = connect(50, "").await?; + c.send(&Data::HwCodecConfig(None)).await?; + if let Some(Data::HwCodecConfig(v)) = c.next_timeout(50).await? { + match v { + Some(v) => { + scrap::hwcodec::HwCodecConfig::set(v); + return Ok(()); + } + None => { + bail!("hwcodec config is none"); + } + } + } + bail!("failed to get hwcodec config"); +} + +#[cfg(feature = "hwcodec")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn client_get_hwcodec_config_thread(wait_sec: u64) { + static ONCE: std::sync::Once = std::sync::Once::new(); + if !crate::platform::is_installed() + || !scrap::codec::enable_hwcodec_option() + || scrap::hwcodec::HwCodecConfig::already_set() + { + return; + } + ONCE.call_once(move || { + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(1)); + let mut intervals: Vec = vec![wait_sec, 3, 3, 6, 9]; + for i in intervals.drain(..) { + if i > 0 { + std::thread::sleep(std::time::Duration::from_secs(i)); + } + if get_hwcodec_config_from_server().is_ok() { + break; + } + } + }); + }); +} + +#[cfg(feature = "hwcodec")] +#[tokio::main(flavor = "current_thread")] +pub async fn hwcodec_process() { + let s = scrap::hwcodec::check_available_hwcodec(); + for _ in 0..5 { + match crate::ipc::connect(1000, "").await { + Ok(mut conn) => { + match conn + .send(&crate::ipc::Data::HwCodecConfig(Some(s.clone()))) + .await + { + Ok(()) => { + log::info!("send ok"); + break; + } + Err(e) => { + log::error!("send failed: {e:?}"); + } + } + } + Err(e) => { + log::error!("connect failed: {e:?}"); + } + } + std::thread::sleep(std::time::Duration::from_secs(1)); + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_wayland_screencast_restore_token(key: String) -> ResultType { + let v = handle_wayland_screencast_restore_token(key, "get".to_owned()).await?; + Ok(v.unwrap_or_default()) +} + +#[tokio::main(flavor = "current_thread")] +pub async fn clear_wayland_screencast_restore_token(key: String) -> ResultType { + if let Some(v) = handle_wayland_screencast_restore_token(key, "clear".to_owned()).await? { + return Ok(v.is_empty()); + } + return Ok(false); +} + +#[cfg(all( + feature = "flutter", + not(any(target_os = "android", target_os = "ios")) +))] +#[tokio::main(flavor = "current_thread")] +pub async fn update_controlling_session_count(count: usize) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::ControllingSessionCount(count)).await?; + Ok(()) +} + +#[cfg(target_os = "linux")] +#[tokio::main(flavor = "current_thread")] +pub async fn get_terminal_session_count() -> ResultType { + let ms_timeout = 1_000; + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::TerminalSessionCount(0)).await?; + if let Some(Data::TerminalSessionCount(c)) = c.next_timeout(ms_timeout).await? { + return Ok(c); + } + Ok(0) +} + +async fn handle_wayland_screencast_restore_token( + key: String, + value: String, +) -> ResultType> { + let ms_timeout = 1_000; + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::WaylandScreencastRestoreToken((key, value))) + .await?; + if let Some(Data::WaylandScreencastRestoreToken((_key, v))) = c.next_timeout(ms_timeout).await? + { + return Ok(Some(v)); + } + return Ok(None); +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_install_option(k: String, v: String) -> ResultType<()> { + if let Ok(mut c) = connect(1000, "").await { + c.send(&&Data::InstallOption(Some((k, v)))).await?; + // do not put below before connect, because we need to check should_exit + c.next_timeout(1000).await.ok(); + } + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn verify_ffi_enum_data_size() { + println!("{}", std::mem::size_of::()); + assert!(std::mem::size_of::() <= 120); + } +} diff --git a/vendor/rustdesk/src/kcp_stream.rs b/vendor/rustdesk/src/kcp_stream.rs new file mode 100644 index 0000000..74bd841 --- /dev/null +++ b/vendor/rustdesk/src/kcp_stream.rs @@ -0,0 +1,151 @@ +use hbb_common::{ + anyhow, + bytes::{Bytes, BytesMut}, + bytes_codec::BytesCodec, + config, log, + tcp::{DynTcpStream, FramedStream}, + tokio::{self, net::UdpSocket, sync::mpsc, sync::oneshot}, + tokio_util, ResultType, Stream, +}; +use kcp_sys::{ + endpoint::KcpEndpoint, + packet_def::{KcpPacket, KcpPacketHeader}, + stream, +}; +use std::{net::SocketAddr, sync::Arc}; + +pub struct KcpStream { + _endpoint: KcpEndpoint, + stop_sender: Option>, +} + +impl KcpStream { + fn create_framed(stream: stream::KcpStream, local_addr: Option) -> Stream { + Stream::Tcp(FramedStream( + tokio_util::codec::Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + local_addr.unwrap_or(config::Config::get_any_listen_addr(true)), + None, + 0, + )) + } + + pub async fn accept( + udp_socket: Arc, + timeout: std::time::Duration, + init_packet: Option, + ) -> ResultType<(Self, Stream)> { + let mut endpoint = KcpEndpoint::new(); + endpoint.run().await; + + let (input, output) = ( + endpoint.input_sender(), + endpoint + .output_receiver() + .ok_or_else(|| anyhow::anyhow!("Failed to get output receiver"))?, + ); + let (stop_sender, stop_receiver) = oneshot::channel(); + if let Some(packet) = init_packet { + if packet.len() >= std::mem::size_of::() { + input.send(packet.into()).await?; + } + } + Self::kcp_io(udp_socket.clone(), input, output, stop_receiver).await; + + let conn_id = tokio::time::timeout(timeout, endpoint.accept()).await??; + if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) { + Ok(( + Self { + _endpoint: endpoint, + stop_sender: Some(stop_sender), + }, + Self::create_framed(stream, udp_socket.local_addr().ok()), + )) + } else { + Err(anyhow::anyhow!("Failed to create KcpStream")) + } + } + + pub async fn connect( + udp_socket: Arc, + timeout: std::time::Duration, + ) -> ResultType<(Self, Stream)> { + let mut endpoint = KcpEndpoint::new(); + endpoint.run().await; + + let (input, output) = ( + endpoint.input_sender(), + endpoint + .output_receiver() + .ok_or_else(|| anyhow::anyhow!("Failed to get output receiver"))?, + ); + let (stop_sender, stop_receiver) = oneshot::channel(); + Self::kcp_io(udp_socket.clone(), input, output, stop_receiver).await; + + let conn_id = endpoint.connect(timeout, 0, 0, Bytes::new()).await?; + if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) { + Ok(( + Self { + _endpoint: endpoint, + stop_sender: Some(stop_sender), + }, + Self::create_framed(stream, udp_socket.local_addr().ok()), + )) + } else { + Err(anyhow::anyhow!("Failed to create KcpStream")) + } + } + + async fn kcp_io( + udp_socket: Arc, + input: mpsc::Sender, + mut output: mpsc::Receiver, + mut stop_receiver: oneshot::Receiver<()>, + ) { + let udp = udp_socket.clone(); + tokio::spawn(async move { + let mut buf = vec![0; 1500]; + loop { + tokio::select! { + _ = &mut stop_receiver => { + log::debug!("KCP io loop received stop signal"); + break; + } + Some(data) = output.recv() => { + if let Err(e) = udp.send(&data.inner()).await { + log::debug!("KCP send error: {:?}", e); + break; + } + } + result = udp.recv_from(&mut buf) => { + match result { + Ok((size, _)) => { + if size < std::mem::size_of::() { + continue; + } + input + .send(BytesMut::from(&buf[..size]).into()) + .await.ok(); + } + Err(e) => { + log::debug!("KCP recv_from error: {:?}", e); + break; + } + } + } + else => { + log::debug!("KCP endpoint input closed"); + break; + } + } + } + }); + } +} + +impl Drop for KcpStream { + fn drop(&mut self) { + if let Some(sender) = self.stop_sender.take() { + let _ = sender.send(()); + } + } +} diff --git a/vendor/rustdesk/src/keyboard.rs b/vendor/rustdesk/src/keyboard.rs new file mode 100644 index 0000000..b9cf4da --- /dev/null +++ b/vendor/rustdesk/src/keyboard.rs @@ -0,0 +1,1598 @@ +#[cfg(feature = "flutter")] +use crate::flutter; +#[cfg(target_os = "windows")] +use crate::platform::windows::{get_char_from_vk, get_unicode_from_vk}; +#[cfg(not(any(feature = "flutter", feature = "cli")))] +use crate::ui::CUR_SESSION; +use crate::ui_session_interface::{InvokeUiSession, Session}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +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::*; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use rdev::KeyCode; +use rdev::{Event, EventType, Key}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use std::sync::atomic::{AtomicBool, Ordering}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +#[cfg(windows)] +static mut IS_ALT_GR: bool = false; + +#[allow(dead_code)] +const OS_LOWER_WINDOWS: &str = "windows"; +#[allow(dead_code)] +const OS_LOWER_LINUX: &str = "linux"; +#[allow(dead_code)] +const OS_LOWER_MACOS: &str = "macos"; +#[allow(dead_code)] +const OS_LOWER_ANDROID: &str = "android"; + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); + +// Track key down state for relative mouse mode exit shortcut. +// macOS: Cmd+G (track G key) +// Windows/Linux: Ctrl+Alt (track whichever modifier was pressed last) +// This prevents the exit from retriggering on OS key-repeat. +#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))] +static EXIT_SHORTCUT_KEY_DOWN: AtomicBool = AtomicBool::new(false); + +// Track whether relative mouse mode is currently active. +// This is set by Flutter via set_relative_mouse_mode_state() and checked +// by the rdev grab loop to determine if exit shortcuts should be processed. +#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))] +static RELATIVE_MOUSE_MODE_ACTIVE: AtomicBool = AtomicBool::new(false); + +/// Set the relative mouse mode state from Flutter. +/// This is called when entering or exiting relative mouse mode. +#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))] +pub fn set_relative_mouse_mode_state(active: bool) { + RELATIVE_MOUSE_MODE_ACTIVE.store(active, Ordering::SeqCst); + // Reset exit shortcut state when mode changes to avoid stale state + if !active { + EXIT_SHORTCUT_KEY_DOWN.store(false, Ordering::SeqCst); + } +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false); + +lazy_static::lazy_static! { + static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashMap::new())); + static ref MODIFIERS_STATE: Mutex> = { + let mut m = HashMap::new(); + m.insert(Key::ShiftLeft, false); + m.insert(Key::ShiftRight, false); + m.insert(Key::ControlLeft, false); + m.insert(Key::ControlRight, false); + m.insert(Key::Alt, false); + m.insert(Key::AltGr, false); + m.insert(Key::MetaLeft, false); + m.insert(Key::MetaRight, false); + Mutex::new(m) + }; +} + +pub mod client { + use super::*; + + /// Tracks grab ownership and serializes transitions across threads. + /// + /// Multiple Flutter isolates (one per session window) call + /// `change_grab_status(Run/Wait)` concurrently. Without serialization a + /// stale `Wait` from session A can clobber session B's freshly acquired + /// grab on any desktop OS. + /// + /// Windows and macOS are less susceptible in practice because the Flutter + /// side triggers `enterView` only after a mouse click inside the window, + /// but we cannot rely on that. On Linux/X11, `XGrabKeyboard` can also + /// cause a focus-change feedback loop (~10 Hz), so `last_grab` debounces + /// spurious `Wait` events that arrive shortly after a `Run`. + #[derive(Default)] + struct GrabOwnerState { + owner: Option, + last_grab: Option, + /// True while a deferred-release thread is in flight. Prevents + /// spawning redundant threads during the X11 feedback loop. + deferred_pending: bool, + } + + /// How long after a grab acquisition we suppress Wait from the same session. + /// Must exceed one full X11 feedback cycle (~100 ms: 50 ms enable + 50 ms disable). + #[cfg(target_os = "linux")] + const GRAB_DEBOUNCE_MS: u128 = 300; + + lazy_static::lazy_static! { + static ref IS_GRAB_STARTED: Arc> = Arc::new(Mutex::new(false)); + static ref GRAB_STATE: Arc> = Arc::new(Mutex::new(GrabOwnerState::default())); + } + + #[cfg(target_os = "linux")] + lazy_static::lazy_static! { + static ref GRAB_OP_LOCK: Mutex<()> = Mutex::new(()); + } + + #[cfg(target_os = "linux")] + fn apply_run_grab_if_owner(session_id: u128, disable_first: bool) { + let _lock = GRAB_OP_LOCK.lock().unwrap(); + let gs = GRAB_STATE.lock().unwrap(); + if gs.owner != Some(session_id) { + return; + } + drop(gs); + if disable_first { + log::debug!("[grab] handoff: disable_grab before re-grab"); + rdev::disable_grab(); + } + rdev::enable_grab(); + } + + #[cfg(target_os = "linux")] + fn disable_grab_if_released() { + let _lock = GRAB_OP_LOCK.lock().unwrap(); + let should_disable = { + let gs = GRAB_STATE.lock().unwrap(); + gs.owner.is_none() && gs.last_grab.is_none() + }; + if should_disable { + rdev::disable_grab(); + } + } + + pub fn start_grab_loop() { + let mut lock = IS_GRAB_STARTED.lock().unwrap(); + if *lock { + return; + } + super::start_grab_loop(); + *lock = true; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn change_grab_status(state: GrabState, keyboard_mode: &str, session_id: u128) { + #[cfg(feature = "flutter")] + if !IS_RDEV_ENABLED.load(Ordering::SeqCst) { + return; + } + // Serialize transitions so a stale `Wait` from a previous owner cannot + // clobber a fresh `Run` from a different session window. + let mut release_after_unlock = None; + #[cfg(target_os = "linux")] + let mut run_grab_after_unlock = None; + #[cfg(target_os = "linux")] + let mut disable_after_unlock = false; + let mut gs = GRAB_STATE.lock().unwrap(); + match state { + GrabState::Ready => {} + GrabState::Run => { + #[cfg(windows)] + update_grab_get_key_name(keyboard_mode); + + // Idempotent: if this session already owns the grab, just + // refresh the debounce timer (proves the session is still + // actively focused) and skip the actual grab call. + if gs.owner == Some(session_id) { + gs.last_grab = Some(std::time::Instant::now()); + // Reset so the next Wait can spawn a fresh deferred-release + // timer with an up-to-date snapshot of last_grab. + gs.deferred_pending = false; + log::debug!( + "[grab] Run(0x{:x}): already owner, refresh debounce", + session_id + ); + return; + } + + log::debug!( + "[grab] Run(0x{:x}): prev_owner={}, mode={}", + session_id, + gs.owner + .map_or("none".to_string(), |id| format!("0x{:x}", id)), + keyboard_mode, + ); + + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] + KEYBOARD_HOOKED.store(true, Ordering::SeqCst); + + #[cfg(target_os = "linux")] + let had_owner = gs.owner.is_some(); + gs.owner = Some(session_id); + gs.last_grab = Some(std::time::Instant::now()); + // Invalidate any in-flight deferred release from the previous + // owner so it cannot suppress a fresh timer for the new owner. + gs.deferred_pending = false; + #[cfg(target_os = "linux")] + { + run_grab_after_unlock = Some(had_owner); + } + } + GrabState::Wait => { + // Drop stale `Wait` events that do not correspond to the + // current grab owner. This prevents a late PointerExit from + // session A from releasing session B's freshly acquired grab. + if gs.owner != Some(session_id) { + log::debug!( + "[grab] Wait(0x{:x}): ignored, owner={}", + session_id, + gs.owner + .map_or("none".to_string(), |id| format!("0x{:x}", id)), + ); + return; + } + + // Debounce: on Linux/X11, XGrabKeyboard causes a focus-change + // feedback loop (grab -> PointerExit -> ungrab -> PointerEnter -> + // grab -> ...). Suppress Wait if the grab was acquired recently + // by this same session -- it is X11 feedback, not a real leave. + // A deferred release is scheduled so that a genuine leave within + // the debounce window is not permanently lost. + #[cfg(target_os = "linux")] + if let Some(t) = gs.last_grab { + let elapsed = t.elapsed().as_millis(); + if elapsed < GRAB_DEBOUNCE_MS { + if !gs.deferred_pending { + log::debug!( + "[grab] Wait(0x{:x}): debounced ({}ms < {}ms), scheduling deferred release", + session_id, elapsed, GRAB_DEBOUNCE_MS, + ); + gs.deferred_pending = true; + let remaining = (GRAB_DEBOUNCE_MS - elapsed) as u64 + 50; + let snapshot = gs.last_grab; + let mode = keyboard_mode.to_string(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(remaining)); + let release_keys = { + let mut gs = GRAB_STATE.lock().unwrap(); + // Release only if no new Run has refreshed the grab since. + if gs.owner == Some(session_id) && gs.last_grab == snapshot { + let to_release = take_remote_keys(); + gs.deferred_pending = false; + log::debug!( + "[grab] Wait(0x{:x}): deferred release", + session_id + ); + KEYBOARD_HOOKED.store(false, Ordering::SeqCst); + gs.owner = None; + gs.last_grab = None; + Some(to_release) + } else { + log::debug!( + "[grab] Wait(0x{:x}): deferred release cancelled (grab refreshed)", + session_id, + ); + None + } + }; + if let Some(to_release) = release_keys { + disable_grab_if_released(); + release_remote_keys_for_events(&mode, to_release); + } + }); + } else { + log::debug!( + "[grab] Wait(0x{:x}): debounced, deferred release already pending", + session_id, + ); + } + return; + } + } + + log::debug!("[grab] Wait(0x{:x}): releasing grab", session_id); + + #[cfg(windows)] + rdev::set_get_key_unicode(false); + + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] + KEYBOARD_HOOKED.store(false, Ordering::SeqCst); + + gs.owner = None; + gs.last_grab = None; + gs.deferred_pending = false; + release_after_unlock = Some(take_remote_keys()); + #[cfg(target_os = "linux")] + { + disable_after_unlock = true; + } + } + GrabState::Exit => {} + } + drop(gs); + #[cfg(target_os = "linux")] + { + if disable_after_unlock { + disable_grab_if_released(); + } + if let Some(disable_first) = run_grab_after_unlock { + apply_run_grab_if_owner(session_id, disable_first); + } + } + if let Some(to_release) = release_after_unlock { + release_remote_keys_for_events(keyboard_mode, to_release); + } + } + + pub fn process_event(keyboard_mode: &str, event: &Event, lock_modes: Option) { + let keyboard_mode = get_keyboard_mode_enum(keyboard_mode); + if is_long_press(&event) { + return; + } + let peer = get_peer_platform().to_lowercase(); + for key_event in event_to_key_events(peer, &event, keyboard_mode, lock_modes) { + send_key_event(&key_event); + } + } + + pub fn process_event_with_session( + keyboard_mode: &str, + event: &Event, + lock_modes: Option, + session: &Session, + ) { + let keyboard_mode = get_keyboard_mode_enum(keyboard_mode); + if is_long_press(&event) { + return; + } + let peer = session.peer_platform().to_lowercase(); + for key_event in event_to_key_events(peer, &event, keyboard_mode, lock_modes) { + session.send_key_event(&key_event); + } + } + + pub fn get_modifiers_state( + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) -> (bool, bool, bool, bool) { + let modifiers_lock = MODIFIERS_STATE.lock().unwrap(); + let ctrl = *modifiers_lock.get(&Key::ControlLeft).unwrap() + || *modifiers_lock.get(&Key::ControlRight).unwrap() + || ctrl; + let shift = *modifiers_lock.get(&Key::ShiftLeft).unwrap() + || *modifiers_lock.get(&Key::ShiftRight).unwrap() + || shift; + let command = *modifiers_lock.get(&Key::MetaLeft).unwrap() + || *modifiers_lock.get(&Key::MetaRight).unwrap() + || command; + let alt = *modifiers_lock.get(&Key::Alt).unwrap() + || *modifiers_lock.get(&Key::AltGr).unwrap() + || alt; + + (alt, ctrl, shift, command) + } + + pub fn legacy_modifiers( + key_event: &mut KeyEvent, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + if alt + && !crate::is_control_key(&key_event, &ControlKey::Alt) + && !crate::is_control_key(&key_event, &ControlKey::RAlt) + { + key_event.modifiers.push(ControlKey::Alt.into()); + } + if shift + && !crate::is_control_key(&key_event, &ControlKey::Shift) + && !crate::is_control_key(&key_event, &ControlKey::RShift) + { + key_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl + && !crate::is_control_key(&key_event, &ControlKey::Control) + && !crate::is_control_key(&key_event, &ControlKey::RControl) + { + key_event.modifiers.push(ControlKey::Control.into()); + } + if command + && !crate::is_control_key(&key_event, &ControlKey::Meta) + && !crate::is_control_key(&key_event, &ControlKey::RWin) + { + key_event.modifiers.push(ControlKey::Meta.into()); + } + } + + #[cfg(target_os = "android")] + pub fn map_key_to_control_key(key: &rdev::Key) -> Option { + match key { + Key::Alt => Some(ControlKey::Alt), + Key::ShiftLeft => Some(ControlKey::Shift), + Key::ControlLeft => Some(ControlKey::Control), + Key::MetaLeft => Some(ControlKey::Meta), + Key::AltGr => Some(ControlKey::RAlt), + Key::ShiftRight => Some(ControlKey::RShift), + Key::ControlRight => Some(ControlKey::RControl), + Key::MetaRight => Some(ControlKey::RWin), + _ => None, + } + } + + pub fn event_lock_screen() -> KeyEvent { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::LockScreen); + key_event.down = true; + key_event.mode = KeyboardMode::Legacy.into(); + key_event + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn lock_screen() { + send_key_event(&event_lock_screen()); + } + + pub fn event_ctrl_alt_del() -> KeyEvent { + let mut key_event = KeyEvent::new(); + if get_peer_platform() == "Windows" { + key_event.set_control_key(ControlKey::CtrlAltDel); + key_event.down = true; + } else { + key_event.set_control_key(ControlKey::Delete); + legacy_modifiers(&mut key_event, true, true, false, false); + key_event.press = true; + } + key_event.mode = KeyboardMode::Legacy.into(); + key_event + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn ctrl_alt_del() { + send_key_event(&event_ctrl_alt_del()); + } +} + +#[cfg(windows)] +pub fn update_grab_get_key_name(keyboard_mode: &str) { + match keyboard_mode { + "map" => rdev::set_get_key_unicode(false), + "translate" => rdev::set_get_key_unicode(true), + "legacy" => rdev::set_get_key_unicode(true), + _ => {} + }; +} + +#[cfg(target_os = "windows")] +static mut IS_0X021D_DOWN: bool = false; + +#[cfg(target_os = "macos")] +static mut IS_LEFT_OPTION_DOWN: bool = false; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn get_keyboard_mode() -> String { + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + return session.get_keyboard_mode(); + } + #[cfg(feature = "flutter")] + if let Some(session) = flutter::get_cur_session() { + return session.get_keyboard_mode(); + } + "legacy".to_string() +} + +/// Check if exit shortcut for relative mouse mode is active. +/// Exit shortcuts (only exits, not toggles): +/// - macOS: Cmd+G +/// - Windows/Linux: Ctrl+Alt (triggered when both are pressed) +/// Note: This shortcut is only available in Flutter client. Sciter client does not support relative mouse mode. +#[cfg(feature = "flutter")] +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +fn is_exit_relative_mouse_shortcut(key: Key) -> bool { + let modifiers = MODIFIERS_STATE.lock().unwrap(); + + #[cfg(target_os = "macos")] + { + // macOS: Cmd+G to exit + if key != Key::KeyG { + return false; + } + let meta = *modifiers.get(&Key::MetaLeft).unwrap_or(&false) + || *modifiers.get(&Key::MetaRight).unwrap_or(&false); + return meta; + } + + #[cfg(not(target_os = "macos"))] + { + // Windows/Linux: Ctrl+Alt to exit + // Triggered when Ctrl is pressed while Alt is down, or Alt is pressed while Ctrl is down + let is_ctrl_key = key == Key::ControlLeft || key == Key::ControlRight; + let is_alt_key = key == Key::Alt || key == Key::AltGr; + + if !is_ctrl_key && !is_alt_key { + return false; + } + + let ctrl = *modifiers.get(&Key::ControlLeft).unwrap_or(&false) + || *modifiers.get(&Key::ControlRight).unwrap_or(&false); + let alt = *modifiers.get(&Key::Alt).unwrap_or(&false) + || *modifiers.get(&Key::AltGr).unwrap_or(&false); + + // When Ctrl is pressed and Alt is already down, or vice versa + (is_ctrl_key && alt) || (is_alt_key && ctrl) + } +} + +/// Notify Flutter to exit relative mouse mode. +/// Note: This is Flutter-only. Sciter client does not support relative mouse mode. +#[cfg(feature = "flutter")] +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +fn notify_exit_relative_mouse_mode() { + let session_id = flutter::get_cur_session_id(); + flutter::push_session_event(&session_id, "exit_relative_mouse_mode", vec![]); +} + +/// Handle relative mouse mode shortcuts in the rdev grab loop. +/// Returns true if the event should be blocked from being sent to the peer. +#[cfg(feature = "flutter")] +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +#[inline] +fn can_exit_relative_mouse_mode_from_grab_loop() -> bool { + // Only process exit shortcuts when relative mouse mode is actually active. + // This prevents blocking Ctrl+Alt (or Cmd+G) when not in relative mouse mode. + if !RELATIVE_MOUSE_MODE_ACTIVE.load(Ordering::SeqCst) { + return false; + } + + let Some(session) = flutter::get_cur_session() else { + return false; + }; + + // Only for remote desktop sessions. + if !session.is_default() { + return false; + } + + // Must have keyboard permission and not be in view-only mode. + if !*session.server_keyboard_enabled.read().unwrap() { + return false; + } + let lc = session.lc.read().unwrap(); + if lc.view_only.v { + return false; + } + + // Peer must support relative mouse mode. + crate::common::is_support_relative_mouse_mode_num(lc.version) +} + +#[cfg(feature = "flutter")] +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +#[inline] +fn should_block_relative_mouse_shortcut(key: Key, is_press: bool) -> bool { + if !KEYBOARD_HOOKED.load(Ordering::SeqCst) { + return false; + } + + // Determine which key to track for key-up blocking based on platform + #[cfg(target_os = "macos")] + let is_tracked_key = key == Key::KeyG; + #[cfg(not(target_os = "macos"))] + let is_tracked_key = key == Key::ControlLeft + || key == Key::ControlRight + || key == Key::Alt + || key == Key::AltGr; + + // Block key up if key down was blocked (to avoid orphan key up event on remote). + // This must be checked before clearing the flag below. + if is_tracked_key && !is_press && EXIT_SHORTCUT_KEY_DOWN.swap(false, Ordering::SeqCst) { + return true; + } + + // Exit relative mouse mode shortcuts: + // - macOS: Cmd+G + // - Windows/Linux: Ctrl+Alt + // Guard it to supported/eligible sessions to avoid blocking the chord unexpectedly. + if is_exit_relative_mouse_shortcut(key) { + if !can_exit_relative_mouse_mode_from_grab_loop() { + return false; + } + if is_press { + // Only trigger exit on transition from "not pressed" to "pressed". + // This prevents retriggering on OS key-repeat. + if !EXIT_SHORTCUT_KEY_DOWN.swap(true, Ordering::SeqCst) { + notify_exit_relative_mouse_mode(); + } + } + return true; + } + + false +} + +fn start_grab_loop() { + std::env::set_var("KEYBOARD_ONLY", "y"); + #[cfg(any(target_os = "windows", target_os = "macos"))] + std::thread::spawn(move || { + let try_handle_keyboard = move |event: Event, key: Key, is_press: bool| -> Option { + // fix #2211:CAPS LOCK don't work + if key == Key::CapsLock || key == Key::NumLock { + return Some(event); + } + + let _scan_code = event.position_code; + let _code = event.platform_code as KeyCode; + + #[cfg(feature = "flutter")] + if should_block_relative_mouse_shortcut(key, is_press) { + return None; + } + + let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + client::process_event(&get_keyboard_mode(), &event, None); + if is_press { + None + } else { + Some(event) + } + } else { + Some(event) + }; + + #[cfg(target_os = "windows")] + match _scan_code { + 0x1D | 0x021D => rdev::set_modifier(Key::ControlLeft, is_press), + 0xE01D => rdev::set_modifier(Key::ControlRight, is_press), + 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), + 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), + 0x38 => rdev::set_modifier(Key::Alt, is_press), + // Right Alt + 0xE038 => rdev::set_modifier(Key::AltGr, is_press), + 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), + 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), + _ => {} + } + + #[cfg(target_os = "windows")] + unsafe { + // AltGr + if _scan_code == 0x021D { + IS_0X021D_DOWN = is_press; + } + } + + #[cfg(target_os = "macos")] + unsafe { + if _code == rdev::kVK_Option { + IS_LEFT_OPTION_DOWN = is_press; + } + } + + return res; + }; + let func = move |event: Event| match event.event_type { + EventType::KeyPress(key) => try_handle_keyboard(event, key, true), + EventType::KeyRelease(key) => try_handle_keyboard(event, key, false), + _ => Some(event), + }; + #[cfg(target_os = "macos")] + rdev::set_is_main_thread(false); + #[cfg(target_os = "windows")] + rdev::set_event_popup(false); + if let Err(error) = rdev::grab(func) { + log::error!("rdev Error: {:?}", error) + } + }); + + #[cfg(target_os = "linux")] + if let Err(err) = rdev::start_grab_listen(move |event: Event| match event.event_type { + EventType::KeyPress(key) | EventType::KeyRelease(key) => { + let is_press = matches!(event.event_type, EventType::KeyPress(_)); + if let Key::Unknown(keycode) = key { + log::error!("rdev get unknown key, keycode is {:?}", keycode); + } else { + #[cfg(feature = "flutter")] + if should_block_relative_mouse_shortcut(key, is_press) { + return None; + } + client::process_event(&get_keyboard_mode(), &event, None); + } + None + } + _ => Some(event), + }) { + log::error!("Failed to init rdev grab thread: {:?}", err); + }; +} + +// #[allow(dead_code)] is ok here. No need to stop grabbing loop. +#[allow(dead_code)] +fn stop_grab_loop() -> Result<(), rdev::GrabError> { + #[cfg(any(target_os = "windows", target_os = "macos"))] + rdev::exit_grab()?; + #[cfg(target_os = "linux")] + rdev::exit_grab_listen(); + Ok(()) +} + +pub fn is_long_press(event: &Event) -> bool { + let keys = MODIFIERS_STATE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(k) => { + if let Some(&state) = keys.get(&k) { + if state == true { + return true; + } + } + } + _ => {} + }; + return false; +} + +fn take_remote_keys() -> HashMap { + let mut to_release = TO_RELEASE.lock().unwrap(); + std::mem::take(&mut *to_release) +} + +fn release_remote_keys_for_events(keyboard_mode: &str, to_release: HashMap) { + for (key, mut event) in to_release.into_iter() { + event.event_type = EventType::KeyRelease(key); + client::process_event(keyboard_mode, &event, None); + // If Alt or AltGr is pressed, we need to send another key stoke to release it. + // Because the controlled side may hold the alt state, if local window is switched by [Alt + Tab]. + if key == Key::Alt || key == Key::AltGr { + event.event_type = EventType::KeyPress(key); + client::process_event(keyboard_mode, &event, None); + event.event_type = EventType::KeyRelease(key); + client::process_event(keyboard_mode, &event, None); + } + } +} + +#[allow(dead_code)] +pub fn release_remote_keys(keyboard_mode: &str) { + // todo!: client quit suddenly, how to release keys? + release_remote_keys_for_events(keyboard_mode, take_remote_keys()); +} + +pub fn get_keyboard_mode_enum(keyboard_mode: &str) -> KeyboardMode { + match keyboard_mode { + "map" => KeyboardMode::Map, + "translate" => KeyboardMode::Translate, + "legacy" => KeyboardMode::Legacy, + _ => KeyboardMode::Map, + } +} + +#[inline] +pub fn is_modifier(key: &rdev::Key) -> bool { + matches!( + key, + Key::ShiftLeft + | Key::ShiftRight + | Key::ControlLeft + | Key::ControlRight + | Key::MetaLeft + | Key::MetaRight + | Key::Alt + | Key::AltGr + ) +} + +#[inline] +#[allow(dead_code)] +pub fn is_modifier_code(evt: &KeyEvent) -> bool { + match evt.union { + Some(key_event::Union::Chr(code)) => { + let key = rdev::linux_key_from_code(code); + is_modifier(&key) + } + _ => false, + } +} + +#[inline] +pub fn is_numpad_rdev_key(key: &rdev::Key) -> bool { + matches!( + key, + Key::Kp0 + | Key::Kp1 + | Key::Kp2 + | Key::Kp3 + | Key::Kp4 + | Key::Kp5 + | Key::Kp6 + | Key::Kp7 + | Key::Kp8 + | Key::Kp9 + | Key::KpMinus + | Key::KpMultiply + | Key::KpDivide + | Key::KpPlus + | Key::KpDecimal + ) +} + +#[inline] +pub fn is_letter_rdev_key(key: &rdev::Key) -> bool { + matches!( + key, + Key::KeyA + | Key::KeyB + | Key::KeyC + | Key::KeyD + | Key::KeyE + | Key::KeyF + | Key::KeyG + | Key::KeyH + | Key::KeyI + | Key::KeyJ + | Key::KeyK + | Key::KeyL + | Key::KeyM + | Key::KeyN + | Key::KeyO + | Key::KeyP + | Key::KeyQ + | Key::KeyR + | Key::KeyS + | Key::KeyT + | Key::KeyU + | Key::KeyV + | Key::KeyW + | Key::KeyX + | Key::KeyY + | Key::KeyZ + ) +} + +// https://github.com/rustdesk/rustdesk/issues/8599 +// We just add these keys as letter keys. +#[inline] +pub fn is_letter_rdev_key_ex(key: &rdev::Key) -> bool { + matches!( + key, + Key::LeftBracket | Key::RightBracket | Key::SemiColon | Key::Quote | Key::Comma | Key::Dot + ) +} + +#[inline] +fn is_numpad_key(event: &Event) -> bool { + matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if is_numpad_rdev_key(&key)) +} + +// Check is letter key for lock modes. +// Only letter keys need to check and send Lock key state. +#[inline] +fn is_letter_key_4_lock_modes(event: &Event) -> bool { + matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if (is_letter_rdev_key(&key) || is_letter_rdev_key_ex(&key))) +} + +fn parse_add_lock_modes_modifiers( + key_event: &mut KeyEvent, + lock_modes: i32, + is_numpad_key: bool, + is_letter_key: bool, +) { + const CAPS_LOCK: i32 = 1; + const NUM_LOCK: i32 = 2; + // const SCROLL_LOCK: i32 = 3; + if is_letter_key && (lock_modes & (1 << CAPS_LOCK) != 0) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if is_numpad_key && lock_modes & (1 << NUM_LOCK) != 0 { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + // if lock_modes & (1 << SCROLL_LOCK) != 0 { + // key_event.modifiers.push(ControlKey::ScrollLock.into()); + // } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn add_lock_modes_modifiers(key_event: &mut KeyEvent, is_numpad_key: bool, is_letter_key: bool) { + if is_letter_key && get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if is_numpad_key && get_key_state(enigo::Key::NumLock) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn convert_numpad_keys(key: Key) -> Key { + if get_key_state(enigo::Key::NumLock) { + return key; + } + match key { + Key::Kp0 => Key::Insert, + Key::KpDecimal => Key::Delete, + Key::Kp1 => Key::End, + Key::Kp2 => Key::DownArrow, + Key::Kp3 => Key::PageDown, + Key::Kp4 => Key::LeftArrow, + Key::Kp5 => Key::Clear, + Key::Kp6 => Key::RightArrow, + Key::Kp7 => Key::Home, + Key::Kp8 => Key::UpArrow, + Key::Kp9 => Key::PageUp, + _ => key, + } +} + +fn update_modifiers_state(event: &Event) { + // for mouse + let mut keys = MODIFIERS_STATE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(k) => { + if keys.contains_key(&k) { + keys.insert(k, true); + } + } + EventType::KeyRelease(k) => { + if keys.contains_key(&k) { + keys.insert(k, false); + } + } + _ => {} + }; +} + +pub fn event_to_key_events( + mut peer: String, + event: &Event, + keyboard_mode: KeyboardMode, + _lock_modes: Option, +) -> Vec { + peer.retain(|c| !c.is_whitespace()); + + update_modifiers_state(event); + + match event.event_type { + EventType::KeyPress(key) => { + TO_RELEASE.lock().unwrap().insert(key, event.clone()); + } + EventType::KeyRelease(key) => { + TO_RELEASE.lock().unwrap().remove(&key); + } + _ => {} + } + + let mut key_event = KeyEvent::new(); + key_event.mode = keyboard_mode.into(); + + let mut key_events = match keyboard_mode { + KeyboardMode::Map => map_keyboard_mode(peer.as_str(), event, key_event), + KeyboardMode::Translate => translate_keyboard_mode(peer.as_str(), event, key_event), + _ => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + legacy_keyboard_mode(event, key_event) + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + Vec::new() + } + } + }; + + let is_numpad_key = is_numpad_key(&event); + if keyboard_mode != KeyboardMode::Translate || is_numpad_key { + let is_letter_key = is_letter_key_4_lock_modes(&event); + for key_event in &mut key_events { + if let Some(lock_modes) = _lock_modes { + parse_add_lock_modes_modifiers(key_event, lock_modes, is_numpad_key, is_letter_key); + } else { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + add_lock_modes_modifiers(key_event, is_numpad_key, is_letter_key); + } + } + } + key_events +} + +pub fn send_key_event(key_event: &KeyEvent) { + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + session.send_key_event(key_event); + } + + #[cfg(feature = "flutter")] + if let Some(session) = flutter::get_cur_session() { + session.send_key_event(key_event); + } +} + +pub fn get_peer_platform() -> String { + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + return session.peer_platform(); + } + #[cfg(feature = "flutter")] + if let Some(session) = flutter::get_cur_session() { + return session.peer_platform(); + } + "Windows".to_string() +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { + let mut events = Vec::new(); + // legacy mode(0): Generate characters locally, look for keycode on other side. + let (mut key, down_or_up) = match event.event_type { + EventType::KeyPress(key) => (key, true), + EventType::KeyRelease(key) => (key, false), + _ => { + return events; + } + }; + + let peer = get_peer_platform(); + let is_win = peer == "Windows"; + if is_win { + key = convert_numpad_keys(key); + } + + let alt = get_key_state(enigo::Key::Alt); + #[cfg(windows)] + let ctrl = { + let mut tmp = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + unsafe { + if IS_ALT_GR { + if alt || key == Key::AltGr { + if tmp { + tmp = false; + } + } else { + IS_ALT_GR = false; + } + } + } + tmp + }; + #[cfg(not(windows))] + let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); + #[cfg(windows)] + let command = crate::platform::windows::get_win_key_state(); + #[cfg(not(windows))] + let command = get_key_state(enigo::Key::Meta); + let control_key = match key { + Key::Alt => Some(ControlKey::Alt), + Key::AltGr => Some(ControlKey::RAlt), + Key::Backspace => Some(ControlKey::Backspace), + Key::ControlLeft => { + // when pressing AltGr, an extra VK_LCONTROL with a special + // scancode with bit 9 set is sent, let's ignore this. + #[cfg(windows)] + if (event.position_code >> 8) == 0xE0 { + unsafe { + IS_ALT_GR = true; + } + return events; + } + Some(ControlKey::Control) + } + Key::ControlRight => Some(ControlKey::RControl), + Key::DownArrow => Some(ControlKey::DownArrow), + Key::Escape => Some(ControlKey::Escape), + Key::F1 => Some(ControlKey::F1), + Key::F10 => Some(ControlKey::F10), + Key::F11 => Some(ControlKey::F11), + Key::F12 => Some(ControlKey::F12), + Key::F2 => Some(ControlKey::F2), + Key::F3 => Some(ControlKey::F3), + Key::F4 => Some(ControlKey::F4), + Key::F5 => Some(ControlKey::F5), + Key::F6 => Some(ControlKey::F6), + Key::F7 => Some(ControlKey::F7), + Key::F8 => Some(ControlKey::F8), + Key::F9 => Some(ControlKey::F9), + Key::LeftArrow => Some(ControlKey::LeftArrow), + Key::MetaLeft => Some(ControlKey::Meta), + Key::MetaRight => Some(ControlKey::RWin), + Key::Return => Some(ControlKey::Return), + Key::RightArrow => Some(ControlKey::RightArrow), + Key::ShiftLeft => Some(ControlKey::Shift), + Key::ShiftRight => Some(ControlKey::RShift), + Key::Space => Some(ControlKey::Space), + Key::Tab => Some(ControlKey::Tab), + Key::UpArrow => Some(ControlKey::UpArrow), + Key::Delete => { + if is_win && ctrl && alt { + client::ctrl_alt_del(); + return events; + } + Some(ControlKey::Delete) + } + Key::Apps => Some(ControlKey::Apps), + Key::Cancel => Some(ControlKey::Cancel), + Key::Clear => Some(ControlKey::Clear), + Key::Kana => Some(ControlKey::Kana), + Key::Hangul => Some(ControlKey::Hangul), + Key::Junja => Some(ControlKey::Junja), + Key::Final => Some(ControlKey::Final), + Key::Hanja => Some(ControlKey::Hanja), + Key::Hanji => Some(ControlKey::Hanja), + Key::Lang2 => Some(ControlKey::Convert), + Key::Print => Some(ControlKey::Print), + Key::Select => Some(ControlKey::Select), + Key::Execute => Some(ControlKey::Execute), + Key::PrintScreen => Some(ControlKey::Snapshot), + Key::Help => Some(ControlKey::Help), + Key::Sleep => Some(ControlKey::Sleep), + Key::Separator => Some(ControlKey::Separator), + Key::KpReturn => Some(ControlKey::NumpadEnter), + Key::Kp0 => Some(ControlKey::Numpad0), + Key::Kp1 => Some(ControlKey::Numpad1), + Key::Kp2 => Some(ControlKey::Numpad2), + Key::Kp3 => Some(ControlKey::Numpad3), + Key::Kp4 => Some(ControlKey::Numpad4), + Key::Kp5 => Some(ControlKey::Numpad5), + Key::Kp6 => Some(ControlKey::Numpad6), + Key::Kp7 => Some(ControlKey::Numpad7), + Key::Kp8 => Some(ControlKey::Numpad8), + Key::Kp9 => Some(ControlKey::Numpad9), + Key::KpDivide => Some(ControlKey::Divide), + Key::KpMultiply => Some(ControlKey::Multiply), + Key::KpDecimal => Some(ControlKey::Decimal), + Key::KpMinus => Some(ControlKey::Subtract), + Key::KpPlus => Some(ControlKey::Add), + Key::CapsLock | Key::NumLock | Key::ScrollLock => { + return events; + } + Key::Home => Some(ControlKey::Home), + Key::End => Some(ControlKey::End), + Key::Insert => Some(ControlKey::Insert), + Key::PageUp => Some(ControlKey::PageUp), + Key::PageDown => Some(ControlKey::PageDown), + Key::Pause => Some(ControlKey::Pause), + _ => None, + }; + if let Some(k) = control_key { + key_event.set_control_key(k); + } else { + let name = event + .unicode + .as_ref() + .and_then(|unicode| unicode.name.clone()); + let mut chr = match &name { + Some(ref s) => { + if s.len() <= 2 { + // exclude chinese characters + s.chars().next().unwrap_or('\0') + } else { + '\0' + } + } + _ => '\0', + }; + if chr == '·' { + // special for Chinese + chr = '`'; + } + if chr == '\0' { + chr = match key { + Key::Num1 => '1', + Key::Num2 => '2', + Key::Num3 => '3', + Key::Num4 => '4', + Key::Num5 => '5', + Key::Num6 => '6', + Key::Num7 => '7', + Key::Num8 => '8', + Key::Num9 => '9', + Key::Num0 => '0', + Key::KeyA => 'a', + Key::KeyB => 'b', + Key::KeyC => 'c', + Key::KeyD => 'd', + Key::KeyE => 'e', + Key::KeyF => 'f', + Key::KeyG => 'g', + Key::KeyH => 'h', + Key::KeyI => 'i', + Key::KeyJ => 'j', + Key::KeyK => 'k', + Key::KeyL => 'l', + Key::KeyM => 'm', + Key::KeyN => 'n', + Key::KeyO => 'o', + Key::KeyP => 'p', + Key::KeyQ => 'q', + Key::KeyR => 'r', + Key::KeyS => 's', + Key::KeyT => 't', + Key::KeyU => 'u', + Key::KeyV => 'v', + Key::KeyW => 'w', + Key::KeyX => 'x', + Key::KeyY => 'y', + Key::KeyZ => 'z', + Key::Comma => ',', + Key::Dot => '.', + Key::SemiColon => ';', + Key::Quote => '\'', + Key::LeftBracket => '[', + Key::RightBracket => ']', + Key::Slash => '/', + Key::BackSlash => '\\', + Key::Minus => '-', + Key::Equal => '=', + Key::BackQuote => '`', + _ => '\0', + } + } + if chr != '\0' { + if chr == 'l' && is_win && command { + client::lock_screen(); + return events; + } + key_event.set_chr(chr as _); + } else { + log::error!("Unknown key {:?}", &event); + return events; + } + } + let (alt, ctrl, shift, command) = client::get_modifiers_state(alt, ctrl, shift, command); + client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command); + + if down_or_up == true { + key_event.down = true; + } + events.push(key_event); + events +} + +#[inline] +pub fn map_keyboard_mode(_peer: &str, event: &Event, key_event: KeyEvent) -> Vec { + _map_keyboard_mode(_peer, event, key_event) + .map(|e| vec![e]) + .unwrap_or_default() +} + +fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Option { + match event.event_type { + EventType::KeyPress(..) => { + key_event.down = true; + } + EventType::KeyRelease(..) => { + key_event.down = false; + } + _ => return None, + }; + + #[cfg(target_os = "windows")] + let keycode = match _peer { + OS_LOWER_WINDOWS => { + // https://github.com/rustdesk/rustdesk/issues/1371 + // Filter scancodes that are greater than 255 and the height word is not 0xE0. + if event.position_code > 255 && (event.position_code >> 8) != 0xE0 { + return None; + } + event.position_code + } + OS_LOWER_MACOS => { + if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + rdev::win_scancode_to_macos_iso_code(event.position_code)? + } else { + rdev::win_scancode_to_macos_code(event.position_code)? + } + } + OS_LOWER_ANDROID => rdev::win_scancode_to_android_key_code(event.position_code)?, + _ => rdev::win_scancode_to_linux_code(event.position_code)?, + }; + #[cfg(target_os = "macos")] + let keycode = match _peer { + OS_LOWER_WINDOWS => rdev::macos_code_to_win_scancode(event.platform_code as _)?, + OS_LOWER_MACOS => event.platform_code as _, + OS_LOWER_ANDROID => rdev::macos_code_to_android_key_code(event.platform_code as _)?, + _ => rdev::macos_code_to_linux_code(event.platform_code as _)?, + }; + #[cfg(target_os = "linux")] + let keycode = match _peer { + OS_LOWER_WINDOWS => rdev::linux_code_to_win_scancode(event.position_code as _)?, + OS_LOWER_MACOS => { + if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + rdev::linux_code_to_macos_iso_code(event.position_code as _)? + } else { + rdev::linux_code_to_macos_code(event.position_code as _)? + } + } + OS_LOWER_ANDROID => rdev::linux_code_to_android_key_code(event.position_code as _)?, + _ => event.position_code as _, + }; + #[cfg(any(target_os = "android", target_os = "ios"))] + let keycode = match _peer { + OS_LOWER_WINDOWS => rdev::usb_hid_code_to_win_scancode(event.usb_hid as _)?, + OS_LOWER_LINUX => rdev::usb_hid_code_to_linux_code(event.usb_hid as _)?, + OS_LOWER_MACOS => { + if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + rdev::usb_hid_code_to_macos_iso_code(event.usb_hid as _)? + } else { + rdev::usb_hid_code_to_macos_code(event.usb_hid as _)? + } + } + OS_LOWER_ANDROID => rdev::usb_hid_code_to_android_key_code(event.usb_hid as _)?, + _ => event.usb_hid as _, + }; + key_event.set_chr(keycode as _); + Some(key_event) +} + +#[cfg(not(any(target_os = "ios")))] +fn try_fill_unicode(_peer: &str, event: &Event, key_event: &KeyEvent, events: &mut Vec) { + match &event.unicode { + Some(unicode_info) => { + if let Some(name) = &unicode_info.name { + if name.len() > 0 { + let mut evt = key_event.clone(); + evt.set_seq(name.to_string()); + evt.down = true; + events.push(evt); + } + } + } + None => + { + #[cfg(target_os = "windows")] + if _peer == OS_LOWER_LINUX { + if is_hot_key_modifiers_down() && unsafe { !IS_0X021D_DOWN } { + if let Some(chr) = get_char_from_vk(event.platform_code as u32) { + let mut evt = key_event.clone(); + evt.set_seq(chr.to_string()); + evt.down = true; + events.push(evt); + } + } + } + } + } +} + +#[cfg(target_os = "windows")] +fn try_fill_win2win_hotkey( + peer: &str, + event: &Event, + key_event: &KeyEvent, + events: &mut Vec, +) { + if peer == OS_LOWER_WINDOWS && is_hot_key_modifiers_down() && unsafe { !IS_0X021D_DOWN } { + let mut down = false; + let win2win_hotkey = match event.event_type { + EventType::KeyPress(..) => { + down = true; + if let Some(unicode) = get_unicode_from_vk(event.platform_code as u32) { + Some((unicode as u32 & 0x0000FFFF) | (event.platform_code << 16)) + } else { + None + } + } + EventType::KeyRelease(..) => Some(event.platform_code << 16), + _ => None, + }; + if let Some(code) = win2win_hotkey { + let mut evt = key_event.clone(); + evt.set_win2win_hotkey(code); + evt.down = down; + events.push(evt); + } + } +} + +#[cfg(target_os = "windows")] +fn is_hot_key_modifiers_down() -> bool { + if rdev::get_modifier(Key::ControlLeft) || rdev::get_modifier(Key::ControlRight) { + return true; + } + if rdev::get_modifier(Key::Alt) || rdev::get_modifier(Key::AltGr) { + return true; + } + if rdev::get_modifier(Key::MetaLeft) || rdev::get_modifier(Key::MetaRight) { + return true; + } + return false; +} + +#[inline] +#[cfg(any(target_os = "linux", target_os = "windows"))] +fn is_altgr(event: &Event) -> bool { + #[cfg(target_os = "linux")] + if event.platform_code == 0xFE03 { + true + } else { + false + } + + #[cfg(target_os = "windows")] + if unsafe { IS_0X021D_DOWN } && event.position_code == 0xE038 { + true + } else { + false + } +} + +#[inline] +#[cfg(any(target_os = "linux", target_os = "windows"))] +fn is_press(event: &Event) -> bool { + matches!(event.event_type, EventType::KeyPress(_)) +} + +// https://github.com/rustdesk/rustdesk/wiki/FAQ#keyboard-translation-modes +pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -> Vec { + let mut events: Vec = Vec::new(); + + if let Some(unicode_info) = &event.unicode { + if unicode_info.is_dead { + #[cfg(target_os = "macos")] + if peer != OS_LOWER_MACOS && unsafe { IS_LEFT_OPTION_DOWN } { + // try clear dead key state + // rdev::clear_dead_key_state(); + } else { + return events; + } + #[cfg(not(target_os = "macos"))] + return events; + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if is_numpad_key(&event) { + events.append(&mut map_keyboard_mode(peer, event, key_event)); + return events; + } + + #[cfg(target_os = "macos")] + // ignore right option key + if event.platform_code == rdev::kVK_RightOption as u32 { + return events; + } + + #[cfg(any(target_os = "linux", target_os = "windows"))] + if is_altgr(event) { + return events; + } + + #[cfg(target_os = "windows")] + if event.position_code == 0x021D { + return events; + } + + #[cfg(target_os = "windows")] + try_fill_win2win_hotkey(peer, event, &key_event, &mut events); + + #[cfg(any(target_os = "linux", target_os = "windows"))] + if events.is_empty() && is_press(event) { + try_fill_unicode(peer, event, &key_event, &mut events); + } + + // If AltGr is down, no need to send events other than unicode. + #[cfg(target_os = "windows")] + unsafe { + if IS_0X021D_DOWN { + return events; + } + } + + #[cfg(target_os = "macos")] + if !unsafe { IS_LEFT_OPTION_DOWN } { + try_fill_unicode(peer, event, &key_event, &mut events); + } + + if events.is_empty() { + events.append(&mut map_keyboard_mode(peer, event, key_event)); + } + events +} + +#[cfg(not(any(target_os = "ios")))] +pub fn keycode_to_rdev_key(keycode: u32) -> Key { + #[cfg(target_os = "windows")] + return rdev::win_key_from_scancode(keycode); + #[cfg(any(target_os = "linux"))] + return rdev::linux_key_from_code(keycode); + #[cfg(any(target_os = "android"))] + return rdev::android_key_from_code(keycode); + #[cfg(target_os = "macos")] + return rdev::macos_key_from_code(keycode.try_into().unwrap_or_default()); +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod input_source { + #[cfg(target_os = "macos")] + use hbb_common::log; + use hbb_common::SessionID; + + use crate::ui_interface::{get_local_option, set_local_option}; + + pub const CONFIG_OPTION_INPUT_SOURCE: &str = "input-source"; + // rdev grab mode + pub const CONFIG_INPUT_SOURCE_1: &str = "Input source 1"; + pub const CONFIG_INPUT_SOURCE_1_TIP: &str = "input_source_1_tip"; + // flutter grab mode + pub const CONFIG_INPUT_SOURCE_2: &str = "Input source 2"; + pub const CONFIG_INPUT_SOURCE_2_TIP: &str = "input_source_2_tip"; + + pub const CONFIG_INPUT_SOURCE_DEFAULT: &str = CONFIG_INPUT_SOURCE_1; + + pub fn init_input_source() { + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() { + // If switching from X11 to Wayland, the grab loop will not be started. + // Do not change the config here. + return; + } + #[cfg(target_os = "macos")] + if !crate::platform::macos::is_can_input_monitoring(false) { + log::error!("init_input_source, is_can_input_monitoring() false"); + set_local_option( + CONFIG_OPTION_INPUT_SOURCE.to_string(), + CONFIG_INPUT_SOURCE_2.to_string(), + ); + return; + } + let cur_input_source = get_cur_session_input_source(); + if cur_input_source == CONFIG_INPUT_SOURCE_1 { + super::IS_RDEV_ENABLED.store(true, super::Ordering::SeqCst); + } + super::client::start_grab_loop(); + } + + pub fn change_input_source(session_id: SessionID, input_source: String) { + let cur_input_source = get_cur_session_input_source(); + if cur_input_source == input_source { + return; + } + if input_source == CONFIG_INPUT_SOURCE_1 { + #[cfg(target_os = "macos")] + if !crate::platform::macos::is_can_input_monitoring(false) { + log::error!("change_input_source, is_can_input_monitoring() false"); + return; + } + // It is ok to start grab loop multiple times. + super::client::start_grab_loop(); + super::IS_RDEV_ENABLED.store(true, super::Ordering::SeqCst); + crate::flutter_ffi::session_enter_or_leave(session_id, true); + } else if input_source == CONFIG_INPUT_SOURCE_2 { + // No need to stop grab loop. + crate::flutter_ffi::session_enter_or_leave(session_id, false); + super::IS_RDEV_ENABLED.store(false, super::Ordering::SeqCst); + } + set_local_option(CONFIG_OPTION_INPUT_SOURCE.to_string(), input_source); + } + + #[inline] + pub fn get_cur_session_input_source() -> String { + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() { + return CONFIG_INPUT_SOURCE_2.to_string(); + } + let input_source = get_local_option(CONFIG_OPTION_INPUT_SOURCE.to_string()); + if input_source.is_empty() { + CONFIG_INPUT_SOURCE_DEFAULT.to_string() + } else { + input_source + } + } + + #[inline] + pub fn get_supported_input_source() -> Vec<(String, String)> { + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() { + return vec![( + CONFIG_INPUT_SOURCE_2.to_string(), + CONFIG_INPUT_SOURCE_2_TIP.to_string(), + )]; + } + vec![ + ( + CONFIG_INPUT_SOURCE_1.to_string(), + CONFIG_INPUT_SOURCE_1_TIP.to_string(), + ), + ( + CONFIG_INPUT_SOURCE_2.to_string(), + CONFIG_INPUT_SOURCE_2_TIP.to_string(), + ), + ] + } +} diff --git a/vendor/rustdesk/src/lan.rs b/vendor/rustdesk/src/lan.rs new file mode 100644 index 0000000..38c31ad --- /dev/null +++ b/vendor/rustdesk/src/lan.rs @@ -0,0 +1,344 @@ +#[cfg(not(target_os = "ios"))] +use hbb_common::whoami; +use hbb_common::{ + allow_err, + anyhow::bail, + config::Config, + config::{self, RENDEZVOUS_PORT}, + log, + protobuf::Message as _, + rendezvous_proto::*, + tokio::{ + self, + sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + }, + ResultType, +}; + +use std::{ + collections::{HashMap, HashSet}, + net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs, UdpSocket}, + time::Instant, +}; + +type Message = RendezvousMessage; + +#[cfg(not(target_os = "ios"))] +pub(super) fn start_listening() -> ResultType<()> { + let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port())); + let socket = std::net::UdpSocket::bind(addr)?; + socket.set_read_timeout(Some(std::time::Duration::from_millis(1000)))?; + log::info!("lan discovery listener started"); + loop { + let mut buf = [0; 2048]; + if let Ok((len, addr)) = socket.recv_from(&mut buf) { + if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) { + match msg_in.union { + Some(rendezvous_message::Union::PeerDiscovery(p)) => { + if p.cmd == "ping" + && config::option2bool( + "enable-lan-discovery", + &Config::get_option("enable-lan-discovery"), + ) + { + let id = Config::get_id(); + if p.id == id { + continue; + } + if let Some(self_addr) = get_ipaddr_by_peer(&addr) { + let mut msg_out = Message::new(); + let mut hostname = crate::whoami_hostname(); + // The default hostname is "localhost" which is a bit confusing + if hostname == "localhost" { + hostname = "unknown".to_owned(); + } + let peer = PeerDiscovery { + cmd: "pong".to_owned(), + mac: get_mac(&self_addr), + id, + hostname, + username: crate::platform::get_active_username(), + platform: whoami::platform().to_string(), + ..Default::default() + }; + msg_out.set_peer_discovery(peer); + socket.send_to(&msg_out.write_to_bytes()?, addr).ok(); + } + } + } + _ => {} + } + } + } + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn discover() -> ResultType<()> { + let sockets = send_query()?; + let rx = spawn_wait_responses(sockets); + handle_received_peers(rx).await?; + + log::info!("discover ping done"); + Ok(()) +} + +pub fn send_wol(id: String) { + let interfaces = default_net::get_interfaces(); + for peer in &config::LanPeers::load().peers { + if peer.id == id { + for (_, mac) in peer.ip_mac.iter() { + if let Ok(mac_addr) = mac.parse() { + for interface in &interfaces { + for ipv4 in &interface.ipv4 { + // remove below mask check to avoid unexpected bug + // if (u32::from(ipv4.addr) & u32::from(ipv4.netmask)) == (u32::from(peer_ip) & u32::from(ipv4.netmask)) + log::info!("Send wol to {mac_addr} of {}", ipv4.addr); + allow_err!(wol::send_wol(mac_addr, None, Some(IpAddr::V4(ipv4.addr)))); + } + } + } + } + break; + } + } +} + +#[inline] +fn get_broadcast_port() -> u16 { + (RENDEZVOUS_PORT + 3) as _ +} + +fn get_mac(_ip: &IpAddr) -> String { + #[cfg(not(target_os = "ios"))] + if let Ok(mac) = get_mac_by_ip(_ip) { + mac.to_string() + } else { + "".to_owned() + } + #[cfg(target_os = "ios")] + "".to_owned() +} + +#[cfg(not(target_os = "ios"))] +fn get_mac_by_ip(ip: &IpAddr) -> ResultType { + for interface in default_net::get_interfaces() { + match ip { + IpAddr::V4(local_ipv4) => { + if interface.ipv4.iter().any(|x| x.addr == *local_ipv4) { + if let Some(mac_addr) = interface.mac_addr { + return Ok(mac_addr.address()); + } + } + } + IpAddr::V6(local_ipv6) => { + if interface.ipv6.iter().any(|x| x.addr == *local_ipv6) { + if let Some(mac_addr) = interface.mac_addr { + return Ok(mac_addr.address()); + } + } + } + } + } + bail!("No interface found for ip: {:?}", ip); +} + +// Mainly from https://github.com/shellrow/default-net/blob/cf7ca24e7e6e8e566ed32346c9cfddab3f47e2d6/src/interface/shared.rs#L4 +fn get_ipaddr_by_peer(peer: A) -> Option { + let socket = match UdpSocket::bind("0.0.0.0:0") { + Ok(s) => s, + Err(_) => return None, + }; + + match socket.connect(peer) { + Ok(()) => (), + Err(_) => return None, + }; + + match socket.local_addr() { + Ok(addr) => return Some(addr.ip()), + Err(_) => return None, + }; +} + +fn create_broadcast_sockets() -> Vec { + let mut ipv4s = Vec::new(); + // TODO: maybe we should use a better way to get ipv4 addresses. + // But currently, it's ok to use `[Ipv4Addr::UNSPECIFIED]` for discovery. + // `default_net::get_interfaces()` causes undefined symbols error when `flutter build` on iOS simulator x86_64 + #[cfg(not(any(target_os = "ios")))] + for interface in default_net::get_interfaces() { + for ipv4 in &interface.ipv4 { + ipv4s.push(ipv4.addr.clone()); + } + } + ipv4s.push(Ipv4Addr::UNSPECIFIED); // for robustness + let mut sockets = Vec::new(); + for v4_addr in ipv4s { + // removing v4_addr.is_private() check, https://github.com/rustdesk/rustdesk/issues/4663 + if let Ok(s) = UdpSocket::bind(SocketAddr::from((v4_addr, 0))) { + if s.set_broadcast(true).is_ok() { + sockets.push(s); + } + } + } + sockets +} + +fn send_query() -> ResultType> { + let sockets = create_broadcast_sockets(); + if sockets.is_empty() { + bail!("Found no bindable ipv4 addresses"); + } + + let mut msg_out = Message::new(); + // We may not be able to get the mac address on mobile platforms. + // So we need to use the id to avoid discovering ourselves. + #[cfg(any(target_os = "android", target_os = "ios"))] + let id = crate::ui_interface::get_id(); + // `crate::ui_interface::get_id()` will cause error: + // `get_id()` uses async code with `current_thread`, which is not allowed in this context. + // + // No need to get id for desktop platforms. + // We can use the mac address to identify the device. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let id = "".to_owned(); + let peer = PeerDiscovery { + cmd: "ping".to_owned(), + id, + ..Default::default() + }; + msg_out.set_peer_discovery(peer); + let out = msg_out.write_to_bytes()?; + let maddr = SocketAddr::from(([255, 255, 255, 255], get_broadcast_port())); + for socket in &sockets { + allow_err!(socket.send_to(&out, maddr)); + } + log::info!("discover ping sent"); + Ok(sockets) +} + +fn wait_response( + socket: UdpSocket, + timeout: Option, + tx: UnboundedSender, +) -> ResultType<()> { + let mut last_recv_time = Instant::now(); + + let local_addr = socket.local_addr(); + let try_get_ip_by_peer = match local_addr.as_ref() { + Err(..) => true, + Ok(addr) => addr.ip().is_unspecified(), + }; + let mut mac: Option = None; + + socket.set_read_timeout(timeout)?; + loop { + let mut buf = [0; 2048]; + if let Ok((len, addr)) = socket.recv_from(&mut buf) { + if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) { + match msg_in.union { + Some(rendezvous_message::Union::PeerDiscovery(p)) => { + last_recv_time = Instant::now(); + if p.cmd == "pong" { + let local_mac = if try_get_ip_by_peer { + if let Some(self_addr) = get_ipaddr_by_peer(&addr) { + get_mac(&self_addr) + } else { + "".to_owned() + } + } else { + match mac.as_ref() { + Some(m) => m.clone(), + None => { + let m = if let Ok(local_addr) = local_addr { + get_mac(&local_addr.ip()) + } else { + "".to_owned() + }; + mac = Some(m.clone()); + m + } + } + }; + + if local_mac.is_empty() && p.mac.is_empty() || local_mac != p.mac { + allow_err!(tx.send(config::DiscoveryPeer { + id: p.id.clone(), + ip_mac: HashMap::from([ + (addr.ip().to_string(), p.mac.clone(),) + ]), + username: p.username.clone(), + hostname: p.hostname.clone(), + platform: p.platform.clone(), + online: true, + })); + } + } + } + _ => {} + } + } + } + if last_recv_time.elapsed().as_millis() > 3_000 { + break; + } + } + Ok(()) +} + +fn spawn_wait_responses(sockets: Vec) -> UnboundedReceiver { + let (tx, rx) = unbounded_channel::<_>(); + for socket in sockets { + let tx_clone = tx.clone(); + std::thread::spawn(move || { + allow_err!(wait_response( + socket, + Some(std::time::Duration::from_millis(10)), + tx_clone + )); + }); + } + rx +} + +async fn handle_received_peers(mut rx: UnboundedReceiver) -> ResultType<()> { + let mut peers = config::LanPeers::load().peers; + peers.iter_mut().for_each(|peer| { + peer.online = false; + }); + + let mut response_set = HashSet::new(); + let mut last_write_time: Option = None; + loop { + tokio::select! { + data = rx.recv() => match data { + Some(mut peer) => { + let in_response_set = !response_set.insert(peer.id.clone()); + if let Some(pos) = peers.iter().position(|x| x.is_same_peer(&peer) ) { + let peer1 = peers.remove(pos); + if in_response_set { + peer.ip_mac.extend(peer1.ip_mac); + peer.online = true; + } + } + peers.insert(0, peer); + if last_write_time.map(|t| t.elapsed().as_millis() > 300).unwrap_or(true) { + config::LanPeers::store(&peers); + #[cfg(feature = "flutter")] + crate::flutter_ffi::main_load_lan_peers(); + last_write_time = Some(Instant::now()); + } + } + None => { + break + } + } + } + } + + config::LanPeers::store(&peers); + #[cfg(feature = "flutter")] + crate::flutter_ffi::main_load_lan_peers(); + Ok(()) +} diff --git a/vendor/rustdesk/src/lang.rs b/vendor/rustdesk/src/lang.rs new file mode 100644 index 0000000..6302c2a --- /dev/null +++ b/vendor/rustdesk/src/lang.rs @@ -0,0 +1,278 @@ +use hbb_common::regex::Regex; +use std::ops::Deref; + +mod ar; +mod be; +mod bg; +mod ca; +mod cn; +mod cs; +mod da; +mod de; +mod el; +mod en; +mod eo; +mod es; +mod et; +mod eu; +mod fa; +mod gu; +mod fr; +mod he; +mod hi; +mod hr; +mod hu; +mod id; +mod it; +mod ja; +mod ko; +mod kz; +mod lt; +mod lv; +mod nb; +mod nl; +mod pl; +mod ptbr; +mod ro; +mod ru; +mod sc; +mod sk; +mod sl; +mod sq; +mod sr; +mod sv; +mod th; +mod tr; +mod tw; +mod uk; +mod vi; +mod ta; +mod ge; +mod fi; +mod ml; + +pub const LANGS: &[(&str, &str)] = &[ + ("en", "English"), + ("it", "Italiano"), + ("fr", "Français"), + ("de", "Deutsch"), + ("nl", "Nederlands"), + ("nb", "Norsk bokmål"), + ("zh-cn", "简体中文"), + ("zh-tw", "繁體中文"), + ("pt", "Português"), + ("es", "Español"), + ("et", "Eesti keel"), + ("eu", "Euskara"), + ("hu", "Magyar"), + ("bg", "Български"), + ("be", "Беларуская"), + ("ru", "Русский"), + ("sk", "Slovenčina"), + ("id", "Indonesia"), + ("cs", "Čeština"), + ("da", "Dansk"), + ("eo", "Esperanto"), + ("tr", "Türkçe"), + ("vi", "Tiếng Việt"), + ("pl", "Polski"), + ("ja", "日本語"), + ("ko", "한국어"), + ("kz", "Қазақ"), + ("uk", "Українська"), + ("fa", "فارسی"), + ("ca", "Català"), + ("el", "Ελληνικά"), + ("sv", "Svenska"), + ("sq", "Shqip"), + ("sr", "Srpski"), + ("th", "ภาษาไทย"), + ("sl", "Slovenščina"), + ("ro", "Română"), + ("lt", "Lietuvių"), + ("lv", "Latviešu"), + ("ar", "العربية"), + ("he", "עברית"), + ("hr", "Hrvatski"), + ("sc", "Sardu"), + ("ta", "தமிழ்"), + ("ge", "ქართული"), + ("fi", "Suomi"), + ("ml", "മലയാളം"), + ("hi", "हिंदी"), + ("gu", "ગુજરાતી"), +]; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn translate(name: String) -> String { + let locale = sys_locale::get_locale().unwrap_or_default(); + translate_locale(name, &locale) +} + +pub fn translate_locale(name: String, locale: &str) -> String { + let locale = locale.to_lowercase(); + let mut lang = hbb_common::config::LocalConfig::get_option("lang").to_lowercase(); + if lang.is_empty() { + // zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android + if locale.starts_with("zh") { + lang = (if locale.contains("tw") { + "zh-tw" + } else { + "zh-cn" + }) + .to_owned(); + } + } + if lang.is_empty() { + lang = locale + .split("-") + .next() + .map(|x| x.split("_").next().unwrap_or_default()) + .unwrap_or_default() + .to_owned(); + } + let lang = lang.to_lowercase(); + let m = match lang.as_str() { + "fr" => fr::T.deref(), + "zh-cn" => cn::T.deref(), + "it" => it::T.deref(), + "zh-tw" => tw::T.deref(), + "de" => de::T.deref(), + "nb" => nb::T.deref(), + "nl" => nl::T.deref(), + "es" => es::T.deref(), + "et" => et::T.deref(), + "eu" => eu::T.deref(), + "hu" => hu::T.deref(), + "ru" => ru::T.deref(), + "eo" => eo::T.deref(), + "id" => id::T.deref(), + "br" => ptbr::T.deref(), + "pt" => ptbr::T.deref(), + "tr" => tr::T.deref(), + "cs" => cs::T.deref(), + "da" => da::T.deref(), + "sk" => sk::T.deref(), + "vi" => vi::T.deref(), + "pl" => pl::T.deref(), + "ja" => ja::T.deref(), + "ko" => ko::T.deref(), + "kz" => kz::T.deref(), + "uk" => uk::T.deref(), + "fa" => fa::T.deref(), + "fi" => fi::T.deref(), + "ca" => ca::T.deref(), + "el" => el::T.deref(), + "sv" => sv::T.deref(), + "sq" => sq::T.deref(), + "sr" => sr::T.deref(), + "th" => th::T.deref(), + "sl" => sl::T.deref(), + "ro" => ro::T.deref(), + "lt" => lt::T.deref(), + "lv" => lv::T.deref(), + "ar" => ar::T.deref(), + "bg" => bg::T.deref(), + "be" => be::T.deref(), + "he" => he::T.deref(), + "hr" => hr::T.deref(), + "sc" => sc::T.deref(), + "ta" => ta::T.deref(), + "ge" => ge::T.deref(), + "ml" => ml::T.deref(), + "hi" => hi::T.deref(), + "gu" => gu::T.deref(), + _ => en::T.deref(), + }; + let (name, placeholder_value) = extract_placeholder(&name); + let replace = |s: &&str| { + let mut s = s.to_string(); + if let Some(value) = placeholder_value.as_ref() { + s = s.replace("{}", &value); + } + if !crate::is_rustdesk() { + if s.contains("RustDesk") + && !name.starts_with("upgrade_rustdesk_server_pro") + && name != "powered_by_me" + { + let app_name = crate::get_app_name(); + if !app_name.contains("RustDesk") { + s = s.replace("RustDesk", &app_name); + } else { + // https://github.com/rustdesk/rustdesk-server-pro/issues/845 + // If app_name contains "RustDesk" (e.g., "RustDesk-Admin"), we need to avoid + // replacing "RustDesk" within the already-substituted app_name, which would + // cause duplication like "RustDesk-Admin" -> "RustDesk-Admin-Admin". + // + // app_name only contains alphanumeric and hyphen. + const PLACEHOLDER: &str = "#A-P-P-N-A-M-E#"; + if !s.contains(PLACEHOLDER) { + s = s.replace(&app_name, PLACEHOLDER); + s = s.replace("RustDesk", &app_name); + s = s.replace(PLACEHOLDER, &app_name); + } else { + // It's very unlikely to reach here. + // Skip replacement to avoid incorrect result. + } + } + } + } + s + }; + if let Some(v) = m.get(&name as &str) { + if !v.is_empty() { + return replace(v); + } + } + if lang != "en" { + if let Some(v) = en::T.get(&name as &str) { + if !v.is_empty() { + return replace(v); + } + } + } + replace(&name.as_str()) +} + +// Matching pattern is {} +// Write {value} in the UI and {} in the translation file +// +// Example: +// Write in the UI: translate("There are {24} hours in a day") +// Write in the translation file: ("There are {} hours in a day", "{} hours make up a day") +fn extract_placeholder(input: &str) -> (String, Option) { + if let Ok(re) = Regex::new(r#"\{(.*?)\}"#) { + if let Some(captures) = re.captures(input) { + if let Some(inner_match) = captures.get(1) { + let name = re.replace(input, "{}").to_string(); + let value = inner_match.as_str().to_string(); + return (name, Some(value)); + } + } + } + (input.to_string(), None) +} + +mod test { + #[test] + fn test_extract_placeholders() { + use super::extract_placeholder as f; + + assert_eq!(f(""), ("".to_string(), None)); + assert_eq!( + f("{3} sessions"), + ("{} sessions".to_string(), Some("3".to_string())) + ); + assert_eq!(f(" } { "), (" } { ".to_string(), None)); + // Allow empty value + assert_eq!( + f("{} sessions"), + ("{} sessions".to_string(), Some("".to_string())) + ); + // Match only the first one + assert_eq!( + f("{2} times {4} makes {8}"), + ("{} times {4} makes {8}".to_string(), Some("2".to_string())) + ); + } +} diff --git a/vendor/rustdesk/src/lang/ar.rs b/vendor/rustdesk/src/lang/ar.rs new file mode 100644 index 0000000..4113c13 --- /dev/null +++ b/vendor/rustdesk/src/lang/ar.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "الحالة"), + ("Your Desktop", "سطح مكتبك"), + ("desk_tip", "يمكن الوصول لسطح مكتبك بهذا المعرف والرقم السري."), + ("Password", "كلمة المرور"), + ("Ready", "جاهز"), + ("Established", "تم الانشاء"), + ("connecting_status", "جاري الاتصال بشبكة RustDesk..."), + ("Enable service", "تفعيل الخدمة"), + ("Start service", "بدء الخدمة"), + ("Service is running", "الخدمة تعمل"), + ("Service is not running", "الخدمة لا تعمل"), + ("not_ready_status", "غير جاهز. الرجاء التأكد من الاتصال"), + ("Control Remote Desktop", "التحكم بسطح المكتب البعيد"), + ("Transfer file", "نقل ملف"), + ("Connect", "اتصال"), + ("Recent sessions", "الجلسات الحديثة"), + ("Address book", "كتاب العناوين"), + ("Confirmation", "التأكيد"), + ("TCP tunneling", "نفق TCP"), + ("Remove", "ازالة"), + ("Refresh random password", "تحديث كلمة مرور عشوائية"), + ("Set your own password", "تعيين كلمة مرور خاصة بك"), + ("Enable keyboard/mouse", "تفعيل لوحة المفاتيح/الفأرة"), + ("Enable clipboard", "تفعيل الحافظة"), + ("Enable file transfer", "تفعيل نقل الملفات"), + ("Enable TCP tunneling", "تفعيل نفق TCP"), + ("IP Whitelisting", "القائمة البيضاء للـ IP"), + ("ID/Relay Server", "معرف خادم الوسيط"), + ("Import server config", "استيراد إعدادات الخادم"), + ("Export Server Config", "تصدير إعدادات الخادم"), + ("Import server configuration successfully", "تم استيراد إعدادات الخادم بنجاح"), + ("Export server configuration successfully", "تم تصدير إعدادات الخادم بنجاح"), + ("Invalid server configuration", "إعدادات الخادم غير صحيحة"), + ("Clipboard is empty", "الحافظة فارغة"), + ("Stop service", "إيقاف الخدمة"), + ("Change ID", "تغيير المعرف"), + ("Your new ID", "معرفك الجديد"), + ("length %min% to %max%", "الطول من %min% الى %max%"), + ("starts with a letter", "يبدأ بحرف"), + ("allowed characters", "الحروف المسموح بها"), + ("id_change_tip", "فقط a-z, A-Z, 0-9, - (dash) و _ مسموح بها. اول حرف يجب ان يكون a-z او A-Z. الطول بين 6 و 16."), + ("Website", "الموقع"), + ("About", "عن"), + ("Slogan_tip", "صنع بحب في هذا العالم الفوضوي!"), + ("Privacy Statement", "بيان الخصوصية"), + ("Mute", "كتم"), + ("Build Date", "تاريخ البناء"), + ("Version", "النسخة"), + ("Home", "المنزل"), + ("Audio Input", "مصدر الصوت"), + ("Enhancements", "التحسينات"), + ("Hardware Codec", "ترميز العتاد"), + ("Adaptive bitrate", "معدل بت متكيف"), + ("ID Server", "معرف الخادم"), + ("Relay Server", "خادم الوسيط"), + ("API Server", "خادم API"), + ("invalid_http", "يجب ان يبدأ بـ http:// او https://"), + ("Invalid IP", "عنوان IP غير صحيح"), + ("Invalid format", "صيغة غير صحيحة"), + ("server_not_support", "الخادم غير مدعوم"), + ("Not available", "غير متوفر"), + ("Too frequent", "متكرر جدا"), + ("Cancel", "إلغاء الأمر"), + ("Skip", "تجاوز"), + ("Close", "إغلاق"), + ("Retry", "إعادة المحاولة"), + ("OK", "موافق"), + ("Password Required", "كلمة المرور اجبارية"), + ("Please enter your password", "الرجاء كتابة كلمة المرور"), + ("Remember password", "تذكر كلمة المرور"), + ("Wrong Password", "كلمة مرور خاطئة"), + ("Do you want to enter again?", "هل تريد الادخال مرة اخرى؟"), + ("Connection Error", "خطأ غي الاتصال"), + ("Error", "خطأ"), + ("Reset by the peer", "تمت اعادة التعيين بواسطة القرين"), + ("Connecting...", "جاري الاتصال..."), + ("Connection in progress. Please wait.", "جاري الاتصال, الرجاء الانتظار..."), + ("Please try 1 minute later", "الرجاء المحاولة بعد دقيقة واحدة"), + ("Login Error", "خطأ في تسجيل الدخول"), + ("Successful", "نجاح"), + ("Connected, waiting for image...", "متصل, جاري انتظار الصورة..."), + ("Name", "الاسم"), + ("Type", "النوع"), + ("Modified", "آخر تعديل"), + ("Size", "الحجم"), + ("Show Hidden Files", "عرض الملفات المخفية"), + ("Receive", "استقبال"), + ("Send", "ارسال"), + ("Refresh File", "تحديث الملف"), + ("Local", "المحلي"), + ("Remote", "البعيد"), + ("Remote Computer", "الحاسب البعيد"), + ("Local Computer", "الحاسب المحلي"), + ("Confirm Delete", "تأكيد الحذف"), + ("Delete", "حذف"), + ("Properties", "الخصائص"), + ("Multi Select", "اختيار متعدد"), + ("Select All", "تحديد الكل"), + ("Unselect All", "الغاء تحديد الكل"), + ("Empty Directory", "مجلد فارغ"), + ("Not an empty directory", "مجلد غير فارغ"), + ("Are you sure you want to delete this file?", "هل انت متأكد من أنك تريد حذف هذا الملف؟"), + ("Are you sure you want to delete this empty directory?", "هل انت متأكد من أنك تريد حذف هذا المجلد؟"), + ("Are you sure you want to delete the file of this directory?", "هل انت متأكد من أنك تريد حذف ملفات هذا المجلد؟"), + ("Do this for all conflicts", "فعل هذا لكل التعارضات"), + ("This is irreversible!", "لا يمكن التراجع عن هذا!"), + ("Deleting", "جاري الحذف"), + ("files", "ملفات"), + ("Waiting", "انتظار"), + ("Finished", "انتهى"), + ("Speed", "السرعة"), + ("Custom Image Quality", "جودة صورة مخصصة"), + ("Privacy mode", "وضع الخصوصية"), + ("Block user input", "حجم ادخال المستخدم"), + ("Unblock user input", "الغاء حجب ادخال المستخدم"), + ("Adjust Window", "ضبط النافذة"), + ("Original", "الاصلي"), + ("Shrink", "تقليص"), + ("Stretch", "تمديد"), + ("Scrollbar", "شريط التمرير"), + ("ScrollAuto", "التمرير التلقائي"), + ("Good image quality", "دقة صورة جيدة"), + ("Balanced", "متوازن"), + ("Optimize reaction time", "تحسين وقت رد الفعل"), + ("Custom", "مخصص"), + ("Show remote cursor", "عرض مؤشر الجهاز البعيد"), + ("Show quality monitor", "عرض مراقب الجودة"), + ("Disable clipboard", "تعطيل الحافظة"), + ("Lock after session end", "القفل بعد نهاية هذه الجلسة"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del دخال"), + ("Insert Lock", "قفل الادخال"), + ("Refresh", "تحديث"), + ("ID does not exist", "المعرف غير موجود"), + ("Failed to connect to rendezvous server", "فشل الاتصال بخادم rendezvous"), + ("Please try later", "الرجاء المحاولة لاحقا"), + ("Remote desktop is offline", "سطح المكتب البعيد غير متصل"), + ("Key mismatch", "المفتاح غير متطابق"), + ("Timeout", "نفذ الوقت"), + ("Failed to connect to relay server", "فشل الاتصال بالخادم الوسيط"), + ("Failed to connect via rendezvous server", "فشل الاتصال عير خادم rendezvous"), + ("Failed to connect via relay server", "فشل الاتصال عبر الخادم الوسيط"), + ("Failed to make direct connection to remote desktop", "فشل في اجراء اتصال مباشر لسطح المكتب البعيد"), + ("Set Password", "ضبط كلمة المرور"), + ("OS Password", "كلمة مرور نظام التشغيل"), + ("install_tip", "بسبب صلاحيات تحكم حساب المستخدم. RustDesk قد لا يعمل بشكل صحيح في جهة البعيد في بعض الحالات. لتفادي ذلك. الرجاء الضغط على الزر ادناه لتثبيت RustDesk في جهازك."), + ("Click to upgrade", "اضغط للارتقاء"), + ("Configure", "تهيئة"), + ("config_acc", "لتتمكن من التحكم بسطح مكتبك البعيد, تحتاج الى منح RustDesk اذونات \"امكانية الوصول\"."), + ("config_screen", "لتتمكن من الوصول الى سطح مكتبك البعيد, تحتاج الى منح RustDesk اذونات \"تسجيل الشاشة\"."), + ("Installing ...", "جاري التثبيت..."), + ("Install", "تثبيت"), + ("Installation", "التثبيت"), + ("Installation Path", "مسار التثبيت"), + ("Create start menu shortcuts", "انشاء اختصارات قائمة ابداء"), + ("Create desktop icon", "انشاء اختصار سطح المكتب"), + ("agreement_tip", "بمجرد البدء بالتثبيت, فانت قد قبلت اتفاقية الترخيص."), + ("Accept and Install", "الموافقة والتثبيت"), + ("End-user license agreement", "اتفاقية ترخيص المستخدم النهائي"), + ("Generating ...", "جاري الانشاء..."), + ("Your installation is lower version.", "انت تحاول تثبيت نسخة قديمة."), + ("not_close_tcp_tip", "لا تغلق هذه النافذة اثناء استخدامك للنفق"), + ("Listening ...", "جاري الاستماع..."), + ("Remote Host", "المستضيف البعيد"), + ("Remote Port", "منفذ المستضيف البعيد"), + ("Action", "فعل"), + ("Add", "اضافة"), + ("Local Port", "المنفذ المحلي"), + ("Local Address", "العنوان المحلي"), + ("Change Local Port", "تغيير المنفذ المحلي"), + ("setup_server_tip", "لاتصال اسرع, الرجاء اعداد خادم خاص بك"), + ("Too short, at least 6 characters.", "قصير جدا. يجب ان يكون على الاقل 6 خانات"), + ("The confirmation is not identical.", "التأكيد غير متطابق"), + ("Permissions", "الاذونات"), + ("Accept", "قبول"), + ("Dismiss", "صرف"), + ("Disconnect", "قطع الاتصال"), + ("Enable file copy and paste", "السماح بالنسخ واللصق"), + ("Connected", "متصل"), + ("Direct and encrypted connection", "اتصال مباشر مشفر"), + ("Relayed and encrypted connection", "اتصال غير مباشر مشفر"), + ("Direct and unencrypted connection", "اتصال مباشر غير مشفر"), + ("Relayed and unencrypted connection", "اتصال غير مباشر غير مشفر"), + ("Enter Remote ID", "ادخل المعرف البعيد"), + ("Enter your password", "ادخل كلمة المرور"), + ("Logging in...", "جاري تسجيل الدخول..."), + ("Enable RDP session sharing", "تفعيل مشاركة الجلسة باستخدام RDP"), + ("Auto Login", "تسجيل دخول تلقائي"), + ("Enable direct IP access", "تفعيل الوصول المباشر لعنوان IP"), + ("Rename", "اعادة تسمية"), + ("Space", "مساحة"), + ("Create desktop shortcut", "انشاء اختصار سطح مكتب"), + ("Change Path", "تغيير المسار"), + ("Create Folder", "انشاء مجلد"), + ("Please enter the folder name", "الرجاء ادخال اسم المجلد"), + ("Fix it", "اصلحه"), + ("Warning", "تحذير"), + ("Login screen using Wayland is not supported", "تسجيل الدخول باستخدام Wayland غير مدعوم"), + ("Reboot required", "يجب اعادة التشغيل"), + ("Unsupported display server", "خادم العرض غير مدعوم"), + ("x11 expected", "x11 متوقع"), + ("Port", "المنفذ"), + ("Settings", "الاعدادات"), + ("Username", "اسم المستخدم"), + ("Invalid port", "منفذ خاطئ"), + ("Closed manually by the peer", "اغلاق يدويا بواسطة القرين"), + ("Enable remote configuration modification", "تفعيل تعديل اعدادات البعيد"), + ("Run without install", "تشغيل بدون تثبيت"), + ("Connect via relay", "الاتصال عبر وسيط"), + ("Always connect via relay", "الاتصال باستخدام وسيط دائما"), + ("whitelist_tip", "فقط قائمة الـ IP البيضاء تستطيع الوصول لي"), + ("Login", "تسجيل الدخول"), + ("Verify", "تأكيد"), + ("Remember me", "تذكرني"), + ("Trust this device", "الوثوق بهذا الجهاز"), + ("Verification code", "رمز التحقق"), + ("verification_tip", "رمز التحقق ارسل الى بريدك الالكتروني المسجل. ادخل رمز التحقق للاستمرار بتسجيل الدخول."), + ("Logout", "تسجيل الخروج"), + ("Tags", "العلامات"), + ("Search ID", "البحث المعرف"), + ("whitelist_sep", "مفصولة بفاصلة او فاصلة منقوطة او سطر جديد"), + ("Add ID", "اضافة معرف"), + ("Add Tag", "اضافة علامة"), + ("Unselect all tags", "عدم تحديد كل العلامات"), + ("Network error", "خطأ شبكة"), + ("Username missed", "اسم المستخدم مفقود"), + ("Password missed", "كلمة المرور مفقودة"), + ("Wrong credentials", "اسم مستخدم او كلمة مرور خاطئة"), + ("The verification code is incorrect or has expired", "رمز التحقق غير صحيح او منتهي"), + ("Edit Tag", "تحرير علامة"), + ("Forget Password", "عدم تذكر كلمة المرور"), + ("Favorites", "المفضلة"), + ("Add to Favorites", "اضافة للمفضلة"), + ("Remove from Favorites", "ازالة من المفضلة"), + ("Empty", "فارغ"), + ("Invalid folder name", "اسم المجلد غير صحيح"), + ("Socks5 Proxy", "وكيل Socks5"), + ("Socks5/Http(s) Proxy", "وكيل Socks5/Http(s)"), + ("Discovered", "المكتشفة"), + ("install_daemon_tip", "للبدء مع بدء تشغيل النظام. تحتاج الى تثبيت خدمة النظام."), + ("Remote ID", "المعرف البعيد"), + ("Paste", "لصق"), + ("Paste here?", "لصق هنا؟"), + ("Are you sure to close the connection?", "هل انت متاكد من انك تريد اغلاق هذا الاتصال؟"), + ("Download new version", "تنويل نسخة جديدة"), + ("Touch mode", "وضع اللمس"), + ("Mouse mode", "وضع الفأرة"), + ("One-Finger Tap", "لم اصبع واحد"), + ("Left Mouse", "الفأرة اليسرى"), + ("One-Long Tap", "لمسة واحدة طويلة"), + ("Two-Finger Tap", "لمس اصبعين"), + ("Right Mouse", "الفأرة اليمنى"), + ("One-Finger Move", "نقل الاصبع الواحد"), + ("Double Tap & Move", "لمستان ونقل"), + ("Mouse Drag", "سحب الفأرة"), + ("Three-Finger vertically", "ثلاث اصابع افقيا"), + ("Mouse Wheel", "عجلة الفارة"), + ("Two-Finger Move", "نقل الاصبعين"), + ("Canvas Move", "تحريك اللوحة"), + ("Pinch to Zoom", "قرصة للتكبير"), + ("Canvas Zoom", "تكبير اللوحة"), + ("Reset canvas", "إعادة تعيين اللوحة"), + ("No permission of file transfer", "لا يوجد اذن نقل الملف"), + ("Note", "ملاحظة"), + ("Connection", "الاتصال"), + ("Share screen", "مشاركة الشاشة"), + ("Chat", "محادثة"), + ("Total", "الاجمالي"), + ("items", "عناصر"), + ("Selected", "محدد"), + ("Screen Capture", "لقط الشاشة"), + ("Input Control", "تحكم الادخال"), + ("Audio Capture", "لقط الصوت"), + ("Do you accept?", "هل تقبل؟"), + ("Open System Setting", "فتح اعدادات النظام"), + ("How to get Android input permission?", "كيف تحصل على اذن الادخال في اندرويد؟"), + ("android_input_permission_tip1", "لكي يتمكن جهاز بعيد من التحكم بجهازك الـ Android عن طريق الفارة أو اللمس، يلزمك السماح لـ RustDesk باستخدام خدمة \"إمكانية الوصول\"."), + ("android_input_permission_tip2", "يرجى الانتقال إلى صفحة إعدادات النظام التالية، والعثور على [الخدمات المثبتة]، وتشغيل خدمة [RustDesk Input]."), + ("android_new_connection_tip", "تم استلام طلب تحكم جديد، حيث يريد التحكم بجهازك الحالي."), + ("android_service_will_start_tip", "تشغيل \"لقط الشاشة\" سيبدأ الخدمة تلقائيا، حيث سيسمح للاجهزة الاخرى بطلب الاتصال مع جهازك."), + ("android_stop_service_tip", "اغلاق الخدمة سيغلق جميع الاتصالات القائمة."), + ("android_version_audio_tip", "نسخة اندرويد الحالية لا تدعم خدمة لقط الصوت، الرجاء الترقية الى نسخة 10 او أحدث."), + ("android_start_service_tip", "اضغط تشغيل الخدمة او فعل صلاحية لقط الشاشة لبدء خدمة مشاركة الشاشة."), + ("android_permission_may_not_change_tip", "الاذونات الاتصالات القائمة قد لا تتغير مباشرة الا بعد اعادة الاتصال."), + ("Account", "الحساب"), + ("Overwrite", "استبدال"), + ("This file exists, skip or overwrite this file?", "الملف موجود, هل تريد التجاوز او الاستبدال؟"), + ("Quit", "خروج"), + ("Help", "مساعدة"), + ("Failed", "فشل"), + ("Succeeded", "نجاح"), + ("Someone turns on privacy mode, exit", "شخص ما فعل وضع الخصوصية, خروج"), + ("Unsupported", "غير مدعوم"), + ("Peer denied", "القرين رفض"), + ("Please install plugins", "الرجاء تثبيت الاضافات"), + ("Peer exit", "خروج القرين"), + ("Failed to turn off", "فشل ايقاف التشغيل"), + ("Turned off", "مطفئ"), + ("Language", "اللغة"), + ("Keep RustDesk background service", "ابق خدمة RustDesk تعمل في الخلفية"), + ("Ignore Battery Optimizations", "تجاهل تحسينات البطارية"), + ("android_open_battery_optimizations_tip", "اذا اردت تعطيل هذه الميزة, الرجاء الذهاب الى صفحة اعدادات تطبيق RustDesk, ابحث عن البطارية, الغ تحديد غير مقيد"), + ("Start on boot", "البدء عند تشغيل النظام"), + ("Start the screen sharing service on boot, requires special permissions", "تشغيل خدمة مشاركة الشاشة عند بدء تشغيل النظام, يحتاج الى اذونات خاصة"), + ("Connection not allowed", "الاتصال غير مسموح"), + ("Legacy mode", "الوضع التقليدي"), + ("Map mode", "وضع الخريطة"), + ("Translate mode", "وضع الترجمة"), + ("Use permanent password", "استخدام كلمة مرور دائمة"), + ("Use both passwords", "استخدام طريقتي كلمة المرور"), + ("Set permanent password", "تعيين كلمة مرور دائمة"), + ("Enable remote restart", "تفعيل اعداة تشغيل البعيد"), + ("Restart remote device", "اعادة تشغيل الجهاز البعيد"), + ("Are you sure you want to restart", "هل انت متاكد من انك تريد اعادة التشغيل؟"), + ("Restarting remote device", "جاري اعادة تشغيل البعيد"), + ("remote_restarting_tip", "الجهاز البعيد يعيد التشغيل. الرجاء اغلاق هذه الرسالة واعادة الاتصال باستخدام كلمة المرور الدائمة بعد فترة بسيطة."), + ("Copied", "منسوخ"), + ("Exit Fullscreen", "الخروج من ملئ الشاشة"), + ("Fullscreen", "ملئ الشاشة"), + ("Mobile Actions", "اجراءات الهاتف"), + ("Select Monitor", "اختر شاشة"), + ("Control Actions", "اجراءات التحكم"), + ("Display Settings", "اعدادات العرض"), + ("Ratio", "النسبة"), + ("Image Quality", "جودة الصورة"), + ("Scroll Style", "شكل التمرير"), + ("Show Toolbar", "عرض شريط الادوات"), + ("Hide Toolbar", "اخفاء شريط الادوات"), + ("Direct Connection", "اتصال مباشر"), + ("Relay Connection", "اتصال الوسيط"), + ("Secure Connection", "اتصال آمن"), + ("Insecure Connection", "اتصال غير آمن"), + ("Scale original", "المقياس الأصلي"), + ("Scale adaptive", "مقياس التكيف"), + ("General", "عام"), + ("Security", "الأمان"), + ("Theme", "السمة"), + ("Dark Theme", "سمة غامقة"), + ("Light Theme", "سمة فاتحة"), + ("Dark", "غامق"), + ("Light", "فاتح"), + ("Follow System", "نفس نظام التشغيل"), + ("Enable hardware codec", "تفعيل ترميز العتاد"), + ("Unlock Security Settings", "فتح اعدادات الامان"), + ("Enable audio", "تفعيل الصوت"), + ("Unlock Network Settings", "فتح اعدادات الشبكة"), + ("Server", "الخادم"), + ("Direct IP Access", "وصول مباشر للـ IP"), + ("Proxy", "الوكيل"), + ("Apply", "تطبيق"), + ("Disconnect all devices?", "قطع اتصال كل الاجهزة؟"), + ("Clear", "مسح"), + ("Audio Input Device", "جهاز ادخال الصوت"), + ("Use IP Whitelisting", "استخدام قائمة الـ IP البيضاء"), + ("Network", "الشبكة"), + ("Pin Toolbar", "تثبيت شريط الادوات"), + ("Unpin Toolbar", "الغاء تثبيت شريط الادوات"), + ("Recording", "التسجيل"), + ("Directory", "المسار"), + ("Automatically record incoming sessions", "تسجيل الجلسات القادمة تلقائيا"), + ("Automatically record outgoing sessions", "تسجيل الجلسات الصادرة تلقائيا"), + ("Change", "تغيير"), + ("Start session recording", "بدء تسجيل الجلسة"), + ("Stop session recording", "ايقاف تسجيل الجلسة"), + ("Enable recording session", "تفعيل تسجيل الجلسة"), + ("Enable LAN discovery", "تفعيل اكتشاف الشبكة المحلية"), + ("Deny LAN discovery", "رفض اكتشاف الشبكة المحلية"), + ("Write a message", "اكتب رسالة"), + ("Prompt", "موجه"), + ("Please wait for confirmation of UAC...", "الرجاء انتظار تاكيد تحكم حساب المستخدم..."), + ("elevated_foreground_window_tip", "النافذة الحالية لسطح المكتب البعيد تحتاج صلاحية اعلى لتعمل, لذلك لن تستطيع استخدام الفارة ولوحة المفاتيح مؤقتا. تستطيع انت تطلب من المستخدم البعيد تصغير النافذة الحالية, او ضفط زر الارتقاء في نافذة ادارة الاتصال. لتفادي هذة المشكلة من المستحسن تثبيت البرنامج في الجهاز البعيد."), + ("Disconnected", "مفصول"), + ("Other", "اخرى"), + ("Confirm before closing multiple tabs", "التاكيد قبل اغلاق السنة عديدة"), + ("Keyboard Settings", "اعدادات لوحة المفاتيح"), + ("Full Access", "وصول كامل"), + ("Screen Share", "مشاركة الشاشة"), + ("ubuntu-21-04-required", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."), + ("wayland-requires-higher-linux-version", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."), + ("xdp-portal-unavailable", "لاقط شاشة Wayland فشل. بوابة سطح مكتب XDG ربما توقفت عن العمل او حدث خطأ بها. جرب اعادة تشغليها عن طريق 'systemctl --user restart xdg-desktop-portal'."), + ("JumpLink", "رابط القفز"), + ("Please Select the screen to be shared(Operate on the peer side).", "الرجاء اختيار شاشة لمشاركتها (تعمل على جانب القرين)."), + ("Show RustDesk", "عرض RustDesk"), + ("This PC", "هذا الحاسب"), + ("or", "او"), + ("Elevate", "ارتقاء"), + ("Zoom cursor", "تكبير المؤشر"), + ("Accept sessions via password", "قبول الجلسات عبر كلمة المرور"), + ("Accept sessions via click", "قبول الجلسات عبر الضغط"), + ("Accept sessions via both", "قبول الجلسات عبر الاثنين"), + ("Please wait for the remote side to accept your session request...", "الرجاء الانتظار حتى يقبل الطرف البعيد طلب الجلسة..."), + ("One-time Password", "كلمة مرور لمرة واحدة"), + ("Use one-time password", "استخدام كلمة مرور لمرة واحدة"), + ("One-time password length", "طول كلمة مرور لمرة واحدة"), + ("Request access to your device", "طلب الوصول إلى جهازك"), + ("Hide connection management window", "اخفاء نافذة ادارة الاتصال"), + ("hide_cm_tip", "السماح بالاخفاء فقط في حالة قبول الجلسات عبر كلمة المرور واستخدام كلمة المرور الدائمة"), + ("wayland_experiment_tip", "دعم Wayland لازال في المرحلة التجريبية. الرجاء استخدام X11 اذا اردت وصول كامل."), + ("Right click to select tabs", "الضغط بالزر الايمن لتحديد الالسنة"), + ("Skipped", "متجاوز"), + ("Add to address book", "اضافة الى كتاب العناوين"), + ("Group", "مجموعة"), + ("Search", "بحث"), + ("Closed manually by web console", "اغلق يدويا عبر طرفية الويب"), + ("Local keyboard type", "توع لوحة المفاتيح المحلية"), + ("Select local keyboard type", "اختر نوع لوحة المفاتيح الملحية"), + ("software_render_tip", "اذا كنت تستخدم بطاقة رسوميات Nvidia تحت لينكس والشاشة البعيد تغلق مباشرة بعد الاتصال, قم بالتبديل الى تعريفات Nouveau مفتوحة المصدر واختيار الترميز البرمجي قد يساعد. اعادة تشغيل البرناج مطلوبة."), + ("Always use software rendering", "استخدام الترميز البرمجي دائما"), + ("config_input", "لتتمكن من التحكم بسطح المكتب البعيد. يجب من RustDesk اذونات \"مراقبة المدخلات\"."), + ("config_microphone", "لتتمكن من التحدث. يجب منح RustDesk اذونات \"تسجيل الصوت\"."), + ("request_elevation_tip", "اطلب الارتقاء في حالة وجود شخص في الطرف الاخر."), + ("Wait", "انتظر"), + ("Elevation Error", "خطأ الارتقاء"), + ("Ask the remote user for authentication", "اسأل المستخدم البعيد التوثيق"), + ("Choose this if the remote account is administrator", "اختر اذا كان الحساب البعيد مدير للنظام"), + ("Transmit the username and password of administrator", "انقل اسم المستخدم وكلمة مرور مدير النظام"), + ("still_click_uac_tip", "لازال يحتاج المستخدم البعيد للضغط على موافق في نافذة تحكم حساب المستخدم في RustDesk الذي يعمل."), + ("Request Elevation", "طلب ارتقاء"), + ("wait_accept_uac_tip", "الرجاء انتظار المستخدم البعيد حتى يوافق على طلب تحكم حساب المستخدم."), + ("Elevate successfully", "الارتقاء بنجاح"), + ("uppercase", "حرف كبير"), + ("lowercase", "حرف صغير"), + ("digit", "رقم"), + ("special character", "رمز"), + ("length>=8", "الطول 8 على الاقل"), + ("Weak", "ضعيف"), + ("Medium", "متوسط"), + ("Strong", "قوي"), + ("Switch Sides", "تبديل الاماكن"), + ("Please confirm if you want to share your desktop?", "الرجاء التاكيد على انك تريد مشاركة سطح مكتبك؟"), + ("Display", "العرض"), + ("Default View Style", "شكل العرض الافتراضي"), + ("Default Scroll Style", "شكل التمرير الافتراضي"), + ("Default Image Quality", "دقة الصورة الافتراضية"), + ("Default Codec", "الترميز الاقتراضي"), + ("Bitrate", "معدل البت"), + ("FPS", "مشهد في الثانية"), + ("Auto", "تلقائي"), + ("Other Default Options", "الخيارات الافتراضية الاخرى"), + ("Voice call", "مكالمة صوتية"), + ("Text chat", "دردشة نصية"), + ("Stop voice call", "ايقاف المكالمة الصوتية"), + ("relay_hint_tip", "قد لا يكون ممكن الاتصال مباشرة. يمكنك محاولة الاتصال عبر وسيط. ايضا اذا اردت استخدام وسيط لمحاولتك الاولى اضف لاحقة \"/r\" الى المعرف او اختر \"الاتصال باستخدام وسيط دائما\" في بطاقة الجلسات الحديثة ان وجدت."), + ("Reconnect", "اعادة الاتصال"), + ("Codec", "الترميز"), + ("Resolution", "الدقة"), + ("No transfers in progress", "لا توجد عمليات نقل حاليا"), + ("Set one-time password length", "تعيين طول كلمة مرور المرة الواحدة"), + ("RDP Settings", "اعدادات RDP"), + ("Sort by", "ترتيب حسب"), + ("New Connection", "اتصال جديد"), + ("Restore", "استعادة"), + ("Minimize", "تصغير"), + ("Maximize", "تكبير"), + ("Your Device", "جهازك"), + ("empty_recent_tip", "للاسف. لا توجد جلسات حديثة\nحان الوقت للتخطيط لواحدة جديدة."), + ("empty_favorite_tip", "لا يوجد اقران مفضلين حتى الان؟\nحسنا لنبحث عن شخص للاتصال معه ومن ثم اضافته للمفضلة."), + ("empty_lan_tip", "اه لا, يبدو انك لم تكتشف اي قرين بعد."), + ("empty_address_book_tip", "يا عزيزي, يبدو انه لايوجد حاليا اي اقران في كتاب العناوين."), + ("Empty Username", "اسم مستخدم فارغ"), + ("Empty Password", "كلمة مرور فارغة"), + ("Me", "انا"), + ("identical_file_tip", "هذا الملف مطابق لملف موجود عن القرين."), + ("show_monitors_tip", "عرض الشاشات في شريط الادوات"), + ("View Mode", "وضع العرض"), + ("login_linux_tip", "تحتاج الى تسجيل الدخول حساب لينكس البعيد وتفعيل جلسة سطح مكتب X"), + ("verify_rustdesk_password_tip", "تحقق من كلمة مرور RustDesk"), + ("remember_account_tip", "تذكر هذا الحساب"), + ("os_account_desk_tip", "هذا الحساب مستخدم لتسجيل الدخول الى سطح المكتب البعيد وتفعيل الجلسة"), + ("OS Account", "حساب نظام التشغيل"), + ("another_user_login_title_tip", "مستخدم اخر مسجل دخول حاليا"), + ("another_user_login_text_tip", "قطع الاتصال"), + ("xorg_not_found_title_tip", "Xorg غير موجود"), + ("xorg_not_found_text_tip", "الرجاء تثبيت Xorg"), + ("no_desktop_title_tip", "لا يتوفر سطح مكتب"), + ("no_desktop_text_tip", "الرجاء تثبيت سطح مكتب GNOME"), + ("No need to elevate", "لا حاجة للارتقاء"), + ("System Sound", "صوت النظام"), + ("Default", "الافتراضي"), + ("New RDP", "RDP جديد"), + ("Fingerprint", "البصمة"), + ("Copy Fingerprint", "نسخ البصمة"), + ("no fingerprints", "لا توجد بصمات اصابع"), + ("Select a peer", "اختر قرين"), + ("Select peers", "اختر الاقران"), + ("Plugins", "الاضافات"), + ("Uninstall", "الغاء التثبيت"), + ("Update", "تحديث"), + ("Enable", "تفعيل"), + ("Disable", "تعطيل"), + ("Options", "الخيارات"), + ("resolution_original_tip", "الدقة الأصلية"), + ("resolution_fit_local_tip", "تناسب الدقة المحلية"), + ("resolution_custom_tip", "دقة مخصصة"), + ("Collapse toolbar", "طي شريط الادوات"), + ("Accept and Elevate", "قبول وارتقاء"), + ("accept_and_elevate_btn_tooltip", "قبول الاتصال وارتقاء صلاحيات التحكم بصلاحيات المستخدم."), + ("clipboard_wait_response_timeout_tip", "انتهى وقت الانتظار لنسخ الرد."), + ("Incoming connection", "اتصال قادم"), + ("Outgoing connection", "اتصال مغادر"), + ("Exit", "خروج"), + ("Open", "فتح"), + ("logout_tip", "هل انت متاكد من انك تريد تسجيل الخروج"), + ("Service", "الخدمة"), + ("Start", "تشغيل"), + ("Stop", "ايقاف"), + ("exceed_max_devices", "لقد وصلت الحد الأقصى لعدد الاجهزة التي يمكن دارتها."), + ("Sync with recent sessions", "المزامنة مع الجلسات الحديثة"), + ("Sort tags", "ترتيب العلامات"), + ("Open connection in new tab", "فتح اتصال في لسان جديد"), + ("Move tab to new window", "نقل اللسان الى نافذة جديدة"), + ("Can not be empty", "لا يمكن ان يكون فارغ"), + ("Already exists", "موجود مسبقا"), + ("Change Password", "تغيير كلمة المرور"), + ("Refresh Password", "تحديث كلمة المرور"), + ("ID", "المعرف"), + ("Grid View", "عرض شبكي"), + ("List View", "رعض قائمة"), + ("Select", "اختيار"), + ("Toggle Tags", "تفعيل/تعطيل العلامات"), + ("pull_ab_failed_tip", "فشل تحديث كتاب العناوين"), + ("push_ab_failed_tip", "فشل مزامنة كتاب العناوين مع الخادم"), + ("synced_peer_readded_tip", "الاجهزة الموجودة في الجلسات الحديثة سيتم مزامنتها مع كتاب العناوين"), + ("Change Color", "تغيير اللون"), + ("Primary Color", "اللون الأساسي"), + ("HSV Color", "اللون بنظام HSV"), + ("Installation Successful!", "تم التثبيت بنجاح!"), + ("Installation failed!", "فشل التثبيت!"), + ("Reverse mouse wheel", "عكس عجلة الماوس"), + ("{} sessions", "{} جلسات"), + ("scam_title", "عنوان الاحتيال"), + ("scam_text1", "تحذير! هذا قد يكون هجوم احتيالي."), + ("scam_text2", "يرجى توخي الحذر وعدم الموافقة على الاتصال إذا كنت غير متأكد."), + ("Don't show again", "لا تظهر مرة أخرى"), + ("I Agree", "أوافق"), + ("Decline", "رفض"), + ("Timeout in minutes", "مهلة بالدقائق"), + ("auto_disconnect_option_tip", "سيتم قطع الاتصال تلقائيًا إذا تم تجاوز المهلة."), + ("Connection failed due to inactivity", "فشل الاتصال بسبب عدم النشاط"), + ("Check for software update on startup", "البحث عن تحديثات البرنامج عند بدء التشغيل"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "ترقية خادم RustDesk Pro إلى {}"), + ("pull_group_failed_tip", "فشل سحب المجموعة"), + ("Filter by intersection", "تصفية حسب التقاطع"), + ("Remove wallpaper during incoming sessions", "إزالة الخلفية أثناء الجلسات الواردة"), + ("Test", "اختبار"), + ("display_is_plugged_out_msg", "تم فصل الشاشة"), + ("No displays", "لا توجد شاشات"), + ("Open in new window", "فتح في نافذة جديدة"), + ("Show displays as individual windows", "عرض الشاشات كنافذات منفصلة"), + ("Use all my displays for the remote session", "استخدام جميع شاشاتي للجلسة عن بُعد"), + ("selinux_tip", "يجب تكوين SELinux بشكل صحيح لضمان التشغيل السلس."), + ("Change view", "تغيير العرض"), + ("Big tiles", "بلاط كبير"), + ("Small tiles", "بلاط صغير"), + ("List", "قائمة"), + ("Virtual display", "الشاشة الافتراضية"), + ("Plug out all", "فصل الكل"), + ("True color (4:4:4)", "اللون الحقيقي (4:4:4)"), + ("Enable blocking user input", "تمكين حظر إدخال المستخدم"), + ("id_input_tip", "يرجى إدخال المعرف بشكل صحيح"), + ("privacy_mode_impl_mag_tip", "وضع الخصوصية مفعل. سيتم تعطيل بعض الميزات."), + ("privacy_mode_impl_virtual_display_tip", "وضع الخصوصية مفعل. يتم استخدام شاشة افتراضية."), + ("Enter privacy mode", "دخول وضع الخصوصية"), + ("Exit privacy mode", "الخروج من وضع الخصوصية"), + ("idd_not_support_under_win10_2004_tip", "لا يدعم IDD في Windows 10 الإصدار 2004 أو أقدم."), + ("input_source_1_tip", "المصدر الأول للإدخال"), + ("input_source_2_tip", "المصدر الثاني للإدخال"), + ("Swap control-command key", "تبديل مفتاح التحكم-الأمر"), + ("swap-left-right-mouse", "تبديل زر الماوس الأيسر مع الأيمن"), + ("2FA code", "رمز التحقق الثنائي"), + ("More", "المزيد"), + ("enable-2fa-title", "تمكين التحقق الثنائي"), + ("enable-2fa-desc", "زيادة الأمان عن طريق التحقق الثنائي."), + ("wrong-2fa-code", "رمز التحقق الثنائي غير صحيح"), + ("enter-2fa-title", "إدخال رمز التحقق الثنائي"), + ("Email verification code must be 6 characters.", "يجب أن يتكون رمز التحقق بالبريد الإلكتروني من 6 أحرف."), + ("2FA code must be 6 digits.", "يجب أن يتكون رمز التحقق الثنائي من 6 أرقام."), + ("Multiple Windows sessions found", "تم العثور على جلسات متعددة للنوافذ"), + ("Please select the session you want to connect to", "يرجى اختيار الجلسة التي ترغب في الاتصال بها"), + ("powered_by_me", "مدعوم بواسطة"), + ("outgoing_only_desk_tip", "اتصال الصادر فقط"), + ("preset_password_warning", "تحذير: كلمة المرور المحفوظة قد تكون ضعيفة."), + ("Security Alert", "تنبيه أمني"), + ("My address book", "دليل العناوين الخاص بي"), + ("Personal", "شخصي"), + ("Owner", "المالك"), + ("Set shared password", "تعيين كلمة مرور مشتركة"), + ("Exist in", "موجود في"), + ("Read-only", "للقراءة فقط"), + ("Read/Write", "قراءة/كتابة"), + ("Full Control", "تحكم كامل"), + ("share_warning_tip", "تحذير: قد يتمكن الآخرون من الوصول إلى معلوماتك."), + ("Everyone", "الجميع"), + ("ab_web_console_tip", "وحدة التحكم عبر الويب متاحة."), + ("allow-only-conn-window-open-tip", "السماح بفتح النافذة فقط للاتصال."), + ("no_need_privacy_mode_no_physical_displays_tip", "لا حاجة لوضع الخصوصية إذا لم تكن هناك شاشات فعلية."), + ("Follow remote cursor", "مواكبة المؤشر عن بُعد"), + ("Follow remote window focus", "مواكبة تركيز النافذة عن بُعد"), + ("default_proxy_tip", "تعيين الخادم الوكيل الافتراضي"), + ("no_audio_input_device_tip", "لا يوجد جهاز إدخال صوتي"), + ("Incoming", "وارد"), + ("Outgoing", "صادر"), + ("Clear Wayland screen selection", "مسح تحديد الشاشة Wayland"), + ("clear_Wayland_screen_selection_tip", "مسح اختيار الشاشة Wayland الحالي."), + ("confirm_clear_Wayland_screen_selection_tip", "هل أنت متأكد من مسح تحديد الشاشة Wayland؟"), + ("android_new_voice_call_tip", "مكالمة صوتية جديدة على الأندرويد"), + ("texture_render_tip", "تمكين عرض الرسوميات باستخدام الخامات"), + ("Use texture rendering", "استخدام عرض الخامات"), + ("Floating window", "نافذة عائمة"), + ("floating_window_tip", "تمكين النوافذ العائمة"), + ("Keep screen on", "ابق الشاشة مشغولة"), + ("Never", "أبدًا"), + ("During controlled", "أثناء التحكم"), + ("During service is on", "أثناء تشغيل الخدمة"), + ("Capture screen using DirectX", "التقاط الشاشة باستخدام DirectX"), + ("Back", "رجوع"), + ("Apps", "التطبيقات"), + ("Volume up", "زيادة الصوت"), + ("Volume down", "خفض الصوت"), + ("Power", "الطاقة"), + ("Telegram bot", "بوت تيليجرام"), + ("enable-bot-tip", "تمكين البوت للتفاعل مع RustDesk"), + ("enable-bot-desc", "يمكنك استخدام بوت تيليجرام للتحكم في الجلسات."), + ("cancel-2fa-confirm-tip", "إلغاء تأكيد التحقق الثنائي."), + ("cancel-bot-confirm-tip", "إلغاء تأكيد بوت تيليجرام."), + ("About RustDesk", "حول RustDesk"), + ("Send clipboard keystrokes", "إرسال ضغطات المفاتيح من الحافظة"), + ("network_error_tip", "خطأ في الشبكة، يرجى المحاولة لاحقًا."), + ("Unlock with PIN", "فتح باستخدام الرقم السري"), + ("Requires at least {} characters", "يتطلب على الأقل {} حرفًا"), + ("Wrong PIN", "الرقم السري خاطئ"), + ("Set PIN", "تعيين الرقم السري"), + ("Enable trusted devices", "تمكين الأجهزة الموثوقة"), + ("Manage trusted devices", "إدارة الأجهزة الموثوقة"), + ("Platform", "المنصة"), + ("Days remaining", "الأيام المتبقية"), + ("enable-trusted-devices-tip", "تمكين الأجهزة الموثوقة لتسهيل الوصول."), + ("Parent directory", "الدليل الأب"), + ("Resume", "استئناف"), + ("Invalid file name", "اسم ملف غير صالح"), + ("one-way-file-transfer-tip", "نقل الملفات في اتجاه واحد فقط."), + ("Authentication Required", "التوثيق مطلوب"), + ("Authenticate", "توثيق"), + ("web_id_input_tip", "يرجى إدخال المعرف بشكل صحيح"), + ("Download", "تحميل"), + ("Upload folder", "رفع المجلد"), + ("Upload files", "رفع الملفات"), + ("Clipboard is synchronized", "تمت مزامنة الحافظة"), + ("Update client clipboard", "تحديث حافظة العميل"), + ("Untagged", "غير موسوم"), + ("new-version-of-{}-tip", "تحديث جديد متاح لـ {}"), + ("Accessible devices", "الأجهزة القابلة للوصول"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "ترقية عميل RustDesk البعيد إلى {}"), + ("d3d_render_tip", "تمكين العرض باستخدام D3D"), + ("Use D3D rendering", "استخدام عرض D3D"), + ("Printer", "الطابعة"), + ("printer-os-requirement-tip", "يتطلب تثبيت الطابعة على النظام."), + ("printer-requires-installed-{}-client-tip", "الطابعة تتطلب عميل {} المثبت."), + ("printer-{}-not-installed-tip", "الطابعة {} غير مثبتة"), + ("printer-{}-ready-tip", "الطابعة {} جاهزة"), + ("Install {} Printer", "تثبيت طابعة {}"), + ("Outgoing Print Jobs", "وظائف الطباعة الصادرة"), + ("Incoming Print Jobs", "وظائف الطباعة الواردة"), + ("Incoming Print Job", "وظيفة طباعة واردة"), + ("use-the-default-printer-tip", "استخدم الطابعة الافتراضية"), + ("use-the-selected-printer-tip", "استخدم الطابعة المحددة"), + ("auto-print-tip", "تمكين الطباعة التلقائية"), + ("print-incoming-job-confirm-tip", "هل أنت متأكد من طباعة هذه الوظيفة؟"), + ("remote-printing-disallowed-tile-tip", "الطباعة عن بُعد غير مسموح بها"), + ("remote-printing-disallowed-text-tip", "الطباعة عن بُعد غير مسموح بها على هذا الجهاز"), + ("save-settings-tip", "حفظ الإعدادات"), + ("dont-show-again-tip", "لا تظهر هذا مرة أخرى"), + ("Take screenshot", "التقاط لقطة شاشة"), + ("Taking screenshot", "جارٍ التقاط لقطة الشاشة"), + ("screenshot-merged-screen-not-supported-tip", "لقطة الشاشة للشاشات المدمجة غير مدعومة"), + ("screenshot-action-tip", "إجراء لقطة الشاشة"), + ("Save as", "حفظ باسم"), + ("Copy to clipboard", "نسخ إلى الحافظة"), + ("Enable remote printer", "تمكين الطابعة عن بُعد"), + ("Downloading {}", "جارٍ تنزيل {}"), + ("{} Update", "تحديث {}"), + ("{}-to-update-tip", "يرجى تحديث {}"), + ("download-new-version-failed-tip", "فشل في تنزيل الإصدار الجديد"), + ("Auto update", "التحديث التلقائي"), + ("update-failed-check-msi-tip", "فشل التحقق من طريقة التثبيت. يرجى النقر على زر 'تنزيل' من صفحة الإصدارات للترقية يدويًا."), + ("websocket_tip", "يتم دعم الاتصالات عبر Relay فقط، WebSocket عند استخدام WebRelay."), + ("Use WebSocket", "استخدام WebSocket"), + ("Trackpad speed", "سرعة لوحة التتبع"), + ("Default trackpad speed", "سرعة لوحة التتبع الافتراضية"), + ("Numeric one-time password", "كلمة مرور رقمية لمرة واحدة"), + ("Enable IPv6 P2P connection", "تمكين اتصال نظير إلى نظير عبر IPv6"), + ("Enable UDP hole punching", "تمكين تقنية حفر الثغرات عبر UDP"), + ("View camera", "عرض الكاميرا"), + ("Enable camera", "تمكين الكاميرا"), + ("No cameras", "لا توجد كاميرات"), + ("view_camera_unsupported_tip", "عرض الكاميرا غير مدعوم في هذا الجهاز"), + ("Terminal", "الطرفية"), + ("Enable terminal", "تمكين الطرفية"), + ("New tab", "تبويب جديد"), + ("Keep terminal sessions on disconnect", "الاحتفاظ بجلسات الطرفية عند قطع الاتصال"), + ("Terminal (Run as administrator)", "الطرفية (تشغيل كمسؤول)"), + ("terminal-admin-login-tip", "لتشغيل الطرفية كمسؤول، يرجى إدخال اسم المستخدم وكلمة المرور للمسؤول."), + ("Failed to get user token.", "فشل في الحصول على رمز المستخدم."), + ("Incorrect username or password.", "اسم المستخدم أو كلمة المرور غير صحيحة."), + ("The user is not an administrator.", "المستخدم ليس لديه صلاحيات المسؤول."), + ("Failed to check if the user is an administrator.", "فشل التحقق مما إذا كان المستخدم لديه صلاحيات المسؤول."), + ("Supported only in the installed version.", "مدعوم فقط في النسخة المُثبتة."), + ("elevation_username_tip", "يرجى إدخال اسم مستخدم بصلاحيات المسؤول للمتابعة."), + ("Preparing for installation ...", "جارٍ التحضير للتثبيت..."), + ("Show my cursor", "إظهار المؤشر الخاص بي"), + ("Scale custom", "مقياس مخصص"), + ("Custom scale slider", "شريط تمرير المقياس المخصص"), + ("Decrease", "تصغير"), + ("Increase", "تكبير"), + ("Show virtual mouse", "إظهار الفأرة الافتراضية"), + ("Virtual mouse size", "حجم الفأرة الافتراضية"), + ("Small", "صغير"), + ("Large", "كبير"), + ("Show virtual joystick", "إظهار عصا التحكم الافتراضية"), + ("Edit note", "تعديل الملاحظة"), + ("Alias", "اسم مستعار"), + ("ScrollEdge", "حافة التمرير"), + ("Allow insecure TLS fallback", "السماح بالرجوع إلى TLS غير الآمن"), + ("allow-insecure-tls-fallback-tip", "يسمح باستخدام اتصال TLS غير آمن عند فشل الاتصال الآمن"), + ("Disable UDP", "تعطيل UDP"), + ("disable-udp-tip", "عند التفعيل لن يتم استخدام بروتوكول UDP"), + ("server-oss-not-support-tip", "هذه الميزة غير مدعومة من قبل خادمك"), + ("input note here", "أدخل الملاحظة هنا"), + ("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"), + ("Show terminal extra keys", "إظهار مفاتيح إضافية في الطرفية"), + ("Relative mouse mode", "وضع الماوس النسبي"), + ("rel-mouse-not-supported-peer-tip", "وضع الماوس النسبي غير مدعوم على الجهاز الآخر"), + ("rel-mouse-not-ready-tip", "وضع الماوس النسبي غير جاهز"), + ("rel-mouse-lock-failed-tip", "فشل قفل الماوس النسبي"), + ("rel-mouse-exit-{}-tip", "للخروج من وضع الماوس النسبي اضغط على {}"), + ("rel-mouse-permission-lost-tip", "تم فقدان إذن الماوس النسبي"), + ("Changelog", "سجل التغييرات"), + ("keep-awake-during-outgoing-sessions-label", "إبقاء الجهاز نشطًا أثناء الجلسات الصادرة"), + ("keep-awake-during-incoming-sessions-label", "إبقاء الجهاز نشطًا أثناء الجلسات الواردة"), + ("Continue with {}", "متابعة مع {}"), + ("Display Name", "اسم العرض"), + ("password-hidden-tip", "كلمة المرور مخفية"), + ("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/be.rs b/vendor/rustdesk/src/lang/be.rs new file mode 100644 index 0000000..1a3260c --- /dev/null +++ b/vendor/rustdesk/src/lang/be.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Стан"), + ("Your Desktop", "Ваш працоўны стол"), + ("desk_tip", "Ваш працоўны стол даступны з гэтым ID і паролем."), + ("Password", "Пароль"), + ("Ready", "Гатова"), + ("Established", "Усталявана"), + ("connecting_status", "Ідзе падключэнне да сеткі RustDesk..."), + ("Enable service", "Уключыць службу"), + ("Start service", "Запусціць службу"), + ("Service is running", "Служба запушчана"), + ("Service is not running", "Служба не запушчана"), + ("not_ready_status", "Не падключана. Праверце падключэнне."), + ("Control Remote Desktop", "Новае падключэнне"), + ("Transfer file", "Перадаць файлы"), + ("Connect", "Падключыцца"), + ("Recent sessions", "Апошнія сеансы"), + ("Address book", "Адрасная кніга"), + ("Confirmation", "Пацвярджэнне"), + ("TCP tunneling", "TCP-тунэляванне"), + ("Remove", "Выдаліць"), + ("Refresh random password", "Абнавіць выпадковы пароль"), + ("Set your own password", "Задаць свой пароль"), + ("Enable keyboard/mouse", "Выкарыстоўваць клавіятуру/мыш"), + ("Enable clipboard", "Выкарыстоўваць буфер абмену"), + ("Enable file transfer", "Выкарыстоўваць перадачу файлаў"), + ("Enable TCP tunneling", "Выкарыстоўваць тунэляванне TCP"), + ("IP Whitelisting", "Спіс дазволеных IP-адрасоў"), + ("ID/Relay Server", "ID/Рэтранслятар"), + ("Import server config", "Імпартаваць канфігурацыю сервера"), + ("Export Server Config", "Экспартаваць канфігурацыю сервера"), + ("Import server configuration successfully", "Канфігурацыя сервера паспяхова імпартавана"), + ("Export server configuration successfully", "Канфігурацыя сервера паспяхова экспартавана"), + ("Invalid server configuration", "Няправільная канфігурацыя сервера"), + ("Clipboard is empty", "Буфер абмену пусты"), + ("Stop service", "Спыніць службу"), + ("Change ID", "Змяніць ID"), + ("Your new ID", "Новы ID"), + ("length %min% to %max%", "даўжыня %min%...%max%"), + ("starts with a letter", "пачынаецца з літары"), + ("allowed characters", "дазволеныя сімвалы"), + ("id_change_tip", "Дазволена выкарыстоўваць толькі сімвалы a-z, A-Z, 0-9, - (dash) і _ (падкрэсліванне). Першай павінна быць літара a-z, A-Z. Даўжыня ад 6 да 16."), + ("Website", "Сайт"), + ("About", "Пра праграму"), + ("Slogan_tip", "Зроблена з душой у гэтым вар'яцкім свеце!"), + ("Privacy Statement", "Заява аб канфідэнцыйнасці"), + ("Mute", "Адключыць гук"), + ("Build Date", "Дата зборкі"), + ("Version", "Версія"), + ("Home", "Галоўная"), + ("Audio Input", "Аўдыяўваход"), + ("Enhancements", "Паляпшэнні"), + ("Hardware Codec", "Апаратны кодэк"), + ("Adaptive bitrate", "Адаптыўны бітрэйт"), + ("ID Server", "Сервер ID"), + ("Relay Server", "Рэтранслятар"), + ("API Server", "Сервер API"), + ("invalid_http", "Адрас павінен пачынацца з http:// або https://"), + ("Invalid IP", "Няправільны IP-адрас"), + ("Invalid format", "Няправільны фармат"), + ("server_not_support", "Пакуль не падтрымліваецца серверам"), + ("Not available", "Недаступна"), + ("Too frequent", "Занадта часта"), + ("Cancel", "Скасаваць"), + ("Skip", "Прапусціць"), + ("Close", "Закрыць"), + ("Retry", "Паўтарыць спробу"), + ("OK", "ОК"), + ("Password Required", "Патрабуецца пароль"), + ("Please enter your password", "Увядзіце пароль"), + ("Remember password", "Запомніць пароль"), + ("Wrong Password", "Няправільны пароль"), + ("Do you want to enter again?", "Паўтарыць уваход?"), + ("Connection Error", "Памылка падключэння"), + ("Error", "Памылка"), + ("Reset by the peer", "Скінута абанентам"), + ("Connecting...", "Падключэнне..."), + ("Connection in progress. Please wait.", "Ідзе падключэнне. Пачакайце."), + ("Please try 1 minute later", "Паспрабуйце праз хвіліну"), + ("Login Error", "Памылка ўваходу"), + ("Successful", "Паспяхова"), + ("Connected, waiting for image...", "Падключана, чаканне відарыса..."), + ("Name", "Назва"), + ("Type", "Тып"), + ("Modified", "Зменена"), + ("Size", "Памер"), + ("Show Hidden Files", "Паказаць схаваныя файлы"), + ("Receive", "Атрымаць"), + ("Send", "Адправіць"), + ("Refresh File", "Абнавіць файл"), + ("Local", "Лакальны"), + ("Remote", "Аддалены"), + ("Remote Computer", "Аддалены камп'ютар"), + ("Local Computer", "Лакальны камп'ютар"), + ("Confirm Delete", "Пацвердзіць выдаленне"), + ("Delete", "Выдаліць"), + ("Properties", "Уласцівасці"), + ("Multi Select", "Шматлікі выбар"), + ("Select All", "Выбраць усе"), + ("Unselect All", "Скасаваць выбар усіх"), + ("Empty Directory", "Пусты каталог"), + ("Not an empty directory", "Каталог не пусты"), + ("Are you sure you want to delete this file?", "Выдаліць гэты файл?"), + ("Are you sure you want to delete this empty directory?", "Выдаліць пусты каталог?"), + ("Are you sure you want to delete the file of this directory?", "Выдаліць файл з гэтага каталога?"), + ("Do this for all conflicts", "Прымяніць да ўсіх канфліктаў"), + ("This is irreversible!", "Гэтага нельга адрабіць!"), + ("Deleting", "Ідзе выдаленне"), + ("files", "файлы"), + ("Waiting", "Чаканне"), + ("Finished", "Завершана"), + ("Speed", "Хуткасць"), + ("Custom Image Quality", "Карыстальніцкая якасць відарыса"), + ("Privacy mode", "Рэжым канфідэнцыйнасці"), + ("Block user input", "Заблакіраваць увод на аддаленай прыладзе"), + ("Unblock user input", "Разблакіраваць увод на аддаленай прыладзе"), + ("Adjust Window", "Наладзіць акно"), + ("Original", "Арыгінал"), + ("Shrink", "Сціснуць"), + ("Stretch", "Расцягнуць"), + ("Scrollbar", "Паласа прагортвання"), + ("ScrollAuto", "Аўта-прагортванне"), + ("Good image quality", "Добрая якасць відарыса"), + ("Balanced", "Баланс паміж якасцю і хуткасцю"), + ("Optimize reaction time", "Аптымізацыя хуткасці рэакцыі"), + ("Custom", "Карыстальніцкая"), + ("Show remote cursor", "Паказваць аддалены курсор"), + ("Show quality monitor", "Паказваць манітор якасці"), + ("Disable clipboard", "Адключыць буфер абмену"), + ("Lock after session end", "Заблакіраваць уліковы запіс пасля сеанса"), + ("Insert Ctrl + Alt + Del", "Уставіць Ctrl + Alt + Del"), + ("Insert Lock", "Заблакіраваць уліковы запіс"), + ("Refresh", "Абнавіць"), + ("ID does not exist", "ID не існуе"), + ("Failed to connect to rendezvous server", "Немагчыма падключыцца да прамежкавага сервера"), + ("Please try later", "Паспрабуйце пазней"), + ("Remote desktop is offline", "Аддаленая прылада не ў сетцы"), + ("Key mismatch", "Неадпаведнасць ключоў"), + ("Timeout", "Час чакання скончыўся"), + ("Failed to connect to relay server", "Немагчыма падключыцца да рэтранслятара"), + ("Failed to connect via rendezvous server", "Немагчыма падключыцца праз прамежкавы сервер"), + ("Failed to connect via relay server", "Немагчыма падключыцца праз рэтранслятар"), + ("Failed to make direct connection to remote desktop", "Не ўдалося ўсталяваць прамога падключэння да аддаленай прылады"), + ("Set Password", "Задаць пароль"), + ("OS Password", "Пароль уваходу ў аперацыйную сістэму"), + ("install_tip", "У некаторых выпадках з-за UAC, RustDesk можа працаваць на баку абанента неадпаведным чынам. Каб пазбегнуць магчымых праблем з UAC, націсніце кнопку ніжэй для ўсталявання RustDesk у сістэме."), + ("Click to upgrade", "Абнавіць"), + ("Configure", "Наладзіць"), + ("config_acc", "Каб аддаленна кіраваць сваім працоўным сталом, вам трэба дазволіць RustDesk правы \"доступу\""), + ("config_screen", "Для аддаленага доступу да працоўнага стала вам трэба даць RustDesk правы \"здымку экрана\"."), + ("Installing ...", "Ідзе ўсталёўванне..."), + ("Install", "Усталяваць"), + ("Installation", "Усталёўванне"), + ("Installation Path", "Шлях усталёўвання"), + ("Create start menu shortcuts", "Стварыць ярлыкі ў меню \"Пуск\""), + ("Create desktop icon", "Стварыць значок на працоўным стале"), + ("agreement_tip", "Пачынаючы ўсталёўванне, вы прымаеце ўмовы ліцэнзійнага пагаднення."), + ("Accept and Install", "Прыняць і ўсталяваць"), + ("End-user license agreement", "Ліцэнзійнае пагадненне з канчатковым карыстальнікам"), + ("Generating ...", "Ідзе генерыраванне..."), + ("Your installation is lower version.", "Усталявана ранейшая версія"), + ("not_close_tcp_tip", "Не закрываць гэтага акна пры выкарыстанні тунэлю."), + ("Listening ...", "Чаканне..."), + ("Remote Host", "Аддалены хост"), + ("Remote Port", "Аддалены порт"), + ("Action", "Дзеянне"), + ("Add", "Дадаць"), + ("Local Port", "Лакальны порт"), + ("Local Address", "Лакальны адрас"), + ("Change Local Port", "Змяніць лакальны порт"), + ("setup_server_tip", "Для хутчэйшага падключэння наладзьце ўласны сервер."), + ("Too short, at least 6 characters.", "Занадта кароткі, мінімум 6 сімвалаў."), + ("The confirmation is not identical.", "Пацвярджэнне не супадае."), + ("Permissions", "Дазволы"), + ("Accept", "Прыняць"), + ("Dismiss", "Адхіліць"), + ("Disconnect", "Адключыць"), + ("Enable file copy and paste", "Дазволіць капіяванне і ўстаўку файлаў"), + ("Connected", "Падключана"), + ("Direct and encrypted connection", "Прамое і зашыфраванае падключэнне"), + ("Relayed and encrypted connection", "Рэтрансляванае і зашыфраванае падключэнне"), + ("Direct and unencrypted connection", "Прамое і незашыфраванае падключэнне"), + ("Relayed and unencrypted connection", "Рэтрансляванае і незашыфраванае падключэнне"), + ("Enter Remote ID", "Увядзіце ID абанента"), + ("Enter your password", "Увядзіце пароль"), + ("Logging in...", "Уваходжанне..."), + ("Enable RDP session sharing", "Уключыць абагульванне сеанса RDP"), + ("Auto Login", "Аўтаматычны ўваход ва ўліковы запіс"), + ("Enable direct IP access", "Дазволіць прамы доступ па IP-адрасе"), + ("Rename", "Перайменаваць"), + ("Space", "Месца"), + ("Create desktop shortcut", "Стварыць ярлык на працоўным стале"), + ("Change Path", "Змяніць шлях"), + ("Create Folder", "Стварыць папку"), + ("Please enter the folder name", "Увядзіце імя папкі"), + ("Fix it", "Выправіць"), + ("Warning", "Папярэджанне"), + ("Login screen using Wayland is not supported", "Уваход у сістэму з выкарыстаннем Wayland не падтрымліваецца"), + ("Reboot required", "Патрабуецца перазагрузка"), + ("Unsupported display server", "Сервер адлюстравання не падтрымліваецца"), + ("x11 expected", "Чакаецца X11"), + ("Port", "Порт"), + ("Settings", "Налады"), + ("Username", "Імя карыстальніка"), + ("Invalid port", "Памылковы порт"), + ("Closed manually by the peer", "Закрыта абанентам уручную"), + ("Enable remote configuration modification", "Дазволіць аддаленае змяненне канфігурацыі"), + ("Run without install", "Запусціць без усталявання"), + ("Connect via relay", "Падключыцца праз рэтранслятар"), + ("Always connect via relay", "Заўсёды падключацца праз рэтранслятар"), + ("whitelist_tip", "Атрымліваць доступ да маёй прылады могуць толькі IP-адрасы з белага спісу."), + ("Login", "Увайсці"), + ("Verify", "Праверыць"), + ("Remember me", "Запомніць"), + ("Trust this device", "Давяраць гэтай прыладзе"), + ("Verification code", "Праверачны код"), + ("verification_tip", "Выяўлена новая прылада, на зарэгістраваны адрас электроннай пошты адпраўлены праверачны код. Увядзіце яго, каб працягнуць уваходжанне ў сістэму."), + ("Logout", "Выйсці"), + ("Tags", "Цэтлікі"), + ("Search ID", "Пошук по ID"), + ("whitelist_sep", "Падзяленне коскай, кропкай з коскай, прабелам або новым радком."), + ("Add ID", "Дадаць ID"), + ("Add Tag", "Дадаць цэтлік"), + ("Unselect all tags", "Скасаваць выбар усіх цэтлікаў"), + ("Network error", "Памылка сеткі"), + ("Username missed", "Прапушчана імя карыстальніка"), + ("Password missed", "Прапушчаны пароль"), + ("Wrong credentials", "Памылковае імя або пароль"), + ("The verification code is incorrect or has expired", "Памылковы або пратэрмінаваны праверачны код"), + ("Edit Tag", "Рэдагаваць цэтлік"), + ("Forget Password", "Не захоўваць пароль"), + ("Favorites", "Абранае"), + ("Add to Favorites", "Дадаць у абранае"), + ("Remove from Favorites", "Выдаліць з абранага"), + ("Empty", "Пуста"), + ("Invalid folder name", "Недапушчальная назва папкі"), + ("Socks5 Proxy", "Socks5-проксі"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s)-проксі"), + ("Discovered", "Знойдзена"), + ("install_daemon_tip", "Для запуску пры загрузцы трэба ўсталяваць сістэмную службу"), + ("Remote ID", "ID абанента"), + ("Paste", "Уставіць"), + ("Paste here?", "Уставіць сюды?"), + ("Are you sure to close the connection?", "Закрыць падключэнне?"), + ("Download new version", "Спампаваць новую версію"), + ("Touch mode", "Рэжым сэнсарнага экрана"), + ("Mouse mode", "Рэжым мышы/сэнсарнай панэлі"), + ("One-Finger Tap", "Націсканне адным пальцам"), + ("Left Mouse", "Левая кнопка мышы"), + ("One-Long Tap", "Доўгае націсканне адным пальцам"), + ("Two-Finger Tap", "Націсканне двума пальцамі"), + ("Right Mouse", "Правая кнопка мышы"), + ("One-Finger Move", "Перамяшчэнне адным пальцам"), + ("Double Tap & Move", "Двайное націсканне і перамяшчэнне"), + ("Mouse Drag", "Перацягванне мышшу"), + ("Three-Finger vertically", "Трыма пальцамі па вертыкалі"), + ("Mouse Wheel", "Колца мышы"), + ("Two-Finger Move", "Перамяшчэнне двума пальцамі"), + ("Canvas Move", "Перамяшчэнне палатна"), + ("Pinch to Zoom", "Маштабаванне шчыпком"), + ("Canvas Zoom", "Маштабаванне палатна"), + ("Reset canvas", "Скінуць маштабаванне палатна"), + ("No permission of file transfer", "Няма дазволу на перадачу файлаў"), + ("Note", "Нататка"), + ("Connection", "Падключэнне"), + ("Share screen", "Дэманстрацыя экрана"), + ("Chat", "Чат"), + ("Total", "Усяго"), + ("items", "элементы"), + ("Selected", "Выбрана"), + ("Screen Capture", "Захоп экрана"), + ("Input Control", "Кіраванне ўводам"), + ("Audio Capture", "Захоп аўдыя"), + ("Do you accept?", "Вы згодныя?"), + ("Open System Setting", "Адкрыць налады сістэмы"), + ("How to get Android input permission?", "Як атрымаць дазвол на ўвод Android?"), + ("android_input_permission_tip1", "Каб аддаленая прылада магла кіраваць вашай Android-прыладай з дапамогай мышы або націсканняў, трэба дазволіць RustDesk выкарыстоўваць службу \"Спецыяльныя магчымасці\"."), + ("android_input_permission_tip2", "Зайдзіце на адпаведную старонку сістэмных налад, знайдзіце і перайдзіце ва \"Усталяваныя службы\", уключыце службу \"RustDesk Input\"."), + ("android_new_connection_tip", "Новы запыт на кіраванне вашай бягучай прыладай."), + ("android_service_will_start_tip", "Уключэнне захопу экрана аўтаматычна запускае службу, дазваляючы іншым прыладам запытаць падключэнне да гэтай прылады."), + ("android_stop_service_tip", "Закрыццё службы аўтаматычна закрые ўсе ўстаноўленыя падключэнні."), + ("android_version_audio_tip", "Бягучая версія Android не падтрымлівае захопу гуку, абнавіце яе да Android 10 ці вышэй."), + ("android_start_service_tip", "Націсніце [Запусціць службу] або дазвольце [Захоп экрана], каб запусціць службу дэманстрацыі экрана."), + ("android_permission_may_not_change_tip", "Дазволы для ўстаноўленых падключэнняў не могуць быць зменены, патрабуецца перападключэнне."), + ("Account", "Уліковы запіс"), + ("Overwrite", "Перазапісаць"), + ("This file exists, skip or overwrite this file?", "Файл існуе, прапусціць ці перазапісаць яго?"), + ("Quit", "Выйсці"), + ("Help", "Дапамога"), + ("Failed", "Не ўдалося"), + ("Succeeded", "Выканана"), + ("Someone turns on privacy mode, exit", "Хтосьці ўключыў рэжым канфідэнцыйнасці, выхад"), + ("Unsupported", "Не падтрымліваецца"), + ("Peer denied", "Забаронена абанентам"), + ("Please install plugins", "Усталюйце ўбудовы"), + ("Peer exit", "Абанент выйшаў"), + ("Failed to turn off", "Немагчыма выключыць"), + ("Turned off", "Выключаны"), + ("Language", "Мова"), + ("Keep RustDesk background service", "Захаваць фонавую службу RustDesk"), + ("Ignore Battery Optimizations", "Ігнараваць аптымізацыю ўжывання батарэі"), + ("android_open_battery_optimizations_tip", "Перайдзіце на наступную старонку налад"), + ("Start on boot", "Запускаць пры загрузцы"), + ("Start the screen sharing service on boot, requires special permissions", "Запускаць службу дэманстрацыі экрана пры загрузцы (патрабуюцца спецыяльныя дазволы)"), + ("Connection not allowed", "Падключэнне не дазволена"), + ("Legacy mode", "Састарэлы рэжым"), + ("Map mode", "Рэжым супастаўлення"), + ("Translate mode", "Рэжым перакладу"), + ("Use permanent password", "Выкарыстоўваць пастаянны пароль"), + ("Use both passwords", "Выкарыстоўваць абодва паролі"), + ("Set permanent password", "Задаць пастаянны пароль"), + ("Enable remote restart", "Дазволіць аддалены перазапуск"), + ("Restart remote device", "Перазапусціць аддаленую прыладу"), + ("Are you sure you want to restart", "Вы ўпэўненыя, што хочаце зрабіць перазапуск?"), + ("Restarting remote device", "Ідзе перазапуск аддаленай прылады"), + ("remote_restarting_tip", "Аддаленая прылада перазапускаецца. Закрыйце гэта паведамленне і праз некаторы час перападключыцеся, выкарыстоўваючы пастаянны пароль."), + ("Copied", "Скапіявана"), + ("Exit Fullscreen", "Выйсці з поўнаэкраннага рэжыму"), + ("Fullscreen", "Поўнаэкранны рэжым"), + ("Mobile Actions", "Мабільныя дзеянні"), + ("Select Monitor", "Выберыце манітор"), + ("Control Actions", "Дзеянні па кіраванні"), + ("Display Settings", "Налады адлюстравання"), + ("Ratio", "Суадносіны"), + ("Image Quality", "Якасць відарыса"), + ("Scroll Style", "Стыль прагортвання"), + ("Show Toolbar", "Паказаць панэль інструментаў"), + ("Hide Toolbar", "Схаваць панэль інструментаў"), + ("Direct Connection", "Прамое падключэнне"), + ("Relay Connection", "Рэтрансляванае падключэнне"), + ("Secure Connection", "Бяспечнае падключэнне"), + ("Insecure Connection", "Нябяспечнае падключэнне"), + ("Scale original", "Арыгінальны маштаб"), + ("Scale adaptive", "Адаптыўны маштаб"), + ("General", "Агульныя"), + ("Security", "Бяспека"), + ("Theme", "Тэма"), + ("Dark Theme", "Цёмная тэма"), + ("Light Theme", "Светлая тэма"), + ("Dark", "Цёмная"), + ("Light", "Светлая"), + ("Follow System", "Сістэмная"), + ("Enable hardware codec", "Уключыць апаратны кодэк"), + ("Unlock Security Settings", "Разблакіраваць налады бяспекі"), + ("Enable audio", "Уключыць перадачу гуку"), + ("Unlock Network Settings", "Разблакіраваць сеткавыя налады"), + ("Server", "Сервер"), + ("Direct IP Access", "Прамы IP-доступ"), + ("Proxy", "Проксі"), + ("Apply", "Прымяніць"), + ("Disconnect all devices?", "Адключыць усе прылады?"), + ("Clear", "Ачысціць"), + ("Audio Input Device", "Прылада ўводу гуку"), + ("Use IP Whitelisting", "Выкарыстоўваць белы спіс IP"), + ("Network", "Сетка"), + ("Pin Toolbar", "Закрэпіць панэль інструментаў"), + ("Unpin Toolbar", "Адкрэпіць панэль інструментаў"), + ("Recording", "Запіс"), + ("Directory", "Каталог"), + ("Automatically record incoming sessions", "Аўтаматычна запісваць уваходныя сесіі"), + ("Automatically record outgoing sessions", ""), + ("Change", "Змяніць"), + ("Start session recording", "Пачаць запіс сесіі"), + ("Stop session recording", "Спыніць запіс сесіі"), + ("Enable recording session", "Уключыць запіс сесіі"), + ("Enable LAN discovery", "Уключыць выяўленне ў лакальнай сетцы"), + ("Deny LAN discovery", "Забараніць выяўленне ў лакальнай сетцы"), + ("Write a message", "Напісаць паведамленне"), + ("Prompt", "Падказка"), + ("Please wait for confirmation of UAC...", "Дачакайцеся пацверджання UAC..."), + ("elevated_foreground_window_tip", "Бягучае акно аддаленага працоўнага стала патрабуе вышэйшых прывілегій для працы, таму часова немагчыма выкарыстоўваць мыш і клавіятуру. Можна папрасіць абанента згарнуць бягучае акно або націснуць кнопку павышэння правоў у акне кіравання падключэннем. Каб прадухіліць гэту праблему ў будучыні, рэкамендуецца ўсталяваць праграмнае забеспячэнне на аддаленай прыладзе."), + ("Disconnected", "Адключана"), + ("Other", "Іншае"), + ("Confirm before closing multiple tabs", "Пацвердзіць закрыццё некалькіх укладак"), + ("Keyboard Settings", "Налады клавіятуры"), + ("Full Access", "Поўны доступ"), + ("Screen Share", "Дэманстрацыя экрана"), + ("ubuntu-21-04-required", "Wayland патрабуе Ubuntu версіі 21.04 або навейшай."), + ("wayland-requires-higher-linux-version", "Для Wayland патрабуецца вышэйшая версія дыстрыбутыва Linux. Карыстайцеся працоўным сталом X11 або зменіце сваю АС."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Прагляд"), + ("Please Select the screen to be shared(Operate on the peer side).", "Выберыце экран для дэманстрацыі (кіруецца на баку абанента)."), + ("Show RustDesk", "Паказаць RustDesk"), + ("This PC", "Гэты камп’ютар"), + ("or", "або"), + ("Elevate", "Павысіць"), + ("Zoom cursor", "Маштабаванне курсора"), + ("Accept sessions via password", "Прымаць сеансы па паролю"), + ("Accept sessions via click", "Прымаць сеансы націскам кнопкі"), + ("Accept sessions via both", "Прымаць сеансы па паролю і націскам кнопкі"), + ("Please wait for the remote side to accept your session request...", "Дачакайцеся, пакуль абанент прымае ваш запыт на сеанс..."), + ("One-time Password", "Аднаразовы пароль"), + ("Use one-time password", "Выкарыстоўваць аднаразовы пароль"), + ("One-time password length", "Даўжыня аднагаразовага пароля"), + ("Request access to your device", "Запыт на доступ да вашай прылады"), + ("Hide connection management window", "Схаваць акно кіравання падключэннямі"), + ("hide_cm_tip", "Дазваляць схаванне акна ў выпадку, калі прымаюцца сесіі па паролю або выкарыстоўваецца пастаянны пароль"), + ("wayland_experiment_tip", "Падтрымка Wayland знаходзіцца на эксперыментальнай стадыі, калі вам трэба аўтаматычны доступ, выкарыстоўвайце X11."), + ("Right click to select tabs", "Выбар укладак націсканнем правай кнопкі мышы"), + ("Skipped", "Прапушчана"), + ("Add to address book", "Дадаць у адрасную кнігу"), + ("Group", "Група"), + ("Search", "Пошук"), + ("Closed manually by web console", "Закрыта ўручную праз вэб-кансоль"), + ("Local keyboard type", "Тып лакальнай клавіятуры"), + ("Select local keyboard type", "Выберыце тып лакальнай клавіятуры"), + ("software_render_tip", "Калі ў вас ёсць відэакарта Nvidia і аддаленае акно закрываецца адразу пасля падключэння, магчыма, дапаможа ўсталяванне драйвера Nouveau і выбар выкарыстання праграмнай візуалізацыі. Патрабуецца перазагрузка."), + ("Always use software rendering", "Заўсёды выкарыстоўваць праграмную візуалізацыю"), + ("config_input", "Каб кіраваць аддаленым працоўным сталом праз клавіятуру, трэба дазволіць RustDesk \"Маніторынг уводу\"."), + ("config_microphone", "Каб размаўляць з абанентам, трэба дазволіць RustDesk запіс аўдыя."), + ("request_elevation_tip", "Таксама можна запытаць павышэння правоў, калі хто-небудзь знаходзіцца на баку абанента."), + ("Wait", "Чакайце"), + ("Elevation Error", "Памылка павышэння правоў"), + ("Ask the remote user for authentication", "Запытаць праверку сапраўднасці ў абанента"), + ("Choose this if the remote account is administrator", "Выберыце гэта, калі абанент з'яўляецца адміністратарам"), + ("Transmit the username and password of administrator", "Перадаць імя карыстальніка і пароль адміністратара"), + ("still_click_uac_tip", "Дагэтуль патрэбна, каб абанент націснуў \"OK\" ў акне UAC пры запуску RustDesk."), + ("Request Elevation", "Запытаць павышэння"), + ("wait_accept_uac_tip", "Пачакайце, пакуль абанент пацвердзіць запыт UAC."), + ("Elevate successfully", "Правы павышаны"), + ("uppercase", "верхні рэгістр"), + ("lowercase", "ніжні рэгістр"), + ("digit", "лічбы"), + ("special character", "спецыяльныя сімвалы"), + ("length>=8", "8+ сімвалаў"), + ("Weak", "Слабы"), + ("Medium", "Сярэдні"), + ("Strong", "Моцны"), + ("Switch Sides", "Пераключыць бакі"), + ("Please confirm if you want to share your desktop?", "Вы сапраўды дазваляеце дэманстрацыю працоўнага стала?"), + ("Display", "Адлюстраванне"), + ("Default View Style", "Стандартны стыль адлюстравання"), + ("Default Scroll Style", "Стандартны стыль прагортвання"), + ("Default Image Quality", "Стандартная якасць відарыса"), + ("Default Codec", "Стандартны кодэк"), + ("Bitrate", "Бітрэйт"), + ("FPS", "Колькасць кадраў у секунду"), + ("Auto", "Аўта"), + ("Other Default Options", "Іншыя стандартныя параметры"), + ("Voice call", "Галасавы выклік"), + ("Text chat", "Тэкставы чат"), + ("Stop voice call", "Спыніць галасавы выклік"), + ("relay_hint_tip", "Непасрэднае падключэнне можа быць немагчымым. У гэтым выпадку можна спрабаваць падключыцца праз рэтранслятар.\nАкрамя таго, калі вы хочаце адразу выкарыстоўваць рэтранслятар, можна дадаць да ідэнтыфікатара суфікс \"/r\" або ўключыць \"Заўсёды падключацца праз рэтранслятар\" у наладах абанента."), + ("Reconnect", "Перападключыць"), + ("Codec", "Кодэк"), + ("Resolution", "Раздзяляльнасць"), + ("No transfers in progress", "Перадача не ажыццяўляецца"), + ("Set one-time password length", "Усталяваць даўжыню аднаразовага пароля"), + ("RDP Settings", "Налады RDP"), + ("Sort by", "Сартаваць па"), + ("New Connection", "Новае падключэнне"), + ("Restore", "Аднавіць"), + ("Minimize", "Згарнуць"), + ("Maximize", "Разгарнуць"), + ("Your Device", "Ваша прылада"), + ("empty_recent_tip", "Няма апошніх сеансаў!\nЧас запланаваць новы."), + ("empty_favorite_tip", "Яшчэ няма абраных абанентаў?\nДавайце знойдзем, каго можна дадаць у абранае."), + ("empty_lan_tip", "Абанентаў не знойдзена."), + ("empty_address_book_tip", "У адраснай кнізе няма абанентаў."), + ("Empty Username", "Пустае імя карыстальніка"), + ("Empty Password", "Пусты пароль"), + ("Me", "Я"), + ("identical_file_tip", "Файл ідэнтычны файлу абанента"), + ("show_monitors_tip", "Паказваць маніторы на панэлі інструментаў"), + ("View Mode", "Рэжым прагляду"), + ("login_linux_tip", "Каб уключыць сеанс працоўнага стала X, трэба ўвайсці ў аддалены ўліковы запіс Linux."), + ("verify_rustdesk_password_tip", "Пацвердзіць пароль RustDesk"), + ("remember_account_tip", "Запомніць гэты ўліковы запіс"), + ("os_account_desk_tip", "Гэты ўліковы запіс выкарыстоўваецца для ўваходу ў аддаленую аперацыйную сістэму і ўключэння сеанса працоўнага стала ў рэжыме headless."), + ("OS Account", "Акаўнт АС"), + ("another_user_login_title_tip", "Іншы карыстальнік ужо ўвайшоў у сістэму"), + ("another_user_login_text_tip", "Адключыць"), + ("xorg_not_found_title_tip", "Xorg не знойдзены"), + ("xorg_not_found_text_tip", "Усталюйце Xorg"), + ("no_desktop_title_tip", "Няма даступных працоўных сталоў"), + ("no_desktop_text_tip", "Усталюйце GNOME Desktop"), + ("No need to elevate", "Павышэнне правоў не патрабуецца"), + ("System Sound", "Сістэмны гук"), + ("Default", "Стандартна"), + ("New RDP", "Новы RDP"), + ("Fingerprint", "Адбітак"), + ("Copy Fingerprint", "Капіяваць адбітак"), + ("no fingerprints", "адбіткі адсутнічаюць"), + ("Select a peer", "Выберыце абанента"), + ("Select peers", "Выберыце абанентаў"), + ("Plugins", "Убудовы"), + ("Uninstall", "Выдаліць"), + ("Update", "Абнавіць"), + ("Enable", "Уключыць"), + ("Disable", "Адключыць"), + ("Options", "Параметры"), + ("resolution_original_tip", "Арыгінальная раздзяляльнасць"), + ("resolution_fit_local_tip", "Супадзенне з лакальнай раздзяляльнасцю"), + ("resolution_custom_tip", "Карыстацкая раздзяляльнасць"), + ("Collapse toolbar", "Згарнуць панэль інструментаў"), + ("Accept and Elevate", "Прыняць і павысіць"), + ("accept_and_elevate_btn_tooltip", "Дазволіць падключэнне і павысіць правы UAC."), + ("clipboard_wait_response_timeout_tip", "Час чакання адказу капіявання буфера абмену скончыўся"), + ("Incoming connection", "Уваходнае падключэнне"), + ("Outgoing connection", "Выходнае падключэнне"), + ("Exit", "Выйсці"), + ("Open", "Адкрыць"), + ("logout_tip", "Вы сапраўды хочаце выйсці?"), + ("Service", "Служба"), + ("Start", "Запусціць"), + ("Stop", "Спыніць"), + ("exceed_max_devices", "Дасягнута максімальная колькасць кантраляваных прылад."), + ("Sync with recent sessions", "Сінхранізацыя з апошнімі сеансамі"), + ("Sort tags", "Сартаваць цэтлікі"), + ("Open connection in new tab", "Адкрыць падключэнне ў новай укладцы"), + ("Move tab to new window", "Перамясціць укладку ў новае акно"), + ("Can not be empty", "Ня можа быць пустым"), + ("Already exists", "Ужо існуе"), + ("Change Password", "Змяніць пароль"), + ("Refresh Password", "Абнавіць пароль"), + ("ID", "ID"), + ("Grid View", "Сетка"), + ("List View", "Спіс"), + ("Select", "Выбар"), + ("Toggle Tags", "Пераключыць цэтлікі"), + ("pull_ab_failed_tip", "Немагчыма абнавіць адрасную кнігу"), + ("push_ab_failed_tip", "Немагчыма сінхранізаваць адрасную кнігу з серверам"), + ("synced_peer_readded_tip", "Прылады, якія былі на апошніх сеансах, будуць сінхранізаваны з адраснай кнігай."), + ("Change Color", "Змяніць колер"), + ("Primary Color", "Асноўны колер"), + ("HSV Color", "Колер HSV"), + ("Installation Successful!", "Усталяванне выканана!"), + ("Installation failed!", "Усталяванне не ўдалося."), + ("Reverse mouse wheel", "Адваротнае прагортванне мышшу"), + ("{} sessions", "Колькасць сеансаў: {}"), + ("scam_title", "Вас могуць ПАДМАНУЦЬ!"), + ("scam_text1", "Калі вы размаўляеце па тэлефоне з кімсьці НЕЗНАЁМЫМ і каму вы НЕ ДАВЕРАЕЦЕ, і гэта асоба просіць вас выкарыстаць RustDesk і запусціць яго службу, не працягвайце і неадкладна скончыце размову."), + ("scam_text2", "Магчыма, гэта аферыст, які спрабуе скрасці вашы грошы або іншую асабістую інфармацыю."), + ("Don't show again", "Не паказваць больш"), + ("I Agree", "Згаджаюся"), + ("Decline", "Адхіліць"), + ("Timeout in minutes", "Час чакання (у хвілінах)"), + ("auto_disconnect_option_tip", "Аўтаматычна закрываць уваходныя сеансы пры неактыўнасці карыстальніка"), + ("Connection failed due to inactivity", "Збой падключэння з-за неактыўнасці"), + ("Check for software update on startup", "Праверка абнаўленняў праграмы пры запуску"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Абнавіце RustDesk Server Pro да версіі {} або навейшай!"), + ("pull_group_failed_tip", "Немагчыма абнавіць групу"), + ("Filter by intersection", "Фільтраваць па перасячэнні"), + ("Remove wallpaper during incoming sessions", "Схаваць шпалеры працоўнага стала ў часе ўваходнага сеанса"), + ("Test", "Тэст"), + ("display_is_plugged_out_msg", "Дысплэй адключаны, пераключыцеся на першы дысплэй."), + ("No displays", "Няма дысплэяў"), + ("Open in new window", "Адкрыць у новым акне"), + ("Show displays as individual windows", "Паказваць дысплэі ў асобных вокнах"), + ("Use all my displays for the remote session", "Выкарыстоўваць усе мае дысплэі для аддаленага сеанса"), + ("selinux_tip", "На вашай прыладзе ўключаны SELinux, што можа ствараць перашкоды ў працы RustDesk на баку абанента."), + ("Change view", "Рэжым"), + ("Big tiles", "Вялікія пліткі"), + ("Small tiles", "Маленькія пліткі"), + ("List", "Спіс"), + ("Virtual display", "Віртуальны дысплэй"), + ("Plug out all", "Адключыць усё"), + ("True color (4:4:4)", "True color (4:4:4)"), + ("Enable blocking user input", "Дазволіць блакіраванне ўводу на прыладзе"), + ("id_input_tip", "Можна ўвесці ідэнтыфікатар, прамы IP-адрас або дамен з портам (<дамен>:<порт>).\nКаб атрымаць доступ да прылады на іншым серверы, дадайце адрас сервера (@<адрас_сервера>?key=<ключ_значэнне>), напрыклад:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nКалі трэба атрымаць доступ да прылады на агульнадаступным серверы, увядзіце \"@public\", ключ для публічнага сервера не патрабуецца."), + ("privacy_mode_impl_mag_tip", "Рэжым 1"), + ("privacy_mode_impl_virtual_display_tip", "Рэжым 2"), + ("Enter privacy mode", "Уключыць рэжым канфідэнцыйнасці"), + ("Exit privacy mode", "Адключыць рэжым канфідэнцыйнасці"), + ("idd_not_support_under_win10_2004_tip", "Драйвер непрамога адлюстравання не падтрымліваецца. Патрабуецца Windows 10 версіі 2004 або навейшая."), + ("input_source_1_tip", "Крыніца ўводу 1"), + ("input_source_2_tip", "Крыніца ўводу 2"), + ("Swap control-command key", "Памяняць месцамі значэнні кнопак Ctrl і Command"), + ("swap-left-right-mouse", "Памяняць месцамі значэнні левай і правай кнопак мышы"), + ("2FA code", "Код двухфактарнай праверкі сапраўднасці"), + ("More", "Яшчэ"), + ("enable-2fa-title", "Выкарыстоўваць двухфактарную праверку сапраўднасці"), + ("enable-2fa-desc", "Наладзьце праграму праверкі сапраўднасці. Выкарыстоўвайце, напрыклад, Authy, Microsoft або Google Authenticator на тэлефоне ці камп’ютары.\n\nАдскануйце QR-код з дапамогай праграмы праверкі сапраўднасці і ўвядзіце код, які пакажа гэта праграма, каб уключыць двухфактарную праверку сапраўднасці."), + ("wrong-2fa-code", "Немагчыма пацвердзіць код. Праверце код і налады мясцовага часу."), + ("enter-2fa-title", "Двухфактарная праверка сапраўднасці"), + ("Email verification code must be 6 characters.", "Код пацвярджэння па электроннай пошце павінен складацца з 6 сімвалаў."), + ("2FA code must be 6 digits.", "Код двухфактарнай праверкі сапраўднасці павінен складацца з 6 лічбаў."), + ("Multiple Windows sessions found", "Знойдзена некалькі сеансаў Windows"), + ("Please select the session you want to connect to", "Выберыце сеанс, да якога вы хочаце падключыцца"), + ("powered_by_me", "Заснавана на RustDesk"), + ("outgoing_only_desk_tip", "Гэта спецыялізаваная версія.\nВы можаце падключацца да іншых прылад, але іншыя прылады не могуць падключацца да вашай."), + ("preset_password_warning", "Гэта спецыялізаваная версія з прадвызначаным паролем. Любы, хто ведае гэты пароль, можа атрымаць поўны кантроль над вашай прыладай. Калі гэта для вас нечакана, адразу выдаліце гэта праграмнае забеспячэнне."), + ("Security Alert", "Папярэджанне аб бяспецы"), + ("My address book", "Мая адрасная кніга"), + ("Personal", "Асабістая"), + ("Owner", "Уладальнік"), + ("Set shared password", "Задаць агульны пароль"), + ("Exist in", "Існуе ў"), + ("Read-only", "Толькі для чытання"), + ("Read/Write", "Чытанне і запіс"), + ("Full Control", "Поўны доступ"), + ("share_warning_tip", "Палі вышэй з'яўляюцца агульнымі і бачнымі іншым."), + ("Everyone", "Усе"), + ("ab_web_console_tip", "Больш у вэб-кансолі"), + ("allow-only-conn-window-open-tip", "Дазволіць падключэнне толькі пры адкрытым акне RustDesk"), + ("no_need_privacy_mode_no_physical_displays_tip", "Фізічныя дысплэі адсутнічаюць, няма патрэбы выкарыстоўваць рэжым канфідэнцыйнасці."), + ("Follow remote cursor", "Прытрымлівацца аддаленага курсора"), + ("Follow remote window focus", "Прытрымлівацца фокуса аддаленага акна"), + ("default_proxy_tip", "Стандартныя пратакол і порт: Socks5 і 1080"), + ("no_audio_input_device_tip", "Прылада ўваходнага аудыё не знойдзена."), + ("Incoming", "Уваходныя"), + ("Outgoing", "Выходныя"), + ("Clear Wayland screen selection", "Скасаваць выбар экрана Wayland"), + ("clear_Wayland_screen_selection_tip", "Пасля скасавання можна зноў выбраць экран для дэманстрацыі."), + ("confirm_clear_Wayland_screen_selection_tip", "Скасаваць выбар экрана Wayland?"), + ("android_new_voice_call_tip", "Прыйшоў новы запыт на галасавы выклік. Калі вы прымеце яго, гук пераключыцца на галасавае падключэнне."), + ("texture_render_tip", "Выкарыстоўваць візуалізацыю тэкстур, каб зрабіць відарысы больш плаўнымі."), + ("Use texture rendering", "Візуалізацыя тэкстур"), + ("Floating window", "Нефіксаванае акно"), + ("floating_window_tip", "Дапамагае падтрымліваць фонавую службу RustDesk"), + ("Keep screen on", "Трымаць экран уключаным"), + ("Never", "Ніколі"), + ("During controlled", "Пры кіраванні"), + ("During service is on", "Пры запушчанай службе"), + ("Capture screen using DirectX", "Захоп экрана з выкарыстаннем DirectX"), + ("Back", "Назад"), + ("Apps", "Праграмы"), + ("Volume up", "Гучнасць+"), + ("Volume down", "Гучнасць-"), + ("Power", "Сілкаванне"), + ("Telegram bot", "Telegram-бот"), + ("enable-bot-tip", "Калі ўключана, можна атрымліваць код двухфактарнай праверкі сапраўднасці ад бота. Таксама ён можа выконваць функцыю апавяшчэння пра падключэнне."), + ("enable-bot-desc", "1) Адкрыйце чат з @BotFather.\n2) Адпраўце каманду \"/newbot\". Пасля выканання гэтага кроку вы атрымаеце токен.\n3) Пачніце чат з вашым толькі што створаным ботам. Адпраўце паведамленне, якое пачынаецца з касой рысы (\"/\"), напрыклад, \"/hello\", каб яго актываваць.\n"), + ("cancel-2fa-confirm-tip", "Адключыць двухфактарную праверку сапраўднасці?"), + ("cancel-bot-confirm-tip", "Адключыць Telegram-бота"), + ("About RustDesk", "Пра RustDesk"), + ("Send clipboard keystrokes", "Адпраўляць націсканні клавіш у буфер абмену"), + ("network_error_tip", "Праверце падключэнне да сеткі, пасля чаго націсніце \"Паўтарыць спробу\"."), + ("Unlock with PIN", "Разблакіраваць PIN-кодам"), + ("Requires at least {} characters", "Патрабуецца больш сімвалаў (ад {})"), + ("Wrong PIN", "Памылковы PIN-код"), + ("Set PIN", "Задаць PIN-код"), + ("Enable trusted devices", "Уключэнне давераных прылад"), + ("Manage trusted devices", "Кіраванне даверанымі прыладамі"), + ("Platform", "Платформа"), + ("Days remaining", "Засталося дзён"), + ("enable-trusted-devices-tip", "Дазволіць давераным прыладам прапускаць праверку сапраўднасці 2FA"), + ("Parent directory", "Бацькоўскі каталог"), + ("Resume", "Працягнуць"), + ("Invalid file name", "Памылковая назва файла"), + ("one-way-file-transfer-tip", "На баку абанента ўключана аднабаковая перадача файлаў."), + ("Authentication Required", "Патрабуецца праверка сапраўднасці"), + ("Authenticate", "Прайсці праверку"), + ("web_id_input_tip", "Можна ўвесці ID на тым самым серверы, прамы доступ па IP у вэб-кліенце не падтрымліваецца.\nКалі вы хочаце атрымаць доступ да прылады на іншым серверы, дадайце адрас сервера (@<адрас_сервера>?key=<ключ>), напрыклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nКалі вы хочаце атрымаць доступ да прылады на публічным серверы, увядзіце \"@public\", для публічнага сервера ключ не патрэбны."), + ("Download", "Спампаваць"), + ("Upload folder", "Запампаваць папку"), + ("Upload files", "Запампаваць файлы"), + ("Clipboard is synchronized", "Буфер абмену сінхранізаваны"), + ("Update client clipboard", "Абнавіць буфер абмену кліента"), + ("Untagged", "Без цэтліка"), + ("new-version-of-{}-tip", "Даступна новая версія {}"), + ("Accessible devices", "Даступныя прылады"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Абнавіце кліент RustDesk да версіі {} або навейшай на баку абанента!"), + ("d3d_render_tip", "Пры ўключэнні візуалізацыі D3D на некаторых прыладах аддалены экран можа быць чорным."), + ("Use D3D rendering", "Выкарыстоўваць візуалізацыю D3D"), + ("Printer", "Прынтар"), + ("printer-os-requirement-tip", "Для работы функцыі выходнай сувязі з прынтарам патрабуецца Windows 10 або навейшай версіі."), + ("printer-requires-installed-{}-client-tip", "Каб выкарыстоўваць аддалены друк, {} павінен быць усталяваны на гэтай прыладзе."), + ("printer-{}-not-installed-tip", "Прынтар {} не ўсталяваны."), + ("printer-{}-ready-tip", "Прынтар {} усталяваны і гатовы да выкарыстання."), + ("Install {} Printer", "Усталюйце прынтар {}"), + ("Outgoing Print Jobs", "Выходныя заданні друку"), + ("Incoming Print Jobs", "Уваходныя заданні друку"), + ("Incoming Print Job", "Уваходнае заданне друку"), + ("use-the-default-printer-tip", "Выкарыстоўваць прынтар стандартна"), + ("use-the-selected-printer-tip", "Выкарыстоўваць выбраны прынтар"), + ("auto-print-tip", "Аўтаматычна выконваць друк на выбраным прынтары"), + ("print-incoming-job-confirm-tip", "З аддаленай прылады атрымана заданне на друк. Выканаць яго лакальна?"), + ("remote-printing-disallowed-tile-tip", "Аддалены друк забаронены"), + ("remote-printing-disallowed-text-tip", "Налады дазволаў на баку абанента забараняюць аддалены друк."), + ("save-settings-tip", "Захаваць налады"), + ("dont-show-again-tip", "Больш не паказваць"), + ("Take screenshot", "Зрабіць здымак экрана"), + ("Taking screenshot", "Робіцца здымак экрана"), + ("screenshot-merged-screen-not-supported-tip", "Аб’яднанне здымкаў экранаў з некалькіх дысплэяў у дадзены момант не падтрымліваецца. Пераключыцеся на адзін з дысплэяў і паўтарыце дзеянне."), + ("screenshot-action-tip", "Выберыце, што рабіць з атрыманым здымкам экрана."), + ("Save as", "Захаваць у файл"), + ("Copy to clipboard", "Скапіяваць у буфер абмену"), + ("Enable remote printer", "Выкарыстоўваць аддалены прынтар"), + ("Downloading {}", "Ідзе спампоўванне {}"), + ("{} Update", "Абнавіць {}"), + ("{}-to-update-tip", "{} закрыецца і ўсталюе новую версію."), + ("download-new-version-failed-tip", "Памылка спампоўвання. Можна паўтарыць спробу або націснуць кнопку \"Спампаваць\", каб спампаваць праграму з афіцыйнага сайта і абнавіць уручную."), + ("Auto update", "Аўтаматычнае абнаўленне"), + ("update-failed-check-msi-tip", "Немагчыма вызначыць метад усталявання. Націсніце кнопку \"Спампаваць\", каб спампаваць праграму з афіцыйнага сайта і абнавіце яго ўручную."), + ("websocket_tip", "WebSocket падтрымлівае толькі падключэнні да рэтранслятара."), + ("Use WebSocket", "Выкарыстоўваць WebSocket"), + ("Trackpad speed", "Хуткасць трэкпада"), + ("Default trackpad speed", "Стандартная хуткасць трэкпада"), + ("Numeric one-time password", "Лічбавы аднаразовы пароль"), + ("Enable IPv6 P2P connection", "Выкарыстоўваць падключэнне IPv6 P2P"), + ("Enable UDP hole punching", "Выкарыстоўваць UDP hole punching"), + ("View camera", "Рэжым камеры"), + ("Enable camera", "Уключыць камеру"), + ("No cameras", "Камера адсутнічае"), + ("view_camera_unsupported_tip", "Аддаленая прылада не падтрымлівае рэжыму камеры."), + ("Terminal", "Тэрмінал"), + ("Enable terminal", "Уключыць тэрмінал"), + ("New tab", "Новая ўкладка"), + ("Keep terminal sessions on disconnect", "Захоўваць сеансы тэрмінала пры адключэнні"), + ("Terminal (Run as administrator)", "Тэрмінал (адміністратар)"), + ("terminal-admin-login-tip", "Увядзіце імя карыстальніка і пароль адміністратара абанента."), + ("Failed to get user token.", "Не ўдалося атрымаць токен карыстальніка."), + ("Incorrect username or password.", "Памылковае імя карыстальніка або пароль."), + ("The user is not an administrator.", "Карыстальнік не з’яўляецца адміністратарам."), + ("Failed to check if the user is an administrator.", "Немагчыма праверыць, ці з’яўляецца карыстальнік адміністратарам."), + ("Supported only in the installed version.", "Падтрымліваецца толькі ва ўсталёвачнай версіі."), + ("elevation_username_tip", "Увядзіце карыстальніка або дамен\\карыстальніка"), + ("Preparing for installation ...", "Ідзе падрыхтоўка да ўсталявання..."), + ("Show my cursor", "Паказваць мой курсор"), + ("Scale custom", "Карыстальніцкае маштабаванне"), + ("Custom scale slider", "Карыстальніцкі паўзунок маштабавання"), + ("Decrease", "Паменшыць"), + ("Increase", "Павялічыць"), + ("Show virtual mouse", "Паказаць віртуальную мыш"), + ("Virtual mouse size", "Памер віртуальнай мышы"), + ("Small", "Маленькі"), + ("Large", "Вялікі"), + ("Show virtual joystick", "Паказваць віртуальны джойстык"), + ("Edit note", "Змяніць нататку"), + ("Alias", "Псеўданім"), + ("ScrollEdge", "Прагортваць з краю"), + ("Allow insecure TLS fallback", "Дазволіць небяспечныя TLS"), + ("allow-insecure-tls-fallback-tip", "Стандартна RustDesk правярае сертыфікат сервера на наяўнасць пратаколаў, якія выкарыстоўваюць TLS.\nКалі гэта функцыя ўключана, RustDesk прапусціць дадзены этап і працягне працу ў выпадку няўдалай праверкі."), + ("Disable UDP", "Выключыць UDP"), + ("disable-udp-tip", "Вызначае, ці варта выкарыстоўваць толькі TCP.\nКалі ўключана, RustDesk не будзе выкарыстоўваць UDP 21116, замест чаго будзе выкарыстоўвацца TCP 21116."), + ("server-oss-not-support-tip", "ЗАЎВАГА! у OSS-серверы RustDesk гэта функцыя адсутнічае."), + ("input note here", "увядзіце нататку"), + ("note-at-conn-end-tip", "Запытваць нататку ў канцы сеанса"), + ("Show terminal extra keys", "Паказваць дадатковыя кнопкі тэрмінала"), + ("Relative mouse mode", "Рэжым адноснага перамяшчэння мышы"), + ("rel-mouse-not-supported-peer-tip", "Рэжым адноснага перамяшчэння мышы не падтрымліваецца падключаным абанентам."), + ("rel-mouse-not-ready-tip", "Рэжым адноснага перамяшчэння мышы яшчэ не гатовы. Паспрабуйце зноў."), + ("rel-mouse-lock-failed-tip", "Немагчыма заблакіраваць курсор. Рэжым адноснага перамяшчэння мышы адключаны."), + ("rel-mouse-exit-{}-tip", "Націсніце {}, каб выйсці."), + ("rel-mouse-permission-lost-tip", "Дазвол на выкарыстанне клавіятуры скасаваны. Рэжым адноснага перамяшчэння мышы адключаны."), + ("Changelog", "Журнал змяненняў"), + ("keep-awake-during-outgoing-sessions-label", "Не адключаць экрана ў часе выходных сеансаў"), + ("keep-awake-during-incoming-sessions-label", "Не адключаць экрана ў часе ўваходных сеансаў"), + ("Continue with {}", "Працягнуць з {}"), + ("Display Name", "Імя для адлюстравання"), + ("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."), + ("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/bg.rs b/vendor/rustdesk/src/lang/bg.rs new file mode 100644 index 0000000..17a89ce --- /dev/null +++ b/vendor/rustdesk/src/lang/bg.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Състояне"), + ("Your Desktop", "Вашата работна среда"), + ("desk_tip", "Вашата работна среда не може да бъде достъпена с този потребителски код и парола."), + ("Password", "Парола"), + ("Ready", "Готово"), + ("Established", "Установен"), + ("connecting_status", "Свързване с RustDesk мрежата..."), + ("Enable service", "Разреши услуга"), + ("Start service", "Стартирай услуга"), + ("Service is running", "Услугата работи"), + ("Service is not running", "Услугата не работи"), + ("not_ready_status", "Не е в готовност. Моля проверете мрежова връзка"), + ("Control Remote Desktop", "Отдалечено управление на работна среда"), + ("Transfer file", "Прехвърляне на файл"), + ("Connect", "Свързване"), + ("Recent sessions", "Последни сесии"), + ("Address book", "Адресник"), + ("Confirmation", "Потвърждение"), + ("TCP tunneling", "TCP тунел"), + ("Remove", "Премахни"), + ("Refresh random password", "Нова случйна парола"), + ("Set your own password", "Задайте собствена парола"), + ("Enable keyboard/mouse", "Позволяване на клавиатура/мишка"), + ("Enable clipboard", "Позволяване достъп до клипборда"), + ("Enable file transfer", "Позволяване на прехвърляне на файлове"), + ("Enable TCP tunneling", "Позволяване на TCP тунели"), + ("IP Whitelisting", "Позволени IP"), + ("ID/Relay Server", "ID/Препредаващ сървър"), + ("Import server config", "Възстановяване на сървърните настройки"), + ("Export Server Config", "Съхраняване на сървърни настройки"), + ("Import server configuration successfully", "Успешно възстановяване на сървърни настройки"), + ("Export server configuration successfully", "Успешно съхраняване на сървърни настройки"), + ("Invalid server configuration", "Невалидни сървърни настройки"), + ("Clipboard is empty", "Клипбордът е празен"), + ("Stop service", "Спиране на услуга"), + ("Change ID", "Промяна идентификатор (ID)"), + ("Your new ID", "Вашият нов идентификатор (ID)"), + ("length %min% to %max%", "дължина %min% до %max%"), + ("starts with a letter", "започва с буква"), + ("allowed characters", "разрешени знаци"), + ("id_change_tip", "Само a-z, A-Z, 0-9, - (тире) и _ (долна черта) са сред позволени. Първата буква следва да е a-z, A-Z. С дължина мержу 6 и 16."), + ("Website", "Уебсайт"), + ("About", "За програмата"), + ("Slogan_tip", "Направено от сърце в този хаотичен свят!"), + ("Privacy Statement", "Декларация за поверителност"), + ("Mute", "Без звук"), + ("Build Date", "Дата на създаване"), + ("Version", "Версия"), + ("Home", "Начало"), + ("Audio Input", "Аудио вход"), + ("Enhancements", "Подобрения"), + ("Hardware Codec", "Хардуерен кодек"), + ("Adaptive bitrate", "Адаптивна скорост на предаване"), + ("ID Server", "ID сървър"), + ("Relay Server", "Препращащ сървър"), + ("API Server", "API сървър"), + ("invalid_http", "трябва да започва с http:// или https://"), + ("Invalid IP", "Невалиден IP"), + ("Invalid format", "Невалиден формат"), + ("server_not_support", "Все още не се поддържа от сървъра"), + ("Not available", "Не е наличен"), + ("Too frequent", "Твърде често"), + ("Cancel", "Откажи"), + ("Skip", "Пропусни"), + ("Close", "Затвори"), + ("Retry", "Повтори"), + ("OK", "Добре"), + ("Password Required", "Изисква се парола"), + ("Please enter your password", "Моля въведете парола"), + ("Remember password", "Запомни паролата"), + ("Wrong Password", "Грешна парола"), + ("Do you want to enter again?", "Искате ли да въведете отново?"), + ("Connection Error", "Грешка при свързване"), + ("Error", "Грешка"), + ("Reset by the peer", "Нулирано от партньора"), + ("Connecting...", "Свързване..."), + ("Connection in progress. Please wait.", "Свързването се осъществява. Моля Изчакайте."), + ("Please try 1 minute later", "Моля, опитайте 1 минута по-късно"), + ("Login Error", "Грешка при вписване"), + ("Successful", "Успешно"), + ("Connected, waiting for image...", "Свързано, чака се изображение..."), + ("Name", "Име"), + ("Type", "Тип"), + ("Modified", "Променен"), + ("Size", "Размер"), + ("Show Hidden Files", "Показване на скрити файлове"), + ("Receive", "Получаване"), + ("Send", "Изпращане"), + ("Refresh File", "Опресняване на файла"), + ("Local", "Локално"), + ("Remote", "Отдалечено"), + ("Remote Computer", "Отдалечен компютър"), + ("Local Computer", "Локален компютър"), + ("Confirm Delete", "Потвърдете изтриването"), + ("Delete", "Изтрий"), + ("Properties", "Свойства"), + ("Multi Select", "Множествен избор"), + ("Select All", "Избери всички"), + ("Unselect All", "Избери никой"), + ("Empty Directory", "Празна папка"), + ("Not an empty directory", "Не е празна папка"), + ("Are you sure you want to delete this file?", "Сигурни ли сте, че искате да изтриете този файл?"), + ("Are you sure you want to delete this empty directory?", "Сигурни ли сте, че искате да изтриете тази празна папка?"), + ("Are you sure you want to delete the file of this directory?", "Сигурни ли сте, че искате да изтриете файла от тази папка?"), + ("Do this for all conflicts", "Същото за всички конфликти"), + ("This is irreversible!", "Това е необратимо!"), + ("Deleting", "Изтриване"), + ("files", "файлове"), + ("Waiting", "Изчакване"), + ("Finished", "Завършено"), + ("Speed", "Скорост"), + ("Custom Image Quality", "Качество на изображението по свой избор"), + ("Privacy mode", "Режим на поверителност"), + ("Block user input", "Забрана за потребителско въвеждане"), + ("Unblock user input", "Разрешаване на потребителско въвеждане"), + ("Adjust Window", "Нагласи прозореца"), + ("Original", "Оригинално"), + ("Shrink", "Свиване"), + ("Stretch", "Разтягане"), + ("Scrollbar", "Плъзгач"), + ("ScrollAuto", "Автоматично скролиране"), + ("Good image quality", "Добро качество на изображението"), + ("Balanced", "Уравновесен"), + ("Optimize reaction time", "Оптимизирай времето за реакция"), + ("Custom", "По избор"), + ("Show remote cursor", "Показвай отдалечения курсор"), + ("Show quality monitor", "Показвай прозорец за качество"), + ("Disable clipboard", "Забрана на клипборда"), + ("Lock after session end", "Заключване след край на ползване"), + ("Insert Ctrl + Alt + Del", "Въведи Ctrl + Alt + Del"), + ("Insert Lock", "Въведи заключване"), + ("Refresh", "Обновяване"), + ("ID does not exist", "Несъществуващ идентификатор (ID)"), + ("Failed to connect to rendezvous server", "Неуспешно свързване към сървъра за среща (rendezvous)"), + ("Please try later", "Моля опитайте по-късно"), + ("Remote desktop is offline", "Отдалечената работна среда не е налична"), + ("Key mismatch", "Несъответствие на ключове"), + ("Timeout", "Таймаут"), + ("Failed to connect to relay server", "Неуспешно свързване към препредаващ сървър"), + ("Failed to connect via rendezvous server", "Неуспешно свързване към сървър за срещи (rendezvous)"), + ("Failed to connect via relay server", "Неуспешно свързване чрез препредаващ сървър"), + ("Failed to make direct connection to remote desktop", "Неуспешно установяване на пряка връзка с отдалечена работна среда"), + ("Set Password", "Задаване на парола"), + ("OS Password", "Парола на Операционната система"), + ("install_tip", "Поради UAC, RustDesk в някои случай не може да работи правилно за отдалечена достъп. За да заобиколите UAC, моля, натиснете копчето по-долу, за да поставите RustDesk като системна услуга."), + ("Click to upgrade", "Натиснете, за да надстроите"), + ("Configure", "Настройване"), + ("config_acc", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Достъпност\"."), + ("config_screen", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Запис на екрана\"."), + ("Installing ...", "Инсталиране..."), + ("Install", "Инсталирай"), + ("Installation", "Инсталация"), + ("Installation Path", "Път за инсталация"), + ("Create start menu shortcuts", "Създай връзка от меню 'Старт'."), + ("Create desktop icon", "Създай иконка на работния плот"), + ("agreement_tip", "Започвайки инсталацията, вие приемате лицензионното споразумение."), + ("Accept and Install", "Приемам и инсталирам"), + ("End-user license agreement", "Споразумение с потребителя"), + ("Generating ...", "Създаване..."), + ("Your installation is lower version.", "Вашата инсталация е по-ниска версия."), + ("not_close_tcp_tip", "Не затваряйте този прозорец, докато използвате тунела"), + ("Listening ...", "Слушане..."), + ("Remote Host", "Отдалечен сървър"), + ("Remote Port", "Отдалечен порт"), + ("Action", "Действие"), + ("Add", "Добави"), + ("Local Port", "Локален порт"), + ("Local Address", "Локален адрес"), + ("Change Local Port", "Промяна на локалният порт"), + ("setup_server_tip", "За по-бърза връзка, моля направете свой собствен сървър"), + ("Too short, at least 6 characters.", "Прекалено кратко, поне 6 знака"), + ("The confirmation is not identical.", "Потвърждението не съвпада"), + ("Permissions", "Разрешения"), + ("Accept", "Приеми"), + ("Dismiss", "Отхвърли"), + ("Disconnect", "Прекъсни"), + ("Enable file copy and paste", "Разрешаване копирането и поставяне на файлове"), + ("Connected", "Свързан"), + ("Direct and encrypted connection", "Пряка защитена връзка"), + ("Relayed and encrypted connection", "Препредадена защитена връзка"), + ("Direct and unencrypted connection", "Пряка незащитена връзка"), + ("Relayed and unencrypted connection", "Препредадена незащитена връзка"), + ("Enter Remote ID", "Въведете отдалеченото ID"), + ("Enter your password", "Въведете парола"), + ("Logging in...", "Вписване..."), + ("Enable RDP session sharing", "Позволяване споделянето на RDP сесия"), + ("Auto Login", "Автоматично вписване (Валидно само ако зададете \"Заключване след края на сесията\")"), + ("Enable direct IP access", "Разрешаване пряк IP достъп"), + ("Rename", "Преименуване"), + ("Space", "Пространство"), + ("Create desktop shortcut", "Създайте връзка на работния плот"), + ("Change Path", "Промяна на пътя"), + ("Create Folder", "Създай папка"), + ("Please enter the folder name", "Моля, въведете име на папката"), + ("Fix it", "Оправи го"), + ("Warning", "Внимание"), + ("Login screen using Wayland is not supported", "Екранът за влизане чрез Wayland не се поддържа"), + ("Reboot required", "Нужно е презареждане на ОС"), + ("Unsupported display server", "Неподдържан екранен сървър"), + ("x11 expected", "Очаква се x11"), + ("Port", "Порт"), + ("Settings", "Настройки"), + ("Username", "Потребителско име"), + ("Invalid port", "Невалиден порт"), + ("Closed manually by the peer", "Затворено ръчно от другата страна"), + ("Enable remote configuration modification", "Разрешаване на отдалечена промяна на конфигурацията"), + ("Run without install", "Стартирайте без инсталиране"), + ("Connect via relay", "Свързване чрез препращане"), + ("Always connect via relay", "Винаги чрез препращане"), + ("whitelist_tip", "Само IP адресите от белия списък имат достъп до мен"), + ("Login", "Вписване"), + ("Verify", "Потвърди"), + ("Remember me", "Запомни ме"), + ("Trust this device", "Доверяване на това устройство"), + ("Verification code", "Код за потвърждение"), + ("verification_tip", "На посочения имейл е изпратен код за потвърждение. Моля въведете го, за да продължите с вписването."), + ("Logout", "Отписване (Изход)"), + ("Tags", "Етикети"), + ("Search ID", "Търси ID"), + ("whitelist_sep", "Разделени със запетая, точка и запетая, празни символи или нов ред"), + ("Add ID", "Добави ID"), + ("Add Tag", "Добави етикет"), + ("Unselect all tags", "Премахнете избора на всички етикети (tags)"), + ("Network error", "Мрежова грешка"), + ("Username missed", "Липсва потребителско име"), + ("Password missed", "Липсва парола"), + ("Wrong credentials", "Грешни пълномощия"), + ("The verification code is incorrect or has expired", "Кодът за потвърждение е неправилен или с изтекла давност."), + ("Edit Tag", "Редактирай етикет"), + ("Forget Password", "Забравена парола"), + ("Favorites", "Любими"), + ("Add to Favorites", "Добави към любими"), + ("Remove from Favorites", "Премахване от любими"), + ("Empty", "Празно"), + ("Invalid folder name", "Невалидно име на папка"), + ("Socks5 Proxy", "Socks5 Прокси"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) прокси"), + ("Discovered", "Открит"), + ("install_daemon_tip", "За зареждане при стартиране на ОС трябва да инсталирате RustDesk като системна услуга."), + ("Remote ID", "Отдалечено ID"), + ("Paste", "Постави"), + ("Paste here?", "Постави тук?"), + ("Are you sure to close the connection?", "Сигурни ли сте, че искате да затворите връзката?"), + ("Download new version", "Изтегляне на нова версия"), + ("Touch mode", "Режим сензорен (touch)"), + ("Mouse mode", "Режим мишка"), + ("One-Finger Tap", "Допир с един пръст"), + ("Left Mouse", "Ляв бутон на мишката"), + ("One-Long Tap", "Дълъг допир"), + ("Two-Finger Tap", "Допир с два пръста"), + ("Right Mouse", "Десен бутон на мишката"), + ("One-Finger Move", "Преместване с един пръст"), + ("Double Tap & Move", "Двоен допир и преместване"), + ("Mouse Drag", "Провличане с мишката"), + ("Three-Finger vertically", "Три пръста вертикално"), + ("Mouse Wheel", "Колело на мишката"), + ("Two-Finger Move", "Движение с два пръста"), + ("Canvas Move", "Преместване на платното"), + ("Pinch to Zoom", "Щипнете, за да увеличите"), + ("Canvas Zoom", "Увеличение на платното"), + ("Reset canvas", "Нулиране на платното"), + ("No permission of file transfer", "Няма разрешение за прехвърляне на файлове"), + ("Note", "Бележка"), + ("Connection", "Връзка"), + ("Share screen", "Сподели екран"), + ("Chat", "Чат"), + ("Total", "Общо"), + ("items", "неща"), + ("Selected", "Избрано"), + ("Screen Capture", "Заснемане на екрана"), + ("Input Control", "Управление на въвеждане"), + ("Audio Capture", "Аудиозапис"), + ("Do you accept?", "Приемате ли?"), + ("Open System Setting", "Отворете системните настройки"), + ("How to get Android input permission?", "Как да получим право за въвеждане при Андроид?"), + ("android_input_permission_tip1", "За да може отдалечено устройство да управлява вашето Android устройство чрез мишка или допир, трябва да разрешите на RustDesk да използва услугата \"Достъпност\"."), + ("android_input_permission_tip2", "Моля, отидете на следващата страница със системни настройки, намерете и въведете [Installed Services], включете услугата [RustDesk Input]."), + ("android_new_connection_tip", "Получена е нова заявка за отдалечено управление на вашето текущо устройство."), + ("android_service_will_start_tip", "Включването на \"Заснемане на екрана\" автоматично ще стартира услугата, позволявайки на други устройства да поискат връзка с вашето устройство."), + ("android_stop_service_tip", "Затварянето на услугата автоматично ще затвори всички установени връзки."), + ("android_version_audio_tip", "Текущата версия на Android не поддържа аудиозапис. Моля, актуализирайте устройството с Android 10 или по-нов."), + ("android_start_service_tip", "Докоснете [Start service] или позволете [Screen Capture], за да започне услугата по споделяне на екрана."), + ("android_permission_may_not_change_tip", "Разрешенията за установени връзки може да не се променят незабавно, а ще изискват да се свържете отново."), + ("Account", "Профил"), + ("Overwrite", "Презаписване"), + ("This file exists, skip or overwrite this file?", "Този файл съществува вече. Пропускане или презаписване?"), + ("Quit", "Изход"), + ("Help", "Помощ"), + ("Failed", "Неуспешно"), + ("Succeeded", "Успешно"), + ("Someone turns on privacy mode, exit", "Някой включва режим на поверителност, изход"), + ("Unsupported", "Неподдържан"), + ("Peer denied", "Отказ от другата страна"), + ("Please install plugins", "Моля поставете плъгини"), + ("Peer exit", "Изход от другата страна"), + ("Failed to turn off", "Неуспешен опит за изключване"), + ("Turned off", "Изкключен"), + ("Language", "Език"), + ("Keep RustDesk background service", "Запази RustDesk фоновата услуга"), + ("Ignore Battery Optimizations", "Игнорирай оптимизациите на батерията"), + ("android_open_battery_optimizations_tip", "Ако искате да деактивирате тази функция, моля, отидете на следващата страница с настройки на приложението RustDesk, намерете и въведете [Battery], премахнете отметката от [Unrestricted]"), + ("Start on boot", "Стартирайте при зареждане"), + ("Start the screen sharing service on boot, requires special permissions", ""), + ("Connection not allowed", "Връзката непозволена"), + ("Legacy mode", "По остарял начин"), + ("Map mode", "По начин със съответствие (map)"), + ("Translate mode", "По начин с превод"), + ("Use permanent password", "Използване на постоянна парола"), + ("Use both passwords", "Използване и на двете пароли"), + ("Set permanent password", "Задаване постоянна парола"), + ("Enable remote restart", "Разрешаване на отдалечен рестарт"), + ("Restart remote device", "Рестартиране на отдалечено устройство"), + ("Are you sure you want to restart", "Сигурни ли сте, че искате да рестартирате"), + ("Restarting remote device", "Рестартиране на отдалечено устройство"), + ("remote_restarting_tip", "Отдалеченото устройство се рестартира, моля, затворете това съобщение и се свържете отново с постоянна парола след известно време"), + ("Copied", "Преписано"), + ("Exit Fullscreen", "Изход от цял екран"), + ("Fullscreen", "Цял екран"), + ("Mobile Actions", "Мобилни действия"), + ("Select Monitor", "Изберете монитор"), + ("Control Actions", "Контролни действия"), + ("Display Settings", "Настройки на дисплея"), + ("Ratio", "Съотношение"), + ("Image Quality", "Качество на изображението"), + ("Scroll Style", "Стил на превъртане"), + ("Show Toolbar", "Показване на лентата с инструменти"), + ("Hide Toolbar", "Скриване на лентата с инструменти"), + ("Direct Connection", "Директна връзка"), + ("Relay Connection", "Релейна връзка"), + ("Secure Connection", "Сигурна връзка"), + ("Insecure Connection", "Несигурна връзка"), + ("Scale original", "Оригинален мащаб"), + ("Scale adaptive", "Приспособимо мащабиране"), + ("General", "Основен"), + ("Security", "Сигурност"), + ("Theme", "Тема"), + ("Dark Theme", "Тъмна тема"), + ("Light Theme", "Светла тема"), + ("Dark", "Тъмна"), + ("Light", "Светла"), + ("Follow System", "Следвай система"), + ("Enable hardware codec", "Позволяване на хардуерен кодек"), + ("Unlock Security Settings", "Отключи настройките за сигурност"), + ("Enable audio", "Позволи звук"), + ("Unlock Network Settings", "Отключи мрежовите настройки"), + ("Server", "Сървър"), + ("Direct IP Access", "Пряк IP достъп"), + ("Proxy", "Посредник (Proxy)"), + ("Apply", "Прилагане"), + ("Disconnect all devices?", "Разкачване на всички устройства"), + ("Clear", "Изчистване"), + ("Audio Input Device", "Аудио входно устройство"), + ("Use IP Whitelisting", "Използване бял списък с IP адреси"), + ("Network", "Мрежа"), + ("Pin Toolbar", "Закачане лента с инструменти"), + ("Unpin Toolbar", "Откачюане лента с инструменти"), + ("Recording", "Записване"), + ("Directory", "Директория"), + ("Automatically record incoming sessions", "Автоматичен запис на входящи сесии"), + ("Automatically record outgoing sessions", "Автоматичен запис на изходящи сесии"), + ("Change", "Промени"), + ("Start session recording", "Старт на запис на сесията"), + ("Stop session recording", "Стоип на запис на сесията"), + ("Enable recording session", "Позволяване на записване на сесията"), + ("Enable LAN discovery", "Позволяване откриване във вътрешна мрежа"), + ("Deny LAN discovery", "Забрана за откриване във вътрешна мрежа"), + ("Write a message", "Напишете съобщение"), + ("Prompt", "Подкана"), + ("Please wait for confirmation of UAC...", "Моля изчакайте за потвърждение от UAC..."), + ("elevated_foreground_window_tip", "Текущият прозорец на отдалечения работен плот изисква по-високи привилегии за работа, така че временно не може да използва мишката и клавиатурата. Можете да поискате от отдалечения потребител да минимизира текущия прозорец или да щракнете върху бутона за повдигане в прозореца за управление на връзката. За да избегнете този проблем, се препоръчва да инсталирате софтуера на отдалеченото устройство."), + ("Disconnected", "Прекъсната връзка"), + ("Other", "Други"), + ("Confirm before closing multiple tabs", "Потвърждение преди затваряне на няколко раздела"), + ("Keyboard Settings", "Настройки на клавиатурата"), + ("Full Access", "Пълен достъп"), + ("Screen Share", "Споделяне на екрана"), + ("ubuntu-21-04-required", "Wayland изисква Ubuntu 21.04 или по-нов"), + ("wayland-requires-higher-linux-version", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Препратка"), + ("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."), + ("Show RustDesk", "Покажи RustDesk"), + ("This PC", "Този компютър"), + ("or", "или"), + ("Elevate", "Повишаване"), + ("Zoom cursor", "Уголемяване курсор"), + ("Accept sessions via password", "Приемане сесии чрез парола"), + ("Accept sessions via click", "Приемане сесии чрез клик"), + ("Accept sessions via both", "Приемане сесии и по двата начина"), + ("Please wait for the remote side to accept your session request...", "Моля, изчакайте докато другата страна приеме вашата заявката за сесия..."), + ("One-time Password", "Еднократна парола"), + ("Use one-time password", "Ползване на еднократна парола"), + ("One-time password length", "Дължина на еднократна парола"), + ("Request access to your device", "Искане за достъп до ваше устройство"), + ("Hide connection management window", "Скриване на прозореца за управление на връзка"), + ("hide_cm_tip", "Разрешаване скриване само ако се приемат сесии чрез постоянна парола"), + ("wayland_experiment_tip", "Поддръжката на Wayland е в експериментален стадий, моля, използвайте X11, ако се нуждаете от безконтролен достъп.."), + ("Right click to select tabs", "Десен бутон за избор на раздел"), + ("Skipped", "Пропуснато"), + ("Add to address book", "Добавяне към познати адреси"), + ("Group", "Група"), + ("Search", "Търсене"), + ("Closed manually by web console", "Затворен ръчно от уеб конзола"), + ("Local keyboard type", "Тип на локалната клавиатура"), + ("Select local keyboard type", "Избор на тип на локалната клавиатура"), + ("software_render_tip", "Ако използвате графична карта Nvidia под Linux и отдалеченият прозорец се затваря веднага след свързване, превключването към драйвера Nouveau с отворен код и изборът да използвате софтуерно изобразяване може да помогне. Изисква се рестартиране на софтуера."), + ("Always use software rendering", "Винаги ползвай софтуерно изграждане на картината"), + ("config_input", "За да управлявате отдалечена среда с клавиатура, трябва да предоставите на RustDesk право за \"Input Monitoring\"."), + ("config_microphone", "За да говорите отдалечено, трябва да предоставите на RustDesk право за \"Запис на звук\"."), + ("request_elevation_tip", "Можете също така да поискате разширени права, ако има някой от отдалечената страна."), + ("Wait", "Изчакване"), + ("Elevation Error", "Грешка при повишаване на права"), + ("Ask the remote user for authentication", "Попитайте отдалечения потребител за удостоверяване"), + ("Choose this if the remote account is administrator", "Изберете това, ако отдалеченият потребител е администратор."), + ("Transmit the username and password of administrator", "Предаване на потребителското име и паролата на администратор"), + ("still_click_uac_tip", "Все още изисква отдалеченият потребител да натисне върху OK в прозореца на UAC при стартиран RustDesk."), + ("Request Elevation", "Поискайте повишени права"), + ("wait_accept_uac_tip", "Моля, изчакайте отдалеченият потребител да приеме диалоговия прозорец на UAC."), + ("Elevate successfully", "Успешно получаване на повишени права"), + ("uppercase", "големи букви"), + ("lowercase", "малки букви"), + ("digit", "цифра"), + ("special character", "специален знак"), + ("length>=8", "дължина>=8"), + ("Weak", "Слаба"), + ("Medium", "Средна"), + ("Strong", "Силна"), + ("Switch Sides", "Размяна на страните"), + ("Please confirm if you want to share your desktop?", "Моля, потвърдете ако искате да споделите работното си пространство"), + ("Display", "Екран"), + ("Default View Style", "Стил на изглед по подразбиране"), + ("Default Scroll Style", "Стил на превъртане по подразбиране"), + ("Default Image Quality", "Качество на изображението по подразбиране"), + ("Default Codec", "Кодек по подразбиране"), + ("Bitrate", "Скорост на предаване на данни (bitrate)"), + ("FPS", "Кадри в секунда"), + ("Auto", "Автоматично"), + ("Other Default Options", "Други опции по подразбиране"), + ("Voice call", "Гласови обаждания"), + ("Text chat", "Текстов чат"), + ("Stop voice call", "Прекратяване на гласово обаждане"), + ("relay_hint_tip", "Може да не е възможно да се свържете директно; можете да опитате да се свържете чрез препращаш сървър. Освен това, ако искате да използвате препращаш сървър при първия си опит, добавете наставка \"/r\" към идентификатора или да изберете опцията \"Винаги свързване чрез препращаш сървър\" в картата на последните сесии, ако съществува."), + ("Reconnect", "Повторно свързане"), + ("Codec", "Кодек"), + ("Resolution", "Разделителна способност"), + ("No transfers in progress", "Няма текущи прехвърляния"), + ("Set one-time password length", "Задаване дължина на еднократна парола"), + ("RDP Settings", "RDP настройки"), + ("Sort by", "Сортирай по"), + ("New Connection", "Нова Връзка"), + ("Restore", "Възстанови"), + ("Minimize", "Минимизирай"), + ("Maximize", "На цял екран"), + ("Your Device", "Вашето устройство"), + ("empty_recent_tip", "Ами сега, няма скорошни сесии!\nВреме е да планирате нова."), + ("empty_favorite_tip", "Все още нямате любими връзки?\nНека намерим някой, с когото да се свържете, и да го добавим към вашите любими!"), + ("empty_lan_tip", "О, не, изглежда, че все още не сме открили връзки."), + ("empty_address_book_tip", "Изглежда, че в момента няма изброени връзки във вашата адресна книга."), + ("Empty Username", "Празно потребителско име"), + ("Empty Password", "Празна парола"), + ("Me", "Аз"), + ("identical_file_tip", "Файлът съвпада с този от другата страна."), + ("show_monitors_tip", "Показване на мониторите в лентата с инструменти"), + ("View Mode", "Режим на изглед"), + ("login_linux_tip", "Трябва да влезете в отдалечен Linux акаунт, за да активирате X сесия на работния плот"), + ("verify_rustdesk_password_tip", "Проверете RustDesk паролата"), + ("remember_account_tip", "Запомнете този акаунт"), + ("os_account_desk_tip", "Този акаунт се използва за влизане в отдалечената операционна система и позволява на десктоп сесия без моинитор"), + ("OS Account", "Профил в операционната система"), + ("another_user_login_title_tip", "Друг потребител вече е влязъл"), + ("another_user_login_text_tip", "Прекъснете връзката"), + ("xorg_not_found_title_tip", "Xorg не е намерен"), + ("xorg_not_found_text_tip", "Моля, инсталирайте Xorg"), + ("no_desktop_title_tip", "Няма наличен работен плот"), + ("no_desktop_text_tip", "Моля, инсталирайте работен плот GNOME"), + ("No need to elevate", "Няма нужда за повишаване на права"), + ("System Sound", "Системен звук"), + ("Default", "По подразбиране"), + ("New RDP", "Нов RDP"), + ("Fingerprint", "Пръстов отпечатък"), + ("Copy Fingerprint", "Копиране на пръстов отпечатък"), + ("no fingerprints", "Няма пръстови отпечатъци"), + ("Select a peer", "Избери отдалечена страна"), + ("Select peers", "Избери отдалечени страни"), + ("Plugins", "Плъгини"), + ("Uninstall", "Премахни"), + ("Update", "Обновяване"), + ("Enable", "Позволяване"), + ("Disable", "Забрана"), + ("Options", "Настроики"), + ("resolution_original_tip", "Оригинална разделителна способност"), + ("resolution_fit_local_tip", "Приспособяване към тукашната разделителна способност"), + ("resolution_custom_tip", "Разделителна способност по свой избор"), + ("Collapse toolbar", "Свиване на лентата с инструменти"), + ("Accept and Elevate", "Приемане и предоставяне на допълнителни права"), + ("accept_and_elevate_btn_tooltip", "Приемане на връзката предоставяне на UAC разрешения."), + ("clipboard_wait_response_timeout_tip", "Времето за изчакване на отговор за препис изтече."), + ("Incoming connection", "Входяща връзка"), + ("Outgoing connection", "Изходяща връзка"), + ("Exit", "Изход"), + ("Open", "Отваряне"), + ("logout_tip", "Сигурни ли сте, че искате да излезете?"), + ("Service", "Услуга"), + ("Start", "Стартиране"), + ("Stop", "Спиране"), + ("exceed_max_devices", "Достигнахте максималния брой управлявани устройства."), + ("Sync with recent sessions", "Синхронизиране с последните сесии"), + ("Sort tags", "Подреди етикети"), + ("Open connection in new tab", "Отваряне на връзката в нов раздел"), + ("Move tab to new window", "Превместване на раздела в нов прозорец"), + ("Can not be empty", "Не може да е празно"), + ("Already exists", "Вече съществува"), + ("Change Password", "Промяна на парола"), + ("Refresh Password", "Обновяване парола"), + ("ID", "Идентификатор (ID)"), + ("Grid View", "Табличен изглед"), + ("List View", "Списъчен изглед"), + ("Select", "Избор"), + ("Toggle Tags", "Превключване на етикети"), + ("pull_ab_failed_tip", "Неуспешно опресняване на адресната книга"), + ("push_ab_failed_tip", "Неуспешно синхронизиране на адресната книга със сървъра"), + ("synced_peer_readded_tip", "Устройствата, които са присъствали в последните сесии, ще бъдат синхронизирани обратно към адресната книга."), + ("Change Color", "Промяна на цвета"), + ("Primary Color", "Основен цвят"), + ("HSV Color", "HSV цвят"), + ("Installation Successful!", "Успешно инсталиране!"), + ("Installation failed!", "Неуспешно инсталиране"), + ("Reverse mouse wheel", "Обърнато колелото на мишката"), + ("{} sessions", "{} сесии"), + ("scam_title", "Възможно е да сте ИЗМАМЕНИ!"), + ("scam_text1", "Ако разговаряте по телефона с някой, когото НЕ ПОЗНАВАТЕ и НЯМАТЕ ДОВЕРИЕ, който ви е помолил да използвате RustDesk и да стартирате услугата, не продължавайте и затворете незабавно."), + ("scam_text2", "Те вероятно са измамник, който се опитва да открадне вашите пари или друга лична информация."), + ("Don't show again", "Не показвай отново"), + ("I Agree", "Съгласен съм"), + ("Decline", "Отказвам"), + ("Timeout in minutes", "Време за отговор в минути"), + ("auto_disconnect_option_tip", "Автоматично затваряне на входящите сесии при неактивност на потребителя"), + ("Connection failed due to inactivity", "Автоматично прекъсване на връзката поради неактивност"), + ("Check for software update on startup", "Проверявай за обновления при стартиране"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Моля обновете RustDesk Server Pro на версия {} или по-нова!"), + ("pull_group_failed_tip", "Неуспешно опресняване на групата"), + ("Filter by intersection", "Отсяване по пресичане"), + ("Remove wallpaper during incoming sessions", "Спри фоновото изображение по време на входящи сесии"), + ("Test", "Проверка"), + ("display_is_plugged_out_msg", "Дисплеят е изключен, превключете на първия монитор."), + ("No displays", "Няма екрани"), + ("Open in new window", "Отваряне в нов прозорец"), + ("Show displays as individual windows", "Показване на екраните в отделни прозорци"), + ("Use all my displays for the remote session", "Използвай всички мои екрани за отдалечена връзка"), + ("selinux_tip", "SELinux е активиран на вашето устройство, което може да попречи на RustDesk да работи правилно като контролирана страна."), + ("Change view", "Промяна изглед"), + ("Big tiles", "Големи заглавия"), + ("Small tiles", "Малки заглавия"), + ("List", "Списък"), + ("Virtual display", "Виртуален екран"), + ("Plug out all", "Разкачане на всички"), + ("True color (4:4:4)", ""), + ("Enable blocking user input", "Разрешаване на блокиране на потребителско въвеждане"), + ("id_input_tip", "Можете да въведете ID, директен IP адрес или домейн с порт (:).\nАко искате да получите достъп до устройство на друг сървър, моля, добавете адреса на сървъра (@?key=), например\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nАко искате да получите достъп до устройство на обществен сървър, моля, въведете \"@public\" , ключът не е необходим за публичен сървър"), + ("privacy_mode_impl_mag_tip", "Режим 1"), + ("privacy_mode_impl_virtual_display_tip", "Режим 2"), + ("Enter privacy mode", "Влизане в поверителен режим"), + ("Exit privacy mode", "Изход от поверителен режим"), + ("idd_not_support_under_win10_2004_tip", "Индиректен драйвер за дисплей не се поддържа. Изисква се Windows 10, версия 2004 или по-нова."), + ("input_source_1_tip", "Входен източник 1"), + ("input_source_2_tip", "Входен източник 2"), + ("Swap control-command key", ""), + ("swap-left-right-mouse", "Размяна на копчетата на мишката"), + ("2FA code", "Код за Двуфакторно удостоверяване"), + ("More", "Повече"), + ("enable-2fa-title", "Позволяване на двуфакторно удостоверяване"), + ("enable-2fa-desc", "Моля, настройте вашия удостоверител сега. Можете да използвате приложение за удостоверяване като Authy, Microsoft или Google Authenticator на вашия телефон или настолен компютър.\n\nСканирайте QR кода с вашето приложение и въведете кода, който приложението ви показва, за да активирате двуфакторно удостоверяване."), + ("wrong-2fa-code", "е може да се потвърди кодът. Проверете дали настройките за код и локалното време са правилни"), + ("enter-2fa-title", "Двуфакторно удостоверяване"), + ("Email verification code must be 6 characters.", "Кодът за проверка следва да е с дължина 6 знака."), + ("2FA code must be 6 digits.", "Кодът за 2FA (двуфакторно удостоверяване) трябва да е 6-цифрен"), + ("Multiple Windows sessions found", "Установени са няколко Windwos сесии"), + ("Please select the session you want to connect to", "Моля определете сесия към която искате да се свърженете"), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", "Предупреждение за сигурност"), + ("My address book", "Моята адресна книга"), + ("Personal", "Личен"), + ("Owner", "Собственик"), + ("Set shared password", "Задай споделена парола"), + ("Exist in", "Съществува в"), + ("Read-only", "Само четене"), + ("Read/Write", "Писане/четене"), + ("Full Control", "Пълен контрол"), + ("share_warning_tip", ""), + ("Everyone", "Всички"), + ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", "Следвай отдалечения курсор"), + ("Follow remote window focus", "Следвай фокуса на отдалечените прозорци"), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", "Входящ"), + ("Outgoing", "Изходящ"), + ("Clear Wayland screen selection", "Изчистване избор на Wayland екран"), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", "Използвай рендер на текстури"), + ("Floating window", "Плаващ прозорец"), + ("floating_window_tip", ""), + ("Keep screen on", "Запази екранът включен"), + ("Never", "Никога"), + ("During controlled", "Докато е обект на управление"), + ("During service is on", "Докато услугата е включена"), + ("Capture screen using DirectX", "Заснемай екрана ползвайки DirectX"), + ("Back", "Назад"), + ("Apps", "Приложения"), + ("Volume up", "Усилване звук"), + ("Volume down", "Намаляване звук"), + ("Power", "Мощност"), + ("Telegram bot", "Телеграм бот"), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", "За RustDesk"), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", "Отключване с PIN"), + ("Requires at least {} characters", ""), + ("Wrong PIN", "Грешен PIN"), + ("Set PIN", "Избор PIN"), + ("Enable trusted devices", "Позволяване доверени устройства"), + ("Manage trusted devices", "Управление доверени устройства"), + ("Platform", "Платформа"), + ("Days remaining", "Оставащи дни"), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", "Възобновяване"), + ("Invalid file name", "Невалидно име за файл"), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Моля, надстройте клиента RustDesk до версия {} или по-нова от отдалечената страна!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Преглед на камерата"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Продължи с {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/ca.rs b/vendor/rustdesk/src/lang/ca.rs new file mode 100644 index 0000000..799ca95 --- /dev/null +++ b/vendor/rustdesk/src/lang/ca.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Estat"), + ("Your Desktop", "Aquest ordinador"), + ("desk_tip", "Es pot accedir a aquest equip mitjançant les credencials:"), + ("Password", "Contrasenya"), + ("Ready", "Preparat."), + ("Established", "S'ha establert."), + ("connecting_status", "S'està connectant a la xarxa de RustDesk..."), + ("Enable service", "Habilita el servei."), + ("Start service", "Inicia el servei."), + ("Service is running", "El servei s'està executant."), + ("Service is not running", "El servei no s'està executant."), + ("not_ready_status", "No disponible. Verifiqueu la connexió"), + ("Control Remote Desktop", "Dispositiu remot"), + ("Transfer file", "Transfereix fitxers"), + ("Connect", "Connecta"), + ("Recent sessions", "Sessions recents"), + ("Address book", "Llibreta d'adreces"), + ("Confirmation", "Confirmació"), + ("TCP tunneling", "Túnel TCP"), + ("Remove", "Suprimeix"), + ("Refresh random password", "Actualitza la contrasenya aleatòria"), + ("Set your own password", "Establiu la vostra contrasenya"), + ("Enable keyboard/mouse", "Habilita el teclat/ratolí"), + ("Enable clipboard", "Habilita el porta-retalls"), + ("Enable file transfer", "Habilita la transferència de fitxers"), + ("Enable TCP tunneling", "Habilita el túnel TCP"), + ("IP Whitelisting", "Adreces IP admeses"), + ("ID/Relay Server", "ID/Repetidor del Servidor"), + ("Import server config", "Importa la configuració del servidor"), + ("Export Server Config", "Exporta la configuració del servidor"), + ("Import server configuration successfully", "S'ha importat la configuració del servidor correctament"), + ("Export server configuration successfully", "S'ha exportat la configuració del servidor correctament"), + ("Invalid server configuration", "Configuració del servidor no vàlida"), + ("Clipboard is empty", "El porta-retalls és buit"), + ("Stop service", "Atura el servei"), + ("Change ID", "Canvia la ID"), + ("Your new ID", "Identificador nou"), + ("length %min% to %max%", "Entre %min% i %max% caràcters"), + ("starts with a letter", "Comença amb una lletra"), + ("allowed characters", "Caràcters admesos"), + ("id_change_tip", "Els caràcters admesos són: a-z, A-Z, 0-9, - (dash), _ (guió baix). El primer caràcter ha de ser a-z/A-Z, i una mida de 6 a 16 caràcters."), + ("Website", "Lloc web"), + ("About", "Quant al RustDesk"), + ("Slogan_tip", "Fet de tot cor dins d'aquest món caòtic!\nTraducció: Benet R. i Camps (BennyBeat)."), + ("Privacy Statement", "Declaració de privadesa"), + ("Mute", "Silencia"), + ("Build Date", "Data de compilació"), + ("Version", "Versió"), + ("Home", "Inici"), + ("Audio Input", "Entrada d'àudio"), + ("Enhancements", "Millores"), + ("Hardware Codec", "Codificació per maquinari"), + ("Adaptive bitrate", "Taxa de bits adaptativa"), + ("ID Server", "ID del servidor"), + ("Relay Server", "Repetidor del servidor"), + ("API Server", "Clau API del servidor"), + ("invalid_http", "ha de començar amb http:// o https://"), + ("Invalid IP", "IP no vàlida"), + ("Invalid format", "Format no vàlid"), + ("server_not_support", "Encara no suportat pel servidor"), + ("Not available", "No disponible"), + ("Too frequent", "Massa freqüent"), + ("Cancel", "Cancel·la"), + ("Skip", "Omet"), + ("Close", "Surt"), + ("Retry", "Torna a provar"), + ("OK", "D'acord"), + ("Password Required", "Contrasenya requerida"), + ("Please enter your password", "Inseriu la contrasenya"), + ("Remember password", "Recorda la contrasenya"), + ("Wrong Password", "Contrasenya no vàlida"), + ("Do you want to enter again?", "Voleu tornar a provar?"), + ("Connection Error", "Error de connexió"), + ("Error", "Error"), + ("Reset by the peer", "Restablert pel client"), + ("Connecting...", "S'està connectant..."), + ("Connection in progress. Please wait.", "S'està connectant. Espereu..."), + ("Please try 1 minute later", "Torneu a provar en 1 minut"), + ("Login Error", "Error d'accés"), + ("Successful", "Correcte"), + ("Connected, waiting for image...", "S'ha connectat; en espera de rebre la imatge..."), + ("Name", "Nom"), + ("Type", "Tipus"), + ("Modified", "Modificat"), + ("Size", "Mida"), + ("Show Hidden Files", "Mostra els fitxers ocults"), + ("Receive", "Rep"), + ("Send", "Envia"), + ("Refresh File", "Actualitza"), + ("Local", "Local"), + ("Remote", "Remot"), + ("Remote Computer", "Dispositiu remot"), + ("Local Computer", "Aquest ordinador"), + ("Confirm Delete", "Confirmació de supressió"), + ("Delete", "Suprimeix"), + ("Properties", "Propietats"), + ("Multi Select", "Selecció múltiple"), + ("Select All", "Seleciona-ho tot"), + ("Unselect All", "Desselecciona-ho tot"), + ("Empty Directory", "Carpeta buida"), + ("Not an empty directory", "No és una carpeta buida"), + ("Are you sure you want to delete this file?", "Segur que voleu suprimir aquest fitxer?"), + ("Are you sure you want to delete this empty directory?", "Segur que voleu suprimir aquesta carpeta buida?"), + ("Are you sure you want to delete the file of this directory?", "Segur que voleu suprimir el fitxer d'aquesta carpeta?"), + ("Do this for all conflicts", "Aplica aquesta acció per a tots els conflictes"), + ("This is irreversible!", "Aquesta acció no es pot desfer!"), + ("Deleting", "S'està suprimint"), + ("files", "fitxers"), + ("Waiting", "En espera"), + ("Finished", "Ha finalitzat"), + ("Speed", "Velocitat"), + ("Custom Image Quality", "Qualitat d'imatge personalitzada"), + ("Privacy mode", "Mode privat"), + ("Block user input", "Bloca el control a l'usuari"), + ("Unblock user input", "Desbloca el control a l'usuari"), + ("Adjust Window", "Ajusta la finestra"), + ("Original", "Original"), + ("Shrink", "Encongida"), + ("Stretch", "Ampliada"), + ("Scrollbar", "Barra de desplaçament"), + ("ScrollAuto", "Desplaçament automàtic"), + ("Good image quality", "Bona qualitat d'imatge"), + ("Balanced", "Equilibrada"), + ("Optimize reaction time", "Optimitza el temps de reacció"), + ("Custom", "Personalitzada"), + ("Show remote cursor", "Mostra el cursor remot"), + ("Show quality monitor", "Mostra la informació de flux"), + ("Disable clipboard", "Inhabilita el porta-retalls"), + ("Lock after session end", "Bloca en finalitzar la sessió"), + ("Insert Ctrl + Alt + Del", "Insereix Ctrl + Alt + Del"), + ("Insert Lock", "Bloca"), + ("Refresh", "Actualitza"), + ("ID does not exist", "Aquesta ID no existeix"), + ("Failed to connect to rendezvous server", "Ha fallat en connectar al servidor assignat"), + ("Please try later", "Proveu més tard"), + ("Remote desktop is offline", "El dispositiu remot està desconnectat"), + ("Key mismatch", "La clau no coincideix"), + ("Timeout", "S'ha exhaurit el temps"), + ("Failed to connect to relay server", "Ha fallat en connectar amb el repetidor del servidor"), + ("Failed to connect via rendezvous server", "Ha fallat en connectar mitjançant el servidor assignat"), + ("Failed to connect via relay server", "Ha fallat en connectar mitjançant el repetidor del servidor"), + ("Failed to make direct connection to remote desktop", "Ha fallat la connexió directa amb el dispositiu remot"), + ("Set Password", "Establiu una contrasenya"), + ("OS Password", "Contrasenya del sistema"), + ("install_tip", "En alguns casos és possible que el RustDesk no funcioni correctament per les restriccions UAC («User Account Control»; Control de comptes d'usuari). Per evitar aquest problema, instal·leu el RustDesk al vostre sistema."), + ("Click to upgrade", "Feu clic per a actualitzar"), + ("Configure", "Configura"), + ("config_acc", "Per a poder controlar el dispositiu remotament, faciliteu al RustDesk els permisos d'accessibilitat."), + ("config_screen", "Per a poder controlar el dispositiu remotament, faciliteu al RustDesk els permisos de gravació de pantalla."), + ("Installing ...", "S'està instal·lant..."), + ("Install", "Instal·la"), + ("Installation", "Instal·lació"), + ("Installation Path", "Ruta de la instal·lació"), + ("Create start menu shortcuts", "Crea una drecera al menú d'inici"), + ("Create desktop icon", "Crea una icona a l'escriptori"), + ("agreement_tip", "En iniciar la instal·lació, esteu acceptant l'acord de llicència d'usuari."), + ("Accept and Install", "Accepta i instal·la"), + ("End-user license agreement", "Acord de llicència d'usuari final"), + ("Generating ...", "S'està generant..."), + ("Your installation is lower version.", "La instal·lació actual és una versió inferior"), + ("not_close_tcp_tip", "No tanqueu aquesta finestra mentre utilitzeu el túnel"), + ("Listening ...", "S'està escoltant..."), + ("Remote Host", "Amfitrió remot"), + ("Remote Port", "Port remot"), + ("Action", "Acció"), + ("Add", "Afegeix"), + ("Local Port", "Port local"), + ("Local Address", "Adreça local"), + ("Change Local Port", "Canvia el port local"), + ("setup_server_tip", "Per a connexions més ràpides o privades, configureu el vostre servidor"), + ("Too short, at least 6 characters.", "Massa curt. Són necessaris almenys 6 caràcters."), + ("The confirmation is not identical.", "Les contrasenyes no coincideixen."), + ("Permissions", "Permisos"), + ("Accept", "Accepta"), + ("Dismiss", "Ignora"), + ("Disconnect", "Desconnecta"), + ("Enable file copy and paste", "Habilita la còpia i enganxament de fitxers"), + ("Connected", "Connectat"), + ("Direct and encrypted connection", "Connexió xifrada directa"), + ("Relayed and encrypted connection", "Connexió xifrada per repetidor"), + ("Direct and unencrypted connection", "Connexió directa sense xifratge"), + ("Relayed and unencrypted connection", "Connexió per repetidor sense xifratge"), + ("Enter Remote ID", "Inseriu la ID remota"), + ("Enter your password", "Inseriu la contrasenya"), + ("Logging in...", "S'està iniciant..."), + ("Enable RDP session sharing", "Habilita l'ús compartit de sessions RDP"), + ("Auto Login", "Inici de sessió automàtic"), + ("Enable direct IP access", "Habilita l'accés directe per IP"), + ("Rename", "Reanomena"), + ("Space", "Espai"), + ("Create desktop shortcut", "Crea una drecera a l'escriptori"), + ("Change Path", "Canvia la ruta"), + ("Create Folder", "Carpeta nova"), + ("Please enter the folder name", "Inseriu el nom de la carpeta"), + ("Fix it", "Repara"), + ("Warning", "Atenció"), + ("Login screen using Wayland is not supported", "L'inici de sessió amb Wayland encara no és compatible"), + ("Reboot required", "Cal reiniciar"), + ("Unsupported display server", "Servidor de visualització no compatible"), + ("x11 expected", "x11 necessari"), + ("Port", "Port"), + ("Settings", "Configuració"), + ("Username", "Nom d'usuari"), + ("Invalid port", "Port no vàlid"), + ("Closed manually by the peer", "Tancat manualment pel client"), + ("Enable remote configuration modification", "Habilita la modificació remota de la configuració"), + ("Run without install", "Inicia sense instal·lar"), + ("Connect via relay", "Connecta mitjançant un repetidor"), + ("Always connect via relay", "Connecta sempre mitjançant un repetidor"), + ("whitelist_tip", "Només les IP admeses es podran connectar"), + ("Login", "Inicia la sessió"), + ("Verify", "Verifica"), + ("Remember me", "Recorda'm"), + ("Trust this device", "Confia en aquest dispositiu"), + ("Verification code", "Codi de verificació"), + ("verification_tip", "S'ha enviat un codi de verificació al correu-e registrat. Inseriu-lo per a continuar amb l'inici de sessió."), + ("Logout", "Tanca la sessió"), + ("Tags", "Etiquetes"), + ("Search ID", "Cerca per ID"), + ("whitelist_sep", "Separades per coma, punt i coma, espai o una adreça per línia"), + ("Add ID", "Afegeix una ID"), + ("Add Tag", "Afegeix una etiqueta"), + ("Unselect all tags", "Desselecciona totes les etiquetes"), + ("Network error", "Error de la xarxa"), + ("Username missed", "No s'ha indicat el nom d'usuari"), + ("Password missed", "No s'ha indicat la contrasenya"), + ("Wrong credentials", "Credencials errònies"), + ("The verification code is incorrect or has expired", "El codi de verificació no és vàlid o ha caducat"), + ("Edit Tag", "Edita l'etiqueta"), + ("Forget Password", "Contrasenya oblidada"), + ("Favorites", "Preferits"), + ("Add to Favorites", "Afegeix als preferits"), + ("Remove from Favorites", "Suprimeix dels preferits"), + ("Empty", "Buida"), + ("Invalid folder name", "Nom de carpeta no vàlid"), + ("Socks5 Proxy", "Servidor intermediari Socks5"), + ("Socks5/Http(s) Proxy", "Servidor intermediari Socks5/Http(s)"), + ("Discovered", "Descobert"), + ("install_daemon_tip", "Per a iniciar durant l'arrencada del sistema, heu d'instal·lar el servei."), + ("Remote ID", "ID remota"), + ("Paste", "Enganxa"), + ("Paste here?", "Voleu enganxar aquí?"), + ("Are you sure to close the connection?", "Segur que voleu finalitzar la connexió?"), + ("Download new version", "Baixa la versió nova"), + ("Touch mode", "Mode tàctil"), + ("Mouse mode", "Mode ratolí"), + ("One-Finger Tap", "Toc amb un dit"), + ("Left Mouse", "Botó esquerre"), + ("One-Long Tap", "Toc prolongat"), + ("Two-Finger Tap", "Toc amb dos dits"), + ("Right Mouse", "Botó dret"), + ("One-Finger Move", "Moviment amb un dit"), + ("Double Tap & Move", "Toc doble i moveu"), + ("Mouse Drag", "Arrossega el ratolí"), + ("Three-Finger vertically", "Tres dits en vertical"), + ("Mouse Wheel", "Roda del ratolí"), + ("Two-Finger Move", "Moviment amb dos dits"), + ("Canvas Move", "Moviment del llenç"), + ("Pinch to Zoom", "Pessic per escalar"), + ("Canvas Zoom", "escala del llenç"), + ("Reset canvas", "Reinici del llenç"), + ("No permission of file transfer", "Cap permís per a transferència de fitxers"), + ("Note", "Nota"), + ("Connection", "Connexió"), + ("Share screen", "Compartició de pantalla"), + ("Chat", "Xat"), + ("Total", "Total"), + ("items", "elements"), + ("Selected", "Seleccionat"), + ("Screen Capture", "Captura de pantalla"), + ("Input Control", "Control d'entrada"), + ("Audio Capture", "Captura d'àudio"), + ("Do you accept?", "Voleu acceptar?"), + ("Open System Setting", "Obre la configuració del sistema"), + ("How to get Android input permission?", "Com modificar els permisos a Android?"), + ("android_input_permission_tip1", "Per a controlar de forma remota el vostre dispositiu amb gestos o un ratolí, heu de permetre al RustDesk l'ús del servei «Accessibilitat»."), + ("android_input_permission_tip2", "A l'apartat Configuració del sistema de la pàgina següent, aneu a «Serveis baixats», i activeu el «RustDesk Input»."), + ("android_new_connection_tip", "S'ha rebut una petició nova per a controlar el vostre dispositiu."), + ("android_service_will_start_tip", "Activant «Gravació de pantalla» s'iniciarà automàticament el servei que permet a altres enviar sol·licituds de connexió cap al vostre dispositiu."), + ("android_stop_service_tip", "Tancant el servei finalitzaran automàticament les connexions en ús."), + ("android_version_audio_tip", "Aquesta versió d'Android no suporta la captura d'àudio. Actualitzeu a Android 10 o superior."), + ("android_start_service_tip", "Toqueu a «Inicia el servei» o activeu el permís «Captura de pantalla» per a iniciar el servei de compartició de pantalla."), + ("android_permission_may_not_change_tip", "Els permisos per a les connexions ja establertes poden no canviar, fins que no torneu a connectar."), + ("Account", "Compte"), + ("Overwrite", "Reemplaça"), + ("This file exists, skip or overwrite this file?", "Aquest fitxer ja existeix. Voleu ometre o reemplaçar l'original?"), + ("Quit", "Surt"), + ("Help", "Ajuda"), + ("Failed", "Ha fallat"), + ("Succeeded", "Fet"), + ("Someone turns on privacy mode, exit", "S'ha activat el Mode privat; surt"), + ("Unsupported", "No suportat"), + ("Peer denied", "Client denegat"), + ("Please install plugins", "Instal·leu els complements"), + ("Peer exit", "Finalitzat pel client"), + ("Failed to turn off", "Ha fallat en desactivar"), + ("Turned off", "Desactivat"), + ("Language", "Idioma"), + ("Keep RustDesk background service", "Manté el servei del RustDesk en rerefons"), + ("Ignore Battery Optimizations", "Ignora les optimitzacions de bateria"), + ("android_open_battery_optimizations_tip", "Si voleu desactivar aquesta característica, feu-ho des de la pàgina següent de configuració del RustDesk, utilitzant l'opció relativa a «Bateria»"), + ("Start on boot", "Inicia durant l'arrencada"), + ("Start the screen sharing service on boot, requires special permissions", "Per iniciar la compartició de pantalla durant l'arrencada del sistema, calen permisos especials"), + ("Connection not allowed", "Connexió no permesa"), + ("Legacy mode", "Mode heretat"), + ("Map mode", "Mode mapa"), + ("Translate mode", "Mode traduït"), + ("Use permanent password", "Utilitza la contrasenya permanent"), + ("Use both passwords", "Utilitza totes dues opcions"), + ("Set permanent password", "Estableix la contrasenya permanent"), + ("Enable remote restart", "Habilita el reinici remot"), + ("Restart remote device", "Reinicia el dispositiu remot"), + ("Are you sure you want to restart", "Segur que voleu reiniciar"), + ("Restarting remote device", "Reinici del dispositiu remot"), + ("remote_restarting_tip", "S'està reiniciant el dispositiu remot. Tanqueu aquest missatge i torneu a connectar amb ell mitjançant la contrasenya, un cop estigui en línia."), + ("Copied", "S'ha copiat"), + ("Exit Fullscreen", "Surt de la pantalla completa"), + ("Fullscreen", "Pantalla completa"), + ("Mobile Actions", "Funcions mòbils"), + ("Select Monitor", "Selecció de monitor"), + ("Control Actions", "Control de funcions"), + ("Display Settings", "Configuració de pantalla"), + ("Ratio", "Relació"), + ("Image Quality", "Qualitat de la imatge"), + ("Scroll Style", "Tipus de desplaçament"), + ("Show Toolbar", "Mostra la barra d'eines"), + ("Hide Toolbar", "Amaga la barra d'eines"), + ("Direct Connection", "Connexió directa"), + ("Relay Connection", "Connexió amb repetidor"), + ("Secure Connection", "Connexió segura"), + ("Insecure Connection", "Connexió no segura"), + ("Scale original", "Escala original"), + ("Scale adaptive", "Escala adaptativa"), + ("General", "General"), + ("Security", "Seguretat"), + ("Theme", "Tema"), + ("Dark Theme", "Tema fosc"), + ("Light Theme", "Tema clar"), + ("Dark", "Fosc"), + ("Light", "Clar"), + ("Follow System", "Utilitza la configuració del sistema"), + ("Enable hardware codec", "Habilita la codificació per maquinari"), + ("Unlock Security Settings", "Desbloca la configuració de seguretat"), + ("Enable audio", "Habilita l'àudio"), + ("Unlock Network Settings", "Desbloca la configuració de la xarxa"), + ("Server", "Servidor"), + ("Direct IP Access", "Accés directe per IP"), + ("Proxy", "Servidor intermediari"), + ("Apply", "Aplica"), + ("Disconnect all devices?", "Voleu desconnectar tots els dispositius?"), + ("Clear", "Buida"), + ("Audio Input Device", "Dispositiu d'entrada d'àudio"), + ("Use IP Whitelisting", "Utilitza un llistat d'IP admeses"), + ("Network", "Xarxa"), + ("Pin Toolbar", "Ancora a la barra d'eines"), + ("Unpin Toolbar", "Desancora de la barra d'eines"), + ("Recording", "Gravació"), + ("Directory", "Contactes"), + ("Automatically record incoming sessions", "Enregistrament automàtic de sessions entrants"), + ("Automatically record outgoing sessions", ""), + ("Change", "Canvia"), + ("Start session recording", "Inicia la gravació de la sessió"), + ("Stop session recording", "Atura la gravació de la sessió"), + ("Enable recording session", "Habilita la gravació de la sessió"), + ("Enable LAN discovery", "Habilita el descobriment LAN"), + ("Deny LAN discovery", "Inhabilita el descobriment LAN"), + ("Write a message", "Escriviu un missatge"), + ("Prompt", "Sol·licitud"), + ("Please wait for confirmation of UAC...", "Espereu a la confirmació de l'UAC..."), + ("elevated_foreground_window_tip", "La finestra de connexió actual requereix permisos ampliats per a funcionar i, de forma temporal, no es pot utilitzar ni el teclat ni el ratolí. Demaneu a l'usuari remot que minimitzi la finestra actual, o bé que faci clic al botó Permisos ampliats de la finestra d'administració de la connexió. Per a evitar aquest problema en un futur, instal·leu el RustDesk al dispositiu remot."), + ("Disconnected", "Desconnectat"), + ("Other", "Altre"), + ("Confirm before closing multiple tabs", "Confirma abans de tancar diverses pestanyes alhora"), + ("Keyboard Settings", "Configuració del teclat"), + ("Full Access", "Accés complet"), + ("Screen Share", "Compartició de pantalla"), + ("ubuntu-21-04-required", "Wayland requereix Ubuntu 21.04 o superior"), + ("wayland-requires-higher-linux-version", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Marcador"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioneu la pantalla que compartireu (quina serà visible al client)"), + ("Show RustDesk", "Mostra el RustDesk"), + ("This PC", "Aquest equip"), + ("or", "o"), + ("Elevate", "Permisos ampliats"), + ("Zoom cursor", "Escala del ratolí"), + ("Accept sessions via password", "Accepta les sessions mitjançant una contrasenya"), + ("Accept sessions via click", "Accepta les sessions expressament amb el ratolí"), + ("Accept sessions via both", "Accepta les sessions de totes dues formes"), + ("Please wait for the remote side to accept your session request...", "S'està esperant l'acceptació remota de la vostra connexió..."), + ("One-time Password", "Contrasenya d'un sol ús"), + ("Use one-time password", "Utilitza una contrasenya d'un sol ús"), + ("One-time password length", "Mida de la contrasenya d'un sol ús"), + ("Request access to your device", "Ha demanat connectar al vostre dispositiu"), + ("Hide connection management window", "Amaga la finestra d'administració de la connexió"), + ("hide_cm_tip", "Permet amagar la finestra només en acceptar sessions entrants sempre que s'utilitzi una contrasenya permanent"), + ("wayland_experiment_tip", "El suport per a Wayland està en fase experimental; es recomana l'ús d'x11 si us cal accés de forma desatesa."), + ("Right click to select tabs", "Feu clic amb el botó dret per a seleccionar pestanyes"), + ("Skipped", "S'ha omès"), + ("Add to address book", "Afegeix a la llibreta d'adreces"), + ("Group", "Grup"), + ("Search", "Cerca"), + ("Closed manually by web console", "Tancat manualment per la consola web"), + ("Local keyboard type", "Tipus de teclat local"), + ("Select local keyboard type", "Seleccioneu el tipus de teclat local"), + ("software_render_tip", "Si utilitzeu una gràfica Nvidia a Linux i la connexió remota es tanca immediatament en connectar, canviar al controlador lliure «Nouveau» amb renderització per programari, pot ajudar a solucionar el problema. Es requerirà en aquest cas reiniciar l'aplicació."), + ("Always use software rendering", "Utilitza sempre la renderització de programari"), + ("config_input", "Per a poder controlar el dispositiu remotament amb el teclat, faciliteu al RustDesk els permisos d'entrada necessaris."), + ("config_microphone", "Per a poder parlar remotament, faciliteu al RustDesk els permisos de gravació d'àudio necessaris."), + ("request_elevation_tip", "També, la part remota pot concedir aquests permisos de forma manual."), + ("Wait", "Espereu"), + ("Elevation Error", "Error de permisos"), + ("Ask the remote user for authentication", "Demaneu l'autenticació al client remot"), + ("Choose this if the remote account is administrator", "Trieu aquesta opció si el compte remot té permisos d'administrador"), + ("Transmit the username and password of administrator", "Indiqueu l'usuari i contrasenya de l'administrador"), + ("still_click_uac_tip", "Es requereix acceptació manual a la part remota de la finestra «UAC» del RustDesk en execució."), + ("Request Elevation", "Sol·licita els permisos"), + ("wait_accept_uac_tip", "Espereu fins que l'usuari remot accepti la finestra de diàleg de l'«UAC»."), + ("Elevate successfully", "S'han acceptat els permisos"), + ("uppercase", "majúscula"), + ("lowercase", "minúscula"), + ("digit", "número"), + ("special character", "caràcter especial"), + ("length>=8", "mida>=8"), + ("Weak", "Feble"), + ("Medium", "Acceptable"), + ("Strong", "Segura"), + ("Switch Sides", "Inverteix la connexió"), + ("Please confirm if you want to share your desktop?", "Realment voleu que es controli aquest equip?"), + ("Display", "Pantalla"), + ("Default View Style", "Estil de vista per defecte"), + ("Default Scroll Style", "Estil de desplaçament per defecte"), + ("Default Image Quality", "Qualitat de la imatge per defecte"), + ("Default Codec", "Còdec per defecte"), + ("Bitrate", "Taxa de bits"), + ("FPS", "FPS"), + ("Auto", "Automàtic"), + ("Other Default Options", "Altres opcions per defecte"), + ("Voice call", "Trucada"), + ("Text chat", "Xat"), + ("Stop voice call", "Penja la trucada"), + ("relay_hint_tip", "Quan no sigui possible la connexió directa, podeu provar mitjançant un repetidor. Addicionalment, si voleu que l'ús d'un repetidor sigui la primera opció per defecte, podeu afegir el sufix «/r» a la ID, o seleccionar l'opció «Connecta sempre mitjançant un repetidor» si ja existeix una fitxa amb aquesta ID a la pestanya de connexions recents."), + ("Reconnect", "Torna a connectar"), + ("Codec", "Còdec"), + ("Resolution", "Resolució"), + ("No transfers in progress", "Cap transferència iniciada"), + ("Set one-time password length", "Mida de la contrasenya d'un sol ús"), + ("RDP Settings", "Opcions de connexió RDP"), + ("Sort by", "Organitza per"), + ("New Connection", "Connexió nova"), + ("Restore", "Restaura"), + ("Minimize", "Minimitza"), + ("Maximize", "Maximitza"), + ("Your Device", "Aquest dispositiu"), + ("empty_recent_tip", "No s'ha trobat cap sessió recent!\nS'afegiran automàticament les connexions que realitzeu."), + ("empty_favorite_tip", "No heu afegit cap dispositiu aquí!\nPodeu afegir dispositius favorits en qualsevol moment."), + ("empty_lan_tip", "No s'ha trobat cap dispositiu proper."), + ("empty_address_book_tip", "Sembla que no teniu cap dispositiu a la vostra llista d'adreces."), + ("Empty Username", "Nom d'usuari buit"), + ("Empty Password", "Contrasenya buida"), + ("Me", "Vós"), + ("identical_file_tip", "Aquest fitxer és idèntic al del client."), + ("show_monitors_tip", "Mostra les pantalles a la barra d'eines"), + ("View Mode", "Mode espectador"), + ("login_linux_tip", "És necessari que inicieu prèviament sessió amb un entorn d'escriptori x11 habilitat"), + ("verify_rustdesk_password_tip", "Verifica la contrasenya del RustDesk"), + ("remember_account_tip", "Recorda aquest compte"), + ("os_account_desk_tip", "S'utilitza aquest compte per iniciar la sessió al sistema remot i habilitar el mode sense cap pantalla connectada"), + ("OS Account", "Compte d'usuari"), + ("another_user_login_title_tip", "Altre usuari ha iniciat ja una sessió"), + ("another_user_login_text_tip", "Desconnecta"), + ("xorg_not_found_title_tip", "No s'ha trobat l'entorn Xorg"), + ("xorg_not_found_text_tip", "Instal·leu el Xorg"), + ("no_desktop_title_tip", "Cap escriptori disponible"), + ("no_desktop_text_tip", "Instal·leu l'entorn d'escriptori GNOME"), + ("No need to elevate", "No calen permisos ampliats"), + ("System Sound", "So del sistema"), + ("Default", "per defecte"), + ("New RDP", "Connexió RDP nova"), + ("Fingerprint", "Empremta"), + ("Copy Fingerprint", "Copia l'empremta"), + ("no fingerprints", "Cap empremta"), + ("Select a peer", "Seleccioneu un client"), + ("Select peers", "Seleccioneu els clients"), + ("Plugins", "Complements"), + ("Uninstall", "Desinstal·la"), + ("Update", "Actualitza"), + ("Enable", "Activa"), + ("Disable", "Desactiva"), + ("Options", "Opcions"), + ("resolution_original_tip", "Resolució original"), + ("resolution_fit_local_tip", "Ajusta la resolució local"), + ("resolution_custom_tip", "Resolució personalitzada"), + ("Collapse toolbar", "Minimitza la barra d'eines"), + ("Accept and Elevate", "Accepta i permet"), + ("accept_and_elevate_btn_tooltip", "Accepta la connexió i permet els permisos elevats UAC."), + ("clipboard_wait_response_timeout_tip", "S'ha esgotat el temps d'espera amb la resposta de còpia."), + ("Incoming connection", "Connexió entrant"), + ("Outgoing connection", "Connexió sortint"), + ("Exit", "Surt"), + ("Open", "Obre"), + ("logout_tip", "Segur que voleu desconnectar?"), + ("Service", "Servei"), + ("Start", "Inicia"), + ("Stop", "Atura"), + ("exceed_max_devices", "Heu assolit el nombre màxim de dispositius administrables."), + ("Sync with recent sessions", "Sincronitza amb les sessions recents"), + ("Sort tags", "Ordena les etiquetes"), + ("Open connection in new tab", "Obre la connexió en una pestanya nova"), + ("Move tab to new window", "Mou la pestanya a una finestra nova"), + ("Can not be empty", "No pot estar buit"), + ("Already exists", "Ja existeix"), + ("Change Password", "Canvia la contrasenya"), + ("Refresh Password", "Actualitza la contrasenya"), + ("ID", "ID"), + ("Grid View", "Disposició de graella"), + ("List View", "Disposició de llista"), + ("Select", "Selecciona"), + ("Toggle Tags", "Habilita les etiquetes"), + ("pull_ab_failed_tip", "Ha fallat en actualitzar la llista de contactes"), + ("push_ab_failed_tip", "Ha fallat en actualitzar la llista amb el servidor"), + ("synced_peer_readded_tip", "Els dispositius que es troben a la llista de sessions recents se sincronitzaran novament a la llista de contactes."), + ("Change Color", "Canvia el color"), + ("Primary Color", "Color principal"), + ("HSV Color", "Color HSV"), + ("Installation Successful!", "S'ha instal·lat correctament"), + ("Installation failed!", "Ha fallat la instal·lació"), + ("Reverse mouse wheel", "Inverteix la roda del ratolí"), + ("{} sessions", "{} sessions"), + ("scam_title", "Podríeu ser víctima d'una ESTAFA!"), + ("scam_text1", "Si cap persona qui NO coneixeu NI CONFIEU us demanés l'ús del RustDesk, no continueu i talleu la comunicació immediatament."), + ("scam_text2", "Habitualment solen ser atacants intentant fer-se amb els vostres diners o informació privada."), + ("Don't show again", "No tornis a mostrar"), + ("I Agree", "Accepto"), + ("Decline", "No accepto"), + ("Timeout in minutes", "Temps d'espera en minuts"), + ("auto_disconnect_option_tip", "Tanca automàticament les sessions entrants per inactivitat de l'usuari"), + ("Connection failed due to inactivity", "Ha fallat la connexió per inactivitat"), + ("Check for software update on startup", "Cerca actualitzacions en iniciar"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Actualitzeu el RustDesk Server Pro a la versió {} o superior!"), + ("pull_group_failed_tip", "Ha fallat en actualitzar el grup"), + ("Filter by intersection", "Filtra per intersecció"), + ("Remove wallpaper during incoming sessions", "Inhabilita el fons d'escriptori durant la sessió entrant"), + ("Test", "Prova"), + ("display_is_plugged_out_msg", "El monitor està desconnectat; canvieu primer al monitor principal."), + ("No displays", "Cap monitor"), + ("Open in new window", "Obre en una finestra nova"), + ("Show displays as individual windows", "Mostra cada monitor com una finestra individual"), + ("Use all my displays for the remote session", "Utilitza tots els meus monitors per a la connexió remota"), + ("selinux_tip", "SELinux està activat al vostre dispositiu, la qual cosa evita que el RustDesk funcioni correctament com a equip controlable."), + ("Change view", "Canvia la vista"), + ("Big tiles", "Mosaic gran"), + ("Small tiles", "Mosaic petit"), + ("List", "Llista"), + ("Virtual display", "Pantalla virtual"), + ("Plug out all", "Desconnecta-ho tot"), + ("True color (4:4:4)", "Color real (4:4:4)"), + ("Enable blocking user input", "Bloca el control de l'usuari amb els dispositius d'entrada"), + ("id_input_tip", "Evita que l'usuari pugui interactuar p. ex. amb el teclat o ratolí"), + ("privacy_mode_impl_mag_tip", "Mode 1"), + ("privacy_mode_impl_virtual_display_tip", "Mode 2"), + ("Enter privacy mode", "Inicia el Mode privat"), + ("Exit privacy mode", "Surt del Mode privat"), + ("idd_not_support_under_win10_2004_tip", "El controlador indirecte de pantalla no està suportat; es requereix Windows 10 versió 2004 o superior."), + ("input_source_1_tip", "Font d'entrada 1"), + ("input_source_2_tip", "Font d'entrada 2"), + ("Swap control-command key", "Canvia el comportament de la tecla Control"), + ("swap-left-right-mouse", "Alterna el comportament dels botons esquerre-dret del ratolí"), + ("2FA code", "Codi 2FA"), + ("More", "Més"), + ("enable-2fa-title", "Habilita el mètode d'autenticació de factor doble"), + ("enable-2fa-desc", "Configureu ara el vostre autenticador. Podeu utilitzar una aplicació com 2fast, FreeOTP, MultiOTP, Microsoft o Google Authenticator al vostre telèfon o escriptori.\n\nEscanegeu el codi QR amb l'aplicació i escriviu els caràcters resultants per habilitar l'autenticació de factor doble."), + ("wrong-2fa-code", "Codi 2FA no vàlid. Verifiqueu el que heu escrit i també que la configuració horària sigui correcta"), + ("enter-2fa-title", "Autenticació de factor doble"), + ("Email verification code must be 6 characters.", "El codi de verificació de correu-e són 6 caràcters"), + ("2FA code must be 6 digits.", "El codi de verificació 2FA haurien de ser almenys 6 dígits"), + ("Multiple Windows sessions found", "S'han trobat múltiples sessions en ús del Windows"), + ("Please select the session you want to connect to", "Indiqueu amb quina sessió voleu connectar"), + ("powered_by_me", "Amb la tecnologia de RustDesk"), + ("outgoing_only_desk_tip", "Aquesta és una versió personalitzada.\nPodeu connectar amb altres dispositius, però no s'accepten connexions d'entrada cap el vostre dispositiu."), + ("preset_password_warning", "Aquesta versió personalitzada té una contrasenya preestablerta. Qualsevol persona que la conegui pot tenir accés total al vostre dispositiu. Si no és el comportament desitjat, desinstal·leu aquest programa immediatament."), + ("Security Alert", "Alerta de seguretat"), + ("My address book", "Llibreta d'adreces"), + ("Personal", "Personal"), + ("Owner", "Propietari"), + ("Set shared password", "Establiu una contrasenya compartida"), + ("Exist in", "Existeix a"), + ("Read-only", "Només lectura"), + ("Read/Write", "Lectura/Escriptura"), + ("Full Control", "Control total"), + ("share_warning_tip", "Els camps a continuació estan compartits i són visibles a d'altres."), + ("Everyone", "Tothom"), + ("ab_web_console_tip", "Més a la consola web"), + ("allow-only-conn-window-open-tip", "Permet la connexió només si la finestra del RustDesk està activa"), + ("no_need_privacy_mode_no_physical_displays_tip", "Cap monitor físic. No cal l'ús del Mode privat"), + ("Follow remote cursor", "Segueix al cursor remot"), + ("Follow remote window focus", "Segueix el focus remot de la finestra activa"), + ("default_proxy_tip", "El protocol per defecte és Socks5 al port 1080"), + ("no_audio_input_device_tip", "No s'ha trobat cap dispositiu d'àudio."), + ("Incoming", "Entrant"), + ("Outgoing", "Sortint"), + ("Clear Wayland screen selection", "Neteja la pantalla de selecció Wayland"), + ("clear_Wayland_screen_selection_tip", "En netejar la finestra de selecció, podreu tornar a triar quina pantalla compartir."), + ("confirm_clear_Wayland_screen_selection_tip", "Segur que voleu netejar la pantalla de selecció del Wayland"), + ("android_new_voice_call_tip", "S'ha rebut una petició de trucada entrant. Si accepteu, la font d'àudio canviarà a comunicació per veu."), + ("texture_render_tip", "Utilitzeu aquesta opció per suavitzar la imatge. Desactiveu-ho si trobeu cap problema amb el renderitzat"), + ("Use texture rendering", "Utilitza la renderització de textures"), + ("Floating window", "Finestra flotant"), + ("floating_window_tip", "Ajuda a mantenir el servei del RustDesk en rerefons"), + ("Keep screen on", "Manté la pantalla activa"), + ("Never", "Mai"), + ("During controlled", "Durant la connexió"), + ("During service is on", "Mentre el servei està actiu"), + ("Capture screen using DirectX", "Captura utilitzant el DirectX"), + ("Back", "Enrere"), + ("Apps", "Aplicacions"), + ("Volume up", "Volum amunt"), + ("Volume down", "Volum avall"), + ("Power", "Encesa"), + ("Telegram bot", "Bot del Telegram"), + ("enable-bot-tip", "Si habiliteu aquesta característica, podreu rebre el codi 2FA mitjançant el vostre bot. També funciona com a notificador de la connexió."), + ("enable-bot-desc", "1. Obriu un xat amb @BotFather.\n2. Envieu l'ordre \"/newbot\". Rebreu un testimoni en acompletar aquest pas.\n3. Inicieu una conversa amb el vostre bot nou que acabeu de crear, enviant un missatge que comenci amb (\"/\"), com ara \"/hello\" per a activar-lo.\n"), + ("cancel-2fa-confirm-tip", "Segur que voleu cancel·lar l'autenticació 2FA?"), + ("cancel-bot-confirm-tip", "Segur que voleu cancel·lar el bot de Telegram?"), + ("About RustDesk", "Quant al RustDesk"), + ("Send clipboard keystrokes", "Envia les pulsacions de tecles del porta-retalls"), + ("network_error_tip", "Verifiqueu la vostra connexió a Internet i torneu a provar"), + ("Unlock with PIN", "Desbloca amb PIN"), + ("Requires at least {} characters", "Són necessaris almenys {} caràcters"), + ("Wrong PIN", "PIN no vàlid"), + ("Set PIN", "Definiu un codi PIN"), + ("Enable trusted devices", "Habilita els dispositius de confiança"), + ("Manage trusted devices", "Administra els dispositius de confiança"), + ("Platform", "Platforma"), + ("Days remaining", "Dies restants"), + ("enable-trusted-devices-tip", "Omet l'autenticació de factor doble (2FA) als dispositius de confiança"), + ("Parent directory", "Carpeta pare"), + ("Resume", "Continua"), + ("Invalid file name", "Nom de fitxer no vàlid"), + ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), + ("Authentication Required", "Autenticació requerida"), + ("Authenticate", "Autentica"), + ("web_id_input_tip", "Podeu inserir el número ID al propi servidor; l'accés directe per IP no és compatible amb el client web.\nSi voleu accedir a un dispositiu d'un altre servidor, afegiu l'adreça del servidor, com ara @?key= (p. ex.\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi voleu accedir a un dispositiu en un servidor públic, no cal que inseriu la clau pública «@» per al servidor públic."), + ("Download", "Descarrega"), + ("Upload folder", "Puja una carpeta"), + ("Upload files", "Puja fitxers"), + ("Clipboard is synchronized", "El porta-retalls està sincronitzat"), + ("Update client clipboard", "Actualitza el porta-retalls del client"), + ("Untagged", "Sense etiquetar"), + ("new-version-of-{}-tip", ""), + ("Accessible devices", "Dispositius accessibles"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", "Utilitza renderització D3D"), + ("Printer", "Impressora"), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", "Instal·la {} impressora"), + ("Outgoing Print Jobs", "Treballs d'impressió sortints"), + ("Incoming Print Jobs", "Treballs d'impressió entrants"), + ("Incoming Print Job", "Treballs d'impressió entrant"), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", "Fes una captura de pantalla"), + ("Taking screenshot", "Fent la captura de pantalla"), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", "Anomena i desa"), + ("Copy to clipboard", "Copia al porta-retalls"), + ("Enable remote printer", "Habilita l'impressora remota"), + ("Downloading {}", "Descarregant {}"), + ("{} Update", "{} Actualitza"), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", "Actualització automàtica"), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", "Velocitat del trackpad"), + ("Default trackpad speed", "Velocitat per defecte del trackpad"), + ("Numeric one-time password", "Contrasenya numèrica d'un sol ús"), + ("Enable IPv6 P2P connection", "Habilita la connexió IPv6 P2P"), + ("Enable UDP hole punching", "Activa la perforació UDP"), + ("View camera", "Mostra la càmera"), + ("Enable camera", "Habilita la càmera"), + ("No cameras", "No hi ha càmeres"), + ("view_camera_unsupported_tip", ""), + ("Terminal", "Terminal"), + ("Enable terminal", "Habilita el terminal"), + ("New tab", "Nova finestra"), + ("Keep terminal sessions on disconnect", "Mantingues les sessions de terminal desconnectades"), + ("Terminal (Run as administrator)", "Terminal (executa com a administrador"), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", "No s'ha pogut obtenir el token d'usuari."), + ("Incorrect username or password.", "Nom d'usuari o contrasenya incorrecte"), + ("The user is not an administrator.", "Aquest usuari no és administrador"), + ("Failed to check if the user is an administrator.", "No s'ha pogut comprovar si l'usuari és administrador."), + ("Supported only in the installed version.", "Només compatible amb la versió instal·lada."), + ("elevation_username_tip", ""), + ("Preparing for installation ...", "Preparant per a l'instal·lació..."), + ("Show my cursor", "Mostra el meu punter"), + ("Scale custom", "Escala personalitzada"), + ("Custom scale slider", "Control lliscant d'escala personalitzada"), + ("Decrease", "Disminueix"), + ("Increase", "Augmenta"), + ("Show virtual mouse", "Mostra el ratolí virtual"), + ("Virtual mouse size", "Mida del ratolí virtual"), + ("Small", "Petita"), + ("Large", "Gran"), + ("Show virtual joystick", "Mostra el joystick virtual"), + ("Edit note", "Edita la nota"), + ("Alias", "Alias"), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Continua amb {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/cn.rs b/vendor/rustdesk/src/lang/cn.rs new file mode 100644 index 0000000..1ff10c4 --- /dev/null +++ b/vendor/rustdesk/src/lang/cn.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "状态"), + ("Your Desktop", "你的桌面"), + ("desk_tip", "你的桌面可以通过下面的 ID 和密码访问。"), + ("Password", "密码"), + ("Ready", "就绪"), + ("Established", "已建立"), + ("connecting_status", "正在接入 RustDesk 网络..."), + ("Enable service", "允许服务"), + ("Start service", "启动服务"), + ("Service is running", "服务正在运行"), + ("Service is not running", "服务未运行"), + ("not_ready_status", "未就绪,请检查网络连接"), + ("Control Remote Desktop", "控制远程桌面"), + ("Transfer file", "传输文件"), + ("Connect", "连接"), + ("Recent sessions", "最近访问过"), + ("Address book", "地址簿"), + ("Confirmation", "确认"), + ("TCP tunneling", "TCP 隧道"), + ("Remove", "删除"), + ("Refresh random password", "刷新随机密码"), + ("Set your own password", "设置密码"), + ("Enable keyboard/mouse", "允许控制键盘/鼠标"), + ("Enable clipboard", "允许同步剪贴板"), + ("Enable file transfer", "允许传输文件"), + ("Enable TCP tunneling", "允许建立 TCP 隧道"), + ("IP Whitelisting", "IP 白名单"), + ("ID/Relay Server", "ID/中继服务器"), + ("Import server config", "导入服务器配置"), + ("Export Server Config", "导出服务器配置"), + ("Import server configuration successfully", "导入服务器配置信息成功"), + ("Export server configuration successfully", "导出服务器配置信息成功"), + ("Invalid server configuration", "服务器配置无效,请修改后重新复制配置信息到剪贴板,然后点击此按钮"), + ("Clipboard is empty", "复制配置信息到剪贴板后点击此按钮,可以自动导入配置"), + ("Stop service", "停止服务"), + ("Change ID", "更改 ID"), + ("Your new ID", "你的新 ID"), + ("length %min% to %max%", "长度在 %min% 与 %max% 之间"), + ("starts with a letter", "以字母开头"), + ("allowed characters", "使用允许的字符"), + ("id_change_tip", "只可以使用字母 a-z, A-Z, 0-9, - (dash), _ (下划线)。首字母必须是 a-z, A-Z。长度在 6 与 16 之间。"), + ("Website", "网站"), + ("About", "关于"), + ("Slogan_tip", "在这个混乱的世界中,用心制作!"), + ("Privacy Statement", "隐私声明"), + ("Mute", "静音"), + ("Build Date", "构建日期"), + ("Version", "版本"), + ("Home", "主页"), + ("Audio Input", "音频输入"), + ("Enhancements", "增强功能"), + ("Hardware Codec", "硬件编解码"), + ("Adaptive bitrate", "自适应码率"), + ("ID Server", "ID 服务器"), + ("Relay Server", "中继服务器"), + ("API Server", "API 服务器"), + ("invalid_http", "必须以 http:// 或者 https:// 开头"), + ("Invalid IP", "无效 IP"), + ("Invalid format", "无效格式"), + ("server_not_support", "服务器暂不支持"), + ("Not available", "不可用"), + ("Too frequent", "修改太频繁,请稍后再试"), + ("Cancel", "取消"), + ("Skip", "跳过"), + ("Close", "关闭"), + ("Retry", "再试"), + ("OK", "确认"), + ("Password Required", "需要密码"), + ("Please enter your password", "请输入密码"), + ("Remember password", "记住密码"), + ("Wrong Password", "密码错误"), + ("Do you want to enter again?", "是否要再次输入?"), + ("Connection Error", "连接错误"), + ("Error", "错误"), + ("Reset by the peer", "连接被对方关闭"), + ("Connecting...", "正在连接..."), + ("Connection in progress. Please wait.", "正在进行连接,请稍候。"), + ("Please try 1 minute later", "一分钟后再试"), + ("Login Error", "登录错误"), + ("Successful", "成功"), + ("Connected, waiting for image...", "已连接,等待画面传输..."), + ("Name", "名称"), + ("Type", "类型"), + ("Modified", "修改时间"), + ("Size", "大小"), + ("Show Hidden Files", "显示隐藏文件"), + ("Receive", "接受"), + ("Send", "发送"), + ("Refresh File", "刷新文件"), + ("Local", "本地"), + ("Remote", "远程"), + ("Remote Computer", "远程电脑"), + ("Local Computer", "本地电脑"), + ("Confirm Delete", "确认删除"), + ("Delete", "删除"), + ("Properties", "属性"), + ("Multi Select", "多选"), + ("Select All", "全选"), + ("Unselect All", "取消全选"), + ("Empty Directory", "空文件夹"), + ("Not an empty directory", "这不是一个空文件夹"), + ("Are you sure you want to delete this file?", "是否删除此文件?"), + ("Are you sure you want to delete this empty directory?", "是否删除此空文件夹?"), + ("Are you sure you want to delete the file of this directory?", "是否删除此文件夹下的文件?"), + ("Do this for all conflicts", "应用于其它冲突"), + ("This is irreversible!", "此操作不可逆!"), + ("Deleting", "正在删除"), + ("files", "文件"), + ("Waiting", "正在等待..."), + ("Finished", "完成"), + ("Speed", "速度"), + ("Custom Image Quality", "设置画面质量"), + ("Privacy mode", "隐私模式"), + ("Block user input", "阻止用户输入"), + ("Unblock user input", "取消阻止用户输入"), + ("Adjust Window", "调节窗口"), + ("Original", "原始比例"), + ("Shrink", "收缩"), + ("Stretch", "伸展"), + ("Scrollbar", "滚动条"), + ("ScrollAuto", "自动滚动"), + ("Good image quality", "画质最优化"), + ("Balanced", "平衡"), + ("Optimize reaction time", "速度最优化"), + ("Custom", "自定义"), + ("Show remote cursor", "显示远程光标"), + ("Show quality monitor", "显示质量监测"), + ("Disable clipboard", "禁用剪贴板"), + ("Lock after session end", "会话结束后锁定远程电脑"), + ("Insert Ctrl + Alt + Del", "插入 Ctrl + Alt + Del"), + ("Insert Lock", "锁定远程电脑"), + ("Refresh", "刷新画面"), + ("ID does not exist", "ID 不存在"), + ("Failed to connect to rendezvous server", "连接注册服务器失败"), + ("Please try later", "请稍后再试"), + ("Remote desktop is offline", "远程电脑处于离线状态"), + ("Key mismatch", "Key 不匹配"), + ("Timeout", "连接超时"), + ("Failed to connect to relay server", "无法连接到中继服务器"), + ("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"), + ("Failed to connect via relay server", "无法通过中继服务器建立连接"), + ("Failed to make direct connection to remote desktop", "无法直接连接到远程桌面"), + ("Set Password", "设置密码"), + ("OS Password", "操作系统密码"), + ("install_tip", "你正在运行未安装版本,由于 UAC 限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), + ("Click to upgrade", "点击这里升级"), + ("Configure", "配置"), + ("config_acc", "为了能够远程控制你的桌面, 请给予 RustDesk \"辅助功能\" 权限。"), + ("config_screen", "为了能够远程访问你的桌面, 请给予 RustDesk \"屏幕录制\" 权限。"), + ("Installing ...", "安装中..."), + ("Install", "安装"), + ("Installation", "安装"), + ("Installation Path", "安装路径"), + ("Create start menu shortcuts", "创建启动菜单快捷方式"), + ("Create desktop icon", "创建桌面图标"), + ("agreement_tip", "开始安装即表示接受许可协议。"), + ("Accept and Install", "同意并安装"), + ("End-user license agreement", "用户协议"), + ("Generating ...", "正在生成..."), + ("Your installation is lower version.", "你安装的版本比当前运行的低。"), + ("not_close_tcp_tip", "请在使用隧道的时候,不要关闭本窗口"), + ("Listening ...", "正在等待隧道连接..."), + ("Remote Host", "远程主机"), + ("Remote Port", "远程端口"), + ("Action", "动作"), + ("Add", "添加"), + ("Local Port", "本地端口"), + ("Local Address", "当前地址"), + ("Change Local Port", "修改本地端口"), + ("setup_server_tip", "如果需要更快连接速度,你可以选择自建服务器"), + ("Too short, at least 6 characters.", "太短了,至少 6 个字符"), + ("The confirmation is not identical.", "两次输入不匹配"), + ("Permissions", "权限"), + ("Accept", "接受"), + ("Dismiss", "拒绝"), + ("Disconnect", "断开连接"), + ("Enable file copy and paste", "允许复制粘贴文件"), + ("Connected", "已连接"), + ("Direct and encrypted connection", "加密直连"), + ("Relayed and encrypted connection", "加密中继连接"), + ("Direct and unencrypted connection", "非加密直连"), + ("Relayed and unencrypted connection", "非加密中继连接"), + ("Enter Remote ID", "输入对方 ID"), + ("Enter your password", "输入密码"), + ("Logging in...", "正在登录..."), + ("Enable RDP session sharing", "允许 RDP 会话共享"), + ("Auto Login", "自动登录(设置断开后锁定才有效)"), + ("Enable direct IP access", "允许 IP 直接访问"), + ("Rename", "重命名"), + ("Space", "空格"), + ("Create desktop shortcut", "创建桌面快捷方式"), + ("Change Path", "更改路径"), + ("Create Folder", "创建文件夹"), + ("Please enter the folder name", "请输入文件夹名称"), + ("Fix it", "修复"), + ("Warning", "警告"), + ("Login screen using Wayland is not supported", "不支持使用 Wayland 登录界面"), + ("Reboot required", "重启后才能生效"), + ("Unsupported display server", "不支持当前显示服务器"), + ("x11 expected", "请切换到 x11"), + ("Port", "端口"), + ("Settings", "设置"), + ("Username", "用户名"), + ("Invalid port", "无效端口"), + ("Closed manually by the peer", "被对方手动关闭"), + ("Enable remote configuration modification", "允许远程修改配置"), + ("Run without install", "不安装直接运行"), + ("Connect via relay", "中继连接"), + ("Always connect via relay", "强制走中继连接"), + ("whitelist_tip", "只有白名单里的 IP 才能访问本机"), + ("Login", "登录"), + ("Verify", "验证"), + ("Remember me", "记住我"), + ("Trust this device", "信任此设备"), + ("Verification code", "验证码"), + ("verification_tip", "已向注册邮箱发送了登录验证码,请输入验证码继续登录"), + ("Logout", "登出"), + ("Tags", "标签"), + ("Search ID", "查找 ID"), + ("whitelist_sep", "可以使用逗号,分号,空格或者换行符作为分隔符"), + ("Add ID", "增加 ID"), + ("Add Tag", "增加标签"), + ("Unselect all tags", "取消选择所有标签"), + ("Network error", "网络错误"), + ("Username missed", "用户名没有填写"), + ("Password missed", "密码没有填写"), + ("Wrong credentials", "提供的登录信息错误"), + ("The verification code is incorrect or has expired", "验证码错误或已超时"), + ("Edit Tag", "修改标签"), + ("Forget Password", "忘记密码"), + ("Favorites", "收藏"), + ("Add to Favorites", "加入到收藏"), + ("Remove from Favorites", "从收藏中删除"), + ("Empty", "空空如也"), + ("Invalid folder name", "无效文件夹名称"), + ("Socks5 Proxy", "Socks5 代理"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) 代理"), + ("Discovered", "已发现"), + ("install_daemon_tip", "为了开机启动,请安装系统服务。"), + ("Remote ID", "远程 ID"), + ("Paste", "粘贴"), + ("Paste here?", "粘贴到这里?"), + ("Are you sure to close the connection?", "是否确认关闭连接?"), + ("Download new version", "下载新版本"), + ("Touch mode", "触屏模式"), + ("Mouse mode", "鼠标模式"), + ("One-Finger Tap", "单指轻触"), + ("Left Mouse", "鼠标左键"), + ("One-Long Tap", "单指长按"), + ("Two-Finger Tap", "双指轻触"), + ("Right Mouse", "鼠标右键"), + ("One-Finger Move", "单指移动"), + ("Double Tap & Move", "双击并移动"), + ("Mouse Drag", "鼠标选中拖动"), + ("Three-Finger vertically", "三指垂直滑动"), + ("Mouse Wheel", "鼠标滚轮"), + ("Two-Finger Move", "双指移动"), + ("Canvas Move", "移动画布"), + ("Pinch to Zoom", "双指缩放"), + ("Canvas Zoom", "缩放画布"), + ("Reset canvas", "重置画布"), + ("No permission of file transfer", "没有文件传输权限"), + ("Note", "备注"), + ("Connection", "连接"), + ("Share screen", "共享屏幕"), + ("Chat", "聊天消息"), + ("Total", "总计"), + ("items", "个项目"), + ("Selected", "已选择"), + ("Screen Capture", "屏幕录制"), + ("Input Control", "输入控制"), + ("Audio Capture", "音频录制"), + ("Do you accept?", "是否接受?"), + ("Open System Setting", "打开系统设置"), + ("How to get Android input permission?", "如何获取安卓的输入权限?"), + ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允许 RustDesk 使用\"无障碍\"服务。"), + ("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"), + ("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"), + ("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), + ("android_stop_service_tip", "关闭服务将自动关闭所有已建立的连接。"), + ("android_version_audio_tip", "当前安卓版本不支持音频录制,请升级至安卓 10 或更高。"), + ("android_start_service_tip", "点击开始服务或启用屏幕捕获权限,即可启动屏幕共享服务"), + ("android_permission_may_not_change_tip", "对于已建立的连接,权限可能不会立即发生改变,除非重新建立连接。"), + ("Account", "账户"), + ("Overwrite", "覆盖"), + ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), + ("Quit", "退出"), + ("Help", "帮助"), + ("Failed", "失败"), + ("Succeeded", "成功"), + ("Someone turns on privacy mode, exit", "其他用户使用隐私模式,退出"), + ("Unsupported", "不支持"), + ("Peer denied", "被控端拒绝"), + ("Please install plugins", "请安装插件"), + ("Peer exit", "被控端退出"), + ("Failed to turn off", "退出失败"), + ("Turned off", "退出"), + ("Language", "语言"), + ("Keep RustDesk background service", "保持 RustDesk 后台服务"), + ("Ignore Battery Optimizations", "忽略电池优化"), + ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的 RustDesk 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), + ("Start on boot", "开机自启动"), + ("Start the screen sharing service on boot, requires special permissions", "开机自动启动屏幕共享服务,此功能需要一些特殊权限。"), + ("Connection not allowed", "对方不允许连接"), + ("Legacy mode", "传统模式"), + ("Map mode", "1:1 传输"), + ("Translate mode", "翻译模式"), + ("Use permanent password", "使用固定密码"), + ("Use both passwords", "同时使用两种密码"), + ("Set permanent password", "设置固定密码"), + ("Enable remote restart", "允许远程重启"), + ("Restart remote device", "重启远程电脑"), + ("Are you sure you want to restart", "确定要重启"), + ("Restarting remote device", "正在重启远程设备"), + ("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"), + ("Copied", "已复制"), + ("Exit Fullscreen", "退出全屏"), + ("Fullscreen", "全屏"), + ("Mobile Actions", "移动端操作"), + ("Select Monitor", "选择监视器"), + ("Control Actions", "控制操作"), + ("Display Settings", "显示设置"), + ("Ratio", "比例"), + ("Image Quality", "画质"), + ("Scroll Style", "滚屏方式"), + ("Show Toolbar", "显示工具栏"), + ("Hide Toolbar", "隐藏工具栏"), + ("Direct Connection", "直接连接"), + ("Relay Connection", "中继连接"), + ("Secure Connection", "安全连接"), + ("Insecure Connection", "非安全连接"), + ("Scale original", "原始尺寸"), + ("Scale adaptive", "适应窗口"), + ("General", "常规"), + ("Security", "安全"), + ("Theme", "主题"), + ("Dark Theme", "暗黑主题"), + ("Light Theme", "明亮主题"), + ("Dark", "黑暗"), + ("Light", "明亮"), + ("Follow System", "跟随系统"), + ("Enable hardware codec", "启用硬件编解码"), + ("Unlock Security Settings", "解锁安全设置"), + ("Enable audio", "允许传输音频"), + ("Unlock Network Settings", "解锁网络设置"), + ("Server", "服务器"), + ("Direct IP Access", "IP 直接访问"), + ("Proxy", "代理"), + ("Apply", "应用"), + ("Disconnect all devices?", "断开所有远程连接?"), + ("Clear", "清空"), + ("Audio Input Device", "音频输入设备"), + ("Use IP Whitelisting", "只允许白名单上的 IP 访问"), + ("Network", "网络"), + ("Pin Toolbar", "固定工具栏"), + ("Unpin Toolbar", "取消固定工具栏"), + ("Recording", "录屏"), + ("Directory", "目录"), + ("Automatically record incoming sessions", "自动录制传入会话"), + ("Automatically record outgoing sessions", "自动录制传出会话"), + ("Change", "更改"), + ("Start session recording", "开始录屏"), + ("Stop session recording", "结束录屏"), + ("Enable recording session", "允许录制会话"), + ("Enable LAN discovery", "允许局域网发现"), + ("Deny LAN discovery", "拒绝局域网发现"), + ("Write a message", "输入聊天消息"), + ("Prompt", "提示"), + ("Please wait for confirmation of UAC...", "请等待对方确认 UAC..."), + ("elevated_foreground_window_tip", "远端桌面的当前窗口需要更高的权限才能操作, 暂时无法使用鼠标键盘, 可以请求对方最小化当前窗口, 或者在连接管理窗口点击提升。为避免这个问题,建议在远端设备上安装本软件。"), + ("Disconnected", "会话已结束"), + ("Other", "其他"), + ("Confirm before closing multiple tabs", "关闭多个标签页时向您确认"), + ("Keyboard Settings", "键盘设置"), + ("Full Access", "完全访问"), + ("Screen Share", "仅共享屏幕"), + ("ubuntu-21-04-required", "Wayland 需要 Ubuntu 21.04 或更高版本。"), + ("wayland-requires-higher-linux-version", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"), + ("xdp-portal-unavailable", ""), + ("JumpLink", "查看"), + ("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"), + ("Show RustDesk", "显示 RustDesk"), + ("This PC", "此电脑"), + ("or", "或"), + ("Elevate", "提权"), + ("Zoom cursor", "缩放光标"), + ("Accept sessions via password", "只允许密码访问"), + ("Accept sessions via click", "只允许点击访问"), + ("Accept sessions via both", "允许密码或点击访问"), + ("Please wait for the remote side to accept your session request...", "请等待对方接受你的连接..."), + ("One-time Password", "一次性密码"), + ("Use one-time password", "使用一次性密码"), + ("One-time password length", "一次性密码长度"), + ("Request access to your device", "请求访问你的设备"), + ("Hide connection management window", "隐藏连接管理窗口"), + ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), + ("wayland_experiment_tip", "Wayland 支持处于实验阶段,如果你需要使用无人值守访问,请使用 X11。"), + ("Right click to select tabs", "右键选择选项卡"), + ("Skipped", "已跳过"), + ("Add to address book", "添加到地址簿"), + ("Group", "小组"), + ("Search", "搜索"), + ("Closed manually by web console", "被 web 控制台手动关闭"), + ("Local keyboard type", "本地键盘类型"), + ("Select local keyboard type", "请选择本地键盘类型"), + ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), + ("Always use software rendering", "始终使用软件渲染"), + ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), + ("config_microphone", "为了支持通过麦克风进行音频传输,请给予 RustDesk \"录音\"权限。"), + ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), + ("Wait", "等待"), + ("Elevation Error", "提权失败"), + ("Ask the remote user for authentication", "请求远端用户授权"), + ("Choose this if the remote account is administrator", "当对面电脑是管理员账号时选择该选项"), + ("Transmit the username and password of administrator", "发送管理员账号的用户名密码"), + ("still_click_uac_tip", "依然需要被控端用户在运行 RustDesk 的 UAC 窗口点击确认。"), + ("Request Elevation", "请求提权"), + ("wait_accept_uac_tip", "请等待远端用户确认 UAC 对话框。"), + ("Elevate successfully", "提权成功"), + ("uppercase", "大写字母"), + ("lowercase", "小写字母"), + ("digit", "数字"), + ("special character", "特殊字符"), + ("length>=8", "长度不小于 8"), + ("Weak", "弱"), + ("Medium", "中"), + ("Strong", "强"), + ("Switch Sides", "反转访问方向"), + ("Please confirm if you want to share your desktop?", "请确认是否要让对方访问你的桌面?"), + ("Display", "显示"), + ("Default View Style", "默认显示方式"), + ("Default Scroll Style", "默认滚动方式"), + ("Default Image Quality", "默认图像质量"), + ("Default Codec", "默认编解码"), + ("Bitrate", "码率"), + ("FPS", "帧率"), + ("Auto", "自动"), + ("Other Default Options", "其它默认选项"), + ("Voice call", "语音通话"), + ("Text chat", "文字聊天"), + ("Stop voice call", "停止语音通话"), + ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,如果最近访问里存在该卡片,也可以在卡片选项里选择强制走中继连接。"), + ("Reconnect", "重连"), + ("Codec", "编解码"), + ("Resolution", "分辨率"), + ("No transfers in progress", "无进行中的传输"), + ("Set one-time password length", "设置一次性密码长度"), + ("RDP Settings", "RDP 设置"), + ("Sort by", "排序方式"), + ("New Connection", "新连接"), + ("Restore", "恢复"), + ("Minimize", "最小化"), + ("Maximize", "最大化"), + ("Your Device", "你的设备"), + ("empty_recent_tip", "无最近会话,是时候开始新会话了!"), + ("empty_favorite_tip", "还没有收藏的被控端?找一个人连接并将其添加到收藏吧!"), + ("empty_lan_tip", "情况不妙,似乎未发现任何被控端!"), + ("empty_address_book_tip", "似乎目前地址簿内无被控端"), + ("Empty Username", "空用户名"), + ("Empty Password", "空密码"), + ("Me", "我"), + ("identical_file_tip", "此文件与对方的一致"), + ("show_monitors_tip", "在工具栏上显示监视器"), + ("View Mode", "浏览模式"), + ("login_linux_tip", "登录被控端的 Linux 账户,才能启用 X 桌面"), + ("verify_rustdesk_password_tip", "验证 RustDesk 密码"), + ("remember_account_tip", "记住此账户"), + ("os_account_desk_tip", "在无显示器的环境下,此账户用于登录被控系统,并启用桌面"), + ("OS Account", "系统账户"), + ("another_user_login_title_tip", "其他用户已登录"), + ("another_user_login_text_tip", "断开"), + ("xorg_not_found_title_tip", "Xorg 未安装"), + ("xorg_not_found_text_tip", "请安装 Xorg"), + ("no_desktop_title_tip", "desktop 未安装"), + ("no_desktop_text_tip", "请安装 desktop"), + ("No need to elevate", "无需提升权限"), + ("System Sound", "系统音频"), + ("Default", "默认"), + ("New RDP", "新 RDP 连接"), + ("Fingerprint", "指纹"), + ("Copy Fingerprint", "复制指纹"), + ("no fingerprints", "没有指纹"), + ("Select a peer", "选择一个被控端"), + ("Select peers", "选择被控"), + ("Plugins", "插件"), + ("Uninstall", "卸载"), + ("Update", "更新"), + ("Enable", "启用"), + ("Disable", "禁用"), + ("Options", "选项"), + ("resolution_original_tip", "原始分辨率"), + ("resolution_fit_local_tip", "适应本地分辨率"), + ("resolution_custom_tip", "自定义分辨率"), + ("Collapse toolbar", "折叠工具栏"), + ("Accept and Elevate", "接受并提权"), + ("accept_and_elevate_btn_tooltip", "接受连接并提升 UAC 权限"), + ("clipboard_wait_response_timeout_tip", "等待拷贝响应超时"), + ("Incoming connection", "收到的连接"), + ("Outgoing connection", "发起的连接"), + ("Exit", "退出"), + ("Open", "打开"), + ("logout_tip", "确定要退出登录吗?"), + ("Service", "服务"), + ("Start", "启动"), + ("Stop", "停止"), + ("exceed_max_devices", "管理的设备数已达到最大值"), + ("Sync with recent sessions", "同步最近会话"), + ("Sort tags", "对标签进行排序"), + ("Open connection in new tab", "在选项卡中打开新连接"), + ("Move tab to new window", "将标签页移至新窗口"), + ("Can not be empty", "不能为空"), + ("Already exists", "已经存在"), + ("Change Password", "更改密码"), + ("Refresh Password", "刷新密码"), + ("ID", "ID"), + ("Grid View", "网格视图"), + ("List View", "列表视图"), + ("Select", "选择"), + ("Toggle Tags", "切换标签"), + ("pull_ab_failed_tip", "获取地址簿失败"), + ("push_ab_failed_tip", "上传地址簿失败"), + ("synced_peer_readded_tip", "最近会话中存在的设备将会被重新同步到地址簿。"), + ("Change Color", "更改颜色"), + ("Primary Color", "基本色"), + ("HSV Color", "HSV 色"), + ("Installation Successful!", "安装成功!"), + ("Installation failed!", "安装失败!"), + ("Reverse mouse wheel", "鼠标滚轮反向"), + ("{} sessions", "{} 个会话"), + ("scam_title", "你可能被骗了!"), + ("scam_text1", "如果你正在和素不相识的人通话,而对方要求你使用 RustDesk 启动服务,请勿继续操作并立刻挂断电话。"), + ("scam_text2", "他们很可能是骗子,试图窃取您的钱财或其他个人信息。"), + ("Don't show again", "下次不再显示"), + ("I Agree", "同意"), + ("Decline", "拒绝"), + ("Timeout in minutes", "超时(分钟)"), + ("auto_disconnect_option_tip", "自动关闭不活跃的会话"), + ("Connection failed due to inactivity", "由于长时间无操作, 连接被自动断开"), + ("Check for software update on startup", "启动时检查软件更新"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "请升级专业版服务器到{}或更高版本!"), + ("pull_group_failed_tip", "获取组信息失败"), + ("Filter by intersection", "按交集过滤"), + ("Remove wallpaper during incoming sessions", "接受会话时移除桌面壁纸"), + ("Test", "测试"), + ("display_is_plugged_out_msg", "显示器被拔出,切换到第一个显示器。"), + ("No displays", "没有显示器。"), + ("Open in new window", "在新的窗口中打开"), + ("Show displays as individual windows", "在单个窗口中打开显示器"), + ("Use all my displays for the remote session", "将我的所有显示器用于远程会话"), + ("selinux_tip", "SELinux 处于启用状态,RustDesk 可能无法作为被控正常运行。"), + ("Change view", "更改视图"), + ("Big tiles", "大磁贴"), + ("Small tiles", "小磁贴"), + ("List", "列表"), + ("Virtual display", "虚拟显示器"), + ("Plug out all", "拔出所有"), + ("True color (4:4:4)", "真彩模式(4:4:4)"), + ("Enable blocking user input", "允许阻止用户输入"), + ("id_input_tip", "可以输入 ID、直连 IP,或域名和端口号(<域名>:<端口号>)。\n要访问另一台服务器上的设备,请附加服务器地址(@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"@public\",无需密钥。\n\n如果您想要在首次连接时,强制走中继连接,请在 ID 的后面添加 \"/r\",例如,\"9123456234/r\"。"), + ("privacy_mode_impl_mag_tip", "模式 1"), + ("privacy_mode_impl_virtual_display_tip", "模式 2"), + ("Enter privacy mode", "进入隐私模式"), + ("Exit privacy mode", "退出隐私模式"), + ("idd_not_support_under_win10_2004_tip", "不支持 Indirect display driver 。需要 Windows 10 版本 2004 及更高的版本。"), + ("input_source_1_tip", "输入源 1"), + ("input_source_2_tip", "输入源 2"), + ("Swap control-command key", "交换 Control 键和 Command 键"), + ("swap-left-right-mouse", "交换鼠标左右键"), + ("2FA code", "双重认证代码"), + ("More", "更多"), + ("enable-2fa-title", "启用双重认证"), + ("enable-2fa-desc", "现在请设置身份验证器。您可以在手机或台式电脑上使用 Authy、Microsoft 或 Google Authenticator 等验证器。用验证器扫描二维码,然后输入显示的验证码以启用双重认证。"), + ("wrong-2fa-code", "无法验证此验证码。请检查验证码和本地时间设置是否正确。"), + ("enter-2fa-title", "双重认证"), + ("Email verification code must be 6 characters.", "Email 验证码必须是 6 个字符。"), + ("2FA code must be 6 digits.", "双重认证代码必须是 6 位数字。"), + ("Multiple Windows sessions found", "发现多个 Windows 会话"), + ("Please select the session you want to connect to", "请选择您要连接的会话"), + ("powered_by_me", "由 RustDesk 提供支持"), + ("outgoing_only_desk_tip", "当前版本的软件是定制版本。\n您可以连接至其他设备,但是其他设备无法连接至您的设备。"), + ("preset_password_warning", "此定制版本附有预设密码。 任何知晓此密码的人都能完全控制您的设备。如果这不是您所预期的,请立即卸载此软件。"), + ("Security Alert", "安全警告"), + ("My address book", "我的地址簿"), + ("Personal", "个人的"), + ("Owner", "所有者"), + ("Set shared password", "设置共享密码"), + ("Exist in", "存在于"), + ("Read-only", "只读"), + ("Read/Write", "读写"), + ("Full Control", "完全控制"), + ("share_warning_tip", "上述的字段為共享且对其他人可见。"), + ("Everyone", "所有人"), + ("ab_web_console_tip", "打开 Web 控制台以执行更多操作"), + ("allow-only-conn-window-open-tip", "仅当 RustDesk 窗口打开时允许连接"), + ("no_need_privacy_mode_no_physical_displays_tip", "没有物理显示器,没必要使用隐私模式。"), + ("Follow remote cursor", "跟随远程光标"), + ("Follow remote window focus", "跟随远程窗口焦点"), + ("default_proxy_tip", "默认代理协议及端口为 Socks5 和 1080"), + ("no_audio_input_device_tip", "未找到音频输入设备"), + ("Incoming", "被控"), + ("Outgoing", "主控"), + ("Clear Wayland screen selection", "清除 Wayland 的屏幕选择"), + ("clear_Wayland_screen_selection_tip", "清除 Wayland 的屏幕选择后,您可以重新选择分享的屏幕。"), + ("confirm_clear_Wayland_screen_selection_tip", "是否确认清除 Wayland 的分享屏幕选择?"), + ("android_new_voice_call_tip", "收到新的语音呼叫请求。如果您接受,音频将切换为语音通信。"), + ("texture_render_tip", "使用纹理渲染,使图片更加流畅。 如果您遭遇渲染问题,可尝试关闭此选项。"), + ("Use texture rendering", "使用纹理渲染"), + ("Floating window", "悬浮窗"), + ("floating_window_tip", "有助于保持 RustDesk 后台服务"), + ("Keep screen on", "保持屏幕开启"), + ("Never", "从不"), + ("During controlled", "被控期间"), + ("During service is on", "服务开启期间"), + ("Capture screen using DirectX", "使用 DirectX 捕获屏幕"), + ("Back", "回退"), + ("Apps", "应用"), + ("Volume up", "提升音量"), + ("Volume down", "降低音量"), + ("Power", "电源"), + ("Telegram bot", "Telegram 机器人"), + ("enable-bot-tip", "如果您启用此功能,您可以从您的机器人接收双重认证码,亦可作为连线通知之用。"), + ("enable-bot-desc", "1. 开启与 @BotFather 的对话。\n2. 发送命令 \"/newbot\"。 您将在完成此步骤后收到权杖 (Token)。\n3. 开始与您刚创建的机器人的对话。发送一则以正斜杠 (\"/\") 开头的消息来启用它,例如 \"/hello\"。"), + ("cancel-2fa-confirm-tip", "确定要取消双重认证吗?"), + ("cancel-bot-confirm-tip", "确定要取消 Telegram 机器人吗?"), + ("About RustDesk", "关于 RustDesk"), + ("Send clipboard keystrokes", "发送剪贴板按键"), + ("network_error_tip", "请检查网络连接,然后点击再试"), + ("Unlock with PIN", "使用 PIN 码解锁设置"), + ("Requires at least {} characters", "不少于{}个字符"), + ("Wrong PIN", "PIN 码错误"), + ("Set PIN", "设置 PIN 码"), + ("Enable trusted devices", "启用信任设备"), + ("Manage trusted devices", "管理信任设备"), + ("Platform", "平台"), + ("Days remaining", "剩余天数"), + ("enable-trusted-devices-tip", "允许受信任的设备跳过 2FA 验证"), + ("Parent directory", "父目录"), + ("Resume", "继续"), + ("Invalid file name", "无效文件名"), + ("one-way-file-transfer-tip", "被控端启用了单向文件传输"), + ("Authentication Required", "需要身份验证"), + ("Authenticate", "认证"), + ("web_id_input_tip", "可以输入同一个服务器内的 ID,web 客户端不支持直接 IP 访问。\n要访问另一台服务器上的设备,请附加服务器地址(@<服务器地址>?key=<密钥>)。比如,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=。\n要访问公共服务器上的设备,请输入 \"@public\",无需密钥。"), + ("Download", "下载"), + ("Upload folder", "上传文件夹"), + ("Upload files", "上传文件"), + ("Clipboard is synchronized", "剪贴板已同步"), + ("Update client clipboard", "更新客户端的剪贴板"), + ("Untagged", "无标签"), + ("new-version-of-{}-tip", "{} 版本更新"), + ("Accessible devices", "可访问的设备"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "请在远程端将 RustDesk 客户端升级至版本 {} 或更新版本!"), + ("d3d_render_tip", "当启用 D3D 渲染时,某些机器可能无法显示远程画面。"), + ("Use D3D rendering", "使用 D3D 渲染"), + ("Printer", "打印机"), + ("printer-os-requirement-tip", "打印机的传出功能需要 Windows 10 或更高版本。"), + ("printer-requires-installed-{}-client-tip", "请先安装 {} 客户端。"), + ("printer-{}-not-installed-tip", "未安装 {} 打印机。"), + ("printer-{}-ready-tip", "{} 打印机已安装,您可以使用打印功能了。"), + ("Install {} Printer", "安装 {} 打印机"), + ("Outgoing Print Jobs", "传出的打印任务"), + ("Incoming Print Jobs", "传入的打印任务"), + ("Incoming Print Job", "传入的打印任务"), + ("use-the-default-printer-tip", "使用默认的打印机执行"), + ("use-the-selected-printer-tip", "使用选择的打印机执行"), + ("auto-print-tip", "使用选择的打印机自动执行"), + ("print-incoming-job-confirm-tip", "您收到一个远程打印任务,您想在本地执行它吗?"), + ("remote-printing-disallowed-tile-tip", "不允许远程打印"), + ("remote-printing-disallowed-text-tip", "被控端的权限设置拒绝了远程打印。"), + ("save-settings-tip", "保存设置"), + ("dont-show-again-tip", "不再显示此信息"), + ("Take screenshot", "截屏"), + ("Taking screenshot", "正在截屏"), + ("screenshot-merged-screen-not-supported-tip", "当前不支持多个屏幕的合并截屏,请切换到单个屏幕重试。"), + ("screenshot-action-tip", "请选择如何继续截屏。"), + ("Save as", "另存为"), + ("Copy to clipboard", "复制到剪贴板"), + ("Enable remote printer", "启用远程打印机"), + ("Downloading {}", "正在下载 {}"), + ("{} Update", "{} 更新"), + ("{}-to-update-tip", "即将关闭 {} ,并安装新版本。"), + ("download-new-version-failed-tip", "下载失败,您可以重试或者点击\"下载\"按钮,从发布网址下载,并手动升级。"), + ("Auto update", "自动更新"), + ("update-failed-check-msi-tip", "安装方式检测失败。请点击\"下载\"按钮,从发布网址下载,并手动升级。"), + ("websocket_tip", "使用 WebSocket 时,仅支持中继连接。"), + ("Use WebSocket", "使用 WebSocket"), + ("Trackpad speed", "触控板速度"), + ("Default trackpad speed", "默认触控板速度"), + ("Numeric one-time password", "一次性密码为数字"), + ("Enable IPv6 P2P connection", "启用 IPv6 P2P 连接"), + ("Enable UDP hole punching", "启用 UDP 打洞"), + ("View camera", "查看摄像头"), + ("Enable camera", "允许查看摄像头"), + ("No cameras", "没有摄像头"), + ("view_camera_unsupported_tip", "您的远程端不支持查看摄像头。"), + ("Terminal", "终端"), + ("Enable terminal", "启用终端"), + ("New tab", "新建选项卡"), + ("Keep terminal sessions on disconnect", "断开连接时保持终端会话"), + ("Terminal (Run as administrator)", "终端(以管理员身份运行)"), + ("terminal-admin-login-tip", "请输入被控端的管理员账号密码。"), + ("Failed to get user token.", "获取用户令牌时出错。"), + ("Incorrect username or password.", "用户名或密码不正确。"), + ("The user is not an administrator.", "用户不是管理员。"), + ("Failed to check if the user is an administrator.", "检查用户是否为管理员时出错。"), + ("Supported only in the installed version.", "仅在以安装版本受支持。"), + ("elevation_username_tip", "输入用户名或域名\\用户名"), + ("Preparing for installation ...", "准备安装..."), + ("Show my cursor", "显示我的光标"), + ("Scale custom", "自定义缩放"), + ("Custom scale slider", "自定义缩放滑块"), + ("Decrease", "缩小"), + ("Increase", "放大"), + ("Show virtual mouse", "显示虚拟鼠标"), + ("Virtual mouse size", "虚拟鼠标大小"), + ("Small", "小"), + ("Large", "大"), + ("Show virtual joystick", "显示虚拟摇杆"), + ("Edit note", "编辑备注"), + ("Alias", "别名"), + ("ScrollEdge", "边缘滚动"), + ("Allow insecure TLS fallback", "允许回退到不安全的 TLS 连接"), + ("allow-insecure-tls-fallback-tip", "默认情况下,对于使用 TLS 的协议,RustDesk 会验证服务器证书。\n启用此选项后,在验证失败时,RustDesk 将转为跳过验证步骤并继续连接。"), + ("Disable UDP", "禁用 UDP"), + ("disable-udp-tip", "控制是否仅使用 TCP。\n启用此选项后,RustDesk 将不再使用 UDP 21116,而是使用 TCP 21116。"), + ("server-oss-not-support-tip", "注意:RustDesk 开源服务器 (OSS server) 不包含此功能。"), + ("input note here", "输入备注"), + ("note-at-conn-end-tip", "在连接结束时请求备注"), + ("Show terminal extra keys", "显示终端扩展键"), + ("Relative mouse mode", "相对鼠标模式"), + ("rel-mouse-not-supported-peer-tip", "被控端不支持相对鼠标模式"), + ("rel-mouse-not-ready-tip", "相对鼠标模式尚未准备好,请稍后再试"), + ("rel-mouse-lock-failed-tip", "无法锁定鼠标,相对鼠标模式已禁用"), + ("rel-mouse-exit-{}-tip", "按下 {} 退出"), + ("rel-mouse-permission-lost-tip", "键盘权限被撤销。相对鼠标模式已被禁用。"), + ("Changelog", "更新日志"), + ("keep-awake-during-outgoing-sessions-label", "传出会话期间保持屏幕常亮"), + ("keep-awake-during-incoming-sessions-label", "传入会话期间保持屏幕常亮"), + ("Continue with {}", "使用 {} 登录"), + ("Display Name", "显示名称"), + ("password-hidden-tip", "永久密码已设置(已隐藏)"), + ("preset-password-in-use-tip", "当前使用预设密码"), + ("Enable privacy mode", "允许隐私模式"), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/cs.rs b/vendor/rustdesk/src/lang/cs.rs new file mode 100644 index 0000000..2b9c621 --- /dev/null +++ b/vendor/rustdesk/src/lang/cs.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stav"), + ("Your Desktop", "Vaše plocha"), + ("desk_tip", "Pomocí tohoto ID a hesla lze přistupovat k pracovní ploše."), + ("Password", "Heslo"), + ("Ready", "Připraveno"), + ("Established", "Navázáno"), + ("connecting_status", "Připojování k RustDesk síti..."), + ("Enable service", "Povolit službu"), + ("Start service", "Spustit službu"), + ("Service is running", "Služba je spuštěná"), + ("Service is not running", "Služba není spuštěná"), + ("not_ready_status", "Nepřipraveno. Zkontrolujte své připojení."), + ("Control Remote Desktop", "Ovládat vzdálenou plochu"), + ("Transfer file", "Přenos souborů"), + ("Connect", "Připojit"), + ("Recent sessions", "Nedávné relace"), + ("Address book", "Adresář kontaktů"), + ("Confirmation", "Potvrzení"), + ("TCP tunneling", "TCP tunelování"), + ("Remove", "Odebrat"), + ("Refresh random password", "Vytvořit nové náhodné heslo"), + ("Set your own password", "Nastavte si své vlastní heslo"), + ("Enable keyboard/mouse", "Povolit klávesnici/myš"), + ("Enable clipboard", "Povolit schránku"), + ("Enable file transfer", "Povolit přenos souborů"), + ("Enable TCP tunneling", "Povolit TCP tunelování"), + ("IP Whitelisting", "Povolování pouze z daných IP adres"), + ("ID/Relay Server", "ID/předávací server"), + ("Import server config", "Importovat konfiguraci serveru"), + ("Export Server Config", "Exportovat konfiguraci serveru"), + ("Import server configuration successfully", "Konfigurace serveru úspěšně importována"), + ("Export server configuration successfully", "Konfigurace serveru úspěšně exportována"), + ("Invalid server configuration", "Neplatná konfigurace serveru"), + ("Clipboard is empty", "Schránka je prázdná"), + ("Stop service", "Zastavit službu"), + ("Change ID", "Změnit ID"), + ("Your new ID", "Vaše nové ID"), + ("length %min% to %max%", "délka mezi %min% a %max%"), + ("starts with a letter", "začíná písmenem"), + ("allowed characters", "povolené znaky"), + ("id_change_tip", "Použít je možné pouze znaky a-z, A-Z, 0-9, - (dash) a _ (podtržítko). Dále je třeba aby začínalo písmenem a-z, A-Z. Délka mezi 6 a 16 znaky."), + ("Website", "Webové stránky"), + ("About", "O aplikaci"), + ("Slogan_tip", "Vytvořeno srdcem v tomto chaotickém světě!"), + ("Privacy Statement", "Prohlášení o ochraně osobních údajů"), + ("Mute", "Ztlumit zvuk"), + ("Build Date", "Datum sestavení"), + ("Version", "Verze"), + ("Home", "Domů"), + ("Audio Input", "Vstup zvuku"), + ("Enhancements", "Vylepšení"), + ("Hardware Codec", "Hardwarový kodek"), + ("Adaptive bitrate", "Adaptivní datový tok"), + ("ID Server", "ID Server"), + ("Relay Server", "Předávací server"), + ("API Server", "API Server"), + ("invalid_http", "Je třeba, aby začínalo na http:// nebo https://"), + ("Invalid IP", "Neplatná IP"), + ("Invalid format", "Neplatný formát"), + ("server_not_support", "Server zatím nepodporuje"), + ("Not available", "Není k dispozici"), + ("Too frequent", "Příliš časté"), + ("Cancel", "Storno"), + ("Skip", "Přeskočit"), + ("Close", "Zavřít"), + ("Retry", "Zkusit znovu"), + ("OK", "OK"), + ("Password Required", "Vyžadováno heslo"), + ("Please enter your password", "Zadejte své heslo"), + ("Remember password", "Zapamatovat heslo"), + ("Wrong Password", "Nesprávné heslo"), + ("Do you want to enter again?", "Chcete se znovu připojit?"), + ("Connection Error", "Chyba spojení"), + ("Error", "Chyba"), + ("Reset by the peer", "Resetováno protistranou"), + ("Connecting...", "Připojování..."), + ("Connection in progress. Please wait.", "Probíhá připojování, vyčkejte prosím."), + ("Please try 1 minute later", "Zkuste to prosím o 1 minutu později"), + ("Login Error", "Chyba přihlášení se"), + ("Successful", "Úspěšné"), + ("Connected, waiting for image...", "Připojeno, čeká se na obraz..."), + ("Name", "Název"), + ("Type", "Typ"), + ("Modified", "Změněno"), + ("Size", "Velikost"), + ("Show Hidden Files", "Zobrazit skryté soubory"), + ("Receive", "Přijmout"), + ("Send", "Odeslat"), + ("Refresh File", "Znovu načíst soubor"), + ("Local", "Místní"), + ("Remote", "Vzdálené"), + ("Remote Computer", "Vzdálený počítač"), + ("Local Computer", "Místní počítač"), + ("Confirm Delete", "Potvrdit smazání"), + ("Delete", "Smazat"), + ("Properties", "Vlastnosti"), + ("Multi Select", "Vícenásobný výběr"), + ("Select All", "Vybrat vše"), + ("Unselect All", "Zrušit výběr všech"), + ("Empty Directory", "Prázdná složka"), + ("Not an empty directory", "Neprázdná složka"), + ("Are you sure you want to delete this file?", "Opravdu chcete tento soubor vymazat?"), + ("Are you sure you want to delete this empty directory?", "Opravdu chcete tuto prázdnou složku smazat?"), + ("Are you sure you want to delete the file of this directory?", "Opravdu chcete vymazat soubor z této složky?"), + ("Do this for all conflicts", "Naložit takto se všemi konflikty"), + ("This is irreversible!", "Toto nelze vzít zpět"), + ("Deleting", "Mazání"), + ("files", "soubory"), + ("Waiting", "Čeká se"), + ("Finished", "Dokončeno"), + ("Speed", "Rychlost"), + ("Custom Image Quality", "Uživatelsky určená kvalita obrazu"), + ("Privacy mode", "Režim ochrany soukromí"), + ("Block user input", "Blokovat vstupní zařízení uživatele"), + ("Unblock user input", "Odblokovat vstupní zařízení uživatele"), + ("Adjust Window", "Přizpůsobit velikost okna"), + ("Original", "Původní"), + ("Shrink", "Oříznout"), + ("Stretch", "Roztáhnout"), + ("Scrollbar", "Posuvník"), + ("ScrollAuto", "Automatické rolování"), + ("Good image quality", "Dobrá kvalita obrazu"), + ("Balanced", "Vyvážená"), + ("Optimize reaction time", "Optimalizovat reakční dobu"), + ("Custom", "Vlastní"), + ("Show remote cursor", "Zobrazit vzdálený kurzor"), + ("Show quality monitor", "Zobrazit monitor kvality"), + ("Disable clipboard", "Vypnout schránku"), + ("Lock after session end", "Po ukončení relace zamknout plochu"), + ("Insert Ctrl + Alt + Del", "Vložit Ctrl + Alt + Del"), + ("Insert Lock", "Zamknout"), + ("Refresh", "Načíst znovu"), + ("ID does not exist", "Toto ID neexistuje"), + ("Failed to connect to rendezvous server", "Nepodařilo se připojit ke zprostředkovávajícímu serveru"), + ("Please try later", "Zkuste to později"), + ("Remote desktop is offline", "Vzdálená plocha není připojená ke službě"), + ("Key mismatch", "Neshoda klíčů"), + ("Timeout", "Překročen časový limit pro navázání spojení"), + ("Failed to connect to relay server", "Nepodařilo se připojit k předávacímu serveru"), + ("Failed to connect via rendezvous server", "Nepodařilo se připojit prostřednictvím zprostředkovávajícího serveru"), + ("Failed to connect via relay server", "Nepodařilo se připojit prostřednictvím předávacího serveru"), + ("Failed to make direct connection to remote desktop", "Nepodařilo s navázat přímé připojení ke vzdálené ploše"), + ("Set Password", "Nastavit heslo"), + ("OS Password", "Heslo do operačního systému"), + ("install_tip", "Kvůli řízení oprávnění v systému (UAC), RustDesk v některých případech na protistraně nefunguje správně. Abyste se UAC vyhnuli, klikněte na níže uvedené tlačítko a nainstalujte tak RustDesk do systému."), + ("Click to upgrade", "Aktualizovat"), + ("Configure", "Nastavit"), + ("config_acc", "Aby bylo možné na dálku ovládat vaši plochu, je třeba aplikaci RustDesk udělit oprávnění pro \"Zpřístupnění pro hendikepované\"."), + ("config_screen", "Aby bylo možné přistupovat k vaší ploše na dálku, je třeba aplikaci RustDesk udělit oprávnění pro \"Nahrávání obsahu obrazovky\"."), + ("Installing ...", "Instaluje se ..."), + ("Install", "Nainstalovat"), + ("Installation", "Instalace"), + ("Installation Path", "Umístění instalace"), + ("Create start menu shortcuts", "Vytvořit zástupce v nabídce Start"), + ("Create desktop icon", "Vytvořit ikonu na ploše"), + ("agreement_tip", "Spuštěním instalace přijímáte licenční ujednání."), + ("Accept and Install", "Přijmout a nainstalovat"), + ("End-user license agreement", "Licenční ujednání s koncovým uživatelem"), + ("Generating ...", "Vytváření ..."), + ("Your installation is lower version.", "Máte nainstalovanou starší verzi"), + ("not_close_tcp_tip", "Po dobu, po kterou tunel potřebujete, nezavírejte toto okno"), + ("Listening ...", "Očekávaní spojení ..."), + ("Remote Host", "Vzdálený hostitel"), + ("Remote Port", "Vzdálený port"), + ("Action", "Akce"), + ("Add", "Přidat"), + ("Local Port", "Místní port"), + ("Local Address", "Místní adresa"), + ("Change Local Port", "Změnit místní port"), + ("setup_server_tip", "Rychlejší připojení získáte vytvořením si svého vlastního serveru"), + ("Too short, at least 6 characters.", "Příliš krátké, alespoň 6 znaků."), + ("The confirmation is not identical.", "Kontrolní zadání se neshoduje."), + ("Permissions", "Oprávnění"), + ("Accept", "Přijmout"), + ("Dismiss", "Zahodit"), + ("Disconnect", "Odpojit"), + ("Enable file copy and paste", "Povolit kopírování a vkládání souborů"), + ("Connected", "Připojeno"), + ("Direct and encrypted connection", "Přímé a šifrované spojení"), + ("Relayed and encrypted connection", "Předávané a šifrované spojení"), + ("Direct and unencrypted connection", "Přímé a nešifrované spojení"), + ("Relayed and unencrypted connection", "Předávané a nešifrované spojení"), + ("Enter Remote ID", "Zadejte ID protistrany"), + ("Enter your password", "Zadejte své heslo"), + ("Logging in...", "Přihlašování..."), + ("Enable RDP session sharing", "Povolit sdílení relace RDP"), + ("Auto Login", "Automatické přihlášení"), + ("Enable direct IP access", "Povolit přímý přístup k IP"), + ("Rename", "Přejmenovat"), + ("Space", "Mezera"), + ("Create desktop shortcut", "Vytvořit zástupce na ploše"), + ("Change Path", "Změnit umístění"), + ("Create Folder", "Vytvořit složku"), + ("Please enter the folder name", "Zadejte název pro složku"), + ("Fix it", "Opravit to"), + ("Warning", "Upozornění"), + ("Login screen using Wayland is not supported", "Přihlašovací obrazovka pomocí systému Wayland není podporována"), + ("Reboot required", "Je vyžadován restart"), + ("Unsupported display server", "Nepodporovaný zobrazovací server"), + ("x11 expected", "očekávaný x11"), + ("Port", "Port"), + ("Settings", "Nastavení"), + ("Username", "Uživatelské jméno"), + ("Invalid port", "Neplatné číslo portu"), + ("Closed manually by the peer", "Ručně ukončeno protistranou"), + ("Enable remote configuration modification", "Povolit vzdálenou úpravu konfigurace"), + ("Run without install", "Spustit bez instalace"), + ("Connect via relay", "Připojení přes předávací server"), + ("Always connect via relay", "Vždy se připojovat prostřednictvím předávacího serveru"), + ("whitelist_tip", "Přístup je umožněn pouze z IP adres, nacházejících se na seznamu povolených"), + ("Login", "Přihlásit se"), + ("Verify", "Ověřit"), + ("Remember me", "Zapamatovat si"), + ("Trust this device", "Důvěřovat tomuto zařízení"), + ("Verification code", "Ověřovací kód"), + ("verification_tip", "Na registrovanou e-mailovou adresu byl zaslán ověřovací kód, zadejte jej a pokračujte v přihlašování."), + ("Logout", "Odhlásit se"), + ("Tags", "Štítky"), + ("Search ID", "Hledat ID"), + ("whitelist_sep", "Oddělené čárkou, středníkem, mezerami, nebo novým řádkem."), + ("Add ID", "Přidat ID"), + ("Add Tag", "Přidat štítek"), + ("Unselect all tags", "Zrušit výběr všech štítků"), + ("Network error", "Chyba sítě"), + ("Username missed", "Chybí uživatelské jméno"), + ("Password missed", "Chybí heslo"), + ("Wrong credentials", "Nesprávné přihlašovací údaje"), + ("The verification code is incorrect or has expired", "Ověřovací kód je nesprávný, nebo jeho platnost vypršela"), + ("Edit Tag", "Upravit štítek"), + ("Forget Password", "Přestat si pamatovat heslo"), + ("Favorites", "Oblíbené"), + ("Add to Favorites", "Přidat do oblíbených"), + ("Remove from Favorites", "Odebrat z oblíbených"), + ("Empty", "Prázdné"), + ("Invalid folder name", "Neplatný název složky"), + ("Socks5 Proxy", "Socks5 proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) proxy"), + ("Discovered", "Objeveno"), + ("install_daemon_tip", "Pokud má být spouštěno při startu systému, je třeba nainstalovat systémovou službu."), + ("Remote ID", "Vzdálené ID"), + ("Paste", "Vložit"), + ("Paste here?", "Vložit zde?"), + ("Are you sure to close the connection?", "Opravdu chcete spojení uzavřít?"), + ("Download new version", "Stáhnout novou verzi"), + ("Touch mode", "Režim dotyku"), + ("Mouse mode", "Režim myši"), + ("One-Finger Tap", "Klepnutí jedním prstem"), + ("Left Mouse", "Levé tlačítko myši"), + ("One-Long Tap", "Jedno dlouhé klepnutí"), + ("Two-Finger Tap", "Klepnutí dvěma prsty"), + ("Right Mouse", "Pravé tlačítko myši"), + ("One-Finger Move", "Přesouvání jedním prstem"), + ("Double Tap & Move", "Dvojité klepnutí a přesun"), + ("Mouse Drag", "Přetažení myší"), + ("Three-Finger vertically", "Třemi prsty svisle"), + ("Mouse Wheel", "Kolečko myši"), + ("Two-Finger Move", "Posun dvěma prsty"), + ("Canvas Move", "Posun zobrazení"), + ("Pinch to Zoom", "Přiblížíte roztažením dvěma prsty"), + ("Canvas Zoom", "Přiblížení zobrazení"), + ("Reset canvas", "Vrátit měřítko zobrazení na výchozí"), + ("No permission of file transfer", "Žádné oprávnění k přenosu souborů"), + ("Note", "Poznámka"), + ("Connection", "Připojení"), + ("Share screen", "Sdílet obrazovku"), + ("Chat", "Chat"), + ("Total", "Celkem"), + ("items", "Položek"), + ("Selected", "Vybráno"), + ("Screen Capture", "Zachytávání obrazovky"), + ("Input Control", "Ovládání vstupních zařízení"), + ("Audio Capture", "Zachytávání zvuku"), + ("Do you accept?", "Přijímáte?"), + ("Open System Setting", "Otevřít nastavení systému"), + ("How to get Android input permission?", "Jak v systému Android získat oprávnění pro vstupní zařízení?"), + ("android_input_permission_tip1", "Aby vzdálené zařízení mohlo ovládat vaše Android zařízení prostřednictví myši či dotyků, je třeba povolit, aby RustDesk mohlo používat službu „Zpřístupnění hendikepovaným“."), + ("android_input_permission_tip2", "Přejděte na následující stránku nastavení systému, najděte a přejděte do [Nainstalované služby] a zapněte službu [RustDesk vstup]."), + ("android_new_connection_tip", "Obdržen nový požadavek na řízení zařízení, který chce ovládat vaše stávající zařízení."), + ("android_service_will_start_tip", "Zapnutí „Zachytávání obsahu obrazovky“ automaticky spustí službu, což umožní ostatním zařízením žádat o připojení k vašemu zařízení."), + ("android_stop_service_tip", "Zastavení služby automaticky ukončí veškerá navázaná spojení."), + ("android_version_audio_tip", "Vámi nyní používaná verze systému Android nepodporuje zachytávání zvuku – přejděte na Android 10, nebo novější."), + ("android_start_service_tip", "Klepnutím na možnost [Spustit službu], nebo povolením oprávnění [Snímání obrazovky] spustíte službu sdílení obrazovky."), + ("android_permission_may_not_change_tip", "Oprávnění pro navázaná připojení lze změnit až po opětovném připojení."), + ("Account", "Účet"), + ("Overwrite", "Přepsat"), + ("This file exists, skip or overwrite this file?", "Tento soubor existuje, přeskočit, nebo přepsat tento soubor?"), + ("Quit", "Ukončit"), + ("Help", "Nápověda"), + ("Failed", "Nepodařilo se"), + ("Succeeded", "Úspěšný"), + ("Someone turns on privacy mode, exit", "Někdo zapne režim ochrany soukromí, ukončete ho"), + ("Unsupported", "Nepodporováno"), + ("Peer denied", "Protistrana odmítla"), + ("Please install plugins", "Nainstalujte si prosím pluginy"), + ("Peer exit", "Ukončení protistrany"), + ("Failed to turn off", "Nepodařilo se vypnout"), + ("Turned off", "Vypnutý"), + ("Language", "Jazyk"), + ("Keep RustDesk background service", "Zachovat službu RustDesk na pozadí"), + ("Ignore Battery Optimizations", "Ignorovat optimalizaci baterie"), + ("android_open_battery_optimizations_tip", "Pokud chcete tuto funkci zakázat, přejděte na další stránku nastavení aplikace RustDesk, najděte a zadejte [Baterie], zrušte zaškrtnutí [Neomezeno]."), + ("Start on boot", "Spustit při startu systému"), + ("Start the screen sharing service on boot, requires special permissions", "Spuštění služby sdílení obrazovky při spuštění systému, vyžaduje zvláštní oprávnění"), + ("Connection not allowed", "Připojení není povoleno"), + ("Legacy mode", "Režim Legacy"), + ("Map mode", "Režim mapování"), + ("Translate mode", "Režim překladu"), + ("Use permanent password", "Použít trvalé heslo"), + ("Use both passwords", "Použít obě hesla"), + ("Set permanent password", "Nastavit trvalé heslo"), + ("Enable remote restart", "Povolit vzdálené restartování"), + ("Restart remote device", "Restartovat vzdálené zařízení"), + ("Are you sure you want to restart", "Jste si jisti, že chcete restartovat"), + ("Restarting remote device", "Restartování vzdáleného zařízení"), + ("remote_restarting_tip", "Vzdálené zařízení se restartuje, zavřete prosím toto okno a po chvíli se znovu připojte pomocí trvalého hesla."), + ("Copied", "Zkopírováno"), + ("Exit Fullscreen", "Ukončit celou obrazovku"), + ("Fullscreen", "Celá obrazovka"), + ("Mobile Actions", "Mobilní akce"), + ("Select Monitor", "Vybrat monitor"), + ("Control Actions", "Ovládací akce"), + ("Display Settings", "Nastavení obrazovky"), + ("Ratio", "Poměr"), + ("Image Quality", "Kvalita obrazu"), + ("Scroll Style", "Styl posouvání"), + ("Show Toolbar", "Zobrazit panel nástrojů"), + ("Hide Toolbar", "Skrýt panel nástrojů"), + ("Direct Connection", "Přímé spojení"), + ("Relay Connection", "Připojení předávací server"), + ("Secure Connection", "Zabezpečené připojení"), + ("Insecure Connection", "Nezabezpečené připojení"), + ("Scale original", "Originální měřítko"), + ("Scale adaptive", "Adaptivní měřítko"), + ("General", "Obecné"), + ("Security", "Zabezpečení"), + ("Theme", "Motiv"), + ("Dark Theme", "Tmavý motiv"), + ("Light Theme", "Světlý motiv"), + ("Dark", "Tmavý"), + ("Light", "Světlý"), + ("Follow System", "Podle systému"), + ("Enable hardware codec", "Povolit hardwarový kodek"), + ("Unlock Security Settings", "Odemknout nastavení zabezpečení"), + ("Enable audio", "Povolit zvuk"), + ("Unlock Network Settings", "Odemknout nastavení sítě"), + ("Server", "Server"), + ("Direct IP Access", "Přímý IP přístup"), + ("Proxy", "Proxy"), + ("Apply", "Použít"), + ("Disconnect all devices?", "Odpojit všechna zařízení?"), + ("Clear", "Smazat"), + ("Audio Input Device", "Vstupní zvukové zařízení"), + ("Use IP Whitelisting", "Použít bílou listinu IP"), + ("Network", "Síť"), + ("Pin Toolbar", "Připnout panel nástrojů"), + ("Unpin Toolbar", "Odepnout panel nástrojů"), + ("Recording", "Nahrávání"), + ("Directory", "Adresář"), + ("Automatically record incoming sessions", "Automaticky nahrávat příchozí relace"), + ("Automatically record outgoing sessions", ""), + ("Change", "Změnit"), + ("Start session recording", "Spustit záznam relace"), + ("Stop session recording", "Zastavit záznam relace"), + ("Enable recording session", "Povolit nahrávání relace"), + ("Enable LAN discovery", "Povolit zjišťování sítě LAN"), + ("Deny LAN discovery", "Zakázat zjišťování sítě LAN"), + ("Write a message", "Napsat zprávu"), + ("Prompt", "Výzva"), + ("Please wait for confirmation of UAC...", "Počkejte prosím na potvrzení UAC..."), + ("elevated_foreground_window_tip", "Aktuální okno vzdálené plochy vyžaduje vyšší oprávnění, takže dočasně nemůže používat myš a klávesnici. Můžete vzdáleného uživatele požádat, aby aktuální okno minimalizoval, nebo kliknout na tlačítko pro zvýšení v okně správy připojení. Chcete-li se tomuto problému vyhnout, doporučujeme nainstalovat software na vzdálené zařízení."), + ("Disconnected", "Odpojeno"), + ("Other", "Jiné"), + ("Confirm before closing multiple tabs", "Potvrdit před zavřením více karet"), + ("Keyboard Settings", "Nastavení klávesnice"), + ("Full Access", "Úplný přístup"), + ("Screen Share", "Sdílení obrazovky"), + ("ubuntu-21-04-required", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."), + ("wayland-requires-higher-linux-version", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protistrany)."), + ("Show RustDesk", "Zobrazit RustDesk"), + ("This PC", "Tento počítač"), + ("or", "nebo"), + ("Elevate", "Zvýšit"), + ("Zoom cursor", "Kurzor přiblížení"), + ("Accept sessions via password", "Přijímat relace pomocí hesla"), + ("Accept sessions via click", "Přijímat relace kliknutím"), + ("Accept sessions via both", "Přijímat relace prostřednictvím obou"), + ("Please wait for the remote side to accept your session request...", "Počkejte prosím, až vzdálená strana přijme váš požadavek na relaci..."), + ("One-time Password", "Jednorázové heslo"), + ("Use one-time password", "Použít jednorázové heslo"), + ("One-time password length", "Délka jednorázového hesla"), + ("Request access to your device", "Žádost o přístup k vašemu zařízení"), + ("Hide connection management window", "Skrýt okno správy připojení"), + ("hide_cm_tip", "Povolit skrývání pouze v případě, že přijímáte relace pomocí hesla a používáte trvalé heslo."), + ("wayland_experiment_tip", "Podpora Waylandu je v experimentální fázi, pokud potřebujete bezobslužný přístup, použijte prosím X11."), + ("Right click to select tabs", "Výběr karet kliknutím pravým tlačítkem myši"), + ("Skipped", "Vynecháno"), + ("Add to address book", "Přidat do adresáře"), + ("Group", "Skupina"), + ("Search", "Vyhledávání"), + ("Closed manually by web console", "Uzavřeno ručně pomocí webové konzole"), + ("Local keyboard type", "Typ místní klávesnice"), + ("Select local keyboard type", "Výběr typu místní klávesnice"), + ("software_render_tip", "Pokud používáte grafickou kartu Nvidia v systému Linux a vzdálené okno se po připojení ihned zavře, může vám pomoci přepnutí na open-source ovladač Nouveau a volba softwarového vykreslování. Je nutný restart softwaru."), + ("Always use software rendering", "Vždy použít softwarové vykreslování"), + ("config_input", "Chcete-li ovládat vzdálenou plochu pomocí klávesnice, musíte udělit oprávnění RustDesk \"Sledování vstupu\"."), + ("config_microphone", "Abyste mohli mluvit na dálku, musíte udělit oprávnění RustDesk \"Nahrávat zvuk\"."), + ("request_elevation_tip", "Můžete také požádat o zvýšení, pokud je někdo na vzdálené straně."), + ("Wait", "Počkejte"), + ("Elevation Error", "Chyba navýšení"), + ("Ask the remote user for authentication", "Požádat vzdáleného uživatele o ověření"), + ("Choose this if the remote account is administrator", "Tuto možnost vyberte, pokud je vzdálený účet správce"), + ("Transmit the username and password of administrator", "Přenos uživatelského jména a hesla správce"), + ("still_click_uac_tip", "Stále vyžaduje, aby vzdálený uživatel kliknul na OK v okně UAC spuštěného RustDesku."), + ("Request Elevation", "Žádost o navýšení"), + ("wait_accept_uac_tip", "Počkejte, až vzdálený uživatel přijme dialogové okno UAC."), + ("Elevate successfully", "Úspěšné navýšení"), + ("uppercase", "velká písmena"), + ("lowercase", "malá písmena"), + ("digit", "číslice"), + ("special character", "speciální znak"), + ("length>=8", "délka>=8"), + ("Weak", "Slabé"), + ("Medium", "Střední"), + ("Strong", "Silné"), + ("Switch Sides", "Přepínání stran"), + ("Please confirm if you want to share your desktop?", "Potvrďte prosím, zda chcete sdílet svou plochu?"), + ("Display", "Obrazovka"), + ("Default View Style", "Výchozí styl zobrazení"), + ("Default Scroll Style", "Výchozí styl rolování"), + ("Default Image Quality", "Výchozí kvalita obrazu"), + ("Default Codec", "Výchozí kodek"), + ("Bitrate", "Datový tok"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Ostatní výchozí možnosti"), + ("Voice call", "Hlasové volání"), + ("Text chat", "Textový chat"), + ("Stop voice call", "Zastavit hlasové volání"), + ("relay_hint_tip", "Přímé připojení nemusí být možné, můžete se zkusit připojit přes předávací server. Pokud navíc chcete při prvním pokusu použít předávací server, můžete k ID přidat příponu \"/r\", nebo v kartě posledních relací vybrat možnost \"Vždy se připojovat přes bránu\", pokud existuje."), + ("Reconnect", "Znovu připojit"), + ("Codec", "Kodek"), + ("Resolution", "Rozlišení"), + ("No transfers in progress", "Žádné probíhající přenosy"), + ("Set one-time password length", "Nastavení délky jednorázového hesla"), + ("RDP Settings", "Nastavení RDP"), + ("Sort by", "Seřadit podle"), + ("New Connection", "Nové připojení"), + ("Restore", "Obnovit"), + ("Minimize", "Minimalizovat"), + ("Maximize", "Maximalizovat"), + ("Your Device", "Vaše zařízení"), + ("empty_recent_tip", "Jejda, žádná nedávná relace!\nČas naplánovat novou."), + ("empty_favorite_tip", "Ještě nemáte oblíbené protistrany?\nNajděte někoho, s kým se můžete spojit, a přidejte si ho do oblíbených!"), + ("empty_lan_tip", "Ale ne, vypadá, že jsme ještě neobjevili žádné protistrany."), + ("empty_address_book_tip", "Ach bože, zdá se, že ve vašem adresáři nejsou v současné době uvedeni žádní kolegové."), + ("Empty Username", "Prázdné uživatelské jméno"), + ("Empty Password", "Prázdné heslo"), + ("Me", "Já"), + ("identical_file_tip", "Tento soubor je totožný se souborem partnera."), + ("show_monitors_tip", "Zobrazit monitory na panelu nástrojů"), + ("View Mode", "Režim zobrazení"), + ("login_linux_tip", "Chcete-li povolit relaci plochy X, musíte se přihlásit ke vzdálenému účtu systému Linux."), + ("verify_rustdesk_password_tip", "Ověření hesla RustDesk"), + ("remember_account_tip", "Zapamatovat si tento účet"), + ("os_account_desk_tip", "Tento účet se používá k přihlášení do vzdáleného operačního systému a k povolení relace plochy v režimu headless."), + ("OS Account", "Účet operačního systému"), + ("another_user_login_title_tip", "Další uživatel je již přihlášen"), + ("another_user_login_text_tip", "Odpojit"), + ("xorg_not_found_title_tip", "Xorg nebyl nalezen"), + ("xorg_not_found_text_tip", "Prosím, nainstalujte Xorg"), + ("no_desktop_title_tip", "Není k dispozici žádná plocha"), + ("no_desktop_text_tip", "Nainstalujte si prosím prostředí GNOME"), + ("No need to elevate", "Není třeba navýšení"), + ("System Sound", "Systémový zvuk"), + ("Default", "Výchozí"), + ("New RDP", "Nové RDP"), + ("Fingerprint", "Otisk"), + ("Copy Fingerprint", "Kopírovat otisk"), + ("no fingerprints", "žádný otisk"), + ("Select a peer", "Výběr protistrany"), + ("Select peers", "Vybrat protistrany"), + ("Plugins", "Pluginy"), + ("Uninstall", "Odinstalovat"), + ("Update", "Aktualizovat"), + ("Enable", "Povolit"), + ("Disable", "Zakázat"), + ("Options", "Možnosti"), + ("resolution_original_tip", "Původní rozlišení"), + ("resolution_fit_local_tip", "Přizpůsobit místní rozlišení"), + ("resolution_custom_tip", "Vlastní rozlišení"), + ("Collapse toolbar", "Sbalit panel nástrojů"), + ("Accept and Elevate", "Přijmout navýšení"), + ("accept_and_elevate_btn_tooltip", "Přijměte připojení a zvyšte oprávnění UAC."), + ("clipboard_wait_response_timeout_tip", "Vypršel čas čekání odpovědi na kopii."), + ("Incoming connection", "Příchozí připojení"), + ("Outgoing connection", "Odchozí připojení"), + ("Exit", "Ukončit"), + ("Open", "Otevřít"), + ("logout_tip", "Opravdu se chcete odhlásit?"), + ("Service", "Služba"), + ("Start", "Spustit"), + ("Stop", "Zastavit"), + ("exceed_max_devices", "Dosáhli jste maximálního počtu spravovaných zařízení."), + ("Sync with recent sessions", "Synchronizace s posledními relacemi"), + ("Sort tags", "Seřadit štítky"), + ("Open connection in new tab", "Otevřít připojení na nové kartě"), + ("Move tab to new window", "Přesunout kartu do nového okna"), + ("Can not be empty", "Nemůže být prázdné"), + ("Already exists", "Již existuje"), + ("Change Password", "Změnit heslo"), + ("Refresh Password", "Obnovit heslo"), + ("ID", "ID"), + ("Grid View", "Mřížka"), + ("List View", "Seznam"), + ("Select", "Vybrat"), + ("Toggle Tags", "Přepnout štítky"), + ("pull_ab_failed_tip", "Nepodařilo se obnovit adresář"), + ("push_ab_failed_tip", "Nepodařilo se synchronizovat adresář se serverem"), + ("synced_peer_readded_tip", "Zařízení, která byla přítomna v posledních relacích, budou synchronizována zpět do adresáře."), + ("Change Color", "Změna barvy"), + ("Primary Color", "Základní barva"), + ("HSV Color", "HSV barva"), + ("Installation Successful!", "Instalace úspěšná!"), + ("Installation failed!", "Instalace se nezdařila!"), + ("Reverse mouse wheel", "Reverzní kolečko myši"), + ("{} sessions", "{} sezení"), + ("scam_title", "Možná vás někdo PODVEDL!"), + ("scam_text1", "Pokud telefonujete s někým, koho NEZNÁTE a komu NEDŮVĚŘUJETE a kdo vás požádal o použití služby RustDesk a její spuštění, nepokračujte v hovoru a okamžitě zavěste."), + ("scam_text2", "Pravděpodobně se jedná o podvodníka, který se snaží ukrást vaše peníze nebo jiné soukromé informace."), + ("Don't show again", "Znovu se neukázat"), + ("I Agree", "Souhlasím"), + ("Decline", "Odmítnout"), + ("Timeout in minutes", "Časový limit v minutách"), + ("auto_disconnect_option_tip", "Automatické ukončení příchozích relací při nečinnosti uživatele"), + ("Connection failed due to inactivity", "Připojení se nezdařilo z důvodu nečinnosti"), + ("Check for software update on startup", "Kontrola aktualizace softwaru při spuštění"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Aktualizujte prosím RustDesk Server Pro na verzi {} nebo novější!"), + ("pull_group_failed_tip", "Nepodařilo se obnovit skupinu"), + ("Filter by intersection", "Filtrovat podle průsečíku"), + ("Remove wallpaper during incoming sessions", "Odstranit tapetu během příchozích relací"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Obrazovka je odpojena, přepněte na první obrazovku."), + ("No displays", "Žádné obrazovky"), + ("Open in new window", "Otevřít v novém okně"), + ("Show displays as individual windows", "Zobrazit obrazovky jako jednotlivá okna"), + ("Use all my displays for the remote session", "Použít všechny mé obrazovky pro vzdálenou relaci"), + ("selinux_tip", "Na vašem zařízení je povolen SELinux, což může bránit správnému běhu RustDesku jako řízené strany."), + ("Change view", "Změnit pohled"), + ("Big tiles", "Velké dlaždice"), + ("Small tiles", "Malé dlaždice"), + ("List", "Seznam"), + ("Virtual display", "Virtuální obrazovka"), + ("Plug out all", "Odpojit všechny"), + ("True color (4:4:4)", "Skutečné barvy (4:4:4)"), + ("Enable blocking user input", "Povolit blokování uživatelského vstupu"), + ("id_input_tip", "Můžete zadat ID, přímou IP adresu nebo doménu s portem (:).\nPokud chcete přistupovat k zařízení na jiném serveru, připojte adresu serveru (@?key=), například,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nPokud chcete přistupovat k zařízení na veřejném serveru, zadejte \"@public\", klíč není pro veřejný server potřeba."), + ("privacy_mode_impl_mag_tip", "Režim 1"), + ("privacy_mode_impl_virtual_display_tip", "Režim 2"), + ("Enter privacy mode", "Vstup do režimu soukromí"), + ("Exit privacy mode", "Ukončit režim soukromí"), + ("idd_not_support_under_win10_2004_tip", "Ovladač nepřímého zobrazení není podporován. Je vyžadován systém Windows 10, verze 2004 nebo novější."), + ("input_source_1_tip", "Vstupní zdroj 1"), + ("input_source_2_tip", "Vstupní zdroj 2"), + ("Swap control-command key", "Prohození klávesy control-command"), + ("swap-left-right-mouse", "Prohodit levé a pravé tlačítko myši"), + ("2FA code", "2FA kód"), + ("More", "Více"), + ("enable-2fa-title", "Povolit dvoufaktorové ověřování"), + ("enable-2fa-desc", "Prosím, nastavte si svůj autentizátor. Na svém telefonu nebo počítači můžete použít ověřovací aplikaci, jako je Authy, Microsoft nebo Google Authenticator.\n\nNaskenujte pomocí aplikace QR kód a zadejte kód, který aplikace zobrazí, abyste aktivovali dvoufaktorové ověření."), + ("wrong-2fa-code", "Kód nelze ověřit. Zkontrolujte správnost nastavení kódu a místního času"), + ("enter-2fa-title", "Dvoufaktorová autentizace"), + ("Email verification code must be 6 characters.", "E-mailový ověřovací kód musí mít 6 znaků."), + ("2FA code must be 6 digits.", "Kód 2FA musí mít 6 číslic."), + ("Multiple Windows sessions found", "Bylo nalezeno více relací Windows"), + ("Please select the session you want to connect to", "Vyberte relaci, ke které se chcete připojit"), + ("powered_by_me", "Poháněno společností RustDesk"), + ("outgoing_only_desk_tip", "Toto je přizpůsobená edice.\nMůžete se připojit k jiným zařízením, ale jiná zařízení se k vašemu zařízení připojit nemohou."), + ("preset_password_warning", "Tato upravená edice je dodávána s přednastaveným heslem. Každý, kdo zná toto heslo, může získat plnou kontrolu nad vaším zařízením. Pokud jste to nečekali, okamžitě software odinstalujte."), + ("Security Alert", "Bezpečnostní výstraha"), + ("My address book", "Můj adresář"), + ("Personal", "Osobní"), + ("Owner", "Majitel"), + ("Set shared password", "Nastavit sdílené heslo"), + ("Exist in", "Existuje v"), + ("Read-only", "Pouze ke čtení"), + ("Read/Write", "Režim čtení/zápisu"), + ("Full Control", "Plná kontrola"), + ("share_warning_tip", "Výše uvedená pole jsou sdílená a viditelná pro ostatní."), + ("Everyone", "Každý"), + ("ab_web_console_tip", "Více na webové konzoli"), + ("allow-only-conn-window-open-tip", "Povolit připojení pouze v případě, že je otevřené okno RustDesk"), + ("no_need_privacy_mode_no_physical_displays_tip", "Žádné fyzické displeje, není třeba používat režim soukromí."), + ("Follow remote cursor", "Sledovat dálkový kurzor"), + ("Follow remote window focus", "Sledovat zaměření vzdáleného okna"), + ("default_proxy_tip", "Výchozí protokol a port jsou Socks5 a 1080"), + ("no_audio_input_device_tip", "Nebylo nalezeno žádné vstupní zvukové zařízení."), + ("Incoming", "Příchozí"), + ("Outgoing", "Odchozí"), + ("Clear Wayland screen selection", "Vymazat výběr obrazovky Wayland"), + ("clear_Wayland_screen_selection_tip", "Po vymazání výběru obrazovky můžete znovu vybrat obrazovku, kterou chcete sdílet."), + ("confirm_clear_Wayland_screen_selection_tip", "Opravdu chcete vymazat výběr obrazovky Wayland?"), + ("android_new_voice_call_tip", "Byl přijat nový požadavek na hlasové volání. Pokud hovor přijmete, přepne se zvuk na hlasovou komunikaci."), + ("texture_render_tip", "Použít vykreslování textur, aby byly obrázky hladší."), + ("Use texture rendering", "Použít vykreslování textur"), + ("Floating window", "Plovoucí okno"), + ("floating_window_tip", "Pomáhá udržovat službu RustDesk na pozadí"), + ("Keep screen on", "Ponechat obrazovku zapnutou"), + ("Never", "Nikdy"), + ("During controlled", "Během řízeného"), + ("During service is on", "Během služby je v provozu"), + ("Capture screen using DirectX", "Snímání obrazovky pomocí DirectX"), + ("Back", "Zpět"), + ("Apps", "Aplikace"), + ("Volume up", "Zvýšit hlasitost"), + ("Volume down", "Snížit hlasitost"), + ("Power", "Napájení"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Pokud tuto funkci povolíte, můžete od svého bota obdržet kód 2FA. Může také fungovat jako oznámení o připojení."), + ("enable-bot-desc", "1, Otevřete chat s @BotFather.\n2, Pošlete příkaz \"/newbot\". Po dokončení tohoto kroku obdržíte token.\n3, Spusťte chat s nově vytvořeným botem. Pro jeho aktivaci odešlete zprávu začínající lomítkem vpřed (\"/\"), například \"/hello\".\n"), + ("cancel-2fa-confirm-tip", "Jste si jisti, že chcete zrušit 2FA?"), + ("cancel-bot-confirm-tip", "Jste si jisti, že chcete zrušit bota Telegramu?"), + ("About RustDesk", "O RustDesk"), + ("Send clipboard keystrokes", "Odesílat stisky kláves schránky"), + ("network_error_tip", "Zkontrolujte prosím připojení k síti a klikněte na tlačítko Opakovat."), + ("Unlock with PIN", "Odemknout PINem"), + ("Requires at least {} characters", "Vyžadováno aspoň {} znaků"), + ("Wrong PIN", "Nesprávný PIN"), + ("Set PIN", "Nastavit PIN"), + ("Enable trusted devices", "Povolit důvěryhodná zařízení"), + ("Manage trusted devices", "Spravovat důvěryhodná zařízení"), + ("Platform", "Platforma"), + ("Days remaining", "Zbývajících dnů"), + ("enable-trusted-devices-tip", "Přeskočte 2FA ověření na důvěryhodných zařízeních"), + ("Parent directory", "Rodičovský adresář"), + ("Resume", "Pokračovat"), + ("Invalid file name", "Nesprávný název souboru"), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgradujte prosím klienta RustDesk na verzi {} nebo novější na vzdálené straně!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Zobrazit kameru"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Pokračovat s {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/da.rs b/vendor/rustdesk/src/lang/da.rs new file mode 100644 index 0000000..7410124 --- /dev/null +++ b/vendor/rustdesk/src/lang/da.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Dit skrivebord"), + ("desk_tip", "Du kan give adgang til dit skrivebord med dette ID og denne adgangskode."), + ("Password", "Adgangskode"), + ("Ready", "Klar"), + ("Established", "Etableret"), + ("connecting_status", "Opretter forbindelse til RustDesk-netværket..."), + ("Enable service", "Tænd forbindelsesserveren"), + ("Start service", "Start forbindelsesserveren"), + ("Service is running", "Tjenesten kører"), + ("Service is not running", "Den tilknyttede tjeneste kører ikke"), + ("not_ready_status", "Ikke klar. Tjek venligst din forbindelse"), + ("Control Remote Desktop", "Styr fjernskrivebord"), + ("Transfer file", "Overfør fil"), + ("Connect", "Forbind"), + ("Recent sessions", "Seneste sessioner"), + ("Address book", "Adressebog"), + ("Confirmation", "Bekræftelse"), + ("TCP tunneling", "TCP tunneling"), + ("Remove", "Fjern"), + ("Refresh random password", "Opdater tilfældig adgangskode"), + ("Set your own password", "Indstil din egen adgangskode"), + ("Enable keyboard/mouse", "Tænd for tastatur/mus"), + ("Enable clipboard", "Tænd for udklipsholderen"), + ("Enable file transfer", "Aktivér filoverførsel"), + ("Enable TCP tunneling", "Slå TCP-tunneling til"), + ("IP Whitelisting", "IP whitelisting"), + ("ID/Relay Server", "ID/forbindelsesserver"), + ("Import server config", "Importér serverkonfiguration"), + ("Export Server Config", "Eksportér serverkonfiguration"), + ("Import server configuration successfully", "Importering af serverkonfigurationen lykkedes"), + ("Export server configuration successfully", "Eksportering af serverkonfigurationen lykkedes"), + ("Invalid server configuration", "Ugyldig serverkonfiguration"), + ("Clipboard is empty", "Udklipsholderen er tom"), + ("Stop service", "Sluk for forbindelsesserveren"), + ("Change ID", "Ændr ID"), + ("Your new ID", "Dit nye ID"), + ("length %min% to %max%", "længde %min% til %max%"), + ("starts with a letter", "starter med ét bogstav"), + ("allowed characters", "tilladte tegn"), + ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9, - (dash) og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Antal tegn skal være mellem 6 og 16."), + ("Website", "Hjemmeside"), + ("About", "Om"), + ("Slogan_tip", "Lavet med kærlighed i denne kaotiske verden!"), + ("Privacy Statement", "Privatlivspolitik"), + ("Mute", "Sluk for mikrofonen"), + ("Build Date", "Build dato"), + ("Version", "Version"), + ("Home", "Hjem"), + ("Audio Input", "Lydinput"), + ("Enhancements", "Forbedringer"), + ("Hardware Codec", "Hardware-codec"), + ("Adaptive bitrate", "Adaptiv bitrate"), + ("ID Server", "ID Server"), + ("Relay Server", "Relay Server"), + ("API Server", "API Server"), + ("invalid_http", "Skal begynde med http:// eller https://"), + ("Invalid IP", "Ugyldig IP-adresse"), + ("Invalid format", "Ugyldigt format"), + ("server_not_support", "Endnu ikke understøttet af serveren"), + ("Not available", "Ikke tilgængelig"), + ("Too frequent", "For ofte"), + ("Cancel", "Annullér"), + ("Skip", "Spring over"), + ("Close", "Luk"), + ("Retry", "Prøv igen"), + ("OK", "OK"), + ("Password Required", "Adgangskode påkrævet"), + ("Please enter your password", "Indtast venligst dit kodeord"), + ("Remember password", "Husk kodeord"), + ("Wrong Password", "Forkert kodeord"), + ("Do you want to enter again?", "Vil du forbinde igen?"), + ("Connection Error", "Forbindelsesfejl"), + ("Error", "Fejl"), + ("Reset by the peer", "Nulstillet ved modparten"), + ("Connecting...", "Opretter forbindelse..."), + ("Connection in progress. Please wait.", "Forbindelsen er etableret. Vent venligst."), + ("Please try 1 minute later", "Prøv igen om et minut"), + ("Login Error", "Login fejl"), + ("Successful", "Vellykket"), + ("Connected, waiting for image...", "Tilsluttet, venter på billede..."), + ("Name", "Navn"), + ("Type", "Type"), + ("Modified", "Ændret"), + ("Size", "Størrelse"), + ("Show Hidden Files", "Vis skjulte filer"), + ("Receive", "Modtag"), + ("Send", "Send"), + ("Refresh File", "Genopfrisk fil"), + ("Local", "Lokalt"), + ("Remote", "Remote"), + ("Remote Computer", "Fjerncomputer"), + ("Local Computer", "Lokal computer"), + ("Confirm Delete", "Bekræft sletning"), + ("Delete", "Slet"), + ("Properties", "Egenskaber"), + ("Multi Select", "Flere valg"), + ("Select All", "Vælg alt"), + ("Unselect All", "Fravælg alt"), + ("Empty Directory", "Tomt bibliotek"), + ("Not an empty directory", "Intet tomt bibliotek"), + ("Are you sure you want to delete this file?", "Er du sikker på, at du vil slette denne fil?"), + ("Are you sure you want to delete this empty directory?", "Er du sikker på, at du vil slette dette tomme bibliotek?"), + ("Are you sure you want to delete the file of this directory?", "Er du sikker på, at du vil slette filen til dette bibliotek?"), + ("Do this for all conflicts", "Gør dette for alle konflikter"), + ("This is irreversible!", "Dette kan ikke gendannes!"), + ("Deleting", "Sletter"), + ("files", "Filer"), + ("Waiting", "Venter"), + ("Finished", "Færdig"), + ("Speed", "Hastighed"), + ("Custom Image Quality", "Brugerdefineret billedkvalitet"), + ("Privacy mode", "Privatlivstilstand"), + ("Block user input", "Bloker brugerinput"), + ("Unblock user input", "Fjern blokering af brugerinput"), + ("Adjust Window", "Juster vinduet"), + ("Original", "Original"), + ("Shrink", "Krymp"), + ("Stretch", "Stræk ud"), + ("Scrollbar", "Scrollbar"), + ("ScrollAuto", "Auto-scroll"), + ("Good image quality", "God billedkvalitet"), + ("Balanced", "Afbalanceret"), + ("Optimize reaction time", "Optimeret responstid"), + ("Custom", "Tilpasset"), + ("Show remote cursor", "Vis fjernbetjeningskontrolleret markør"), + ("Show quality monitor", "Vis billedkvalitet"), + ("Disable clipboard", "Deaktiver udklipsholder"), + ("Lock after session end", "Lås efter afslutningen af fjernstyring"), + ("Insert Ctrl + Alt + Del", "Indsæt Ctrl + Alt + Del"), + ("Insert Lock", "Indsæt lås"), + ("Refresh", "Genopfrisk"), + ("ID does not exist", "ID findes ikke"), + ("Failed to connect to rendezvous server", "Forbindelse til forbindelsesserveren mislykkedes"), + ("Please try later", "Prøv igen senere"), + ("Remote desktop is offline", "Fjernskrivebord er offline"), + ("Key mismatch", "Nøgle uoverensstemmelse"), + ("Timeout", "Timeout"), + ("Failed to connect to relay server", "Forbindelse til relay-serveren mislykkedes"), + ("Failed to connect via rendezvous server", "Forbindelse via Rendezvous-server mislykkedes"), + ("Failed to connect via relay server", "Forbindelse via relay-serveren mislykkedes"), + ("Failed to make direct connection to remote desktop", "Direkte forbindelse til fjernskrivebord kunne ikke etableres"), + ("Set Password", "Indstil adgangskode"), + ("OS Password", "Operativsystemadgangskode"), + ("install_tip", "På grund af UAC kan RustDesk ikke fungere korrekt i nogle tilfælde på fjernskrivebordet. For at undgå UAC skal du klikke på knappen nedenfor for at installere RustDesk på systemet"), + ("Click to upgrade", "Klik for at opgradere"), + ("Configure", "Konfigurer"), + ("config_acc", "For at kontrollere dit skrivebord på afstand skal du give RustDesk \"Access \" Rettigheder."), + ("config_screen", "For at kunne få adgang til dit skrivebord langtfra, skal du give RustDesk \"skærmstøtte \" tilladelser."), + ("Installing ...", "Installerer ..."), + ("Install", "installere"), + ("Installation", "Installation"), + ("Installation Path", "Installationsti"), + ("Create start menu shortcuts", "Opret start menu genveje"), + ("Create desktop icon", "Opret skrivebords-genvej"), + ("agreement_tip", "Hvis du starter installationen, skal du acceptere licensaftalen"), + ("Accept and Install", "Accepter og installer"), + ("End-user license agreement", "Licensaftale for slutbrugere"), + ("Generating ...", "Genererer kode ..."), + ("Your installation is lower version.", "Din installation er en ældre version."), + ("not_close_tcp_tip", "Luk ikke dette vindue, mens du bruger tunnelen."), + ("Listening ...", "Lytter ..."), + ("Remote Host", "Fjern-Host"), + ("Remote Port", "Fjern-Port"), + ("Action", "Handling"), + ("Add", "Tilføj"), + ("Local Port", "Lokal Port"), + ("Local Address", "Lokal adresse"), + ("Change Local Port", "Skift lokal port"), + ("setup_server_tip", "For en hurtigere forbindelse skal du indstille din egen forbindelsesserver"), + ("Too short, at least 6 characters.", "For kort, brug mindst 6 tegn."), + ("The confirmation is not identical.", "Bekræftelsen er ikke identisk."), + ("Permissions", "Tilladelser"), + ("Accept", "Accepter"), + ("Dismiss", "Afvis"), + ("Disconnect", "Frakobl"), + ("Enable file copy and paste", "Tillad kopiering og indsæt af filer"), + ("Connected", "Forbundet"), + ("Direct and encrypted connection", "Direkte og krypteret forbindelse"), + ("Relayed and encrypted connection", "Viderestillet og krypteret forbindelse"), + ("Direct and unencrypted connection", "Direkte og ukrypteret forbindelse"), + ("Relayed and unencrypted connection", "Viderestillet og ukrypteret forbindelse"), + ("Enter Remote ID", "Indtast Remote-ID"), + ("Enter your password", "Skriv dit kodeord"), + ("Logging in...", "Logger ind..."), + ("Enable RDP session sharing", "Aktivér RDP sessiongodkendelse"), + ("Auto Login", "Automatisk login (kun gyldigt hvis du har konfigureret \"Lås efter afslutningen af sessionen\")"), + ("Enable direct IP access", "Aktivér direkte IP-adgang"), + ("Rename", "Omdøb"), + ("Space", "Plads"), + ("Create desktop shortcut", "Opret skrivebords-genvej"), + ("Change Path", "Skift stien"), + ("Create Folder", "Opret mappe"), + ("Please enter the folder name", "Indtast venligst mappens navn"), + ("Fix it", "Kør reparation"), + ("Warning", "Advarsel"), + ("Login screen using Wayland is not supported", "Login skærm med Wayland understøttes ikke"), + ("Reboot required", "Genstart krævet"), + ("Unsupported display server", "Ikke-understøttet displayserver"), + ("x11 expected", "X11 Forventet"), + ("Port", "Port"), + ("Settings", "Indstillinger"), + ("Username", " Brugernavn"), + ("Invalid port", "Ugyldig port"), + ("Closed manually by the peer", "Manuelt lukket af peer"), + ("Enable remote configuration modification", "Tillad fjernkonfigurering"), + ("Run without install", "Kør uden installation"), + ("Connect via relay", "Forbind via viderestilling"), + ("Always connect via relay", "Forbindelse via viderestillings-server"), + ("whitelist_tip", "Kun IP'er på whitelisten kan få adgang til mig"), + ("Login", "Login"), + ("Verify", "Verificér"), + ("Remember me", "Husk mig"), + ("Trust this device", "Husk denne enhed"), + ("Verification code", "Verifikationskode"), + ("verification_tip", "En bekræftelseskode er blevet sendt til den registrerede e-mail adresse. Indtast bekræftelseskoden for at logge på."), + ("Logout", "Logger af"), + ("Tags", "Nøgleord"), + ("Search ID", "Søg efter ID"), + ("whitelist_sep", "Adskilt af komma, semikolon, mellemrum eller linjebrud"), + ("Add ID", "Tilføj ID"), + ("Add Tag", "Tilføj nøgleord"), + ("Unselect all tags", "Fravælg alle nøgleord"), + ("Network error", "Netværksfejl"), + ("Username missed", "Glemt brugernavn"), + ("Password missed", "Glemt kodeord"), + ("Wrong credentials", "Forkerte registreringsdata"), + ("The verification code is incorrect or has expired", "Bekræftelsesnøglen er forkert eller er udløbet"), + ("Edit Tag", "Rediger nøgleord"), + ("Forget Password", "Glem adgangskoden"), + ("Favorites", "Favoritter"), + ("Add to Favorites", "Tilføj til favoritter"), + ("Remove from Favorites", "Fjern favoritter"), + ("Empty", "Tom"), + ("Invalid folder name", "Ugyldigt mappenavn"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "Fundet"), + ("install_daemon_tip", "For at starte efter PC'en er startet op, skal du installere systemtjenesten"), + ("Remote ID", "Fjern-ID"), + ("Paste", "Indsæt"), + ("Paste here?", "Indsæt her?"), + ("Are you sure to close the connection?", "Er du sikker på at du vil afslutte forbindelsen?"), + ("Download new version", "Download ny version"), + ("Touch mode", "Touch-tilstand"), + ("Mouse mode", "Muse-tilstand"), + ("One-Finger Tap", "En-finger-tryk"), + ("Left Mouse", "Venstre mus"), + ("One-Long Tap", "Tryk og hold med en finger"), + ("Two-Finger Tap", "Tryk med to fingre"), + ("Right Mouse", "Højre mus"), + ("One-Finger Move", "En-finger bevægelse"), + ("Double Tap & Move", "Dobbeltklik og flyt"), + ("Mouse Drag", "Træk med musen"), + ("Three-Finger vertically", "Tre fingre lodret"), + ("Mouse Wheel", "Mussehjul"), + ("Two-Finger Move", "To-fingre bevægelse"), + ("Canvas Move", "Flyt lærred"), + ("Pinch to Zoom", "Knib for at zoome ind"), + ("Canvas Zoom", "Lærred zoom"), + ("Reset canvas", "Nulstil lærred"), + ("No permission of file transfer", "Ingen tilladelse til at overføre filen"), + ("Note", "Note"), + ("Connection", "Forbindelse"), + ("Share screen", "Del skærmen"), + ("Chat", "Chat"), + ("Total", "Total"), + ("items", "artikel"), + ("Selected", "Valgte"), + ("Screen Capture", "Skærmoptagelse"), + ("Input Control", "Inputkontrol"), + ("Audio Capture", "Lydoptagelse"), + ("Do you accept?", "Accepterer du?"), + ("Open System Setting", "Åbn systemindstillingen"), + ("How to get Android input permission?", "Hvordan får jeg en Android-input tilladelse?"), + ("android_input_permission_tip1", "For at en ekstern enhed kan kontrollere din Android-enhed via mus eller berøring, skal du give RustDesk mulighed for at bruge tjenesten \"tilgængelighed \"."), + ("android_input_permission_tip2", "Gå til den næste systemindstillingsside, søg og indtast [installerede tjenester], tænd for [RustDesk Input] tjenesten."), + ("android_new_connection_tip", "En ny anmodning blev modtaget, der gerne vil kontrollere din nuværende enhed."), + ("android_service_will_start_tip", "Ved at tænde for skærmoptagelsen startes tjenesten automatisk, så andre enheder kan anmode om en forbindelse fra denne enhed."), + ("android_stop_service_tip", "Ved at lukke tjenesten lukkes alle fremstillede forbindelser automatisk."), + ("android_version_audio_tip", "Den aktuelle Android-version understøtter ikke lydoptagelse. Android 10 eller højere er påkrævet."), + ("android_start_service_tip", "Tryk [Start tjeneste] eller aktivér [Skærmoptagelse] tilladelse for at dele skærmen."), + ("android_permission_may_not_change_tip", "Rettigheder til oprettede forbindelser ændres ikke med det samme før der forbindelsen genoprettes."), + ("Account", "Konto"), + ("Overwrite", "Overskriv"), + ("This file exists, skip or overwrite this file?", "Denne fil findes allerede, vil du springe over eller overskrive denne fil?"), + ("Quit", "Afslut"), + ("Help", "Hjælp"), + ("Failed", "Mislykkedet"), + ("Succeeded", "Vellykket"), + ("Someone turns on privacy mode, exit", "Nogen aktiverede privatlivstilstand, afslut"), + ("Unsupported", "Ikke understøttet"), + ("Peer denied", "Modpart nægtet"), + ("Please install plugins", "Installer venligst plugins"), + ("Peer exit", "Modpart-Afslut"), + ("Failed to turn off", "Mislykkedes i at lukke ned"), + ("Turned off", "Slukket"), + ("Language", "Sprog"), + ("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"), + ("Ignore Battery Optimizations", "Ignorér betteri optimeringer"), + ("android_open_battery_optimizations_tip", "Hvis du ønsker at slukke for denne funktion, åbn RustDesk appens indstillinger, tryk på [Batteri], og fjern flueben ved [Uden begrænsninger]"), + ("Start on boot", "Start under opstart"), + ("Start the screen sharing service on boot, requires special permissions", "Start skærmdelingstjenesten under opstart, kræver specielle rettigheder"), + ("Connection not allowed", "Forbindelse ikke tilladt"), + ("Legacy mode", "Bagudkompatibilitetstilstand"), + ("Map mode", "Kortmodus"), + ("Translate mode", "Oversættelsesmodus"), + ("Use permanent password", "Brug permanent adgangskode"), + ("Use both passwords", "Brug begge typer adgangskoder"), + ("Set permanent password", "Sæt permanent adgangskode"), + ("Enable remote restart", "Aktivér fjerngenstart"), + ("Restart remote device", "Genstart fjernenhed"), + ("Are you sure you want to restart", "Er du sikker på at du vil genstarte"), + ("Restarting remote device", "Genstarter fjernenhed"), + ("remote_restarting_tip", "Enheden genstarter - Lukker denne besked ned, og tilslutter igen om et øjeblik"), + ("Copied", "Kopieret"), + ("Exit Fullscreen", "Afslut fuldskærm"), + ("Fullscreen", "Fuld skærm"), + ("Mobile Actions", "Mobile handlinger"), + ("Select Monitor", "Vælg skærm"), + ("Control Actions", "Kontrolhandlinger"), + ("Display Settings", "Skærmindstillinger"), + ("Ratio", "Forhold"), + ("Image Quality", "Billedkvalitet"), + ("Scroll Style", "Rullestil"), + ("Show Toolbar", "Vis værktøjslinje"), + ("Hide Toolbar", "Skjul værktøjslinje"), + ("Direct Connection", "Direkte forbindelse"), + ("Relay Connection", "Viderestillingsforbindelse"), + ("Secure Connection", "Sikker forbindelse"), + ("Insecure Connection", "Usikker forbindelse"), + ("Scale original", "Original skalering"), + ("Scale adaptive", "Adaptiv skalering"), + ("General", "Generelt"), + ("Security", "Sikkerhed"), + ("Theme", "Thema"), + ("Dark Theme", "Mørk Tema"), + ("Light Theme", "Lys Tema"), + ("Dark", "Mørk"), + ("Light", "Lys"), + ("Follow System", "Følg System"), + ("Enable hardware codec", "Aktivér hardware-codec"), + ("Unlock Security Settings", "Lås op for sikkerhedsindstillinger"), + ("Enable audio", "Aktivér Lyd"), + ("Unlock Network Settings", "Lås op for Netværksindstillinger"), + ("Server", "Server"), + ("Direct IP Access", "Direkte IP Adgang"), + ("Proxy", "Proxy"), + ("Apply", "Anvend"), + ("Disconnect all devices?", "Afbryd alle enheder?"), + ("Clear", "Nulstil"), + ("Audio Input Device", "Lydindgangsenhed"), + ("Use IP Whitelisting", "Brug IP Whitelisting"), + ("Network", "Netværk"), + ("Pin Toolbar", "Fastgør værktøjslinjen"), + ("Unpin Toolbar", "Frigiv værktøjslinjen"), + ("Recording", "Optager"), + ("Directory", "Mappe"), + ("Automatically record incoming sessions", "Optag automatisk indgående sessioner"), + ("Automatically record outgoing sessions", ""), + ("Change", "Ændr"), + ("Start session recording", "Start sessionsoptagelse"), + ("Stop session recording", "Stop sessionsoptagelse"), + ("Enable recording session", "Aktivér optagelsessession"), + ("Enable LAN discovery", "Aktivér opdagelse via det lokale netværk"), + ("Deny LAN discovery", "Afvis opdagelse via det lokale netværk"), + ("Write a message", "Skriv en besked"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Vent venligst på UAC-bekræftelse..."), + ("elevated_foreground_window_tip", "Det nuværende vindue på fjernskrivebordet kræver højere rettigheder for at køre, så det er midlertidigt ikke muligt at bruge musen og tastaturet. Du kan bede fjernbrugeren om at minimere vinduet, eller trykke på elevér knappen i forbindelsesvinduet. For at undgå dette problem, er det anbefalet at installere RustDesk på fjernenheden."), + ("Disconnected", "Afbrudt"), + ("Other", "Andre"), + ("Confirm before closing multiple tabs", "Bekræft nedlukning hvis der er flere faner"), + ("Keyboard Settings", "Tastaturindstillinger"), + ("Full Access", "Fuld adgang"), + ("Screen Share", "Skærmdeling"), + ("ubuntu-21-04-required", "Wayland kræver Ubuntu version 21.04 eller nyere."), + ("wayland-requires-higher-linux-version", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på modtagersiden)."), + ("Show RustDesk", "Vis RustDesk"), + ("This PC", "Denne PC"), + ("or", "eller"), + ("Elevate", "Elevér"), + ("Zoom cursor", "Zoom markør"), + ("Accept sessions via password", "Acceptér sessioner via adgangskode"), + ("Accept sessions via click", "Acceptér sessioner via klik"), + ("Accept sessions via both", "Acceptér sessioner via begge"), + ("Please wait for the remote side to accept your session request...", "Vent venligst på at fjernklienten accepterer din sessionsforespørgsel..."), + ("One-time Password", "Engangskode"), + ("Use one-time password", "Brug engangskode"), + ("One-time password length", "Engangskode længde"), + ("Request access to your device", "Efterspørg adgang til din enhed"), + ("Hide connection management window", "Skjul forbindelseshåndteringsvindue"), + ("hide_cm_tip", "Tillad at skjule, hvis der kun forbindes ved brug af midlertidige og permanente adgangskoder"), + ("wayland_experiment_tip", "Wayland understøttelse er stadigvæk under udvikling. Hvis du har brug for ubemandet adgang, bedes du bruge X11."), + ("Right click to select tabs", "Højreklik for at vælge faner"), + ("Skipped", "Sprunget over"), + ("Add to address book", "Tilføj til adressebog"), + ("Group", "Gruppe"), + ("Search", "Søg"), + ("Closed manually by web console", "Lukket ned manuelt af webkonsollen"), + ("Local keyboard type", "Lokal tastatur type"), + ("Select local keyboard type", "Vælg lokal tastatur type"), + ("software_render_tip", "Hvis du bruger et Nvidia grafikkort på Linux, og fjernskrivebordsvinduet lukker ned med det samme efter forbindelsen er oprettet, kan det hjælpe at skifte til Nouveau open-source driveren, og aktivere software rendering. Et genstart af RustDesk er nødvendigt."), + ("Always use software rendering", "Brug altid software rendering"), + ("config_input", "For at styre fjernskrivebordet med tastaturet, skal du give Rustdesk rettigheder til at optage tastetryk"), + ("config_microphone", "For at tale sammen over fjernstyring, skal du give RustDesk rettigheder til at optage lyd"), + ("request_elevation_tip", "Du kan også spørge om elevationsrettigheder, hvis der er nogen i nærheden af fjernenheden."), + ("Wait", "Vent"), + ("Elevation Error", "Elevationsfejl"), + ("Ask the remote user for authentication", "Spørg fjernbrugeren for godkendelse"), + ("Choose this if the remote account is administrator", "Vælg dette hvis fjernbrugeren er en administrator"), + ("Transmit the username and password of administrator", "Send brugernavnet og adgangskoden på administratoren"), + ("still_click_uac_tip", "Kræver stadigvæk at fjernbrugeren skal trykke OK på UAC vinduet ved kørsel af RustDesk."), + ("Request Elevation", "Efterspørger elevation"), + ("wait_accept_uac_tip", "Vent venligst på at fjernbrugeren accepterer UAC dialog forespørgslen."), + ("Elevate successfully", "Elevation lykkedes"), + ("uppercase", "store bogstaver"), + ("lowercase", "små bogstaver"), + ("digit", "ciffer"), + ("special character", "specielt tegn"), + ("length>=8", "længde>=8"), + ("Weak", "Svag"), + ("Medium", "Mellem"), + ("Strong", "Stærk"), + ("Switch Sides", "Skift sider"), + ("Please confirm if you want to share your desktop?", "Bekræft venligst, om du vil dele dit skrivebord?"), + ("Display", "Visning"), + ("Default View Style", "Standard visningsstil"), + ("Default Scroll Style", "Standard scrollestil"), + ("Default Image Quality", "Standard billedkvalitet"), + ("Default Codec", "Standard codec"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Andre standardindstillinger"), + ("Voice call", "Stemmeopkald"), + ("Text chat", "Tekstchat"), + ("Stop voice call", "Stop stemmeopkald"), + ("relay_hint_tip", "Det kan ske, at det ikke er muligt at forbinde direkte; du kan forsøge at forbinde via en relay-server. Derudover, hvis du ønsker at bruge en relay-server på dit første forsøg, kan du tilføje \"/r\" efter ID'et, eller bruge valgmuligheden \"Forbind altid via relay-server\" i fanen for seneste sessioner, hvis den findes."), + ("Reconnect", "Genopret"), + ("Codec", "Codec"), + ("Resolution", "Opløsning"), + ("No transfers in progress", "Ingen overførsler i gang"), + ("Set one-time password length", "Sæt engangsadgangskode længde"), + ("RDP Settings", "RDP indstillinger"), + ("Sort by", "Sortér efter"), + ("New Connection", "Ny forbindelse"), + ("Restore", "Gendan"), + ("Minimize", "Minimér"), + ("Maximize", "Maksimér"), + ("Your Device", "Din enhed"), + ("empty_recent_tip", "Ups, ingen seneste sessioner!\nTid til at oprette en ny."), + ("empty_favorite_tip", "Ingen yndlings modparter endnu?\nLad os finde én at forbinde til, og tilføje den til dine favoritter!"), + ("empty_lan_tip", "Åh nej, det ser ud til, at vi ikke kunne finde nogen modparter endnu."), + ("empty_address_book_tip", "Åh nej, det ser ud til at der ikke er nogle modparter der er tilføjet til din adressebog."), + ("Empty Username", "Tom brugernavn"), + ("Empty Password", "Tom adgangskode"), + ("Me", "Mig"), + ("identical_file_tip", "Denne fil er identisk med modpartens."), + ("show_monitors_tip", "Vis skærme i værktøjsbjælken"), + ("View Mode", "Visningstilstand"), + ("login_linux_tip", "Du skal logge på en fjernstyret Linux konto for at aktivere en X skrivebordssession"), + ("verify_rustdesk_password_tip", "Bekræft RustDesk adgangskode"), + ("remember_account_tip", "Husk denne konto"), + ("os_account_desk_tip", "Denne konto benyttes til at logge på fjernsystemet, og aktivere skrivebordssessionen i hovedløs tilstand"), + ("OS Account", "Styresystem konto"), + ("another_user_login_title_tip", "En anden bruger er allerede logget ind"), + ("another_user_login_text_tip", "Frakobl"), + ("xorg_not_found_title_tip", "Xorg ikke fundet"), + ("xorg_not_found_text_tip", "Installér venlist Xorg"), + ("no_desktop_title_tip", "Intet skrivebordsmiljø er tilgængeligt"), + ("no_desktop_text_tip", "Installér venligst GNOME skrivebordet"), + ("No need to elevate", "Ingen grund til at elevere"), + ("System Sound", "Systemlyd"), + ("Default", "Standard"), + ("New RDP", "Ny RDP"), + ("Fingerprint", "Fingeraftryk"), + ("Copy Fingerprint", "Kopiér fingeraftryk"), + ("no fingerprints", "Ingen fingeraftryk"), + ("Select a peer", "Vælg en peer"), + ("Select peers", "Vælg peers"), + ("Plugins", "Plugins"), + ("Uninstall", "Afinstallér"), + ("Update", "Opdatér"), + ("Enable", "Aktivér"), + ("Disable", "Deaktivér"), + ("Options", "Valgmuligheder"), + ("resolution_original_tip", "Original skærmopløsning"), + ("resolution_fit_local_tip", "Tilpas lokal skærmopløsning"), + ("resolution_custom_tip", "Bruger-tilpasset skærmopløsning"), + ("Collapse toolbar", "Skjul værktøjsbjælke"), + ("Accept and Elevate", "Acceptér og elevér"), + ("accept_and_elevate_btn_tooltip", "Acceptér forbindelsen og elevér UAC tilladelser"), + ("clipboard_wait_response_timeout_tip", "Tiden for at vente på en kopieringsforespørgsel udløb"), + ("Incoming connection", "Indgående forbindelse"), + ("Outgoing connection", "Udgående forbindelse"), + ("Exit", "Afslut"), + ("Open", "Åben"), + ("logout_tip", "Er du sikker på at du vil logge af?"), + ("Service", "Tjeneste"), + ("Start", "Start"), + ("Stop", "Stop"), + ("exceed_max_devices", "Du har nået det maksimale antal håndtérbare enheder."), + ("Sync with recent sessions", "Synkronisér med tidligere sessioner"), + ("Sort tags", "Sortér nøgleord"), + ("Open connection in new tab", "Åbn forbindelse i en ny fane"), + ("Move tab to new window", "Flyt fane i et nyt vindue"), + ("Can not be empty", "Kan ikke være tom"), + ("Already exists", "Findes allerede"), + ("Change Password", "Skift adgangskode"), + ("Refresh Password", "Genopfrisk adgangskode"), + ("ID", "ID"), + ("Grid View", "Gittervisning"), + ("List View", "Listevisning"), + ("Select", "Vælg"), + ("Toggle Tags", "Slå nøgleord til/fra"), + ("pull_ab_failed_tip", "Opdatering af adressebog mislykkedes"), + ("push_ab_failed_tip", "Synkronisering af adressebog til serveren mislykkedes"), + ("synced_peer_readded_tip", "Enhederne, som var til stede i de seneste sessioner, vil blive synkroniseret tilbage til adressebogen."), + ("Change Color", "Skift farve"), + ("Primary Color", "Primær farve"), + ("HSV Color", "HSV farve"), + ("Installation Successful!", "Installation fuldført!"), + ("Installation failed!", "Installation mislykkedes!"), + ("Reverse mouse wheel", "Invertér musehjul"), + ("{} sessions", "{} sessioner"), + ("scam_title", "ADVARSEL: Du kan blive SVINDLET!"), + ("scam_text1", "Hvis du taler telefon med en person du IKKE kender, og IKKE stoler på, som har bedt dig om at bruge RustDesk til at forbinde til din PC, stop med det samme, og læg på omgående."), + ("scam_text2", "Det er højest sandsynligvis en svinder som forsøger at stjæle dine penge eller andre personlige oplysninger."), + ("Don't show again", "Vis ikke igen"), + ("I Agree", "Jeg accepterer"), + ("Decline", "Afvis"), + ("Timeout in minutes", "Udløbstid i minutter"), + ("auto_disconnect_option_tip", "Luk automatisk indkommende sessioner ved inaktivitet"), + ("Connection failed due to inactivity", "Forbindelsen blev afbrudt grundet inaktivitet"), + ("Check for software update on startup", "Søg efter opdateringer ved opstart"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Opgradér venligst RustDesk Server Pro til version {} eller nyere!"), + ("pull_group_failed_tip", "Genindlæsning af gruppe mislykkedes"), + ("Filter by intersection", "Filtrér efter intersection"), + ("Remove wallpaper during incoming sessions", "Skjul baggrundsskærm ved indgående forbindelser"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Skærmen er slukket, skift til den første skærm."), + ("No displays", "Ingen skærme"), + ("Open in new window", "Åbn i et nyt vindue"), + ("Show displays as individual windows", "Vis skærme som selvstændige vinduer"), + ("Use all my displays for the remote session", "Brug alle mine skærme til fjernforbindelsen"), + ("selinux_tip", "SELinux er aktiveret på din enhed, som kan forhindre RustDesk i at køre normalt."), + ("Change view", "Skift visning"), + ("Big tiles", "Store fliser"), + ("Small tiles", "Små fliser"), + ("List", "Liste"), + ("Virtual display", "Virtuel skærm"), + ("Plug out all", "Frakobl alt"), + ("True color (4:4:4)", "True color (4:4:4)"), + ("Enable blocking user input", "Aktivér blokering af brugerstyring"), + ("id_input_tip", "Du kan indtaste ét ID, en direkte IP adresse, eller et domæne med en port (:).\nHvis du ønsker at forbinde til en enhed på en anden server, tilføj da server adressen (@?key=), fx,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHvis du ønsker adgang til en enhed på en offentlig server, indtast venligst \"@offentlig server\", nøglen er ikke nødvendig for offentlige servere.\n\nHvis du gerne vil tvinge brugen af en relay-forbindelse på den første forbindelse, tilføj \"/r\" efter ID'et, fx, \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Tilstand 1"), + ("privacy_mode_impl_virtual_display_tip", "Tilstand 2"), + ("Enter privacy mode", "Start privatlivstilstand"), + ("Exit privacy mode", "Afslut privatlivstilstand"), + ("idd_not_support_under_win10_2004_tip", "Indirekte grafik drivere er ikke understøttet. Windows 10 version 2004 eller nyere er påkrævet."), + ("input_source_1_tip", "Input kilde 1"), + ("input_source_2_tip", "Input kilde 2"), + ("Swap control-command key", "Byt rundt på Control & Command tasterne"), + ("swap-left-right-mouse", "Byt rundt på venstre og højre musetaster"), + ("2FA code", "To-faktor kode"), + ("More", "Mere"), + ("enable-2fa-title", "Tænd for to-faktor godkendelse"), + ("enable-2fa-desc", "Åbn din godkendelsesapp nu. Du kan bruge en godkendelsesapp så som Authy, Microsoft eller Google Authenticator på din telefon eller din PC.\n\nScan QR koden med din app og indtast koden som din app fremviser, for at aktivere for to-faktor godkendelse."), + ("wrong-2fa-code", "Kan ikke verificere koden. Forsikr at koden og tidsindstillingerne på enheden er korrekte"), + ("enter-2fa-title", "To-faktor godkendelse"), + ("Email verification code must be 6 characters.", "E-mail bekræftelseskode skal være mindst 6 tegn"), + ("2FA code must be 6 digits.", "To-faktor kode skal være mindst 6 cifre"), + ("Multiple Windows sessions found", "Flere Windows sessioner fundet"), + ("Please select the session you want to connect to", "Vælg venligst sessionen du ønsker at forbinde til"), + ("powered_by_me", "Drives af RustDesk"), + ("outgoing_only_desk_tip", "Dette er en brugertilpasset udgave.\nDu kan forbinde til andre enheder, men andre enheder kan ikke forbinde til din enhed."), + ("preset_password_warning", "Denne brugertilpassede udgave har en forudbestemt adgangskode. Alle der kender til denne adgangskode, kan få fuld adgang til din enhed. Hvis du ikke forventede dette, bør du afinstallere denne udgave af RustDesk med det samme."), + ("Security Alert", "Sikkerhedsalarm"), + ("My address book", "Min adressebog"), + ("Personal", "Personlig"), + ("Owner", "Ejer"), + ("Set shared password", "Sæt delt adgangskode"), + ("Exist in", "Findes i"), + ("Read-only", "Skrivebeskyttet"), + ("Read/Write", "Læse/Skrive"), + ("Full Control", "Fuld kontrol"), + ("share_warning_tip", "Felterne for oven er delt og synlige for andre."), + ("Everyone", "Alle"), + ("ab_web_console_tip", "Mere på web konsollen"), + ("allow-only-conn-window-open-tip", "Tillad kun fjernforbindelser hvis RustDesk vinduet er synligt"), + ("no_need_privacy_mode_no_physical_displays_tip", "Ingen fysiske skærme, ingen nødvendighed for at bruge privatlivstilstanden."), + ("Follow remote cursor", "Følg musemarkør på fjernforbindelse"), + ("Follow remote window focus", "Følg vinduefokus på fjernforbindelse"), + ("default_proxy_tip", "Protokollen og porten som anvendes som standard er Socks5 og 1080"), + ("no_audio_input_device_tip", "Ingen lydinput enhed fundet"), + ("Incoming", "Indgående"), + ("Outgoing", "Udgående"), + ("Clear Wayland screen selection", "Ryd Wayland skærmvalg"), + ("clear_Wayland_screen_selection_tip", "Efter at fravælge den valgte skærm, kan du genvælge skærmen som skal deles."), + ("confirm_clear_Wayland_screen_selection_tip", "Er du sikker på at du vil fjerne Wayland skærmvalget?"), + ("android_new_voice_call_tip", "Du har modtaget en ny stemmeopkaldsforespørgsel. Hvis du accepterer, vil lyden skifte til stemmekommunikation."), + ("texture_render_tip", "Brug tekstur-rendering for at gøre billedkvaliteten blødere. Du kan også prøve at deaktivere denne funktion, hvis du oplever problemer."), + ("Use texture rendering", "Anvend tekstur-rendering"), + ("Floating window", "Svævende vindue"), + ("floating_window_tip", "Det hjælper på at RustDesk baggrundstjenesten kører"), + ("Keep screen on", "Hold skærmen tændt"), + ("Never", "Aldrig"), + ("During controlled", "Imens under kontrol"), + ("During service is on", "Imens tjenesten kører"), + ("Capture screen using DirectX", "Optag skærm med DirectX"), + ("Back", "Tilbage"), + ("Apps", "Apps"), + ("Volume up", "Skru op for lyd"), + ("Volume down", "Skru ned for lyd"), + ("Power", "Tænd/Sluk"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Hvis du aktiverer denne funktion, kan du modtage to-faktor godkendelseskoden fra din robot. Den kan også fungere som en notifikation for forbindelsesanmodninger."), + ("enable-bot-desc", "1. Åbn en chat med @BotFather.\n2. Send kommandoen \"/newbot\". Du vil modtage en nøgle efter at have gennemført dette trin.\n3. Start en chat med din nyoprettede bot. Send en besked som begynder med skråstreg \"/\", som fx \"/hello\", for at aktivere den.\n"), + ("cancel-2fa-confirm-tip", "Er du sikker på at du vil afbryde to-faktor godkendelse?"), + ("cancel-bot-confirm-tip", "Er du sikker på at du vil afbryde Telegram robotten?"), + ("About RustDesk", "Om RustDesk"), + ("Send clipboard keystrokes", "Send udklipsholder tastetryk"), + ("network_error_tip", "Tjek venligst din internetforbindelse, og forsøg igen."), + ("Unlock with PIN", "Lås op med PIN"), + ("Requires at least {} characters", "Kræver mindst {} tegn"), + ("Wrong PIN", "Forkert PIN"), + ("Set PIN", "Sæt PIN"), + ("Enable trusted devices", "Aktivér troværdige enheder"), + ("Manage trusted devices", "Administrér troværdige enheder"), + ("Platform", "Platform"), + ("Days remaining", "Dage tilbage"), + ("enable-trusted-devices-tip", "Spring to-faktor godkendelse over på troværdige enheder"), + ("Parent directory", "mappe"), + ("Resume", "Fortsæt"), + ("Invalid file name", "Ugyldigt filnavn"), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Opgrader venligst RustDesk-klienten til version {} eller nyere på fjernsiden!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Se kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Fortsæt med {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/de.rs b/vendor/rustdesk/src/lang/de.rs new file mode 100644 index 0000000..7d18cd7 --- /dev/null +++ b/vendor/rustdesk/src/lang/de.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Ihr Desktop"), + ("desk_tip", "Mit dieser ID und diesem Passwort kann auf Ihren Desktop zugegriffen werden."), + ("Password", "Passwort"), + ("Ready", "Bereit"), + ("Established", "Verbunden"), + ("connecting_status", "Verbinden mit dem RustDesk-Netzwerk …"), + ("Enable service", "Vermittlungsdienst aktivieren"), + ("Start service", "Vermittlungsdienst starten"), + ("Service is running", "Vermittlungsdienst aktiv"), + ("Service is not running", "Vermittlungsdienst deaktiviert"), + ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Netzwerkverbindung."), + ("Control Remote Desktop", "Entfernten Desktop steuern"), + ("Transfer file", "Datei übertragen"), + ("Connect", "Verbinden"), + ("Recent sessions", "Letzte Sitzungen"), + ("Address book", "Adressbuch"), + ("Confirmation", "Bestätigung"), + ("TCP tunneling", "TCP-Tunnelung"), + ("Remove", "Entfernen"), + ("Refresh random password", "Zufälliges Passwort erzeugen"), + ("Set your own password", "Eigenes Passwort festlegen"), + ("Enable keyboard/mouse", "Tastatur und Maus aktivieren"), + ("Enable clipboard", "Zwischenablage aktivieren"), + ("Enable file transfer", "Dateiübertragung aktivieren"), + ("Enable TCP tunneling", "TCP-Tunnelung aktivieren"), + ("IP Whitelisting", "IP-Whitelist"), + ("ID/Relay Server", "ID/Relay-Server"), + ("Import server config", "Serverkonfiguration importieren"), + ("Export Server Config", "Serverkonfiguration exportieren"), + ("Import server configuration successfully", "Serverkonfiguration erfolgreich importiert"), + ("Export server configuration successfully", "Serverkonfiguration erfolgreich exportiert"), + ("Invalid server configuration", "Ungültige Serverkonfiguration"), + ("Clipboard is empty", "Zwischenablage ist leer"), + ("Stop service", "Vermittlungsdienst stoppen"), + ("Change ID", "ID ändern"), + ("Your new ID", "Ihre neue ID"), + ("length %min% to %max%", "Länge %min% bis %max%"), + ("starts with a letter", "Beginnt mit Buchstabe"), + ("allowed characters", "Erlaubte Zeichen"), + ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9, - (Bindestrich) und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), + ("Website", "Webseite"), + ("About", "Über"), + ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), + ("Privacy Statement", "Datenschutz"), + ("Mute", "Stummschalten"), + ("Build Date", "Erstelldatum"), + ("Version", "Version"), + ("Home", "Startseite"), + ("Audio Input", "Audioeingang"), + ("Enhancements", "Verbesserungen"), + ("Hardware Codec", "Hardware-Codec"), + ("Adaptive bitrate", "Bitrate automatisch anpassen"), + ("ID Server", "ID-Server"), + ("Relay Server", "Relay-Server"), + ("API Server", "API-Server"), + ("invalid_http", "Muss mit http:// oder https:// beginnen"), + ("Invalid IP", "Ungültige IP-Adresse"), + ("Invalid format", "Ungültiges Format"), + ("server_not_support", "Diese Funktion wird noch nicht vom Server unterstützt."), + ("Not available", "Nicht verfügbar"), + ("Too frequent", "Zu häufig"), + ("Cancel", "Abbrechen"), + ("Skip", "Überspringen"), + ("Close", "Schließen"), + ("Retry", "Erneut versuchen"), + ("OK", "OK"), + ("Password Required", "Passwort erforderlich"), + ("Please enter your password", "Bitte geben Sie Ihr Passwort ein"), + ("Remember password", "Passwort merken"), + ("Wrong Password", "Falsches Passwort"), + ("Do you want to enter again?", "Erneut verbinden?"), + ("Connection Error", "Verbindungsfehler"), + ("Error", "Fehler"), + ("Reset by the peer", "Verbindung wurde von der Gegenstelle zurückgesetzt."), + ("Connecting...", "Verbindung wird hergestellt …"), + ("Connection in progress. Please wait.", "Die Verbindung wird hergestellt. Bitte warten …"), + ("Please try 1 minute later", "Bitte versuchen Sie es später erneut"), + ("Login Error", "Anmeldefehler"), + ("Successful", "Erfolgreich"), + ("Connected, waiting for image...", "Verbindung hergestellt. Warte auf anderen Bildschirm …"), + ("Name", "Name"), + ("Type", "Typ"), + ("Modified", "Geändert"), + ("Size", "Größe"), + ("Show Hidden Files", "Versteckte Dateien anzeigen"), + ("Receive", "Empfangen"), + ("Send", "Senden"), + ("Refresh File", "Datei aktualisieren"), + ("Local", "Lokal"), + ("Remote", "Entfernt"), + ("Remote Computer", "Entfernter Computer"), + ("Local Computer", "Dieser Computer"), + ("Confirm Delete", "Löschen bestätigen"), + ("Delete", "Löschen"), + ("Properties", "Eigenschaften"), + ("Multi Select", "Mehrfachauswahl"), + ("Select All", "Alles auswählen"), + ("Unselect All", "Alles abwählen"), + ("Empty Directory", "Leerer Ordner"), + ("Not an empty directory", "Ordner ist nicht leer."), + ("Are you sure you want to delete this file?", "Sind Sie sicher, dass Sie diese Datei löschen wollen?"), + ("Are you sure you want to delete this empty directory?", "Sind Sie sicher, dass Sie diesen leeren Ordner löschen möchten?"), + ("Are you sure you want to delete the file of this directory?", "Sind Sie sicher, dass Sie die Datei dieses Ordners löschen möchten?"), + ("Do this for all conflicts", "Für alle Konflikte merken"), + ("This is irreversible!", "Dies kann nicht rückgängig gemacht werden!"), + ("Deleting", "Löschen"), + ("files", "Dateien"), + ("Waiting", "Warten"), + ("Finished", "Fertiggestellt"), + ("Speed", "Geschwindigkeit"), + ("Custom Image Quality", "Benutzerdefinierte Bildqualität"), + ("Privacy mode", "Datenschutzmodus"), + ("Block user input", "Benutzereingaben blockieren"), + ("Unblock user input", "Benutzereingaben freigeben"), + ("Adjust Window", "Fenster anpassen"), + ("Original", "Original"), + ("Shrink", "Verkleinern"), + ("Stretch", "Strecken"), + ("Scrollbar", "Scroll-Leiste"), + ("ScrollAuto", "Automatisch scrollen"), + ("Good image quality", "Hohe Bildqualität"), + ("Balanced", "Ausgeglichene Bildqualität"), + ("Optimize reaction time", "Reaktionszeit optimieren"), + ("Custom", "Benutzerdefiniert"), + ("Show remote cursor", "Entfernten Cursor anzeigen"), + ("Show quality monitor", "Qualitätsüberwachung anzeigen"), + ("Disable clipboard", "Zwischenablage deaktivieren"), + ("Lock after session end", "Nach Sitzungsende sperren"), + ("Insert Ctrl + Alt + Del", "Strg + Alt + Entf senden"), + ("Insert Lock", "Win+L (Sperren) senden"), + ("Refresh", "Aktualisieren"), + ("ID does not exist", "Diese ID existiert nicht."), + ("Failed to connect to rendezvous server", "Verbindung zum Rendezvous-Server fehlgeschlagen"), + ("Please try later", "Bitte versuchen Sie es später erneut."), + ("Remote desktop is offline", "Entfernter Desktop ist offline."), + ("Key mismatch", "Schlüssel stimmen nicht überein."), + ("Timeout", "Zeitüberschreitung"), + ("Failed to connect to relay server", "Verbindung zum Relay-Server ist fehlgeschlagen"), + ("Failed to connect via rendezvous server", "Verbindung über Rendezvous-Server ist fehlgeschlagen"), + ("Failed to connect via relay server", "Verbindung über Relay-Server ist fehlgeschlagen"), + ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten Desktop ist fehlgeschlagen"), + ("Set Password", "Passwort festlegen"), + ("OS Password", "Betriebssystem-Passwort"), + ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten und installieren RustDesk auf dem System."), + ("Click to upgrade", "Zum Upgraden klicken"), + ("Configure", "Konfigurieren"), + ("config_acc", "Um Ihren PC aus der Ferne zu steuern, müssen Sie RustDesk Zugriffsrechte erteilen."), + ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk die Berechtigung \"Bildschirmaufnahme\" erteilen."), + ("Installing ...", "Wird installiert …"), + ("Install", "Installieren"), + ("Installation", "Installation"), + ("Installation Path", "Installationspfad"), + ("Create start menu shortcuts", "Verknüpfung im Startmenü erstellen"), + ("Create desktop icon", "Desktop-Verknüpfung erstellen"), + ("agreement_tip", "Durch die Installation akzeptieren Sie die Lizenzvereinbarung."), + ("Accept and Install", "Akzeptieren und Installieren"), + ("End-user license agreement", "Lizenzvereinbarung für Endbenutzer"), + ("Generating ...", "Wird generiert …"), + ("Your installation is lower version.", "Ihre Version ist veraltet."), + ("not_close_tcp_tip", "Schließen Sie dieses Fenster nicht, solange Sie den Tunnel benutzen."), + ("Listening ...", "Lauschen …"), + ("Remote Host", "Entfernter PC"), + ("Remote Port", "Entfernter Port"), + ("Action", "Aktion"), + ("Add", "Hinzufügen"), + ("Local Port", "Lokaler Port"), + ("Local Address", "Lokale Adresse"), + ("Change Local Port", "Lokalen Port ändern"), + ("setup_server_tip", "für eine schnellere Verbindung richten Sie bitte Ihren eigenen Server ein."), + ("Too short, at least 6 characters.", "Zu kurz, mindestens 6 Zeichen."), + ("The confirmation is not identical.", "Die Passwörter stimmen nicht überein."), + ("Permissions", "Berechtigungen"), + ("Accept", "Akzeptieren"), + ("Dismiss", "Ablehnen"), + ("Disconnect", "Verbindung trennen"), + ("Enable file copy and paste", "Kopieren und Einfügen von Dateien zulassen"), + ("Connected", "Verbunden"), + ("Direct and encrypted connection", "Direkte und verschlüsselte Verbindung"), + ("Relayed and encrypted connection", "Vermittelte und verschlüsselte Verbindung"), + ("Direct and unencrypted connection", "Direkte und unverschlüsselte Verbindung"), + ("Relayed and unencrypted connection", "Vermittelte und unverschlüsselte Verbindung"), + ("Enter Remote ID", "Entfernte ID eingeben"), + ("Enter your password", "Geben Sie Ihr Passwort ein"), + ("Logging in...", "Anmelden …"), + ("Enable RDP session sharing", "RDP-Sitzungsfreigabe aktivieren"), + ("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Nach Sitzungsende sperren\" aktiviert haben)"), + ("Enable direct IP access", "Direkten IP-Zugang aktivieren"), + ("Rename", "Umbenennen"), + ("Space", "Speicherplatz"), + ("Create desktop shortcut", "Desktop-Verknüpfung erstellen"), + ("Change Path", "Pfad ändern"), + ("Create Folder", "Ordner erstellen"), + ("Please enter the folder name", "Bitte geben Sie den Ordnernamen ein"), + ("Fix it", "Reparieren"), + ("Warning", "Warnung"), + ("Login screen using Wayland is not supported", "Anmeldebildschirm mit Wayland wird nicht unterstützt."), + ("Reboot required", "Neustart erforderlich"), + ("Unsupported display server", "Nicht unterstützter Anzeigeserver"), + ("x11 expected", "X11 erwartet"), + ("Port", "Port"), + ("Settings", "Einstellungen"), + ("Username", "Benutzername"), + ("Invalid port", "Ungültiger Port"), + ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen."), + ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"), + ("Run without install", "Ohne Installation ausführen"), + ("Connect via relay", "Über Relay-Server verbinden"), + ("Always connect via relay", "Immer über Relay-Server verbinden"), + ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), + ("Login", "Anmelden"), + ("Verify", "Überprüfen"), + ("Remember me", "Login merken"), + ("Trust this device", "Diesem Gerät vertrauen"), + ("Verification code", "Verifizierungscode"), + ("verification_tip", "Ein Verifizierungscode wurde an die registrierte E-Mail-Adresse gesendet. Geben Sie den Verifizierungscode ein, um sich erneut anzumelden."), + ("Logout", "Abmelden"), + ("Tags", "Tags"), + ("Search ID", "ID suchen"), + ("whitelist_sep", "Getrennt durch Komma, Semikolon, Leerzeichen oder Zeilenumbruch"), + ("Add ID", "ID hinzufügen"), + ("Add Tag", "Tag hinzufügen"), + ("Unselect all tags", "Alle Tags abwählen"), + ("Network error", "Netzwerkfehler"), + ("Username missed", "Benutzername fehlt"), + ("Password missed", "Passwort fehlt"), + ("Wrong credentials", "Falsche Anmeldedaten"), + ("The verification code is incorrect or has expired", "Der Verifizierungscode ist falsch oder abgelaufen"), + ("Edit Tag", "Tag bearbeiten"), + ("Forget Password", "Gespeichertes Passwort löschen"), + ("Favorites", "Favoriten"), + ("Add to Favorites", "Zu Favoriten hinzufügen"), + ("Remove from Favorites", "Aus Favoriten entfernen"), + ("Empty", "Keine Einträge"), + ("Invalid folder name", "Ungültiger Ordnername"), + ("Socks5 Proxy", "SOCKS5-Proxy"), + ("Socks5/Http(s) Proxy", "SOCKS5/HTTP(S)-Proxy"), + ("Discovered", "Im LAN erkannt"), + ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein."), + ("Remote ID", "Entfernte ID"), + ("Paste", "Einfügen"), + ("Paste here?", "Hier einfügen?"), + ("Are you sure to close the connection?", "Möchten Sie diese Verbindung wirklich schließen?"), + ("Download new version", "Neue Version herunterladen"), + ("Touch mode", "Touch-Modus"), + ("Mouse mode", "Mausmodus"), + ("One-Finger Tap", "1-Finger-Tipp"), + ("Left Mouse", "Linksklick"), + ("One-Long Tap", "1-Finger-Halten"), + ("Two-Finger Tap", "2-Finger-Tipp"), + ("Right Mouse", "Rechtsklick"), + ("One-Finger Move", "Einen Finger bewegen"), + ("Double Tap & Move", "Doppeltippen und bewegen"), + ("Mouse Drag", "Maus bewegen"), + ("Three-Finger vertically", "Drei Finger vertikal bewegen"), + ("Mouse Wheel", "Mausrad"), + ("Two-Finger Move", "Zwei Finger bewegen"), + ("Canvas Move", "Sichtfeld bewegen"), + ("Pinch to Zoom", "2-Finger-Zoom"), + ("Canvas Zoom", "Sichtfeld-Zoom"), + ("Reset canvas", "Sichtfeld zurücksetzen"), + ("No permission of file transfer", "Keine Berechtigung für die Dateiübertragung"), + ("Note", "Hinweis"), + ("Connection", "Verbindung"), + ("Share screen", "Bildschirm freigeben"), + ("Chat", "Chat"), + ("Total", "Gesamt"), + ("items", "Einträge"), + ("Selected", "ausgewählt"), + ("Screen Capture", "Bildschirmaufnahme"), + ("Input Control", "Eingabesteuerung"), + ("Audio Capture", "Audioaufnahme"), + ("Do you accept?", "Verbindung zulassen?"), + ("Open System Setting", "Systemeinstellung öffnen"), + ("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"), + ("android_input_permission_tip1", "Damit ein entferntes Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."), + ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen Sie \"Installierte Dienste\" und schalten Sie den Dienst \"RustDesk Input\" ein."), + ("android_new_connection_tip", "möchte Ihr Gerät steuern."), + ("android_service_will_start_tip", "Durch das Aktivieren der Bildschirmfreigabe wird der Dienst automatisch gestartet, sodass andere Geräte dieses Android-Gerät steuern können."), + ("android_stop_service_tip", "Durch das Deaktivieren des Dienstes werden automatisch alle hergestellten Verbindungen getrennt."), + ("android_version_audio_tip", "Ihre Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher, falls möglich."), + ("android_start_service_tip", "Tippen Sie auf \"Vermittlungsdienst starten\" oder aktivieren Sie die Berechtigung \"Bildschirmaufnahme\", um den Bildschirmfreigabedienst zu starten."), + ("android_permission_may_not_change_tip", "Die Berechtigungen für bestehende Verbindungen werden erst nach einer erneuten Verbindung geändert."), + ("Account", "Konto"), + ("Overwrite", "Überschreiben"), + ("This file exists, skip or overwrite this file?", "Diese Datei existiert; überspringen oder überschreiben?"), + ("Quit", "Beenden"), + ("Help", "Hilfe"), + ("Failed", "Fehlgeschlagen"), + ("Succeeded", "Erfolgreich"), + ("Someone turns on privacy mode, exit", "Jemand hat den Datenschutzmodus aktiviert, wird beendet …"), + ("Unsupported", "Nicht unterstützt"), + ("Peer denied", "Die Gegenstelle hat die Verbindung abgelehnt."), + ("Please install plugins", "Bitte installieren Sie Plugins"), + ("Peer exit", "Die Gegenstelle hat die Verbindung getrennt."), + ("Failed to turn off", "Ausschalten fehlgeschlagen"), + ("Turned off", "Ausgeschaltet"), + ("Language", "Sprache"), + ("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"), + ("Ignore Battery Optimizations", "Akkuoptimierung ignorieren"), + ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Akkuoptimierung öffnen?"), + ("Start on boot", "Beim Booten starten"), + ("Start the screen sharing service on boot, requires special permissions", "Bildschirmfreigabedienst beim Booten starten, erfordert zusätzliche Berechtigungen"), + ("Connection not allowed", "Verbindung abgelehnt"), + ("Legacy mode", "Kompatibilitätsmodus"), + ("Map mode", "Zuordnungsmodus"), + ("Translate mode", "Übersetzungsmodus"), + ("Use permanent password", "Permanentes Passwort verwenden"), + ("Use both passwords", "Beide Passwörter verwenden"), + ("Set permanent password", "Permanentes Passwort festlegen"), + ("Enable remote restart", "Entfernten Neustart aktivieren"), + ("Restart remote device", "Entferntes Gerät neu starten"), + ("Are you sure you want to restart", "Möchten Sie das entfernte Gerät wirklich neu starten?"), + ("Restarting remote device", "Entferntes Gerät wird neu gestartet"), + ("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem permanenten Passwort erneut."), + ("Copied", "Kopiert"), + ("Exit Fullscreen", "Vollbild beenden"), + ("Fullscreen", "Vollbild"), + ("Mobile Actions", "Mobile Aktionen"), + ("Select Monitor", "Bildschirm auswählen"), + ("Control Actions", "Aktionen"), + ("Display Settings", "Anzeigeeinstellungen"), + ("Ratio", "Verhältnis"), + ("Image Quality", "Bildqualität"), + ("Scroll Style", "Scroll-Stil"), + ("Show Toolbar", "Symbolleiste anzeigen"), + ("Hide Toolbar", "Symbolleiste ausblenden"), + ("Direct Connection", "Direkte Verbindung"), + ("Relay Connection", "Relay-Verbindung"), + ("Secure Connection", "Sichere Verbindung"), + ("Insecure Connection", "Unsichere Verbindung"), + ("Scale original", "Keine Skalierung"), + ("Scale adaptive", "Anpassbare Skalierung"), + ("General", "Allgemein"), + ("Security", "Sicherheit"), + ("Theme", "Farbgebung"), + ("Dark Theme", "Dunkle Farbgebung"), + ("Light Theme", "Helle Farbgebung"), + ("Dark", "Dunkel"), + ("Light", "Hell"), + ("Follow System", "Systemstandard"), + ("Enable hardware codec", "Hardware-Codec aktivieren"), + ("Unlock Security Settings", "Sicherheitseinstellungen entsperren"), + ("Enable audio", "Audio aktivieren"), + ("Unlock Network Settings", "Netzwerkeinstellungen entsperren"), + ("Server", "Server"), + ("Direct IP Access", "Direkter IP-Zugang"), + ("Proxy", "Proxy"), + ("Apply", "Anwenden"), + ("Disconnect all devices?", "Alle Geräte trennen?"), + ("Clear", "Zurücksetzen"), + ("Audio Input Device", "Audioeingabegerät"), + ("Use IP Whitelisting", "IP-Whitelist verwenden"), + ("Network", "Netzwerk"), + ("Pin Toolbar", "Symbolleiste anpinnen"), + ("Unpin Toolbar", "Symbolleiste lösen"), + ("Recording", "Aufnahme"), + ("Directory", "Verzeichnis"), + ("Automatically record incoming sessions", "Eingehende Sitzungen automatisch aufzeichnen"), + ("Automatically record outgoing sessions", "Ausgehende Sitzungen automatisch aufzeichnen"), + ("Change", "Ändern"), + ("Start session recording", "Sitzungsaufzeichnung starten"), + ("Stop session recording", "Sitzungsaufzeichnung beenden"), + ("Enable recording session", "Sitzungsaufzeichnung aktivieren"), + ("Enable LAN discovery", "LAN-Erkennung aktivieren"), + ("Deny LAN discovery", "LAN-Erkennung verbieten"), + ("Write a message", "Nachricht schreiben"), + ("Prompt", "Meldung"), + ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten …"), + ("elevated_foreground_window_tip", "Das aktuell geöffnete Fenster des ferngesteuerten Computers erfordert höhere Rechte. Deshalb ist es derzeit nicht möglich, die Maus und die Tastatur zu verwenden. Bitten Sie den Nutzer, dessen Computer Sie fernsteuern, das Fenster zu minimieren oder die Rechte zu erhöhen. Um dieses Problem zukünftig zu vermeiden, wird empfohlen, die Software auf dem ferngesteuerten Computer zu installieren."), + ("Disconnected", "Verbindung abgebrochen"), + ("Other", "Weitere Einstellungen"), + ("Confirm before closing multiple tabs", "Nachfragen, wenn mehrere Tabs geschlossen werden"), + ("Keyboard Settings", "Tastatureinstellungen"), + ("Full Access", "Vollzugriff"), + ("Screen Share", "Bildschirmfreigabe"), + ("ubuntu-21-04-required", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), + ("wayland-requires-higher-linux-version", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."), + ("xdp-portal-unavailable", "Die Bildschirmaufnahme mit Wayland ist fehlgeschlagen. Das XDG-Desktop-Portal ist möglicherweise abgestürzt oder nicht verfügbar. Versuchen Sie, es mit `systemctl --user restart xdg-desktop-portal` neu zu starten."), + ("JumpLink", "Anzeigen"), + ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."), + ("Show RustDesk", "RustDesk anzeigen"), + ("This PC", "Dieser PC"), + ("or", "oder"), + ("Elevate", "Zugriff gewähren"), + ("Zoom cursor", "Cursor vergrößern"), + ("Accept sessions via password", "Sitzung mit Passwort bestätigen"), + ("Accept sessions via click", "Sitzung mit einem Klick bestätigen"), + ("Accept sessions via both", "Sitzung mit Klick und Passwort bestätigen"), + ("Please wait for the remote side to accept your session request...", "Bitte warten Sie, bis die Gegenseite Ihre Sitzungsanfrage akzeptiert hat …"), + ("One-time Password", "Einmalpasswort"), + ("Use one-time password", "Einmalpasswort verwenden"), + ("One-time password length", "Länge des Einmalpassworts"), + ("Request access to your device", "Zugriff auf Ihr Gerät anfordern"), + ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"), + ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff über ein permanentes Passwort erfolgt."), + ("wayland_experiment_tip", "Die Unterstützung von Wayland ist nur experimentell. Bitte nutzen Sie X11, wenn Sie einen unbeaufsichtigten Zugriff benötigen."), + ("Right click to select tabs", "Tabs mit rechtem Mausklick auswählen"), + ("Skipped", "Übersprungen"), + ("Add to address book", "Zum Adressbuch hinzufügen"), + ("Group", "Gruppe"), + ("Search", "Suchen"), + ("Closed manually by web console", "Manuell über die Webkonsole geschlossen"), + ("Local keyboard type", "Lokaler Tastaturtyp"), + ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), + ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte unter Linux verwenden und sich das entfernte Fenster sofort nach dem Verbindungsaufbau schließt, kann ein Wechsel zum Open-Source-Treiber Nouveau und die Verwendung von Software-Rendering helfen. Ein Neustart der Software ist erforderlich."), + ("Always use software rendering", "Software-Rendering immer verwenden"), + ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk die Berechtigung \"Eingabeüberwachung\" erteilen."), + ("config_microphone", "Um aus der Ferne sprechen zu können, müssen Sie RustDesk die Berechtigung \"Audio aufzeichnen\" erteilen."), + ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), + ("Wait", "Warten"), + ("Elevation Error", "Berechtigungsfehler"), + ("Ask the remote user for authentication", "Den entfernten Benutzer zur Authentifizierung auffordern"), + ("Choose this if the remote account is administrator", "Wählen Sie dies, wenn das entfernte Konto Administrator ist."), + ("Transmit the username and password of administrator", "Benutzernamen und Passwort des Administrators übertragen"), + ("still_click_uac_tip", "Der entfernte Benutzer muss immer noch im UAC-Fenster von RustDesk auf \"Ja\" klicken."), + ("Request Elevation", "Erhöhte Rechte anfordern"), + ("wait_accept_uac_tip", "Bitte warten Sie, bis der entfernte Benutzer den UAC-Dialog akzeptiert hat."), + ("Elevate successfully", "Erhöhung der Rechte erfolgreich"), + ("uppercase", "Großbuchstaben"), + ("lowercase", "Kleinbuchstaben"), + ("digit", "Ziffern"), + ("special character", "Sonderzeichen"), + ("length>=8", "Länge ≥ 8"), + ("Weak", "Schwach"), + ("Medium", "Mittel"), + ("Strong", "Stark"), + ("Switch Sides", "Seiten wechseln"), + ("Please confirm if you want to share your desktop?", "Bitte bestätigen Sie, wenn Sie Ihren Desktop freigeben möchten."), + ("Display", "Bildschirm"), + ("Default View Style", "Standard-Ansichtsstil"), + ("Default Scroll Style", "Standard-Scroll-Stil"), + ("Default Image Quality", "Standard-Bildqualität"), + ("Default Codec", "Standard-Codec"), + ("Bitrate", "Bitrate"), + ("FPS", "fps"), + ("Auto", "Automatisch"), + ("Other Default Options", "Weitere Standardeinstellungen"), + ("Voice call", "Sprachanruf"), + ("Text chat", "Text-Chat"), + ("Stop voice call", "Sprachanruf beenden"), + ("relay_hint_tip", "Wenn eine direkte Verbindung nicht möglich ist, können Sie versuchen, eine Verbindung über einen Relay-Server herzustellen.\nWenn Sie eine Relay-Verbindung beim ersten Versuch herstellen möchten, können Sie das Suffix \"/r\" an die ID anhängen oder die Option \"Immer über Relay-Server verbinden\" in der Liste der letzten Sitzungen auswählen, sofern diese vorhanden ist."), + ("Reconnect", "Erneut verbinden"), + ("Codec", "Codec"), + ("Resolution", "Auflösung"), + ("No transfers in progress", "Keine Übertragungen im Gange"), + ("Set one-time password length", "Länge des Einmalpassworts festlegen"), + ("RDP Settings", "RDP-Einstellungen"), + ("Sort by", "Sortieren nach"), + ("New Connection", "Neue Verbindung"), + ("Restore", "Verkleinern"), + ("Minimize", "Minimieren"), + ("Maximize", "Maximieren"), + ("Your Device", "Ihr Gerät"), + ("empty_recent_tip", "Ups, keine aktuellen Sitzungen!\nZeit, eine neue zu planen."), + ("empty_favorite_tip", "Noch keine favorisierte Gegenstelle?\nLassen Sie uns jemanden finden, mit dem wir uns verbinden können und fügen Sie ihn zu Ihren Favoriten hinzu!"), + ("empty_lan_tip", "Oh nein, es sieht so aus, als hätten wir noch keine Gegenstelle entdeckt."), + ("empty_address_book_tip", "Oh je, es scheint, dass in Ihrem Adressbuch derzeit keine Gegenstellen aufgeführt sind."), + ("Empty Username", "Leerer Benutzername"), + ("Empty Password", "Leeres Passwort"), + ("Me", "Ich"), + ("identical_file_tip", "Diese Datei ist identisch mit der Datei der Gegenstelle."), + ("show_monitors_tip", "Bildschirme in der Symbolleiste anzeigen"), + ("View Mode", "Ansichtsmodus"), + ("login_linux_tip", "Sie müssen sich an einem entfernten Linux-Konto anmelden, um eine X-Desktop-Sitzung zu eröffnen."), + ("verify_rustdesk_password_tip", "RustDesk-Passwort bestätigen"), + ("remember_account_tip", "Dieses Konto merken"), + ("os_account_desk_tip", "Dieses Konto wird verwendet, um sich beim entfernten Betriebssystem anzumelden und die Desktop-Sitzung im Headless-Modus zu aktivieren."), + ("OS Account", "Betriebssystem-Konto"), + ("another_user_login_title_tip", "Ein anderer Benutzer ist bereits angemeldet."), + ("another_user_login_text_tip", "Trennen"), + ("xorg_not_found_title_tip", "Xorg nicht gefunden."), + ("xorg_not_found_text_tip", "Bitte installieren Sie Xorg."), + ("no_desktop_title_tip", "Es ist keine Desktopumgebung verfügbar."), + ("no_desktop_text_tip", "Bitte installieren Sie den GNOME-Desktop."), + ("No need to elevate", "Erhöhung der Rechte nicht erforderlich"), + ("System Sound", "Systemsound"), + ("Default", "Systemstandard"), + ("New RDP", "Neue RDP-Verbindung"), + ("Fingerprint", "Fingerabdruck"), + ("Copy Fingerprint", "Fingerabdruck kopieren"), + ("no fingerprints", "Keine Fingerabdrücke"), + ("Select a peer", "Gegenstelle auswählen"), + ("Select peers", "Gegenstellen auswählen"), + ("Plugins", "Plugins"), + ("Uninstall", "Deinstallieren"), + ("Update", "Update"), + ("Enable", "Aktivieren"), + ("Disable", "Deaktivieren"), + ("Options", "Einstellungen"), + ("resolution_original_tip", "Originale Auflösung"), + ("resolution_fit_local_tip", "Lokale Auflösung anpassen"), + ("resolution_custom_tip", "Benutzerdefinierte Auflösung"), + ("Collapse toolbar", "Symbolleiste einklappen"), + ("Accept and Elevate", "Akzeptieren und Rechte erhöhen"), + ("accept_and_elevate_btn_tooltip", "Akzeptieren Sie die Verbindung und erhöhen Sie die UAC-Berechtigungen."), + ("clipboard_wait_response_timeout_tip", "Zeitüberschreitung beim Warten auf die Antwort der Kopie."), + ("Incoming connection", "Eingehende Verbindung"), + ("Outgoing connection", "Ausgehende Verbindung"), + ("Exit", "Beenden"), + ("Open", "Öffnen"), + ("logout_tip", "Sind Sie sicher, dass Sie sich abmelden wollen?"), + ("Service", "Vermittlungsdienst"), + ("Start", "Starten"), + ("Stop", "Stopp"), + ("exceed_max_devices", "Sie haben die maximale Anzahl der verwalteten Geräte erreicht."), + ("Sync with recent sessions", "Synchronisierung mit den letzten Sitzungen"), + ("Sort tags", "Tags sortieren"), + ("Open connection in new tab", "Verbindung in neuem Tab öffnen"), + ("Move tab to new window", "Tab in neues Fenster verschieben"), + ("Can not be empty", "Darf nicht leer sein"), + ("Already exists", "Existiert bereits"), + ("Change Password", "Passwort ändern"), + ("Refresh Password", "Passwort aktualisieren"), + ("ID", "ID"), + ("Grid View", "Rasteransicht"), + ("List View", "Listenansicht"), + ("Select", "Auswählen"), + ("Toggle Tags", "Tags umschalten"), + ("pull_ab_failed_tip", "Aktualisierung des Adressbuchs fehlgeschlagen"), + ("push_ab_failed_tip", "Synchronisierung des Adressbuchs mit dem Server fehlgeschlagen"), + ("synced_peer_readded_tip", "Die Geräte, die in den letzten Sitzungen vorhanden waren, werden erneut zum Adressbuch hinzugefügt."), + ("Change Color", "Farbe ändern"), + ("Primary Color", "Primärfarbe"), + ("HSV Color", "HSV-Farbe"), + ("Installation Successful!", "Installation erfolgreich!"), + ("Installation failed!", "Installation fehlgeschlagen!"), + ("Reverse mouse wheel", "Mausrad rückwärtsdrehen"), + ("{} sessions", "{} Sitzungen"), + ("scam_title", "Sie werden möglicherweise BETROGEN!"), + ("scam_text1", "Wenn Sie mit jemandem telefonieren, den Sie NICHT KENNEN, dem Sie NICHT VERTRAUEN und der Sie gebeten hat, RustDesk zu benutzen und den Dienst zu starten, fahren Sie nicht fort und legen Sie sofort auf."), + ("scam_text2", "Es handelt sich wahrscheinlich um einen Betrüger, der versucht, Ihr Geld oder andere private Informationen zu stehlen."), + ("Don't show again", "Nicht mehr anzeigen"), + ("I Agree", "Ich bin einverstanden"), + ("Decline", "Ablehnen"), + ("Timeout in minutes", "Zeitüberschreitung in Minuten"), + ("auto_disconnect_option_tip", "Eingehende Sitzungen bei Benutzer-Inaktivität automatisch schließen"), + ("Connection failed due to inactivity", "Automatische Trennung der Verbindung aufgrund von Inaktivität"), + ("Check for software update on startup", "Beim Start auf Softwareaktualisierung prüfen"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Bitte aktualisieren Sie RustDesk Server Pro auf die Version {} oder neuer!"), + ("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"), + ("Filter by intersection", "Nach Schnittmenge filtern"), + ("Remove wallpaper during incoming sessions", "Hintergrundbild bei eingehenden Sitzungen entfernen"), + ("Test", "Testen"), + ("display_is_plugged_out_msg", "Der Bildschirm ist nicht angeschlossen, schalten Sie auf den ersten Bildschirm um."), + ("No displays", "Keine Bildschirme"), + ("Open in new window", "In einem neuen Fenster öffnen"), + ("Show displays as individual windows", "Jeden Bildschirm in einem eigenen Fenster anzeigen"), + ("Use all my displays for the remote session", "Alle meine Bildschirme für die Fernsitzung verwenden"), + ("selinux_tip", "SELinux ist auf Ihrem Gerät aktiviert, was dazu führen kann, dass RustDesk als kontrollierte Seite nicht richtig läuft."), + ("Change view", "Ansicht ändern"), + ("Big tiles", "Große Kacheln"), + ("Small tiles", "Kleine Kacheln"), + ("List", "Liste"), + ("Virtual display", "Virtueller Bildschirm"), + ("Plug out all", "Alle ausschalten"), + ("True color (4:4:4)", "True Color (4:4:4)"), + ("Enable blocking user input", "Blockieren von Benutzereingaben aktivieren"), + ("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (:) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Modus 1"), + ("privacy_mode_impl_virtual_display_tip", "Modus 2"), + ("Enter privacy mode", "Datenschutzmodus aktiviert"), + ("Exit privacy mode", "Datenschutzmodus beendet"), + ("idd_not_support_under_win10_2004_tip", "Indirekter Grafiktreiber wird nicht unterstützt. Windows 10, Version 2004 oder neuer ist erforderlich."), + ("input_source_1_tip", "Eingangsquelle 1"), + ("input_source_2_tip", "Eingangsquelle 2"), + ("Swap control-command key", "Steuerungs- und Befehlstasten tauschen"), + ("swap-left-right-mouse", "Linke und rechte Maustaste tauschen"), + ("2FA code", "2FA-Code"), + ("More", "Mehr"), + ("enable-2fa-title", "Zwei-Faktor-Authentifizierung aktivieren"), + ("enable-2fa-desc", "Bitte richten Sie jetzt Ihren Authentifikator ein. Sie können eine Authentifizierungs-App wie Authy, Microsoft oder Google Authenticator auf Ihrem Telefon oder Desktop verwenden.\n\nScannen Sie den QR-Code mit Ihrer App und geben Sie den Code ein, den Ihre App anzeigt, um die Zwei-Faktor-Authentifizierung zu aktivieren."), + ("wrong-2fa-code", "Der Code kann nicht verifiziert werden. Prüfen Sie, ob der Code und die lokalen Zeiteinstellungen korrekt sind."), + ("enter-2fa-title", "Zwei-Faktor-Authentifizierung"), + ("Email verification code must be 6 characters.", "Der E-Mail-Verifizierungscode muss aus 6 Zeichen bestehen."), + ("2FA code must be 6 digits.", "Der 2FA-Code muss 6 Ziffern haben."), + ("Multiple Windows sessions found", "Mehrere Windows-Sitzungen gefunden"), + ("Please select the session you want to connect to", "Bitte wählen Sie die Sitzung, mit der Sie sich verbinden möchten"), + ("powered_by_me", "Unterstützt von RustDesk"), + ("outgoing_only_desk_tip", "Dies ist eine benutzerdefinierte Ausgabe von RustDesk.\nSie können eine Verbindung zu anderen Geräten herstellen, aber andere Geräte können keine Verbindung zu Ihrem Gerät herstellen."), + ("preset_password_warning", "Dies ist eine benutzerdefinierte Ausgabe von RustDesk mit einem voreingestellten Passwort. Jeder, der dieses Passwort kennt, kann die volle Kontrolle über Ihr Gerät erlangen. Wenn Sie dies nicht beabsichtigen, deinstallieren Sie diese Software bitte umgehend."), + ("Security Alert", "Sicherheitswarnung"), + ("My address book", "Mein Adressbuch"), + ("Personal", "Persönlich"), + ("Owner", "Eigentümer"), + ("Set shared password", "Geteiltes Passwort festlegen"), + ("Exist in", "Existiert in …?"), + ("Read-only", "Nur lesen"), + ("Read/Write", "Lesen/Schreiben"), + ("Full Control", "Vollständige Kontrolle"), + ("share_warning_tip", "Die obigen Felder werden geteilt und sind für andere sichtbar."), + ("Everyone", "Jeder"), + ("ab_web_console_tip", "Mehr über Webkonsole"), + ("allow-only-conn-window-open-tip", "Verbindung nur zulassen, wenn das RustDesk-Fenster geöffnet ist"), + ("no_need_privacy_mode_no_physical_displays_tip", "Keine physischen Bildschirme; keine Notwendigkeit, den Datenschutzmodus zu verwenden."), + ("Follow remote cursor", "Dem entfernten Cursor folgen"), + ("Follow remote window focus", "Dem Fokus des entfernten Fensters folgen"), + ("default_proxy_tip", "Standardprotokoll und -port sind SOCKS5 und 1080"), + ("no_audio_input_device_tip", "Kein Audio-Eingabegerät gefunden."), + ("Incoming", "Eingehend"), + ("Outgoing", "Ausgehend"), + ("Clear Wayland screen selection", "Wayland-Bildschirmauswahl löschen"), + ("clear_Wayland_screen_selection_tip", "Nachdem Sie die Bildschirmauswahl gelöscht haben, können Sie den freizugebenden Bildschirm erneut auswählen."), + ("confirm_clear_Wayland_screen_selection_tip", "Sind Sie sicher, dass Sie die Auswahl des Wayland-Bildschirms löschen möchten?"), + ("android_new_voice_call_tip", "Eine neue Sprachanrufanfrage wurde empfangen. Wenn Sie die Anfrage annehmen, wird der Ton auf Sprachkommunikation umgeschaltet."), + ("texture_render_tip", "Verwenden Sie Textur-Rendering, um die Bilder glatter zu machen. Sie können diese Option deaktivieren, wenn Sie Rendering-Probleme haben."), + ("Use texture rendering", "Textur-Rendering verwenden"), + ("Floating window", "Schwebendes Fenster"), + ("floating_window_tip", "Es hilft dabei, RustDesk im Hintergrund auszuführen."), + ("Keep screen on", "Bildschirm eingeschaltet lassen"), + ("Never", "Niemals"), + ("During controlled", "Wenn kontrolliert"), + ("During service is on", "Wenn der Dienst läuft"), + ("Capture screen using DirectX", "Bildschirm mit DirectX aufnehmen"), + ("Back", "Zurück"), + ("Apps", "Apps"), + ("Volume up", "Lauter"), + ("Volume down", "Leiser"), + ("Power", "Power"), + ("Telegram bot", "Telegram-Bot"), + ("enable-bot-tip", "Wenn Sie diese Funktion aktivieren, können Sie den 2FA-Code von Ihrem Bot erhalten. Er kann auch als Verbindungsbenachrichtigung dienen."), + ("enable-bot-desc", "1. Öffnen Sie einen Chat mit @BotFather.\n2. Senden Sie den Befehl \"/newbot\". Sie erhalten ein Token, nachdem Sie diesen Schritt abgeschlossen haben.\n3. Starten Sie einen Chat mit Ihrem neu erstellten Bot. Senden Sie eine Nachricht, die mit einem Schrägstrich (\"/\") beginnt, z. B. \"/hello\", um ihn zu aktivieren.\n"), + ("cancel-2fa-confirm-tip", "Sind Sie sicher, dass Sie 2FA abbrechen möchten?"), + ("cancel-bot-confirm-tip", "Sind Sie sicher, dass Sie Telegram-Bot abbrechen möchten?"), + ("About RustDesk", "Über RustDesk"), + ("Send clipboard keystrokes", "Tastenanschläge aus der Zwischenablage senden"), + ("network_error_tip", "Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es dann erneut."), + ("Unlock with PIN", "Mit PIN entsperren"), + ("Requires at least {} characters", "Erfordert mindestens {} Zeichen"), + ("Wrong PIN", "Falsche PIN"), + ("Set PIN", "PIN festlegen"), + ("Enable trusted devices", "Vertrauenswürdige Geräte aktivieren"), + ("Manage trusted devices", "Vertrauenswürdige Geräte verwalten"), + ("Platform", "Plattform"), + ("Days remaining", "Verbleibende Tage"), + ("enable-trusted-devices-tip", "2FA-Verifizierung auf vertrauenswürdigen Geräten überspringen"), + ("Parent directory", "Übergeordnetes Verzeichnis"), + ("Resume", "Fortsetzen"), + ("Invalid file name", "Ungültiger Dateiname"), + ("one-way-file-transfer-tip", "Die einseitige Dateiübertragung ist auf der kontrollierten Seite aktiviert."), + ("Authentication Required", "Authentifizierung erforderlich"), + ("Authenticate", "Authentifizieren"), + ("web_id_input_tip", "Sie können eine ID auf demselben Server eingeben, direkter IP-Zugriff wird im Web-Client nicht unterstützt.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt."), + ("Download", "Herunterladen"), + ("Upload folder", "Ordner hochladen"), + ("Upload files", "Dateien hochladen"), + ("Clipboard is synchronized", "Zwischenablage ist synchronisiert"), + ("Update client clipboard", "Client-Zwischenablage aktualisieren"), + ("Untagged", "Unmarkiert"), + ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), + ("Accessible devices", "Erreichbare Geräte"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Bitte aktualisieren Sie den RustDesk-Client auf der Remote-Seite auf Version {} oder neuer!"), + ("d3d_render_tip", "Wenn das D3D-Rendering aktiviert ist, kann der entfernte Bildschirm auf manchen Rechnern schwarz sein."), + ("Use D3D rendering", "D3D-Rendering verwenden"), + ("Printer", "Drucker"), + ("printer-os-requirement-tip", "Für die Funktion des Druckerausgangs ist Windows 10 oder höher erforderlich."), + ("printer-requires-installed-{}-client-tip", "Um den entfernten Druck nutzen zu können, muss {} auf diesem Gerät installiert sein."), + ("printer-{}-not-installed-tip", "Der Drucker {} ist nicht installiert."), + ("printer-{}-ready-tip", "Der Drucker {} ist installiert und einsatzbereit."), + ("Install {} Printer", "Drucker {} installieren"), + ("Outgoing Print Jobs", "Ausgehende Druckaufträge"), + ("Incoming Print Jobs", "Eingehende Druckaufträge"), + ("Incoming Print Job", "Eingehender Druckauftrag"), + ("use-the-default-printer-tip", "Standarddrucker verwenden"), + ("use-the-selected-printer-tip", "Ausgewählten Drucker verwenden"), + ("auto-print-tip", "Automatisch mit dem ausgewählten Drucker drucken"), + ("print-incoming-job-confirm-tip", "Sie haben einen Druckauftrag aus der Ferne erhalten. Möchten Sie ihn bei sich selbst ausführen?"), + ("remote-printing-disallowed-tile-tip", "Entferntes Drucken nicht erlaubt"), + ("remote-printing-disallowed-text-tip", "Die Berechtigungseinstellungen der kontrollierten Seite verweigern den entfernten Druck."), + ("save-settings-tip", "Einstellungen speichern"), + ("dont-show-again-tip", "Nicht mehr anzeigen"), + ("Take screenshot", "Screenshot aufnehmen"), + ("Taking screenshot", "Screenshot aufnehmen …"), + ("screenshot-merged-screen-not-supported-tip", "Das Zusammenführen von Screenshots von mehreren Bildschirmen wird derzeit nicht unterstützt. Bitte wechseln Sie zu einem einzelnen Bildschirm und versuchen Sie es erneut."), + ("screenshot-action-tip", "Bitte wählen Sie aus, wie Sie mit dem Screenshot fortfahren möchten."), + ("Save as", "Speichern unter"), + ("Copy to clipboard", "In Zwischenablage kopieren"), + ("Enable remote printer", "Entfernten Drucker aktivieren"), + ("Downloading {}", "{} herunterladen"), + ("{} Update", "{} aktualisieren"), + ("{}-to-update-tip", "{} wird jetzt geschlossen und die neue Version installiert."), + ("download-new-version-failed-tip", "Download fehlgeschlagen. Sie können es erneut versuchen oder auf die Schaltfläche \"Herunterladen\" klicken, um von der Versionsseite herunterzuladen und manuell zu aktualisieren."), + ("Auto update", "Automatisch aktualisieren"), + ("update-failed-check-msi-tip", "Prüfung der Installationsmethode fehlgeschlagen. Bitte klicken Sie auf die Schaltfläche \"Herunterladen\", um von der Versionsseite herunterzuladen und manuell zu aktualisieren."), + ("websocket_tip", "Bei der Verwendung von WebSocket werden nur Relay-Verbindungen unterstützt."), + ("Use WebSocket", "WebSocket verwenden"), + ("Trackpad speed", "Geschwindigkeit des Trackpads"), + ("Default trackpad speed", "Standardgeschwindigkeit des Trackpads"), + ("Numeric one-time password", "Numerisches Einmalpasswort"), + ("Enable IPv6 P2P connection", "IPv6-P2P-Verbindung aktivieren"), + ("Enable UDP hole punching", "UDP-Hole-Punching aktivieren"), + ("View camera", "Kamera anzeigen"), + ("Enable camera", "Kamera zulassen"), + ("No cameras", "Keine Kameras"), + ("view_camera_unsupported_tip", "Das entfernte Gerät kann die Kamera nicht anzeigen."), + ("Terminal", "Terminal"), + ("Enable terminal", "Terminal zulassen"), + ("New tab", "Neuer Tab"), + ("Keep terminal sessions on disconnect", "Terminalsitzungen beim Trennen der Verbindung beibehalten"), + ("Terminal (Run as administrator)", "Terminal (als Administrator ausführen)"), + ("terminal-admin-login-tip", "Bitte geben Sie den Benutzernamen und das Passwort des Administrators der kontrollierten Seite ein."), + ("Failed to get user token.", "Benutzer-Token konnte nicht abgerufen werden."), + ("Incorrect username or password.", "Falscher Benutzername oder falsches Passwort."), + ("The user is not an administrator.", "Der Benutzer ist kein Administrator."), + ("Failed to check if the user is an administrator.", "Es konnte nicht geprüft werden, ob der Benutzer ein Administrator ist."), + ("Supported only in the installed version.", "Wird nur in der installierten Version unterstützt."), + ("elevation_username_tip", "Geben Sie Benutzername oder Domäne\\Benutzername ein"), + ("Preparing for installation ...", "Installation wird vorbereitet …"), + ("Show my cursor", "Meinen Cursor anzeigen"), + ("Scale custom", "Benutzerdefinierte Skalierung"), + ("Custom scale slider", "Schieberegler für benutzerdefinierte Skalierung"), + ("Decrease", "Verringern"), + ("Increase", "Erhöhen"), + ("Show virtual mouse", "Virtuelle Maus anzeigen"), + ("Virtual mouse size", "Virtuelle Mausgröße"), + ("Small", "Klein"), + ("Large", "Groß"), + ("Show virtual joystick", "Virtuellen Joystick anzeigen"), + ("Edit note", "Hinweis bearbeiten"), + ("Alias", "Alias"), + ("ScrollEdge", "Scrollen am Rand"), + ("Allow insecure TLS fallback", "Unsicheres TLS-Fallback zulassen"), + ("allow-insecure-tls-fallback-tip", "Standardmäßig überprüft RustDesk das Serverzertifikat für Protokolle, die TLS verwenden. Wenn diese Option aktiviert ist, überspringt RustDesk den Überprüfungsschritt und fährt im Falle eines Überprüfungsfehlers fort."), + ("Disable UDP", "UDP deaktivieren"), + ("disable-udp-tip", "Legt fest, ob nur TCP verwendet werden soll. Wenn diese Option aktiviert ist, verwendet RustDesk nicht mehr UDP 21116, sondern stattdessen TCP 21116."), + ("server-oss-not-support-tip", "HINWEIS: RustDesk Server OSS enthält diese Funktion nicht."), + ("input note here", "Hier eine Notiz eingeben"), + ("note-at-conn-end-tip", "Am Ende der Verbindung um eine Notiz bitten."), + ("Show terminal extra keys", "Zusätzliche Tasten des Terminals anzeigen"), + ("Relative mouse mode", "Relativer Mausmodus"), + ("rel-mouse-not-supported-peer-tip", "Der relative Mausmodus wird von der verbundenen Gegenstelle nicht unterstützt."), + ("rel-mouse-not-ready-tip", "Der relative Mausmodus ist noch nicht bereit. Bitte versuchen Sie es erneut."), + ("rel-mouse-lock-failed-tip", "Cursor konnte nicht gesperrt werden. Der relative Mausmodus wurde deaktiviert."), + ("rel-mouse-exit-{}-tip", "Drücken Sie {} zum Beenden."), + ("rel-mouse-permission-lost-tip", "Die Tastaturberechtigung wurde widerrufen. Der relative Mausmodus wurde deaktiviert."), + ("Changelog", "Änderungsprotokoll"), + ("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"), + ("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"), + ("Continue with {}", "Fortfahren mit {}"), + ("Display Name", "Anzeigename"), + ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), + ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/el.rs b/vendor/rustdesk/src/lang/el.rs new file mode 100644 index 0000000..0633889 --- /dev/null +++ b/vendor/rustdesk/src/lang/el.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Κατάσταση"), + ("Your Desktop", "Ο σταθμός εργασίας σας"), + ("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το ID και τον κωδικό πρόσβασης."), + ("Password", "Κωδικός πρόσβασης"), + ("Ready", "Έτοιμο"), + ("Established", "Συνδέθηκε"), + ("connecting_status", "Σύνδεση στο δίκτυο RustDesk..."), + ("Enable service", "Ενεργοποίηση υπηρεσίας"), + ("Start service", "Έναρξη υπηρεσίας"), + ("Service is running", "Η υπηρεσία εκτελείται"), + ("Service is not running", "Η υπηρεσία δεν εκτελείται"), + ("not_ready_status", "Δεν είναι έτοιμο. Ελέγξτε τη σύνδεσή σας στο δίκτυο"), + ("Control Remote Desktop", "Έλεγχος απομακρυσμένου σταθμού εργασίας"), + ("Transfer file", "Μεταφορά αρχείου"), + ("Connect", "Σύνδεση"), + ("Recent sessions", "Πρόσφατες συνεδρίες"), + ("Address book", "Βιβλίο διευθύνσεων"), + ("Confirmation", "Επιβεβαίωση"), + ("TCP tunneling", "Σήραγγα TCP"), + ("Remove", "Κατάργηση"), + ("Refresh random password", "Ανανέωση τυχαίου κωδικού πρόσβασης"), + ("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"), + ("Enable keyboard/mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"), + ("Enable clipboard", "Ενεργοποίηση προχείρου"), + ("Enable file transfer", "Ενεργοποίηση μεταφοράς αρχείων"), + ("Enable TCP tunneling", "Ενεργοποίηση σήραγγας TCP"), + ("IP Whitelisting", "Λίστα επιτρεπόμενων IP"), + ("ID/Relay Server", "ID/Διακομιστής Αναμετάδοσης"), + ("Import server config", "Εισαγωγή διαμόρφωσης διακομιστή"), + ("Export Server Config", "Εξαγωγή διαμόρφωσης διακομιστή"), + ("Import server configuration successfully", "Επιτυχής εισαγωγή διαμόρφωσης διακομιστή"), + ("Export server configuration successfully", "Επιτυχής εξαγωγή διαμόρφωσης διακομιστή"), + ("Invalid server configuration", "Μη έγκυρη διαμόρφωση διακομιστή"), + ("Clipboard is empty", "Το πρόχειρο είναι κενό"), + ("Stop service", "Διακοπή υπηρεσίας"), + ("Change ID", "Αλλαγή του ID σας"), + ("Your new ID", "Το νέο σας ID"), + ("length %min% to %max%", "μέγεθος από %min% έως %max%"), + ("starts with a letter", "ξεκινά με γράμμα"), + ("allowed characters", "επιτρεπόμενοι χαρακτήρες"), + ("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9, - (παύλα) και _ (κάτω παύλα). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), + ("Website", "Ιστότοπος"), + ("About", "Σχετικά"), + ("Slogan_tip", "Φτιαγμένο με πάθος - σε έναν κόσμο που βυθίζεται στο χάος!"), + ("Privacy Statement", "Πολιτική απορρήτου"), + ("Mute", "Σίγαση"), + ("Build Date", "Ημερομηνία δημιουργίας"), + ("Version", "Έκδοση"), + ("Home", "Αρχική"), + ("Audio Input", "Είσοδος ήχου"), + ("Enhancements", "Βελτιώσεις"), + ("Hardware Codec", "Κωδικοποιητής υλικού"), + ("Adaptive bitrate", "Προσαρμοστικός ρυθμός μετάδοσης bit"), + ("ID Server", "Διακομιστής ID"), + ("Relay Server", "Διακομιστής αναμετάδοσης"), + ("API Server", "Διακομιστής API"), + ("invalid_http", "Πρέπει να ξεκινά με http:// ή https://"), + ("Invalid IP", "Μη έγκυρη διεύθυνση IP"), + ("Invalid format", "Μη έγκυρη μορφή"), + ("server_not_support", "Αυτή η δυνατότητα δεν υποστηρίζεται από τον διακομιστή"), + ("Not available", "Μη διαθέσιμο"), + ("Too frequent", "Πολύ συχνά"), + ("Cancel", "Ακύρωση"), + ("Skip", "Παράλειψη"), + ("Close", "Κλείσιμο"), + ("Retry", "Δοκίμασε ξανά"), + ("OK", "Εντάξει"), + ("Password Required", "Απαιτείται κωδικός πρόσβασης"), + ("Please enter your password", "Παρακαλώ εισάγετε τον κωδικό πρόσβασης"), + ("Remember password", "Απομνημόνευση κωδικού πρόσβασης"), + ("Wrong Password", "Λάθος κωδικός πρόσβασης"), + ("Do you want to enter again?", "Θέλετε να γίνει επανασύνδεση;"), + ("Connection Error", "Σφάλμα σύνδεσης"), + ("Error", "Σφάλμα"), + ("Reset by the peer", "Η σύνδεση επαναφέρθηκε από τον απομακρυσμένο σταθμό"), + ("Connecting...", "Σύνδεση..."), + ("Connection in progress. Please wait.", "Σύνδεση σε εξέλιξη. Παρακαλώ περιμένετε."), + ("Please try 1 minute later", "Παρακαλώ δοκιμάστε ξανά σε 1 λεπτό"), + ("Login Error", "Σφάλμα εισόδου"), + ("Successful", "Επιτυχής"), + ("Connected, waiting for image...", "Συνδέθηκε, αναμονή για εικόνα..."), + ("Name", "Όνομα"), + ("Type", "Τύπος"), + ("Modified", "Τροποποιήθηκε"), + ("Size", "Μέγεθος"), + ("Show Hidden Files", "Εμφάνιση κρυφών αρχείων"), + ("Receive", "Λήψη"), + ("Send", "Αποστολή"), + ("Refresh File", "Ανανέωση αρχείου"), + ("Local", "Τοπικό"), + ("Remote", "Απομακρυσμένο"), + ("Remote Computer", "Απομακρυσμένος υπολογιστής"), + ("Local Computer", "Τοπικός υπολογιστής"), + ("Confirm Delete", "Επιβεβαίωση διαγραφής"), + ("Delete", "Διαγραφή"), + ("Properties", "Ιδιότητες"), + ("Multi Select", "Πολλαπλή επιλογή"), + ("Select All", "Επιλογή όλων"), + ("Unselect All", "Κατάργηση επιλογής όλων"), + ("Empty Directory", "Κενός φάκελος"), + ("Not an empty directory", "Η διαδρομή δεν είναι κενή"), + ("Are you sure you want to delete this file?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;"), + ("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την κενή διαδρομή;"), + ("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτής της διαδρομής;"), + ("Do this for all conflicts", "Κάνε αυτό για όλες τις διενέξεις"), + ("This is irreversible!", "Αυτό είναι μη αναστρέψιμο!"), + ("Deleting", "Διαγραφή"), + ("files", "αρχείων"), + ("Waiting", "Αναμονή"), + ("Finished", "Ολοκληρώθηκε"), + ("Speed", "Ταχύτητα"), + ("Custom Image Quality", "Προσαρμοσμένη ποιότητα εικόνας"), + ("Privacy mode", "Λειτουργία απορρήτου"), + ("Block user input", "Αποκλεισμός χειρισμού από τον χρήστη"), + ("Unblock user input", "Κατάργηση αποκλεισμού χειρισμού από τον χρήστη"), + ("Adjust Window", "Προσαρμογή παραθύρου"), + ("Original", "Πρωτότυπο"), + ("Shrink", "Συρρίκνωση"), + ("Stretch", "Προσαρμογή"), + ("Scrollbar", "Μπάρα κύλισης"), + ("ScrollAuto", "Αυτόματη κύλιση"), + ("Good image quality", "Καλή ποιότητα εικόνας"), + ("Balanced", "Ισορροπημένη"), + ("Optimize reaction time", "Βελτιστοποίηση απόκρισης"), + ("Custom", "Προσαρμοσμένη ποιότητας εικόνας"), + ("Show remote cursor", "Εμφάνιση απομακρυσμένου κέρσορα"), + ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας σύνδεσης"), + ("Disable clipboard", "Απενεργοποίηση προχείρου"), + ("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"), + ("Insert Ctrl + Alt + Del", "Εισαγωγή Ctrl + Alt + Del"), + ("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"), + ("Refresh", "Ανανέωση"), + ("ID does not exist", "Το ID αυτό δεν υπάρχει"), + ("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με τον διακομιστή"), + ("Please try later", "Παρακαλώ δοκιμάστε αργότερα"), + ("Remote desktop is offline", "Ο απομακρυσμένος σταθμός εργασίας είναι εκτός σύνδεσης"), + ("Key mismatch", "Μη έγκυρο κλειδί"), + ("Timeout", "Τέλος χρόνου"), + ("Failed to connect to relay server", "Αποτυχία σύνδεσης με τον διακομιστή αναμετάδοσης"), + ("Failed to connect via rendezvous server", "Απέτυχε η σύνδεση μέσω διακομιστή"), + ("Failed to connect via relay server", "Απέτυχε η σύνδεση μέσω διακομιστή αναμετάδοσης"), + ("Failed to make direct connection to remote desktop", "Απέτυχε η απευθείας σύνδεση με τον απομακρυσμένο σταθμό εργασίας"), + ("Set Password", "Ορίστε κωδικό πρόσβασης"), + ("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"), + ("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"), + ("Click to upgrade", "Κάντε κλίκ για αναβάθμιση τώρα"), + ("Configure", "Διαμόρφωση"), + ("config_acc", "Για να ελέγξετε την επιφάνεια εργασίας σας από απόσταση, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Προσβασιμότητας\"."), + ("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στην επιφάνεια εργασίας σας, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Εγγραφή οθόνης\"."), + ("Installing ...", "Γίνεται εγκατάσταση ..."), + ("Install", "Εγκατάσταση"), + ("Installation", "Η εγκατάσταση"), + ("Installation Path", "Διαδρομή εγκατάστασης"), + ("Create start menu shortcuts", "Δημιουργία συντομεύσεων μενού έναρξης"), + ("Create desktop icon", "Δημιουργία εικονιδίου επιφάνειας εργασίας"), + ("agreement_tip", "Με την εγκατάσταση, αποδέχεστε την άδεια χρήσης"), + ("Accept and Install", "Αποδοχή και εγκατάσταση"), + ("End-user license agreement", "Σύμβαση άδειας χρήσης τελικού χρήστη"), + ("Generating ...", "Δημιουργία ..."), + ("Your installation is lower version.", "Η έκδοση της εγκατάστασής σας είναι παλαιότερη."), + ("not_close_tcp_tip", "Μην κλείσετε αυτό το παράθυρο ενώ χρησιμοποιείτε το τούνελ."), + ("Listening ...", "Αναμονή ..."), + ("Remote Host", "Απομακρυσμένος υπολογιστής"), + ("Remote Port", "Απομακρυσμένη θύρα"), + ("Action", "Δράση"), + ("Add", "Προσθήκη"), + ("Local Port", "Τοπική θύρα"), + ("Local Address", "Τοπική διεύθυνση"), + ("Change Local Port", "Αλλαγή τοπικής θύρας"), + ("setup_server_tip", "Για πιο γρήγορη σύνδεση, παρακαλούμε να ρυθμίστε τον δικό σας διακομιστή σύνδεσης"), + ("Too short, at least 6 characters.", "Πολύ μικρό, χρειάζεται τουλάχιστον 6 χαρακτήρες."), + ("The confirmation is not identical.", "Η επιβεβαίωση δεν είναι πανομοιότυπη."), + ("Permissions", "Άδειες"), + ("Accept", "Αποδοχή"), + ("Dismiss", "Απόρριψη"), + ("Disconnect", "Αποσύνδεση"), + ("Enable file copy and paste", "Να επιτρέπεται η αντιγραφή και επικόλληση αρχείων"), + ("Connected", "Συνδεδεμένο"), + ("Direct and encrypted connection", "Άμεση και κρυπτογραφημένη σύνδεση"), + ("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"), + ("Direct and unencrypted connection", "Άμεση και μη κρυπτογραφημένη σύνδεση"), + ("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"), + ("Enter Remote ID", "Εισαγωγή του απομακρυσμένου ID"), + ("Enter your password", "Εισάγετε τον κωδικό σας"), + ("Logging in...", "Γίνεται σύνδεση..."), + ("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"), + ("Auto Login", "Αυτόματη είσοδος"), + ("Enable direct IP access", "Ενεργοποίηση άμεσης πρόσβασης IP"), + ("Rename", "Μετονομασία"), + ("Space", "Χώρος"), + ("Create desktop shortcut", "Δημιουργία συντόμευσης στην επιφάνεια εργασίας"), + ("Change Path", "Αλλαγή διαδρομής δίσκου"), + ("Create Folder", "Δημιουργία φακέλου"), + ("Please enter the folder name", "Παρακαλώ εισάγετε το όνομα του φακέλου"), + ("Fix it", "Επιδιόρθωσε το"), + ("Warning", "Προειδοποίηση"), + ("Login screen using Wayland is not supported", "Η οθόνη εισόδου με χρήση του Wayland δεν υποστηρίζεται"), + ("Reboot required", "Απαιτείται επανεκκίνηση"), + ("Unsupported display server", "Μη υποστηριζόμενος διακομιστής εμφάνισης "), + ("x11 expected", "αναμένεται X11"), + ("Port", "Θύρα"), + ("Settings", "Ρυθμίσεις"), + ("Username", "Όνομα χρήστη"), + ("Invalid port", "Μη έγκυρη θύρα"), + ("Closed manually by the peer", "Τερματίστηκε από τον απομακρυσμένο σταθμό"), + ("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης διαμόρφωσης"), + ("Run without install", "Εκτέλεση χωρίς εγκατάσταση"), + ("Connect via relay", "Σύνδεση μέσω αναμεταδότη"), + ("Always connect via relay", "Να γίνεται σύνδεση πάντα μέσω αναμεταδότη"), + ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων να έχουν πρόσβαση σε εμένα"), + ("Login", "Σύνδεση"), + ("Verify", "Επαλήθευση"), + ("Remember me", "Να με θυμάσαι"), + ("Trust this device", "Να εμπιστεύομαι αυτή την συσκευή"), + ("Verification code", "Κωδικός επαλήθευσης"), + ("verification_tip", "Ένας κωδικός επαλήθευσης έχει σταλεί στην καταχωρημένη διεύθυνση email. Εισαγάγετε τον κωδικό επαλήθευσης για να συνεχίσετε τη σύνδεση."), + ("Logout", "Αποσύνδεση"), + ("Tags", "Ετικέτες"), + ("Search ID", "Αναζήτηση ID"), + ("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, κενό ή νέα γραμμή"), + ("Add ID", "Προσθήκη ID"), + ("Add Tag", "Προσθήκη ετικέτας"), + ("Unselect all tags", "Αποεπιλογή όλων των ετικετών"), + ("Network error", "Σφάλμα δικτύου"), + ("Username missed", "Δεν συμπληρώσατε το όνομα χρήστη"), + ("Password missed", "Δεν συμπληρώσατε τον κωδικό πρόσβασης"), + ("Wrong credentials", "Λάθος διαπιστευτήρια"), + ("The verification code is incorrect or has expired", "Ο κωδικός επαλήθευσης είναι λανθασμένος ή έχει λήξει"), + ("Edit Tag", "Επεξεργασία ετικέτας"), + ("Forget Password", "Διαγραφή απομνημονευμένου κωδικού"), + ("Favorites", "Αγαπημένα"), + ("Add to Favorites", "Προσθήκη στα αγαπημένα"), + ("Remove from Favorites", "Κατάργηση από τα Αγαπημένα"), + ("Empty", "Άδειο"), + ("Invalid folder name", "Μη έγκυρο όνομα φακέλου"), + ("Socks5 Proxy", "Διαμεσολαβητής Socks5"), + ("Socks5/Http(s) Proxy", "Διαμεσολαβητής Socks5/Http(s)"), + ("Discovered", "Ανακαλύφθηκαν"), + ("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος."), + ("Remote ID", "Απομακρυσμένο ID"), + ("Paste", "Επικόλληση"), + ("Paste here?", "Επικόλληση εδώ;"), + ("Are you sure to close the connection?", "Είστε βέβαιοι ότι θέλετε να κλείσετε αυτήν τη σύνδεση;"), + ("Download new version", "Λήψη νέας έκδοσης"), + ("Touch mode", "Λειτουργία αφής"), + ("Mouse mode", "Λειτουργία ποντικιού"), + ("One-Finger Tap", "Πάτημα με ένα δάχτυλο"), + ("Left Mouse", "Αριστερό κλικ"), + ("One-Long Tap", "Παρατεταμένο πάτημα με ένα δάχτυλο"), + ("Two-Finger Tap", "Πάτημα με δύο δάχτυλα"), + ("Right Mouse", "Δεξί κλικ"), + ("One-Finger Move", "Κίνηση με ένα δάχτυλο"), + ("Double Tap & Move", "Διπλό πάτημα και μετακίνηση"), + ("Mouse Drag", "Σύρετε το ποντίκι"), + ("Three-Finger vertically", "Τρία δάχτυλα, κάθετα"), + ("Mouse Wheel", "Τροχός ποντικιού"), + ("Two-Finger Move", "Κίνηση με δύο δάχτυλα"), + ("Canvas Move", "Κίνηση καμβά"), + ("Pinch to Zoom", "Τσίμπημα για ζουμ"), + ("Canvas Zoom", "Ζουμ σε καμβά"), + ("Reset canvas", "Επαναφορά καμβά"), + ("No permission of file transfer", "Δεν υπάρχει άδεια για την μεταφορά αρχείων"), + ("Note", "Σημείωση"), + ("Connection", "Σύνδεση"), + ("Share screen", "Κοινή χρήση οθόνης"), + ("Chat", "Κουβέντα"), + ("Total", "Σύνολο"), + ("items", "στοιχεία"), + ("Selected", "Επιλεγμένα"), + ("Screen Capture", "Καταγραφή οθόνης"), + ("Input Control", "Έλεγχος εισόδου"), + ("Audio Capture", "Εγγραφή ήχου"), + ("Do you accept?", "Δέχεσαι;"), + ("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"), + ("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισόδου για Android;"), + ("android_input_permission_tip1", "Για να μπορεί μία απομακρυσμένη συσκευή να ελέγχει τη συσκευή σας Android, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."), + ("android_input_permission_tip2", "Παρακαλούμε να μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."), + ("android_new_connection_tip", "Έχει ληφθεί νέο αίτημα ελέγχου, το οποίο θέλει να ελέγξει την τρέχουσα συσκευή σας."), + ("android_service_will_start_tip", "Η ενεργοποίηση της \"Καταγραφής οθόνης\" θα ξεκινήσει αυτόματα την υπηρεσία, επιτρέποντας σε άλλες συσκευές να ζητήσουν σύνδεση με τη συσκευή σας."), + ("android_stop_service_tip", "Το κλείσιμο της υπηρεσίας αυτής θα κλείσει αυτόματα όλες τις υπάρχουσες συνδέσεις."), + ("android_version_audio_tip", "Η τρέχουσα έκδοση Android δεν υποστηρίζει εγγραφή ήχου, αναβαθμίστε σε Android 10 ή νεότερη έκδοση."), + ("android_start_service_tip", "Πατήστε [Έναρξη υπηρεσίας] ή ενεργοποιήστε την άδεια [Καταγραφή οθόνης] για να ξεκινήσετε την υπηρεσία κοινής χρήσης οθόνης."), + ("android_permission_may_not_change_tip", "Τα δικαιώματα για τις καθιερωμένες συνδέσεις δεν μπορούν να αλλάξουν άμεσα μέχρι να επανασυνδεθούν."), + ("Account", "Λογαριασμός"), + ("Overwrite", "Αντικατάσταση"), + ("This file exists, skip or overwrite this file?", "Αυτό το αρχείο υπάρχει, παράβλεψη ή αντικατάσταση αυτού του αρχείου;"), + ("Quit", "Έξοδος"), + ("Help", "Βοήθεια"), + ("Failed", "Απέτυχε"), + ("Succeeded", "Επιτυχής"), + ("Someone turns on privacy mode, exit", "Κάποιος ενεργοποιεί τη λειτουργία απορρήτου, έξοδος"), + ("Unsupported", "Δεν υποστηρίζεται"), + ("Peer denied", "Ο απομακρυσμένος σταθμός έχει απορριφθεί"), + ("Please install plugins", "Παρακαλώ εγκαταστήστε τα πρόσθετα"), + ("Peer exit", "Ο απομακρυσμένος σταθμός έχει αποσυνδεθεί"), + ("Failed to turn off", "Αποτυχία απενεργοποίησης"), + ("Turned off", "Απενεργοποιημένο"), + ("Language", "Γλώσσα"), + ("Keep RustDesk background service", "Διατήρηση της υπηρεσίας παρασκηνίου του RustDesk"), + ("Ignore Battery Optimizations", "Αγνόηση βελτιστοποιήσεων μπαταρίας"), + ("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"), + ("Start on boot", "Έναρξη κατά την εκκίνηση"), + ("Start the screen sharing service on boot, requires special permissions", "Η έναρξη της υπηρεσίας κοινής χρήσης οθόνης κατά την εκκίνηση, απαιτεί ειδικά δικαιώματα"), + ("Connection not allowed", "Η σύνδεση δεν επιτρέπεται"), + ("Legacy mode", "Λειτουργία συμβατότητας"), + ("Map mode", "Λειτουργία χάρτη"), + ("Translate mode", "Λειτουργία μετάφρασης"), + ("Use permanent password", "Χρήση μόνιμου κωδικού πρόσβασης"), + ("Use both passwords", "Χρήση και των δύο κωδικών πρόσβασης"), + ("Set permanent password", "Ορισμός μόνιμου κωδικού πρόσβασης"), + ("Enable remote restart", "Ενεργοποίηση απομακρυσμένης επανεκκίνησης"), + ("Restart remote device", "Επανεκκίνηση απομακρυσμένης συσκευής"), + ("Are you sure you want to restart", "Είστε βέβαιοι ότι θέλετε να κάνετε επανεκκίνηση"), + ("Restarting remote device", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής"), + ("remote_restarting_tip", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής. Κλείστε αυτό το πλαίσιο μηνύματος και επανασυνδεθείτε με τον μόνιμο κωδικό πρόσβασης μετά από λίγο."), + ("Copied", "Αντιγράφηκε"), + ("Exit Fullscreen", "Έξοδος από πλήρη οθόνη"), + ("Fullscreen", "Πλήρης οθόνη"), + ("Mobile Actions", "Ενέργειες για κινητά"), + ("Select Monitor", "Επιλογή οθόνης"), + ("Control Actions", "Ενέργειες ελέγχου"), + ("Display Settings", "Ρυθμίσεις οθόνης"), + ("Ratio", "Αναλογία"), + ("Image Quality", "Ποιότητα εικόνας"), + ("Scroll Style", "Στυλ κύλισης"), + ("Show Toolbar", "Εμφάνιση γραμμής εργαλείων"), + ("Hide Toolbar", "Απόκρυψη γραμμής εργαλείων"), + ("Direct Connection", "Απευθείας σύνδεση"), + ("Relay Connection", "Αναμεταδιδόμενη σύνδεση"), + ("Secure Connection", "Ασφαλής σύνδεση"), + ("Insecure Connection", "Μη ασφαλής σύνδεση"), + ("Scale original", "Κλιμάκωση πρωτότυπου"), + ("Scale adaptive", "Προσαρμοσμένη κλίμακα"), + ("General", "Γενικά"), + ("Security", "Ασφάλεια"), + ("Theme", "Θέμα"), + ("Dark Theme", "Σκούρο θέμα"), + ("Light Theme", "Φωτεινό θέμα"), + ("Dark", "Σκούρο"), + ("Light", "Φωτεινό"), + ("Follow System", "Από το σύστημα"), + ("Enable hardware codec", "Ενεργοποίηση κωδικοποιητή υλικού"), + ("Unlock Security Settings", "Ξεκλείδωμα ρυθμίσεων ασφαλείας"), + ("Enable audio", "Ενεργοποίηση ήχου"), + ("Unlock Network Settings", "Ξεκλείδωμα ρυθμίσεων δικτύου"), + ("Server", "Διακομιστής"), + ("Direct IP Access", "Άμεση πρόσβαση IP"), + ("Proxy", "Διαμεσολαβητής"), + ("Apply", "Εφαρμογή"), + ("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"), + ("Clear", "Καθαρισμός"), + ("Audio Input Device", "Συσκευή εισόδου ήχου"), + ("Use IP Whitelisting", "Χρήση λίστας επιτρεπόμενων IP"), + ("Network", "Δίκτυο"), + ("Pin Toolbar", "Καρφίτσωμα γραμμής εργαλείων"), + ("Unpin Toolbar", "Ξεκαρφίτσωμα γραμμής εργαλείων"), + ("Recording", "Εγγραφή"), + ("Directory", "Διαδρομή"), + ("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"), + ("Automatically record outgoing sessions", "Αυτόματη εγγραφή εξερχόμενων συνεδριών"), + ("Change", "Αλλαγή"), + ("Start session recording", "Έναρξη εγγραφής συνεδρίας"), + ("Stop session recording", "Διακοπή εγγραφής συνεδρίας"), + ("Enable recording session", "Ενεργοποίηση εγγραφής συνεδρίας"), + ("Enable LAN discovery", "Ενεργοποίηση εντοπισμού LAN"), + ("Deny LAN discovery", "Απαγόρευση εντοπισμού LAN"), + ("Write a message", "Γράψτε ένα μήνυμα"), + ("Prompt", "Υπενθυμίζω"), + ("Please wait for confirmation of UAC...", "Παρακαλώ περιμένετε για επιβεβαίωση του UAC..."), + ("elevated_foreground_window_tip", "Το τρέχον παράθυρο της απομακρυσμένης επιφάνειας εργασίας απαιτεί υψηλότερα δικαιώματα για να λειτουργήσει, επομένως δεν μπορεί να χρησιμοποιήσει προσωρινά το ποντίκι και το πληκτρολόγιο. Μπορείτε να ζητήσετε από τον απομακρυσμένο χρήστη να ελαχιστοποιήσει το τρέχον παράθυρο ή να κάνετε κλικ στο κουμπί ανύψωσης στο παράθυρο διαχείρισης σύνδεσης. Για να αποφύγετε αυτό το πρόβλημα, συνιστάται η εγκατάσταση του λογισμικού στην απομακρυσμένη συσκευή."), + ("Disconnected", "Αποσυνδέθηκε"), + ("Other", "Άλλα"), + ("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσουν πολλαπλές καρτέλες"), + ("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"), + ("Full Access", "Πλήρης πρόσβαση"), + ("Screen Share", "Κοινή χρήση οθόνης"), + ("ubuntu-21-04-required", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), + ("wayland-requires-higher-linux-version", "Το Wayland απαιτεί υψηλότερη έκδοση διανομής του linux. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Σύνδεσμος μετάβασης"), + ("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."), + ("Show RustDesk", "Εμφάνιση του RustDesk"), + ("This PC", "Αυτός ο υπολογιστής"), + ("or", "ή"), + ("Elevate", "Ανύψωση"), + ("Zoom cursor", "Δρομέας ζουμ"), + ("Accept sessions via password", "Αποδοχή συνεδριών με κωδικό πρόσβασης"), + ("Accept sessions via click", "Αποδοχή συνεδριών με κλικ"), + ("Accept sessions via both", "Αποδοχή συνεδριών και με τα δύο"), + ("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα της συνεδρίας σας..."), + ("One-time Password", "Κωδικός μίας χρήσης"), + ("Use one-time password", "Χρήση κωδικού πρόσβασης μίας χρήσης"), + ("One-time password length", "Μήκος κωδικού πρόσβασης μίας χρήσης"), + ("Request access to your device", "Αίτημα πρόσβασης στη συσκευή σας"), + ("Hide connection management window", "Απόκρυψη παραθύρου διαχείρισης σύνδεσης"), + ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), + ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), + ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), + ("Skipped", "Παραλήφθηκε"), + ("Add to address book", "Προσθήκη στο βιβλίο διευθύνσεων"), + ("Group", "Ομάδα"), + ("Search", "Αναζήτηση"), + ("Closed manually by web console", "Κλείσιμο χειροκίνητα από την κονσόλα ιστού"), + ("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"), + ("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"), + ("software_render_tip", "Εάν χρησιμοποιείτε κάρτα γραφικών της Nvidia σε Linux και το παράθυρο απομακρυσμένης πρόσβασης κλείνει αμέσως μετά τη σύνδεση, η μετάβαση στο πρόγραμμα οδήγησης της Nouveau ανοιχτού κώδικα και η επιλογή χρήσης απόδοσης λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση του λογισμικού."), + ("Always use software rendering", "Να χρησιμοποιείτε πάντα η απόδοση λογισμικού"), + ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με το πληκτρολόγιο, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Παρακολούθηση εισόδου\"."), + ("config_microphone", "Για να μιλήσετε εξ αποστάσεως, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Εγγραφή ήχου\"."), + ("request_elevation_tip", "Μπορείτε επίσης να ζητήσετε ανύψωση εάν υπάρχει κάποιος στην απομακρυσμένη πλευρά."), + ("Wait", "Περιμένετε"), + ("Elevation Error", "Σφάλμα ανύψωσης"), + ("Ask the remote user for authentication", "Ζητήστε από τον απομακρυσμένο χρήστη έλεγχο ταυτότητας"), + ("Choose this if the remote account is administrator", "Επιλέξτε αυτό εάν ο απομακρυσμένος λογαριασμός είναι διαχειριστής"), + ("Transmit the username and password of administrator", "Μεταδώστε το όνομα χρήστη και τον κωδικό πρόσβασης του διαχειριστή"), + ("still_click_uac_tip", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο πλήκτρο Εντάξει στο παράθυρο UAC όπου εκτελείται το RustDesk."), + ("Request Elevation", "Αίτημα ανύψωσης"), + ("wait_accept_uac_tip", "Περιμένετε μέχρι ο απομακρυσμένος χρήστης να αποδεχτεί το παράθυρο διαλόγου UAC."), + ("Elevate successfully", "Επιτυχής ανύψωση"), + ("uppercase", "κεφαλαία γράμματα"), + ("lowercase", "πεζά γράμματα"), + ("digit", "αριθμός"), + ("special character", "ειδικός χαρακτήρας"), + ("length>=8", "μήκος>=8"), + ("Weak", "Αδύναμο"), + ("Medium", "Μέτριο"), + ("Strong", "Δυνατό"), + ("Switch Sides", "Αλλαγή πλευρών"), + ("Please confirm if you want to share your desktop?", "Παρακαλώ επιβεβαιώστε αν επιθυμείτε την κοινή χρήση της επιφάνειας εργασίας;"), + ("Display", "Εμφάνιση"), + ("Default View Style", "Προκαθορισμένος τρόπος εμφάνισης"), + ("Default Scroll Style", "Προκαθορισμένος τρόπος κύλισης"), + ("Default Image Quality", "Προκαθορισμένη ποιότητα εικόνας"), + ("Default Codec", "Προκαθορισμένη κωδικοποίηση"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Αυτόματο"), + ("Other Default Options", "Άλλες προκαθορισμένες ρυθμίσεις"), + ("Voice call", "Φωνητική κλήση"), + ("Text chat", "Συνομιλία κειμένου"), + ("Stop voice call", "Διακοπή φωνητικής κλήσης"), + ("relay_hint_tip", "Ενδέχεται να μην είναι δυνατή η απευθείας σύνδεση: μπορείτε να δοκιμάσετε να συνδεθείτε μέσω αναμετάδοσης. Επιπλέον, εάν θέλετε να χρησιμοποιήσετε την αναμετάδοση στην πρώτη σας προσπάθεια, μπορείτε να προσθέσετε την \"/r\" κατάληξη στο ID ή να επιλέξετε την επιλογή \"Πάντα σύνδεση μέσω αναμετάδοσης\" στην κάρτα πρόσφατων συνεδριών, εάν υπάρχει."), + ("Reconnect", "Επανασύνδεση"), + ("Codec", "Κωδικοποίηση"), + ("Resolution", "Ανάλυση"), + ("No transfers in progress", "Δεν υπάρχουν μεταφορές σε εξέλιξη"), + ("Set one-time password length", "Μέγεθος κωδικού μιας χρήσης"), + ("RDP Settings", "Ρυθμίσεις RDP"), + ("Sort by", "Ταξινόμηση κατά"), + ("New Connection", "Νέα σύνδεση"), + ("Restore", "Επαναφορά"), + ("Minimize", "Ελαχιστοποίηση"), + ("Maximize", "Μεγιστοποίηση"), + ("Your Device", "Η συσκευή σας"), + ("empty_recent_tip", "Ωχ, δεν υπάρχουν πρόσφατες συνεδρίες!\nΏρα να προγραμματίσετε μια νέα."), + ("empty_favorite_tip", "Δεν έχετε ακόμα αγαπημένους απομακρυσμένους σταθμούς;\nΑς βρούμε κάποιον για να συνδεθούμε και ας τον προσθέσουμε στα αγαπημένα σας!"), + ("empty_lan_tip", "Ωχ όχι, φαίνεται ότι δεν έχουμε ανακαλύψει ακόμη κανέναν απομακρυσμένο σταθμό."), + ("empty_address_book_tip", "Ω, Αγαπητέ/ή μου, φαίνεται ότι αυτήν τη στιγμή δεν υπάρχουν απομακρυσμένοι σταθμοί στο βιβλίο διευθύνσεών σας."), + ("Empty Username", "Κενό όνομα χρήστη"), + ("Empty Password", "Κενός κωδικός πρόσβασης"), + ("Me", "Εγώ"), + ("identical_file_tip", "Αυτό το αρχείο είναι πανομοιότυπο με αυτό του απομακρυσμένου σταθμού."), + ("show_monitors_tip", "Εμφάνιση οθονών στη γραμμή εργαλείων"), + ("View Mode", "Λειτουργία προβολής"), + ("login_linux_tip", "Πρέπει να συνδεθείτε σε έναν απομακρυσμένο λογαριασμό Linux για να ενεργοποιήσετε μια συνεδρία επιφάνειας εργασίας X"), + ("verify_rustdesk_password_tip", "Επιβεβαιώστε τον κωδικό του RustDesk"), + ("remember_account_tip", "Απομνημόνευση αυτού του λογαριασμού"), + ("os_account_desk_tip", "Αυτός ο λογαριασμός χρησιμοποιείται για σύνδεση στο απομακρυσμένο λειτουργικό σύστημα και ενεργοποίηση της συνεδρίας επιφάνειας εργασίας σε headless"), + ("OS Account", "Λογαριασμός λειτουργικού συστήματος"), + ("another_user_login_title_tip", "Υπάρχει ήδη άλλος συνδεδεμένος χρήστης"), + ("another_user_login_text_tip", "Αποσύνδεση"), + ("xorg_not_found_title_tip", "Δεν βρέθηκε το Xorg"), + ("xorg_not_found_text_tip", "Παρακαλώ εγκαταστήστε το Xorg"), + ("no_desktop_title_tip", "Δεν υπάρχει διαθέσιμο περιβάλλον επιφάνειας εργασίας"), + ("no_desktop_text_tip", "Παρακαλώ εγκαταστήστε το περιβάλλον GNOME"), + ("No need to elevate", "Δεν χρειάζεται ανύψωση"), + ("System Sound", "Ήχος συστήματος"), + ("Default", "Προκαθορισμένο"), + ("New RDP", "Νέα RDP"), + ("Fingerprint", "Δακτυλικό αποτύπωμα"), + ("Copy Fingerprint", "Αντιγραφή δακτυλικού αποτυπώματος"), + ("no fingerprints", "χωρίς δακτυλικά αποτυπώματα"), + ("Select a peer", "Επιλέξτε έναν σταθμό"), + ("Select peers", "Επιλέξτε σταθμούς"), + ("Plugins", "Επεκτάσεις"), + ("Uninstall", "Κατάργηση εγκατάστασης"), + ("Update", "Ενημέρωση"), + ("Enable", "Ενεργοποίηση"), + ("Disable", "Απενεργοποίηση"), + ("Options", "Επιλογές"), + ("resolution_original_tip", "Αρχική ανάλυση"), + ("resolution_fit_local_tip", "Προσαρμογή στην τοπική ανάλυση"), + ("resolution_custom_tip", "Προσαρμοσμένη ανάλυση"), + ("Collapse toolbar", "Σύμπτυξη γραμμής εργαλείων"), + ("Accept and Elevate", "Αποδοχή και ανύψωση"), + ("accept_and_elevate_btn_tooltip", "Αποδεχτείτε τη σύνδεση και ανυψώστε τα δικαιώματα UAC."), + ("clipboard_wait_response_timeout_tip", "Λήξη χρονικού ορίου αναμονής για απάντηση αντιγραφής."), + ("Incoming connection", "Εισερχόμενη σύνδεση"), + ("Outgoing connection", "Εξερχόμενη σύνδεση"), + ("Exit", "Έξοδος"), + ("Open", "Άνοιγμα"), + ("logout_tip", "Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε;"), + ("Service", "Υπηρεσία"), + ("Start", "Έναρξη"), + ("Stop", "Διακοπή"), + ("exceed_max_devices", "Έχετε φτάσει τον μέγιστο αριθμό διαχειριζόμενων συσκευών."), + ("Sync with recent sessions", "Συγχρονισμός των πρόσφατων συνεδριών"), + ("Sort tags", "Ταξινόμηση ετικετών"), + ("Open connection in new tab", "Άνοιγμα σύνδεσης σε νέα καρτέλα"), + ("Move tab to new window", "Μετακίνηση καρτέλας σε νέο παράθυρο"), + ("Can not be empty", "Δεν μπορεί να είναι κενό"), + ("Already exists", "Υπάρχει ήδη"), + ("Change Password", "Αλλαγή κωδικού"), + ("Refresh Password", "Ανανέωση κωδικού"), + ("ID", "ID"), + ("Grid View", "Προβολή σε πλακίδια"), + ("List View", "Προβολή σε λίστα"), + ("Select", "Επιλογή"), + ("Toggle Tags", "Εναλλαγή ετικετών"), + ("pull_ab_failed_tip", "Η ανανέωση του βιβλίου διευθύνσεων απέτυχε"), + ("push_ab_failed_tip", "Αποτυχία συγχρονισμού του βιβλίου διευθύνσεων με τον διακομιστή"), + ("synced_peer_readded_tip", "Οι συσκευές που υπήρχαν στις πρόσφατες συνεδρίες θα συγχρονιστούν ξανά με το βιβλίο διευθύνσεων."), + ("Change Color", "Αλλαγή χρώματος"), + ("Primary Color", "Κυρίως χρώμα"), + ("HSV Color", "Χρώμα HSV"), + ("Installation Successful!", "Επιτυχής εγκατάσταση!"), + ("Installation failed!", "Αποτυχία εγκατάστασης!"), + ("Reverse mouse wheel", "Αντιστροφή τροχού ποντικιού"), + ("{} sessions", "{} συνεδρίες"), + ("scam_title", "Ίσως είστε θύμα ΑΠΑΤΗΣ!"), + ("scam_text1", "Αν σας καλέσει κάποιος τον οποίο ΔΕΝ γνωρίζεται και ΔΕΝ ΕΜΠΙΣΤΕΥΕΣΤΕ, και σας ζητήσει να ανοίξετε το RustDesk, και να του πείτε τα στοιχεία που εμφανίζει, ΜΗΝ συνεχίσετε και κλείστε το τηλέφωνο ΑΜΕΣΩΣ."), + ("scam_text2", "Πιθανόν είναι κάποιος απατεώνας ο οποίος προσπαθεί να αποσπάσει τα στοιχεία του τραπεζικού σας λογαριασμού, τα χρήματά σας και τις προσωπικές σας πληροφορίες."), + ("Don't show again", "Να μην εμφανιστεί ξανά"), + ("I Agree", "Συμφωνώ"), + ("Decline", "Διαφωνώ"), + ("Timeout in minutes", "Τέλος χρόνου σε λεπτά"), + ("auto_disconnect_option_tip", "Αυτόματο κλείσιμο εισερχόμενων συνεδριών σε περίπτωση αδράνειας χρήστη"), + ("Connection failed due to inactivity", "Η σύνδεση τερματίστηκε έπειτα από την πάροδο του χρόνου αδράνειας"), + ("Check for software update on startup", "Έλεγχος για ενημερώσεις κατά την εκκίνηση"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Παρακαλώ ενημερώστε το RustDesk Server Pro στην έκδοση {} ή νεότερη!"), + ("pull_group_failed_tip", "Αποτυχία ανανέωσης της ομάδας"), + ("Filter by intersection", "Φιλτράρισμα κατά διασταύρωση"), + ("Remove wallpaper during incoming sessions", "Αφαίρεση εικόνας φόντου στις εισερχόμενες συνδέσεις"), + ("Test", "Δοκιμή"), + ("display_is_plugged_out_msg", "Η οθόνη είναι αποσυνδεδεμένη από την πρίζα, μεταβείτε στην πρώτη οθόνη."), + ("No displays", "Δεν υπάρχουν οθόνες"), + ("Open in new window", "Άνοιγμα σε νέο παράθυρο"), + ("Show displays as individual windows", "Εμφάνιση οθονών σε ξεχωριστά παράθυρα"), + ("Use all my displays for the remote session", "Χρήση όλων των οθονών της απομακρυσμένης σύνδεσης"), + ("selinux_tip", "Το SELinux είναι ενεργοποιημένο στη συσκευή σας, κάτι που ενδέχεται να εμποδίσει την σωστή λειτουργία του RustDesk ως ελεγχόμενης πλευράς."), + ("Change view", "Αλλαγή απεικόνισης"), + ("Big tiles", "Μεγάλα εικονίδια"), + ("Small tiles", "Μικρά εικονίδια"), + ("List", "Λίστα"), + ("Virtual display", "Εινονική οθόνη"), + ("Plug out all", "Αποσύνδεση όλων"), + ("True color (4:4:4)", "Αληθινό χρώμα (4:4:4)"), + ("Enable blocking user input", "Ενεργοποίηση αποκλεισμού χειρισμού από τον χρήστη"), + ("id_input_tip", "Μπορείτε να εισάγετε ένα ID, μια διεύθυνση IP, ή ένα όνομα τομέα με την αντίστοιχη πόρτα (:).\nΑν θέλετε να συνδεθείτε σε μια συσκευή σε άλλο διακομιστή, παρακαλώ να προσθέσετε και την διεύθυνση του διακομιστή (@?key=), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΑν θέλετε να συνδεθείτε σε κάποιο δημόσιο διακομιστή, προσθέστε το όνομά του \"@public\", η παράμετρος key δεν απαιτείται για τους δημόσιους διακομιστές."), + ("privacy_mode_impl_mag_tip", "Λειτουργία 1"), + ("privacy_mode_impl_virtual_display_tip", "Λειτουργία 2"), + ("Enter privacy mode", "Ενεργοποίηση λειτουργίας απορρήτου"), + ("Exit privacy mode", "Διακοπή λειτουργίας απορρήτου"), + ("idd_not_support_under_win10_2004_tip", "Το πρόγραμμα οδήγησης έμμεσης οθόνης δεν υποστηρίζεται. Απαιτείτε λειτουργικό σύστημα Windows 10 έκδοση 2004 ή νεότερο."), + ("input_source_1_tip", "Πηγή εισόδου 1"), + ("input_source_2_tip", "Πηγή εισόδου 2"), + ("Swap control-command key", "Εναλλαγή κουμπιών control-command"), + ("swap-left-right-mouse", "Εναλλαγή αριστερό-δεξί κουμπί του ποντικιού"), + ("2FA code", "κωδικός 2FA"), + ("More", "Περισσότερα"), + ("enable-2fa-title", "Ενεργοποίηση πιστοποίησης δύο παραγόντων"), + ("enable-2fa-desc", "Παρακαλούμε να ρυθμίστε τώρα τον έλεγχο ταυτότητας. Μπορείτε να χρησιμοποιήσετε μια εφαρμογή ελέγχου ταυτότητας όπως το Authy, το Microsoft ή το Google Authenticator στο τηλέφωνο ή τον υπολογιστή σας.\n\nΣαρώστε τον κωδικό QR με την εφαρμογή σας και εισαγάγετε τον κωδικό που εμφανίζει η εφαρμογή σας για να ενεργοποιήσετε τον έλεγχο ταυτότητας δύο παραγόντων."), + ("wrong-2fa-code", "Δεν είναι δυνατή η επαλήθευση του κωδικού. Ελέγξτε ότι οι ρυθμίσεις κωδικού και τοπικής ώρας είναι σωστές."), + ("enter-2fa-title", "Έλεγχος ταυτότητας δύο παραγόντων"), + ("Email verification code must be 6 characters.", "Ο κωδικός επαλήθευσης email πρέπει να είναι έως 6 χαρακτήρες"), + ("2FA code must be 6 digits.", "Ο κωδικός 2FA πρέπει να είναι 6ψήφιος."), + ("Multiple Windows sessions found", "Βρέθηκαν πολλές συνεδρίες των Windows"), + ("Please select the session you want to connect to", "Επιλέξτε τη συνεδρία στην οποία θέλετε να συνδεθείτε"), + ("powered_by_me", "Με την υποστήριξη του RustDesk"), + ("outgoing_only_desk_tip", "Αυτή είναι μια προσαρμοσμένη έκδοση.\nΜπορείτε να συνδεθείτε με άλλες συσκευές, αλλά άλλες συσκευές δεν μπορούν να συνδεθούν με τη δική σας συσκευή."), + ("preset_password_warning", "Αυτή η προσαρμοσμένη έκδοση συνοδεύεται από έναν προκαθορισμένο κωδικό πρόσβασης. Όποιος γνωρίζει αυτόν τον κωδικό πρόσβασης θα μπορούσε να αποκτήσει τον πλήρη έλεγχο της συσκευής σας. Εάν δεν το περιμένατε αυτό, απεγκαταστήστε αμέσως το λογισμικό."), + ("Security Alert", "Ειδοποίηση ασφαλείας"), + ("My address book", "Το βιβλίο διευθύνσεών μου"), + ("Personal", "Προσωπικό"), + ("Owner", "Ιδιοκτήτης"), + ("Set shared password", "Ορίστε έναν κοινόχρηστο κωδικό πρόσβασης"), + ("Exist in", "Υπάρχει στο"), + ("Read-only", "Μόνο για ανάγνωση"), + ("Read/Write", "Ανάγνωση/Εγγραφή"), + ("Full Control", "Πλήρης έλεγχος"), + ("share_warning_tip", "Τα παραπάνω πεδία είναι κοινόχρηστα και ορατά σε άλλους."), + ("Everyone", "Όλοι"), + ("ab_web_console_tip", "Περισσότερα στην κονσόλα web"), + ("allow-only-conn-window-open-tip", "Να επιτρέπεται η σύνδεση μόνο εάν το παράθυρο RustDesk είναι ανοιχτό"), + ("no_need_privacy_mode_no_physical_displays_tip", "Δεν υπάρχουν φυσικές οθόνες, δεν χρειάζεται να χρησιμοποιήσετε τη λειτουργία απορρήτου."), + ("Follow remote cursor", "Παρακολούθηση απομακρυσμένου κέρσορα"), + ("Follow remote window focus", "Παρακολούθηση απομακρυσμένου ενεργού παραθύρου"), + ("default_proxy_tip", "Το προεπιλεγμένο πρωτόκολλο και η θύρα είναι Socks5 και 1080"), + ("no_audio_input_device_tip", "Δεν βρέθηκε συσκευή εισόδου ήχου."), + ("Incoming", "Εισερχόμενη"), + ("Outgoing", "Εξερχόμενη"), + ("Clear Wayland screen selection", "Εκκαθάριση επιλογής οθόνης Wayland"), + ("clear_Wayland_screen_selection_tip", "Αφού διαγράψετε την επιλογή οθόνης, μπορείτε να επιλέξετε ξανά την οθόνη για κοινή χρήση."), + ("confirm_clear_Wayland_screen_selection_tip", "Είστε βέβαιοι ότι θέλετε να διαγράψετε την επιλογή οθόνης Wayland;"), + ("android_new_voice_call_tip", "Ελήφθη ένα νέο αίτημα φωνητικής κλήσης. Εάν το αποδεχτείτε, ο ήχος θα μεταβεί σε φωνητική επικοινωνία."), + ("texture_render_tip", "Χρησιμοποιήστε την απόδοση υφής για να κάνετε τις εικόνες πιο ομαλές. Μπορείτε να δοκιμάσετε να απενεργοποιήσετε αυτήν την επιλογή εάν αντιμετωπίσετε προβλήματα απόδοσης."), + ("Use texture rendering", "Χρήση απόδοσης υφής"), + ("Floating window", "Πλωτό παράθυρο"), + ("floating_window_tip", "Βοηθά στη διατήρηση της υπηρεσίας παρασκηνίου RustDesk"), + ("Keep screen on", "Διατήρηση οθόνης Ανοιχτή"), + ("Never", "Ποτέ"), + ("During controlled", "Κατα την διάρκεια απομακρυσμένου ελέγχου"), + ("During service is on", "Κατα την εκκίνηση της υπηρεσίας Rustdesk"), + ("Capture screen using DirectX", "Καταγραφή οθόνης με χρήση DirectX"), + ("Back", "Πίσω"), + ("Apps", "Εφαρμογές"), + ("Volume up", "Αύξηση έντασης"), + ("Volume down", "Μείωση έντασης"), + ("Power", "Ενέργεια"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Εάν ενεργοποιήσετε αυτήν τη δυνατότητα, μπορείτε να λάβετε τον κωδικό 2FA από το bot σας. Μπορεί επίσης να λειτουργήσει ως ειδοποίηση σύνδεσης."), + ("enable-bot-desc", "1, Ανοίξτε μια συνομιλία με τον @BotFather., Στείλτε την εντολή \"/newbot\". Θα λάβετε ένα διακριτικό αφού ολοκληρώσετε αυτό το βήμα.3, Ξεκινήστε μια συνομιλία με το bot που μόλις δημιουργήσατε. Στείλτε ένα μήνυμα που αρχίζει με κάθετο (\"/\") όπως \"/hello\" για να το ενεργοποιήσετε."), + ("cancel-2fa-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το 2FA;"), + ("cancel-bot-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το Telegram bot;"), + ("About RustDesk", "Πληροφορίες για το RustDesk"), + ("Send clipboard keystrokes", "Αποστολή προχείρου με πλήκτρα συντόμευσης"), + ("network_error_tip", "Ελέγξτε τη σύνδεσή σας στο δίκτυο και, στη συνέχεια, κάντε κλικ στην επανάληψη."), + ("Unlock with PIN", "Ξεκλείδωμα με PIN"), + ("Requires at least {} characters", "Απαιτούνται τουλάχιστον {} χαρακτήρες"), + ("Wrong PIN", "Λάθος PIN"), + ("Set PIN", "Ορισμός PIN"), + ("Enable trusted devices", "Ενεργοποίηση αξιόπιστων συσκευών"), + ("Manage trusted devices", "Διαχείριση αξιόπιστων συσκευών"), + ("Platform", "Πλατφόρμα"), + ("Days remaining", "Ημέρες που απομένουν"), + ("enable-trusted-devices-tip", "Παράβλεψη επαλήθευσης 2FA σε αξιόπιστες συσκευές."), + ("Parent directory", "Γονικός φάκελος"), + ("Resume", "Συνέχεια"), + ("Invalid file name", "Μη έγκυρο όνομα αρχείου"), + ("one-way-file-transfer-tip", "Η μονόδρομη μεταφορά αρχείων είναι ενεργοποιημένη στην ελεγχόμενη πλευρά."), + ("Authentication Required", "Απαιτείται έλεγχος ταυτότητας"), + ("Authenticate", "Πιστοποίηση"), + ("web_id_input_tip", "Μπορείτε να εισαγάγετε ένα ID στον ίδιο διακομιστή, η άμεση πρόσβαση IP δεν υποστηρίζεται στον web client.\nΕάν θέλετε να αποκτήσετε πρόσβαση σε μια συσκευή σε άλλον διακομιστή, παρακαλούμε να προσθέστε τη διεύθυνση διακομιστή (@?key=), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΕάν θέλετε να αποκτήσετε πρόσβαση σε μια συσκευή σε δημόσιο διακομιστή, παρακαλούμε να εισαγάγετε \"@public\". Το κλειδί δεν είναι απαραίτητο για δημόσιο διακομιστή."), + ("Download", "Λήψη"), + ("Upload folder", "Μεταφόρτωση φακέλου"), + ("Upload files", "Μεταφόρτωση αρχείων"), + ("Clipboard is synchronized", "Το πρόχειρο έχει συγχρονιστεί"), + ("Update client clipboard", "Ενημέρωση απομακρισμένου προχείρου"), + ("Untagged", "Χωρίς ετικέτα"), + ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), + ("Accessible devices", "Προσβάσιμες συσκευές"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"), + ("d3d_render_tip", "Όταν είναι ενεργοποιημένη η απόδοση D3D, η οθόνη του τηλεχειριστηρίου ενδέχεται να είναι μαύρη σε ορισμένα μηχανήματα."), + ("Use D3D rendering", "Χρήση απόδοσης D3D"), + ("Printer", "Εκτυπωτής"), + ("printer-os-requirement-tip", "Η λειτουργία εξερχόμενης εκτύπωσης του εκτυπωτή απαιτεί Windows 10 ή νεότερη έκδοση."), + ("printer-requires-installed-{}-client-tip", "Για να χρησιμοποιήσετε την απομακρυσμένη εκτύπωση, πρέπει να εγκατασταθεί το {} σε αυτήν τη συσκευή."), + ("printer-{}-not-installed-tip", "Ο εκτυπωτής {} δεν είναι εγκατεστημένος."), + ("printer-{}-ready-tip", "Ο εκτυπωτής {} είναι εγκατεστημένος και έτοιμος για χρήση."), + ("Install {} Printer", "Εγκατάσταση εκτυπωτή {}"), + ("Outgoing Print Jobs", "Εξερχόμενες εργασίες εκτύπωσης"), + ("Incoming Print Jobs", "Εισερχόμενες εργασίες εκτύπωσης"), + ("Incoming Print Job", "Εισερχόμενη εργασία εκτύπωσης"), + ("use-the-default-printer-tip", "Χρήση του προεπιλεγμένου εκτυπωτή"), + ("use-the-selected-printer-tip", "Χρήση του επιλεγμένου εκτυπωτή"), + ("auto-print-tip", "Εκτυπώστε αυτόματα χρησιμοποιώντας τον επιλεγμένο εκτυπωτή."), + ("print-incoming-job-confirm-tip", "Λάβατε μια εργασία εκτύπωσης από απόσταση. Θέλετε να την εκτελέσετε από την πλευρά σας;"), + ("remote-printing-disallowed-tile-tip", "Η απομακρυσμένη εκτύπωση δεν επιτρέπεται"), + ("remote-printing-disallowed-text-tip", "Οι ρυθμίσεις δικαιωμάτων της ελεγχόμενης πλευράς απαγορεύουν την Απομακρυσμένη Εκτύπωση."), + ("save-settings-tip", "Αποθήκευση ρυθμίσεων"), + ("dont-show-again-tip", "Να μην εμφανιστεί ξανά αυτό"), + ("Take screenshot", "Λήψη στιγμιότυπου οθόνης"), + ("Taking screenshot", "Γίνεται λήψη στιγμιότυπου οθόνης"), + ("screenshot-merged-screen-not-supported-tip", "Η συγχώνευση στιγμιότυπων οθόνης από πολλές οθόνες δεν υποστηρίζεται προς το παρόν. Αλλάξτε σε μία μόνο οθόνη και δοκιμάστε ξανά."), + ("screenshot-action-tip", "Επιλέξτε πώς θα συνεχίσετε με το στιγμιότυπο οθόνης."), + ("Save as", "Αποθήκευση ως"), + ("Copy to clipboard", "Αντιγραφή στο πρόχειρο"), + ("Enable remote printer", "Ενεργοποίηση απομακρυσμένου εκτυπωτή"), + ("Downloading {}", "Γίνεται Λήψη {}"), + ("{} Update", "{} Ενημέρωση"), + ("{}-to-update-tip", "Το {} θα κλείσει τώρα και θα εγκαταστήσει τη νέα έκδοση."), + ("download-new-version-failed-tip", "Η λήψη απέτυχε. Μπορείτε να δοκιμάσετε ξανά ή να κάνετε κλικ στο κουμπί \"Λήψη\" για να κάνετε λήψη από τη σελίδα έκδοσης και να κάνετε αναβάθμιση χειροκίνητα."), + ("Auto update", "Αυτόματη ενημέρωση"), + ("update-failed-check-msi-tip", "Η μέθοδος εγκατάστασης απέτυχε. Κάντε κλικ στο κουμπί \"Λήψη\" για λήψη από τη σελίδα έκδοσης και κάντε χειροκίνητα την αναβάθμιση."), + ("websocket_tip", "Όταν χρησιμοποιείτε το WebSocket, υποστηρίζονται μόνο συνδέσεις αναμετάδοσης."), + ("Use WebSocket", "Χρήση WebSocket"), + ("Trackpad speed", "Ταχύτητα trackpad"), + ("Default trackpad speed", "Προεπιλεγμένη ταχύτητα trackpad"), + ("Numeric one-time password", "Αριθμητικός κωδικός πρόσβασης μίας χρήσης"), + ("Enable IPv6 P2P connection", "Ενεργοποίηση σύνδεσης IPv6 P2P"), + ("Enable UDP hole punching", "Ενεργοποίηση διάτρησης οπών UDP"), + ("View camera", "Προβολή κάμερας"), + ("Enable camera", "Ενεργοποίηση κάμερας"), + ("No cameras", "Δεν υπάρχουν κάμερες"), + ("view_camera_unsupported_tip", "Η τηλεχειριστήριο δεν υποστηρίζει την προβολή της κάμερας."), + ("Terminal", "Τερματικό"), + ("Enable terminal", "Ενεργοποίηση τερματικού"), + ("New tab", "Νέα καρτέλα"), + ("Keep terminal sessions on disconnect", "Διατήρηση περιόδων λειτουργίας τερματικού κατά την αποσύνδεση"), + ("Terminal (Run as administrator)", "Τερματικό (Εκτέλεση ως διαχειριστής)"), + ("terminal-admin-login-tip", "Παρακαλώ εισάγετε το όνομα χρήστη και τον κωδικό πρόσβασης διαχειριστή της ελεγχόμενης πλευράς."), + ("Failed to get user token.", "Αποτυχία λήψης διακριτικού χρήστη."), + ("Incorrect username or password.", "Λανθασμένο όνομα χρήστη ή κωδικός πρόσβασης."), + ("The user is not an administrator.", "Ο χρήστης δεν είναι διαχειριστής."), + ("Failed to check if the user is an administrator.", "Αποτυχία ελέγχου εάν ο χρήστης είναι διαχειριστής."), + ("Supported only in the installed version.", "Υποστηρίζεται μόνο στην εγκατεστημένη έκδοση."), + ("elevation_username_tip", "Εισαγάγετε όνομα χρήστη ή τομέα\\όνομα χρήστη"), + ("Preparing for installation ...", "Προετοιμασία για εγκατάσταση..."), + ("Show my cursor", "Εμφάνιση του κέρσορα μου"), + ("Scale custom", "Προσαρμοσμένη κλίμακα"), + ("Custom scale slider", "Ρυθμιστικό προσαρμοσμένης κλίμακας"), + ("Decrease", "Μείωση"), + ("Increase", "Αύξηση"), + ("Show virtual mouse", "Εμφάνιση εικονικού ποντικιού"), + ("Virtual mouse size", "Μέγεθος εικονικού ποντικιού"), + ("Small", "Μικρό"), + ("Large", "Μεγάλο"), + ("Show virtual joystick", "Εμφάνιση εικονικού joystick"), + ("Edit note", "Επεξεργασία σημείωσης"), + ("Alias", "Ψευδώνυμο"), + ("ScrollEdge", "Άκρη κύλισης"), + ("Allow insecure TLS fallback", "Να επιτρέπεται η μη ασφαλής εφεδρική λειτουργία TLS"), + ("allow-insecure-tls-fallback-tip", "Από προεπιλογή, το RustDesk επαληθεύει το πιστοποιητικό διακομιστή για πρωτόκολλα που χρησιμοποιούν TLS.\nΜε ενεργοποιημένη αυτήν την επιλογή, το RustDesk θα παρακάμψει το βήμα επαλήθευσης και θα προχωρήσει σε περίπτωση αποτυχίας επαλήθευσης."), + ("Disable UDP", "Απενεργοποίηση UDP"), + ("disable-udp-tip", "Ελέγχει εάν θα χρησιμοποιείται μόνο TCP.\nΌταν είναι ενεργοποιημένη αυτή η επιλογή, το RustDesk δεν θα χρησιμοποιεί πλέον το UDP 21116, αλλά θα χρησιμοποιείται το TCP 21116."), + ("server-oss-not-support-tip", "ΣΗΜΕΙΩΣΗ: Το OSS του διακομιστή RustDesk δεν περιλαμβάνει αυτήν τη λειτουργία."), + ("input note here", "εισάγετε σημείωση εδώ"), + ("note-at-conn-end-tip", "Ζητήστε σημείωση στο τέλος της σύνδεσης"), + ("Show terminal extra keys", "Εμφάνιση επιπλέον κλειδιών τερματικού"), + ("Relative mouse mode", "Σχετική λειτουργία ποντικιού"), + ("rel-mouse-not-supported-peer-tip", "Η λειτουργία σχετικού ποντικιού δεν υποστηρίζεται από τον συνδεδεμένο ομότιμο υπολογιστή."), + ("rel-mouse-not-ready-tip", "Η λειτουργία σχετικού ποντικιού δεν είναι ακόμη έτοιμη. Δοκιμάστε ξανά."), + ("rel-mouse-lock-failed-tip", "Αποτυχία κλειδώματος δρομέα. Η λειτουργία σχετικού ποντικιού έχει απενεργοποιηθεί."), + ("rel-mouse-exit-{}-tip", "Πιέστε {} για έξοδο."), + ("rel-mouse-permission-lost-tip", "Η άδεια πληκτρολογίου ανακλήθηκε. Η λειτουργία σχετικού ποντικιού απενεργοποιήθηκε."), + ("Changelog", "Αρχείο αλλαγών"), + ("keep-awake-during-outgoing-sessions-label", "Διατήρηση ενεργής οθόνης κατά τη διάρκεια εξερχόμενων συνεδριών"), + ("keep-awake-during-incoming-sessions-label", "Διατήρηση ενεργής οθόνης κατά τη διάρκεια των εισερχόμενων συνεδριών"), + ("Continue with {}", "Συνέχεια με {}"), + ("Display Name", "Εμφανιζόμενο όνομα"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/en.rs b/vendor/rustdesk/src/lang/en.rs new file mode 100644 index 0000000..37928a5 --- /dev/null +++ b/vendor/rustdesk/src/lang/en.rs @@ -0,0 +1,278 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("desk_tip", "Your desktop can be accessed with this ID and password."), + ("connecting_status", "Connecting to the RustDesk network..."), + ("not_ready_status", "Not ready. Please check your connection"), + ("ID/Relay Server", "ID/Relay server"), + ("id_change_tip", "Only a-z, A-Z, 0-9, - (dash) and _ (underscore) characters allowed. The first letter must be a-z, A-Z. Length between 6 and 16."), + ("Slogan_tip", "Made with heart in this chaotic world!"), + ("Build Date", "Build date"), + ("Audio Input", "Audio input"), + ("Hardware Codec", "Hardware codec"), + ("ID Server", "ID server"), + ("Relay Server", "Relay server"), + ("API Server", "API server"), + ("invalid_http", "must start with http:// or https://"), + ("server_not_support", "Not yet supported by the server"), + ("Password Required", "Password required"), + ("Wrong Password", "Wrong password"), + ("Connection Error", "Connection error"), + ("Login Error", "Login error"), + ("Show Hidden Files", "Show hidden files"), + ("Refresh File", "Refresh file"), + ("Remote Computer", "Remote computer"), + ("Local Computer", "Local computer"), + ("Confirm Delete", "Confirm delete"), + ("Multi Select", "Multi select"), + ("Select All", "Select all"), + ("Unselect All", "Unselect all"), + ("Empty Directory", "Empty directory"), + ("Custom Image Quality", "Custom image quality"), + ("Adjust Window", "Adjust window"), + ("Insert Lock", "Insert lock"), + ("Set Password", "Set password"), + ("OS Password", "OS password"), + ("install_tip", "Due to UAC, RustDesk can not work properly as the remote side in some cases. To avoid UAC, please click the button below to install RustDesk to the system."), + ("config_acc", "In order to control your Desktop remotely, you need to grant RustDesk \"Accessibility\" permissions."), + ("config_screen", "In order to access your Desktop remotely, you need to grant RustDesk \"Screen Recording\" permissions."), + ("Installation Path", "Installation path"), + ("agreement_tip", "By starting the installation, you accept the license agreement."), + ("Accept and Install", "Accept and install"), + ("not_close_tcp_tip", "Don't close this window while you are using the tunnel"), + ("Remote Host", "Remote host"), + ("Remote Port", "Remote port"), + ("Local Port", "Local port"), + ("Local Address", "Local address"), + ("Change Local Port", "Change local port"), + ("setup_server_tip", "For faster connection, please set up your own server"), + ("Enter Remote ID", "Enter remote ID"), + ("Auto Login", "Auto Login (Only valid if you set \"Lock after session end\")"), + ("Change Path", "Change path"), + ("Create Folder", "Create folder"), + ("whitelist_tip", "Only whitelisted IP can access me"), + ("verification_tip", "A verification code has been sent to the registered email address, enter the verification code to continue logging in."), + ("whitelist_sep", "Separated by comma, semicolon, spaces or new line"), + ("Add Tag", "Add tag"), + ("Wrong credentials", "Wrong username or password"), + ("Edit Tag", "Edit tag"), + ("Forget Password", "Forget password"), + ("Add to Favorites", "Add to favorites"), + ("Remove from Favorites", "Remove from favorites"), + ("Socks5 Proxy", "Socks5 proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) proxy"), + ("install_daemon_tip", "For starting on boot, you need to install system service."), + ("Are you sure to close the connection?", "Are you sure you want to close the connection?"), + ("One-Finger Tap", "One-finger tap"), + ("Left Mouse", "Left mouse"), + ("One-Long Tap", "One-long tap"), + ("Two-Finger Tap", "Two-finger tap"), + ("Right Mouse", "Right mouse"), + ("One-Finger Move", "One-finger move"), + ("Double Tap & Move", "Double tap & move"), + ("Mouse Drag", "Mouse drag"), + ("Three-Finger vertically", "Three-finger vertically"), + ("Mouse Wheel", "Mouse wheel"), + ("Two-Finger Move", "Two-finger move"), + ("Canvas Move", "Canvas move"), + ("Pinch to Zoom", "Pinch to zoom"), + ("Canvas Zoom", "Canvas zoom"), + ("Screen Capture", "Screen capture"), + ("Input Control", "Input control"), + ("Audio Capture", "Audio capture"), + ("Open System Setting", "Open system setting"), + ("android_input_permission_tip1", "In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the \"Accessibility\" service."), + ("android_input_permission_tip2", "Please go to the next system settings page, find and enter [Installed Services], turn on [RustDesk Input] service."), + ("android_new_connection_tip", "New control request has been received, which wants to control your current device."), + ("android_service_will_start_tip", "Turning on \"Screen Capture\" will automatically start the service, allowing other devices to request a connection to your device."), + ("android_stop_service_tip", "Closing the service will automatically close all established connections."), + ("android_version_audio_tip", "The current Android version does not support audio capture, please upgrade to Android 10 or higher."), + ("android_start_service_tip", "Tap [Start service] or enable [Screen Capture] permission to start the screen sharing service."), + ("android_permission_may_not_change_tip", "Permissions for established connections may not be changed instantly until reconnected."), + ("doc_mac_permission", "https://cstudio.ch/hello-agent/docs/en/client/mac/#enable-permissions"), + ("Ignore Battery Optimizations", "Ignore battery optimizations"), + ("android_open_battery_optimizations_tip", "If you want to disable this feature, please go to the next RustDesk application settings page, find and enter [Battery], Uncheck [Unrestricted]"), + ("remote_restarting_tip", "Remote device is restarting, please close this message box and reconnect with permanent password after a while"), + ("Exit Fullscreen", "Exit fullscreen"), + ("Mobile Actions", "Mobile actions"), + ("Select Monitor", "Select monitor"), + ("Control Actions", "Control actions"), + ("Display Settings", "Display settings"), + ("Image Quality", "Image quality"), + ("Scroll Style", "Scroll style"), + ("Show Toolbar", "Show toolbar"), + ("Hide Toolbar", "Hide toolbar"), + ("Direct Connection", "Direct connection"), + ("Relay Connection", "Relay connection"), + ("Secure Connection", "Secure connection"), + ("Insecure Connection", "Insecure connection"), + ("Dark Theme", "Dark theme"), + ("Light Theme", "Light theme"), + ("Follow System", "Follow system"), + ("Unlock Security Settings", "Unlock security settings"), + ("Unlock Network Settings", "Unlock network settings"), + ("Direct IP Access", "Direct IP access"), + ("Audio Input Device", "Audio input device"), + ("Use IP Whitelisting", "Use IP whitelisting"), + ("Pin Toolbar", "Pin toolbar"), + ("Unpin Toolbar", "Unpin toolbar"), + ("elevated_foreground_window_tip", "The current window of the remote desktop requires higher privilege to operate, so it's unable to use the mouse and keyboard temporarily. You can request the remote user to minimize the current window, or click elevation button on the connection management window. To avoid this problem, it is recommended to install the software on the remote device."), + ("Keyboard Settings", "Keyboard settings"), + ("Full Access", "Full access"), + ("Screen Share", "Screen share"), + ("ubuntu-21-04-required", "Wayland requires Ubuntu 21.04 or higher version."), + ("wayland-requires-higher-linux-version", "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."), + ("xdp-portal-unavailable", "Wayland screen capture failed. The XDG Desktop Portal may have crashed or is unavailable. Try restarting it with `systemctl --user restart xdg-desktop-portal`."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Please select the screen to be shared(Operate on the peer side)."), + ("One-time Password", "One-time password"), + ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using permanent password"), + ("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), + ("software_render_tip", "If you're using Nvidia graphics card under Linux and the remote window closes immediately after connecting, switching to the open-source Nouveau driver and choosing to use software rendering may help. A software restart is required."), + ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), + ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions."), + ("request_elevation_tip", "You can also request elevation if there is someone on the remote side."), + ("Elevation Error", "Elevation error"), + ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."), + ("Request Elevation", "Request elevation"), + ("wait_accept_uac_tip", "Please wait for the remote user to accept the UAC dialog."), + ("Switch Sides", "Switch sides"), + ("Default View Style", "Default view style"), + ("Default Scroll Style", "Default scroll style"), + ("Default Image Quality", "Default image quality"), + ("Default Codec", "Default codec"), + ("Other Default Options", "Other default options"), + ("relay_hint_tip", "It may not be possible to connect directly; you can try connecting via relay. Additionally, if you want to use a relay on your first attempt, you can add the \"/r\" suffix to the ID or select the option \"Always connect via relay\" in the card of recent sessions if it exists."), + ("RDP Settings", "RDP settings"), + ("New Connection", "New connection"), + ("Your Device", "Your device"), + ("empty_recent_tip", "Oops, no recent sessions!\nTime to plan a new one."), + ("empty_favorite_tip", "No favorite peers yet?\nLet's find someone to connect with and add it to your favorites!"), + ("empty_lan_tip", "Oh no, it looks like we haven't discovered any peers yet."), + ("empty_address_book_tip", "Oh dear, it appears that there are currently no peers listed in your address book."), + ("Empty Username", "Empty username"), + ("Empty Password", "Empty password"), + ("identical_file_tip", "This file is identical with the peer's one."), + ("show_monitors_tip", "Show monitors in toolbar"), + ("View Mode", "View mode"), + ("login_linux_tip", "You need to login to remote Linux account to enable a X desktop session"), + ("verify_rustdesk_password_tip", "Verify RustDesk password"), + ("remember_account_tip", "Remember this account"), + ("os_account_desk_tip", "This account is used to login the remote OS and enable the desktop session in headless"), + ("OS Account", "OS account"), + ("another_user_login_title_tip", "Another user already logged in"), + ("another_user_login_text_tip", "Disconnect"), + ("xorg_not_found_title_tip", "Xorg not found"), + ("xorg_not_found_text_tip", "Please install Xorg"), + ("no_desktop_title_tip", "No desktop environment is available"), + ("no_desktop_text_tip", "Please install GNOME desktop"), + ("System Sound", "System sound"), + ("Copy Fingerprint", "Copy fingerprint"), + ("no fingerprints", "No fingerprints"), + ("resolution_original_tip", "Original resolution"), + ("resolution_fit_local_tip", "Fit local resolution"), + ("resolution_custom_tip", "Custom resolution"), + ("Accept and Elevate", "Accept and elevate"), + ("accept_and_elevate_btn_tooltip", "Accept the connection and elevate UAC permissions."), + ("clipboard_wait_response_timeout_tip", "Timed out waiting for copy response."), + ("logout_tip", "Are you sure you want to log out?"), + ("exceed_max_devices", "You have reached the maximum number of managed devices."), + ("Change Password", "Change password"), + ("Refresh Password", "Refresh password"), + ("Grid View", "Grid view"), + ("List View", "List view"), + ("Toggle Tags", "Toggle tags"), + ("pull_ab_failed_tip", "Failed to refresh address book"), + ("push_ab_failed_tip", "Failed to sync address book to server"), + ("synced_peer_readded_tip", "The devices that were present in the recent sessions will be synchronized back to the address book."), + ("Change Color", "Change color"), + ("Primary Color", "Primary color"), + ("HSV Color", "HSV color"), + ("Installation Successful!", "Installation successful!"), + ("scam_title", "You May Be Being SCAMMED!"), + ("scam_text1", "If you are on the phone with someone you DON'T know AND TRUST who has asked you to use RustDesk and start the service, do not proceed and hang up immediately."), + ("scam_text2", "They are likely a scammer trying to steal your money or other private information."), + ("auto_disconnect_option_tip", "Automatically close incoming sessions on user inactivity"), + ("Connection failed due to inactivity", "Automatically disconnected due to inactivity"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Please upgrade RustDesk Server Pro to version {} or newer!"), + ("pull_group_failed_tip", "Failed to refresh group"), + ("doc_fix_wayland", "https://cstudio.ch/hello-agent/docs/en/client/linux/#x11-required"), + ("display_is_plugged_out_msg", "The display is plugged out, switch to the first display."), + ("selinux_tip", "SELinux is enabled on your device, which may prevent RustDesk from running properly as controlled side."), + ("id_input_tip", "You can input an ID, a direct IP, or a domain with a port (:).\nIf you want to access a device on another server, please append the server address (@?key=), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"@public\", the key is not needed for public server.\n\nIf you want to force the use of a relay connection on the first connection, add \"/r\" at the end of the ID, for example, \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Mode 1"), + ("privacy_mode_impl_virtual_display_tip", "Mode 2"), + ("idd_not_support_under_win10_2004_tip", "Indirect display driver is not supported. Windows 10, version 2004 or newer is required."), + ("input_source_1_tip", "Input source 1"), + ("input_source_2_tip", "Input source 2"), + ("swap-left-right-mouse", "Swap left-right mouse button"), + ("2FA code", "2FA code"), + ("enable-2fa-title", "Enable two-factor authentication"), + ("enable-2fa-desc", "Please set up your authenticator now. You can use an authenticator app such as Authy, Microsoft or Google Authenticator on your phone or desktop.\n\nScan the QR code with your app and enter the code that your app shows to enable two-factor authentication."), + ("wrong-2fa-code", "Can't verify the code. Check that code and local time settings are correct"), + ("enter-2fa-title", "Two-factor authentication"), + ("powered_by_me", "Powered by RustDesk"), + ("outgoing_only_desk_tip", "This is a customized edition.\nYou can connect to other devices, but other devices cannot connect to your device."), + ("preset_password_warning", "This customized edition comes with a preset password. Anyone knowing this password could gain full control of your device. If you did not expect this, uninstall the software immediately."), + ("share_warning_tip", "The fields above are shared and visible to others."), + ("ab_web_console_tip", "More on web console"), + ("allow-only-conn-window-open-tip", "Only allow connection if RustDesk window is open"), + ("no_need_privacy_mode_no_physical_displays_tip", "No physical displays, no need to use the privacy mode."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), + ("default_proxy_tip", "Default protocol and port are Socks5 and 1080"), + ("no_audio_input_device_tip", "No audio input device found."), + ("clear_Wayland_screen_selection_tip", "After clearing the screen selection, you can reselect the screen to share."), + ("confirm_clear_Wayland_screen_selection_tip", "Are you sure you want to clear the Wayland screen selection?"), + ("android_new_voice_call_tip", "A new voice call request was received. If you accept, the audio will switch to voice communication."), + ("texture_render_tip", "Use texture rendering to make the pictures smoother. You could try disabling this option if you encounter rendering issues."), + ("floating_window_tip", "It helps to keep RustDesk background service"), + ("enable-bot-tip", "If you enable this feature, you can receive the 2FA code from your bot. It can also function as a connection notification."), + ("enable-bot-desc", "1. Open a chat with @BotFather.\n2. Send the command \"/newbot\". You will receive a token after completing this step.\n3. Start a chat with your newly created bot. Send a message beginning with a forward slash (\"/\") like \"/hello\" to activate it.\n"), + ("cancel-2fa-confirm-tip", "Are you sure you want to cancel 2FA?"), + ("cancel-bot-confirm-tip", "Are you sure you want to cancel Telegram bot?"), + ("About RustDesk", ""), + ("network_error_tip", "Please check your network connection, then click retry."), + ("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"), + ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), + ("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (@?key=), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"@public\", the key is not needed for public server."), + ("new-version-of-{}-tip", "There is a new version of {} available"), + ("View camera", "View camera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Please upgrade the RustDesk client to version {} or newer on the remote side!"), + ("view_camera_unsupported_tip", "The remote device does not support viewing the camera."), + ("d3d_render_tip", "When D3D rendering is enabled, the remote control screen may be black on some machines."), + ("printer-requires-installed-{}-client-tip", "In order to use remote printing, {} needs to be installed on this device."), + ("printer-os-requirement-tip", "The printer outgoing function requires Windows 10 or higher."), + ("printer-{}-not-installed-tip", "The {} Printer is not installed."), + ("printer-{}-ready-tip", "The {} Printer is installed and ready to use."), + ("auto-print-tip", "Print automatically using the selected printer."), + ("print-incoming-job-confirm-tip", "You received a print job from remote. Do you want to execute it at your side?"), + ("use-the-default-printer-tip", "Use the default printer"), + ("use-the-selected-printer-tip", "Use the selected printer"), + ("remote-printing-disallowed-tile-tip", "Remote Printing disallowed"), + ("remote-printing-disallowed-text-tip", "The permission settings of the controlled side deny Remote Printing."), + ("save-settings-tip", "Save settings"), + ("dont-show-again-tip", "Don't show this again"), + ("screenshot-merged-screen-not-supported-tip", "Merging screenshots of multiple displays is currently not supported. Please switch to a single display and try again."), + ("screenshot-action-tip", "Please select how to continue with the screenshot."), + ("{}-to-update-tip", "{} will close now and install the new version."), + ("download-new-version-failed-tip", "Download failed. You can try again or click the \"Download\" button to download from the release page and upgrade manually."), + ("update-failed-check-msi-tip", "Installation method check failed. Please click the \"Download\" button to download from the release page and upgrade manually."), + ("websocket_tip", "When using WebSocket, only relay connections are supported."), + ("terminal-admin-login-tip", "Please input the administrator username and password of the controlled side."), + ("elevation_username_tip", "Input username or domain\\username"), + ("allow-insecure-tls-fallback-tip", "By default, RustDesk verifies the server certificate for protocols using TLS.\nWith this option enabled, RustDesk will fall back to skipping the verification step and proceed in case of verification failure."), + ("disable-udp-tip", "Controls whether to use TCP only.\nWhen this option enabled, RustDesk will not use UDP 21116 any more, TCP 21116 will be used instead."), + ("server-oss-not-support-tip", "NOTE: RustDesk server OSS doesn't include this feature."), + ("note-at-conn-end-tip", "Ask for note at end of connection"), + ("rel-mouse-not-supported-peer-tip", "Relative Mouse Mode is not supported by the connected peer."), + ("rel-mouse-not-ready-tip", "Relative Mouse Mode is not ready yet. Please try again."), + ("rel-mouse-lock-failed-tip", "Failed to lock cursor. Relative Mouse Mode has been disabled."), + ("rel-mouse-exit-{}-tip", "Press {} to exit."), + ("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative Mouse Mode has been disabled."), + ("keep-awake-during-outgoing-sessions-label", "Keep screen awake during outgoing sessions"), + ("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"), + ("password-hidden-tip", "Permanent password is set (hidden)."), + ("preset-password-in-use-tip", "Preset password is currently in use."), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/eo.rs b/vendor/rustdesk/src/lang/eo.rs new file mode 100644 index 0000000..16d43c9 --- /dev/null +++ b/vendor/rustdesk/src/lang/eo.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stato"), + ("Your Desktop", "Via aparato"), + ("desk_tip", "Via aparato povas esti alirita kun tiu identigilo kaj pasvorto"), + ("Password", "Pasvorto"), + ("Ready", "Preta"), + ("Established", "Establis"), + ("connecting_status", "Konektante al la reto RustDesk..."), + ("Enable service", "Ebligi servon"), + ("Start service", "Starti servon"), + ("Service is running", "La servo funkcias"), + ("Service is not running", "La servo ne funkcias"), + ("not_ready_status", "Ne preta, bonvolu kontroli la retkonekto"), + ("Control Remote Desktop", "Kontroli foran aparaton"), + ("Transfer file", "Transigi dosieron"), + ("Connect", "Konekti al"), + ("Recent sessions", "Lastaj sesioj"), + ("Address book", "Adresaro"), + ("Confirmation", "Konfirmacio"), + ("TCP tunneling", "Tunelado TCP"), + ("Remove", "Forigi"), + ("Refresh random password", "Regeneri hazardan pasvorton"), + ("Set your own password", "Agordi vian propran pasvorton"), + ("Enable keyboard/mouse", "Ebligi klavaro/muso"), + ("Enable clipboard", "Sinkronigi poŝon"), + ("Enable file transfer", "Ebligi dosiertransigado"), + ("Enable TCP tunneling", "Ebligi tunelado TCP"), + ("IP Whitelisting", "Listo de IP akceptataj"), + ("ID/Relay Server", "Identigila/Relajsa servilo"), + ("Import server config", "Importi servilan agordon"), + ("Export Server Config", "Eksporti servilan agordon"), + ("Import server configuration successfully", "Importi servilan agordon sukcese"), + ("Export server configuration successfully", "Eksporti servilan agordon sukcese"), + ("Invalid server configuration", "Nevalida servila agordo"), + ("Clipboard is empty", "La poŝo estas malplena"), + ("Stop service", "Haltu servon"), + ("Change ID", "Ŝanĝi identigilon"), + ("Your new ID", "Via nova identigilo"), + ("length %min% to %max%", "longeco %min% al %max%"), + ("starts with a letter", "komencas kun letero"), + ("allowed characters", "permesitaj signoj"), + ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, - (dash), _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."), + ("Website", "Retejo"), + ("About", "Pri"), + ("Slogan_tip", "Farita kun koro en ĉi tiu ĥaosa mondo!"), + ("Privacy Statement", "Deklaro Pri Privateco"), + ("Mute", "Muta"), + ("Build Date", "konstruada dato"), + ("Version", "Versio"), + ("Home", "Hejmo"), + ("Audio Input", "Aŭdia Enigo"), + ("Enhancements", "Plibonigoj"), + ("Hardware Codec", "Aparataro Kodeko"), + ("Adaptive bitrate", "Adapta bitrapido"), + ("ID Server", "Servilo de identigiloj"), + ("Relay Server", "Relajsa Servilo"), + ("API Server", "Servilo de API"), + ("invalid_http", "Devas komenci kun http:// aŭ https://"), + ("Invalid IP", "IP nevalida"), + ("Invalid format", "Formato nevalida"), + ("server_not_support", "Ankoraŭ ne subtenata de la servilo"), + ("Not available", "Nedisponebla"), + ("Too frequent", "Tro ofte ŝanĝita, bonvolu reprovi poste"), + ("Cancel", "Nuligi"), + ("Skip", "Ignori"), + ("Close", "Fermi"), + ("Retry", "Reprovi"), + ("OK", "Konfermi"), + ("Password Required", "Pasvorto deviga"), + ("Please enter your password", "Bonvolu tajpi vian pasvorton"), + ("Remember password", "Memori pasvorton"), + ("Wrong Password", "Erara pasvorto"), + ("Do you want to enter again?", "Ĉu vi aliri denove?"), + ("Connection Error", "Eraro de konektado"), + ("Error", "Eraro"), + ("Reset by the peer", "La konekto estas fermita de la samtavolano"), + ("Connecting...", "Konektante..."), + ("Connection in progress. Please wait.", "Konektado farata. Bonvolu atendi."), + ("Please try 1 minute later", "Reprovi post 1 minuto"), + ("Login Error", "Eraro de konektado"), + ("Successful", "Sukceso"), + ("Connected, waiting for image...", "Konektita, atendante bildon..."), + ("Name", "Nomo"), + ("Type", "Tipo"), + ("Modified", "Modifita"), + ("Size", "Grandeco"), + ("Show Hidden Files", "Montri kaŝitajn dosierojn"), + ("Receive", "Akcepti"), + ("Send", "Sendi"), + ("Refresh File", "Aktualigu Dosieron"), + ("Local", "Loka"), + ("Remote", "Fora"), + ("Remote Computer", "Fora komputilo"), + ("Local Computer", "Loka komputilo"), + ("Confirm Delete", "Konfermi la forigo"), + ("Delete", "Forigi"), + ("Properties", "Propraĵoj"), + ("Multi Select", "Pluropa Elekto"), + ("Select All", "Elektu Ĉiujn"), + ("Unselect All", "Malelektu Ĉiujn"), + ("Empty Directory", "Malplena Dosierujo"), + ("Not an empty directory", "Ne Malplena Dosierujo"), + ("Are you sure you want to delete this file?", "Ĉu vi certas, ke vi volas forigi ĉi tiun dosieron?"), + ("Are you sure you want to delete this empty directory?", "Ĉu vi certas, ke vi volas forigi ĉi tiun malplenan dosierujon?"), + ("Are you sure you want to delete the file of this directory?", "Ĉu vi certa. ke vi volas forigi la dosieron de ĉi tiu dosierujo"), + ("Do this for all conflicts", "Same por ĉiuj konfliktoj"), + ("This is irreversible!", "Ĉi tio estas neinversigebla!"), + ("Deleting", "Forigado"), + ("files", "dosiero"), + ("Waiting", "Atendante..."), + ("Finished", "Finita"), + ("Speed", "Rapideco"), + ("Custom Image Quality", "Agordi bildan kvaliton"), + ("Privacy mode", "Modo privata"), + ("Block user input", "Bloki uzanta enigo"), + ("Unblock user input", "Malbloki uzanta enigo"), + ("Adjust Window", "Adapti fenestro"), + ("Original", "Originala rilatumo"), + ("Shrink", "Ŝrumpi"), + ("Stretch", "Streĉi"), + ("Scrollbar", "Rulumbreto"), + ("ScrollAuto", "Rulumu Aŭtomate"), + ("Good image quality", "Bona bilda kvalito"), + ("Balanced", "Normala bilda kvalito"), + ("Optimize reaction time", "Optimigi reakcia tempo"), + ("Custom", ""), + ("Show remote cursor", "Montri foran kursoron"), + ("Show quality monitor", "Montri kvalito monitoron"), + ("Disable clipboard", "Malebligi poŝon"), + ("Lock after session end", "Ŝlosi foran komputilon post malkonektado"), + ("Insert Ctrl + Alt + Del", "Enmeti Ctrl + Alt + Del"), + ("Insert Lock", "Ŝlosi foran komputilon"), + ("Refresh", "Refreŝigi ekranon"), + ("ID does not exist", "La identigilo ne ekzistas"), + ("Failed to connect to rendezvous server", "Malsukcesis konekti al la servilo rendezvous"), + ("Please try later", "Bonvolu provi poste"), + ("Remote desktop is offline", "La fora aparato estas senkonektita"), + ("Key mismatch", "Miskongruo de klavoj"), + ("Timeout", "Konekta posttempo"), + ("Failed to connect to relay server", "Malsukcesis konekti al la relajsa servilo"), + ("Failed to connect via rendezvous server", "Malsukcesis konekti per servilo rendezvous"), + ("Failed to connect via relay server", "Malsukcesis konekti per relajsa servilo"), + ("Failed to make direct connection to remote desktop", "Malsukcesis konekti direkte"), + ("Set Password", "Agordi pasvorton"), + ("OS Password", "Pasvorto de la operaciumo"), + ("install_tip", "Vi ne uzas instalita versio. Pro limigoj pro UAC, kiel aparato kontrolata, en kelkaj kazoj, ne estos ebla kontroli la muson kaj klavaron aŭ registri la ekranon. Bonvolu alkliku la butonon malsupre por instali RustDesk sur la operaciumo por eviti la demando supre."), + ("Click to upgrade", "Alklaki por plibonigi"), + ("Configure", "Konfiguri"), + ("config_acc", "Por uzi vian foran aparaton, bonvolu doni la permeson \"alirebleco\" al RustDesk."), + ("config_screen", "Por uzi vian foran aparaton, bonvolu doni la permeson \"ekranregistrado\" al RustDesk."), + ("Installing ...", "Instalante..."), + ("Install", "Instali"), + ("Installation", "Instalado"), + ("Installation Path", "Vojo de instalo"), + ("Create start menu shortcuts", "Aldoni ligilojn sur la startmenuo"), + ("Create desktop icon", "Aldoni ligilojn sur la labortablo"), + ("agreement_tip", "Starti la instaladon signifas akcepti la permesilon."), + ("Accept and Install", "Akcepti kaj instali"), + ("End-user license agreement", "Uzanta permesilon"), + ("Generating ...", "Generante..."), + ("Your installation is lower version.", "Via versio de instalaĵo estas pli malalta ol la lasta."), + ("not_close_tcp_tip", "Bonvolu ne fermu tiun fenestron dum la uzo de la tunelo"), + ("Listening ...", "Atendante konekton al la tunelo..."), + ("Remote Host", "Fora gastiganto"), + ("Remote Port", "Fora pordo"), + ("Action", "Ago"), + ("Add", "Aldoni"), + ("Local Port", "Loka pordo"), + ("Local Address", "Loka Adreso"), + ("Change Local Port", "Ŝanĝi Loka Pordo"), + ("setup_server_tip", "Se vi bezonas pli rapida konekcio, vi povas krei vian propran servilon"), + ("Too short, at least 6 characters.", "Tro mallonga, almenaŭ 6 signoj."), + ("The confirmation is not identical.", "Ambaŭ enigoj ne kongruas"), + ("Permissions", "Permesoj"), + ("Accept", "Akcepti"), + ("Dismiss", "Malakcepti"), + ("Disconnect", "Malkonekti"), + ("Enable file copy and paste", "Permesu kopii kaj alglui dosierojn"), + ("Connected", "Konektata"), + ("Direct and encrypted connection", "Konekcio direkta ĉifrata"), + ("Relayed and encrypted connection", "Konekcio relajsa ĉifrata"), + ("Direct and unencrypted connection", "Konekcio direkta neĉifrata"), + ("Relayed and unencrypted connection", "Konekcio relajsa neĉifrata"), + ("Enter Remote ID", "Tajpu foran identigilon"), + ("Enter your password", "Tajpu vian pasvorton"), + ("Logging in...", "Konektante..."), + ("Enable RDP session sharing", "Ebligi la kundivido de sesio RDP"), + ("Auto Login", "Aŭtomata konektado (la ŝloso nur estos ebligita post la malebligado de la unua parametro)"), + ("Enable direct IP access", "Permesi direkta eniro per IP"), + ("Rename", "Renomi"), + ("Space", "Spaco"), + ("Create desktop shortcut", "Krei ligilon sur la labortablon"), + ("Change Path", "Ŝanĝi vojon"), + ("Create Folder", "Krei dosierujon"), + ("Please enter the folder name", "Bonvolu enigi la dosiernomon"), + ("Fix it", "Riparu ĝin"), + ("Warning", "Averto"), + ("Login screen using Wayland is not supported", "Konektajn ekranojn uzantajn Wayland ne estas subtenitaj"), + ("Reboot required", "Restarto deviga"), + ("Unsupported display server", "La aktuala bilda servilo ne estas subtenita"), + ("x11 expected", "Bonvolu uzi x11"), + ("Port", "Pordo"), + ("Settings", "Agordoj"), + ("Username", " Uzanta nomo"), + ("Invalid port", "Pordo nevalida"), + ("Closed manually by the peer", "Manuale fermita de la samtavolano"), + ("Enable remote configuration modification", "Permesi foran redaktadon de la konfiguracio"), + ("Run without install", "Plenumi sen instali"), + ("Connect via relay", "Konekti per relajso"), + ("Always connect via relay", "Ĉiam konekti per relajso"), + ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), + ("Login", "Ensaluti"), + ("Verify", "Kontrolis"), + ("Remember me", "Memori min"), + ("Trust this device", "Fidu ĉi tiun aparaton"), + ("Verification code", "Konfirmkodo"), + ("verification_tip", "Konfirmkodo estis sendita al la registrita retpoŝta adreso, enigu la konfirmkodon por daŭrigi ensaluti."), + ("Logout", "Elsaluti"), + ("Tags", "Etikedi"), + ("Search ID", "Serĉi ID"), + ("whitelist_sep", "Vi povas uzi komon, punktokomon, spacon aŭ linsalton kiel apartigilo"), + ("Add ID", "Aldoni identigilo"), + ("Add Tag", "Aldoni etikedo"), + ("Unselect all tags", "Malselekti ĉiujn etikedojn"), + ("Network error", "Reta eraro"), + ("Username missed", "Uzantnomo forgesita"), + ("Password missed", "Pasvorto forgesita"), + ("Wrong credentials", "Identigilo aŭ pasvorto erara"), + ("The verification code is incorrect or has expired", ""), + ("Edit Tag", "Redakti etikedo"), + ("Forget Password", "Forgesi pasvorton"), + ("Favorites", "Favorataj"), + ("Add to Favorites", "Aldoni al la favorataj"), + ("Remove from Favorites", "Forigi el la favorataj"), + ("Empty", "Malplena"), + ("Invalid folder name", "Dosiernomo nevalida"), + ("Socks5 Proxy", "Socks5 prokura servilo"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) prokura servilo"), + ("Discovered", "Malkovritaj"), + ("install_daemon_tip", "Por komenci ĉe ekŝargo, oni devas instali sisteman servon."), + ("Remote ID", "Fora identigilo"), + ("Paste", "Alglui"), + ("Paste here?", "Alglui ĉi tie?"), + ("Are you sure to close the connection?", "Ĉu vi vere volas fermi la konekton?"), + ("Download new version", "Elŝuti la novan version"), + ("Touch mode", "Tuŝa modo"), + ("Mouse mode", "musa modo"), + ("One-Finger Tap", "Unufingra Frapeto"), + ("Left Mouse", "Maldekstra Muso"), + ("One-Long Tap", "Unulonga Frapeto"), + ("Two-Finger Tap", "Dufingra Frapeto"), + ("Right Mouse", "Deskra Muso"), + ("One-Finger Move", "Unufingra Movo"), + ("Double Tap & Move", "Duobla Frapeto & Movo"), + ("Mouse Drag", "Muso Trenadi"), + ("Three-Finger vertically", "Tri Figroj Vertikale"), + ("Mouse Wheel", "Musa Rado"), + ("Two-Finger Move", "Dufingra Movo"), + ("Canvas Move", "Kanvasa Movo"), + ("Pinch to Zoom", "Pinĉi al Zomo"), + ("Canvas Zoom", "Kanvasa Zomo"), + ("Reset canvas", "Restarigi kanvaso"), + ("No permission of file transfer", "Neniu permeso de dosiertransigo"), + ("Note", "Notu"), + ("Connection", "Konekto"), + ("Share screen", "Kunhavigi Ekranon"), + ("Chat", "Babilo"), + ("Total", "Sumo"), + ("items", "eroj"), + ("Selected", "Elektita"), + ("Screen Capture", "Ekrankapto"), + ("Input Control", "Eniga Kontrolo"), + ("Audio Capture", "Sonkontrolo"), + ("Do you accept?", "Ĉu vi akceptas?"), + ("Open System Setting", "Malfermi Sistemajn Agordojn"), + ("How to get Android input permission?", "Kiel akiri Android enigajn permesojn"), + ("android_input_permission_tip1", "Por ke fora aparato regu vian Android-aparaton per muso aŭ tuŝo, vi devas permesi al RustDesk uzi la servon \"Alirebleco\"."), + ("android_input_permission_tip2", "Bonvolu iri al la sekva paĝo de sistemaj agordoj, trovi kaj eniri [Instatajn Servojn], ŝalti la servon [RustDesk Enigo]."), + ("android_new_connection_tip", "Nova kontrolpeto estis ricevita, kiu volas kontroli vian nunan aparaton."), + ("android_service_will_start_tip", "Ŝalti \"Ekrankapto\" aŭtomate startos la servon, permesante al aliaj aparatoj peti konekton al via aparato."), + ("android_stop_service_tip", "Fermante la servon aŭtomate fermos ĉiujn establitajn konektojn."), + ("android_version_audio_tip", "La nuna versio da Android ne subtenas sonkapton, bonvolu ĝisdatigi al Android 10 aŭ pli alta."), + ("android_start_service_tip", "Frapu [Komenci servo] aŭ ebligu la permeson de [Ekrankapto] por komenci la servon de kundivido de ekrano."), + ("android_permission_may_not_change_tip", "Permesoj por establitaj konektoj neble estas ŝanĝitaj tuj ĝis rekonektitaj."), + ("Account", "Konto"), + ("Overwrite", "anstataŭigi"), + ("This file exists, skip or overwrite this file?", "Ĉi tiu dosiero ekzistas, ĉu preterlasi aŭ anstataŭi ĉi tiun dosieron?"), + ("Quit", "Forlasi"), + ("Help", "Helpi"), + ("Failed", "Malsukcesa"), + ("Succeeded", "Sukcesa"), + ("Someone turns on privacy mode, exit", "Iu ŝaltas modon privata, Eliro"), + ("Unsupported", "Nesubtenata"), + ("Peer denied", "Samulo rifuzita"), + ("Please install plugins", "Bonvolu instali kromprogramojn"), + ("Peer exit", "Samulo eliras"), + ("Failed to turn off", "Malsukcesis malŝalti"), + ("Turned off", "Malŝaltita"), + ("Language", "Lingvo"), + ("Keep RustDesk background service", "Tenu RustDesk fonan servon"), + ("Ignore Battery Optimizations", "Ignoru Bateria Optimumigojn"), + ("android_open_battery_optimizations_tip", "Se vi volas malŝalti ĉi tiun funkcion, bonvolu iri al la sekva paĝo de agordoj de la aplikaĵo de RustDesk, trovi kaj eniri [Baterio], Malmarku [Senrestrikta]"), + ("Start on boot", "Komencu ĉe ekfunkciigo"), + ("Start the screen sharing service on boot, requires special permissions", "Komencu la servon de kundivido de ekrano ĉe lanĉo, postulas specialajn permesojn"), + ("Connection not allowed", "Konekto ne rajtas"), + ("Legacy mode", ""), + ("Map mode", "Mapa modo"), + ("Translate mode", "Traduki modo"), + ("Use permanent password", "Uzu permanenta pasvorto"), + ("Use both passwords", "Uzu ambaŭ pasvorto"), + ("Set permanent password", "Starigi permanenta pasvorto"), + ("Enable remote restart", "Permesi fora restartas"), + ("Restart remote device", "Restartu fora aparato"), + ("Are you sure you want to restart", "Ĉu vi certas, ke vi volas restarti"), + ("Restarting remote device", "Restartas fora aparato"), + ("remote_restarting_tip", "Fora aparato restartiĝas, bonvolu fermi ĉi tiun mesaĝkeston kaj rekonekti kun permanenta pasvorto post iom da tempo"), + ("Copied", "Kopiita"), + ("Exit Fullscreen", "Eliru Plenekranon"), + ("Fullscreen", "Plenekrane"), + ("Mobile Actions", "Poŝtelefonaj Agoj"), + ("Select Monitor", "Elektu Monitoron"), + ("Control Actions", "Kontrolaj Agoj"), + ("Display Settings", "Montraj Agordoj"), + ("Ratio", "Proporcio"), + ("Image Quality", "Bilda Kvalito"), + ("Scroll Style", "Ruluma Stilo"), + ("Show Toolbar", "Montri Ilobreton"), + ("Hide Toolbar", "Kaŝi Ilobreton"), + ("Direct Connection", "Rekta Konekto"), + ("Relay Connection", "Relajsa Konekto"), + ("Secure Connection", "Sekura Konekto"), + ("Insecure Connection", "Nesekura Konekto"), + ("Scale original", "Skalo originalo"), + ("Scale adaptive", "Skalo adapta"), + ("General", ""), + ("Security", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Light Theme", ""), + ("Dark", ""), + ("Light", ""), + ("Follow System", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable audio", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Pin Toolbar", ""), + ("Unpin Toolbar", ""), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), + ("Enable recording session", ""), + ("Enable LAN discovery", ""), + ("Deny LAN discovery", ""), + ("Write a message", ""), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), + ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), + ("Full Access", ""), + ("Screen Share", ""), + ("ubuntu-21-04-required", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."), + ("wayland-requires-higher-linux-version", "Wayland postulas pli altan version de linuksa distro. Bonvolu provi X11-labortablon aŭ ŝanĝi vian OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Bonvolu Elekti la ekranon por esti dividita (Funkciu ĉe la sama flanko)."), + ("Show RustDesk", ""), + ("This PC", ""), + ("or", ""), + ("Elevate", ""), + ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), + ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), + ("Skipped", ""), + ("Add to address book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), + ("Select a peer", ""), + ("Select peers", ""), + ("Plugins", ""), + ("Uninstall", ""), + ("Update", ""), + ("Enable", ""), + ("Disable", ""), + ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), + ("Collapse toolbar", ""), + ("Accept and Elevate", ""), + ("accept_and_elevate_btn_tooltip", ""), + ("clipboard_wait_response_timeout_tip", ""), + ("Incoming connection", ""), + ("Outgoing connection", ""), + ("Exit", ""), + ("Open", ""), + ("logout_tip", ""), + ("Service", ""), + ("Start", ""), + ("Stop", ""), + ("exceed_max_devices", ""), + ("Sync with recent sessions", ""), + ("Sort tags", ""), + ("Open connection in new tab", ""), + ("Move tab to new window", ""), + ("Can not be empty", ""), + ("Already exists", ""), + ("Change Password", ""), + ("Refresh Password", ""), + ("ID", ""), + ("Grid View", ""), + ("List View", ""), + ("Select", ""), + ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), + ("synced_peer_readded_tip", ""), + ("Change Color", ""), + ("Primary Color", ""), + ("HSV Color", ""), + ("Installation Successful!", ""), + ("Installation failed!", ""), + ("Reverse mouse wheel", ""), + ("{} sessions", ""), + ("scam_title", ""), + ("scam_text1", ""), + ("scam_text2", ""), + ("Don't show again", ""), + ("I Agree", ""), + ("Decline", ""), + ("Timeout in minutes", ""), + ("auto_disconnect_option_tip", ""), + ("Connection failed due to inactivity", ""), + ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), + ("pull_group_failed_tip", ""), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), + ("idd_not_support_under_win10_2004_tip", ""), + ("input_source_1_tip", ""), + ("input_source_2_tip", ""), + ("Swap control-command key", ""), + ("swap-left-right-mouse", ""), + ("2FA code", ""), + ("More", ""), + ("enable-2fa-title", ""), + ("enable-2fa-desc", ""), + ("wrong-2fa-code", ""), + ("enter-2fa-title", ""), + ("Email verification code must be 6 characters.", ""), + ("2FA code must be 6 digits.", ""), + ("Multiple Windows sessions found", ""), + ("Please select the session you want to connect to", ""), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("share_warning_tip", ""), + ("Everyone", ""), + ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", ""), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", ""), + ("Floating window", ""), + ("floating_window_tip", ""), + ("Keep screen on", ""), + ("Never", ""), + ("During controlled", ""), + ("During service is on", ""), + ("Capture screen using DirectX", ""), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Rigardi kameron"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", ""), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/es.rs b/vendor/rustdesk/src/lang/es.rs new file mode 100644 index 0000000..2e543c2 --- /dev/null +++ b/vendor/rustdesk/src/lang/es.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Estado"), + ("Your Desktop", "Tu escritorio"), + ("desk_tip", "Puedes acceder a tu escritorio con esta ID y contraseña."), + ("Password", "Contraseña"), + ("Ready", "Listo"), + ("Established", "Establecido"), + ("connecting_status", "Conexión a la red RustDesk en progreso..."), + ("Enable service", "Habilitar Servicio"), + ("Start service", "Iniciar Servicio"), + ("Service is running", "El servicio se está ejecutando"), + ("Service is not running", "El servicio no se está ejecutando"), + ("not_ready_status", "No está listo. Comprueba tu conexión"), + ("Control Remote Desktop", "Controlar escritorio remoto"), + ("Transfer file", "Transferir archivo"), + ("Connect", "Conectar"), + ("Recent sessions", "Sesiones recientes"), + ("Address book", "Directorio"), + ("Confirmation", "Confirmación"), + ("TCP tunneling", "Túnel TCP"), + ("Remove", "Quitar"), + ("Refresh random password", "Actualizar contraseña aleatoria"), + ("Set your own password", "Establece tu propia contraseña"), + ("Enable keyboard/mouse", "Habilitar teclado/ratón"), + ("Enable clipboard", "Habilitar portapapeles"), + ("Enable file transfer", "Habilitar transferencia de archivos"), + ("Enable TCP tunneling", "Habilitar túnel TCP"), + ("IP Whitelisting", "Direcciones IP admitidas"), + ("ID/Relay Server", "Servidor ID/Relay"), + ("Import server config", "Importar configuración de servidor"), + ("Export Server Config", "Exportar configuración del servidor"), + ("Import server configuration successfully", "Configuración de servidor importada con éxito"), + ("Export server configuration successfully", "Configuración de servidor exportada con éxito"), + ("Invalid server configuration", "Configuración de servidor incorrecta"), + ("Clipboard is empty", "El portapapeles está vacío"), + ("Stop service", "Detener servicio"), + ("Change ID", "Cambiar ID"), + ("Your new ID", "Tu nueva ID"), + ("length %min% to %max%", "de %min% a %max% de longitud"), + ("starts with a letter", "comenzar con una letra"), + ("allowed characters", "Caracteres permitidos"), + ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9, - (dash) e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."), + ("Website", "Sitio web"), + ("About", "Acerca de"), + ("Slogan_tip", "¡Hecho con corazón en este mundo caótico!"), + ("Privacy Statement", "Declaración de privacidad"), + ("Mute", "Silenciar"), + ("Build Date", "Fecha de compilación"), + ("Version", "Versión"), + ("Home", "Inicio"), + ("Audio Input", "Entrada de audio"), + ("Enhancements", "Mejoras"), + ("Hardware Codec", "Códec de hardware"), + ("Adaptive bitrate", "Tasa de bits adaptativa"), + ("ID Server", "Servidor de IDs"), + ("Relay Server", "Servidor Relay"), + ("API Server", "Servidor API"), + ("invalid_http", "debe comenzar con http:// o https://"), + ("Invalid IP", "IP incorrecta"), + ("Invalid format", "Formato incorrecto"), + ("server_not_support", "Aún no es compatible con el servidor"), + ("Not available", "No disponible"), + ("Too frequent", "Demasiado frecuente"), + ("Cancel", "Cancelar"), + ("Skip", "Omitir"), + ("Close", "Cerrar"), + ("Retry", "Reintentar"), + ("OK", ""), + ("Password Required", "Se requiere contraseña"), + ("Please enter your password", "Por favor, introduzca su contraseña"), + ("Remember password", "Recordar contraseña"), + ("Wrong Password", "Contraseña incorrecta"), + ("Do you want to enter again?", "¿Quieres volver a entrar?"), + ("Connection Error", "Error de conexión"), + ("Error", ""), + ("Reset by the peer", "Restablecido por el par"), + ("Connecting...", "Conectando..."), + ("Connection in progress. Please wait.", "Conexión en curso. Espere por favor."), + ("Please try 1 minute later", "Intente 1 minuto más tarde"), + ("Login Error", "Error de inicio de sesión"), + ("Successful", "Exitoso"), + ("Connected, waiting for image...", "Conectado, esperando imagen..."), + ("Name", "Nombre"), + ("Type", "Tipo"), + ("Modified", "Modificado"), + ("Size", "Tamaño"), + ("Show Hidden Files", "Mostrar archivos ocultos"), + ("Receive", "Recibir"), + ("Send", "Enviar"), + ("Refresh File", "Actualizar archivo"), + ("Local", ""), + ("Remote", "Remoto"), + ("Remote Computer", "Computadora remota"), + ("Local Computer", "Computadora local"), + ("Confirm Delete", "Confirmar eliminación"), + ("Delete", "Eliminar"), + ("Properties", "Propiedades"), + ("Multi Select", "Selección múltiple"), + ("Select All", "Seleccionar Todo"), + ("Unselect All", "Deseleccionar Todo"), + ("Empty Directory", "Directorio vacío"), + ("Not an empty directory", "No es un directorio vacío"), + ("Are you sure you want to delete this file?", "¿Estás seguro de que quieres eliminar este archivo?"), + ("Are you sure you want to delete this empty directory?", "¿Estás seguro de que deseas eliminar este directorio vacío?"), + ("Are you sure you want to delete the file of this directory?", "¿Estás seguro de que deseas eliminar el archivo de este directorio?"), + ("Do this for all conflicts", "Haga esto para todos los conflictos"), + ("This is irreversible!", "¡Esto es irreversible!"), + ("Deleting", "Eliminando"), + ("files", "archivos"), + ("Waiting", "Esperando"), + ("Finished", "Terminado"), + ("Speed", "Velocidad"), + ("Custom Image Quality", "Calidad de imagen personalizada"), + ("Privacy mode", "Modo privado"), + ("Block user input", "Bloquear entrada de usuario"), + ("Unblock user input", "Desbloquear entrada de usuario"), + ("Adjust Window", "Ajustar ventana"), + ("Original", "Original"), + ("Shrink", "Encoger"), + ("Stretch", "Estirar"), + ("Scrollbar", "Barra de desplazamiento"), + ("ScrollAuto", "Desplazamiento automático"), + ("Good image quality", "Buena calidad de imagen"), + ("Balanced", "Equilibrado"), + ("Optimize reaction time", "Optimizar el tiempo de reacción"), + ("Custom", "Personalizado"), + ("Show remote cursor", "Mostrar cursor remoto"), + ("Show quality monitor", "Mostrar calidad del monitor"), + ("Disable clipboard", "Deshabilitar portapapeles"), + ("Lock after session end", "Bloquear después del final de la sesión"), + ("Insert Ctrl + Alt + Del", "Insertar Ctrl + Alt + Del"), + ("Insert Lock", "Insertar bloqueo"), + ("Refresh", "Actualizar"), + ("ID does not exist", "La ID no existe"), + ("Failed to connect to rendezvous server", "No se pudo conectar al servidor de encuentro"), + ("Please try later", "Por favor intente mas tarde"), + ("Remote desktop is offline", "El escritorio remoto está desconectado"), + ("Key mismatch", "La clave no coincide"), + ("Timeout", "Tiempo agotado"), + ("Failed to connect to relay server", "No se pudo conectar al servidor de retransmisión"), + ("Failed to connect via rendezvous server", "No se pudo conectar a través del servidor de encuentro"), + ("Failed to connect via relay server", "No se pudo conectar a través del servidor de retransmisión"), + ("Failed to make direct connection to remote desktop", "No se pudo establecer la conexión directa con el escritorio remoto"), + ("Set Password", "Configurar la contraseña"), + ("OS Password", "Contraseña del sistema operativo"), + ("install_tip", "Debido al Control de cuentas de usuario, es posible que RustDesk no funcione correctamente como escritorio remoto. Para evitar este problema, haga clic en el botón de abajo para instalar RustDesk a nivel de sistema."), + ("Click to upgrade", "Clic para actualizar"), + ("Configure", "Configurar"), + ("config_acc", "Para controlar su escritorio desde el exterior, debe otorgar permiso a RustDesk de \"Accesibilidad\"."), + ("config_screen", "Para controlar su escritorio desde el exterior, debe otorgar permiso a RustDesk de \"Grabación de pantalla\"."), + ("Installing ...", "Instalando ..."), + ("Install", "Instalar"), + ("Installation", "Instalación"), + ("Installation Path", "Ruta de instalación"), + ("Create start menu shortcuts", "Crear accesos directos en el menú de inicio"), + ("Create desktop icon", "Crear icono de escritorio"), + ("agreement_tip", "Al iniciar la instalación, acepta los términos del acuerdo de licencia."), + ("Accept and Install", "Aceptar e instalar"), + ("End-user license agreement", "Acuerdo de licencia de usuario final"), + ("Generating ...", "Generando ..."), + ("Your installation is lower version.", "Su instalación es una versión inferior."), + ("not_close_tcp_tip", "No cierre esta ventana mientras esté usando el túnel"), + ("Listening ...", "Escuchando ..."), + ("Remote Host", "Anfitrión remoto"), + ("Remote Port", "Puerto remoto"), + ("Action", "Acción"), + ("Add", "Agregar"), + ("Local Port", "Puerto local"), + ("Local Address", "Dirección Local"), + ("Change Local Port", "Cambiar Puerto Local"), + ("setup_server_tip", "Para una conexión más rápida, configure su propio servidor"), + ("Too short, at least 6 characters.", "Demasiado corto, al menos 6 caracteres."), + ("The confirmation is not identical.", "La confirmación no coincide."), + ("Permissions", "Permisos"), + ("Accept", "Aceptar"), + ("Dismiss", "Cancelar"), + ("Disconnect", "Desconectar"), + ("Enable file copy and paste", "Permitir copiar y pegar archivos"), + ("Connected", "Conectado"), + ("Direct and encrypted connection", "Conexión directa y cifrada"), + ("Relayed and encrypted connection", "Conexión retransmitida y cifrada"), + ("Direct and unencrypted connection", "Conexión directa y sin cifrar"), + ("Relayed and unencrypted connection", "Conexión retransmitida y sin cifrar"), + ("Enter Remote ID", "Introduzca el ID remoto"), + ("Enter your password", "Introduzca su contraseña"), + ("Logging in...", "Iniciando sesión..."), + ("Enable RDP session sharing", "Habilitar el uso compartido de sesiones RDP"), + ("Auto Login", "Inicio de sesión automático"), + ("Enable direct IP access", "Habilitar acceso IP directo"), + ("Rename", "Renombrar"), + ("Space", "Espacio"), + ("Create desktop shortcut", "Crear acceso directo en el escritorio"), + ("Change Path", "Cambiar ruta"), + ("Create Folder", "Crear carpeta"), + ("Please enter the folder name", "Por favor introduzca el nombre de la carpeta"), + ("Fix it", "Resolver"), + ("Warning", "Aviso"), + ("Login screen using Wayland is not supported", "La pantalla de inicio de sesión con Wayland no es compatible"), + ("Reboot required", "Reinicio requerido"), + ("Unsupported display server", "Servidor de visualización no compatible"), + ("x11 expected", "x11 necesario"), + ("Port", "Puerto"), + ("Settings", "Ajustes"), + ("Username", "Nombre de usuario"), + ("Invalid port", "Puerto incorrecto"), + ("Closed manually by the peer", "Cerrado manualmente por el par"), + ("Enable remote configuration modification", "Habilitar modificación remota de configuración"), + ("Run without install", "Ejecutar sin instalar"), + ("Connect via relay", ""), + ("Always connect via relay", "Conéctese siempre a través de relay"), + ("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"), + ("Login", "Iniciar sesión"), + ("Verify", "Verificar"), + ("Remember me", "Recordarme"), + ("Trust this device", "Confiar en este dispositivo"), + ("Verification code", "Código de verificación"), + ("verification_tip", "Se ha detectado un nuevo dispositivo y se ha enviado un código de verificación a la dirección de correo registrada. Introduzca el código de verificación para continuar con el inicio de sesión."), + ("Logout", "Salir"), + ("Tags", "Tags"), + ("Search ID", "Buscar ID"), + ("whitelist_sep", "Separados por coma, punto y coma, espacio o nueva línea"), + ("Add ID", "Agregar ID"), + ("Add Tag", "Agregar tag"), + ("Unselect all tags", "Deseleccionar todos los tags"), + ("Network error", "Error de red"), + ("Username missed", "Olvidó su nombre de usuario"), + ("Password missed", "Olvidó su contraseña"), + ("Wrong credentials", "Credenciales incorrectas"), + ("The verification code is incorrect or has expired", ""), + ("Edit Tag", "Editar tag"), + ("Forget Password", "Olvidar contraseña"), + ("Favorites", "Favoritos"), + ("Add to Favorites", "Agregar a favoritos"), + ("Remove from Favorites", "Quitar de favoritos"), + ("Empty", "Vacío"), + ("Invalid folder name", "Nombre de carpeta incorrecto"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Discovered", "Descubierto"), + ("install_daemon_tip", "Para comenzar en el encendido, debe instalar el servicio del sistema."), + ("Remote ID", "ID remoto"), + ("Paste", "Pegar"), + ("Paste here?", "¿Pegar aquí?"), + ("Are you sure to close the connection?", "¿Estás seguro de cerrar la conexión?"), + ("Download new version", "Descargar nueva versión"), + ("Touch mode", "Modo táctil"), + ("Mouse mode", "Modo ratón"), + ("One-Finger Tap", "Toque con un dedo"), + ("Left Mouse", "Botón izquierdo"), + ("One-Long Tap", "Un toque largo"), + ("Two-Finger Tap", "Toque con dos dedos"), + ("Right Mouse", "Botón derecho"), + ("One-Finger Move", "Movimiento con un dedo"), + ("Double Tap & Move", "Toca dos veces y mueve"), + ("Mouse Drag", "Arrastre de ratón"), + ("Three-Finger vertically", "Tres dedos verticalmente"), + ("Mouse Wheel", "Rueda de ratón"), + ("Two-Finger Move", "Movimiento con dos dedos"), + ("Canvas Move", "Movimiento de lienzo"), + ("Pinch to Zoom", "Pellizcar para ampliar"), + ("Canvas Zoom", "Ampliar lienzo"), + ("Reset canvas", "Restablecer lienzo"), + ("No permission of file transfer", "Sin permiso de transferencia de archivos"), + ("Note", "Nota"), + ("Connection", "Conexión"), + ("Share screen", "Compartir pantalla"), + ("Chat", "Chat"), + ("Total", "Total"), + ("items", "items"), + ("Selected", "Seleccionado"), + ("Screen Capture", "Captura de pantalla"), + ("Input Control", "Control de entrada"), + ("Audio Capture", "Captura de audio"), + ("Do you accept?", "¿Aceptas?"), + ("Open System Setting", "Configuración del sistema abierto"), + ("How to get Android input permission?", "¿Cómo obtener el permiso de entrada de Android?"), + ("android_input_permission_tip1", "Para que un dispositivo remoto controle su dispositivo Android a través del ratón o toque, debe permitir que RustDesk use el servicio de \"Accesibilidad\"."), + ("android_input_permission_tip2", "Vaya a la página de configuración del sistema que se abrirá a continuación, busque y acceda a [Servicios instalados], active el servicio [RustDesk Input]."), + ("android_new_connection_tip", "Se recibió una nueva solicitud de control para el dispositivo actual."), + ("android_service_will_start_tip", "Habilitar la captura de pantalla iniciará automáticamente el servicio, lo que permitirá que otros dispositivos soliciten una conexión desde este dispositivo."), + ("android_stop_service_tip", "Cerrar el servicio cerrará automáticamente todas las conexiones establecidas."), + ("android_version_audio_tip", "La versión actual de Android no admite la captura de audio, actualice a Android 10 o posterior."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), + ("Account", "Cuenta"), + ("Overwrite", "Sobrescribir"), + ("This file exists, skip or overwrite this file?", "Este archivo existe, ¿omitir o sobrescribir este archivo?"), + ("Quit", "Salir"), + ("Help", "Ayuda"), + ("Failed", "Fallido"), + ("Succeeded", "Logrado"), + ("Someone turns on privacy mode, exit", "Alguien active el modo privacidad, salga"), + ("Unsupported", "No soportado"), + ("Peer denied", "Par denegado"), + ("Please install plugins", "Instale complementos"), + ("Peer exit", "Par salio"), + ("Failed to turn off", "Error al apagar"), + ("Turned off", "Apagado"), + ("Language", "Idioma"), + ("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"), + ("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"), + ("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"), + ("Start on boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), + ("Connection not allowed", "Conexión no disponible"), + ("Legacy mode", "Modo heredado"), + ("Map mode", "Modo mapa"), + ("Translate mode", "Modo traducido"), + ("Use permanent password", "Usar contraseña permamente"), + ("Use both passwords", "Usar ambas contraseñas"), + ("Set permanent password", "Establecer contraseña permamente"), + ("Enable remote restart", "Habilitar reinicio remoto"), + ("Restart remote device", "Reiniciar dispositivo"), + ("Are you sure you want to restart", "¿Estás seguro de que deseas reiniciar?"), + ("Restarting remote device", "Reiniciando dispositivo remoto"), + ("remote_restarting_tip", "El dispositivo remoto se está reiniciando. Por favor cierre este mensaje y vuelva a conectarse con la contraseña peremanente en unos momentos."), + ("Copied", "Copiado"), + ("Exit Fullscreen", "Salir de pantalla completa"), + ("Fullscreen", "Pantalla completa"), + ("Mobile Actions", "Acciones móviles"), + ("Select Monitor", "Seleccionar monitor"), + ("Control Actions", "Acciones de control"), + ("Display Settings", "Configuración de pantalla"), + ("Ratio", "Relación"), + ("Image Quality", "Calidad de imagen"), + ("Scroll Style", "Estilo de desplazamiento"), + ("Show Toolbar", ""), + ("Hide Toolbar", ""), + ("Direct Connection", "Conexión directa"), + ("Relay Connection", "Conexión Relay"), + ("Secure Connection", "Conexión segura"), + ("Insecure Connection", "Conexión insegura"), + ("Scale original", "Escala original"), + ("Scale adaptive", "Escala adaptativa"), + ("General", ""), + ("Security", "Seguridad"), + ("Theme", "Tema"), + ("Dark Theme", "Tema Oscuro"), + ("Light Theme", ""), + ("Dark", "Oscuro"), + ("Light", "Claro"), + ("Follow System", "Tema del sistema"), + ("Enable hardware codec", "Habilitar códec por hardware"), + ("Unlock Security Settings", "Desbloquear ajustes de seguridad"), + ("Enable audio", "Habilitar Audio"), + ("Unlock Network Settings", "Desbloquear Ajustes de Red"), + ("Server", "Servidor"), + ("Direct IP Access", "Acceso IP Directo"), + ("Proxy", ""), + ("Apply", "Aplicar"), + ("Disconnect all devices?", "¿Desconectar todos los dispositivos?"), + ("Clear", "Borrar"), + ("Audio Input Device", "Dispositivo de entrada de audio"), + ("Use IP Whitelisting", "Usar lista de IPs admitidas"), + ("Network", "Red"), + ("Pin Toolbar", ""), + ("Unpin Toolbar", ""), + ("Recording", "Grabando"), + ("Directory", "Directorio"), + ("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"), + ("Automatically record outgoing sessions", ""), + ("Change", "Cambiar"), + ("Start session recording", "Comenzar grabación de sesión"), + ("Stop session recording", "Detener grabación de sesión"), + ("Enable recording session", "Habilitar grabación de sesión"), + ("Enable LAN discovery", "Habilitar descubrimiento de LAN"), + ("Deny LAN discovery", "Denegar descubrimiento de LAN"), + ("Write a message", "Escribir un mensaje"), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", "Por favor, espera confirmación de UAC"), + ("elevated_foreground_window_tip", "La ventana actual del escritorio remoto necesita privilegios elevados para funcionar, así que no puedes usar ratón y teclado temporalmente. Puedes solicitar al usuario remoto que minimize la ventana actual o hacer clic en el botón de elevación de la ventana de gestión de conexión. Para evitar este problema, se recomienda instalar el programa en el dispositivo remto."), + ("Disconnected", "Desconectado"), + ("Other", "Otro"), + ("Confirm before closing multiple tabs", "Confirmar antes de cerrar múltiples pestañas"), + ("Keyboard Settings", "Ajustes de teclado"), + ("Full Access", "Acceso completo"), + ("Screen Share", "Compartir pantalla"), + ("ubuntu-21-04-required", "Wayland requiere Ubuntu 21.04 o una versión superior."), + ("wayland-requires-higher-linux-version", "Wayland requiere una versión superior de la distribución de Linux. Pruebe el escritorio X11 o cambie su sistema operativo."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Ver"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seleccione la pantalla que se compartirá (Operar en el lado del par)."), + ("Show RustDesk", "Mostrar RustDesk"), + ("This PC", "Este PC"), + ("or", "o"), + ("Elevate", "Elevar privilegios"), + ("Zoom cursor", "Ampliar cursor"), + ("Accept sessions via password", "Aceptar sesiones a través de contraseña"), + ("Accept sessions via click", "Aceptar sesiones a través de clic"), + ("Accept sessions via both", "Aceptar sesiones a través de ambos"), + ("Please wait for the remote side to accept your session request...", "Por favor, espere a que el lado remoto acepte su solicitud de sesión"), + ("One-time Password", "Contraseña de un solo uso"), + ("Use one-time password", "Usar contraseña de un solo uso"), + ("One-time password length", "Longitud de la contraseña de un solo uso"), + ("Request access to your device", "Solicitud de acceso a su dispositivo"), + ("Hide connection management window", "Ocultar ventana de gestión de conexión"), + ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), + ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), + ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), + ("Skipped", "Omitido"), + ("Add to address book", "Añadir al directorio"), + ("Group", "Grupo"), + ("Search", "Búsqueda"), + ("Closed manually by web console", "Cerrado manualmente por la consola web"), + ("Local keyboard type", "Tipo de teclado local"), + ("Select local keyboard type", "Seleccionar tipo de teclado local"), + ("software_render_tip", "Si tienes una gráfica Nvidia y la ventana remota se cierra inmediatamente, instalar el driver nouveau y elegir renderizado por software podría ayudar. Se requiere reiniciar la aplicación."), + ("Always use software rendering", "Usar siempre renderizado por software"), + ("config_input", "Para controlar el escritorio remoto con el teclado necesitas dar a RustDesk permisos de \"Monitorización de entrada\"."), + ("config_microphone", "Para poder hablar de forma remota necesitas darle a RustDesk permisos de \"Grabar Audio\"."), + ("request_elevation_tip", "También puedes solicitar elevación de privilegios si hay alguien en el lado remoto."), + ("Wait", "Esperar"), + ("Elevation Error", "Error de elevación de privilegios"), + ("Ask the remote user for authentication", "Pida autenticación al usuario remoto"), + ("Choose this if the remote account is administrator", "Elegir si la cuenta remota es de administrador"), + ("Transmit the username and password of administrator", "Transmitir usuario y contraseña del administrador"), + ("still_click_uac_tip", "Aún se necesita que el usuario remoto haga click en OK en la ventana UAC del RusDesk en ejecución."), + ("Request Elevation", "Solicitar Elevación de privilegios"), + ("wait_accept_uac_tip", "Por favor espere a que el usuario remoto acepte el diálogo UAC."), + ("Elevate successfully", "Elevación de privilegios exitosa"), + ("uppercase", "mayúsculas"), + ("lowercase", "minúsculas"), + ("digit", "dígito"), + ("special character", "carácter especial"), + ("length>=8", "longitud>=8"), + ("Weak", "Débil"), + ("Medium", "Media"), + ("Strong", "Fuerte"), + ("Switch Sides", "Intercambiar lados"), + ("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"), + ("Display", "Pantalla"), + ("Default View Style", "Estilo de vista predeterminado"), + ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), + ("Default Image Quality", "Calidad de imagen predeterminada"), + ("Default Codec", "Códec predeterminado"), + ("Bitrate", "Tasa de bits"), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", "Otras opciones predeterminadas"), + ("Voice call", "Llamada de voz"), + ("Text chat", "Chat de texto"), + ("Stop voice call", "Detener llamada de voz"), + ("relay_hint_tip", "Puede que no sea posible conectar directamente. Puedes tratar de conectar a través de relay. \nAdicionalmente, si quieres usar relay en el primer intento, puedes añadir el sufijo \"/r\" a la ID o seleccionar la opción \"Conectar siempre a través de relay\" en la tarjeta del par."), + ("Reconnect", "Reconectar"), + ("Codec", "Códec"), + ("Resolution", "Resolución"), + ("No transfers in progress", "No hay transferencias en curso"), + ("Set one-time password length", "Establecer contraseña de un solo uso"), + ("RDP Settings", "Ajustes RDP"), + ("Sort by", "Ordenar por"), + ("New Connection", "Nueva conexión"), + ("Restore", "Restaurar"), + ("Minimize", "Minimizar"), + ("Maximize", "Maximizar"), + ("Your Device", "Tu dispositivo"), + ("empty_recent_tip", "¡Vaya, no hay conexiones recientes!\nEs hora de planificar una nueva."), + ("empty_favorite_tip", "¿Sin pares favoritos aún?\nEncontremos uno al que conectarte y ¡añádelo a tus favoritos!"), + ("empty_lan_tip", "Oh no, parece que aún no has descubierto ningún par."), + ("empty_address_book_tip", "Parece que actualmente no hay pares en tu directorio."), + ("Empty Username", "Nombre de usuario vacío"), + ("Empty Password", "Contraseña vacía"), + ("Me", "Yo"), + ("identical_file_tip", "Este archivo es idéntico al del par."), + ("show_monitors_tip", "Mostrar monitores en la barra de herramientas"), + ("View Mode", "Modo Vista"), + ("login_linux_tip", "Necesitas iniciar sesión con la cueneta del Linux remoto para activar una sesión de escritorio X"), + ("verify_rustdesk_password_tip", "Verificar la contraseña de RustDesk"), + ("remember_account_tip", "Recordar esta cuenta"), + ("os_account_desk_tip", "Esta cueneta se usa para iniciar sesión en el sistema operativo remoto y habilitar la sesión de escritorio en headless."), + ("OS Account", "Cuenta del SO"), + ("another_user_login_title_tip", "Otro usuario ya ha iniciado sesión"), + ("another_user_login_text_tip", "Desconectar"), + ("xorg_not_found_title_tip", "Xorg no hallado"), + ("xorg_not_found_text_tip", "Por favor, instala Xorg"), + ("no_desktop_title_tip", "No hay escritorio disponible"), + ("no_desktop_text_tip", "Por favor, instala GNOME Desktop"), + ("No need to elevate", "No es necesario elevar privilegios"), + ("System Sound", "Sonido del Sistema"), + ("Default", "Predeterminado"), + ("New RDP", "Nuevo RDP"), + ("Fingerprint", "Huella digital"), + ("Copy Fingerprint", "Copiar huella digital"), + ("no fingerprints", "sin huellas digitales"), + ("Select a peer", "Seleccionar un par"), + ("Select peers", "Seleccionar pares"), + ("Plugins", "Complementos"), + ("Uninstall", "Desinstalar"), + ("Update", "Actualizar"), + ("Enable", "Habilitar"), + ("Disable", "Inhabilitar"), + ("Options", "Opciones"), + ("resolution_original_tip", "Resolución original"), + ("resolution_fit_local_tip", "Ajustar resolución local"), + ("resolution_custom_tip", "Resolución personalizada"), + ("Collapse toolbar", "Contraer barra de herramientas"), + ("Accept and Elevate", "Aceptar y Elevar"), + ("accept_and_elevate_btn_tooltip", "Aceptar la conexión y elevar permisos UAC."), + ("clipboard_wait_response_timeout_tip", "Tiempo de espera para copia agotado."), + ("Incoming connection", "Conexión entrante"), + ("Outgoing connection", "Conexión saliente"), + ("Exit", "Salir"), + ("Open", "Abrir"), + ("logout_tip", "¿Seguro que deseas cerrar sesión?"), + ("Service", "Servicio"), + ("Start", "Iniciar"), + ("Stop", "Detener"), + ("exceed_max_devices", "Has alcanzado el máximo número de dispositivos administrados."), + ("Sync with recent sessions", "Sincronizar con sesiones recientes"), + ("Sort tags", "Ordenar etiquetas"), + ("Open connection in new tab", "Abrir conexión en nueva pestaña"), + ("Move tab to new window", "Mover pestaña a nueva ventana"), + ("Can not be empty", "No puede estar vacío"), + ("Already exists", "Ya existe"), + ("Change Password", "Cambiar contraseña"), + ("Refresh Password", "Refrescar contraseña"), + ("ID", ""), + ("Grid View", "Vista Cuadrícula"), + ("List View", "Vista Lista"), + ("Select", "Seleccionar"), + ("Toggle Tags", "Alternar Etiquetas"), + ("pull_ab_failed_tip", "No se ha podido refrescar el directorio"), + ("push_ab_failed_tip", "No se ha podido sincronizar el directorio con el servidor"), + ("synced_peer_readded_tip", "Los dispositivos presentes en sesiones recientes se sincronizarán con el directorio."), + ("Change Color", "Cambiar Color"), + ("Primary Color", "Color Primario"), + ("HSV Color", "Color HSV"), + ("Installation Successful!", "Instalación exitosa"), + ("Installation failed!", "La instalación ha fallado"), + ("Reverse mouse wheel", "Invertir rueda del ratón"), + ("{} sessions", "{} sesiones"), + ("scam_title", "Podrías estar siendo ESTAFADO!"), + ("scam_text1", "Si estás al teléfono con alguien a quien NO conoces y en quien NO confías y te ha pedido que uses RustDesk e inicies el servicio, no lo hagas y cuelga inmediatamente."), + ("scam_text2", "Probablemente son estafadores tratando de robar tu dinero o información privada."), + ("Don't show again", "No mostrar de nuevo"), + ("I Agree", "Estoy de acuerdo"), + ("Decline", "Rechazar"), + ("Timeout in minutes", "Tiempo de espera en minutos"), + ("auto_disconnect_option_tip", "Cerrar sesiones entrantes automáticamente por inactividad del usuario."), + ("Connection failed due to inactivity", "Desconectar automáticamente por inactividad."), + ("Check for software update on startup", "Comprobar actualización al iniciar"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "¡Por favor, actualiza RustDesk Server Pro a la versión {} o superior"), + ("pull_group_failed_tip", "No se ha podido refrescar el grupo"), + ("Filter by intersection", "Filtrar por intersección"), + ("Remove wallpaper during incoming sessions", "Quitar el fondo de pantalla durante sesiones entrantes"), + ("Test", "Probar"), + ("display_is_plugged_out_msg", "La pantalla está desconectada, cambia a la principal."), + ("No displays", "No hay pantallas"), + ("Open in new window", "Abrir en una nueva ventana"), + ("Show displays as individual windows", "Mostrar pantallas como ventanas individuales"), + ("Use all my displays for the remote session", "Usar todas mis pantallas para la sesión remota"), + ("selinux_tip", "SELinux está activado en tu dispositivo, lo que puede hacer que RustDesk no se ejecute correctamente como lado controlado."), + ("Change view", "Cambiar vista"), + ("Big tiles", "Mosaicos grandes"), + ("Small tiles", "Mosaicos pequeños"), + ("List", "Lista"), + ("Virtual display", "Pantalla virtual"), + ("Plug out all", "Desconectar todo"), + ("True color (4:4:4)", "Color real (4:4:4)"), + ("Enable blocking user input", "Habilitar el bloqueo de la entrada del usuario"), + ("id_input_tip", "Puedes introducir una ID, una IP directa o un dominio con un puerto (:).\nSi quieres acceder a un dispositivo en otro servidor, por favor añade la ip del servidor (@?key=), por ejemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi quieres acceder a un dispositivo en un servidor público, por favor, introduce \"@public\", la clave no es necesaria para un servidor público."), + ("privacy_mode_impl_mag_tip", "Modo 1"), + ("privacy_mode_impl_virtual_display_tip", "Modo 2"), + ("Enter privacy mode", "Entrar al modo privado"), + ("Exit privacy mode", "Salir del modo privado"), + ("idd_not_support_under_win10_2004_tip", "El controlador de pantalla indirecto no está soportado. Se necesita Windows 10, versión 2004 o superior."), + ("input_source_1_tip", "Fuente de entrada 1"), + ("input_source_2_tip", "Fuente de entrada 2"), + ("Swap control-command key", "Intercambiar teclas control-comando"), + ("swap-left-right-mouse", "Intercambiar botones derecho-izquierdo del ratón"), + ("2FA code", "Código 2FA"), + ("More", "Más"), + ("enable-2fa-title", "Habilitar autenticación en dos pasos"), + ("enable-2fa-desc", "Por favor, configura tu autenticador ahora. Puedes usar una app de autenticación como Authy, Microsoft o Google Authenticator en tu teléfono u ordenador.\n\nEscanea el código QR con tu app e introduce el código mostrado para habilitar la autenticación en dos pasos."), + ("wrong-2fa-code", "No se puede verificar el código. Comprueba que el código y los ajustes de hora local son correctos"), + ("enter-2fa-title", "Autenticación en dos pasos"), + ("Email verification code must be 6 characters.", "El código de verificación por mail debe tener 6 caracteres"), + ("2FA code must be 6 digits.", "El cóidigo 2FA debe tener 6 dígitos"), + ("Multiple Windows sessions found", "Encontradas sesiones de múltiples ventanas"), + ("Please select the session you want to connect to", "Por favor, seleccione la sesión a la que se desea conectar"), + ("powered_by_me", "Con tecnología de RustDesk"), + ("outgoing_only_desk_tip", "Esta es una edición personalizada.\nPuedes conectarte a otros dispositivos, pero ellos no pueden conectarse al tuyo."), + ("preset_password_warning", "Esta edición personalizada viene con una contraseña preestablecida. Cualquiera que la conozca podrá tener control total de tu dispositivo.Si no es esto lo que esperabas, desinstala el software inmediatamente."), + ("Security Alert", "Alerta de Seguridad"), + ("My address book", "Mi directorio"), + ("Personal", "Personal"), + ("Owner", "Propietario"), + ("Set shared password", "Establecer contraseña compartida"), + ("Exist in", "Existe en"), + ("Read-only", "Solo-lectura"), + ("Read/Write", "Lectura/Escritura"), + ("Full Control", "Control Total"), + ("share_warning_tip", "Los campos mostrados arriba son compartidos y visibles por otros."), + ("Everyone", "Todos"), + ("ab_web_console_tip", "Más en consola web"), + ("allow-only-conn-window-open-tip", "Permitir la conexión solo si la ventana RusDesk está abierta"), + ("no_need_privacy_mode_no_physical_displays_tip", "No hay pantallas físicas, no es necesario usar el modo privado."), + ("Follow remote cursor", "Seguir cursor remoto"), + ("Follow remote window focus", "Seguir ventana remota activa"), + ("default_proxy_tip", "El protocolo y puerto predeterminados es Socks5 y 1080"), + ("no_audio_input_device_tip", "No se ha encontrado ningún dispositivo de entrada de autio."), + ("Incoming", "Entrante"), + ("Outgoing", "Saliente"), + ("Clear Wayland screen selection", "Borrar la selección de pantalla Wayland"), + ("clear_Wayland_screen_selection_tip", "Tras borrar la selección de pantalla, puedes volver a seleccionarla para compartir."), + ("confirm_clear_Wayland_screen_selection_tip", "¿Seguro que deseas borrar la selección de pantalla Wayland?"), + ("android_new_voice_call_tip", "Se ha recibido una nueva solicitud de llamada de voz. Si aceptas el audio cambiará a comunicación de voz."), + ("texture_render_tip", "Usar renderizado de texturas para hacer las imágenes más suaves."), + ("Use texture rendering", "Usar renderizado de texturas"), + ("Floating window", "Ventana flotante"), + ("floating_window_tip", "Ayuda a mantener el servicio de RustDesk de fondo"), + ("Keep screen on", "Mantener la pantalla encendida"), + ("Never", "Nunca"), + ("During controlled", "Mientras está siendo controlado"), + ("During service is on", "Mientras el servicio está activo"), + ("Capture screen using DirectX", "Capturar pantalla con DirectX"), + ("Back", "Atrás"), + ("Apps", ""), + ("Volume up", "Bajar volumen"), + ("Volume down", "Subir volumen"), + ("Power", "Encendido"), + ("Telegram bot", "Bot de Telegram"), + ("enable-bot-tip", "Si activas esta característica puedes recibir código 2FA de tu bot. También puede funcionar como notificación de conexión."), + ("enable-bot-desc", "1, Abre un chat con @BotFather.\n2, Envía el comando \"/newbot\". Recibirás un token tras completar esta paso.\n3, Inicia un chat con tu bot recién creado. Envía un mensaje que comience con una barra (\"/\") como \"/hola\" para activarlo.\n"), + ("cancel-2fa-confirm-tip", "¿Seguro que quieres cancelar 2FA?"), + ("cancel-bot-confirm-tip", "¿Seguro que quieres cancelar el bot de Telegram?"), + ("About RustDesk", "Acerca de RustDesk"), + ("Send clipboard keystrokes", "Enviar pulsaciones de teclas"), + ("network_error_tip", "Por fvor, comprueba tu conexión de red e inténtalo de nuevo."), + ("Unlock with PIN", "Desbloquear con PIN"), + ("Requires at least {} characters", "Se necesitan al menos {} caracteres"), + ("Wrong PIN", "PIN erróneo"), + ("Set PIN", "Establecer PIN"), + ("Enable trusted devices", "Habilitar dispositivos de confianza"), + ("Manage trusted devices", "Gestionar dispositivos de confianza"), + ("Platform", "Plataforma"), + ("Days remaining", "Días restantes"), + ("enable-trusted-devices-tip", "Omitir la verificación en dos fases en dispositivos de confianza"), + ("Parent directory", "Directorio superior"), + ("Resume", "Continuar"), + ("Invalid file name", "Nombre de archivo no válido"), + ("one-way-file-transfer-tip", "La transferencia en un sentido está habilitada en el lado controlado."), + ("Authentication Required", "Se requiere autenticación"), + ("Authenticate", "Autenticar"), + ("web_id_input_tip", "Puedes introducir una ID en el mismo servidor, el cliente web no soporta acceso vía IP.\nSi quieres acceder a un dispositivo en otro servidor, por favor, agrega la dirección del servidor (@?clave=), por ejemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi quieres accedder a un dispositivo en un servidor público, por favor, introduce \"@public\", la clave no es necesaria para el servidor público."), + ("Download", "Descarga"), + ("Upload folder", "Subir carpeta"), + ("Upload files", "Subir archivos"), + ("Clipboard is synchronized", "Portapapeles sincronizado"), + ("Update client clipboard", "Actualizar portapapeles del cliente"), + ("Untagged", "Sin itiquetar"), + ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"), + ("d3d_render_tip", "Al activar el renderizado D3D, la pantalla de control remoto puede verse negra en algunos equipos."), + ("Use D3D rendering", "Usar renderizado D3D"), + ("Printer", "Impresora"), + ("printer-os-requirement-tip", "La función de salida de impresora necesita Windows 10 o superior."), + ("printer-requires-installed-{}-client-tip", "Para usar la impresión remota, {} necesita estar instalado en tu dispositivo."), + ("printer-{}-not-installed-tip", "La impresora {} no está instalada."), + ("printer-{}-ready-tip", "La impresora {} está instalada y lista para usar."), + ("Install {} Printer", "Instalar la impresora {}"), + ("Outgoing Print Jobs", "Tareas salientes de impresión"), + ("Incoming Print Jobs", "Tareas entrantes de impresión"), + ("Incoming Print Job", "Trabajo entrante de impresión"), + ("use-the-default-printer-tip", "Usar la impresora predeterminada"), + ("use-the-selected-printer-tip", "Usar la impresora seleccionada"), + ("auto-print-tip", "Imprimir automáticamente usando la impresora seleccionada."), + ("print-incoming-job-confirm-tip", "Has recibido una tarea de impresión remota. ¿Deseas ejecutarla en tu lado?"), + ("remote-printing-disallowed-tile-tip", "Impresión remota inhabilitada"), + ("remote-printing-disallowed-text-tip", "Los ajustes de permisos del lado controlado no permiten la impresión remota."), + ("save-settings-tip", "Guardar ajustes"), + ("dont-show-again-tip", "No volver a mostrar"), + ("Take screenshot", "Tomar captura de pantalla"), + ("Taking screenshot", "Tomando captura de pantalla"), + ("screenshot-merged-screen-not-supported-tip", "La fusión de capturas de pantalla de múltiples monitores no está soportada. Por favor, cambie a un monitor e inténtelo de nuevo."), + ("screenshot-action-tip", "Por favor, seleccione cómo continuar con la captura de pantalla."), + ("Save as", "Guardar como"), + ("Copy to clipboard", "Copiar al portapapeles"), + ("Enable remote printer", "Habilitar impresora remota"), + ("Downloading {}", "Descargando {}"), + ("{} Update", "{} Actualizar"), + ("{}-to-update-tip", "{} Se cerrará ahora e instalará la nueva versión."), + ("download-new-version-failed-tip", "Descarga fallida. Puedes volver a intentarlo o hacer clic en el botón \"Download\" para descargar desde la página de lanzamientos y actualizar manualmente."), + ("Auto update", "Auto actualizar"), + ("update-failed-check-msi-tip", "Comprobación de método de instalación fallida. Por favor, haz clic en el botón \"Download\" para descargar desde la página de lanzamientos y actualizar manualmente."), + ("websocket_tip", "Al usar WebSocket, solo se permiten conexiones relay."), + ("Use WebSocket", "Usar WebSocket"), + ("Trackpad speed", "Velocidad de trackpad"), + ("Default trackpad speed", "Velocidad predeterminada de trackpad"), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Ver cámara"), + ("Enable camera", "Habilitar cámara"), + ("No cameras", "No hay cámaras"), + ("view_camera_unsupported_tip", "El dispositivo remoto no soporta la visualización de la cámara."), + ("Terminal", ""), + ("Enable terminal", "Habilitar terminal"), + ("New tab", "Nueva pestaña"), + ("Keep terminal sessions on disconnect", "Mantener sesiones de terminal al desconectar"), + ("Terminal (Run as administrator)", "Terminal (Ejecutar como administrador)"), + ("terminal-admin-login-tip", "Por favor, introduzca el usuario y la contrasseña del administrador en el lado controlado."), + ("Failed to get user token.", "No se ha podido obtener el token de usuario"), + ("Incorrect username or password.", "Nombre y contraseña incorrectos"), + ("The user is not an administrator.", "El usuario no es un administrador."), + ("Failed to check if the user is an administrator.", "No se ha podido comprobar si el usuario es un administrador."), + ("Supported only in the installed version.", "Soportado solo en la versión instalada."), + ("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", "Escala personalizada"), + ("Custom scale slider", "Control deslizante de escala personalizada"), + ("Decrease", "Disminuir"), + ("Increase", "Aumentar"), + ("Show virtual mouse", "Mostrar ratón virtual"), + ("Virtual mouse size", "Tamaño del ratón virtual"), + ("Small", "Pequeño"), + ("Large", "Grande"), + ("Show virtual joystick", "Mostrar joystick virtual"), + ("Edit note", "Editar nota"), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Continuar con {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/et.rs b/vendor/rustdesk/src/lang/et.rs new file mode 100644 index 0000000..a00c312 --- /dev/null +++ b/vendor/rustdesk/src/lang/et.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Olek"), + ("Your Desktop", "Sinu töölaud"), + ("desk_tip", "Sinu töölauale saab selle ID ja parooliga ligi pääseda."), + ("Password", "Parool"), + ("Ready", "Valmis"), + ("Established", "Ühendus loodud"), + ("connecting_status", "RustDeski võrguga ühendumine..."), + ("Enable service", "Luba teenus"), + ("Start service", "Käivita teenus"), + ("Service is running", "Teenus töötab"), + ("Service is not running", "Teenus ei tööta"), + ("not_ready_status", "Pole valmis. Palun kontrolli oma ühendust"), + ("Control Remote Desktop", "Juhi kaugtöölauda"), + ("Transfer file", "Edasta fail"), + ("Connect", "Ühenda"), + ("Recent sessions", "Viimatised seansid"), + ("Address book", "Aadressiraamat"), + ("Confirmation", "Kinnitus"), + ("TCP tunneling", "TCP-tunneldamine"), + ("Remove", "Eemalda"), + ("Refresh random password", "Värskenda juhuslik parool"), + ("Set your own password", "Määra oma parool"), + ("Enable keyboard/mouse", "Luba klaviatuur/hiir"), + ("Enable clipboard", "Luba lõikelaud"), + ("Enable file transfer", "Luba failiedastus"), + ("Enable TCP tunneling", "Luba TCP-tunneldamine"), + ("IP Whitelisting", "IP lubamisloend"), + ("ID/Relay Server", "ID-/releeserver"), + ("Import server config", "Impordi serveriseadistus"), + ("Export Server Config", "Ekspordi serveriseadistus"), + ("Import server configuration successfully", "Serveriseadistus edukalt imporditud"), + ("Export server configuration successfully", "Serveriseadistus edukalt eksporditud"), + ("Invalid server configuration", "Sobimatu serveriseadistus"), + ("Clipboard is empty", "Lõikelaud on tühi"), + ("Stop service", "Peata teenus"), + ("Change ID", "Muuda ID-d"), + ("Your new ID", "Sinu uus ID"), + ("length %min% to %max%", "pikkus %min%-%max%"), + ("starts with a letter", "algab tähega"), + ("allowed characters", "lubatud tähemärgid"), + ("id_change_tip", "Lubatud on vaid a-z, A-Z, 0-9, - (kriips) ja _ (alakriips) tähemärgid. Esimene täht peab olema a-z või A-Z. Pikkus vahemikus 6-16."), + ("Website", "Veebileht"), + ("About", "Meist"), + ("Slogan_tip", "Loodud südamega selles kaootilises maailmas!"), + ("Privacy Statement", "Privaatsusteatis"), + ("Mute", "Hääletu"), + ("Build Date", "Ehituskuupäev"), + ("Version", "Versioon"), + ("Home", "Kodu"), + ("Audio Input", "Helisisend"), + ("Enhancements", "Täiendused"), + ("Hardware Codec", "Riistvarakoodek"), + ("Adaptive bitrate", "Kohanduv bitikiirus"), + ("ID Server", "ID-server"), + ("Relay Server", "Releeserver"), + ("API Server", "Rakendusliidese server"), + ("invalid_http", "peab algama: http:// või https://"), + ("Invalid IP", "Sobimatu IP"), + ("Invalid format", "Sobimatu vorming"), + ("server_not_support", "Pole veel serveri poolt toetatud"), + ("Not available", "Pole saadaval"), + ("Too frequent", "Liiga sagedane"), + ("Cancel", "Tühista"), + ("Skip", "Jäta vahele"), + ("Close", "Sulge"), + ("Retry", "Proovi uuesti"), + ("OK", "OK"), + ("Password Required", "Parool on nõutud"), + ("Please enter your password", "Palun sisesta oma parool"), + ("Remember password", "Jäta parool meelde"), + ("Wrong Password", "Vale parool"), + ("Do you want to enter again?", "Kas soovid uuesti sisestada?"), + ("Connection Error", "Ühenduse viga"), + ("Error", "Viga"), + ("Reset by the peer", "Partneri poolt lähtestatud"), + ("Connecting...", "Ühendamine..."), + ("Connection in progress. Please wait.", "Ühendus on käimas. Palun oota."), + ("Please try 1 minute later", "Palun proovi 1 minuti pärast"), + ("Login Error", "Sisselogimise viga"), + ("Successful", "Edukas"), + ("Connected, waiting for image...", "Ühendatud, pildi ootamine..."), + ("Name", "Nimi"), + ("Type", "Tüüp"), + ("Modified", "Muudetud"), + ("Size", "Suurus"), + ("Show Hidden Files", "Kuva peidetud faile"), + ("Receive", "Võta vastu"), + ("Send", "Saada"), + ("Refresh File", "Värskenda faili"), + ("Local", "Kohalik"), + ("Remote", "Kaugjuhitav"), + ("Remote Computer", "Kaugarvuti"), + ("Local Computer", "Kohalik arvuti"), + ("Confirm Delete", "Kinnita kustutamist"), + ("Delete", "Kustuta"), + ("Properties", "Atribuudid"), + ("Multi Select", "Mitmikvalik"), + ("Select All", "Vali kõik"), + ("Unselect All", "Tühista kõigi valik"), + ("Empty Directory", "Tühi kaust"), + ("Not an empty directory", "Pole tühi kaust"), + ("Are you sure you want to delete this file?", "Kas soovid kindlasti selle faili eemaldada?"), + ("Are you sure you want to delete this empty directory?", "Kas soovid kindlasti selle tühja kausta eemaldada?"), + ("Are you sure you want to delete the file of this directory?", "Kas soovid kindlasti selle kausta faili eemaldada?"), + ("Do this for all conflicts", "Tee see kõigi konfliktide puhul"), + ("This is irreversible!", "See on pöördumatu!"), + ("Deleting", "Kustutamine"), + ("files", "failid"), + ("Waiting", "Ootamine"), + ("Finished", "Lõpetatud"), + ("Speed", "Kiirus"), + ("Custom Image Quality", "Kohandatud pildikvaliteet"), + ("Privacy mode", "Privaatsusrežiim"), + ("Block user input", "Blokeeri kasutajasisend"), + ("Unblock user input", "Eemalda kasutajasisendi blokeering"), + ("Adjust Window", "Kohanda akent"), + ("Original", "Algne"), + ("Shrink", "Vähenda"), + ("Stretch", "Venita"), + ("Scrollbar", "Kerimisriba"), + ("ScrollAuto", "Automaatne kerimine"), + ("Good image quality", "Hea pildikvaliteet"), + ("Balanced", "Tasakaalustatud"), + ("Optimize reaction time", "Optimeeri reageerimisaeg"), + ("Custom", "Kohandatud"), + ("Show remote cursor", "Kuva kaugkursorit"), + ("Show quality monitor", "Kuva kvaliteedijälgija"), + ("Disable clipboard", "Keela lõikelaud"), + ("Lock after session end", "Lukusta pärast seansi lõppu"), + ("Insert Ctrl + Alt + Del", "Sisesta Ctrl + Alt + Del"), + ("Insert Lock", "Sisesta lukk"), + ("Refresh", "Värskenda"), + ("ID does not exist", "ID-d ei eksisteeri"), + ("Failed to connect to rendezvous server", "Kohtumisserveriga ühendumine ebaõnnestus"), + ("Please try later", "Palun proovi hiljem"), + ("Remote desktop is offline", "Kaugtöölaud on väljas"), + ("Key mismatch", "Võtme sobimatus"), + ("Timeout", "Ajalõpp"), + ("Failed to connect to relay server", "Releeserveriga ühendumine ebaõnnestus"), + ("Failed to connect via rendezvous server", "Kohtumisserveri kaudu ühendumine ebaõnnestus"), + ("Failed to connect via relay server", "Releeserveri kaudu ühendumine ebaõnnestus"), + ("Failed to make direct connection to remote desktop", "Kaugtöölauaga otsese ühenduse loomine ebaõnnestus"), + ("Set Password", "Määra parool"), + ("OS Password", "Opsüsteemi parool"), + ("install_tip", "Kasutajakonto kontrolli (UAC) tõttu ei saa RustDesk mõnel juhul korralikult kaugjuhtimispoolena töötada. Kontrolli vältimiseks palun klõpsa alloleval nupul, et RustDesk oma süsteemi paigaldada."), + ("Click to upgrade", "Vajuta täiendamiseks"), + ("Configure", "Seadista"), + ("config_acc", "Töölaua kaugjuhtimiseks tuleb RustDeskile anda \"juurdepääsetavuse\" õigused."), + ("config_screen", "Töölaua kaugjuhtimiseks tuleb RustDeskile anda \"ekraanisalvestuse\" õigused."), + ("Installing ...", "Installimine..."), + ("Install", "Installi"), + ("Installation", "Paigaldus"), + ("Installation Path", "Paigaldustee"), + ("Create start menu shortcuts", "Loo Start-menüü otseteed"), + ("Create desktop icon", "Loo töölauaikoon"), + ("agreement_tip", "Paigalduse alustamisel nõustud litsentsilepinguga."), + ("Accept and Install", "Nõustu ja paigalda"), + ("End-user license agreement", "Lõppkasutaja litsentsileping"), + ("Generating ...", "Loomine..."), + ("Your installation is lower version.", "Sinu paigaldus kasutab vanemat versiooni."), + ("not_close_tcp_tip", "Ära sulge seda akent, kuni kasutad tunnelit"), + ("Listening ...", "Kuulamine..."), + ("Remote Host", "Kaughost"), + ("Remote Port", "Kaugport"), + ("Action", "Tegevus"), + ("Add", "Lisa"), + ("Local Port", "Kohalik port"), + ("Local Address", "Kohalik aadress"), + ("Change Local Port", "Muuda kohalikku porti"), + ("setup_server_tip", "Kiirema ühenduse jaoks palun seadista oma server"), + ("Too short, at least 6 characters.", "Liiga lühike, peab olema vähemalt 6 tähemärki."), + ("The confirmation is not identical.", "Kinnitus ei ole identne."), + ("Permissions", "Õigused"), + ("Accept", "Nõustu"), + ("Dismiss", "Loobu"), + ("Disconnect", "Ühendu lahti"), + ("Enable file copy and paste", "Luba failide kopeerimine ja kleepimine"), + ("Connected", "Ühendatud"), + ("Direct and encrypted connection", "Otsene ja krüpteeritud ühendus"), + ("Relayed and encrypted connection", "Vahendatud ja krüpteeritud ühendus"), + ("Direct and unencrypted connection", "Otsene ja krüpteerimata ühendus"), + ("Relayed and unencrypted connection", "Vahendatud ja krüpteerimata ühendus"), + ("Enter Remote ID", "Sisesta kaug-ID"), + ("Enter your password", "Sisesta oma parool"), + ("Logging in...", "Sisselogimine..."), + ("Enable RDP session sharing", "Luba RDP-seansi jagamine"), + ("Auto Login", "Logi automaatselt sisse (Kehtib vaid valiku \"lukusta pärast seansi lõppu\" lubamisel)"), + ("Enable direct IP access", "Luba otsene IP-juurdepääs"), + ("Rename", "Nimeta ümber"), + ("Space", "Ruum"), + ("Create desktop shortcut", "Loo töölauaotsetee"), + ("Change Path", "Muuda failiteed"), + ("Create Folder", "Loo kaust"), + ("Please enter the folder name", "Palun sisesta kausta nimi"), + ("Fix it", "Paranda see"), + ("Warning", "Hoiatus"), + ("Login screen using Wayland is not supported", "Waylandi kasutav sisselogimisekraan ei ole toetatud"), + ("Reboot required", "Taaskäivitus nõutud"), + ("Unsupported display server", "Mittetoetatud kuvaserver"), + ("x11 expected", "Oodatakse x11"), + ("Port", "Port"), + ("Settings", "Seaded"), + ("Username", "Kasutajanimi"), + ("Invalid port", "Sobimatu port"), + ("Closed manually by the peer", "Partneri poolt käsitsi suletud"), + ("Enable remote configuration modification", "Luba kaugtöölaua seadistuse muutmine"), + ("Run without install", "Käivita paigaldamata"), + ("Connect via relay", "Ühenda relee kaudu"), + ("Always connect via relay", "Ühenda alati relee kaudu"), + ("whitelist_tip", "Ainult lubamisloendis IP saab mulle ligi"), + ("Login", "Logi sisse"), + ("Verify", "Kinnita"), + ("Remember me", "Jäta mind meelde"), + ("Trust this device", "Usalda seda seadet"), + ("Verification code", "Kinnituskood"), + ("verification_tip", "Registreeritud e-posti aadressile on saadetud kinnituskood, sisselogimise jätkamiseks sisesta kinnituskood."), + ("Logout", "Logi välja"), + ("Tags", "Sildid"), + ("Search ID", "Otsi ID-d"), + ("whitelist_sep", "Eraldatud koma, semikooloni, tühikute või uue reaga"), + ("Add ID", "Lisa ID"), + ("Add Tag", "Lisa silt"), + ("Unselect all tags", "Tühista kõik sildid"), + ("Network error", "Võrgu viga"), + ("Username missed", "Kasutajanimi on puudu"), + ("Password missed", "Parool on puudu"), + ("Wrong credentials", "Vale kasutajanimi või parool"), + ("The verification code is incorrect or has expired", "Kinnituskood on vale või aegunud"), + ("Edit Tag", "Muuda silti"), + ("Forget Password", "Unusta parool"), + ("Favorites", "Lemmikud"), + ("Add to Favorites", "Lisa lemmikutesse"), + ("Remove from Favorites", "Eemalda lemmikutest"), + ("Empty", "Tühi"), + ("Invalid folder name", "Kehtetu kaustanimi"), + ("Socks5 Proxy", "Socks5 proksi"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) proksi"), + ("Discovered", "Avastatud"), + ("install_daemon_tip", "Süsteemikäivitusel käivitamiseks tuleb paigaldada süsteemiteenus."), + ("Remote ID", "Kaug-ID"), + ("Paste", "Kleebi"), + ("Paste here?", "Kleebid siia?"), + ("Are you sure to close the connection?", "Kas soovid kindlasti ühenduse sulgeda?"), + ("Download new version", "Laadi alla uus versioon"), + ("Touch mode", "Puuterežiim"), + ("Mouse mode", "Hiirerežiim"), + ("One-Finger Tap", "Ühe sõrme koputus"), + ("Left Mouse", "Vasak hiireklahv"), + ("One-Long Tap", "Üks pikk koputus"), + ("Two-Finger Tap", "Kahe sõrme koputus"), + ("Right Mouse", "Parem hiireklahv"), + ("One-Finger Move", "Üks sõrmeliigutus"), + ("Double Tap & Move", "Topeltkoputus ja liigutus"), + ("Mouse Drag", "Hiirega sikutamine"), + ("Three-Finger vertically", "Kolm sõrme vertikaalselt"), + ("Mouse Wheel", "Hiirerullik"), + ("Two-Finger Move", "Kahe sõrme liigutus"), + ("Canvas Move", "Lõuendi liigutus"), + ("Pinch to Zoom", "Näpistus-suum"), + ("Canvas Zoom", "Lõuendi suum"), + ("Reset canvas", "Lähtesta lõuend"), + ("No permission of file transfer", "Failiülekande luba puudub"), + ("Note", "Märkus"), + ("Connection", "Ühendus"), + ("Share screen", "Jaga ekraani"), + ("Chat", "Vestlus"), + ("Total", "Kokku"), + ("items", "üksust"), + ("Selected", "Valitud"), + ("Screen Capture", "Ekraanisalvestus"), + ("Input Control", "Sisendjuhtimine"), + ("Audio Capture", "Helisalvestus"), + ("Do you accept?", "Kas nõustud?"), + ("Open System Setting", "Ava süsteemisätted"), + ("How to get Android input permission?", "Kuidas saada Androidi sisendi luba?"), + ("android_input_permission_tip1", "Selleks, et kaugseade saaks sinu Androidi seadet juhtida hiire või puute abil, pead andma RustDeskile \"juurdepääsetavuse\" loa."), + ("android_input_permission_tip2", "Palun mine järgmisele süsteemiseadete lehele, leia ja sisesta [Paigaldatud teenused], lülita teenus [RustDesk Input] sisse."), + ("android_new_connection_tip", "Saabunud on uus juhtimistaotlus, mis soovib sinu praegust seadet juhtida."), + ("android_service_will_start_tip", "\"Ekraanisalvestuse\" lubamine käivitab teenuse automaatselt, lubades teistel seadetel sinu seadmesse ühendust taotleda."), + ("android_stop_service_tip", "Teenuse sulgemine sulgeb automaatselt kõik loodud ühendused."), + ("android_version_audio_tip", "Kasutatav Androidi versioon ei toeta helisalvestust, palun täienda Android 10 või uuemale versioonile."), + ("android_start_service_tip", "Koputa [Alusta teenust] või anna [Ekraanisalvestuse] luba, et ekraanijagamisteenust alustada."), + ("android_permission_may_not_change_tip", "Loodud ühenduste õigused ei pruugi muutuda enne taasühendamist koheselt."), + ("Account", "Konto"), + ("Overwrite", "Ülekirjutamine"), + ("This file exists, skip or overwrite this file?", "See fail eksisteerib, kas soovid selle vahele jätta või ülekirjutada?"), + ("Quit", "Välju"), + ("Help", "Abi"), + ("Failed", "Ebaõnnestus"), + ("Succeeded", "Õnnestus"), + ("Someone turns on privacy mode, exit", "Keegi lülitab sisse privaatsusrežiimi, välju"), + ("Unsupported", "Mittetoetatud"), + ("Peer denied", "Partner keeldus"), + ("Please install plugins", "Palun paigalda pluginad"), + ("Peer exit", "Partner väljub"), + ("Failed to turn off", "Väljalülitamine ebaõnnestus"), + ("Turned off", "Väljalülitatud"), + ("Language", "Keel"), + ("Keep RustDesk background service", "Säilita RustDeski taustateenus"), + ("Ignore Battery Optimizations", "Ignoreeri akuoptimeerimisi"), + ("android_open_battery_optimizations_tip", "Kui soovid selle funktsiooni keelata, palun mine järgmisele RustDeski rakenduse seadete lehele, leia ja sisesta [Aku], eemalda linnuke valikult [Piiramata]"), + ("Start on boot", "Käivita seadme käivitamisel"), + ("Start the screen sharing service on boot, requires special permissions", "Käivita ekraanijagamise teenus seadme käivitamisel, nõuab eriõigusi"), + ("Connection not allowed", "Ühendus ei ole lubatud"), + ("Legacy mode", "Pärandrežiim"), + ("Map mode", "Kaardirežiim"), + ("Translate mode", "Tõlkerežiim"), + ("Use permanent password", "Kasuta püsiparooli"), + ("Use both passwords", "Kasuta mõlemat parooli"), + ("Set permanent password", "Seadista püsiparool"), + ("Enable remote restart", "Luba kaugtaaskäivitamine"), + ("Restart remote device", "Taaskäivita kaugseade"), + ("Are you sure you want to restart", "Kas oled kindel, et soovid taaskäivitada"), + ("Restarting remote device", "Kaugseadme taaskäivitamine"), + ("remote_restarting_tip", "Kaugseade taaskäivitub, palun sulge see sõnumikast ja ühendu mõne aja pärast uuesti püsiva parooliga."), + ("Copied", "Kopeeritud"), + ("Exit Fullscreen", "Välju täisekraanist"), + ("Fullscreen", "Täisekraan"), + ("Mobile Actions", "Mobiilitegevused"), + ("Select Monitor", "Vali kuvar"), + ("Control Actions", "Juhtimistegevused"), + ("Display Settings", "Kuvasätted"), + ("Ratio", "Kuvasuhe"), + ("Image Quality", "Pildikvaliteet"), + ("Scroll Style", "Kerimisstiil"), + ("Show Toolbar", "Kuva tööriistariba"), + ("Hide Toolbar", "Peida tööriistariba"), + ("Direct Connection", "Otseühendus"), + ("Relay Connection", "Releeühendus"), + ("Secure Connection", "Turvaline ühendus"), + ("Insecure Connection", "Ebaturvaline ühendus"), + ("Scale original", "Originaalskaala"), + ("Scale adaptive", "Kohanduv skaala"), + ("General", "Üldine"), + ("Security", "Turvalisus"), + ("Theme", "Teema"), + ("Dark Theme", "Tume teema"), + ("Light Theme", "Hele teema"), + ("Dark", "Tume"), + ("Light", "Hele"), + ("Follow System", "Järgi süsteemi"), + ("Enable hardware codec", "Luba riistvarakooder"), + ("Unlock Security Settings", "Lukusta lahti turvasätted"), + ("Enable audio", "Luba heli"), + ("Unlock Network Settings", "Lukusta lahti võrgusätted"), + ("Server", "Server"), + ("Direct IP Access", "Otsene IP-ligipääs"), + ("Proxy", "Proksi"), + ("Apply", "Rakenda"), + ("Disconnect all devices?", "Ühendad kõik seadmed lahti?"), + ("Clear", "Tühjenda"), + ("Audio Input Device", "Heli sisendseade"), + ("Use IP Whitelisting", "Kasuta IP-lubamisloendit"), + ("Network", "Võrk"), + ("Pin Toolbar", "Kinnita tööriistariba"), + ("Unpin Toolbar", "Eemalda tööriistariba kinnitus"), + ("Recording", "Salvestamine"), + ("Directory", "Kaust"), + ("Automatically record incoming sessions", "Salvesta alati sisenevad ühendused"), + ("Automatically record outgoing sessions", "Salvesta alati väljuvad ühendused"), + ("Change", "Muuda"), + ("Start session recording", "Alusta seansisalvestust"), + ("Stop session recording", "Peata seansisalvestus"), + ("Enable recording session", "Luba seansisalvestus"), + ("Enable LAN discovery", "Luba LAN-avastamine"), + ("Deny LAN discovery", "Keela LAN-avastamine"), + ("Write a message", "Kirjuta sõnum"), + ("Prompt", "Küsi"), + ("Please wait for confirmation of UAC...", "Palun oota UAC (kasutajakonto kontroll) kinnitust..."), + ("elevated_foreground_window_tip", "Kaugtöölaua praegune aken nõuab töötamiseks kõrgemaid õigusi, mistõttu ei saa see ajutiselt hiirt ja klaviatuuri kasutada. Võid kaugkasutajal paluda minimeerida praegune aken või klõpsata ühenduse haldamise aknas kõrgendatud loa nuppu. Selle probleemi vältimiseks on soovitatav kaugseadmesse tarkvara paigaldada."), + ("Disconnected", "Ühendus katkestatud"), + ("Other", "Muu"), + ("Confirm before closing multiple tabs", "Kinnita enne mitme vahekaardi sulgemist"), + ("Keyboard Settings", "Klaviatuurisätted"), + ("Full Access", "Täielik ligipääs"), + ("Screen Share", "Ekraanijagamine"), + ("ubuntu-21-04-required", "Wayland nõuab Ubuntu 21.04 või uuemat versiooni."), + ("wayland-requires-higher-linux-version", "Wayland nõuab Linuxi distributsiooni uuemat versiooni. Palun proovi X11 töölaual või muuda oma operatsioonisüsteemi."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Palun vali jagatav ekraan (tegutse partneri poolel)."), + ("Show RustDesk", "Kuva RustDesk"), + ("This PC", "See arvuti"), + ("or", "või"), + ("Elevate", "Tõsta"), + ("Zoom cursor", "Suumi kursorit"), + ("Accept sessions via password", "Aktsepteeri seansid parooli kaudu"), + ("Accept sessions via click", "Aktsepteeri seansid klõpsamise kaudu"), + ("Accept sessions via both", "Aktsepteeri seansid mõlema kaudu"), + ("Please wait for the remote side to accept your session request...", "Palun oota, kuni kaugpool aktsepteerib sinu seansitaotluse..."), + ("One-time Password", "Ühekordne parool"), + ("Use one-time password", "Kasuta ühekordset parooli"), + ("One-time password length", "Ühekordse parooli pikkus"), + ("Request access to your device", "Taotle oma seadmele juurdepääsu"), + ("Hide connection management window", "Peida ühenduse haldamise aken"), + ("hide_cm_tip", "Luba varjamine ainult siis, kui parooliga seansse võetakse vastu ning kasutatakse püsivat parooli."), + ("wayland_experiment_tip", "Waylandi tugi on katsetusjärgus, järelvalveta juurdepääsu vajadusel palun kasuta X11."), + ("Right click to select tabs", "Paremklõpsa vahekaartide valimiseks"), + ("Skipped", "Vahelejäetud"), + ("Add to address book", "Lisa aadressiraamatusse"), + ("Group", "Grupeeri"), + ("Search", "Otsi"), + ("Closed manually by web console", "Veebikonsooli kaudu käsitsi suletud"), + ("Local keyboard type", "Kohalik klaviatuuritüüp"), + ("Select local keyboard type", "Vali kohalik klaviatuuritüüp"), + ("software_render_tip", "Kui kasutad Linuxis Nvidia graafikakaarti ja kaugaken sulgub kohe pärast ühendamist, võib aidata üleminek avatud lähtekoodiga Nouveau draiverile ja valida tarkvaraline renderdamise. Vajalik on tarkvara taaskäivitamine."), + ("Always use software rendering", "Kasuta alati tarkvaralist renderdust"), + ("config_input", "Kaugtöölaua klaviatuuriga juhtimiseks pead andma RustDeskile \"sisendi jälgimise\" õigused."), + ("config_microphone", "Kaugelt rääkimiseks pead andma RustDeskile \"heli salvestamise\" õigused."), + ("request_elevation_tip", "Sa võid kõrgendatud õigusi taotleda ka siis, kui keegi on kaugpoolel."), + ("Wait", "Oota"), + ("Elevation Error", "Kõrgendatud õiguste viga"), + ("Ask the remote user for authentication", "Küsi kaugkasutajalt autentimist"), + ("Choose this if the remote account is administrator", "Vali see, kui kaugkonto on administraator"), + ("Transmit the username and password of administrator", "Edasta administraatori kasutajanimi ja parool"), + ("still_click_uac_tip", "Kaugkasutaja peab siiski ise vajutama käitatud RustDeski kasutajakonto kontrollis (UAC) OK-nuppu."), + ("Request Elevation", "Taotle kõrgendatud õigusi"), + ("wait_accept_uac_tip", "Palun oota, kuni kaugkasutaja nõustub UAC-dialoogiga (kasutajakonto kontroll)."), + ("Elevate successfully", "Kõrgendamine õnnestus"), + ("uppercase", "suurtäht"), + ("lowercase", "väiketäht"), + ("digit", "number"), + ("special character", "erimärk"), + ("length>=8", "pikkus>=8"), + ("Weak", "Nõrk"), + ("Medium", "Keskmine"), + ("Strong", "Tugev"), + ("Switch Sides", "Vaheta pooli"), + ("Please confirm if you want to share your desktop?", "Palun kinnita, kas soovid oma töölauda jagada?"), + ("Display", "Kuva"), + ("Default View Style", "Vaikimisi kuvastiil"), + ("Default Scroll Style", "Vaikimisi kerimisstiil"), + ("Default Image Quality", "Vaikimisi pildikvaliteet"), + ("Default Codec", "Vaikimisi koodek"), + ("Bitrate", "Bitikiirus"), + ("FPS", "FPS"), + ("Auto", "Automaatne"), + ("Other Default Options", "Teised vaikevalikud"), + ("Voice call", "Häälkõne"), + ("Text chat", "Tekstivestlus"), + ("Stop voice call", "Peata häälkõne"), + ("relay_hint_tip", "Otseühendust ei pruugi olla võimalik luua; võid proovida ühendust relee kaudu. Lisaks, kui soovid esimesel katsel kasutada releed, võid lisada ID-le järelliite \"/r\" või valida viimaste seansside kaardil - kui see on olemas - valiku \"Ühenda alati relee kaudu\"."), + ("Reconnect", "Ühenda uuesti"), + ("Codec", "Koodek"), + ("Resolution", "Resolutsioon"), + ("No transfers in progress", "Ülekandeid ei toimu"), + ("Set one-time password length", "Seadista ühekordse parooli pikkus"), + ("RDP Settings", "RDP seaded"), + ("Sort by", "Sorteeri"), + ("New Connection", "Uus ühendus"), + ("Restore", "Taasta"), + ("Minimize", "Minimeeri"), + ("Maximize", "Maksimeeri"), + ("Your Device", "Sinu seade"), + ("empty_recent_tip", "Ups, hiljutised seansid puuduvad!\nAeg uus planeerida."), + ("empty_favorite_tip", "Ei ole veel ühtegi lemmikpartnerit?\nLeia keegi, kellega suhelda ja lisa ta oma lemmikute hulka!"), + ("empty_lan_tip", "Oh ei, tundub, et me pole veel ühtegi partnerit avastanud."), + ("empty_address_book_tip", "Oh ei, tundub et sinu aadressiraamatus ei ole hetkel ühtegi partnerit."), + ("Empty Username", "Tühi kasutajanimi"), + ("Empty Password", "Tühi parool"), + ("Me", "Mina"), + ("identical_file_tip", "See fail on partneri omaga identne."), + ("show_monitors_tip", "Kuva kuvarid tööriistaribal"), + ("View Mode", "Kuvarežiim"), + ("login_linux_tip", "X-töölaua seansi lubamiseks pead sisse logima Linuxi kaugkontosse."), + ("verify_rustdesk_password_tip", "Kinnita RustDeski parool"), + ("remember_account_tip", "Jäta see konto meelde"), + ("os_account_desk_tip", "Seda kontot kasutatakse kaug-opsüsteemi sisselogimiseks ja töölaua seansi lubamiseks headless-režiimis."), + ("OS Account", "Opsüsteemi konto"), + ("another_user_login_title_tip", "Teine kasutaja on juba sisse logitud"), + ("another_user_login_text_tip", "Ühenda lahti"), + ("xorg_not_found_title_tip", "Xorg-i ei leitud"), + ("xorg_not_found_text_tip", "Palun paigalda Xorg"), + ("no_desktop_title_tip", "Töölaud pole saadaval"), + ("no_desktop_text_tip", "Palun paigalda GNOME Desktop"), + ("No need to elevate", "Kõrgendamine pole vajalik"), + ("System Sound", "Süsteemiheli"), + ("Default", "Vaikimisi"), + ("New RDP", "Uus RDP"), + ("Fingerprint", "Sõrmejälg"), + ("Copy Fingerprint", "Kopeeri sõrmejälg"), + ("no fingerprints", "Sõrmejäljed puuduvad"), + ("Select a peer", "Vali partner"), + ("Select peers", "Vali partnerid"), + ("Plugins", "Pluginad"), + ("Uninstall", "Desinstalli"), + ("Update", "Uuenda"), + ("Enable", "Luba"), + ("Disable", "Keela"), + ("Options", "Valikud"), + ("resolution_original_tip", "Originaalne eraldusvõime"), + ("resolution_fit_local_tip", "Ühita kohaliku eraldusvõimega"), + ("resolution_custom_tip", "Kohandatud eraldusvõime"), + ("Collapse toolbar", "Kompaktne tööriistariba"), + ("Accept and Elevate", "Luba kõrgendatud õigustega"), + ("accept_and_elevate_btn_tooltip", "Võta ühendus vastu ja anna kõrgemad UAC-õigused (kasutajakonto kontroll)."), + ("clipboard_wait_response_timeout_tip", "Koopia vastuse ootamisel tekkis ajalõpp."), + ("Incoming connection", "Sissetulev ühendus"), + ("Outgoing connection", "Väljuv ühendus"), + ("Exit", "Välju"), + ("Open", "Ava"), + ("logout_tip", "Kas soovid kindlasti välja logida?"), + ("Service", "Teenused"), + ("Start", "Käivita"), + ("Stop", "Peata"), + ("exceed_max_devices", "Oled saavutanud hallatavate seadmete maksimaalse arvu."), + ("Sync with recent sessions", "Sünkroniseeri viimaste seanssidega"), + ("Sort tags", "Sorteeri silte"), + ("Open connection in new tab", "Ava ühendus uuel vahekaardil"), + ("Move tab to new window", "Liiguta vahekaart uude aknasse"), + ("Can not be empty", "Ei tohi olla tühi"), + ("Already exists", "Juba eksisteerib"), + ("Change Password", "Vaheta parooli"), + ("Refresh Password", "Värskenda parool"), + ("ID", "ID"), + ("Grid View", "Ruudustikuvaade"), + ("List View", "Loendivaade"), + ("Select", "Vali"), + ("Toggle Tags", "Lülita silte"), + ("pull_ab_failed_tip", "Aadressiraamatu värskendamine ebaõnnestus"), + ("push_ab_failed_tip", "Aadressiraamatu sünkroonimine serveriga ebaõnnestus"), + ("synced_peer_readded_tip", "Hiljutistel seanssidel olnud seadmed sünkroonitakse tagasi aadressiraamatusse."), + ("Change Color", "Vaheta värvi"), + ("Primary Color", "Põhivärv"), + ("HSV Color", "HSV-värv"), + ("Installation Successful!", "Paigaldus oli edukas!"), + ("Installation failed!", "Paigaldus ebaõnnestus!"), + ("Reverse mouse wheel", "Pööra hiireratas"), + ("{} sessions", "{} seanssi"), + ("scam_title", "Võid olla KELMUSE ohver!"), + ("scam_text1", "Kui räägid telefoniga kellegagi, keda EI TUNNE ja EI USALDA, kes on palunud sul RustDeski kasutada ja teenus käivitada, ära jätka ning lõpeta kõne koheselt."), + ("scam_text2", "Tõenäoliselt on tegemist petturiga, kes üritab sinu raha või muid privaatseid andmeid varastada."), + ("Don't show again", "Ära kuva uuesti"), + ("I Agree", "Nõustun"), + ("Decline", "Keeldu"), + ("Timeout in minutes", "Ajalõpp minutites"), + ("auto_disconnect_option_tip", "Sissetulevate seansside automaatne sulgemine kasutaja mitteaktiivsuse korral"), + ("Connection failed due to inactivity", "Mitteaktiivsuse tõttu automaatselt lahti ühendatud"), + ("Check for software update on startup", "Kontrolli käivitusel tarkvarauuendust"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Palun täienda RustDesk Server Pro versioonile {} või uuem!"), + ("pull_group_failed_tip", "Grupi värskendamine ebaõnnestus"), + ("Filter by intersection", "Filtreeri ristumiste järgi"), + ("Remove wallpaper during incoming sessions", "Eemalda taustapilt sissetulevate seansside ajal"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "See kuvar on välja lülitatud, lülita esmasele kuvarile."), + ("No displays", "Kuvarid puuduvad"), + ("Open in new window", "Ava uues aknas"), + ("Show displays as individual windows", "Kuva kuvarid eraldi akendena"), + ("Use all my displays for the remote session", "Kasuta kõiki minu kuvarid kaugseansi jaoks"), + ("selinux_tip", "SELinux on su seadmes lubatud, mis võib RustDeskil takistada juhitud poolel toimimist."), + ("Change view", "Muuda vaadet"), + ("Big tiles", "Suured plaadid"), + ("Small tiles", "Väikesed plaadid"), + ("List", "Loend"), + ("Virtual display", "Virtuaalne kuvar"), + ("Plug out all", "Eemalda kõik"), + ("True color (4:4:4)", "Tõeline värv (4:4:4)"), + ("Enable blocking user input", "Luba kasutaja sisendi blokeerimine"), + ("id_input_tip", "Võid sisestada ID, otsese IP või domeeni koos pordiga (:).\nKui soovid juurdepääsu seadmele mõnes teises serveris, lisa palun serveri aadress (@?key=), näiteks,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nKui soovid juurdepääsu seadmele avalikus serveris, sisesta \"@public\", avaliku serveri puhul ei ole võtit vaja."), + ("privacy_mode_impl_mag_tip", "Režiim 1"), + ("privacy_mode_impl_virtual_display_tip", "Režiim 2"), + ("Enter privacy mode", "Sisene privaatsusrežiimi"), + ("Exit privacy mode", "Välju privaatsusrežiimist"), + ("idd_not_support_under_win10_2004_tip", "Kaudse kuvari draiver ei ole toetatud. Vajalik on Windows 10, versioon 2004 või uuem."), + ("input_source_1_tip", "Sisendallikas 1"), + ("input_source_2_tip", "Sisendallikas 2"), + ("Swap control-command key", "Vaheta klahvid Control ja Command"), + ("swap-left-right-mouse", "Vaheta vasak ja parem hiirenupp"), + ("2FA code", "2FA kood"), + ("More", "Rohkem"), + ("enable-2fa-title", "Luba kaheastmeline autentimine"), + ("enable-2fa-desc", "Palun seadista oma autentimisrakendus nüüd. Sa saad kasutada autentimisrakendust nagu Authy, Microsoft või Google Authenticator oma telefonis või lauaarvutis.\n\nSkaneeri QR-kood oma rakendusega ja sisesta kood, mida sinu rakendus näitab, et lubada kaheastmeline autentimine."), + ("wrong-2fa-code", "Koodi ei saa kinnitada. Kontrolli, et kood ja kohalikud ajaseaded oleksid õiged."), + ("enter-2fa-title", "Kaheastmeline autentimine"), + ("Email verification code must be 6 characters.", "E-posti kinnituskood peab olema 6 tähemärki."), + ("2FA code must be 6 digits.", "2FA kood peab olema 6 numbrit."), + ("Multiple Windows sessions found", "Leitud mitu Windowsi seanssi"), + ("Please select the session you want to connect to", "Palun vali seanss, millega soovid ühendada"), + ("powered_by_me", "Põhineb RustDeskil"), + ("outgoing_only_desk_tip", "See on kohandatud versioon.\nSa saad ühenduda teiste seadmetega, kuid teised seadmed ei saa sinu seadmega ühenduda."), + ("preset_password_warning", "See kohandatud versioon sisaldab eelmääratud parooli. Igaüks, kes seda parooli teab, võib saada täieliku kontrolli sinu seadme üle. Kui sa ei oodanud seda, desinstalli tarkvara kohe."), + ("Security Alert", "Turvahoiatus"), + ("My address book", "Minu aadressiraamat"), + ("Personal", "Isiklik"), + ("Owner", "Omanik"), + ("Set shared password", "Seadista jagatud parool"), + ("Exist in", "Eksisteerib"), + ("Read-only", "Ainult lugemiseks"), + ("Read/Write", "Lugemiseks/Kirjutamiseks"), + ("Full Control", "Täielik kontroll"), + ("share_warning_tip", "Ülalolevad väljad on teistele jagatud ja nähtavad."), + ("Everyone", "Igaüks"), + ("ab_web_console_tip", "Rohkem leiad veebikonsoolist"), + ("allow-only-conn-window-open-tip", "Luba ühendus ainult siis, kui RustDeski aken on avatud."), + ("no_need_privacy_mode_no_physical_displays_tip", "Füüsilisi ekraane pole, privaatsusrežiimi kasutamine pole vajalik."), + ("Follow remote cursor", "Jälgi kaugkursorit"), + ("Follow remote window focus", "Jälgi kaugakna fookust"), + ("default_proxy_tip", "Vaikimisi protokoll ja port on Socks5 ja 1080."), + ("no_audio_input_device_tip", "Heli sisendseadet ei leitud."), + ("Incoming", "Sissetulev"), + ("Outgoing", "Väljuv"), + ("Clear Wayland screen selection", "Tühjenda Waylandi ekraanivalik"), + ("clear_Wayland_screen_selection_tip", "Pärast ekraanivaliku tühistamist saad uuesti jagatava ekraani valida."), + ("confirm_clear_Wayland_screen_selection_tip", "Kas oled kindel, et soovid Waylandi ekraanivaliku tühistada?"), + ("android_new_voice_call_tip", "Uus häälkõne taotlus on saadud. Vastu võtmisel lülitub heli häälkommunikatsioonile."), + ("texture_render_tip", "Kasuta tekstuurirenderdust, et muuta pildid sujuvamaks. Renderdusprobleemide esinemisel võid proovida selle valiku keelata."), + ("Use texture rendering", "Kasuta tekstuurirenderdust"), + ("Floating window", "Ujuv aken"), + ("floating_window_tip", "See aitab säilitada RustDeski taustateenust."), + ("Keep screen on", "Hoia ekraan sees"), + ("Never", "Mitte kunagi"), + ("During controlled", "Juhtimise ajal"), + ("During service is on", "Teenuse käitamisel"), + ("Capture screen using DirectX", "Salvesta ekraani DirectX abil"), + ("Back", "Tagasi"), + ("Apps", "Rakendused"), + ("Volume up", "Heli üles"), + ("Volume down", "Heli alla"), + ("Power", "Toide"), + ("Telegram bot", "Telegrami bot"), + ("enable-bot-tip", "Kui lubad selle funktsiooni, saad 2FA koodi oma botilt. See võib töötada ka ühenduse teavitusena."), + ("enable-bot-desc", "1. Ava vestlus kasutajaga @BotFather.\n2. Saada käsklus \"/newbot\". Pärast selle sammu lõpetamist saad tokeni.\n3. Alusta vestlust oma uue loodud botiga. Saada sõnum, mis algab kaldkriipsuga (\"/\") nagu \"/hello\", et see aktiveerida.\n"), + ("cancel-2fa-confirm-tip", "Kas oled kindel, et soovid 2FA tühistada?"), + ("cancel-bot-confirm-tip", "Kas oled kindel, et soovid Telegrami boti tühistada?"), + ("About RustDesk", "RustDeski teave"), + ("Send clipboard keystrokes", "Saada lõikelaua klahvivajutused"), + ("network_error_tip", "Palun kontrolli oma võrguühendust ja seejärel klõpsa nuppu \"Proovi uuesti\"."), + ("Unlock with PIN", "Ava PIN-koodiga"), + ("Requires at least {} characters", "Nõuab vähemalt {} tähemärki"), + ("Wrong PIN", "Vale PIN"), + ("Set PIN", "Seadista PIN"), + ("Enable trusted devices", "Luba usaldusväärsed seadmed"), + ("Manage trusted devices", "Halda usaldusväärseid seadmeid"), + ("Platform", "Platvorm"), + ("Days remaining", "Päevi jäänud"), + ("enable-trusted-devices-tip", "Jäta usaldatud seadmetes 2FA kinnitamine vahele"), + ("Parent directory", "Ülemkaust"), + ("Resume", "Jätka"), + ("Invalid file name", "Kehtetu failinimi"), + ("one-way-file-transfer-tip", "Ühesuunaline failiedastus on lubatud juhitataval poolel."), + ("Authentication Required", "Autentimine nõutud"), + ("Authenticate", "Autendi"), + ("web_id_input_tip", "Saad sisestada sama serveri ID, otse IP-juurdepääs ei ole veebikliendis toetatud.\nKui soovid seadmele teises serveris ligi pääseda, palun lisa serveri aadress (@?key=), näiteks,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nKui soovid seadmele avalikus serveris ligi pääseda, palun sisesta \"@public\"; võti ei ole avaliku serveri jaoks vajalik."), + ("Download", "Laadi alla"), + ("Upload folder", "Laadi kaust üles"), + ("Upload files", "Laadi failid üles"), + ("Clipboard is synchronized", "Lõikelaud on sünkroonitud"), + ("Update client clipboard", "Uuenda kliendi lõikelauda"), + ("Untagged", "Sildistamata"), + ("new-version-of-{}-tip", "Saadaval on {} uus versioon"), + ("Accessible devices", "Ligipääsetavad seadmed"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Täiendage RustDeski klient kaugküljel versioonile {} või uuemale!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Vaata kaamerat"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Jätka koos {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/eu.rs b/vendor/rustdesk/src/lang/eu.rs new file mode 100644 index 0000000..aaf8a8b --- /dev/null +++ b/vendor/rustdesk/src/lang/eu.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Egoera"), + ("Your Desktop", "Zure mahaigaina"), + ("desk_tip", "Mahaigainera zure ID eta pasahitzarekin sartu zaitezke"), + ("Password", "Pasahitza"), + ("Ready", "Prest"), + ("Established", "Ezarrita"), + ("connecting_status", "RustDesk sarera konektatzen..."), + ("Enable service", "Gaitu zerbitzua"), + ("Start service", "Hasi zerbitzua"), + ("Service is running", "Zerbitzua martxan dago"), + ("Service is not running", "Zerbitzua ez dago martxan"), + ("not_ready_status", "Ez dago prest. Mesedez, egiaztatu zure konexioa"), + ("Control Remote Desktop", "Kontrolatu urruneko mahaigaina"), + ("Transfer file", "Transferitu fitxategia"), + ("Connect", "Konektatu"), + ("Recent sessions", "Azken saioak"), + ("Address book", "Helbide-liburua"), + ("Confirmation", "Berrespena"), + ("TCP tunneling", "TCP tunela"), + ("Remove", "Kendu"), + ("Refresh random password", "Freskatu ausazko pasahitza"), + ("Set your own password", "Ezarri zure pasahitza"), + ("Enable keyboard/mouse", "Gaitu teklatua/sagua"), + ("Enable clipboard", "Gaitu arbela"), + ("Enable file transfer", "Gaitu fitxategien transferentzia"), + ("Enable TCP tunneling", "Gaitu TCP tunela"), + ("IP Whitelisting", "Onartutako IP helbideak"), + ("ID/Relay Server", "ID/Relay zerbitzaria"), + ("Import server config", "Inportatu zerbitzariaren konfigurazioa"), + ("Export Server Config", "Esportatu zerbitzariaren konfigurazioa"), + ("Import server configuration successfully", "Zerbitzariaren konfigurazioa ondo inportatu da"), + ("Export server configuration successfully", "Zerbitzariaren konfigurazioa ondo esportatu da"), + ("Invalid server configuration", "Zerbitzariaren konfigurazioa baliogabea da"), + ("Clipboard is empty", "Arbela hutsik dago"), + ("Stop service", "Gelditu zerbitzua"), + ("Change ID", "Aldatu IDa"), + ("Your new ID", "Zure ID berria"), + ("length %min% to %max%", "%min%(e)tik %max% arteko luzera"), + ("starts with a letter", "hizki batekin hasten da"), + ("allowed characters", "onartutako karaktereak"), + ("id_change_tip", "Soilik a-z, A-Z, 0-9, - (dash) eta _ (barra baxua) karaktereak daude onartuta. Lehen hizkia a-z, A-Z izan behar da. Luzera 6 eta 16 artekoa izan behar da."), + ("Website", "Webgunea"), + ("About", "Honi buruz"), + ("Slogan_tip", "Bihotzez eginda mundu kaotiko honetan!"), + ("Privacy Statement", "Pribatutasun-politika"), + ("Mute", "Mututu"), + ("Build Date", "Konpilazio-data"), + ("Version", "Bertsioa"), + ("Home", "Hasiera"), + ("Audio Input", "Audio sarrera"), + ("Enhancements", "Hobespenak"), + ("Hardware Codec", "Hardware kodeka"), + ("Adaptive bitrate", "Bit-emari moldagarria"), + ("ID Server", "ID zerbitzaria"), + ("Relay Server", "Relay zerbitzaria"), + ("API Server", "API zerbitzaria"), + ("invalid_http", "http:// edo https://-rekin hasi behar da"), + ("Invalid IP", "IP baliogabea"), + ("Invalid format", "Formatu baliogabea"), + ("server_not_support", "Oraindik ez dago zerbitzariarengatik onartuta"), + ("Not available", "Ez dago eskuragarri"), + ("Too frequent", "Sarriegia"), + ("Cancel", "Utzi"), + ("Skip", "Saltatu"), + ("Close", "Itxi"), + ("Retry", "Saiatu berriro"), + ("OK", "Ondo"), + ("Password Required", "Pasahitza beharrezkoa da"), + ("Please enter your password", "Mesedez, sartu zure pasahitza"), + ("Remember password", "Gogoratu pasahitza"), + ("Wrong Password", "Pasahitz okerra"), + ("Do you want to enter again?", "Berriro sartu nahi zara?"), + ("Connection Error", "Konexio errorea"), + ("Error", "Errorea"), + ("Reset by the peer", "Konexioa parekidearengatik berrezarrita"), + ("Connecting...", "Konektatzen..."), + ("Connection in progress. Please wait.", "Konexioa abian da. Itxaron mesedez."), + ("Please try 1 minute later", "Mesedez, saiatu berrito minutu bat pasata"), + ("Login Error", "Saio-hasiera errorea"), + ("Successful", "Arrakastatsua"), + ("Connected, waiting for image...", "Konektatuta, irudiaren zain..."), + ("Name", "Izena"), + ("Type", "Mota"), + ("Modified", "Aldatua"), + ("Size", "Tamaina"), + ("Show Hidden Files", "Erakutsi ezkutuko fitxategiak"), + ("Receive", "Jaso"), + ("Send", "Bidali"), + ("Refresh File", "Freskatu fitxategia"), + ("Local", "Lokala"), + ("Remote", "Urrunekoa"), + ("Remote Computer", "Urruneko ordenagailua"), + ("Local Computer", "Ordenagailu lokala"), + ("Confirm Delete", "Berretsi ezabapena"), + ("Delete", "Ezabatu"), + ("Properties", "Ezaugarriak"), + ("Multi Select", "Multi-hautapena"), + ("Select All", "Hautatu guztiak"), + ("Unselect All", "Desautatu denak"), + ("Empty Directory", "Karpeta hutsa"), + ("Not an empty directory", "Ez da karpeta huts bat"), + ("Are you sure you want to delete this file?", "Ziur zaude fitxategi hau ezabatu nahi duzula?"), + ("Are you sure you want to delete this empty directory?", "Ziur zaude karpeta huts hau ezabatu nahi duzula?"), + ("Are you sure you want to delete the file of this directory?", "Ziur zaude karpeta honen fitxategia ezabatu nahi duzula?"), + ("Do this for all conflicts", "Egin hau gatazka guztietarako"), + ("This is irreversible!", "Hau ezin da atzera bueltatu!"), + ("Deleting", "Ezabatzen"), + ("files", "fitxategiak"), + ("Waiting", "Zain"), + ("Finished", "Bukatuta"), + ("Speed", "Abiadura"), + ("Custom Image Quality", "Irudi kalitate pertsonalizatua"), + ("Privacy mode", "Pribatutasun modua"), + ("Block user input", "Blokeatu erabiltzailearen sarrera"), + ("Unblock user input", "Desblokeatu erabiltzailearen sarrera"), + ("Adjust Window", "Doitu leihoa"), + ("Original", "Originala"), + ("Shrink", "Txikitu"), + ("Stretch", "Luzatu"), + ("Scrollbar", "Korritze-barra"), + ("ScrollAuto", "Korritze automatikoa"), + ("Good image quality", "Irudi kalitate ona"), + ("Balanced", "Orekatua"), + ("Optimize reaction time", "Optimizatu erreakzio-denbiora"), + ("Custom", "Pertsonalizatua"), + ("Show remote cursor", "Erakutsi urruneko kurtsorea"), + ("Show quality monitor", "Erakutsi kalitate monitorea"), + ("Disable clipboard", "Desgaitu arbela"), + ("Lock after session end", "Blokeatu sesioa amaitu ostean"), + ("Insert Ctrl + Alt + Del", "Sartu Ctrl + Alt + Del"), + ("Insert Lock", "Sarrera-blokeoa"), + ("Refresh", "Freskatu"), + ("ID does not exist", "IDa ez da existitzen"), + ("Failed to connect to rendezvous server", "Topaketa zerbitzarira konektatzeak huts egin du"), + ("Please try later", "Mesedez, saiatu berriro geroago"), + ("Remote desktop is offline", "Urruneko mahaigaina lineaz kanpo dago"), + ("Key mismatch", "Gakoak ez datoz bat"), + ("Timeout", "Denbora-muga"), + ("Failed to connect to relay server", "Igorpen zerbitzarira konektatzeak huts egin du"), + ("Failed to connect via rendezvous server", "Topaketa zerbitzariaren bidez konektatzeak huts egin du"), + ("Failed to connect via relay server", "Igorpen zerbitzariaren bidez konektatzeak huts egin du"), + ("Failed to make direct connection to remote desktop", "Urruneko mahaigainera zuzeneko konexio bat ezartzeak huts egin du"), + ("Set Password", "Ezarri pasahitza"), + ("OS Password", "Sistema eragilearen pasahitza"), + ("install_tip", "Erabiltzaile Kontuen Kontrolarengatik, RustDesk ezin du ondo funtzionatu urruneko mahaigainean. EKK saihesteko, mesedez, egin klik azpiko botoian RustDesk sistema mailan instalatzeko."), + ("Click to upgrade", "Egin klik bertsio-berritzeko"), + ("Configure", "Konfiguratu"), + ("config_acc", "Zure mahaigaina urrunetik kontrolatzeko, RustDesk-i \"Irisgarritasuna\" baimenak eman behar dituzu."), + ("config_screen", "Zure mahaigaina kanpotik kontrolatzeko, RustDesk-i \"Pantaila grabatu\" baimena eman behar duzu."), + ("Installing ...", "Instalantzen..."), + ("Install", "Instalatu"), + ("Installation", "Instalazioa"), + ("Installation Path", "Instalazio bide-izena"), + ("Create start menu shortcuts", "Sortu hasiera-menuko lasterbideak"), + ("Create desktop icon", "Sortu mahaigaineko ikonoa"), + ("agreement_tip", "Instalazioa hastean, lizentzia-kontratua onartzen duzu."), + ("Accept and Install", "Onartu eta instalatu"), + ("End-user license agreement", "Azken erabiltzailearen lizentzia akordioa"), + ("Generating ...", "Sortzen..."), + ("Your installation is lower version.", "Zure instalazioak bertsio zaharragoa du."), + ("not_close_tcp_tip", "Ez itxi leiho hau tunela erabili bitartean"), + ("Listening ...", "Entzuten..."), + ("Remote Host", "Urruneko ostalaria"), + ("Remote Port", "Urruneko ataka"), + ("Action", "Ekintza"), + ("Add", "Gehitu"), + ("Local Port", "Ataka lokala"), + ("Local Address", "Helbide lokala"), + ("Change Local Port", "Aldatu ataka lokala"), + ("setup_server_tip", "Konexio azkarragorako, konfiguratu zure zerbitzaria"), + ("Too short, at least 6 characters.", "Laburregia, 6 karaktere gutxienez."), + ("The confirmation is not identical.", "Berrespena ez dator bat."), + ("Permissions", "Baimenak"), + ("Accept", "Onartu"), + ("Dismiss", "Ezeztatu"), + ("Disconnect", "Deskonektatu"), + ("Enable file copy and paste", "Gaitu fitxategien kopiatze eta itsastea"), + ("Connected", "Konektatuta"), + ("Direct and encrypted connection", "Zifratutako konexio zuzena"), + ("Relayed and encrypted connection", "Zifratutako konexio igorria"), + ("Direct and unencrypted connection", "Zifratu gabeko konexio zuzena"), + ("Relayed and unencrypted connection", "Zifratu gabeko konexio igorria"), + ("Enter Remote ID", "Sartu urruneko IDa"), + ("Enter your password", "Sartu zure pasahitza"), + ("Logging in...", "Saioa hasten..."), + ("Enable RDP session sharing", "Gaitu RDP saio-partekatzea"), + ("Auto Login", "Saio-haste automatikoa"), + ("Enable direct IP access", "Gaitu IP sarbide zuzena"), + ("Rename", "Berrizendatu"), + ("Space", "Zuriunea"), + ("Create desktop shortcut", "Sortu mahaigaineko lasterbidea"), + ("Change Path", "Aldatu bide-izena"), + ("Create Folder", "Sortu karpeta"), + ("Please enter the folder name", "Mesedez, sartu karpetaren izena"), + ("Fix it", "Konpondu"), + ("Warning", "Oharra"), + ("Login screen using Wayland is not supported", "Saio-hasiera Wayland erabilita ez dago onartuta"), + ("Reboot required", "Berrabiaraztea beharrezkoa"), + ("Unsupported display server", "Bistaratze-zerbitzaria ez da bateragarria"), + ("x11 expected", "x11 espero zen"), + ("Port", "Ataka"), + ("Settings", "Ezarpenak"), + ("Username", "Erabiltzaile-izena"), + ("Invalid port", "Ataka baliogabea"), + ("Closed manually by the peer", "Parekideak konexioa eskuz itxi du"), + ("Enable remote configuration modification", "Gaitu urruneko konfigurazio-aldaketak"), + ("Run without install", "Exekutatu instalatu gabe"), + ("Connect via relay", "Konektatu igorpen-zerbitzari batetik"), + ("Always connect via relay", "Konektatu beti igorpen-zerbitzari batetik"), + ("whitelist_tip", "Baimendutako IPak soilik konektatu daitezke mahaigain honetara"), + ("Login", "Saio-hasiera"), + ("Verify", "Egiaztatu"), + ("Remember me", "Gogoratu"), + ("Trust this device", "Gailu honetaz fidatu"), + ("Verification code", "Egiaztapen-kodea"), + ("verification_tip", "Egiaztapen-kode bat bidali da erregistratutako helbide elektronikora. Sartu egiaztapen-kodea saio-hasiera jarraitzeko."), + ("Logout", "Saioa bukatu"), + ("Tags", "Etiketak"), + ("Search ID", "Bilatu IDa"), + ("whitelist_sep", "Koma, puntu koma, zuriune edo lerro berriengatik banatuta"), + ("Add ID", "Gehitu IDa"), + ("Add Tag", "Gehitu etiketa"), + ("Unselect all tags", "Desautatu etiketa guztiak"), + ("Network error", "Sare-errorea"), + ("Username missed", "Erabiltzaile-izena ahaztu duzu"), + ("Password missed", "Pasahitza ahaztu duzu"), + ("Wrong credentials", "Kredentzial baliogabeak"), + ("The verification code is incorrect or has expired", "Egiaztapen-kodea baliogabe edo iraungitua da"), + ("Edit Tag", "Editatu etiketa"), + ("Forget Password", "Ahaztu pasahitza"), + ("Favorites", "Gogokoenak"), + ("Add to Favorites", "Gehitu gogokoenetara"), + ("Remove from Favorites", "Kendu gpgokoenetatik"), + ("Empty", "Hutsik"), + ("Invalid folder name", "Karpeta-izen baliogabea"), + ("Socks5 Proxy", "Socks5 proxia"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) proxia"), + ("Discovered", "Aurkituta"), + ("install_daemon_tip", "Ordenagailua pizterakoan hasteko, sistemaren zerbitzua instalatu behar duzu."), + ("Remote ID", "Urruneko IDa"), + ("Paste", "Itsatsi"), + ("Paste here?", "Itsatsi hemen?"), + ("Are you sure to close the connection?", "Ziur zaude konexioa itxi nahi duzula?"), + ("Download new version", "Deskargatu bertsio berria"), + ("Touch mode", "Ukipen modua"), + ("Mouse mode", "Sagu modua"), + ("One-Finger Tap", "Hatz bakarreko ukipena"), + ("Left Mouse", "Ezkerreko botoia"), + ("One-Long Tap", "Ukipen luzea"), + ("Two-Finger Tap", "Bi hatzeko ukipena"), + ("Right Mouse", "Eskuineko botoia"), + ("One-Finger Move", "Hatz bakarreko mugimendua"), + ("Double Tap & Move", "Bi aldiz ukitu eta mugitu"), + ("Mouse Drag", "Saguarekin arrastatu"), + ("Three-Finger vertically", "Hiru hatz bertikalki"), + ("Mouse Wheel", "Saguaren gurpila"), + ("Two-Finger Move", "Bi hatzeko mugimendua"), + ("Canvas Move", "Oihal-mugimendua"), + ("Pinch to Zoom", "Atximurkatu zoom egiteko"), + ("Canvas Zoom", "Oihal-zooma"), + ("Reset canvas", "Berrezarri oihala"), + ("No permission of file transfer", "Ez duzu baimenik fitxategiak transferitzeko"), + ("Note", "Nota"), + ("Connection", "Konexioa"), + ("Share screen", "Partekatu pantaila"), + ("Chat", "Txata"), + ("Total", "Guztira"), + ("items", "elementuak"), + ("Selected", "hautatuta"), + ("Screen Capture", "Pantaila-grabazioa"), + ("Input Control", "Sarrera-kontrola"), + ("Audio Capture", "Audio-grabazioa"), + ("Do you accept?", "Onartzen al duzu?"), + ("Open System Setting", "Ireki sistemaren ezarpenak"), + ("How to get Android input permission?", "Nola lortu dezaket Android sarrera-baimena?"), + ("android_input_permission_tip1", "Urruneko gailu batek zure Android gailua saguaren edo ukipenaren bidez kontrolatzeko, RustDesk \"Irisgarritasuna\" zerbitzua erabiltzeko baimena eman behar diozu."), + ("android_input_permission_tip2", "Joan hurrengo irekiko den sistemaren konfigurazio orrira, bilatu eta sartu [Instalatutako zerbitzuak], aktibatu [RustDesk Input] zerbitzua."), + ("android_new_connection_tip", "Kontrol-eskaera berri bat jaso da uneko gailuarentzat."), + ("android_service_will_start_tip", "Pantaila-argazkia gaitzen baduzu, zerbitzua automatikoki abiaraziko da, eta beste gailu batzuek gailu honetatik konexioa eskatzeko aukera izango dute."), + ("android_stop_service_tip", "Zerbitzua ixteak ezarritako konexio guztiak automatikoki itxiko ditu."), + ("android_version_audio_tip", "Uneko Android bertsioak ez du audioa grabatzea onartzen; mesedez, eguneratu Android 10 edo berrira."), + ("android_start_service_tip", "Sakatu [Hasi zerbitzua] edo gaitu [Pantaila-grabazioa] baimena pantaila-partekatzea hasteko."), + ("android_permission_may_not_change_tip", "Ezarritako konexioen baimenak ez dira aldatuko berriro konektatu arte."), + ("Account", "Kontua"), + ("Overwrite", "Berridatzi"), + ("This file exists, skip or overwrite this file?", "Fitxategi hau existitzen da dagoeneko, saltatu edo berridatzi nahi duzu?"), + ("Quit", "Irten"), + ("Help", "Laguntza"), + ("Failed", "Huts egin du"), + ("Succeeded", "Arrakastatsua izan da"), + ("Someone turns on privacy mode, exit", "Norbaitek pribatutasun modua hasten du, irten"), + ("Unsupported", "Ez da onartzen"), + ("Peer denied", "Parekidea ukatuta"), + ("Please install plugins", "Mesedez, instalatu plugin hauek"), + ("Peer exit", "Parekidea irten da"), + ("Failed to turn off", "Itzaltzeak huts egin du"), + ("Turned off", "Itzalita"), + ("Language", "Hizkuntza"), + ("Keep RustDesk background service", "Mantendu RustDesk atzeko planoko zerbitzu bezala"), + ("Ignore Battery Optimizations", "Ezikusi bateria optimizazioak"), + ("android_open_battery_optimizations_tip", "Ezaugarri hau desgaitu nahi baduzu, joan zaitez RustDesk aplikazioaren ezarpen orrira, bilatu eta saltu [Bateria] orrira eta kendu [Mugarik gabe]"), + ("Start on boot", "Hasi abiaraztean"), + ("Start the screen sharing service on boot, requires special permissions", "Hasi pantaila-partekatze zerbitzua abiaraztean, baimen bereziak behar ditu"), + ("Connection not allowed", "Konexioa ez dago baimenduta"), + ("Legacy mode", "Legatu-modua"), + ("Map mode", "Mapa modua"), + ("Translate mode", "Itzultze modua"), + ("Use permanent password", "Erabili betirako pasahitza"), + ("Use both passwords", "Erabili bi pasahitzak"), + ("Set permanent password", "Ezarri betirako pasahitza"), + ("Enable remote restart", "Gaitu urruneko berrabiaraztea"), + ("Restart remote device", "Berrabiarazi urruneko gailua"), + ("Are you sure you want to restart", "Ziur zaude berrabiarazi nahi duzula?"), + ("Restarting remote device", "Urruneko gailua berrabiarazten"), + ("remote_restarting_tip", "Urruneko gailua berrabiarazten dabil. Mesedez, itxi mezu hau eta konektatu betirako pasahitzarekin une bat pasa ostean."), + ("Copied", "Kopiatuta"), + ("Exit Fullscreen", "Irten pantaila osotik"), + ("Fullscreen", "Pantaila osoa"), + ("Mobile Actions", "Mugikor-ekintzak"), + ("Select Monitor", "Hautatu monitorea"), + ("Control Actions", "Kontrol-ekintzak"), + ("Display Settings", "Pantailaren ezarpenak"), + ("Ratio", "Erlazioa"), + ("Image Quality", "Irudiaren kalitatea"), + ("Scroll Style", "Korritze estiloa"), + ("Show Toolbar", "Erakutsi tresna-barra"), + ("Hide Toolbar", "Ezkutatu tresna-barra"), + ("Direct Connection", "Konexio zuzena"), + ("Relay Connection", "Konexio igorria"), + ("Secure Connection", "Konexio segurua"), + ("Insecure Connection", "Konexio ez-segurua"), + ("Scale original", "Jatorrizko eskala"), + ("Scale adaptive", "Eskala moldagarria"), + ("General", "Orokorra"), + ("Security", "Segurtasuna"), + ("Theme", "Itxura"), + ("Dark Theme", "Itxura iluna"), + ("Light Theme", "Itxura argia"), + ("Dark", "Iluna"), + ("Light", "Argia"), + ("Follow System", "Jarraitu sistemaren itxura"), + ("Enable hardware codec", "Gaitu hardware kodeka"), + ("Unlock Security Settings", "Desblokeatu segurtasun ezarpenak"), + ("Enable audio", "Gaitu audioa"), + ("Unlock Network Settings", "Desblokeatu sare-ezarpenak"), + ("Server", "Zerbitzaria"), + ("Direct IP Access", "IP sarbide zuzena"), + ("Proxy", "Proxia"), + ("Apply", "Aplikatu"), + ("Disconnect all devices?", "Deskonektatu gailu guztiak?"), + ("Clear", "Garbitu"), + ("Audio Input Device", "Audio sarrera gailua"), + ("Use IP Whitelisting", "Erabili IP onartuen zerrenda"), + ("Network", "Sarea"), + ("Pin Toolbar", "Ainguratu tresna-barra"), + ("Unpin Toolbar", "Aingura kendu tresna-barrari"), + ("Recording", "Grabatzen"), + ("Directory", "Direktorioa"), + ("Automatically record incoming sessions", "Automatikoki grabatu sarrerako saioak"), + ("Automatically record outgoing sessions", ""), + ("Change", "Aldatu"), + ("Start session recording", "Hasi saioaren grabaketa"), + ("Stop session recording", "Gelditu saioaren grabaketa"), + ("Enable recording session", "Gaitu saioen grabaketa"), + ("Enable LAN discovery", "Gaitu LAN ezagutza"), + ("Deny LAN discovery", "Ukatu LAN ezagutza"), + ("Write a message", "Idatzi mezu bat"), + ("Prompt", "Testu-gonbita"), + ("Please wait for confirmation of UAC...", "Mesedez, itxaron UAC berrespenari"), + ("elevated_foreground_window_tip", "Mahaigaineko uneko urruneko leihoak goi mailako baimenak behar ditu funtzionatzeko. Beraz, ezin duzu sagua eta teklatua erabili aldi baterako. Urruneko erabiltzaileari uneko leihoa minizatu edo kudeatze-leihoan maila igotzeko botoia erabili dezan eskatu diezaiokezu. Arazo hau saihesteko, programa instalatzea gomendatzen da urruneko gailuan."), + ("Disconnected", "Deskonektatuta"), + ("Other", "Besteak"), + ("Confirm before closing multiple tabs", "Berretsi fitxa ugari itxi baino lehen"), + ("Keyboard Settings", "Teklatuaren ezarpenak"), + ("Full Access", "Sarbide osoa"), + ("Screen Share", "Pantailaren partekatzea"), + ("ubuntu-21-04-required", "Wayland Ubuntu 21.04 edo bertsio berriagoa behar du."), + ("wayland-requires-higher-linux-version", "Wayland-ek linux banaketa berriago bat behar du. Saiatu X11 mahaigainarekin edo aldatu zure sistema eragilea."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Ikusi"), + ("Please Select the screen to be shared(Operate on the peer side).", "Mesedez, hautatu partekatuko den pantaila (Kudeatu parekidearen aldean)"), + ("Show RustDesk", "Erakutsi RustDesk"), + ("This PC", "PC hau"), + ("or", "edo"), + ("Elevate", "Igo maila"), + ("Zoom cursor", "Handitu kurtsorea"), + ("Accept sessions via password", "Onartu saioak pasahitzaren bidez"), + ("Accept sessions via click", "Onartu saioak klikaren bidez"), + ("Accept sessions via both", "Onatu saioak bientzako"), + ("Please wait for the remote side to accept your session request...", "Mesedez, itxaron urruneko aldeak zure saio eskaera onartu dezan..."), + ("One-time Password", "Aldi bateko pasahitza"), + ("Use one-time password", "Erabili aldi baterako pasahitza"), + ("One-time password length", "Aldi baterako pasahitzaren luzera"), + ("Request access to your device", "Eskatu sarrera zure gailura"), + ("Hide connection management window", "Ezkutatu konexio kudeatze leihoa"), + ("hide_cm_tip", "Utzi ezkutatzen saioak pasahitzarekin onartzen badira eta pasahitza betirakoa bada"), + ("wayland_experiment_tip", "Wayland euskarria oraindik fase esperimentalean dago, mesedez, erabili X11 arretarik gabeko sarrera behar baduzu."), + ("Right click to select tabs", "Eskuineko klika fitxak hautatzeko"), + ("Skipped", "Saltatuta"), + ("Add to address book", "Gehitu helbide-liburura"), + ("Group", "Taldea"), + ("Search", "Bilatu"), + ("Closed manually by web console", "Web kontsolarengatik eskuz itxita"), + ("Local keyboard type", "Teklatu mota lokala"), + ("Select local keyboard type", "Hautatu teklatu mota lokala"), + ("software_render_tip", "Nvidia bideo-txartela baduzu eta urruneko leihoa berehala ixten bada, nouveau driver-a instalatzea eta software errenderizazioa hautatzea lagundu dezake. Aplikazioa berrabiarazi behar da."), + ("Always use software rendering", "Erabili software bidezko errenderizazioa beti"), + ("config_input", "Urruneko mahaigaina teklatuaren bidez kontrolatzeko, RustDesk-i \"Sarrera monitorizazioa\" baimena eman behar diozu."), + ("config_microphone", "Urrunetik hitz egin ahal izateko, RustDeski-i \"Grabatu audioa\" baimena eman behar diozu."), + ("request_elevation_tip", "Pribilegioen maila igotzea eskatu ahal duzu ere norbait badago urruneko aldean"), + ("Wait", "Itxaron"), + ("Elevation Error", "Maila-igotze errorea"), + ("Ask the remote user for authentication", "Eskatu autentifikazioa urruneko erabiltzaileari"), + ("Choose this if the remote account is administrator", "Aukeratu urruneko kontua administratzailea bada"), + ("Transmit the username and password of administrator", "Igorri administratzailearen erabiltzailea eta pasahitza"), + ("still_click_uac_tip", "Oraindik beharrezkoa da urruneko erabiltzaileak Ondo botoiari klik egitea exekutatzen ari den RustDesk UAC leihoan"), + ("Request Elevation", "Eskatu pribilegioen maila igotzea"), + ("wait_accept_uac_tip", "Mesedez, itxaron urruneko erabiltzaileak UAC onartu arte."), + ("Elevate successfully", "Maila igotzea ondo joan da"), + ("uppercase", "maiuskula"), + ("lowercase", "minuskula"), + ("digit", "zenbakia"), + ("special character", "karaktere berezia"), + ("length>=8", "luzera>=8"), + ("Weak", "Ahula"), + ("Medium", "Ertaina"), + ("Strong", "Indartsua"), + ("Switch Sides", "Aldatu aldeak"), + ("Please confirm if you want to share your desktop?", "Mesedez, berretsi zure mahaigaina partekatu nahi duzula"), + ("Display", "Pantaila"), + ("Default View Style", "Ikuspen estilo lehenetsia"), + ("Default Scroll Style", "Korritze estilo lehenetsia"), + ("Default Image Quality", "Irudi kalitate lehenetsia"), + ("Default Codec", "Kodek lehenetsia"), + ("Bitrate", "Bit-tasa"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Beste aukera lehenetsiak"), + ("Voice call", "Ahots-deia"), + ("Text chat", "Testu-txata"), + ("Stop voice call", "Gelditu ahots-deia"), + ("relay_hint_tip", "Posible da ezinezkoa izatea zuzen konektatzea. Igorpen zerbitzari baten bidez konektatzen saiatu zaitezke. Horrez gain, igorpena lehen aldiz erabili nahi baduzu, \"/r\" atzitu dezakezu IDari edo \"Beti igorpen zerbitzari baten bidez konektatu\" aukeratu azken saioen txartelan existitzen bada."), + ("Reconnect", "Berriro konektatu"), + ("Codec", "Kodeka"), + ("Resolution", "Bereizmena"), + ("No transfers in progress", "Ez dago transferentziarik abian"), + ("Set one-time password length", "Ezarri aldi baterako pasahitzaren luzera"), + ("RDP Settings", "RDP ezarpenak"), + ("Sort by", "Ordenatu honengatik"), + ("New Connection", "Konexio berria"), + ("Restore", "Berrezarri"), + ("Minimize", "Minimizatu"), + ("Maximize", "Maximizatu"), + ("Your Device", "Zure gailua"), + ("empty_recent_tip", "Ups, ez dago azken saiorik!\nBerri bat planifikatzeko ordua da."), + ("empty_favorite_tip", "Parekide gogokorik gabe oraindik?\nBilatu norbait konektatzeko eta gehitu zure gogokoetara!"), + ("empty_lan_tip", "Ai ez, badirudi ez duzula parekiderik aurkitu oraindik."), + ("empty_address_book_tip", "Badirudi ez dagoela parekiderik zure helbide-liburuan."), + ("Empty Username", "Erabiltzaile-izena hutsik"), + ("Empty Password", "Pasahitza hutsik"), + ("Me", "Ni"), + ("identical_file_tip", "Fitxategi hau parekidearen berdina da."), + ("show_monitors_tip", "Erakutsi monitoreak tresna-barran"), + ("View Mode", "Ikuspen modua"), + ("login_linux_tip", "Urruneko Linux kontu batera hasi behar duzu saioa X mahaigain saio bat gaitzeko"), + ("verify_rustdesk_password_tip", "Berretsi RustDesk pasahitza"), + ("remember_account_tip", "Gogoratu kontu hau"), + ("os_account_desk_tip", "Kontu hau bururik gabe urruneko SE hasi eta mahaigaineko saioa gaitzeko erabiltzen da"), + ("OS Account", "SE kontua"), + ("another_user_login_title_tip", "Beste erabiltzaile batek saioa hasi du dagoeneko"), + ("another_user_login_text_tip", "Deskonektatu"), + ("xorg_not_found_title_tip", "Ez da Xorg aurkitu"), + ("xorg_not_found_text_tip", "Mesedez, instalatu ezazu Xorg"), + ("no_desktop_title_tip", "Ez dago mahaigainik eskuragarri"), + ("no_desktop_text_tip", "Mesedez, instalatu ezazu GNOME Desktop"), + ("No need to elevate", "Ez da beharrezkoa pribilegioen maila igotzea"), + ("System Sound", "Sistemaren soinua"), + ("Default", "Lehenetsia"), + ("New RDP", "RDP berria"), + ("Fingerprint", "Hatz-marka"), + ("Copy Fingerprint", "Kopiatu hatz-marka"), + ("no fingerprints", "hatz-markarik ez"), + ("Select a peer", "Hautatu parekidea"), + ("Select peers", "Hautatu parekideak"), + ("Plugins", "Pluginak"), + ("Uninstall", "Desinstalatu"), + ("Update", "Eguneratu"), + ("Enable", "Gaitu"), + ("Disable", "Desgaitu"), + ("Options", "Aukerak"), + ("resolution_original_tip", "Jatorrizko bereizmena"), + ("resolution_fit_local_tip", "Bereizmen lokala egokitu"), + ("resolution_custom_tip", "Bereizmen pertsonalizatua"), + ("Collapse toolbar", "Ezkutatu tresna-barra"), + ("Accept and Elevate", "Onartu eta igo maila"), + ("accept_and_elevate_btn_tooltip", "Konexioa onartu eta UAC baimenak mailaz igo."), + ("clipboard_wait_response_timeout_tip", "Kopiatzeko denbora-muga gainditu da."), + ("Incoming connection", "Sarrerako konexioa"), + ("Outgoing connection", "Irteerako konexioa"), + ("Exit", "Irten"), + ("Open", "Ireki"), + ("logout_tip", "Saioa itxi nahi duzu?"), + ("Service", "Zerbitzua"), + ("Start", "Hasi"), + ("Stop", "Gelditu"), + ("exceed_max_devices", "Kudeatutako gailuen mugara heldu zara."), + ("Sync with recent sessions", "Sinkronizatu azken saioekin"), + ("Sort tags", "Ordenatu etiketak"), + ("Open connection in new tab", "Ireki konexioa fitxa berri batean"), + ("Move tab to new window", "Mugitu fitxa leiho berri batera"), + ("Can not be empty", "Ezin da hutsik egon"), + ("Already exists", "Dagoeneko existitzen da"), + ("Change Password", "Aldatu pasahitza"), + ("Refresh Password", "Freskatu pasahitza"), + ("ID", "ID"), + ("Grid View", "Sareta-ikuspegia"), + ("List View", "Zerrenda-ikuspegia"), + ("Select", "Hautatu"), + ("Toggle Tags", "Aldatu etiketak"), + ("pull_ab_failed_tip", "Ezin izan da direktorio freskatu"), + ("push_ab_failed_tip", "Ezin izan da zerbitzariko direktorioarekin sinkronizatu"), + ("synced_peer_readded_tip", "Azken saioetako gailuak direktorioarekin sinkronizatuko dira"), + ("Change Color", "Aldatu kolorea"), + ("Primary Color", "Kolore nagusia"), + ("HSV Color", "HSV kolorea"), + ("Installation Successful!", "Instalazioa ondo joan da"), + ("Installation failed!", "Instalazioak huts egin du"), + ("Reverse mouse wheel", "Inbertitu saguaren gurpila"), + ("{} sessions", "{} saio"), + ("scam_title", "IRUZURTUA izan zintezke!"), + ("scam_text1", "Ezagutzen eta fidatzen EZ duzun norbaitekin telefonoz bazaude eta RustDesk erabiltzeko eta zerbitzua abiarazteko eskatu badizute, ez ezazu egin eta eskegi berehala."), + ("scam_text2", "Ziurrenik zure dirua edo informazio pribatua lapurtzen saiatzen ari diren iruzurgileak dira."), + ("Don't show again", "Ez erakutsi berriro"), + ("I Agree", "Onartzen dut"), + ("Decline", "Ukatu"), + ("Timeout in minutes", "Denbora-muga minututan"), + ("auto_disconnect_option_tip", "Itxi sarrerako konexioak automatikoki erabiltzailearen jarduera faltagatik."), + ("Connection failed due to inactivity", "Konexioak huts egin du jarduera faltagatik"), + ("Check for software update on startup", "Egiaztatu software eguneraketak abiatzerakoan"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Mesedez, bertsio-berritu RustDesk Server Pro {} bertsiora edo berriago batera!"), + ("pull_group_failed_tip", "Ezin izan da taldea freskatu"), + ("Filter by intersection", "Iragazi bidegurutzez"), + ("Remove wallpaper during incoming sessions", "Kendu horma-papera sarrerako saioetan"), + ("Test", "Probatu"), + ("display_is_plugged_out_msg", "Pantaila deskonektatuta dago, aldatu nagusira."), + ("No displays", "Ez dago pantailarik"), + ("Open in new window", "Ireki leiho berrian"), + ("Show displays as individual windows", "Erakutsi pantailak banakako leiho gisa"), + ("Use all my displays for the remote session", "Erabili nire pantaila guztiak urruneko saiorako"), + ("selinux_tip", "SELinux zure gailuan gaituta dago. Honek RustDesk kontrolatutako alde gisa dagoenean behar bezala ez exekutatzea ekarri dezake."), + ("Change view", "Aldatu ikuspegia"), + ("Big tiles", "Baldosa handiak"), + ("Small tiles", "Baldosa txikiak"), + ("List", "Zerrenda"), + ("Virtual display", "Pantaila birtuala"), + ("Plug out all", "Deskonektatu dena"), + ("True color (4:4:4)", "Kolore erreala (4:4:4)"), + ("Enable blocking user input", "Gaitu erabiltzailearen sarreraren blokeoa"), + ("id_input_tip", "ID bat, IP zuzena edo ataka duen domeinu bat sar dezakezu (:).\nBeste zerbitzari bateko gailu batera sartu nahi baduzu, erantsi zerbitzariaren helbidea (@?key=), adibidez,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nZerbitzari publiko bateko gailuan sartu nahi baduzu, idatzi \"@public\", gakoa ez da zerbitzari publikorako beharrezkoa.\n\nLehen konexioan igorpen-zerbitzari bat erabiltzea behartu nahi baduzu, gehitu \"/r\" IDaren amaieran, adibidez, \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "1 modua"), + ("privacy_mode_impl_virtual_display_tip", "2 modua"), + ("Enter privacy mode", "Sartu modu pribatuan"), + ("Exit privacy mode", "Irten modu pribatutik"), + ("idd_not_support_under_win10_2004_tip", "Zeharkako pantaila kudeatzailea ez dago onartuta. Windows 10, 2004 bertsioa edo berriagoa behar da."), + ("input_source_1_tip", "Sarrera-iturri 1"), + ("input_source_2_tip", "Sarrera-iturri 2"), + ("Swap control-command key", "Aldatu kontrol-komando teklak"), + ("swap-left-right-mouse", "Aldatu saguaren ezker-eskuin botoiak"), + ("2FA code", "2FA kodea"), + ("More", "Gehiago"), + ("enable-2fa-title", "Gaitu bi faktoreko autentifikazioa"), + ("enable-2fa-desc", "Konfiguratu orain zure autentifikatzailea. Authy, Microsoft edo Google Authenticator bezalako aplikazio autentifikatzaile bat erabil dezakezu telefonoan edo mahaigainean.\n\nEskaneatu QR kodea zure aplikazioarekin eta idatzi aplikazioak erakusten duen kodea bi faktoreko autentifikazioa gaitzeko."), + ("wrong-2fa-code", "Ezin da kodea egiaztatu. Egiaztatu kodea eta tokiko orduaren ezarpenak zuzenak direla"), + ("enter-2fa-title", "Bi faktoreko autentifikazioa"), + ("Email verification code must be 6 characters.", "Posta elektronikoa egiaztatzeko kodeak 6 karaktere izan behar ditu."), + ("2FA code must be 6 digits.", "2FA kodeak 6 digitu izan behar ditu."), + ("Multiple Windows sessions found", "Windows saio anitz aurkitu dira"), + ("Please select the session you want to connect to", "Mesedez, aukeratu konektatu nahi duzun saioa"), + ("powered_by_me", "RustDesk-ek egina"), + ("outgoing_only_desk_tip", "Edizio pertsonalizatua da hau.\nBeste gailuetara konekta zaitezke, baina beste gailu batzuk ezin dira zure gailura konektatu."), + ("preset_password_warning", "Edizio pertsonalizatu hau aurrez ezarritako pasahitz batekin dator. Pasahitz hau ezagutzen duenak zure gailuaren kontrol osoa lor dezake. Hau espero ez bazenuen, desinstalatu softwarea berehala."), + ("Security Alert", "Segurtasun Alerta"), + ("My address book", "Nire helbide-liburua"), + ("Personal", "Pertsonala"), + ("Owner", "Jabea"), + ("Set shared password", "Ezarri pasahitz partekatua"), + ("Exist in", "Bertan existitzen da"), + ("Read-only", "Irakurtzeko soilik"), + ("Read/Write", "Irakurri/Idatzi"), + ("Full Control", "Kontrol Osoa"), + ("share_warning_tip", "Goiko eremuak partekatuak eta besteengatik ikusgai daude."), + ("Everyone", "Denek"), + ("ab_web_console_tip", "Gehiago web kontsolan"), + ("allow-only-conn-window-open-tip", "Baimendu konexioa RustDesk leihoa irekita badago"), + ("no_need_privacy_mode_no_physical_displays_tip", "Ez dago pantaila fisikorik, ez dago pribatutasun modua erabili beharrik."), + ("Follow remote cursor", "Jarraitu urruneko kurtsorea"), + ("Follow remote window focus", "Jarraitu urruneko leiho fokua"), + ("default_proxy_tip", "Lehenetsitako protokoloa eta ataka Socks5 eta 1080 dira"), + ("no_audio_input_device_tip", "Ez da aurkitu audio sarrerako gailurik."), + ("Incoming", "Sarrerakoa"), + ("Outgoing", "Irteerakoa"), + ("Clear Wayland screen selection", "Garbitu Wayland pantaila-hautapena"), + ("clear_Wayland_screen_selection_tip", "Pantaila hautapena garbitu ondoren, partekatzeko pantaila hauta dezakezu."), + ("confirm_clear_Wayland_screen_selection_tip", "Ziur Wayland pantaila-hautapena garbituko duzula?"), + ("android_new_voice_call_tip", "Ahots-dei eskaera berri bat jaso da. Onartzen baduzu, audioa ahots bidezko komunikaziora aldatuko da."), + ("texture_render_tip", "Erabili testura-errenderizazioa irudiak leunagoak egiteko."), + ("Use texture rendering", "Erabili testura-errenderizazioa"), + ("Floating window", "Leiho flotagarria"), + ("floating_window_tip", "RustDesk atzeko planoko zerbitzua mantentzen laguntzen du"), + ("Keep screen on", "Mantendu pantaila piztuta"), + ("Never", "Inoiz"), + ("During controlled", "Kontrolatu bitartean"), + ("During service is on", "Zerbitzua aktibo dagoen bitartean"), + ("Capture screen using DirectX", "Harrapatu pantaila DirectX erabiliz"), + ("Back", "Atzera"), + ("Apps", "Aplikazioak"), + ("Volume up", "Igo bolumena"), + ("Volume down", "Jaitsi bolumena"), + ("Power", "Piztu/Itzali"), + ("Telegram bot", "Telegrameko bot-a"), + ("enable-bot-tip", "Ezaugarri hau gaitzen baduzu, zure bot-etik 2FA kodea jaso dezakezu. Konexio jakinarazpenetarako ere balio dezake."), + ("enable-bot-desc", "1, Ireki txat bat @BotFather bot-arekin.\n2, Bidali \"/newbot\" agindua. Token bat jasoko duzu urrats honen ostean.\n3, Hasi txat bat zure bot berriarekin. Bidali mezu bat aurreko barra batekin (\"/\") \"/kaixo\" bezala gaitzeko.\n"), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Mesedez, eguneratu RustDesk bezeroa {} bertsiora edo berriagoa urruneko aldean!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Ikusi kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "{} honekin jarraitu"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/fa.rs b/vendor/rustdesk/src/lang/fa.rs new file mode 100644 index 0000000..d34e423 --- /dev/null +++ b/vendor/rustdesk/src/lang/fa.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "وضعیت"), + ("Your Desktop", "دسکتاپ شما"), + ("desk_tip", "دسکتاپ شما با این شناسه و رمز عبور قابل دسترسی است"), + ("Password", "رمز عبور"), + ("Ready", "آماده به کار"), + ("Established", "اتصال برقرار شد"), + ("connecting_status", "...در حال برقراری ارتباط با سرور"), + ("Enable service", "فعالسازی سرویس"), + ("Start service", "اجرای سرویس"), + ("Service is running", "سرویس در حال اجرا است"), + ("Service is not running", "سرویس اجرا نشده"), + ("not_ready_status", "ارتباط برقرار نشد. لطفا شبکه خود را بررسی کنید"), + ("Control Remote Desktop", "کنترل دسکتاپ میزبان"), + ("Transfer file", "انتقال فایل"), + ("Connect", "اتصال"), + ("Recent sessions", "جلسات اخیر"), + ("Address book", "دفترچه آدرس"), + ("Confirmation", "تایید"), + ("TCP tunneling", "TCP تانل"), + ("Remove", "حذف"), + ("Refresh random password", "بروزرسانی رمز عبور تصادفی"), + ("Set your own password", "!رمز عبور دلخواه بگذارید"), + ("Enable keyboard/mouse", " فعالسازی ماوس/صفحه کلید"), + ("Enable clipboard", "فعال سازی کلیپبورد"), + ("Enable file transfer", "انتقال فایل را فعال کنید"), + ("Enable TCP tunneling", "را فعال کنید TCP تانل"), + ("IP Whitelisting", "های مجاز IP لیست"), + ("ID/Relay Server", "ID/Relay سرور"), + ("Import server config", "تنظیم سرور با فایل"), + ("Export Server Config", "ایجاد فایل تظیمات از سرور فعلی"), + ("Import server configuration successfully", "تنظیمات سرور با فایل کانفیگ با موفقیت انجام شد"), + ("Export server configuration successfully", "ایجاد فایل کانفیگ از تنظیمات فعلی با موفقیت انجام شد"), + ("Invalid server configuration", "تنظیمات سرور نامعتبر است"), + ("Clipboard is empty", "کلیپبورد خالی است"), + ("Stop service", "توقف سرویس"), + ("Change ID", "تعویض شناسه"), + ("Your new ID", "جدید ID"), + ("length %min% to %max%", "%max% تا %min% طول از"), + ("starts with a letter", "با حرف شروع می شود"), + ("allowed characters", "کارکترهای مجاز"), + ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), + ("Website", "وب سایت"), + ("About", "درباره"), + ("Slogan_tip", "ساخته شده با ❤️‍(عشق) در این دنیای پر هرج و مرج!"), + ("Privacy Statement", "بیانیه حریم خصوصی"), + ("Mute", "بستن صدا"), + ("Build Date", "تاریخ ساخت"), + ("Version", "نسخه"), + ("Home", "صفحه اصلی"), + ("Audio Input", "ورودی صدا"), + ("Enhancements", "بهبودها"), + ("Hardware Codec", "کدک سخت افزاری"), + ("Adaptive bitrate", "سازگار Bitrate"), + ("ID Server", "شناسه سرور"), + ("Relay Server", "Relay سرور"), + ("API Server", "API سرور"), + ("invalid_http", "شروع شود http:// یا https:// باید با"), + ("Invalid IP", "نامعتبر است IP آدرس"), + ("Invalid format", "فرمت نادرست است"), + ("server_not_support", "هنوز توسط سرور مورد نظر پشتیبانی نمی شود"), + ("Not available", "در دسترسی نیست"), + ("Too frequent", "خیلی رایج"), + ("Cancel", "لغو"), + ("Skip", "رد کردن"), + ("Close", "بستن"), + ("Retry", "تلاش مجدد"), + ("OK", "قبول"), + ("Password Required", "رمز عبور لازم است"), + ("Please enter your password", "رمز عبور خود را وارد کنید"), + ("Remember password", "رمز عبور را به خاطر بسپار"), + ("Wrong Password", "رمز عبور اشتباه است"), + ("Do you want to enter again?", "آیا میخواهید مجددا وارد شوید؟"), + ("Connection Error", "خطا در اتصال"), + ("Error", "خطا"), + ("Reset by the peer", "توسط میزبان حذف شد"), + ("Connecting...", "...در حال اتصال"), + ("Connection in progress. Please wait.", "در حال اتصال. لطفا متظر بمانید"), + ("Please try 1 minute later", "لطفا بعد از 1 دقیقه مجددا تلاش کنید"), + ("Login Error", "ورود ناموفق بود"), + ("Successful", "با موفقیت انجام شد"), + ("Connected, waiting for image...", "...ارتباط برقرار شد. انتظار برای دریافت تصاویر"), + ("Name", "نام"), + ("Type", "نوع فایل"), + ("Modified", "تاریخ تغییر"), + ("Size", "سایز"), + ("Show Hidden Files", "نمایش فایل های مخفی"), + ("Receive", "دریافت"), + ("Send", "ارسال"), + ("Refresh File", "به روزرسانی فایل"), + ("Local", "محلی"), + ("Remote", "از راه دور"), + ("Remote Computer", "سیستم راه دور"), + ("Local Computer", "سیسستم محلی"), + ("Confirm Delete", "تایید حذف"), + ("Delete", "حذف"), + ("Properties", "مشخصات"), + ("Multi Select", "انتخاب دسته ای"), + ("Select All", "انتخاب همه"), + ("Unselect All", "لغو انتخاب همه"), + ("Empty Directory", "پوشه خالی"), + ("Not an empty directory", "پوشه خالی نیست"), + ("Are you sure you want to delete this file?", "از حذف این فایل مطمئن هستید؟"), + ("Are you sure you want to delete this empty directory?", "از حذف این پوشه خالی مطمئن هستید؟"), + ("Are you sure you want to delete the file of this directory?", "از حذف فایل موجود در این پوشه مطمئن هستید؟"), + ("Do this for all conflicts", "این عمل برای همه ی تضادها انجام شود"), + ("This is irreversible!", "این اقدام برگشت ناپذیر است!"), + ("Deleting", "در حال حذف"), + ("files", "فایل ها"), + ("Waiting", "انتظار"), + ("Finished", "تکمیل شد"), + ("Speed", "سرعت"), + ("Custom Image Quality", "سفارشی سازی : کیفیت تصاویر"), + ("Privacy mode", "حالت حریم خصوصی"), + ("Block user input", "بستن ورودی کاربر"), + ("Unblock user input", "بازکردن ورودی کاربر"), + ("Adjust Window", "تنظیم پنجره"), + ("Original", "اصل"), + ("Shrink", "کوچک کردن"), + ("Stretch", "کشیدن تصویر"), + ("Scrollbar", "اسکرول بار"), + ("ScrollAuto", "پیمایش/اسکرول خودکار"), + ("Good image quality", "کیفیت خوب تصویر"), + ("Balanced", "متعادل"), + ("Optimize reaction time", "بهینه سازی : زمان واکنش"), + ("Custom", "سفارشی"), + ("Show remote cursor", "نمایش مکان نما موس میزبان"), + ("Show quality monitor", "نمایش کیفیت مانیتور"), + ("Disable clipboard", " غیرفعالسازی کلیپبورد"), + ("Lock after session end", "قفل کردن حساب کاربری سیستم عامل پس از پایان جلسه"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del افزودن"), + ("Insert Lock", "قفل کردن سیستم"), + ("Refresh", "تازه سازی"), + ("ID does not exist", "شناسه وجود ندارد"), + ("Failed to connect to rendezvous server", "اتصال به سرور تولید شناسه انجام نشد"), + ("Please try later", "لطفا بعدا تلاش کنید"), + ("Remote desktop is offline", "دسکتاپ راه دور آفلاین است"), + ("Key mismatch", "عدم تطابق کلید"), + ("Timeout", "زمان انتظار به پایان رسید"), + ("Failed to connect to relay server", "سرور وصل نشد Relay به"), + ("Failed to connect via rendezvous server", "اتصال از طریق سرور تولید شناسه انجام نشد"), + ("Failed to connect via relay server", "انجام نشد Relay اتصال از طریق سرور"), + ("Failed to make direct connection to remote desktop", "اتصال مستقیم به دسکتاپ راه دور انجام نشد"), + ("Set Password", "تنظیم رمزعبور"), + ("OS Password", "رمز عبور سیستم عامل"), + ("install_tip", "لطفا برنامه را نصب کنید UAC و جلوگیری از خطای RustDesk برای راحتی در استفاده از نرم افزار"), + ("Click to upgrade", "برای ارتقا کلیک کنید"), + ("Configure", "تنظیم"), + ("config_acc", "بدهید \"access\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), + ("config_screen", "بدهید \"screenshot\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), + ("Installing ...", "...در حال نصب"), + ("Install", "نصب"), + ("Installation", "نصب و راه اندازی"), + ("Installation Path", "محل نصب"), + ("Create start menu shortcuts", "Start ایجاد میانبرها در منوی"), + ("Create desktop icon", "ایجاد آیکن در دسکتاپ"), + ("agreement_tip", "با شروع نصب، شرایط توافق نامه مجوز را می پذیرید"), + ("Accept and Install", "قبول و شروع نصب"), + ("End-user license agreement", "قرارداد مجوز کاربر نهایی"), + ("Generating ...", "...در حال تولید"), + ("Your installation is lower version.", "نسخه قدیمی تری نصب شده است"), + ("not_close_tcp_tip", "هنگام استفاده از تونل این پنجره را نبندید"), + ("Listening ...", "...انتظار"), + ("Remote Host", "هاست راه دور"), + ("Remote Port", "پورت راه دور"), + ("Action", "عملیات"), + ("Add", "افزودن"), + ("Local Port", "پورت محلی"), + ("Local Address", "آدرس محلی"), + ("Change Local Port", "تغییر پورت محلی"), + ("setup_server_tip", "برای اتصال سریعتر، سرور اتصال شخصی خود را راه اندازی کنید"), + ("Too short, at least 6 characters.", "بسیار کوتاه حداقل 6 کاراکتر مورد نیاز است"), + ("The confirmation is not identical.", "تأیید ناموفق بود."), + ("Permissions", "دسترسی ها"), + ("Accept", "پذیرفتن"), + ("Dismiss", "رد کردن"), + ("Disconnect", "قطع اتصال"), + ("Enable file copy and paste", "مجاز بودن کپی و چسباندن فایل"), + ("Connected", "متصل شده"), + ("Direct and encrypted connection", "اتصال مستقیم و رمزگذاری شده"), + ("Relayed and encrypted connection", "و رمزگذاری شده Relay اتصال از طریق"), + ("Direct and unencrypted connection", "اتصال مستقیم و بدون رمزگذاری"), + ("Relayed and unencrypted connection", "و رمزگذاری نشده Relay اتصال از طریق"), + ("Enter Remote ID", "شناسه از راه دور را وارد کنید"), + ("Enter your password", "رمز عبور خود را وارد کنید"), + ("Logging in...", "...در حال ورود"), + ("Enable RDP session sharing", "را فعال کنید RDP اشتراک گذاری جلسه"), + ("Auto Login", "ورود خودکار"), + ("Enable direct IP access", "را فعال کنید IP دسترسی مستقیم"), + ("Rename", "تغییر نام"), + ("Space", "فضا"), + ("Create desktop shortcut", "ساخت میانبر روی دسکتاپ"), + ("Change Path", "تغییر مسیر"), + ("Create Folder", "ایجاد پوشه"), + ("Please enter the folder name", "نام پوشه را وارد کنید"), + ("Fix it", "بازسازی"), + ("Warning", "هشدار"), + ("Login screen using Wayland is not supported", "پشتیبانی نمی شود Wayland ورود به سیستم با استفاده از "), + ("Reboot required", "راه اندازی مجدد مورد نیاز است"), + ("Unsupported display server", "سرور تصویر پشتیبانی نشده است"), + ("x11 expected", "X11 مورد انتظار است"), + ("Port", "پورت"), + ("Settings", "تنظیمات"), + ("Username", "نام کاربری"), + ("Invalid port", "پورت نامعتبر است"), + ("Closed manually by the peer", "به صورت دستی توسط میزبان بسته شد"), + ("Enable remote configuration modification", "فعال بودن اعمال تغییرات پیکربندی از راه دور"), + ("Run without install", "بدون نصب اجرا شود"), + ("Connect via relay", "اتصال با رله"), + ("Always connect via relay", "برای اتصال استفاده شود Relay از"), + ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), + ("Login", "ورود"), + ("Verify", "تأیید کنید"), + ("Remember me", "مرا به یاد داشته باش"), + ("Trust this device", "به این دستگاه اعتماد کنید"), + ("Verification code", "کد تایید"), + ("verification_tip", "یک دستگاه جدید شناسایی شده است و یک کد تأیید به آدرس ایمیل ثبت شده ارسال شده است، برای ادامه ورود، کد تأیید را وارد کنید."), + ("Logout", "خروج"), + ("Tags", "برچسب ها"), + ("Search ID", "جستجوی شناسه"), + ("whitelist_sep", "با کاما، نقطه ویرگول، فاصله یا خط جدید از هم جدا می شوند"), + ("Add ID", "افزودن شناسه"), + ("Add Tag", "افزودن برچسب"), + ("Unselect all tags", "همه برچسب ها را لغو انتخاب کنید"), + ("Network error", "خطای شبکه"), + ("Username missed", "نام کاربری وجود ندارد"), + ("Password missed", "رمزعبور وجود ندارد"), + ("Wrong credentials", "اعتبارنامه نادرست است"), + ("The verification code is incorrect or has expired", "کد تأیید نادرست است یا منقضی شده است"), + ("Edit Tag", "ویرایش برچسب"), + ("Forget Password", "رمز عبور ذخیره نشود"), + ("Favorites", "اتصالات دلخواه"), + ("Add to Favorites", "افزودن به علاقه مندی ها"), + ("Remove from Favorites", "از علاقه مندی ها حذف شود"), + ("Empty", "موردی وجود ندارد"), + ("Invalid folder name", "نام پوشه نامعتبر است"), + ("Socks5 Proxy", "Socks5 پروکسی"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) پروکسی"), + ("Discovered", "پیدا شده"), + ("install_daemon_tip", "برای شروع در هنگام راه اندازی، باید سرویس سیستم را نصب کنید"), + ("Remote ID", "شناسه راه دور"), + ("Paste", "درج"), + ("Paste here?", "اینجا درج شود؟"), + ("Are you sure to close the connection?", "آیا مطمئن هستید که می خواهید اتصال را پایان دهید؟"), + ("Download new version", "دانلود نسخه جدید"), + ("Touch mode", "حالت لمسی"), + ("Mouse mode", "حالت ماوس"), + ("One-Finger Tap", "با یک انگشت لمس کنید"), + ("Left Mouse", "دکمه سمت چپ ماوس"), + ("One-Long Tap", "لمس طولانی با یک انگشت"), + ("Two-Finger Tap", "لمس دو انگشتی"), + ("Right Mouse", "دکمه سمت راست ماوس"), + ("One-Finger Move", "با یک انگشت حرکت کنید"), + ("Double Tap & Move", "دو ضربه سریع بزنید و حرکت دهید"), + ("Mouse Drag", "کشیدن ماوس"), + ("Three-Finger vertically", "سه انگشت عمودی"), + ("Mouse Wheel", "چرخ ماوس"), + ("Two-Finger Move", "با دو انگشت حرکت کنید"), + ("Canvas Move", "حرکت دادن صفحه"), + ("Pinch to Zoom", "با دو انگشت بکشید تا زوم شود"), + ("Canvas Zoom", "بزرگنمایی صفحه"), + ("Reset canvas", "بازنشانی صفحه"), + ("No permission of file transfer", "مجوز انتقال فایل داده نشده"), + ("Note", "یادداشت"), + ("Connection", "ارتباط"), + ("Share screen", "اشتراک گذاری صفحه"), + ("Chat", "چت"), + ("Total", "مجموع"), + ("items", "آیتم ها"), + ("Selected", "انتخاب شده"), + ("Screen Capture", "ضبط صفحه"), + ("Input Control", "کنترل ورودی"), + ("Audio Capture", "ضبط صدا"), + ("Do you accept?", "آیا می پذیرید؟"), + ("Open System Setting", "باز کردن تنظیمات سیستم"), + ("How to get Android input permission?", "چگونه مجوز ورود به سیستم اندروید را دریافت کنیم؟"), + ("android_input_permission_tip1", "استفاده کند \"Accessibility\" اجازه دهید از ویژگی RustDesk شما را از طریق ماوس یا صفحه ی لمسی کنترل کند، باید به Android برای اینکه یک دستگاه از راه دور بتواند دستگاه"), + ("android_input_permission_tip2", "را پیدا کنید و فعال نمایید \"RustDesk Input\" بشوید ، سپس گزینه \"Installed Services\" وارد قسمت \"Accessibility\" در صفحه تنظیمات "), + ("android_new_connection_tip", "درخواست جدیدی برای مدیریت دستگاه فعلی شما دریافت شده است."), + ("android_service_will_start_tip", "فعال کردن ضبط صفحه به طور خودکار سرویس را راه اندازی می کند و به دستگاه های دیگر امکان می دهد درخواست اتصال به آن دستگاه را داشته باشند."), + ("android_stop_service_tip", "با بستن سرویس، تمام اتصالات برقرار شده به طور خودکار بسته می شود"), + ("android_version_audio_tip", "نسخه فعلی اندروید از ضبط صدا پشتیبانی نمی‌کند، لطفاً به اندروید 10 یا بالاتر به‌روزرسانی کنید"), + ("android_start_service_tip", "را فعال کنید [Screen Capture] ضربه بزنید یا مجوز [Start service] برای شروع سرویس اشتراک ‌گذاری صفحه، روی"), + ("android_permission_may_not_change_tip", "مجوزهای ایجاد شده یا تغییر یافته برای اتصالات جاری تغییر نخواهد کرد، برای تغییر نیاز است مجددا اتصال برقرار گردد"), + ("Account", "حساب کاربری"), + ("Overwrite", "بازنویسی"), + ("This file exists, skip or overwrite this file?", "این فایل وجود دارد، از فایل رد شود یا آن را بازنویسی کند؟"), + ("Quit", "خروج"), + ("Help", "راهنما"), + ("Failed", "ناموفق"), + ("Succeeded", "موفقیت آمیز"), + ("Someone turns on privacy mode, exit", "اگر شخصی حالت حریم خصوصی را روشن کرد، خارج شوید"), + ("Unsupported", "پشتیبانی نشده"), + ("Peer denied", "توسط میزبان راه دور رد شد"), + ("Please install plugins", "لطفا افزونه ها را نصب کنید"), + ("Peer exit", "میزبان خارج شد"), + ("Failed to turn off", "خاموش کردن انجام نشد"), + ("Turned off", "خاموش شد"), + ("Language", "زبان"), + ("Keep RustDesk background service", "را در پس زمینه نگه دارید RustDesk سرویس"), + ("Ignore Battery Optimizations", "بهینه سازی باتری نادیده گرفته شود"), + ("android_open_battery_optimizations_tip", "به صفحه تنظیمات بعدی بروید"), + ("Start on boot", "در هنگام بوت شروع شود"), + ("Start the screen sharing service on boot, requires special permissions", "سرویس اشتراک‌گذاری صفحه را در بوت راه‌اندازی کنید، به مجوزهای خاصی نیاز دارد"), + ("Connection not allowed", "اتصال مجاز نیست"), + ("Legacy mode", "legacy حالت"), + ("Map mode", "map حالت"), + ("Translate mode", "حالت ترجمه"), + ("Use permanent password", "از رمز عبور دائمی استفاده شود"), + ("Use both passwords", "از هر دو رمز عبور استفاده شود"), + ("Set permanent password", "یک رمز عبور دائمی تنظیم شود"), + ("Enable remote restart", "فعال کردن قابلیت ریستارت از راه دور"), + ("Restart remote device", "ریستارت کردن از راه دور"), + ("Are you sure you want to restart", "ایا مطمئن هستید میخواهید راه اندازی مجدد انجام بدید؟"), + ("Restarting remote device", "در حال راه اندازی مجدد دستگاه راه دور"), + ("remote_restarting_tip", "دستگاه راه دور در حال راه اندازی مجدد است. این پیام را ببندید و پس از مدتی با استفاده از یک رمز عبور دائمی دوباره وصل شوید."), + ("Copied", "کپی شده است"), + ("Exit Fullscreen", "از حالت تمام صفحه خارج شوید"), + ("Fullscreen", "تمام صفحه"), + ("Mobile Actions", "اقدامات موبایل"), + ("Select Monitor", "مانیتور را انتخاب کنید"), + ("Control Actions", "اقدامات مدیریتی"), + ("Display Settings", "تنظیمات نمایشگر"), + ("Ratio", "نسبت"), + ("Image Quality", "کیفیت تصویر"), + ("Scroll Style", "سبک اسکرول"), + ("Show Toolbar", "نمایش نوار ابزار"), + ("Hide Toolbar", "پنهان کردن نوار ابزار"), + ("Direct Connection", "ارتباط مستقیم"), + ("Relay Connection", "Relay ارتباط"), + ("Secure Connection", "ارتباط امن"), + ("Insecure Connection", "ارتباط غیر امن"), + ("Scale original", "مقیاس اصلی"), + ("Scale adaptive", "مقیاس تطبیقی"), + ("General", "عمومی"), + ("Security", "امنیت"), + ("Theme", "نمایه"), + ("Dark Theme", "نمایه تیره"), + ("Light Theme", "نمایه روشن"), + ("Dark", "تیره"), + ("Light", "روشن"), + ("Follow System", "پیروی از سیستم"), + ("Enable hardware codec", "فعال سازی کدک سخت افزاری"), + ("Unlock Security Settings", "دسترسی کامل به تنظیمات امنیتی"), + ("Enable audio", "فعال شدن صدا"), + ("Unlock Network Settings", "دسترسی کامل به تنظیمات شبکه"), + ("Server", "سرور"), + ("Direct IP Access", "IP دسترسی مستقیم به"), + ("Proxy", "پروکسی"), + ("Apply", "اعمال تغییرات"), + ("Disconnect all devices?", "همه دستگاه ها قطع شوند؟"), + ("Clear", "پاک کردن"), + ("Audio Input Device", "منبع صدا"), + ("Use IP Whitelisting", "های مجاز IP استفاده از"), + ("Network", "شبکه"), + ("Pin Toolbar", "سجاق کردن نوار ابزار"), + ("Unpin Toolbar", "خروج از حالت سجاق نوار ابزار"), + ("Recording", "در حال ضبط"), + ("Directory", "مسیر"), + ("Automatically record incoming sessions", "ضبط خودکار جلسات ورودی"), + ("Automatically record outgoing sessions", "ضبط خودکار جلسات خروجی"), + ("Change", "تغییر"), + ("Start session recording", "شروع ضبط جلسه"), + ("Stop session recording", "توقف ضبط جلسه"), + ("Enable recording session", "فعالسازی ضبط جلسه"), + ("Enable LAN discovery", "فعالسازی جستجو در شبکه"), + ("Deny LAN discovery", "غیر فعالسازی جستجو در شبکه"), + ("Write a message", "یک پیام بنویسید"), + ("Prompt", "سریع"), + ("Please wait for confirmation of UAC...", "باشید UAC لطفا منتظر تایید"), + ("elevated_foreground_window_tip", "پنجره فعلی دسکتاپ راه دور برای کار کردن به دسترسی بالاتری نیاز دارد، بنابراین نمی‌تواند به طور موقت از ماوس و صفحه کلید استفاده کند. می توانید از کاربر راه دور درخواست کنید که پنجره فعلی را به پایین منتقل کند یا روی دکمه ارتقاء دسترسی در پنجره مدیریت اتصال کلیک کنید. برای جلوگیری از این مشکل، توصیه می شود نرم افزار را روی دستگاه از راه دور نصب کنید."), + ("Disconnected", "قطع ارتباط"), + ("Other", "سایر"), + ("Confirm before closing multiple tabs", "تایید بستن دسته ای برگه ها"), + ("Keyboard Settings", "تنظیمات صفحه کلید"), + ("Full Access", "دسترسی کامل"), + ("Screen Share", "اشتراک گذاری صفحه"), + ("ubuntu-21-04-required", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"), + ("wayland-requires-higher-linux-version", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"), + ("xdp-portal-unavailable", ""), + ("JumpLink", "چشم انداز"), + ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), + ("Show RustDesk", "RustDesk نمایش"), + ("This PC", "This PC"), + ("or", "یا"), + ("Elevate", "ارتقاء"), + ("Zoom cursor", " بزرگنمایی نشانگر ماوس"), + ("Accept sessions via password", "قبول درخواست با رمز عبور"), + ("Accept sessions via click", "قبول درخواست با کلیک موس"), + ("Accept sessions via both", "قبول درخواست با هر دو"), + ("Please wait for the remote side to accept your session request...", "...لطفا صبر کنید تا میزبان درخواست شما را قبول کند"), + ("One-time Password", "رمز عبور یکبار مصرف"), + ("Use one-time password", "استفاده از رمز عبور یکبار مصرف"), + ("One-time password length", "طول رمز عبور یکبار مصرف"), + ("Request access to your device", "دسترسی به دستگاه خود را درخواست کنید"), + ("Hide connection management window", "پنهان کردن پنجره مدیریت اتصال"), + ("hide_cm_tip", "فقط در صورت پذیرفتن جلسات از طریق رمز عبور و استفاده از رمز عبور دائمی، مخفی شدن مجاز است"), + ("wayland_experiment_tip", "پشتیبانی Wayland در مرحله آزمایشی است، لطفاً در صورت نیاز به دسترسی بدون مراقبت از X11 استفاده کنید."), + ("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید"), + ("Skipped", "رد شد"), + ("Add to address book", "افزودن به دفترچه آدرس"), + ("Group", "گروه"), + ("Search", "جستجو"), + ("Closed manually by web console", "به صورت دستی توسط کنسول وب بسته شد"), + ("Local keyboard type", "نوع صفحه کلید محلی"), + ("Select local keyboard type", "نوع صفحه کلید محلی را انتخاب کنید"), + ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), + ("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"), + ("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."), + ("config_microphone", "را بدهید. RustDesk \"Record Audio\" برای صحبت از راه دور، باید مجوز"), + ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتقاء دسترسی دهید."), + ("Wait", "صبر کنید"), + ("Elevation Error", "خطای ارتقاء دسترسی"), + ("Ask the remote user for authentication", "درخواست احراز هویت از یک کاربر راه دور"), + ("Choose this if the remote account is administrator", "اگر حساب راه دور یک مدیر است، این را انتخاب کنید"), + ("Transmit the username and password of administrator", "نام کاربری و رمز عبور مدیر را منتقل کنید"), + ("still_click_uac_tip", "همچنان کاربر از راه دور نیاز دارد که روی OK در پنجره UAC اجرای RustDesk کلیک کند."), + ("Request Elevation", "درخواست ارتقاء دسترسی"), + ("wait_accept_uac_tip", "لطفاً منتظر بمانید تا کاربر راه دور درخواست پنجره UAC را بپذیرد."), + ("Elevate successfully", "ارتقاء دسترسی با موفقیت انجام شد"), + ("uppercase", "حروف بزرگ"), + ("lowercase", "حروف کوچک"), + ("digit", "عدد"), + ("special character", "کاراکتر خاص"), + ("length>=8", "حداقل طول 8 کاراکتر"), + ("Weak", "ضعیف"), + ("Medium", "متوسط"), + ("Strong", "قوی"), + ("Switch Sides", "طرفین را عوض کنید"), + ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), + ("Display", "نمایش دادن"), + ("Default View Style", "سبک نمایش پیش فرض"), + ("Default Scroll Style", "سبک پیش‌ فرض اسکرول"), + ("Default Image Quality", "کیفیت تصویر پیش فرض"), + ("Default Codec", "کدک پیش فرض"), + ("Bitrate", "میزان بیت صفحه نمایش"), + ("FPS", "فریم در ثانیه"), + ("Auto", "خودکار"), + ("Other Default Options", "سایر گزینه های پیش فرض"), + ("Voice call", "تماس صوتی"), + ("Text chat", "گفتگو متنی (چت متنی)"), + ("Stop voice call", "توقف تماس صوتی"), + ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \" همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), + ("Reconnect", "اتصال مجدد"), + ("Codec", "کدک"), + ("Resolution", "وضوح"), + ("No transfers in progress", "هیچ انتقالی در حال انجام نیست"), + ("Set one-time password length", "طول رمز یکبار مصرف را تعیین کنید"), + ("RDP Settings", "RDP تنظیمات"), + ("Sort by", "مرتب سازی بر اساس"), + ("New Connection", "اتصال جدید"), + ("Restore", "بازیابی"), + ("Minimize", "کوچک کردن پنجره"), + ("Maximize", "بزرک کردن پنجره"), + ("Your Device", "دستگاه شما"), + ("empty_recent_tip", "اوه، هیچ جلسه اخیری وجود ندارد!\nزمان برنامه ریزی جلسه جدید است"), + ("empty_favorite_tip", "هنوز همتای مورد علاقه‌ای ندارید؟\nبیایید فردی را برای ارتباط پیدا کنیم و آن را به موارد دلخواه خود اضافه کنیم!"), + ("empty_lan_tip", "اوه نه، به نظر می رسد که ما هنوز همتای خود را پیدا نکرده ایم"), + ("empty_address_book_tip", "اوه ، به نظر می رسد که در حال حاضر هیچ همتایی در دفترچه آدرس شما وجود ندارد"), + ("Empty Username", "نام کاربری خالی است"), + ("Empty Password", "رمز عبور خالی است"), + ("Me", "من"), + ("identical_file_tip", "این فایل با فایل همتا یکسان است."), + ("show_monitors_tip", "نمایش مانیتورها در نوار ابزار"), + ("View Mode", "حالت مشاهده"), + ("login_linux_tip", "برای فعال کردن دسکتاپ X، باید به حساب لینوکس راه دور وارد شوید"), + ("verify_rustdesk_password_tip", "رمز عبور RustDesk را تأیید کنید"), + ("remember_account_tip", "این حساب را به خاطر بسپارید"), + ("os_account_desk_tip", "این حساب برای ورود به سیستم عامل راه دور و فعال کردن جلسه دسکتاپ در هدلس استفاده می شود"), + ("OS Account", "حساب کاربری سیستم عامل"), + ("another_user_login_title_tip", "کاربر دیگری قبلاً وارد شده است"), + ("another_user_login_text_tip", "قطع شدن"), + ("xorg_not_found_title_tip", "پیدا نشد Xorg"), + ("xorg_not_found_text_tip", "لطفا Xorg را نصب کنید"), + ("no_desktop_title_tip", "هیچ دسکتاپی در دسترس نیست"), + ("no_desktop_text_tip", "لطفا دسکتاپ گنوم را نصب کنید"), + ("No need to elevate", "نیازی به ارتقاء نیست"), + ("System Sound", "صدای سیستم"), + ("Default", "پیش فرض"), + ("New RDP", "ریموت جدید"), + ("Fingerprint", "\n اثر انگشت"), + ("Copy Fingerprint", "کپی کردن اثر انگشت"), + ("no fingerprints", "بدون اثر انگشت"), + ("Select a peer", "یک همتا را انتخاب کنید"), + ("Select peers", "همتایان را انتخاب کنید"), + ("Plugins", "پلاگین ها"), + ("Uninstall", "حذف نصب"), + ("Update", "به روز رسانی"), + ("Enable", "فعال کردن"), + ("Disable", "غیر فعال کردن"), + ("Options", "گزینه ها"), + ("resolution_original_tip", "وضوح اصلی"), + ("resolution_fit_local_tip", "متناسب با وضوح محلی"), + ("resolution_custom_tip", "وضوح سفارشی"), + ("Collapse toolbar", "جمع کردن نوار ابزار"), + ("Accept and Elevate", "بپذیرید و افزایش دهید"), + ("accept_and_elevate_btn_tooltip", "را افزایش دهید UAC اتصال را بپذیرید و مجوزهای."), + ("clipboard_wait_response_timeout_tip", "زمان انتظار برای مشخص شدن وضعیت کپی تمام شد."), + ("Incoming connection", "اتصال ورودی"), + ("Outgoing connection", "اتصال خروجی"), + ("Exit", "خروج"), + ("Open", "باز کردن"), + ("logout_tip", "آیا برای خارج شدن مطمئن هستید؟"), + ("Service", "سرویس"), + ("Start", "شروع"), + ("Stop", "توقف"), + ("exceed_max_devices", "شما به حداکثر تعداد دستگاه های مدیریت شده رسیده اید."), + ("Sync with recent sessions", "با جلسات اخیر همگام سازی کنید"), + ("Sort tags", "مرتب سازی برچسب ها"), + ("Open connection in new tab", "اتصال را در تب جدید باز کنید"), + ("Move tab to new window", "تب را به پنجره جدید منتقل کنید"), + ("Can not be empty", "نمیتواند خالی باشد"), + ("Already exists", "درحال حاضر وجود دارد"), + ("Change Password", "رمز عبور را تغییر دهید"), + ("Refresh Password", "رمز عبور را تازه کنید"), + ("ID", "شناسه"), + ("Grid View", "نمای توری شکل"), + ("List View", "نمایش به صورت لیست"), + ("Select", "انتخاب کنید"), + ("Toggle Tags", "تگ ها را تغییر دهید"), + ("pull_ab_failed_tip", "دفترچه آدرس بازخوانی نشد"), + ("push_ab_failed_tip", "دفترچه آدرس با سرور همگام سازی نشد"), + ("synced_peer_readded_tip", "دستگاه هایی که در جلسات اخیر حضور داشتند با دفترچه آدرس همگام سازی می شوند"), + ("Change Color", "تغییر رنگ"), + ("Primary Color", "رنگ اولیه"), + ("HSV Color", "رنگ HSV"), + ("Installation Successful!", "نصب با موفقیت انجام شد!"), + ("Installation failed!", "نصب انجام نشد!"), + ("Reverse mouse wheel", "معکوس کردن چرخ موس"), + ("{} sessions", "{} جلسه"), + ("scam_title", "ممکن است شما در حال کلاهبرداری باشید!"), + ("scam_text1", "استفاده کنید و سرویس را راه‌اندازی کنید، صحبت می‌کنید، ادامه ندهید و بلافاصله تلفن را قطع کنید RustDesk اگر با شخصی که نمی‌شناسید و به او اعتماد ندارید و از شما خواسته است از"), + ("scam_text2", "آنها احتمالا یک کلاهبردار هستند که سعی در سرقت پول یا سایر اطلاعات خصوصی شما دارند."), + ("Don't show again", "دیگر نشان نده"), + ("I Agree", "موافقم"), + ("Decline", "نمی پذیرم"), + ("Timeout in minutes", "مدت زمان انتظار به دقیقه"), + ("auto_disconnect_option_tip", "بسته شدن خودکار جلسات ورودی در صورت عدم فعالیت کاربر"), + ("Connection failed due to inactivity", "اتصال به دلیل عدم فعالیت بسته شد"), + ("Check for software update on startup", "در هنگلم شروع برنامه بروزرسانی را بررسی کن"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "را به نسخه {} یا جدیدتر ارتقا دهید RustDesk Server Pro لظفا"), + ("pull_group_failed_tip", "گروه بازخوانی نشد"), + ("Filter by intersection", "فیلتر بر اساس اشتراک"), + ("Remove wallpaper during incoming sessions", "را در جلسات ورودی حذف کنید Wallpaper"), + ("Test", "تست"), + ("display_is_plugged_out_msg", "صفحه نمایش قطع شده است، به صفحه نمایش اول بروید."), + ("No displays", "بدون نمایشگر"), + ("Open in new window", "باز کردن در پنجره جدید"), + ("Show displays as individual windows", "نمایش نمایشگرها به عنوان پنجره های جداگانه"), + ("Use all my displays for the remote session", "از همه نمایشگرهای من برای جلسه راه دور استفاده کنید"), + ("selinux_tip", "به عنوان کنترل شده جلوگیری کند RustDesk روی دستگاه شما فعال است ، که ممکن است از اجرای صحیح SELinux"), + ("Change view", "تغییر نمای"), + ("Big tiles", "کاشی های بزرگ"), + ("Small tiles", "کاشی های کوچک"), + ("List", "لیست"), + ("Virtual display", "نمایش مجازی"), + ("Plug out all", "همه را خارج کنید"), + ("True color (4:4:4)", "رنگ واقعی (4:4:4)"), + ("Enable blocking user input", "مسدود کردن ورودی کاربر را فعال کنید"), + ("id_input_tip", " \"@public\" :برای دسترسی به سرورهای عمومی نیازی به کلید نیست ، مثل \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\n برای مثال (@?key=) :اگر می خواهید به دستگاه دیگری در سروری دسترسی پیدا کنید ، ادرس سرور را اضافه نمایید ماتتد \n (domain:port)یا یک دامنه با پورت را وارد کنید IP شما می توانید یک شناسه یا یک"), + ("privacy_mode_impl_mag_tip", "حالت 1"), + ("privacy_mode_impl_virtual_display_tip", "حالت 2"), + ("Enter privacy mode", "ورود به حالت حریم خصوصی"), + ("Exit privacy mode", "خروج از حالت حریم خصوصی"), + ("idd_not_support_under_win10_2004_tip", "درایور نمایش غیر مستقیم پشتیبانی نمی شود. ویندوز 10، نسخه 2004 یا جدیدتر مورد نیاز است"), + ("input_source_1_tip", "منبع ورودی 1"), + ("input_source_2_tip", "منبع ورودی 2"), + ("Swap control-command key", "گرفتن چندین نمایشگر در حالت کاربر زیاد پشتیبانی نمی شود. اگر می‌خواهید چند نمایشگر را کنترل کنید، لطفاً پس از نصب دوباره امتحان کنید."), + ("swap-left-right-mouse", "دکمه چپ و راست ماوس را عوض کنید"), + ("2FA code", "کد ورود 2 مرحله ای"), + ("More", "بیشتر"), + ("enable-2fa-title", "احراز هویت دو مرحله ای را فعال کنید"), + ("enable-2fa-desc", "بارکد سه بعدی را اسکن کنید و کد نمایش داده شده در برنامه را وارد کنید تا احراز هویت دو مرحله ای فعال گردد n\n برروی تلفن همراه خود استفاده کنید ، Authy, Microsoft یا Google Authenticator لطفاً هم اکنون برنامه تأیید کننده خود را تنظیم کنید. می توانید از یک برنامه احراز هویت مانند"), + ("wrong-2fa-code", "نمی توان کد را تأیید کرد. بررسی کنید که تنظیمات کد و زمان محلی درست باشد"), + ("enter-2fa-title", "احراز هویت دو مرحله ای"), + ("Email verification code must be 6 characters.", "کد تأیید ایمیل باید 6 کاراکتر باشد"), + ("2FA code must be 6 digits.", "کد احراز هویت دو مرحله ای باید 6 رقم باشد"), + ("Multiple Windows sessions found", "چندین جلسه پیدا شد"), + ("Please select the session you want to connect to", "لطفاً جلسه ای را که می خواهید به آن متصل شوید انتخاب کنید"), + ("powered_by_me", "Powered by RustDesk"), + ("outgoing_only_desk_tip", "این یک نسخه سفارشی شده است.\nشما می توانید به دستگاه های دیگر متصل شوید، اما دستگاه های دیگر نمی توانند به دستگاه شما متصل شوند"), + ("preset_password_warning", "این نسخه سفارشی شده با رمز عبور از پیش تعیین شده ارائه می شود. هر کسی که این رمز عبور را بداند می تواند کنترل کامل دستگاه شما را به دست آورد. اگر انتظار این را نداشتید، بلافاصله نرم افزار را حذف نصب کنید"), + ("Security Alert", "هشدار امنیتی"), + ("My address book", "دفترچه آدرس من"), + ("Personal", "شخصی"), + ("Owner", "مالک"), + ("Set shared password", "تنظیم رمز عبور مشترک"), + ("Exist in", "وجود داشته باشد"), + ("Read-only", "فقط خواندنی"), + ("Read/Write", "خواندن/نوشتن"), + ("Full Control", "تسلط کامل"), + ("share_warning_tip", "فیلدهای بالا به اشتراک گذاشته شده و برای دیگران قابل مشاهده است"), + ("Everyone", "هر کس"), + ("ab_web_console_tip", "اطلاعات بیشتر در کنسول وب"), + ("allow-only-conn-window-open-tip", "باز است اتصال برقرار شود RustDesk زمانی که"), + ("no_need_privacy_mode_no_physical_displays_tip", "بدون نمایشگر فیزیکی نیازی به استفاده از حالت خصوصی نیست"), + ("Follow remote cursor", "مکان نما ریموت را دنبال کنید"), + ("Follow remote window focus", "دنبال کردن فوکوس پنجره راه دور"), + ("default_proxy_tip", "و پورت 1080 می باشد Sock5 پرونکل پیش فرض"), + ("no_audio_input_device_tip", "دستگاه ورودی صوتی پیدا نشد"), + ("Incoming", "ورودی"), + ("Outgoing", "خروجی"), + ("Clear Wayland screen selection", "پاک کردن انتخاب صفحه Wayland"), + ("clear_Wayland_screen_selection_tip", "پس از پاک کردن صفحه انتخابی، می توانید صفحه را برای اشتراک گذاری مجدد انتخاب کنید"), + ("confirm_clear_Wayland_screen_selection_tip", "را پاک می کنید؟ Wayland آیا مطمئن هستید که انتخاب صفحه"), + ("android_new_voice_call_tip", "یک درخواست تماس صوتی جدید دریافت شد. اگر بپذیرید، صدا به ارتباط صوتی تغییر خواهد کرد."), + ("texture_render_tip", "از رندر بافت برای صاف کردن تصاویر استفاده کنید. اگر با مشکل رندر مواجه شدید، می توانید این گزینه را غیرفعال کنید."), + ("Use texture rendering", "از رندر بافت استفاده کنید"), + ("Floating window", "پنجره شناور"), + ("floating_window_tip", "کمک می کند RustDesk این به حفظ سرویس پس زمینه"), + ("Keep screen on", "صفحه نمایش را روشن نگه دارید"), + ("Never", "هرگز"), + ("During controlled", "در حین کنترل"), + ("During service is on", "در حین سرویس روشن است"), + ("Capture screen using DirectX", "DirectX تصویربرداری از صفحه نمایش با استفاده از"), + ("Back", "برگشت"), + ("Apps", "برنامه ها"), + ("Volume up", "افزایش صدا"), + ("Volume down", "کاهش صدا"), + ("Power", "پاور"), + ("Telegram bot", "ربات تلگرام"), + ("enable-bot-tip", "اگر این ویژگی را فعال کنید، می توانید کد تائید دو مرحله ای را از ربات خود دریافت کنید. همچنین می تواند به عنوان یک اعلان اتصال عمل کند."), + ("enable-bot-desc", "ربات، اعلان‌های اتصال و کدهای تأیید دو مرحله‌ای را برای شما ارسال می‌کند."), + ("cancel-2fa-confirm-tip", "آیا مطمئن هستید که می خواهید تائید دو مرحله ای را لغو کنید؟"), + ("cancel-bot-confirm-tip", "آیا مطمئن هستید که می خواهید ربات تلگرام را لغو کنید؟"), + ("About RustDesk", "RustDesk درباره"), + ("Send clipboard keystrokes", "ارسال کلیدهای کلیپ بورد"), + ("network_error_tip", "لطفاً اتصال شبکه خود را بررسی کنید، سپس روی امتحان مجدد کلیک کنید."), + ("Unlock with PIN", "باز کردن قفل با پین"), + ("Requires at least {} characters", "حداقل به {} کاراکترها نیاز دارد"), + ("Wrong PIN", "پین اشتباه است"), + ("Set PIN", "پین را تنظیم کنید"), + ("Enable trusted devices", "فعال کردن دستگاه‌های مورد اعتماد"), + ("Manage trusted devices", "مدیریت دستگاه‌های مورد اعتماد"), + ("Platform", "پلتفرم"), + ("Days remaining", "روزهای باقی‌مانده"), + ("enable-trusted-devices-tip", "فعال کردن این گزینه فقط به دستگاه‌های مورد اعتماد اجازه اتصال می‌دهد"), + ("Parent directory", "فهرست والد"), + ("Resume", "ادامه دادن"), + ("Invalid file name", "نام فایل نامعتبر است"), + ("one-way-file-transfer-tip", "انتقال فایل فقط در یک جهت انجام می‌شود"), + ("Authentication Required", "احراز هویت مورد نیاز است"), + ("Authenticate", "احراز هویت"), + ("web_id_input_tip", "لطفاً شناسه وب را وارد کنید"), + ("Download", "دانلود"), + ("Upload folder", "آپلود پوشه"), + ("Upload files", "آپلود فایل‌ها"), + ("Clipboard is synchronized", "کلیپ‌بورد همگام‌سازی شده است"), + ("Update client clipboard", "به‌روزرسانی کلیپ‌بورد کاربر"), + ("Untagged", "بدون برچسب"), + ("new-version-of-{}-tip", "نسخه جدید {} در دسترس است"), + ("Accessible devices", "دستگاه‌های در دسترس"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "لطفاً RustDesk را به نسخه {} یا جدیدتر در سمت راه دور ارتقا دهید"), + ("d3d_render_tip", "فعال کردن رندر D3D برای عملکرد بهتر"), + ("Use D3D rendering", "استفاده از رندر D3D"), + ("Printer", "چاپگر"), + ("printer-os-requirement-tip", "سیستم‌عامل شما باید از چاپ از راه دور پشتیبانی کند"), + ("printer-requires-installed-{}-client-tip", "برای استفاده از چاپگر، کلاینت {} باید نصب باشد"), + ("printer-{}-not-installed-tip", "چاپگر {} نصب نشده است"), + ("printer-{}-ready-tip", "چاپگر {} آماده است"), + ("Install {} Printer", "{} نصب چاپگر"), + ("Outgoing Print Jobs", "وظایف چاپ خروجی"), + ("Incoming Print Jobs", "وظایف چاپ ورودی"), + ("Incoming Print Job", "وظیفه چاپ ورودی"), + ("use-the-default-printer-tip", "از چاپگر پیش‌فرض استفاده کنید"), + ("use-the-selected-printer-tip", "از چاپگر انتخاب‌شده استفاده کنید"), + ("auto-print-tip", "چاپ خودکار فعال است"), + ("print-incoming-job-confirm-tip", "آیا می‌خواهید کار چاپ ورودی را تأیید کنید"), + ("remote-printing-disallowed-tile-tip", "چاپ از راه دور غیرفعال است"), + ("remote-printing-disallowed-text-tip", "شما مجوز لازم برای چاپ از راه دور را ندارید"), + ("save-settings-tip", "تنظیمات را ذخیره کنید"), + ("dont-show-again-tip", "دیگر نمایش داده نشود"), + ("Take screenshot", "عکس گرفتن"), + ("Taking screenshot", "در حال گرفتن عکس"), + ("screenshot-merged-screen-not-supported-tip", "ادغام تصاویر از نمایشگرهای متعدد در حال حاضر پشتیبانی نمی شود. لطفاً به یک صفحه نمایش واحد تغییر دهید و دوباره امتحان کنید."), + ("screenshot-action-tip", "لطفاً نحوه ادامه با تصویر را انتخاب کنید."), + ("Save as", "ذخیره به عنوان"), + ("Copy to clipboard", "در کلیپ بورد کپی کنید"), + ("Enable remote printer", "چاپگر از راه دور را فعال کنید"), + ("Downloading {}", "بارگیری {}"), + ("{} Update", "{} به روز رسانی"), + ("{}-to-update-tip", "{} اکنون بسته خواهد شد و نسخه جدید را نصب می کند."), + ("download-new-version-failed-tip", "بارگیری ناموفق بود. می توانید دوباره امتحان کنید یا روی دکمه 'بارگیری' کلیک کنید تا از صفحه انتشار بارگیری کنید و به صورت دستی ارتقا دهید."), + ("Auto update", "بروزرسانی خودکار"), + ("update-failed-check-msi-tip", "بررسی روش نصب انجام نشد. لطفاً برای بارگیری از صفحه انتشار ، روی دکمه 'بارگیری' کلیک کنید و به صورت دستی ارتقا دهید."), + ("websocket_tip", "فقط اتصالات رله پشتیبانی می شوند ، WebSocket هنگام استفاده از ."), + ("Use WebSocket", "استفاده کنید WebSocket از"), + ("Trackpad speed", "سرعت ترک‌پد"), + ("Default trackpad speed", "سرعت پیش‌فرض ترک‌پد"), + ("Numeric one-time password", "رمز عبور یک‌بار مصرف عددی"), + ("Enable IPv6 P2P connection", "فعال‌سازی اتصال همتا‌به‌همتای IPv6"), + ("Enable UDP hole punching", "فعال‌سازی تکنیک UDP hole punching"), + ("View camera", "نمایش دوربین"), + ("Enable camera", "فعال کردن دوربین"), + ("No cameras", "هیچ دوربینی یافت نشد"), + ("view_camera_unsupported_tip", "ریموت از مشاهده دوربین پشتیبانی نمی کند."), + ("Terminal", "ترمینال"), + ("Enable terminal", "فعال‌سازی ترمینال"), + ("New tab", "زبانه جدید"), + ("Keep terminal sessions on disconnect", "حفظ جلسات ترمینال پس از قطع اتصال"), + ("Terminal (Run as administrator)", "ترمینال (اجرای به عنوان مدیر سیستم)"), + ("terminal-admin-login-tip", "برای اجرای ترمینال به‌ عنوان مدیر، نام کاربری و رمز عبور مدیر سیستم ریموت را وارد کنید."), + ("Failed to get user token.", "دریافت توکن کاربر ناموفق بود."), + ("Incorrect username or password.", "نام کاربری یا رمز عبور اشتباه است."), + ("The user is not an administrator.", "کاربر دارای دسترسی مدیر سیستم نیست."), + ("Failed to check if the user is an administrator.", "بررسی وضعیت مدیر سیستم برای کاربر ناموفق بود."), + ("Supported only in the installed version.", "فقط در نسخه نصب‌ شده پشتیبانی می‌شود."), + ("elevation_username_tip", "وارد نمایید domain\\username یا username نام کاربری را به صورت"), + ("Preparing for installation ...", "در حال آماده‌سازی برای نصب..."), + ("Show my cursor", "نمایش نشانگر من"), + ("Scale custom", "مقیاس سفارشی"), + ("Custom scale slider", "نوار لغزنده مقیاس سفارشی"), + ("Decrease", "کاهش"), + ("Increase", "افزایش"), + ("Show virtual mouse", "نمایش ماوس مجازی"), + ("Virtual mouse size", "اندازه ماوس مجازی"), + ("Small", "کوچک"), + ("Large", "بزرگ"), + ("Show virtual joystick", "نمایش جوی‌استیک مجازی"), + ("Edit note", "ویرایش یادداشت"), + ("Alias", "نام مستعار"), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", "استفاده از TLS غیر امن در ارتباط"), + ("allow-insecure-tls-fallback-tip", "به‌طور پیش‌فرض، RustDesk گواهی سرور را برای پروتکل‌ها با استفاده از TLS تأیید می‌کند.\nبا فعال بودن این گزینه، RustDesk دوباره مرحله تأیید را رد می‌کند و در صورت عدم موفقیت تأیید ادامه می‌دهد."), + ("Disable UDP", "UDP غیر فعال کردن"), + ("disable-udp-tip", "کنترل می کند که آیا فقط از TCP استفاده شود یا خیر.\nوقتی این گزینه فعال باشد، RustDesk دیگر از UDP 21116 استفاده نمی کند، به جای آن از TCP 21116 استفاده می شود."), + ("server-oss-not-support-tip", "توجه: سرور RustDesk OSS این ویژگی را ندارد."), + ("input note here", "یادداشت را اینجا وارد کنید"), + ("note-at-conn-end-tip", "در پایان اتصال، یادداشت بخواهید"), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "ادامه با {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/fi.rs b/vendor/rustdesk/src/lang/fi.rs new file mode 100644 index 0000000..1bddd39 --- /dev/null +++ b/vendor/rustdesk/src/lang/fi.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Tila"), + ("Your Desktop", "Oma työpöytä"), + ("desk_tip", "Työpöytääsi voidaan käyttää tällä tunnuksella ja salasanalla."), + ("Password", "Salasana"), + ("Ready", "Valmis"), + ("Established", "Yhdistetty"), + ("connecting_status", "Yhdistetään RustDesk verkkoon..."), + ("Enable service", "Ota palvelu käyttöön"), + ("Start service", "Käynnistä palvelu"), + ("Service is running", "Palvelu on käynnissä"), + ("Service is not running", "Palvelu ei ole käynnissä"), + ("not_ready_status", "Ei valmis tarkista yhteys."), + ("Control Remote Desktop", "Hallitse etätyöpöytää"), + ("Transfer file", "Siirrä tiedosto"), + ("Connect", "Yhdistä"), + ("Recent sessions", "Viimeisimmät istunnot"), + ("Address book", "Osoitekirja"), + ("Confirmation", "Vahvistus"), + ("TCP tunneling", "TCP tunnelointi"), + ("Remove", "Poista"), + ("Refresh random password", "Päivitä satunnainen salasana"), + ("Set your own password", "Aseta oma salasana"), + ("Enable keyboard/mouse", "Salli näppäimistö ja hiiri"), + ("Enable clipboard", "Salli leikepöytä"), + ("Enable file transfer", "Salli tiedostonsiirto"), + ("Enable TCP tunneling", "Salli TCP tunnelointi"), + ("IP Whitelisting", "IP osoitteiden sallintalista"), + ("ID/Relay Server", "ID/Välityspalvelin"), + ("Import server config", "Tuo palvelimen asetukset"), + ("Export Server Config", "Vie palvelimen asetukset"), + ("Import server configuration successfully", "Palvelimen asetukset tuotu onnistuneesti"), + ("Export server configuration successfully", "Palvelimen asetukset viety onnistuneesti"), + ("Invalid server configuration", "Virheellinen palvelimen määritys"), + ("Clipboard is empty", "Leikepöytä on tyhjä"), + ("Stop service", "Pysäytä palvelu"), + ("Change ID", "Vaihda ID"), + ("Your new ID", "Uusi ID"), + ("length %min% to %max%", "pituus %min%–%max%"), + ("starts with a letter", "alkaa kirjaimella"), + ("allowed characters", "sallitut merkit"), + ("id_change_tip", "Sallitut merkit: a–z, A–Z, 0–9, - ja _. Ensimmäisen merkin on oltava kirjain. Pituus 6–16 merkkiä."), + ("Website", "Verkkosivusto"), + ("About", "Tietoa"), + ("Slogan_tip", "Tehty sydämellä tässä kaoottisessa maailmassa!"), + ("Privacy Statement", "Tietosuojaseloste"), + ("Mute", "Mykistä"), + ("Build Date", "Koontipäivä"), + ("Version", "Versio"), + ("Home", "Etusivu"), + ("Audio Input", "Äänitulo"), + ("Enhancements", "Parannukset"), + ("Hardware Codec", "Laitteistokoodekki"), + ("Adaptive bitrate", "Mukautuva bittinopeus"), + ("ID Server", "ID palvelin"), + ("Relay Server", "Välityspalvelin"), + ("API Server", "API palvelin"), + ("invalid_http", "Osoitteen on alettava http:// tai https://"), + ("Invalid IP", "Virheellinen IP osoite"), + ("Invalid format", "Virheellinen muoto"), + ("server_not_support", "Palvelin ei tue tätä ominaisuutta"), + ("Not available", "Ei saatavilla"), + ("Too frequent", "Liian tiheä pyyntö"), + ("Cancel", "Peruuta"), + ("Skip", "Ohita"), + ("Close", "Sulje"), + ("Retry", "Yritä uudelleen"), + ("OK", "OK"), + ("Password Required", "Salasana vaaditaan"), + ("Please enter your password", "Syötä salasanasi"), + ("Remember password", "Muista salasana"), + ("Wrong Password", "Väärä salasana"), + ("Do you want to enter again?", "Haluatko yrittää uudelleen?"), + ("Connection Error", "Yhteysvirhe"), + ("Error", "Virhe"), + ("Reset by the peer", "Yhteys katkaistu vastapuolen toimesta"), + ("Connecting...", "Yhdistetään..."), + ("Connection in progress. Please wait.", "Yhdistetään – odota hetki."), + ("Please try 1 minute later", "Yritä uudelleen minuutin kuluttua"), + ("Login Error", "Kirjautumisvirhe"), + ("Successful", "Onnistui"), + ("Connected, waiting for image...", "Yhdistetty, odotetaan kuvaa..."), + ("Name", "Nimi"), + ("Type", "Tyyppi"), + ("Modified", "Muokattu"), + ("Size", "Koko"), + ("Show Hidden Files", "Näytä piilotetut tiedostot"), + ("Receive", "Vastaanota"), + ("Send", "Lähetä"), + ("Refresh File", "Päivitä tiedosto"), + ("Local", "Paikallinen"), + ("Remote", "Etä"), + ("Remote Computer", "Etätietokone"), + ("Local Computer", "Paikallinen tietokone"), + ("Confirm Delete", "Vahvista poisto"), + ("Delete", "Poista"), + ("Properties", "Ominaisuudet"), + ("Multi Select", "Monivalinta"), + ("Select All", "Valitse kaikki"), + ("Unselect All", "Poista kaikki valinnat"), + ("Empty Directory", "Tyhjä kansio"), + ("Not an empty directory", "Hakemisto ei ole tyhjä"), + ("Are you sure you want to delete this file?", "Haluatko varmasti poistaa tämän tiedoston?"), + ("Are you sure you want to delete this empty directory?", "Haluatko varmasti poistaa tämän tyhjän hakemiston?"), + ("Are you sure you want to delete the file of this directory?", "Haluatko varmasti poistaa tämän hakemiston tiedoston?"), + ("Do this for all conflicts", "Tee sama kaikille ristiriidoille"), + ("This is irreversible!", "Tätä toimintoa ei voi perua!"), + ("Deleting", "Poistetaan"), + ("files", "tiedostoa"), + ("Waiting", "Odotetaan"), + ("Finished", "Valmis"), + ("Speed", "Nopeus"), + ("Custom Image Quality", "Mukautettu kuvanlaatu"), + ("Privacy mode", "Yksityisyystila"), + ("Block user input", "Estä käyttäjän toiminta"), + ("Unblock user input", "Salli käyttäjän toiminta"), + ("Adjust Window", "Sovita ikkuna"), + ("Original", "Alkuperäinen"), + ("Shrink", "Pienennä"), + ("Stretch", "Venytä"), + ("Scrollbar", "Vierityspalkki"), + ("ScrollAuto", "Automaattinen vieritys"), + ("Good image quality", "Hyvä kuvanlaatu"), + ("Balanced", "Tasapainotettu"), + ("Optimize reaction time", "Optimoi vasteaika"), + ("Custom", "Mukautettu"), + ("Show remote cursor", "Näytä etäkursori"), + ("Show quality monitor", "Näytä laadunvalvonta"), + ("Disable clipboard", "Poista leikepöytä käytöstä"), + ("Lock after session end", "Lukitse istunnon päätyttyä"), + ("Insert Ctrl + Alt + Del", "Lähetä Ctrl + Alt + Del"), + ("Insert Lock", "Aseta lukitse"), + ("Refresh", "Päivitä"), + ("ID does not exist", "Tunnusta ei ole olemassa"), + ("Failed to connect to rendezvous server", "Yhteys tapaamispalvelimeen epäonnistui"), + ("Please try later", "Yritä myöhemmin uudelleen"), + ("Remote desktop is offline", "Etätyöpöytä ei ole online tilassa"), + ("Key mismatch", "Avaimet eivät täsmää"), + ("Timeout", "Aikakatkaisu"), + ("Failed to connect to relay server", "Yhteys välityspalvelimeen epäonnistui"), + ("Failed to connect via rendezvous server", "Yhteys tapaamispalvelimen kautta epäonnistui"), + ("Failed to connect via relay server", "Yhteys välityspalvelimen kautta epäonnistui"), + ("Failed to make direct connection to remote desktop", "Suora yhteys etätyöpöytään epäonnistui"), + ("Set Password", "Aseta salasana"), + ("OS Password", "Käyttöjärjestelmän salasana"), + ("install_tip", "Joissain tapauksissa RustDesk ei toimi oikein etäpuolella UAC:n vuoksi. Välttääksesi tämän, napsauta alla olevaa painiketta asentaaksesi RustDeskin järjestelmään."), + ("Click to upgrade", "Päivitä napsauttamalla"), + ("Configure", "Määritä"), + ("config_acc", "Etätyöpöydän hallintaa varten sinun on annettava RustDeskille ”Esteettömyys”-oikeudet."), + ("config_screen", "Etätyöpöydän käyttöä varten sinun on annettava RustDeskille ”Näytön tallennus” oikeudet."), + ("Installing ...", "Asennetaan ..."), + ("Install", "Asenna"), + ("Installation", "Asennus"), + ("Installation Path", "Asennuspolku"), + ("Create start menu shortcuts", "Luo pikakuvakkeet Käynnistä valikkoon"), + ("Create desktop icon", "Luo kuvake työpöydälle"), + ("agreement_tip", "Aloittamalla asennuksen hyväksyt käyttöoikeussopimuksen."), + ("Accept and Install", "Hyväksy ja asenna"), + ("End-user license agreement", "Käyttöoikeussopimus"), + ("Generating ...", "Luodaan ..."), + ("Your installation is lower version.", "Asennettu versio on vanhempi."), + ("not_close_tcp_tip", "Älä sulje tätä ikkunaa tunnelin ollessa käytössä"), + ("Listening ...", "Kuunnellaan ..."), + ("Remote Host", "Etätietokone"), + ("Remote Port", "Etäportti"), + ("Action", "Toiminto"), + ("Add", "Lisää"), + ("Local Port", "Paikallinen portti"), + ("Local Address", "Paikallinen osoite"), + ("Change Local Port", "Vaihda paikallinen porttia"), + ("setup_server_tip", "Nopeampaa yhteyttä varten voit asettaa oman palvelimen"), + ("Too short, at least 6 characters.", "Liian lyhyt, vähintään 6 merkkiä."), + ("The confirmation is not identical.", "Vahvistus ei täsmää."), + ("Permissions", "Oikeudet"), + ("Accept", "Hyväksy"), + ("Dismiss", "Hylkää"), + ("Disconnect", "Katkaise yhteys"), + ("Enable file copy and paste", "Salli tiedostojen kopiointi ja liittäminen"), + ("Connected", "Yhdistetty"), + ("Direct and encrypted connection", "Suora ja salattu yhteys"), + ("Relayed and encrypted connection", "Välitetty ja salattu yhteys"), + ("Direct and unencrypted connection", "Suora ja salaamaton yhteys"), + ("Relayed and unencrypted connection", "Välitetty ja salaamaton yhteys"), + ("Enter Remote ID", "Anna ID"), + ("Enter your password", "Syötä salasanasi"), + ("Logging in...", "Kirjaudutaan sisään..."), + ("Enable RDP session sharing", "Salli RDP istunnon jakaminen"), + ("Auto Login", "Automaattinen kirjautuminen"), + ("Enable direct IP access", "Salli suora IP yhteys"), + ("Rename", "Nimeä uudelleen"), + ("Space", "Välilyönti"), + ("Create desktop shortcut", "Luo työpöydän pikakuvake"), + ("Change Path", "Vaihda polku"), + ("Create Folder", "Luo kansio"), + ("Please enter the folder name", "Anna kansion nimi"), + ("Fix it", "Korjaa"), + ("Warning", "Varoitus"), + ("Login screen using Wayland is not supported", "Kirjautumisnäyttö Waylandilla ei ole tuettu"), + ("Reboot required", "Uudelleenkäynnistys vaaditaan"), + ("Unsupported display server", "Näyttöpalvelin ei ole tuettu"), + ("x11 expected", "X11 odotettu"), + ("Port", "Portti"), + ("Settings", "Asetukset"), + ("Username", "Käyttäjänimi"), + ("Invalid port", "Virheellinen portti"), + ("Closed manually by the peer", "Suljettu vastapuolen toimesta"), + ("Enable remote configuration modification", "Salli etäasetusten muokkaus"), + ("Run without install", "Suorita ilman asennusta"), + ("Connect via relay", "Yhdistä välityspalvelimen kautta"), + ("Always connect via relay", "Yhdistä aina välityspalvelimen kautta"), + ("whitelist_tip", "Vain sallitut IP osoitteet voivat muodostaa yhteyden"), + ("Login", "Kirjaudu sisään"), + ("Verify", "Vahvista"), + ("Remember me", "Muista minut"), + ("Trust this device", "Luota tähän laitteeseen"), + ("Verification code", "Vahvistuskoodi"), + ("verification_tip", "Vahvistuskoodi on lähetetty rekisteröityyn sähköpostiosoitteeseen. Syötä koodi jatkaaksesi kirjautumista."), + ("Logout", "Kirjaudu ulos"), + ("Tags", "Tunnisteet"), + ("Search ID", "Hae ID"), + ("whitelist_sep", "Valkoisen listan erotin"), + ("Add ID", "Lisää ID"), + ("Add Tag", "Lisää tunniste"), + ("Unselect all tags", "Poista kaikki tunnistevalinnat"), + ("Network error", "Verkkovirhe"), + ("Username missed", "Käyttäjänimi puuttuu"), + ("Password missed", "Salasana puuttuu"), + ("Wrong credentials", "Virheelliset kirjautumistiedot"), + ("The verification code is incorrect or has expired", "Vahvistuskoodi on virheellinen tai vanhentunut"), + ("Edit Tag", "Muokkaa tunnistetta"), + ("Forget Password", "Unohditko salasanasi"), + ("Favorites", "Suosikit"), + ("Add to Favorites", "Lisää suosikkeihin"), + ("Remove from Favorites", "Poista suosikeista"), + ("Empty", "Tyhjä"), + ("Invalid folder name", "Virheellinen kansion nimi"), + ("Socks5 Proxy", "Socks5 välityspalvelin"), + ("Socks5/Http(s) Proxy", "Socks5/HTTP(s)-välityspalvelin"), + ("Discovered", "Löydetty"), + ("install_daemon_tip", "Palvelun automaattista käynnistystä varten RustDesk daemon on asennettava järjestelmään."), + ("Remote ID", "Etätunnus"), + ("Paste", "Liitä"), + ("Paste here?", "Liitä tähän?"), + ("Are you sure to close the connection?", "Haluatko varmasti katkaista yhteyden?"), + ("Download new version", "Lataa uusi versio"), + ("Touch mode", "Kosketustila"), + ("Mouse mode", "Hiiritila"), + ("One-Finger Tap", "Yksi sormipainallus"), + ("Left Mouse", "Vasen hiiren painike"), + ("One-Long Tap", "Pitkä painallus yhdellä sormella"), + ("Two-Finger Tap", "Kahden sormen napautus"), + ("Right Mouse", "Oikea hiiren painike"), + ("One-Finger Move", "Yhden sormen liike"), + ("Double Tap & Move", "Kaksoisnapautus ja liike"), + ("Mouse Drag", "Vedä hiirellä"), + ("Three-Finger vertically", "Kolmen sormen pystysuora liike"), + ("Mouse Wheel", "Hiiren rulla"), + ("Two-Finger Move", "Kahden sormen liike"), + ("Canvas Move", "Siirrä näkymää"), + ("Pinch to Zoom", "Lähennä tai loitonna"), + ("Canvas Zoom", "Suurennus"), + ("Reset canvas", "Palauta näkymä"), + ("No permission of file transfer", "Ei oikeutta tiedostonsiirtoon"), + ("Note", "Huomautus"), + ("Connection", "Yhteys"), + ("Share screen", "Jaa näyttö"), + ("Chat", "Keskustelu"), + ("Total", "Yhteensä"), + ("items", "kohdetta"), + ("Selected", "Valittu"), + ("Screen Capture", "Näytön kaappaus"), + ("Input Control", "Tulon hallinta"), + ("Audio Capture", "Äänen tallennus"), + ("Do you accept?", "Hyväksytkö?"), + ("Open System Setting", "Avaa järjestelmäasetukset"), + ("How to get Android input permission?", "Kuinka myöntää Androidin oikeudet?"), + ("android_input_permission_tip1", "Siirry Androidin asetuksiin ja ota RustDeskille käyttöön 'Syötteen ohjaus' oikeus."), + ("android_input_permission_tip2", "Jos et löydä asetusta, etsi 'Esteettömyys' ja salli RustDesk ohjelman käyttö."), + ("android_new_connection_tip", "Uusi yhteyspyyntö vastaanotettu."), + ("android_service_will_start_tip", "RustDesk palvelu käynnistyy taustalla."), + ("android_stop_service_tip", "Pysäytä taustapalvelu tarvittaessa RustDeskin asetuksista."), + ("android_version_audio_tip", "Äänensiirto vaatii Android 10:n tai uudemman."), + ("android_start_service_tip", "RustDesk palvelu käynnistetään..."), + ("android_permission_may_not_change_tip", "Oikeudet eivät ehkä päivity heti. Käynnistä sovellus uudelleen, jos muutokset eivät tule voimaan."), + ("Account", "Tili"), + ("Overwrite", "Korvaa"), + ("This file exists, skip or overwrite this file?", "Tämä tiedosto on jo olemassa, ohitetaanko vai korvataanko se?"), + ("Quit", "Poistu"), + ("Help", "Ohje"), + ("Failed", "Epäonnistui"), + ("Succeeded", "Onnistui"), + ("Someone turns on privacy mode, exit", "Yksityisyystila otettu käyttöön, poistutaan"), + ("Unsupported", "Ei tuettu"), + ("Peer denied", "Vastapuoli hylkäsi pyynnön"), + ("Please install plugins", "Asenna tarvittavat lisäosat"), + ("Peer exit", "Vastapuoli sulki yhteyden"), + ("Failed to turn off", "Sammutus epäonnistui"), + ("Turned off", "Sammutettu"), + ("Language", "Kieli"), + ("Keep RustDesk background service", "Pidä RustDeskin taustapalvelu käynnissä"), + ("Ignore Battery Optimizations", "Ohita akun optimoinnit"), + ("android_open_battery_optimizations_tip", "Poista RustDeskin akkuoptimointi, jotta yhteys pysyy vakaana taustalla."), + ("Start on boot", "Käynnistä automaattisesti laitteen käynnistyessä"), + ("Start the screen sharing service on boot, requires special permissions", "Käynnistä näytönjakopalvelu laitteen käynnistyessä (vaatii erityisoikeudet)"), + ("Connection not allowed", "Yhteyttä ei sallita"), + ("Legacy mode", "Perinteinen tila"), + ("Map mode", "Karttatila"), + ("Translate mode", "Käännöstila"), + ("Use permanent password", "Käytä pysyvää salasanaa"), + ("Use both passwords", "Käytä molempia salasanoja"), + ("Set permanent password", "Aseta pysyvä salasana"), + ("Enable remote restart", "Salli etäuudelleenkäynnistys"), + ("Restart remote device", "Käynnistä etälaite uudelleen"), + ("Are you sure you want to restart", "Haluatko varmasti käynnistää laitteen uudelleen?"), + ("Restarting remote device", "Etälaitetta käynnistetään uudelleen"), + ("remote_restarting_tip", "Odota, kunnes etälaite käynnistyy uudelleen ja muodostaa yhteyden."), + ("Copied", "Kopioitu"), + ("Exit Fullscreen", "Poistu koko näytöstä"), + ("Fullscreen", "Koko näyttö"), + ("Mobile Actions", "Puhelin toiminnot"), + ("Select Monitor", "Valitse näyttö"), + ("Control Actions", "Ohjaustoiminnot"), + ("Display Settings", "Näyttöasetukset"), + ("Ratio", "Suhde"), + ("Image Quality", "Kuvanlaatu"), + ("Scroll Style", "Vieritystyyli"), + ("Show Toolbar", "Näytä työkalupalkki"), + ("Hide Toolbar", "Piilota työkalupalkki"), + ("Direct Connection", "Suora yhteys"), + ("Relay Connection", "Välitetty yhteys"), + ("Secure Connection", "Suojattu yhteys"), + ("Insecure Connection", "Suojaamaton yhteys"), + ("Scale original", "Skaalaa alkuperäinen"), + ("Scale adaptive", "Mukautuva skaalaus"), + ("General", "Yleiset"), + ("Security", "Turvallisuus"), + ("Theme", "Teema"), + ("Dark Theme", "Tumma teema"), + ("Light Theme", "Vaalea teema"), + ("Dark", "Tumma"), + ("Light", "Vaalea"), + ("Follow System", "Seuraa järjestelmän teemaa"), + ("Enable hardware codec", "Käytä laitteistokoodausta"), + ("Unlock Security Settings", "Avaa suojausasetukset"), + ("Enable audio", "Ota ääni käyttöön"), + ("Unlock Network Settings", "Avaa verkkoasetukset"), + ("Server", "Palvelin"), + ("Direct IP Access", "Suora IP yhteys"), + ("Proxy", "Välityspalvelin"), + ("Apply", "Käytä"), + ("Disconnect all devices?", "Katkaistaanko yhteys kaikkiin laitteisiin?"), + ("Clear", "Tyhjennä"), + ("Audio Input Device", "Äänitulolaite"), + ("Use IP Whitelisting", "Käytä IP sallitut listaa"), + ("Network", "Verkko"), + ("Pin Toolbar", "Kiinnitä työkalupalkki"), + ("Unpin Toolbar", "Irrota työkalupalkki"), + ("Recording", "Tallennus"), + ("Directory", "Hakemisto"), + ("Automatically record incoming sessions", "Tallenna saapuvat istunnot automaattisesti"), + ("Automatically record outgoing sessions", "Tallenna lähtevät istunnot automaattisesti"), + ("Change", "Vaihda"), + ("Start session recording", "Aloita istunnon tallennus"), + ("Stop session recording", "Lopeta istunnon tallennus"), + ("Enable recording session", "Ota istunnon tallennus käyttöön"), + ("Enable LAN discovery", "Ota LAN havaitseminen käyttöön"), + ("Deny LAN discovery", "Estä LAN havaitseminen"), + ("Write a message", "Kirjoita viesti"), + ("Prompt", "Kehote"), + ("Please wait for confirmation of UAC...", "Odota UAC hyväksyntää..."), + ("elevated_foreground_window_tip", "Käyttäjävalvontaikkuna on etualalla, hyväksy pyyntö etäkäytön jatkamiseksi."), + ("Disconnected", "Yhteys katkaistu"), + ("Other", "Muu"), + ("Confirm before closing multiple tabs", "Vahvista ennen useiden välilehtien sulkemista"), + ("Keyboard Settings", "Näppäimistöasetukset"), + ("Full Access", "Täysi käyttöoikeus"), + ("Screen Share", "Näytönjako"), + ("ubuntu-21-04-required", "Wayland vaatii Ubuntu 21.04:n tai uudemman version."), + ("wayland-requires-higher-linux-version", "Wayland vaatii uudemman Linux jakelun version. Kokeile X11 työpöytää tai vaihda käyttöjärjestelmää."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Pikalinkki"), + ("Please Select the screen to be shared(Operate on the peer side).", "Valitse jaettava näyttö (toiminto etäpäässä)."), + ("Show RustDesk", "Näytä RustDesk"), + ("This PC", "Tämä tietokone"), + ("or", "tai"), + ("Elevate", "Korota oikeudet"), + ("Zoom cursor", "Suurennusosoitin"), + ("Accept sessions via password", "Hyväksy istunnot salasanalla"), + ("Accept sessions via click", "Hyväksy istunnot napsauttamalla"), + ("Accept sessions via both", "Hyväksy istunnot kummallakin tavalla"), + ("Please wait for the remote side to accept your session request...", "Odota, että etäpää hyväksyy istuntopyyntösi..."), + ("One-time Password", "Kertakäyttösalasana"), + ("Use one-time password", "Käytä kertakäyttösalasanaa"), + ("One-time password length", "Kertakäyttösalasanan pituus"), + ("Request access to your device", "Pyydä pääsyä laitteeseesi"), + ("Hide connection management window", "Piilota yhteydenhallintaikkuna"), + ("hide_cm_tip", "Yhteydenhallintaikkuna voidaan piilottaa, jotta etäistunto ei keskeydy."), + ("wayland_experiment_tip", "Wayland tuki on kokeellinen ja saattaa aiheuttaa yhteysongelmia."), + ("Right click to select tabs", "Valitse välilehti hiiren oikealla painikkeella"), + ("Skipped", "Ohitettu"), + ("Add to address book", "Lisää osoitekirjaan"), + ("Group", "Ryhmä"), + ("Search", "Haku"), + ("Closed manually by web console", "Suljettu manuaalisesti verkkokonsolista"), + ("Local keyboard type", "Paikallinen näppäimistötyyppi"), + ("Select local keyboard type", "Valitse paikallinen näppäimistötyyppi"), + ("software_render_tip", "Jos laitteistokiihdytys ei toimi oikein, voit käyttää ohjelmistopohjaista renderöintiä."), + ("Always use software rendering", "Käytä aina ohjelmistopohjaista renderöintiä"), + ("config_input", "Syöteasetukset"), + ("config_microphone", "Mikrofoni"), + ("request_elevation_tip", "Etätoiminto vaatii järjestelmänvalvojan oikeudet."), + ("Wait", "Odota"), + ("Elevation Error", "Oikeuksien korotus epäonnistui"), + ("Ask the remote user for authentication", "Pyydä etäkäyttäjää vahvistamaan oikeudet"), + ("Choose this if the remote account is administrator", "Valitse tämä, jos etätili on järjestelmänvalvoja"), + ("Transmit the username and password of administrator", "Lähetä järjestelmänvalvojan käyttäjätunnus ja salasana"), + ("still_click_uac_tip", "Etäkäyttäjän on edelleen hyväksyttävä UAC kehote omalla koneellaan."), + ("Request Elevation", "Pyydä oikeuksien korotusta"), + ("wait_accept_uac_tip", "Odota, että etäkäyttäjä hyväksyy UAC pyynnön..."), + ("Elevate successfully", "Oikeuksien korotus onnistui"), + ("uppercase", "iso kirjain"), + ("lowercase", "pieni kirjain"), + ("digit", "numero"), + ("special character", "erikoismerkki"), + ("length>=8", "vähintään 8 merkkiä"), + ("Weak", "Heikko"), + ("Medium", "Keskitaso"), + ("Strong", "Vahva"), + ("Switch Sides", "Vaihda puolia"), + ("Please confirm if you want to share your desktop?", "Haluatko varmasti jakaa työpöytäsi?"), + ("Display", "Näyttö"), + ("Default View Style", "Oletusnäkymän tyyli"), + ("Default Scroll Style", "Oletusvieritys tyyli"), + ("Default Image Quality", "Oletuskuvanlaatu"), + ("Default Codec", "Oletuskoodekki"), + ("Bitrate", "Bittinopeus"), + ("FPS", "Kuvataajuus (FPS)"), + ("Auto", "Automaattinen"), + ("Other Default Options", "Muut oletusasetukset"), + ("Voice call", "Äänipuhelu"), + ("Text chat", "Tekstikeskustelu"), + ("Stop voice call", "Lopeta äänipuhelu"), + ("relay_hint_tip", "Jos suora yhteys ei toimi, käytetään automaattisesti välityspalvelinta."), + ("Reconnect", "Yhdistä uudelleen"), + ("Codec", "Koodekki"), + ("Resolution", "Resoluutio"), + ("No transfers in progress", "Ei käynnissä olevia siirtoja"), + ("Set one-time password length", "Aseta kertakäyttösalasanan pituus"), + ("RDP Settings", "RDP asetukset"), + ("Sort by", "Järjestä"), + ("New Connection", "Uusi yhteys"), + ("Restore", "Palauta"), + ("Minimize", "Pienennä"), + ("Maximize", "Suurenna"), + ("Your Device", "Sinun laitteesi"), + ("empty_recent_tip", "Ei äskettäisiä istuntoja"), + ("empty_favorite_tip", "Ei suosikkeja"), + ("empty_lan_tip", "LAN laitteita ei löytynyt"), + ("empty_address_book_tip", "Osoitekirja on tyhjä"), + ("Empty Username", "Tyhjä käyttäjänimi"), + ("Empty Password", "Tyhjä salasana"), + ("Me", "Minä"), + ("identical_file_tip", "Saman niminen tiedosto on jo olemassa"), + ("show_monitors_tip", "Näytä kaikki käytettävissä olevat näytöt"), + ("View Mode", "Näkymätila"), + ("login_linux_tip", "Kirjaudu sisään Linux käyttäjätunnuksellasi"), + ("verify_rustdesk_password_tip", "Vahvista RustDesk salasanasi kirjautumista varten"), + ("remember_account_tip", "Muista tilini kirjautumista varten"), + ("os_account_desk_tip", "Käytä käyttöjärjestelmän käyttäjätiliä kirjautumiseen"), + ("OS Account", "Käyttöjärjestelmän tili"), + ("another_user_login_title_tip", "Toinen käyttäjä on kirjautunut sisään"), + ("another_user_login_text_tip", "Etäistunto keskeytetään, koska toinen käyttäjä on ottanut hallinnan."), + ("xorg_not_found_title_tip", "Xorg ei löydy"), + ("xorg_not_found_text_tip", "X11 palvelinta ei löydetty. Vaihda Xorg ympäristöön jatkaaksesi."), + ("no_desktop_title_tip", "Työpöytää ei havaittu"), + ("no_desktop_text_tip", "Työpöytäympäristöä ei löydy. Asenna esimerkiksi GNOME tai XFCE."), + ("No need to elevate", "Oikeuksien korotusta ei tarvita"), + ("System Sound", "Järjestelmän ääni"), + ("Default", "Oletus"), + ("New RDP", "Uusi RDP yhteys"), + ("Fingerprint", "Sormenjälki"), + ("Copy Fingerprint", "Kopioi sormenjälki"), + ("no fingerprints", "Ei sormenjälkiä"), + ("Select a peer", "Valitse vastapää"), + ("Select peers", "Valitse useita vastapään laitteita"), + ("Plugins", "Laajennukset"), + ("Uninstall", "Poista asennus"), + ("Update", "Päivitä"), + ("Enable", "Ota käyttöön"), + ("Disable", "Poista käytöstä"), + ("Options", "Asetukset"), + ("resolution_original_tip", "Näytä alkuperäisessä resoluutiossa ilman skaalausta"), + ("resolution_fit_local_tip", "Sovita etänäyttö paikalliseen näkymään"), + ("resolution_custom_tip", "Käytä mukautettua resoluutiota"), + ("Collapse toolbar", "Tiivistä työkalupalkki"), + ("Accept and Elevate", "Hyväksy ja korota oikeudet"), + ("accept_and_elevate_btn_tooltip", "Hyväksy ja korota oikeudet järjestelmänvalvojaksi"), + ("clipboard_wait_response_timeout_tip", "Leikepöydän pyyntö aikakatkaistiin – ei vastausta etäpäästä."), + ("Incoming connection", "Saapuva yhteys"), + ("Outgoing connection", "Lähtevä yhteys"), + ("Exit", "Poistu"), + ("Open", "Avaa"), + ("logout_tip", "Haluatko varmasti kirjautua ulos?"), + ("Service", "Palvelu"), + ("Start", "Käynnistä"), + ("Stop", "Pysäytä"), + ("exceed_max_devices", "Olet saavuttanut hallittavien laitteiden enimmäismäärän."), + ("Sync with recent sessions", "Synkronoi viimeisimpiin istuntoihin"), + ("Sort tags", "Järjestä tunnisteet"), + ("Open connection in new tab", "Avaa yhteys uuteen välilehteen"), + ("Move tab to new window", "Siirrä välilehti uuteen ikkunaan"), + ("Can not be empty", "Ei voi olla tyhjä"), + ("Already exists", "On jo olemassa"), + ("Change Password", "Vaihda salasana"), + ("Refresh Password", "Päivitä salasana"), + ("ID", "Tunnus"), + ("Grid View", "Ruudukkonäkymä"), + ("List View", "Luettelonäkymä"), + ("Select", "Valitse"), + ("Toggle Tags", "Näytä/piilota tunnisteet"), + ("pull_ab_failed_tip", "Osoitekirjan lataus epäonnistui palvelimelta."), + ("push_ab_failed_tip", "Osoitekirjan lähetys palvelimelle epäonnistui."), + ("synced_peer_readded_tip", "Synkronoitu laite lisättiin uudelleen."), + ("Change Color", "Vaihda väri"), + ("Primary Color", "Pääväri"), + ("HSV Color", "HSV väriarvot"), + ("Installation Successful!", "Asennus onnistui!"), + ("Installation failed!", "Asennus epäonnistui!"), + ("Reverse mouse wheel", "Käänteinen hiiren rullaussuunta"), + ("{} sessions", "{} istuntoa"), + ("scam_title", "Huijausvaroitus"), + ("scam_text1", "Älä anna tuntemattomille henkilöille pääsyä tietokoneeseesi."), + ("scam_text2", "RustDesk ei koskaan pyydä maksua tai etäkäyttöä ilman lupaasi."), + ("Don't show again", "Älä näytä uudelleen"), + ("I Agree", "Hyväksyn"), + ("Decline", "Hylkää"), + ("Timeout in minutes", "Aikakatkaisu minuuteissa"), + ("auto_disconnect_option_tip", "Katkaise yhteys automaattisesti, jos ei aktiivisuutta määräaikaan mennessä."), + ("Connection failed due to inactivity", "Yhteys epäonnistui toimettomuuden vuoksi"), + ("Check for software update on startup", "Tarkista ohjelmistopäivitykset käynnistyksen yhteydessä"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Päivitä RustDesk Server Pro versioon {} jatkaaksesi."), + ("pull_group_failed_tip", "Ryhmäasetusten nouto epäonnistui."), + ("Filter by intersection", "Suodata leikkausten perusteella"), + ("Remove wallpaper during incoming sessions", "Poista taustakuva saapuvien istuntojen ajaksi"), + ("Test", "Testaa"), + ("display_is_plugged_out_msg", "Näyttö on irrotettu"), + ("No displays", "Ei näyttöjä"), + ("Open in new window", "Avaa uudessa ikkunassa"), + ("Show displays as individual windows", "Näytä näytöt erillisinä ikkunoina"), + ("Use all my displays for the remote session", "Käytä kaikkia näyttöjä etäistunnossa"), + ("selinux_tip", "SELinux saattaa estää etäyhteyden toiminnan. Tarkista asetukset."), + ("Change view", "Vaihda näkymä"), + ("Big tiles", "Suuret ruudut"), + ("Small tiles", "Pienet ruudut"), + ("List", "Lista"), + ("Virtual display", "Virtuaalinäyttö"), + ("Plug out all", "Irrota kaikki"), + ("True color (4:4:4)", "Tarkka väri (4:4:4)"), + ("Enable blocking user input", "Estä käyttäjän syöte etäpäässä"), + ("id_input_tip", "Anna etätunnus muodossa tunnus@palvelin"), + ("privacy_mode_impl_mag_tip", "Yksityisyystila käyttää suurennustekniikkaa piilottaakseen sisällön."), + ("privacy_mode_impl_virtual_display_tip", "Yksityisyystila käyttää virtuaalinäyttöä tietosuojan takaamiseksi."), + ("Enter privacy mode", "Siirry yksityisyystilaan"), + ("Exit privacy mode", "Poistu yksityisyystilasta"), + ("idd_not_support_under_win10_2004_tip", "Virtuaalinäyttöä ei tueta Windows 10 2004 versiota vanhemmissa järjestelmissä."), + ("input_source_1_tip", "Valitse syöte 1: fyysinen näppäimistö tai hiiri"), + ("input_source_2_tip", "Valitse syöte 2: virtuaalinen syöte"), + ("Swap control-command key", "Vaihda Ctrl ja Command näppäinten paikkaa"), + ("swap-left-right-mouse", "Vaihda hiiren vasen ja oikea painike"), + ("2FA code", "2FA koodi"), + ("More", "Lisää"), + ("enable-2fa-title", "Ota kaksivaiheinen todennus käyttöön"), + ("enable-2fa-desc", "Lisää turvallisuutta vahvistamalla kirjautumisesi 2FA koodilla."), + ("wrong-2fa-code", "Väärä 2FA koodi"), + ("enter-2fa-title", "Syötä 2FA koodi"), + ("Email verification code must be 6 characters.", "Sähköpostivarmennuskoodin on oltava 6 merkkiä pitkä."), + ("2FA code must be 6 digits.", "2FA koodin on oltava 6 numeroa."), + ("Multiple Windows sessions found", "Useita Windows istuntoja havaittu"), + ("Please select the session you want to connect to", "Valitse istunto, johon haluat muodostaa yhteyden"), + ("powered_by_me", "Ylpeästi kehitetty omavaraisesti"), + ("outgoing_only_desk_tip", "Tämä asennus tukee vain lähteviä yhteyksiä."), + ("preset_password_warning", "Esiasetettu salasana voi olla turvaton — vaihda se suojataksesi yhteytesi."), + ("Security Alert", "Turvailmoitus"), + ("My address book", "Oma osoitekirja"), + ("Personal", "Henkilökohtainen"), + ("Owner", "Omistaja"), + ("Set shared password", "Aseta jaettu salasana"), + ("Exist in", "Sisältyy kohteeseen"), + ("Read-only", "Vain luku"), + ("Read/Write", "Luku ja kirjoitus"), + ("Full Control", "Täysi hallinta"), + ("share_warning_tip", "Jakaminen antaa muille pääsyn laitteeseesi. Varmista, että luotat käyttäjään."), + ("Everyone", "Kaikki"), + ("ab_web_console_tip", "Osoitekirjaa voidaan hallita myös verkkokonsolin kautta."), + ("allow-only-conn-window-open-tip", "Salli vain yksi yhteyshallintaikkuna kerrallaan."), + ("no_need_privacy_mode_no_physical_displays_tip", "Yksityisyystilaa ei tarvita, koska fyysisiä näyttöjä ei ole."), + ("Follow remote cursor", "Seuraa etäosoitinta"), + ("Follow remote window focus", "Seuraa etäikkunan kohdistusta"), + ("default_proxy_tip", "Käytetään oletusarvoista välityspalvelinta, ellei muuta määritetty."), + ("no_audio_input_device_tip", "Äänitulolaitetta ei löydy."), + ("Incoming", "Saapuva"), + ("Outgoing", "Lähtevä"), + ("Clear Wayland screen selection", "Tyhjennä Wayland näyttövalinta"), + ("clear_Wayland_screen_selection_tip", "Tyhjentää nykyisen Wayland näytön valinnan."), + ("confirm_clear_Wayland_screen_selection_tip", "Haluatko varmasti tyhjentää Wayland näyttövalinnan?"), + ("android_new_voice_call_tip", "Uusi äänipuhelu aloitettu"), + ("texture_render_tip", "Käytä tekstuuripohjaista renderöintiä paremman suorituskyvyn saavuttamiseksi."), + ("Use texture rendering", "Käytä tekstuurirenderöintiä"), + ("Floating window", "Kelluva ikkuna"), + ("floating_window_tip", "Kelluva ikkuna pysyy muiden sovellusten päällä etäistunnon aikana."), + ("Keep screen on", "Pidä näyttö päällä"), + ("Never", "Ei koskaan"), + ("During controlled", "Kun etäohjattuna"), + ("During service is on", "Kun palvelu on käynnissä"), + ("Capture screen using DirectX", "Kaappaa näyttö käyttämällä DirectX"), + ("Back", "Takaisin"), + ("Apps", "Sovellukset"), + ("Volume up", "Lisää äänenvoimakkuutta"), + ("Volume down", "Vähennä äänenvoimakkuutta"), + ("Power", "Virta"), + ("Telegram bot", "Telegram-botti"), + ("enable-bot-tip", "Ota Telegram botti käyttöön etähallintaa varten."), + ("enable-bot-desc", "Mahdollistaa ilmoitukset ja etätoiminnot Telegramin kautta."), + ("cancel-2fa-confirm-tip", "Haluatko varmasti poistaa kaksivaiheisen todennuksen käytöstä?"), + ("cancel-bot-confirm-tip", "Haluatko varmasti poistaa Telegram-botin käytöstä?"), + ("About RustDesk", "Tietoa RustDeskistä"), + ("Send clipboard keystrokes", "Lähetä leikepöydän näppäinsyötteet"), + ("network_error_tip", "Verkkovirhe – tarkista yhteys ja yritä uudelleen."), + ("Unlock with PIN", "Avaa PIN-koodilla"), + ("Requires at least {} characters", "Vaatii vähintään {} merkkiä"), + ("Wrong PIN", "Väärä PIN-koodi"), + ("Set PIN", "Aseta PIN-koodi"), + ("Enable trusted devices", "Ota luotetut laitteet käyttöön"), + ("Manage trusted devices", "Hallitse luotettuja laitteita"), + ("Platform", "Alusta"), + ("Days remaining", "Päiviä jäljellä"), + ("enable-trusted-devices-tip", "Vain luotetut laitteet voivat muodostaa yhteyden ilman lisävahvistusta."), + ("Parent directory", "Ylähakemisto"), + ("Resume", "Jatka"), + ("Invalid file name", "Virheellinen tiedostonimi"), + ("one-way-file-transfer-tip", "Tiedostonsiirto on yksisuuntainen – vain lähetys tai vastaanotto."), + ("Authentication Required", "Tunnistautuminen vaaditaan"), + ("Authenticate", "Tunnistaudu"), + ("web_id_input_tip", "Anna etätunnus verkkoliittymässä muodossa tunnus@palvelin"), + ("Download", "Lataa"), + ("Upload folder", "Lataa kansio"), + ("Upload files", "Lataa tiedostoja"), + ("Clipboard is synchronized", "Leikepöytä on synkronoitu"), + ("Update client clipboard", "Päivitä asiakkaan leikepöytä"), + ("Untagged", "Tunnisteeton"), + ("new-version-of-{}-tip", "Uusi versio sovelluksesta {} on saatavilla"), + ("Accessible devices", "Käytettävissä olevat laitteet"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Päivitä etä-RustDesk-asiakasversioon {} yhteensopivuuden takaamiseksi"), + ("d3d_render_tip", "Käytä Direct3D-renderöintiä paremman suorituskyvyn saavuttamiseksi"), + ("Use D3D rendering", "Käytä D3D-renderöintiä"), + ("Printer", "Tulostin"), + ("printer-os-requirement-tip", "Tulostustoiminto vaatii yhteensopivan käyttöjärjestelmän"), + ("printer-requires-installed-{}-client-tip", "Tulostus vaatii, että {} asiakas on asennettu"), + ("printer-{}-not-installed-tip", "{} tulostinta ei ole asennettu"), + ("printer-{}-ready-tip", "{}-tulostin on valmis"), + ("Install {} Printer", "Asenna {} tulostin"), + ("Outgoing Print Jobs", "Lähtevät tulostustyöt"), + ("Incoming Print Jobs", "Saapuvat tulostustyöt"), + ("Incoming Print Job", "Saapuva tulostustyö"), + ("use-the-default-printer-tip", "Käytä oletustulostinta"), + ("use-the-selected-printer-tip", "Käytä valittua tulostinta"), + ("auto-print-tip", "Tulosta saapuvat työt automaattisesti"), + ("print-incoming-job-confirm-tip", "Hyväksytäänkö saapuvan tulostustyön tulostus?"), + ("remote-printing-disallowed-tile-tip", "Etätulostus estetty"), + ("remote-printing-disallowed-text-tip", "Etätulostus ei ole sallittu tässä laitteessa tai yhteydessä."), + ("save-settings-tip", "Tallenna asetukset"), + ("dont-show-again-tip", "Älä näytä uudelleen"), + ("Take screenshot", "Ota kuvakaappaus"), + ("Taking screenshot", "Otetaan kuvakaappausta"), + ("screenshot-merged-screen-not-supported-tip", "Yhdistetyn näytön kuvakaappaus ei ole tuettu"), + ("screenshot-action-tip", "Valitse, mitä haluat tehdä kuvakaappaukselle"), + ("Save as", "Tallenna nimellä"), + ("Copy to clipboard", "Kopioi leikepöydälle"), + ("Enable remote printer", "Ota etätulostin käyttöön"), + ("Downloading {}", "Ladataan {}"), + ("{} Update", "{} päivitys"), + ("{}-to-update-tip", "Päivitä sovellus {} jatkaaksesi"), + ("download-new-version-failed-tip", "Uuden version lataus epäonnistui"), + ("Auto update", "Automaattinen päivitys"), + ("update-failed-check-msi-tip", "Päivitys epäonnistui – tarkista MSI asennuspaketti"), + ("websocket_tip", "Käytä WebSocket protokollaa yhteyden muodostamiseen"), + ("Use WebSocket", "Käytä WebSocketia"), + ("Trackpad speed", "Kosketuslevyn nopeus"), + ("Default trackpad speed", "Oletusnopeus kosketuslevylle"), + ("Numeric one-time password", "Numeerinen kertakäyttösalasana"), + ("Enable IPv6 P2P connection", "Ota IPv6 P2P yhteys käyttöön"), + ("Enable UDP hole punching", "Ota käyttöön UDP hole punching tekniikka"), + ("View camera", "Näytä kamera"), + ("Enable camera", "Ota kamera käyttöön"), + ("No cameras", "Ei kameroita"), + ("view_camera_unsupported_tip", "Kameranäkymä ei ole tuettu tällä alustalla"), + ("Terminal", "Pääte"), + ("Enable terminal", "Ota pääte käyttöön"), + ("New tab", "Uusi välilehti"), + ("Keep terminal sessions on disconnect", "Säilytä pääteistunnot yhteyden katketessa"), + ("Terminal (Run as administrator)", "Pääte (Suorita järjestelmänvalvojana)"), + ("terminal-admin-login-tip", "Kirjaudu järjestelmänvalvojana käyttääksesi tätä päätettä"), + ("Failed to get user token.", "Käyttäjätunnuksen hakeminen epäonnistui."), + ("Incorrect username or password.", "Virheellinen käyttäjätunnus tai salasana."), + ("The user is not an administrator.", "Käyttäjä ei ole järjestelmänvalvoja."), + ("Failed to check if the user is an administrator.", "Järjestelmänvalvojan tarkistus epäonnistui."), + ("Supported only in the installed version.", "Tuettu vain asennetussa versiossa."), + ("elevation_username_tip", "Anna järjestelmänvalvojan käyttäjätunnus oikeuksien korotusta varten"), + ("Preparing for installation ...", "Valmistellaan asennusta..."), + ("Show my cursor", "Näytä osoittimeni"), + ("Scale custom", "Mukautettu skaalaus"), + ("Custom scale slider", "Mukautetun skaalauksen liukusäädin"), + ("Decrease", "Pienennä"), + ("Increase", "Suurenna"), + ("Show virtual mouse", "Näytä virtuaalinen hiiri"), + ("Virtual mouse size", "Virtuaalihiiren koko"), + ("Small", "Pieni"), + ("Large", "Suuri"), + ("Show virtual joystick", "Näytä virtuaalinen ohjain"), + ("Edit note", "Muokkaa muistiinpanoa"), + ("Alias", "Alias"), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Jatka käyttäen {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/fr.rs b/vendor/rustdesk/src/lang/fr.rs new file mode 100644 index 0000000..ab6ed2e --- /dev/null +++ b/vendor/rustdesk/src/lang/fr.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "État"), + ("Your Desktop", "Votre bureau"), + ("desk_tip", "Votre bureau est accessible via l’identifiant et le mot de passe ci-dessous."), + ("Password", "Mot de passe"), + ("Ready", "Prêt"), + ("Established", "Établie"), + ("connecting_status", "Connexion au réseau RustDesk…"), + ("Enable service", "Activer le service"), + ("Start service", "Démarrer le service"), + ("Service is running", "Le service est en cours d’exécution"), + ("Service is not running", "Le service est inactif"), + ("not_ready_status", "Pas prêt ; veuillez vérifier la connexion"), + ("Control Remote Desktop", "Contrôler un bureau à distance"), + ("Transfer file", "Transférer des fichiers"), + ("Connect", "Se connecter"), + ("Recent sessions", "Sessions récentes"), + ("Address book", "Carnet d’adresses"), + ("Confirmation", "Confirmation"), + ("TCP tunneling", "Tunnel TCP"), + ("Remove", "Retirer"), + ("Refresh random password", "Générer un nouveau mot de passe aléatoire"), + ("Set your own password", "Définir votre propre mot de passe"), + ("Enable keyboard/mouse", "Activer le contrôle clavier/souris"), + ("Enable clipboard", "Activer la synchronisation du presse-papier"), + ("Enable file transfer", "Activer le transfert de fichiers"), + ("Enable TCP tunneling", "Activer le tunnel TCP"), + ("IP Whitelisting", "Liste blanche d’adresses IP"), + ("ID/Relay Server", "Serveur ID/relais"), + ("Import server config", "Importer la configuration du serveur"), + ("Export Server Config", "Exporter la configuration du serveur"), + ("Import server configuration successfully", "Configuration du serveur importée avec succès"), + ("Export server configuration successfully", "Configuration du serveur exportée avec succès"), + ("Invalid server configuration", "Configuration du serveur non valide"), + ("Clipboard is empty", "Le presse-papier est vide"), + ("Stop service", "Arrêter le service"), + ("Change ID", "Modifier l’ID"), + ("Your new ID", "Votre nouvel ID"), + ("length %min% to %max%", "longueur de %min% à %max%"), + ("starts with a letter", "commence par une lettre"), + ("allowed characters", "caractères autorisés"), + ("id_change_tip", "Seuls les caractères a-z, A-Z, 0-9, - (trait d’union) et _ (tiret bas) sont autorisés. La première lettre doit être a-z ou A-Z. La longueur doit être comprise entre 6 et 16."), + ("Website", "Site web"), + ("About", "À propos"), + ("Slogan_tip", "Fait avec cœur dans ce monde chaotique !"), + ("Privacy Statement", "Déclaration de confidentialité"), + ("Mute", "Muet"), + ("Build Date", "Date de compilation"), + ("Version", "Version"), + ("Home", "Accueil"), + ("Audio Input", "Entrée audio"), + ("Enhancements", "Améliorations"), + ("Hardware Codec", "Transcodage matériel"), + ("Adaptive bitrate", "Débit adaptatif"), + ("ID Server", "Serveur ID"), + ("Relay Server", "Serveur relais"), + ("API Server", "Serveur API"), + ("invalid_http", "Doit commencer par http:// ou https://"), + ("Invalid IP", "IP non valide"), + ("Invalid format", "Format non valide"), + ("server_not_support", "Non encore pris en charge par le serveur"), + ("Not available", "Non disponible"), + ("Too frequent", "Modifié trop fréquemment, veuillez réessayer plus tard"), + ("Cancel", "Annuler"), + ("Skip", "Ignorer"), + ("Close", "Fermer"), + ("Retry", "Réessayer"), + ("OK", "Valider"), + ("Password Required", "Mot de passe requis"), + ("Please enter your password", "Veuillez saisir votre mot de passe"), + ("Remember password", "Mémoriser le mot de passe"), + ("Wrong Password", "Mauvais mot de passe"), + ("Do you want to enter again?", "Voulez-vous ressaisir le mot de passe ?"), + ("Connection Error", "Erreur de connexion"), + ("Error", "Erreur"), + ("Reset by the peer", "Terminée par l’appareil distant"), + ("Connecting...", "Connexion…"), + ("Connection in progress. Please wait.", "Connexion en cours ; veuillez patienter."), + ("Please try 1 minute later", "Veuillez réessayer dans une minute"), + ("Login Error", "Erreur de connexion"), + ("Successful", "Succès"), + ("Connected, waiting for image...", "Connecté ; en attente de l’image…"), + ("Name", "Nom"), + ("Type", "Type"), + ("Modified", "Modifié le"), + ("Size", "Taille"), + ("Show Hidden Files", "Afficher les fichiers cachés"), + ("Receive", "Recevoir"), + ("Send", "Envoyer"), + ("Refresh File", "Rafraîchir le contenu"), + ("Local", "Local"), + ("Remote", "Distant"), + ("Remote Computer", "Appareil distant"), + ("Local Computer", "Appareil local"), + ("Confirm Delete", "Confirmer la suppression"), + ("Delete", "Supprimer"), + ("Properties", "Propriétés"), + ("Multi Select", "Sélection multiple"), + ("Select All", "Tout sélectionner"), + ("Unselect All", "Tout déselectionner"), + ("Empty Directory", "Répertoire vide"), + ("Not an empty directory", "Répertoire non vide"), + ("Are you sure you want to delete this file?", "Voulez-vous vraiment supprimer ce fichier ?"), + ("Are you sure you want to delete this empty directory?", "Voulez-vous vraiment supprimer ce répertoire vide ?"), + ("Are you sure you want to delete the file of this directory?", "Voulez-vous vraiment supprimer le fichier de ce répertoire ?"), + ("Do this for all conflicts", "Appliquer à tous les conflits"), + ("This is irreversible!", "Cette action est irréversible !"), + ("Deleting", "Suppression"), + ("files", "fichiers"), + ("Waiting", "En attente"), + ("Finished", "Terminé"), + ("Speed", "Vitesse"), + ("Custom Image Quality", "Qualité d’image personnalisée"), + ("Privacy mode", "Mode de confidentialité"), + ("Block user input", "Bloquer la saisie de l’utilisateur"), + ("Unblock user input", "Débloquer la saisie de l’utilisateur"), + ("Adjust Window", "Ajuster la fenêtre"), + ("Original", "Ratio d'origine"), + ("Shrink", "Rétrécir"), + ("Stretch", "Étirer"), + ("Scrollbar", "Barre de défilement"), + ("ScrollAuto", "Défilement automatique"), + ("Good image quality", "Bonne qualité d’image"), + ("Balanced", "Équilibré"), + ("Optimize reaction time", "Optimiser le temps de réaction"), + ("Custom", "Personnalisé"), + ("Show remote cursor", "Afficher le curseur distant"), + ("Show quality monitor", "Afficher le moniteur de qualité"), + ("Disable clipboard", "Désactiver le presse-papier"), + ("Lock after session end", "Verrouiller l’appareil distant après la déconnexion"), + ("Insert Ctrl + Alt + Del", "Envoyer Ctrl + Alt + Del"), + ("Insert Lock", "Verrouiller l’appareil distant"), + ("Refresh", "Rafraîchir l’écran"), + ("ID does not exist", "L’ID n’existe pas"), + ("Failed to connect to rendezvous server", "Échec de la connexion au serveur de rendez-vous"), + ("Please try later", "Veuillez essayer plus tard"), + ("Remote desktop is offline", "Le bureau distant est hors ligne"), + ("Key mismatch", "Discordance des clés"), + ("Timeout", "Connexion expirée"), + ("Failed to connect to relay server", "Échec de la connexion au serveur relais"), + ("Failed to connect via rendezvous server", "Échec de la connexion via le serveur de rendez-vous"), + ("Failed to connect via relay server", "Échec de la connexion via le serveur relais"), + ("Failed to make direct connection to remote desktop", "Échec de la connexion directe au bureau distant"), + ("Set Password", "Définir le mot de passe"), + ("OS Password", "Mot de passe du système d’exploitation"), + ("install_tip", "RustDesk n’est pas installé, ce qui peut limiter son utilisation à cause de l’UAC. Cliquez ci-dessous pour l’installer."), + ("Click to upgrade", "Mettre à niveau"), + ("Configure", "Configurer"), + ("config_acc", "L’autorisation « Accessibilité » est requise pour contrôler votre bureau à distance."), + ("config_screen", "L’autorisation « Enregistrement d’écran » est requise pour accéder à votre bureau à distance."), + ("Installing ...", "Installation…"), + ("Install", "Installer"), + ("Installation", "Installation"), + ("Installation Path", "Chemin d’installation"), + ("Create start menu shortcuts", "Créer des raccourcis dans le menu démarrer"), + ("Create desktop icon", "Créer une icône sur le bureau"), + ("agreement_tip", "En lançant l’installation, vous acceptez le contrat de licence."), + ("Accept and Install", "Accepter et installer"), + ("End-user license agreement", "Conditions générales d’utilisation"), + ("Generating ...", "Génération…"), + ("Your installation is lower version.", "La version installée est antérieure à la version en cours d’exécution."), + ("not_close_tcp_tip", "Veuillez ne pas fermer cette fenêtre lors de l’utilisation du tunnel"), + ("Listening ...", "En attente de connexion…"), + ("Remote Host", "Hôte distant"), + ("Remote Port", "Port distant"), + ("Action", "Action"), + ("Add", "Ajouter"), + ("Local Port", "Port local"), + ("Local Address", "Adresse locale"), + ("Change Local Port", "Changer le port local"), + ("setup_server_tip", "N’hésitez pas à mettre en place votre propre serveur afin d’améliorer la connexion"), + ("Too short, at least 6 characters.", "Trop court, 6 caractères minimum."), + ("The confirmation is not identical.", "Les deux entrées ne correspondent pas."), + ("Permissions", "Autorisations"), + ("Accept", "Accepter"), + ("Dismiss", "Rejeter"), + ("Disconnect", "Déconnecter"), + ("Enable file copy and paste", "Activer le copier-coller de fichiers"), + ("Connected", "Connecté"), + ("Direct and encrypted connection", "Connexion directe chiffrée"), + ("Relayed and encrypted connection", "Connexion via relais chiffrée"), + ("Direct and unencrypted connection", "Connexion directe non chiffrée"), + ("Relayed and unencrypted connection", "Connexion via relais non chiffrée"), + ("Enter Remote ID", "Saisissez l’ID de l’appareil distant"), + ("Enter your password", "Saisissez votre mot de passe"), + ("Logging in...", "En cours de connexion…"), + ("Enable RDP session sharing", "Activer le partage de session RDP"), + ("Auto Login", "Connexion automatique (Requiert l’activation de l’option « Verrouiller l’appareil distant après la déconnexion »)"), + ("Enable direct IP access", "Activer l’accès direct par adresse IP"), + ("Rename", "Renommer"), + ("Space", "Espace"), + ("Create desktop shortcut", "Créer un raccourci sur le bureau"), + ("Change Path", "Modifier le chemin"), + ("Create Folder", "Créer un dossier"), + ("Please enter the folder name", "Veuillez saisir le nom du dossier"), + ("Fix it", "Réparer"), + ("Warning", "Avertissement"), + ("Login screen using Wayland is not supported", "L’écran de connexion n’est pas pris en charge sous Wayland"), + ("Reboot required", "Redémarrage requis"), + ("Unsupported display server", "Le serveur d’affichage n’est pas pris en charge"), + ("x11 expected", "x11 attendu"), + ("Port", "Port"), + ("Settings", "Paramètres"), + ("Username", " Nom d’utilisateur"), + ("Invalid port", "Port non valide"), + ("Closed manually by the peer", "Terminée manuellement par l’appareil distant"), + ("Enable remote configuration modification", "Activer la modification de la configuration à distance"), + ("Run without install", "Exécuter sans installer"), + ("Connect via relay", "Connecter via relais"), + ("Always connect via relay", "Forcer la connexion via relais"), + ("whitelist_tip", "Seules les adresses IP incluses dans la liste blanche pourront accéder à mon appareil"), + ("Login", "Connexion"), + ("Verify", "Vérifier"), + ("Remember me", "Se souvenir de moi"), + ("Trust this device", "Faire confiance à cet appareil"), + ("Verification code", "Code de vérification"), + ("verification_tip", "Un code de vérification a été envoyé à l’adresse électronique enregistrée ; saisissez le code de vérification afin de poursuivre la connexion."), + ("Logout", "Déconnexion"), + ("Tags", "Étiquettes"), + ("Search ID", "Rechercher un ID"), + ("whitelist_sep", "Vous pouvez utiliser une virgule, un point-virgule, un espace ou une nouvelle ligne comme séparateur"), + ("Add ID", "Ajouter un ID"), + ("Add Tag", "Ajouter une étiquette"), + ("Unselect all tags", "Désélectionner toutes les étiquettes"), + ("Network error", "Erreur réseau"), + ("Username missed", "Nom d’utilisateur manquant"), + ("Password missed", "Mot de passe manquant"), + ("Wrong credentials", "Identifiant ou mot de passe erroné"), + ("The verification code is incorrect or has expired", "Le code de vérification est incorrect ou a expiré"), + ("Edit Tag", "Modifier l’étiquette"), + ("Forget Password", "Oublier le mot de passe"), + ("Favorites", "Favoris"), + ("Add to Favorites", "Ajouter aux favoris"), + ("Remove from Favorites", "Retirer des favoris"), + ("Empty", "Vide"), + ("Invalid folder name", "Nom de dossier non valide"), + ("Socks5 Proxy", "Socks5 Agents"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Discovered", "Découverts"), + ("install_daemon_tip", "Le service système doit être installé avant de pouvoir activer l’exécution au démarrage du système."), + ("Remote ID", "ID de l’appareil distant"), + ("Paste", "Coller"), + ("Paste here?", "Coller ici ?"), + ("Are you sure to close the connection?", "Voulez-vous vraiment terminer la connexion ?"), + ("Download new version", "Télécharger la nouvelle version"), + ("Touch mode", "Mode tactile"), + ("Mouse mode", "Mode souris"), + ("One-Finger Tap", "Appui simple"), + ("Left Mouse", "Clic gauche"), + ("One-Long Tap", "Appui prolongé"), + ("Two-Finger Tap", "Appui à deux doigts"), + ("Right Mouse", "Clic droit"), + ("One-Finger Move", "Mouvement à un doigt"), + ("Double Tap & Move", "Mouvement après double appui"), + ("Mouse Drag", "Glissement de la souris"), + ("Three-Finger vertically", "Trois doigts verticalement"), + ("Mouse Wheel", "Roulette de la souris"), + ("Two-Finger Move", "Mouvement à deux doigts"), + ("Canvas Move", "Déplacer la vue"), + ("Pinch to Zoom", "Pincer pour zoomer"), + ("Canvas Zoom", "Zoom sur la vue"), + ("Reset canvas", "Réinitialiser la vue"), + ("No permission of file transfer", "Absence de l’autorisation de transfert de fichiers"), + ("Note", "Note"), + ("Connection", "Connexion"), + ("Share screen", "Partage d’écran"), + ("Chat", "Discussion"), + ("Total", "Total"), + ("items", "éléments"), + ("Selected", "Sélectionné(s)"), + ("Screen Capture", "Capture de l’écran"), + ("Input Control", "Contrôle de la saisie"), + ("Audio Capture", "Capture de l’audio"), + ("Do you accept?", "Acceptez-vous ?"), + ("Open System Setting", "Ouvrir les paramètres système"), + ("How to get Android input permission?", "Comment obtenir l’autorisation de contrôle de la saisie sur Android ?"), + ("android_input_permission_tip1", "Pour qu’un appareil distant puisse contrôler votre appareil Android via la souris ou le toucher d’écran, vous devez autoriser RustDesk à utiliser le service « Accessibilité »."), + ("android_input_permission_tip2", "Veuillez accéder à la page suivante des paramètres système, puis recherchez et accédez à la section [Services installés] ; activez ensuite le service [RustDesk Input]."), + ("android_new_connection_tip", "Une nouvelle demande de contrôle a été reçue, elle souhaite contrôler votre appareil actuel."), + ("android_service_will_start_tip", "L’activation de la capture de l’écran démarrera automatiquement le service, ce qui permettra aux appareils distants d’initier une connexion vers cet appareil."), + ("android_stop_service_tip", "L’arrêt du service terminera automatiquement toutes les connexions établies."), + ("android_version_audio_tip", "La version actuelle d’Android ne prend pas en charge la capture de l’audio, veuillez passer à Android 10 ou supérieur."), + ("android_start_service_tip", "Appuyez sur [Démarrer le service] ou activez l’autorisation [Capture de l’écran] pour démarrer le service de partage d’écran."), + ("android_permission_may_not_change_tip", "Les modifications des autorisations peuvent requérir une reconnexion avant d’être prises en compte par les connexions déjà établies."), + ("Account", "Compte"), + ("Overwrite", "Écraser"), + ("This file exists, skip or overwrite this file?", "Ce fichier existe déjà, ignorer ou écraser ce fichier ?"), + ("Quit", "Quitter"), + ("Help", "Aide"), + ("Failed", "Échec"), + ("Succeeded", "Succès"), + ("Someone turns on privacy mode, exit", "Quelqu’un active le mode de confidentialité, désactiver"), + ("Unsupported", "Non pris en charge"), + ("Peer denied", "Refusé par l’appareil distant"), + ("Please install plugins", "Veuillez installer les plugins"), + ("Peer exit", "Désactivé par l’appareil distant"), + ("Failed to turn off", "Échec de la désactivation"), + ("Turned off", "Désactivé"), + ("Language", "Langue"), + ("Keep RustDesk background service", "Garder le service RustDesk en arrière plan"), + ("Ignore Battery Optimizations", "Ignorer les optimisations de la batterie"), + ("android_open_battery_optimizations_tip", "Pour désactiver cette fonctionnalité, veuillez accéder à la page suivante des paramètres de l’application RustDesk, puis recherchez et accédez à la section [Batterie] ; décochez ensuite l’option [Sans restriction]."), + ("Start on boot", "Lancer au démarrage"), + ("Start the screen sharing service on boot, requires special permissions", "Lancer le service de partage d’écran au démarrage, nécessite des autorisations spéciales"), + ("Connection not allowed", "Connexion non autorisée"), + ("Legacy mode", "Mode hérité"), + ("Map mode", "Mode correspondance"), + ("Translate mode", "Mode traduction"), + ("Use permanent password", "Utiliser un mot de passe permanent"), + ("Use both passwords", "Utiliser les deux mots de passe"), + ("Set permanent password", "Définir le mot de passe permanent"), + ("Enable remote restart", "Activer le redémarrage à distance"), + ("Restart remote device", "Redémarrer l’appareil distant"), + ("Are you sure you want to restart", "Voulez-vous vraiment redémarrer"), + ("Restarting remote device", "Redémarrage de l’appareil distant"), + ("remote_restarting_tip", "L'appareil distant redémarre ; veuillez fermer cette boîte de dialogue et vous reconnecter en utilisant le mot de passe permanent dans quelques instants"), + ("Copied", "Copié"), + ("Exit Fullscreen", "Quitter le mode plein écran"), + ("Fullscreen", "Plein écran"), + ("Mobile Actions", "Actions mobiles"), + ("Select Monitor", "Sélection du moniteur"), + ("Control Actions", "Actions de contrôle"), + ("Display Settings", "Paramètres d’affichage"), + ("Ratio", "Rapport"), + ("Image Quality", "Qualité d’image"), + ("Scroll Style", "Style de défilement"), + ("Show Toolbar", "Afficher la barre d’outils"), + ("Hide Toolbar", "Cacher la barre d’outils"), + ("Direct Connection", "Connexion directe"), + ("Relay Connection", "Connexion via relais"), + ("Secure Connection", "Connexion sécurisée"), + ("Insecure Connection", "Connexion non sécurisée"), + ("Scale original", "Échelle originale"), + ("Scale adaptive", "Échelle adaptative"), + ("General", "Général"), + ("Security", "Sécurité"), + ("Theme", "Thème"), + ("Dark Theme", "Thème sombre"), + ("Light Theme", "Thème clair"), + ("Dark", "Sombre"), + ("Light", "Clair"), + ("Follow System", "Suivi système"), + ("Enable hardware codec", "Activer le transcodage matériel"), + ("Unlock Security Settings", "Déverrouiller les paramètres de sécurité"), + ("Enable audio", "Activer l’audio"), + ("Unlock Network Settings", "Déverrouiller les paramètres réseau"), + ("Server", "Serveur"), + ("Direct IP Access", "Accès direct par adresse IP"), + ("Proxy", "Proxy"), + ("Apply", "Appliquer"), + ("Disconnect all devices?", "Déconnecter tous les appareils ?"), + ("Clear", "Effacer"), + ("Audio Input Device", "Périphérique source audio"), + ("Use IP Whitelisting", "Utiliser une liste blanche d’adresses IP"), + ("Network", "Réseau"), + ("Pin Toolbar", "Épingler la barre d’outils"), + ("Unpin Toolbar", "Détacher la barre d’outils"), + ("Recording", "Enregistrement"), + ("Directory", "Répertoire"), + ("Automatically record incoming sessions", "Enregistrer automatiquement les sessions entrantes"), + ("Automatically record outgoing sessions", "Enregistrer automatiquement les sessions sortantes"), + ("Change", "Modifier"), + ("Start session recording", "Commencer l’enregistrement"), + ("Stop session recording", "Stopper l’enregistrement"), + ("Enable recording session", "Activer l’enregistrement de session"), + ("Enable LAN discovery", "Activer la découverte sur réseau local"), + ("Deny LAN discovery", "Interdire la découverte sur réseau local"), + ("Write a message", "Écrire un message"), + ("Prompt", "Annonce"), + ("Please wait for confirmation of UAC...", "Veuillez attendre la confirmation de l’UAC…"), + ("elevated_foreground_window_tip", "La fenêtre active du bureau distant nécessite des privilèges plus élevés pour fonctionner, la souris et le clavier ne peuvent donc pas l’atteindre actuellement. Vous pouvez demander à l’utilisateur distant de réduire la fenêtre active ou de cliquer sur le bouton d’élévation dans la fenêtre de gestion de la connexion. Il est conseillé d’installer le logiciel sur l’appareil distant afin d’éviter ce problème."), + ("Disconnected", "Déconnecté"), + ("Other", "Divers"), + ("Confirm before closing multiple tabs", "Confirmer avant de fermer plusieurs onglets"), + ("Keyboard Settings", "Paramètres du clavier"), + ("Full Access", "Accès total"), + ("Screen Share", "Partage d’écran"), + ("ubuntu-21-04-required", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."), + ("wayland-requires-higher-linux-version", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."), + ("xdp-portal-unavailable", "Échec de la capture de l’écran Wayland. Le portail de bureau XDG a peut-être planté ou n’est pas disponible. Essayez de le redémarrer avec la commande `systemctl --user restart xdg-desktop-portal`."), + ("JumpLink", "Afficher"), + ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l’écran à partager (côté appareil distant)."), + ("Show RustDesk", "Afficher RustDesk"), + ("This PC", "Ce PC"), + ("or", "ou"), + ("Elevate", "Élever les privilèges"), + ("Zoom cursor", "Augmenter la taille du curseur"), + ("Accept sessions via password", "Accepter les sessions via mot de passe"), + ("Accept sessions via click", "Accepter les sessions via clic de confirmation"), + ("Accept sessions via both", "Accepter les sessions via mot de passe ou clic de confirmation"), + ("Please wait for the remote side to accept your session request...", "Veuillez attendre que votre demande de session distante soit acceptée…"), + ("One-time Password", "Mot de passe à usage unique"), + ("Use one-time password", "Utiliser un mot de passe à usage unique"), + ("One-time password length", "Longueur du mot de passe à usage unique"), + ("Request access to your device", "Demande l’accès à votre appareil"), + ("Hide connection management window", "Cacher la fenêtre de gestion de la connexion"), + ("hide_cm_tip", "Requiert d’accepter les sessions via mot de passe avec un mot de passe permanent"), + ("wayland_experiment_tip", "La prise en charge de Wayland est en phase expérimentale, veuillez utiliser X11 si vous avez besoin d’un accès non assisté."), + ("Right click to select tabs", "Clic droit pour sélectionner les onglets"), + ("Skipped", "Ignoré"), + ("Add to address book", "Ajouter au carnet d’adresses"), + ("Group", "Groupe"), + ("Search", "Rechercher"), + ("Closed manually by web console", "Terminée manuellement par la console web"), + ("Local keyboard type", "Disposition du clavier local"), + ("Select local keyboard type", "Sélectionner la disposition du clavier local"), + ("software_render_tip", "Si vous utilisez une carte graphique Nvidia sous Linux et que la fenêtre distante se ferme immédiatement après la connexion, l’installation du pilote open-source Nouveau et l’utilisation du rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), + ("Always use software rendering", "Toujours utiliser le rendu logiciel"), + ("config_input", "Vous devez accorder à RustDesk l’autorisation « Surveillance de l’entrée » pour contrôler le bureau distant avec le clavier."), + ("config_microphone", "Vous devez accorder à RustDesk l’autorisation « Enregistrer l’audio » pour discuter à distance."), + ("request_elevation_tip", "Vous pouvez également demander une élévation des privilèges si un utilisateur est présent côté distant."), + ("Wait", "Attendre"), + ("Elevation Error", "Erreur d’élévation des privilèges"), + ("Ask the remote user for authentication", "Demander à l’utilisateur distant de s’authentifier"), + ("Choose this if the remote account is administrator", "Sélectionnez cette option si le compte distant est administrateur"), + ("Transmit the username and password of administrator", "Transmettre le nom d’utilisateur et le mot de passe d’un compte administrateur"), + ("still_click_uac_tip", "L’utilisateur distant devra malgré tout confirmer l’UAC de l’instance RustDesk en cours d’éxécution."), + ("Request Elevation", "Demander l’élévation des privilèges"), + ("wait_accept_uac_tip", "Veuillez attendre l’acceptation de l’UAC par l’utilisateur distant."), + ("Elevate successfully", "Élévation des privilèges réussie"), + ("uppercase", "majuscule"), + ("lowercase", "minuscule"), + ("digit", "chiffre"), + ("special character", "caractère spécial"), + ("length>=8", "longueur ≥ 8"), + ("Weak", "Faible"), + ("Medium", "Moyen"), + ("Strong", "Fort"), + ("Switch Sides", "Inverser la prise de contrôle"), + ("Please confirm if you want to share your desktop?", "Voulez-vous vraiment partager votre bureau ?"), + ("Display", "Affichage"), + ("Default View Style", "Style de vue par défaut"), + ("Default Scroll Style", "Style de défilement par défaut"), + ("Default Image Quality", "Qualité d’image par défaut"), + ("Default Codec", "Codec par défaut"), + ("Bitrate", "Débit"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Autres options par défaut"), + ("Voice call", "Appel vocal"), + ("Text chat", "Conversation textuelle"), + ("Stop voice call", "Terminer l’appel vocal"), + ("relay_hint_tip", "Il n’est pas toujours possible d’établir une connexion directe, mais une connexion via serveur relais est envisageable. En outre, si vous souhaitez utiliser un relais dès la première tentative, vous pouvez ajouter le suffixe « /r » à l’ID ou activer l’option « Forcer la connexion via relais » depuis la carte des sessions récentes, si elle s’y trouve."), + ("Reconnect", "Se reconnecter"), + ("Codec", "Codec"), + ("Resolution", "Résolution"), + ("No transfers in progress", "Aucun transfert en cours"), + ("Set one-time password length", "Définir la longueur du mot de passe à usage unique"), + ("RDP Settings", "Paramètres RDP"), + ("Sort by", "Trier par"), + ("New Connection", "Nouvelle connexion"), + ("Restore", "Restaurer"), + ("Minimize", "Minimiser"), + ("Maximize", "Maximiser"), + ("Your Device", "Votre appareil"), + ("empty_recent_tip", "Oups, aucune session récente !\nIl est l’heure d’en organiser une nouvelle."), + ("empty_favorite_tip", "Vous n’avez pas encore d’appareils distants favoris ?\nTrouvez quelqu’un avec qui vous connecter et ajoutez-le à vos favoris !"), + ("empty_lan_tip", "Oh non, il semble que nous n’avons pas encore découvert d’appareils sur le réseau local."), + ("empty_address_book_tip", "Mince, il n’y a actuellement aucun appareil distant répertorié dans votre carnet d’adresses."), + ("Empty Username", "Nom d’utilisation non renseigné"), + ("Empty Password", "Mot de passe non renseigné"), + ("Me", "Moi"), + ("identical_file_tip", "Ce fichier est identique à celui sur l’appareil distant."), + ("show_monitors_tip", "Afficher les écrans dans la barre d’outils"), + ("View Mode", "Mode vue"), + ("login_linux_tip", "Vous devez vous connecter au compte Linux distant pour établir une session de bureau X"), + ("verify_rustdesk_password_tip", "Vérifier le mot de passe RustDesk"), + ("remember_account_tip", "Se souvenir de ce compte"), + ("os_account_desk_tip", "Ce compte est utilisé pour se connecter au système d’exploitation distant et activer la session de bureau en mode sans affichage"), + ("OS Account", "Compte du système d’exploitation"), + ("another_user_login_title_tip", "Un autre utilisateur est déjà connecté"), + ("another_user_login_text_tip", "Déconnecter"), + ("xorg_not_found_title_tip", "Xorg introuvable"), + ("xorg_not_found_text_tip", "Veuillez installer Xorg"), + ("no_desktop_title_tip", "Aucun environnement de bureau n’est disponible"), + ("no_desktop_text_tip", "Veuillez installer l’environnement de bureau GNOME"), + ("No need to elevate", "Élever les privilèges n’est pas nécessaire"), + ("System Sound", "Son système"), + ("Default", "Défaut"), + ("New RDP", "Nouvel RDP"), + ("Fingerprint", "Empreinte numérique"), + ("Copy Fingerprint", "Copier l’empreinte numérique"), + ("no fingerprints", "Aucune empreinte numérique"), + ("Select a peer", "Sélectionnez l’appareil distant"), + ("Select peers", "Sélectionnez les appareils distants"), + ("Plugins", "Plugins"), + ("Uninstall", "Désinstaller"), + ("Update", "Mettre à jour"), + ("Enable", "Activer"), + ("Disable", "Désactiver"), + ("Options", "Options"), + ("resolution_original_tip", "Résolution d’origine"), + ("resolution_fit_local_tip", "Adapter à la résolution locale"), + ("resolution_custom_tip", "Résolution personnalisée"), + ("Collapse toolbar", "Réduire la barre d’outils"), + ("Accept and Elevate", "Accepter et élever les privilèges"), + ("accept_and_elevate_btn_tooltip", "Accepter la connexion et élever les privilèges UAC."), + ("clipboard_wait_response_timeout_tip", "Expiration du délai d’attente du presse-papier."), + ("Incoming connection", "Connexion entrante"), + ("Outgoing connection", "Connexion sortante"), + ("Exit", "Quitter"), + ("Open", "Ouvrir"), + ("logout_tip", "Voulez-vous vraiment vous déconnecter ?"), + ("Service", "Service"), + ("Start", "Démarrer"), + ("Stop", "Arrêter"), + ("exceed_max_devices", "Vous avez atteint le nombre maximal d’appareils gérés."), + ("Sync with recent sessions", "Synchroniser avec les sessions récentes"), + ("Sort tags", "Trier les étiquettes"), + ("Open connection in new tab", "Ouvrir les connexions dans un nouvel onglet"), + ("Move tab to new window", "Déplacer l’onglet vers une nouvelle fenêtre"), + ("Can not be empty", "Ne peut pas être vide"), + ("Already exists", "Existe déjà"), + ("Change Password", "Modifier le mot de passe"), + ("Refresh Password", "Actualiser le mot de passe"), + ("ID", "ID"), + ("Grid View", "Vue Grille"), + ("List View", "Vue Liste"), + ("Select", "Sélectionner"), + ("Toggle Tags", "Basculer les étiquettes"), + ("pull_ab_failed_tip", "Échec de l’actualisation du carnet d’adresses"), + ("push_ab_failed_tip", "Échec de la synchronisation du carnet d’adresses avec le serveur"), + ("synced_peer_readded_tip", "Les appareils qui étaient présents dans les sessions récentes seront synchronisés vers le carnet d’adresses."), + ("Change Color", "Modifier la couleur"), + ("Primary Color", "Couleur principale"), + ("HSV Color", "Couleur TSV"), + ("Installation Successful!", "Installation réussie !"), + ("Installation failed!", "Échec de l’installation !"), + ("Reverse mouse wheel", "Inverser le sens de la molette de la souris"), + ("{} sessions", "{} sessions"), + ("scam_title", "Vous êtes peut-être victime d’une ESCROQUERIE !"), + ("scam_text1", "Si vous êtes au téléphone avec quelqu’un QUE VOUS NE CONNAISSEZ PAS et en qui VOUS N’AVEZ PAS CONFIANCE et qui vous a demandé d’utiliser RustDesk et de démarrer le service, ne le faites pas et raccrochez immédiatement."), + ("scam_text2", "Il s’agit probablement d’un escroc qui tente de vous voler de l’argent ou d’autres informations personnelles."), + ("Don't show again", "Ne plus afficher"), + ("I Agree", "J’accepte"), + ("Decline", "Refuser"), + ("Timeout in minutes", "Délai d’expiration en minutes"), + ("auto_disconnect_option_tip", "Terminer automatiquement les sessions entrantes en cas d’inactivité de l’utilisateur"), + ("Connection failed due to inactivity", "Déconnecté automatiquement pour cause d’inactivité"), + ("Check for software update on startup", "Vérifier la disponibilité des mises à jour au démarrage"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Veuillez mettre à jour RustDesk Server Pro vers la version {} ou une version ultérieure !"), + ("pull_group_failed_tip", "Échec de l’actualisation du groupe"), + ("Filter by intersection", "Filtrer par intersection"), + ("Remove wallpaper during incoming sessions", "Cacher le fond d’écran lors des sessions entrantes"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "L’affichage est débranché, passez sur le premier affichage."), + ("No displays", "Aucun affichage"), + ("Open in new window", "Ouvrir dans une nouvelle fenêtre"), + ("Show displays as individual windows", "Montrer les affichages sous forme de fenêtres individuelles"), + ("Use all my displays for the remote session", "Utiliser tous mes affichages pour la session à distance"), + ("selinux_tip", "SELinux est activé sur votre appareil, ce qui peut empêcher RustDesk de fonctionner correctement sur la machine contrôlée."), + ("Change view", "Disposition"), + ("Big tiles", "Grandes tuiles"), + ("Small tiles", "Petites tuiles"), + ("List", "Liste"), + ("Virtual display", "Affichage virtuel"), + ("Plug out all", "Tout débrancher"), + ("True color (4:4:4)", "Couleur réelle (4:4:4)"), + ("Enable blocking user input", "Activer le blocage des entrées de l’utilisateur"), + ("id_input_tip", "Vous pouvez saisir un ID, une adresse IP ou un nom de domaine suivi d’un port (:).\nSi vous souhaitez accéder à un appareil sur un autre serveur, veuillez ajouter l’adresse du serveur (@?key=), par exemple :\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi vous souhaitez accéder à un appareil sur un serveur public, veuillez saisir « @public » (la clé n’est pas nécessaire pour le serveur public).\n\nSi vous souhaitez forcer l’utilisation d’une connexion via relais dès la première tentative, ajoutez « /r » après l’ID, par exemple : « 9123456234/r »."), + ("privacy_mode_impl_mag_tip", "Mode 1"), + ("privacy_mode_impl_virtual_display_tip", "Mode 2"), + ("Enter privacy mode", "Entrer en mode de confidentialité"), + ("Exit privacy mode", "Quitter le mode de confidentialité"), + ("idd_not_support_under_win10_2004_tip", "Le pilote d’affichage indirect n’est pas pris en charge. Windows 10 version 2004 ou ultérieure est requis."), + ("input_source_1_tip", "Entrée source 1"), + ("input_source_2_tip", "Entrée source 2"), + ("Swap control-command key", "Intervertir la touche contrôle-commande"), + ("swap-left-right-mouse", "Intervertir les boutons gauche et droit de la souris"), + ("2FA code", "Code 2FA"), + ("More", "Plus"), + ("enable-2fa-title", "Activer l’authentification à deux facteurs"), + ("enable-2fa-desc", "Veuillez maintenant configurer votre authentificateur. Vous pouvez utiliser une application d’authentification telle qu’Authy, Microsoft ou Google Authenticator sur votre téléphone ou votre ordinateur.\n\nScannez le code QR avec votre application puis saisissez le code affiché par votre application afin d’activer l’authentification à deux facteurs."), + ("wrong-2fa-code", "Impossible de vérifier le code. Vérifiez l’exactitude du code saisi ainsi que des paramètres d’heure locale"), + ("enter-2fa-title", "Authentification à deux facteurs"), + ("Email verification code must be 6 characters.", "Le code de vérification de l’adresse électronique doit être composé de 6 caractères."), + ("2FA code must be 6 digits.", "Le code 2FA doit être composé de 6 chiffres."), + ("Multiple Windows sessions found", "Plusieurs sessions Windows ont été trouvées"), + ("Please select the session you want to connect to", "Veuillez sélectionner la session à laquelle vous souhaitez vous connecter"), + ("powered_by_me", "Utilise la technologie RustDesk"), + ("outgoing_only_desk_tip", "Vous utilisez une version personnalisée.\nVous pouvez vous connecter à d’autres appareils, mais les autres appareils ne peuvent pas se connecter au vôtre."), + ("preset_password_warning", "Cette version personnalisée est livrée avec un mot de passe prédéfini. Toute personne connaissant ce mot de passe pourrait prendre le contrôle total de votre appareil. Si vous ne vous y attendiez pas, désinstallez immédiatement le logiciel."), + ("Security Alert", "Alerte de sécurité"), + ("My address book", "Mon carnet d’adresses"), + ("Personal", "Personnel"), + ("Owner", "Propriétaire"), + ("Set shared password", "Définir le mot de passe partagé"), + ("Exist in", "Existe dans"), + ("Read-only", "Lecture seule"), + ("Read/Write", "Lecture/Écriture"), + ("Full Control", "Contrôle complet"), + ("share_warning_tip", "Les champs ci-dessus sont partagés et visibles par les autres."), + ("Everyone", "Tout le monde"), + ("ab_web_console_tip", "Plus sur la console web"), + ("allow-only-conn-window-open-tip", "N’autoriser la connexion que si la fenêtre RustDesk est ouverte"), + ("no_need_privacy_mode_no_physical_displays_tip", "Aucun affichage physique ; l’utilisation du mode de confidentialité n’est pas nécessaire."), + ("Follow remote cursor", "Suivre le curseur distant"), + ("Follow remote window focus", "Suivre la focalisation de fenêtre distante"), + ("default_proxy_tip", "Le protocole et le port par défaut sont Socks5 et 1080"), + ("no_audio_input_device_tip", "Aucun périphérique d’entrée audio trouvé."), + ("Incoming", "Entrantes"), + ("Outgoing", "Sortantes"), + ("Clear Wayland screen selection", "Effacer la sélection d’écran Wayland"), + ("clear_Wayland_screen_selection_tip", "Une fois la sélection d’écran effacée, vous pourrez resélectionner l’écran à partager."), + ("confirm_clear_Wayland_screen_selection_tip", "Voulez-vous vraiment effacer la sélection d’écran Wayland ?"), + ("android_new_voice_call_tip", "Une nouvelle demande d’appel vocal a été reçue. Si vous acceptez, l’audio passera sur la communication vocale."), + ("texture_render_tip", "Utiliser le rendu de texture afin de lisser les images. Désactiver cette option permet de résoudre certains problèmes de rendu."), + ("Use texture rendering", "Utiliser le rendu de texture"), + ("Floating window", "Fenêtre flottante"), + ("floating_window_tip", "Aide à maintenir le service en arrière-plan"), + ("Keep screen on", "Maintenir l’écran allumé"), + ("Never", "Jamais"), + ("During controlled", "Lorsque l’appareil est contrôlé"), + ("During service is on", "Lorsque le service est actif"), + ("Capture screen using DirectX", "Utiliser DirectX pour capturer l’écran"), + ("Back", "Retour"), + ("Apps", "Applis"), + ("Volume up", "Volume haut"), + ("Volume down", "Volume bas"), + ("Power", "Marche/Arrêt"), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Activer cette fonctionnalité vous permet de recevoir le code 2FA depuis votre bot. Peut également servir de notification de connexion."), + ("enable-bot-desc", "1. Entamez une discussion avec @BotFather.\n2. Envoyez-lui la commande « newbot ». Vous recevrez un jeton suite à cette étape.\n3. Entamez une discussion avec votre bot nouvellement créé. Envoyez-lui un message commençant par une barre oblique (« / ») tel que « /hello » afin de l’activer.\n"), + ("cancel-2fa-confirm-tip", "Voulez-vous vraiment désactiver l’authentication à deux facteurs ?"), + ("cancel-bot-confirm-tip", "Voulez-vous vraiment désactiver le bot Telegram ?"), + ("About RustDesk", "À propos de RustDesk"), + ("Send clipboard keystrokes", "Taper le contenu du presse-papier"), + ("network_error_tip", "Veuillez vérifier votre connexion réseau puis réessayer."), + ("Unlock with PIN", "Déverrouiller par code PIN"), + ("Requires at least {} characters", "Requiert un minimum de {} caractères"), + ("Wrong PIN", "Code PIN erroné"), + ("Set PIN", "Définir le code PIN"), + ("Enable trusted devices", "Activer les appareils de confiance"), + ("Manage trusted devices", "Gérer les appareils de confiance"), + ("Platform", "Plateforme"), + ("Days remaining", "Jours restants"), + ("enable-trusted-devices-tip", "Ne pas demander de code 2FA sur les appareils de confiance"), + ("Parent directory", "Répertoire parent"), + ("Resume", "Reprendre"), + ("Invalid file name", "Nom de fichier non valide"), + ("one-way-file-transfer-tip", "Le transfert de fichiers à sens unique est activé côté appareil contrôlé."), + ("Authentication Required", "Authentication requise"), + ("Authenticate", "Authentifier"), + ("web_id_input_tip", "Vous pouvez saisir un ID sur le même serveur ; le client web ne prend pas en charge l’accès par adresse IP.\nSi vous souhaitez accéder à un appareil sur un autre serveur, veuillez ajouter l’adresse du serveur (@?key=), par exemple :\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi vous souhaitez accéder à un appareil sur un serveur public, veuillez saisir « @public » (la clé n’est pas nécessaire pour le serveur public)."), + ("Download", "Télécharger"), + ("Upload folder", "Téléverser le dossier"), + ("Upload files", "Téléverser les fichiers"), + ("Clipboard is synchronized", "Le presse-papier est synchronisé"), + ("Update client clipboard", "Actualiser le presse-papier du client"), + ("Untagged", "Sans étiquette"), + ("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"), + ("Accessible devices", "Appareils accessibles"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre le client RustDesk distant à jour vers la version {} ou ultérieure !"), + ("d3d_render_tip", "Sur certaines machines, l’écran du contrôle à distance peut rester noir lors de l’utilisation du rendu D3D."), + ("Use D3D rendering", "Utiliser le rendu D3D"), + ("Printer", "Imprimante"), + ("printer-os-requirement-tip", "La fonction d’impression sortante nécessite Windows 10 ou une version ultérieure."), + ("printer-requires-installed-{}-client-tip", "{} doit être installé sur cet appareil avant de pouvoir utiliser l’impression à distance."), + ("printer-{}-not-installed-tip", "L’imprimante {} n’est pas installée."), + ("printer-{}-ready-tip", "L’imprimante {} est installée et opérationnelle."), + ("Install {} Printer", "Installer l’imprimante {}"), + ("Outgoing Print Jobs", "Impressions sortantes"), + ("Incoming Print Jobs", "Impressions entrantes"), + ("Incoming Print Job", "Impression entrante"), + ("use-the-default-printer-tip", "Utiliser l’imprimante par défaut"), + ("use-the-selected-printer-tip", "Utiliser l’imprimante sélectionnée"), + ("auto-print-tip", "Imprimer automatiquement en utilisant l’imprimante sélectionnée."), + ("print-incoming-job-confirm-tip", "L’appareil distant vous a envoyé une impression ; voulez-vous l’exécuter de votre côté ?"), + ("remote-printing-disallowed-tile-tip", "Impression à distance non autorisée"), + ("remote-printing-disallowed-text-tip", "Les paramètres de l’appareil contrôlé n’autorisent pas l’impression à distance."), + ("save-settings-tip", "Enregistrer les paramètres"), + ("dont-show-again-tip", "Ne plus afficher"), + ("Take screenshot", "Prendre une capture d’écran"), + ("Taking screenshot", "Prise de capture d’écran"), + ("screenshot-merged-screen-not-supported-tip", "Actuellement, la prise de capture d’écran ne prend pas en charge les affichages multiples. Veuillez réessayer après avoir sélectionné un seul affichage."), + ("screenshot-action-tip", "Veuillez choisir l’action à effectuer avec la capture d’écran."), + ("Save as", "Enregistrer sous"), + ("Copy to clipboard", "Copier dans le presse-papier"), + ("Enable remote printer", "Activer l’impression à distance"), + ("Downloading {}", "Téléchargement de {}"), + ("{} Update", "Mise à jour de {}"), + ("{}-to-update-tip", "{} va maintenant quitter afin d’installer la nouvelle version."), + ("download-new-version-failed-tip", "Le téléchargement a échoué. Vous pouvez réessayer, ou bien cliquer sur le bouton « Télécharger » pour vous rendre sur la page de publication afin de mettre à jour manuellement."), + ("Auto update", "Installer les mises à jour automatiquement"), + ("update-failed-check-msi-tip", "La vérification de la méthode d’installation a échoué. Veuillez cliquer sur le bouton « Télécharger » pour vous rendre sur la page de publication afin de mettre à jour manuellement."), + ("websocket_tip", "Seules les connexions via relais sont prises en charge lors de l’utilisation de WebSocket."), + ("Use WebSocket", "Utiliser WebSocket"), + ("Trackpad speed", "Vitesse du pavé tactile"), + ("Default trackpad speed", "Vitesse par défaut du pavé tactile"), + ("Numeric one-time password", "Mot de passe à usage unique numérique"), + ("Enable IPv6 P2P connection", "Activer la connexion P2P IPv6"), + ("Enable UDP hole punching", "Activer le « hole punching » UDP"), + ("View camera", "Afficher la caméra"), + ("Enable camera", "Activer la caméra"), + ("No cameras", "Aucune caméra"), + ("view_camera_unsupported_tip", "L’appareil distant ne prend pas en charge l’affichage de la caméra."), + ("Terminal", "Terminal"), + ("Enable terminal", "Activer le terminal"), + ("New tab", "Nouvel onglet"), + ("Keep terminal sessions on disconnect", "Maintenir les sessions du terminal lors de la déconnexion"), + ("Terminal (Run as administrator)", "Terminal (administrateur)"), + ("terminal-admin-login-tip", "Veuillez saisir le nom d’utilisateur et le mot de passe de l’administrateur de l’appareil contrôlé."), + ("Failed to get user token.", "Échec de l’obtention du jeton utilisateur."), + ("Incorrect username or password.", "Nom d’utilisateur ou mot de passe incorrect."), + ("The user is not an administrator.", "L’utilisateur n’est pas un administrateur."), + ("Failed to check if the user is an administrator.", "Échec de la vérification du statut d’administrateur de l’utilisateur."), + ("Supported only in the installed version.", "Uniquement pris en charge dans la version installée."), + ("elevation_username_tip", "Saisissez un nom d’utilisateur ou un domaine\\utilisateur"), + ("Preparing for installation ...", "Préparation de l’installation…"), + ("Show my cursor", "Afficher mon curseur"), + ("Scale custom", "Échelle personnalisée"), + ("Custom scale slider", "Curseur d’échelle personnalisée"), + ("Decrease", "Diminuer"), + ("Increase", "Augmenter"), + ("Show virtual mouse", "Afficher la souris virtuelle"), + ("Virtual mouse size", "Taille de la souris virtuelle"), + ("Small", "Petite"), + ("Large", "Grande"), + ("Show virtual joystick", "Afficher le joystick virtuel"), + ("Edit note", "Modifier la note"), + ("Alias", "Alias"), + ("ScrollEdge", "Défilement sur les bords"), + ("Allow insecure TLS fallback", "Utiliser une connexion TLS non sécurisée si nécessaire"), + ("allow-insecure-tls-fallback-tip", "Par défaut, RustDesk vérifie le certificat du serveur lors de l’utilisation de protocoles utilisant TLS.\nLorsque cette option est activée, RustDesk autorise les connexions même en cas d’échec de l’étape de vérification."), + ("Disable UDP", "Désactiver UDP"), + ("disable-udp-tip", "Contrôle l’utilisation exclusive du mode TCP.\nLorsque cette option est activée, RustDesk n’utilise plus le port UDP 21116 et utilise le port TCP 21116 à la place."), + ("server-oss-not-support-tip", "Note : Cette fonctionnalité n’est pas disponible sous la version open-source du serveur RustDesk."), + ("input note here", "saisir la note ici"), + ("note-at-conn-end-tip", "Proposer de rédiger une note une fois la connexion terminée"), + ("Show terminal extra keys", "Afficher les touches supplémentaires du terminal"), + ("Relative mouse mode", "Mode souris relative"), + ("rel-mouse-not-supported-peer-tip", "Le mode souris relative n’est pas pris en charge par l’appareil distant."), + ("rel-mouse-not-ready-tip", "Le mode souris relative n’est pas encore prêt ; veuillez réessayer."), + ("rel-mouse-lock-failed-tip", "Échec du verrouillage du curseur. Le mode souris relative a été désactivé."), + ("rel-mouse-exit-{}-tip", "Appuyez sur {} pour quitter."), + ("rel-mouse-permission-lost-tip", "L’autorisation de contrôle du clavier a été révoquée. Le mode souris relative a été désactivé."), + ("Changelog", "Journal des modifications"), + ("keep-awake-during-outgoing-sessions-label", "Maintenir l’écran allumé lors des sessions sortantes"), + ("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"), + ("Continue with {}", "Continuer avec {}"), + ("Display Name", "Nom d’affichage"), + ("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."), + ("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/ge.rs b/vendor/rustdesk/src/lang/ge.rs new file mode 100644 index 0000000..fba2fd8 --- /dev/null +++ b/vendor/rustdesk/src/lang/ge.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "სტატუსი"), + ("Your Desktop", "თქვენი სამუშაო მაგიდა"), + ("desk_tip", "თქვენი სამუშაო მაგიდა ხელმისაწვდომია ამ ID-ით და პაროლით."), + ("Password", "პაროლი"), + ("Ready", "მზადაა"), + ("Established", "დამყარებულია"), + ("connecting_status", "RustDesk ქსელთან დაკავშირება..."), + ("Enable service", "სერვისის ჩართვა"), + ("Start service", "სერვისის გაშვება"), + ("Service is running", "სერვისი გაშვებულია"), + ("Service is not running", "სერვისი არ არის გაშვებული"), + ("not_ready_status", "არ არის დაკავშირებული. შეამოწმეთ კავშირი."), + ("Control Remote Desktop", "ახალი კავშირი"), + ("Transfer file", "ფაილების გადაცემა"), + ("Connect", "დაკავშირება"), + ("Recent sessions", "ბოლო სესიები"), + ("Address book", "მისამართების წიგნი"), + ("Confirmation", "დადასტურება"), + ("TCP tunneling", "TCP ტუნელირება"), + ("Remove", "წაშლა"), + ("Refresh random password", "შემთხვევითი პაროლის განახლება"), + ("Set your own password", "საკუთარი პაროლის დაყენება"), + ("Enable keyboard/mouse", "კლავიატურის/თაგუნას გამოყენება"), + ("Enable clipboard", "გაცვლის ბუფერის გამოყენება"), + ("Enable file transfer", "ფაილების გადაცემის გამოყენება"), + ("Enable TCP tunneling", "TCP ტუნელირების გამოყენება"), + ("IP Whitelisting", "დაშვებული IP მისამართების სია"), + ("ID/Relay Server", "ID/რეტრანსლატორი"), + ("Import server config", "სერვერის კონფიგურაციის იმპორტი"), + ("Export Server Config", "სერვერის კონფიგურაციის ექსპორტი"), + ("Import server configuration successfully", "სერვერის კონფიგურაცია წარმატებით იმპორტირებულია"), + ("Export server configuration successfully", "სერვერის კონფიგურაცია წარმატებით ექსპორტირებულია"), + ("Invalid server configuration", "არასწორი სერვერის კონფიგურაცია"), + ("Clipboard is empty", "გაცვლის ბუფერი ცარიელია"), + ("Stop service", "სერვისის გაჩერება"), + ("Change ID", "ID-ის შეცვლა"), + ("Your new ID", "თქვენი ახალი ID"), + ("length %min% to %max%", "სიგრძე %min%...%max%"), + ("starts with a letter", "იწყება ასოთი"), + ("allowed characters", "დაშვებული სიმბოლოები"), + ("id_change_tip", "დაშვებულია მხოლოდ a-z, A-Z, 0-9, - (დეფისი) და _ (ქვედა ტირე) სიმბოლოები. პირველი უნდა იყოს a-z, A-Z ასო. სიგრძე 6-დან 16-მდე."), + ("Website", "ვებგვერდი"), + ("About", "პროგრამის შესახებ"), + ("Slogan_tip", "შექმნილია გულით ამ შეშლილ სამყაროში!"), + ("Privacy Statement", "კონფიდენციალურობის განაცხადი"), + ("Mute", "ხმის გათიშვა"), + ("Build Date", "აგების თარიღი"), + ("Version", "ვერსია"), + ("Home", "მთავარი"), + ("Audio Input", "აუდიო შესავალი"), + ("Enhancements", "გაუმჯობესებები"), + ("Hardware Codec", "აპარატული კოდეკი"), + ("Adaptive bitrate", "ადაპტური ბიტრეიტი"), + ("ID Server", "ID სერვერი"), + ("Relay Server", "რეტრანსლატორი"), + ("API Server", "API სერვერი"), + ("invalid_http", "მისამართი უნდა იწყებოდეს http:// ან https://-ით"), + ("Invalid IP", "არასწორი IP მისამართი"), + ("Invalid format", "არასწორი ფორმატი"), + ("server_not_support", "ჯერ სერვერით არ არის მხარდაჭერილი"), + ("Not available", "მიუწვდომელია"), + ("Too frequent", "ძალიან ხშირად"), + ("Cancel", "გაუქმება"), + ("Skip", "გამოტოვება"), + ("Close", "დახურვა"), + ("Retry", "ხელახლა ცდა"), + ("OK", "დიახ"), + ("Password Required", "საჭიროა პაროლი"), + ("Please enter your password", "შეიყვანეთ თქვენი პაროლი"), + ("Remember password", "პაროლის დამახსოვრება"), + ("Wrong Password", "არასწორი პაროლი"), + ("Do you want to enter again?", "გსურთ ხელახლა შესვლა?"), + ("Connection Error", "დაკავშირების შეცდომა"), + ("Error", "შეცდომა"), + ("Reset by the peer", "გადატვირთულია დაშორებული კვანძის მიერ"), + ("Connecting...", "დაკავშირება..."), + ("Connection in progress. Please wait.", "მიმდინარეობს დაკავშირება. გთხოვთ, მოიცადოთ."), + ("Please try 1 minute later", "სცადეთ ერთი წუთის შემდეგ"), + ("Login Error", "შესვლის შეცდომა"), + ("Successful", "წარმატებული"), + ("Connected, waiting for image...", "დაკავშირებულია, გამოსახულების მოლოდინში..."), + ("Name", "სახელი"), + ("Type", "ტიპი"), + ("Modified", "შეცვლილი"), + ("Size", "ზომა"), + ("Show Hidden Files", "დამალული ფაილების ჩვენება"), + ("Receive", "მიღება"), + ("Send", "გაგზავნა"), + ("Refresh File", "ფაილის განახლება"), + ("Local", "ლოკალური"), + ("Remote", "დაშორებული"), + ("Remote Computer", "დაშორებული კომპიუტერი"), + ("Local Computer", "ლოკალური კომპიუტერი"), + ("Confirm Delete", "წაშლის დადასტურება"), + ("Delete", "წაშლა"), + ("Properties", "თვისებები"), + ("Multi Select", "მრავლობითი არჩევანი"), + ("Select All", "ყველას არჩევა"), + ("Unselect All", "ყველას მოხსნა"), + ("Empty Directory", "ცარიელი საქაღალდე"), + ("Not an empty directory", "საქაღალდე არ არის ცარიელი"), + ("Are you sure you want to delete this file?", "ნამდვილად გსურთ ამ ფაილის წაშლა?"), + ("Are you sure you want to delete this empty directory?", "ნამდვილად გსურთ ამ ცარიელი საქაღალდის წაშლა?"), + ("Are you sure you want to delete the file of this directory?", "ნამდვილად გსურთ ამ საქაღალდიდან ფაილის წაშლა?"), + ("Do this for all conflicts", "გააკეთეთ ეს ყველა კონფლიქტისთვის"), + ("This is irreversible!", "ეს შეუქცევადია!"), + ("Deleting", "წაშლა"), + ("files", "ფაილები"), + ("Waiting", "მოლოდინი"), + ("Finished", "დასრულებულია"), + ("Speed", "სიჩქარე"), + ("Custom Image Quality", "მომხმარებლის მიერ განსაზღვრული გამოსახულების ხარისხი"), + ("Privacy mode", "კონფიდენციალურობის რეჟიმი"), + ("Block user input", "დაშორებულ მოწყობილობაზე შეყვანის დაბლოკვა"), + ("Unblock user input", "დაშორებულ მოწყობილობაზე შეყვანის განბლოკვა"), + ("Adjust Window", "ფანჯრის მორგება"), + ("Original", "ორიგინალი"), + ("Shrink", "შემცირება"), + ("Stretch", "გაჭიმვა"), + ("Scrollbar", "გადაადგილების ზოლი"), + ("ScrollAuto", "ავტოგადაადგილება"), + ("Good image quality", "საუკეთესო გამოსახულების ხარისხი"), + ("Balanced", "ბალანსი ხარისხსა და რეაგირებას შორის"), + ("Optimize reaction time", "საუკეთესო რეაგირების დრო"), + ("Custom", "მომხმარებლის მიერ განსაზღვრული"), + ("Show remote cursor", "დაშორებული კურსორის ჩვენება"), + ("Show quality monitor", "ხარისხის მონიტორის ჩვენება"), + ("Disable clipboard", "გაცვლის ბუფერის გამორთვა"), + ("Lock after session end", "სესიის დასრულების შემდეგ ანგარიშის დაბლოკვა"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del ჩასმა"), + ("Insert Lock", "ანგარიშის დაბლოკვა"), + ("Refresh", "განახლება"), + ("ID does not exist", "ID არ არსებობს"), + ("Failed to connect to rendezvous server", "შუამავალ სერვერთან დაკავშირება შეუძლებელია"), + ("Please try later", "სცადეთ მოგვიანებით"), + ("Remote desktop is offline", "დაშორებული მოწყობილობა არ არის ონლაინ"), + ("Key mismatch", "გასაღების შეუსაბამობა"), + ("Timeout", "დროის ამოწურვა"), + ("Failed to connect to relay server", "რეტრანსლატორთან დაკავშირება შეუძლებელია"), + ("Failed to connect via rendezvous server", "შუამავალი სერვერის მეშვეობით დაკავშირება შეუძლებელია"), + ("Failed to connect via relay server", "რეტრანსლატორის მეშვეობით დაკავშირება შეუძლებელია"), + ("Failed to make direct connection to remote desktop", "დაშორებულ მოწყობილობასთან პირდაპირი კავშირის დამყარება შეუძლებელია"), + ("Set Password", "პაროლის დაყენება"), + ("OS Password", "ოპერაციული სისტემის პაროლი"), + ("install_tip", "ზოგიერთ შემთხვევაში UAC-ის გამო RustDesk შეიძლება არასწორად მუშაობდეს დაშორებულ კვანძზე. UAC-თან დაკავშირებული პრობლემების თავიდან ასაცილებლად დააჭირეთ ქვემოთ მოცემულ ღილაკს სისტემაში RustDesk-ის დასაყენებლად."), + ("Click to upgrade", "დააჭირეთ განახლებისთვის"), + ("Configure", "კონფიგურაცია"), + ("config_acc", "თქვენი სამუშაო მაგიდის დისტანციური მართვისთვის უნდა მიანიჭოთ RustDesk-ს \"წვდომის\" უფლებები"), + ("config_screen", "სამუშაო მაგიდაზე დისტანციური წვდომისთვის უნდა მიანიჭოთ RustDesk-ს \"ეკრანის ანაბეჭდის\" უფლებები"), + ("Installing ...", "ინსტალაცია..."), + ("Install", "დაინსტალირება"), + ("Installation", "ინსტალაცია"), + ("Installation Path", "ინსტალაციის გზა"), + ("Create start menu shortcuts", "მენიუში მალსახმობების შექმნა"), + ("Create desktop icon", "სამუშაო მაგიდაზე ხატულის შექმნა"), + ("agreement_tip", "ინსტალაციის დაწყებით თქვენ ეთანხმებით სალიცენზიო შეთანხმების პირობებს."), + ("Accept and Install", "დათანხმება და ინსტალაცია"), + ("End-user license agreement", "საბოლოო მომხმარებლის სალიცენზიო შეთანხმება"), + ("Generating ...", "გენერაცია..."), + ("Your installation is lower version.", "თქვენი ინსტალაცია უფრო ადრეული ვერსიაა."), + ("not_close_tcp_tip", "ტუნელის გამოყენებისას არ დახუროთ ეს ფანჯარა."), + ("Listening ...", "მოსმენა..."), + ("Remote Host", "დაშორებული კვანძი"), + ("Remote Port", "დაშორებული პორტი"), + ("Action", "მოქმედება"), + ("Add", "დამატება"), + ("Local Port", "ლოკალური პორტი"), + ("Local Address", "ლოკალური მისამართი"), + ("Change Local Port", "ლოკალური პორტის შეცვლა"), + ("setup_server_tip", "უფრო სწრაფი დაკავშირებისთვის დააყენეთ საკუთარი სერვერი."), + ("Too short, at least 6 characters.", "ძალიან მოკლეა, მინიმუმ 6 სიმბოლო."), + ("The confirmation is not identical.", "დადასტურება არ ემთხვევა"), + ("Permissions", "უფლებები"), + ("Accept", "მიღება"), + ("Dismiss", "უარყოფა"), + ("Disconnect", "გათიშვა"), + ("Enable file copy and paste", "ფაილების კოპირების და ჩასმის დაშვება"), + ("Connected", "დაკავშირებულია"), + ("Direct and encrypted connection", "პირდაპირი და დაშიფრული კავშირი"), + ("Relayed and encrypted connection", "რეტრანსლირებული და დაშიფრული კავშირი"), + ("Direct and unencrypted connection", "პირდაპირი და დაუშიფრავი კავშირი"), + ("Relayed and unencrypted connection", "რეტრანსლირებული და დაუშიფრავი კავშირი"), + ("Enter Remote ID", "შეიყვანეთ დაშორებული ID"), + ("Enter your password", "შეიყვანეთ თქვენი პაროლი"), + ("Logging in...", "შესვლა..."), + ("Enable RDP session sharing", "RDP სესიის გაზიარების გამოყენება"), + ("Auto Login", "ავტომატური შესვლა ანგარიშში"), + ("Enable direct IP access", "პირდაპირი IP წვდომის გამოყენება"), + ("Rename", "გადარქმევა"), + ("Space", "სივრცე"), + ("Create desktop shortcut", "სამუშაო მაგიდაზე მალსახმობის შექმნა"), + ("Change Path", "გზის შეცვლა"), + ("Create Folder", "საქაღალდის შექმნა"), + ("Please enter the folder name", "შეიყვანეთ საქაღალდის სახელი"), + ("Fix it", "გამოსწორება"), + ("Warning", "გაფრთხილება"), + ("Login screen using Wayland is not supported", "Wayland-ის გამოყენებით შესვლის ეკრანი არ არის მხარდაჭერილი"), + ("Reboot required", "საჭიროა გადატვირთვა"), + ("Unsupported display server", "არამხარდაჭერილი ჩვენების სერვერი"), + ("x11 expected", "მოსალოდნელია X11"), + ("Port", "პორტი"), + ("Settings", "პარამეტრები"), + ("Username", "მომხმარებლის სახელი"), + ("Invalid port", "არასწორი პორტი"), + ("Closed manually by the peer", "დახურულია დაშორებული კვანძის მიერ ხელით"), + ("Enable remote configuration modification", "დაშორებული კონფიგურაციის ცვლილების დაშვება"), + ("Run without install", "გაშვება ინსტალაციის გარეშე"), + ("Connect via relay", "რეტრანსლატორის მეშვეობით დაკავშირება"), + ("Always connect via relay", "ყოველთვის დაკავშირება რეტრანსლატორის მეშვეობით"), + ("whitelist_tip", "მხოლოდ თეთრ სიაში არსებულ IP მისამართებს შეუძლიათ ჩემს მოწყობილობაზე წვდომა."), + ("Login", "შესვლა"), + ("Verify", "შემოწმება"), + ("Remember me", "დამიმახსოვრე"), + ("Trust this device", "სანდო მოწყობილობა"), + ("Verification code", "შემოწმების კოდი"), + ("verification_tip", "აღმოჩენილია ახალი მოწყობილობა, რეგისტრირებულ ელფოსტაზე გაგზავნილია შემოწმების კოდი. შეიყვანეთ ის სისტემაში შესვლის გასაგრძელებლად."), + ("Logout", "გამოსვლა"), + ("Tags", "ჭდეები"), + ("Search ID", "ID-ით ძიება"), + ("whitelist_sep", "გამოყოფა მძიმით, წერტილ-მძიმით, ჰარით ან ახალი ხაზით."), + ("Add ID", "ID-ის დამატება"), + ("Add Tag", "საკვანძო სიტყვის დამატება"), + ("Unselect all tags", "ყველა ჭდის მოხსნა"), + ("Network error", "ქსელის შეცდომა"), + ("Username missed", "მომხმარებლის სახელი აკლია"), + ("Password missed", "პაროლი დაგავიწყდათ"), + ("Wrong credentials", "არასწორი მონაცემები"), + ("The verification code is incorrect or has expired", "შემოწმების კოდი არასწორია ან ვადაგასულია"), + ("Edit Tag", "ჭდის შეცვლა"), + ("Forget Password", "პაროლის დავიწყება"), + ("Favorites", "რჩეულები"), + ("Add to Favorites", "რჩეულებში დამატება"), + ("Remove from Favorites", "რჩეულებიდან წაშლა"), + ("Empty", "ცარიელი"), + ("Invalid folder name", "არასწორი საქაღალდის სახელი"), + ("Socks5 Proxy", "SOCKS5-პროქსი"), + ("Socks5/Http(s) Proxy", ""), + ("Discovered", "ნაპოვნია"), + ("install_daemon_tip", "ჩატვირთვისას გასაშვებად საჭიროა სისტემური სერვისის დაყენება"), + ("Remote ID", "დაშორებული ID"), + ("Paste", "ჩასმა"), + ("Paste here?", "ჩასმა აქ?"), + ("Are you sure to close the connection?", "ნამდვილად გსურთ კავშირის დასრულება?"), + ("Download new version", "ახალი ვერსიის ჩამოტვირთვა"), + ("Touch mode", "სენსორული რეჟიმი"), + ("Mouse mode", "თაგუნას/ტაჩპადის რეჟიმი"), + ("One-Finger Tap", "ერთი თითით შეხება"), + ("Left Mouse", "თაგუნას მარცხენა ღილაკი"), + ("One-Long Tap", "ერთი თითით ხანგრძლივი შეხება"), + ("Two-Finger Tap", "ორი თითით შეხება"), + ("Right Mouse", "თაგუნას მარჯვენა ღილაკი"), + ("One-Finger Move", "ერთი თითით გადაადგილება"), + ("Double Tap & Move", "ორმაგი შეხება და გადაადგილება"), + ("Mouse Drag", "თაგუნათი გადათრევა"), + ("Three-Finger vertically", "სამი თითით ვერტიკალურად"), + ("Mouse Wheel", "თაგუნას ბორბალი"), + ("Two-Finger Move", "ორი თითით გადაადგილება"), + ("Canvas Move", "ტილოს გადაადგილება"), + ("Pinch to Zoom", "მასშტაბირება თითებით"), + ("Canvas Zoom", "ტილოს მასშტაბი"), + ("Reset canvas", "ტილოს მასშტაბის გადატვირთვა"), + ("No permission of file transfer", "ფაილების გადაცემის უფლება არ არის"), + ("Note", "შენიშვნა"), + ("Connection", "კავშირი"), + ("Share screen", "ეკრანის დემონსტრაცია"), + ("Chat", "ჩატი"), + ("Total", "სულ"), + ("items", "ელემენტები"), + ("Selected", "არჩეულია"), + ("Screen Capture", "ეკრანის ჩაწერა"), + ("Input Control", "შეყვანის კონტროლი"), + ("Audio Capture", "აუდიოს ჩაწერა"), + ("Do you accept?", "თანახმა ხართ?"), + ("Open System Setting", "სისტემის პარამეტრების გახსნა"), + ("How to get Android input permission?", "როგორ მივიღოთ Android-ის შეყვანის უფლება?"), + ("android_input_permission_tip1", "იმისთვის, რომ დაშორებულმა მოწყობილობამ შეძლოს თქვენი Android-მოწყობილობის მართვა თაგუნათი ან შეხებით, საჭიროა RustDesk-ისთვის \"სპეციალური შესაძლებლობების\" სერვისის გამოყენების უფლების მინიჭება."), + ("android_input_permission_tip2", "გადადით სისტემის პარამეტრების შესაბამის გვერდზე, იპოვეთ და შედით \"დაინსტალირებულ სერვისებში\", ჩართეთ \"RustDesk Input\" სერვისი."), + ("android_new_connection_tip", "მიღებულია ახალი მოთხოვნა თქვენი მიმდინარე მოწყობილობის მართვაზე."), + ("android_service_will_start_tip", "ეკრანის ჩაწერის ჩართვა ავტომატურად გაუშვებს სერვისს, რაც სხვა მოწყობილობებს საშუალებას აძლევს მოითხოვონ ამ მოწყობილობასთან დაკავშირება."), + ("android_stop_service_tip", "სერვისის დახურვა ავტომატურად დახურავს ყველა დამყარებულ კავშირს."), + ("android_version_audio_tip", "Android-ის მიმდინარე ვერსია არ უჭერს მხარს ხმის ჩაწერას, განაახლეთ Android 10-მდე ან უფრო ახალ ვერსიამდე."), + ("android_start_service_tip", "დააჭირეთ [სერვისის გაშვება] ან დაუშვით [ეკრანის ჩაწერა] ეკრანის დემონსტრაციის სერვისის გასაშვებად."), + ("android_permission_may_not_change_tip", "დამყარებული კავშირების უფლებები ვერ შეიცვლება, საჭიროა ხელახალი დაკავშირება."), + ("Account", "ანგარიში"), + ("Overwrite", "გადაწერა"), + ("This file exists, skip or overwrite this file?", "ფაილი უკვე არსებობს, გამოტოვოთ თუ გადავწეროთ?"), + ("Quit", "გასვლა"), + ("Help", "დახმარება"), + ("Failed", "ვერ შესრულდა"), + ("Succeeded", "შესრულდა"), + ("Someone turns on privacy mode, exit", "ვიღაცამ ჩართო კონფიდენციალურობის რეჟიმი, გასვლა"), + ("Unsupported", "არ არის მხარდაჭერილი"), + ("Peer denied", "უარყოფილია დაშორებული კვანძის მიერ"), + ("Please install plugins", "დააინსტალირეთ პლაგინები"), + ("Peer exit", "გათიშულია მომხმარებლის მიერ"), + ("Failed to turn off", "გამორთვა შეუძლებელია"), + ("Turned off", "გამორთული"), + ("Language", "ენა"), + ("Keep RustDesk background service", "RustDesk-ის ფონური სერვისის შენარჩუნება"), + ("Ignore Battery Optimizations", "ბატარეის ოპტიმიზაციის იგნორირება"), + ("android_open_battery_optimizations_tip", "გადადით პარამეტრების შემდეგ გვერდზე"), + ("Start on boot", "ჩართვისას გაშვება"), + ("Start the screen sharing service on boot, requires special permissions", "ეკრანის გაზიარების სერვისის გაშვება ჩართვისას (საჭიროებს სპეციალურ უფლებებს)"), + ("Connection not allowed", "კავშირი არ არის დაშვებული"), + ("Legacy mode", "ძველი რეჟიმი"), + ("Map mode", "რუკის რეჟიმი"), + ("Translate mode", "თარგმნის რეჟიმი"), + ("Use permanent password", "მუდმივი პაროლის გამოყენება"), + ("Use both passwords", "ორივე პაროლის გამოყენება"), + ("Set permanent password", "მუდმივი პაროლის დაყენება"), + ("Enable remote restart", "დისტანციური გადატვირთვის დაშვება"), + ("Restart remote device", "დისტანციური მოწყობილობის გადატვირთვა"), + ("Are you sure you want to restart", "დარწმუნებული ხართ, რომ გსურთ გადატვირთვა?"), + ("Restarting remote device", "დისტანციური მოწყობილობის გადატვირთვა"), + ("remote_restarting_tip", "დისტანციური მოწყობილობა იტვირთება. დახურეთ ეს შეტყობინება და გარკვეული დროის შემდეგ ხელახლა დაუკავშირდით მუდმივი პაროლის გამოყენებით."), + ("Copied", "დაკოპირებულია"), + ("Exit Fullscreen", "სრული ეკრანიდან გასვლა"), + ("Fullscreen", "სრული ეკრანი"), + ("Mobile Actions", "მობილური ქმედებები"), + ("Select Monitor", "აირჩიეთ მონიტორი"), + ("Control Actions", "მართვის ქმედებები"), + ("Display Settings", "ეკრანის პარამეტრები"), + ("Ratio", "თანაფარდობა"), + ("Image Quality", "გამოსახულების ხარისხი"), + ("Scroll Style", "გადაადგილების სტილი"), + ("Show Toolbar", "ხელსაწყოთა პანელის ჩვენება"), + ("Hide Toolbar", "ხელსაწყოთა პანელის დამალვა"), + ("Direct Connection", "პირდაპირი კავშირი"), + ("Relay Connection", "რეტრანსლირებული კავშირი"), + ("Secure Connection", "უსაფრთხო კავშირი"), + ("Insecure Connection", "არაუსაფრთხო კავშირი"), + ("Scale original", "ორიგინალური მასშტაბი"), + ("Scale adaptive", "ადაპტირებადი მასშტაბი"), + ("General", "ზოგადი"), + ("Security", "უსაფრთხოება"), + ("Theme", "თემა"), + ("Dark Theme", "მუქი თემა"), + ("Light Theme", "ნათელი თემა"), + ("Dark", "მუქი"), + ("Light", "ნათელი"), + ("Follow System", "სისტემური"), + ("Enable hardware codec", "აპარატურული კოდეკის გამოყენება"), + ("Unlock Security Settings", "უსაფრთხოების პარამეტრების განბლოკვა"), + ("Enable audio", "აუდიოს ჩართვა"), + ("Unlock Network Settings", "ქსელის პარამეტრების განბლოკვა"), + ("Server", "სერვერი"), + ("Direct IP Access", "პირდაპირი IP წვდომა"), + ("Proxy", "პროქსი"), + ("Apply", "გამოყენება"), + ("Disconnect all devices?", "გავთიშოთ ყველა მოწყობილობა?"), + ("Clear", "გასუფთავება"), + ("Audio Input Device", "აუდიოს შეყვანის მოწყობილობა"), + ("Use IP Whitelisting", "IP თეთრი სიის გამოყენება"), + ("Network", "ქსელი"), + ("Pin Toolbar", "ხელსაწყოთა პანელის მიმაგრება"), + ("Unpin Toolbar", "ხელსაწყოთა პანელის მოხსნა"), + ("Recording", "ჩაწერა"), + ("Directory", "საქაღალდე"), + ("Automatically record incoming sessions", "შემომავალი სესიების ავტომატური ჩაწერა"), + ("Automatically record outgoing sessions", "გამავალი სესიების ავტომატური ჩაწერა"), + ("Change", "შეცვლა"), + ("Start session recording", "სესიის ჩაწერის დაწყება"), + ("Stop session recording", "სესიის ჩაწერის შეწყვეტა"), + ("Enable recording session", "სესიის ჩაწერის ჩართვა"), + ("Enable LAN discovery", "LAN აღმოჩენის ჩართვა"), + ("Deny LAN discovery", "LAN აღმოჩენის უარყოფა"), + ("Write a message", "შეტყობინების დაწერა"), + ("Prompt", "მინიშნება"), + ("Please wait for confirmation of UAC...", "გთხოვთ, დაელოდოთ UAC-ის დადასტურებას..."), + ("elevated_foreground_window_tip", "მიმდინარე დისტანციური სამუშაო მაგიდის ფანჯარა მოითხოვს მაღალ პრივილეგიებს სამუშაოდ, ამიტომ დროებით შეუძლებელია მაუსისა და კლავიატურის გამოყენება. შეგიძლიათ სთხოვოთ დისტანციურ მომხმარებელს ჩაკეცოს მიმდინარე ფანჯარა ან დააჭიროთ უფლებების აწევის ღილაკს კავშირის მართვის ფანჯარაში. ამ პრობლემის თავიდან ასაცილებლად რეკომენდებულია პროგრამული უზრუნველყოფის ინსტალაცია დისტანციურ მოწყობილობაზე."), + ("Disconnected", "გათიშულია"), + ("Other", "სხვა"), + ("Confirm before closing multiple tabs", "რამდენიმე ჩანართის დახურვის დადასტურება"), + ("Keyboard Settings", "კლავიატურის პარამეტრები"), + ("Full Access", "სრული წვდომა"), + ("Screen Share", "ეკრანის გაზიარება"), + ("ubuntu-21-04-required", "Wayland საჭიროებს Ubuntu 21.04 ან უფრო ახალ ვერსიას."), + ("wayland-requires-higher-linux-version", "Wayland-ს სჭირდება Linux-ის დისტრიბუტივის უფრო ახალი ვერსია. გამოიყენეთ X11 სამუშაო მაგიდა ან შეცვალეთ ოპერაციული სისტემა."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "ნახვა"), + ("Please Select the screen to be shared(Operate on the peer side).", "აირჩიეთ ეკრანი გასაზიარებლად (იმუშავეთ პარტნიორის მხარეს)."), + ("Show RustDesk", "RustDesk-ის ჩვენება"), + ("This PC", "ეს კომპიუტერი"), + ("or", "ან"), + ("Elevate", "უფლებების აწევა"), + ("Zoom cursor", "კურსორის მასშტაბირება"), + ("Accept sessions via password", "სესიების მიღება პაროლით"), + ("Accept sessions via click", "სესიების მიღება ღილაკზე დაჭერით"), + ("Accept sessions via both", "სესიების მიღება პაროლით და ღილაკზე დაჭერით"), + ("Please wait for the remote side to accept your session request...", "გთხოვთ, დაელოდოთ, სანამ დისტანციური მხარე მიიღებს თქვენს სესიის მოთხოვნას..."), + ("One-time Password", "ერთჯერადი პაროლი"), + ("Use one-time password", "ერთჯერადი პაროლის გამოყენება"), + ("One-time password length", "ერთჯერადი პაროლის სიგრძე"), + ("Request access to your device", "თქვენს მოწყობილობაზე წვდომის მოთხოვნა"), + ("Hide connection management window", "კავშირის მართვის ფანჯრის დამალვა"), + ("hide_cm_tip", "დამალვის დაშვება, თუ სესიები მიიღება პაროლით ან გამოიყენება მუდმივი პაროლი"), + ("wayland_experiment_tip", "Wayland-ის მხარდაჭერა ექსპერიმენტულ ეტაპზეა, გამოიყენეთ X11, თუ გჭირდებათ ავტომატური წვდომა."), + ("Right click to select tabs", "ჩანართების არჩევა მარჯვენა ღილაკით"), + ("Skipped", "გამოტოვებულია"), + ("Add to address book", "მისამართების წიგნში დამატება"), + ("Group", "ჯგუფი"), + ("Search", "ძიება"), + ("Closed manually by web console", "ხელით დაიხურა ვებ-კონსოლის საშუალებით"), + ("Local keyboard type", "ლოკალური კლავიატურის ტიპი"), + ("Select local keyboard type", "აირჩიეთ ლოკალური კლავიატურის ტიპი"), + ("software_render_tip", "თუ გაქვთ Nvidia ვიდეობარათი და დისტანციური ფანჯარა იხურება დაკავშირებისთანავე, შეიძლება დაგეხმაროთ Nouveau დრაივერის დაყენება და პროგრამული ვიზუალიზაციის არჩევა. საჭირო იქნება გადატვირთვა."), + ("Always use software rendering", "ყოველთვის გამოიყენეთ პროგრამული ვიზუალიზაცია"), + ("config_input", "დისტანციური სამუშაო მაგიდის კლავიატურით სამართავად, საჭიროა RustDesk-ისთვის \"შეყვანის მონიტორინგის\" უფლების მინიჭება."), + ("config_microphone", "დისტანციურ მხარესთან სასაუბროდ, საჭიროა RustDesk-ისთვის \"აუდიოს ჩაწერის\" უფლების მინიჭება."), + ("request_elevation_tip", "ასევე შეგიძლიათ მოითხოვოთ უფლებების აწევა, თუ ვინმე არის დისტანციურ მხარეს."), + ("Wait", "დაელოდეთ"), + ("Elevation Error", "უფლებების აწევის შეცდომა"), + ("Ask the remote user for authentication", "მოითხოვეთ ავთენტიფიკაცია დისტანციური მომხმარებლისგან"), + ("Choose this if the remote account is administrator", "აირჩიეთ ეს, თუ დისტანციური ანგარიში ადმინისტრატორია"), + ("Transmit the username and password of administrator", "ადმინისტრატორის სახელის და პაროლის გადაცემა"), + ("still_click_uac_tip", "კვლავ საჭიროა, რომ დისტანციურმა მომხმარებელმა დააჭიროს \"OK\"-ს UAC ფანჯარაში RustDesk-ის გაშვებისას."), + ("Request Elevation", "უფლებების აწევის მოთხოვნა"), + ("wait_accept_uac_tip", "დაელოდეთ, სანამ დისტანციური მომხმარებელი დაადასტურებს UAC მოთხოვნას."), + ("Elevate successfully", "უფლებები წარმატებით აიწია"), + ("uppercase", "დიდი ასოები"), + ("lowercase", "პატარა ასოები"), + ("digit", "ციფრები"), + ("special character", "სპეციალური სიმბოლოები"), + ("length>=8", "8+ სიმბოლო"), + ("Weak", "სუსტი"), + ("Medium", "საშუალო"), + ("Strong", "ძლიერი"), + ("Switch Sides", "მხარეების გადართვა"), + ("Please confirm if you want to share your desktop?", "ადასტურებთ, რომ გსურთ სამუშაო მაგიდის გაზიარება?"), + ("Display", "ეკრანი"), + ("Default View Style", "ნაგულისხმევი ჩვენების სტილი"), + ("Default Scroll Style", "ნაგულისხმევი გადაადგილების სტილი"), + ("Default Image Quality", "ნაგულისხმევი გამოსახულების ხარისხი"), + ("Default Codec", "ნაგულისხმევი კოდეკი"), + ("Bitrate", "ბიტრეიტი"), + ("FPS", "კადრების სიხშირე"), + ("Auto", "ავტო"), + ("Other Default Options", "სხვა ნაგულისხმევი პარამეტრები"), + ("Voice call", "ხმოვანი ზარი"), + ("Text chat", "ტექსტური ჩატი"), + ("Stop voice call", "ხმოვანი ზარის დასრულება"), + ("relay_hint_tip", "პირდაპირი კავშირი შეიძლება შეუძლებელი იყოს. ამ შემთხვევაში შეგიძლიათ სცადოთ რეტრანსლატორის გავლით დაკავშირება.\nასევე, თუ გსურთ პირდაპირ რეტრანსლატორის გამოყენება, შეგიძლიათ დაამატოთ ID-ს სუფიქსი \"/r\" ან ჩართოთ \"ყოველთვის დაუკავშირდით რეტრანსლატორის გავლით\" დისტანციური კვანძის პარამეტრებში."), + ("Reconnect", "ხელახლა დაკავშირება"), + ("Codec", "კოდეკი"), + ("Resolution", "გარჩევადობა"), + ("No transfers in progress", "გადაცემა არ მიმდინარეობს"), + ("Set one-time password length", "ერთჯერადი პაროლის სიგრძის დაყენება"), + ("RDP Settings", "RDP პარამეტრები"), + ("Sort by", "სორტირება"), + ("New Connection", "ახალი კავშირი"), + ("Restore", "აღდგენა"), + ("Minimize", "ჩაკეცვა"), + ("Maximize", "გაშლა"), + ("Your Device", "თქვენი მოწყობილობა"), + ("empty_recent_tip", "არ არის ბოლო სესიები!\nდროა დაგეგმოთ ახალი."), + ("empty_favorite_tip", "ჯერ არ გაქვთ რჩეული დისტანციური კვანძები?\nმოდით, ვნახოთ, ვის შეიძლება დავამატოთ რჩეულებში!"), + ("empty_lan_tip", "დისტანციური კვანძები ვერ მოიძებნა."), + ("empty_address_book_tip", "მისამართების წიგნში არ არის დისტანციური კვანძები."), + ("Empty Username", "ცარიელი მომხმარებლის სახელი"), + ("Empty Password", "ცარიელი პაროლი"), + ("Me", "მე"), + ("identical_file_tip", "ფაილი იდენტურია დისტანციურ კვანძზე არსებული ფაილის"), + ("show_monitors_tip", "მონიტორების ჩვენება ხელსაწყოთა პანელზე"), + ("View Mode", "ნახვის რეჟიმი"), + ("login_linux_tip", "X სამუშაო მაგიდის სესიის ჩასართავად, საჭიროა დისტანციურ Linux ანგარიშში შესვლა."), + ("verify_rustdesk_password_tip", "დაადასტურეთ RustDesk-ის პაროლი"), + ("remember_account_tip", "დაიმახსოვრეთ ეს ანგარიში"), + ("os_account_desk_tip", "ეს ანგარიში გამოიყენება დისტანციურ ოპერაციულ სისტემაში შესასვლელად და headless რეჟიმში სამუშაო მაგიდის სესიის ჩასართავად."), + ("OS Account", "ოპერაციული სისტემის ანგარიში"), + ("another_user_login_title_tip", "სხვა მომხმარებელი უკვე შესულია სისტემაში"), + ("another_user_login_text_tip", "გათიშვა"), + ("xorg_not_found_title_tip", "Xorg ვერ მოიძებნა"), + ("xorg_not_found_text_tip", "დააინსტალირეთ Xorg"), + ("no_desktop_title_tip", "სამუშაო მაგიდა არ არის ხელმისაწვდომი"), + ("no_desktop_text_tip", "დააინსტალირეთ GNOME Desktop"), + ("No need to elevate", "უფლებების აწევა არ არის საჭირო"), + ("System Sound", "სისტემური ხმა"), + ("Default", "ნაგულისხმევი"), + ("New RDP", "ახალი RDP"), + ("Fingerprint", "ანაბეჭდი"), + ("Copy Fingerprint", "ანაბეჭდის კოპირება"), + ("no fingerprints", "ანაბეჭდები არ არის"), + ("Select a peer", "აირჩიეთ დისტანციური კვანძი"), + ("Select peers", "აირჩიეთ დისტანციური კვანძები"), + ("Plugins", "დანამატები"), + ("Uninstall", "წაშლა"), + ("Update", "განახლება"), + ("Enable", "ჩართვა"), + ("Disable", "გამორთვა"), + ("Options", "პარამეტრები"), + ("resolution_original_tip", "საწყისი გარჩევადობა"), + ("resolution_fit_local_tip", "ლოკალური გარჩევადობის შესაბამისი"), + ("resolution_custom_tip", "მორგებული გარჩევადობა"), + ("Collapse toolbar", "ხელსაწყოთა პანელის ჩაკეცვა"), + ("Accept and Elevate", "მიღება და უფლებების აწევა"), + ("accept_and_elevate_btn_tooltip", "კავშირის დაშვება და UAC უფლებების აწევა."), + ("clipboard_wait_response_timeout_tip", "გაცვლის ბუფერის კოპირების ლოდინის დრო ამოიწურა"), + ("Incoming connection", "შემომავალი კავშირი"), + ("Outgoing connection", "გამავალი კავშირი"), + ("Exit", "გასვლა"), + ("Open", "გახსნა"), + ("logout_tip", "ნამდვილად გსურთ გასვლა?"), + ("Service", "სერვისი"), + ("Start", "გაშვება"), + ("Stop", "შეჩერება"), + ("exceed_max_devices", "მიღწეულია სამართავი მოწყობილობების მაქსიმალური რაოდენობა."), + ("Sync with recent sessions", "ბოლო სესიების სინქრონიზაცია"), + ("Sort tags", "ტეგების სორტირება"), + ("Open connection in new tab", "კავშირის გახსნა ახალ ჩანართში"), + ("Move tab to new window", "ჩანართის გადატანა ახალ ფანჯარაში"), + ("Can not be empty", "არ შეიძლება იყოს ცარიელი"), + ("Already exists", "უკვე არსებობს"), + ("Change Password", "პაროლის შეცვლა"), + ("Refresh Password", "პაროლის განახლება"), + ("ID", "ID"), + ("Grid View", "ბადე"), + ("List View", "სია"), + ("Select", "არჩევა"), + ("Toggle Tags", "ტეგების გადართვა"), + ("pull_ab_failed_tip", "მისამართების წიგნის განახლება შეუძლებელია"), + ("push_ab_failed_tip", "მისამართების წიგნის სერვერთან სინქრონიზაცია შეუძლებელია"), + ("synced_peer_readded_tip", "ბოლო სესიებში არსებული მოწყობილობები დასინქრონიზდება მისამართების წიგნში."), + ("Change Color", "ფერის შეცვლა"), + ("Primary Color", "ძირითადი ფერი"), + ("HSV Color", "HSV ფერი"), + ("Installation Successful!", "ინსტალაცია წარმატებით დასრულდა!"), + ("Installation failed!", "ინსტალაცია ვერ განხორციელდა!"), + ("Reverse mouse wheel", "მაუსის ბორბლის რევერსირება"), + ("{} sessions", "{} სესია"), + ("scam_title", "თქვენ შეიძლება გაცურონ!"), + ("scam_text1", "თუ ტელეფონით ესაუბრებით ვინმეს, ვისაც არ იცნობთ და არ ენდობით, და ის გთხოვთ გამოიყენოთ RustDesk და გაუშვათ მისი სერვისი, არ გააგრძელოთ და დაუყოვნებლივ შეწყვიტეთ საუბარი."), + ("scam_text2", "სავარაუდოდ, ეს არის თაღლითი, რომელიც ცდილობს მოიპაროს თქვენი ფული ან სხვა პირადი ინფორმაცია."), + ("Don't show again", "აღარ აჩვენოთ"), + ("I Agree", "ვეთანხმები"), + ("Decline", "უარყოფა"), + ("Timeout in minutes", "ლოდინის დრო (წუთები)"), + ("auto_disconnect_option_tip", "ავტომატურად დახუროს შემომავალი სესიები მომხმარებლის არააქტიურობისას"), + ("Connection failed due to inactivity", "კავშირი ვერ განხორციელდა არააქტიურობის გამო"), + ("Check for software update on startup", "პროგრამის განახლების შემოწმება გაშვებისას"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "განაახლეთ RustDesk Server Pro ვერსიამდე {} ან უფრო ახალი!"), + ("pull_group_failed_tip", "ჯგუფის განახლება შეუძლებელია"), + ("Filter by intersection", "ფილტრაცია გადაკვეთით"), + ("Remove wallpaper during incoming sessions", "სამუშაო მაგიდის ფონის დამალვა შემომავალი სესიის დროს"), + ("Test", "ტესტი"), + ("display_is_plugged_out_msg", "ეკრანი გამორთულია, გადართეთ პირველ ეკრანზე."), + ("No displays", "ეკრანები არ არის"), + ("Open in new window", "ახალ ფანჯარაში გახსნა"), + ("Show displays as individual windows", "ეკრანების ცალკეულ ფანჯრებში ჩვენება"), + ("Use all my displays for the remote session", "ყველა ჩემი ეკრანის გამოყენება დისტანციური სესიისთვის"), + ("selinux_tip", "თქვენს მოწყობილობაზე ჩართულია SELinux, რამაც შეიძლება ხელი შეუშალოს RustDesk-ის სწორ მუშაობას მართულ მხარეზე."), + ("Change view", "ხედი"), + ("Big tiles", "დიდი ხატულები"), + ("Small tiles", "პატარა ხატულები"), + ("List", "სია"), + ("Virtual display", "ვირტუალური ეკრანი"), + ("Plug out all", "ყველას გამორთვა"), + ("True color (4:4:4)", "True color (4:4:4)"), + ("Enable blocking user input", "მომხმარებლის შეყვანის დაბლოკვის დაშვება"), + ("id_input_tip", "შეგიძლიათ შეიყვანოთ იდენტიფიკატორი, პირდაპირი IP მისამართი ან დომენი პორტით (<დომენი>:<პორტი>).\nთუ გჭირდებათ წვდომა მოწყობილობაზე სხვა სერვერზე, დაამატეთ სერვერის მისამართი (@<სერვერის_მისამართი>?key=<გასაღების_მნიშვნელობა>), მაგალითად:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nთუ გჭირდებათ წვდომა მოწყობილობაზე საჯარო სერვერზე, შეიყვანეთ \"@public\", გასაღები საჯარო სერვერისთვის არ არის საჭირო."), + ("privacy_mode_impl_mag_tip", "რეჟიმი 1"), + ("privacy_mode_impl_virtual_display_tip", "რეჟიმი 2"), + ("Enter privacy mode", "კონფიდენციალურობის რეჟიმის ჩართვა"), + ("Exit privacy mode", "კონფიდენციალურობის რეჟიმის გამორთვა"), + ("idd_not_support_under_win10_2004_tip", "არაპირდაპირი ჩვენების დრაივერი არ არის მხარდაჭერილი. საჭიროა Windows 10 ვერსია 2004 ან უფრო ახალი."), + ("input_source_1_tip", "შეყვანის წყარო 1"), + ("input_source_2_tip", "შეყვანის წყარო 2"), + ("Swap control-command key", "Ctrl და Command ღილაკების მნიშვნელობების გაცვლა"), + ("swap-left-right-mouse", "მაუსის მარცხენა და მარჯვენა ღილაკების მნიშვნელობების გაცვლა"), + ("2FA code", "ორფაქტორიანი ავთენტიფიკაციის კოდი"), + ("More", "მეტი"), + ("enable-2fa-title", "ორფაქტორიანი ავთენტიფიკაციის გამოყენება"), + ("enable-2fa-desc", "მოაწყვეთ ავთენტიფიკაციის აპლიკაცია. გამოიყენეთ, მაგალითად, Authy, Microsoft ან Google Authenticator ტელეფონზე ან კომპიუტერზე.\n\nდაასკანერეთ QR კოდი ავთენტიფიკაციის აპლიკაციით და შეიყვანეთ კოდი, რომელიც გამოჩნდება ამ აპლიკაციაში, ორფაქტორიანი ავთენტიფიკაციის ჩასართავად."), + ("wrong-2fa-code", "კოდის დადასტურება შეუძლებელია. შეამოწმეთ კოდი და ადგილობრივი დროის პარამეტრები."), + ("enter-2fa-title", "ორფაქტორიანი ავთენტიფიკაცია"), + ("Email verification code must be 6 characters.", "ელ-ფოსტის დადასტურების კოდი უნდა შედგებოდეს 6 სიმბოლოსგან."), + ("2FA code must be 6 digits.", "ორფაქტორიანი ავთენტიფიკაციის კოდი უნდა შედგებოდეს 6 ციფრისგან."), + ("Multiple Windows sessions found", "აღმოჩენილია Windows-ის რამდენიმე სესია"), + ("Please select the session you want to connect to", "აირჩიეთ სესია, რომელთანაც გსურთ დაკავშირება"), + ("powered_by_me", "RustDesk-ზე დაფუძნებული"), + ("outgoing_only_desk_tip", "ეს სპეციალიზებული ვერსიაა.\nშეგიძლიათ დაუკავშირდეთ სხვა მოწყობილობებს, მაგრამ სხვა მოწყობილობებს არ შეუძლიათ დაუკავშირდნენ თქვენსას."), + ("preset_password_warning", "ეს სპეციალიზებული ვერსიაა წინასწარ დაყენებული პაროლით. ნებისმიერს, ვინც იცის ეს პაროლი, შეუძლია მიიღოს სრული კონტროლი თქვენს მოწყობილობაზე. თუ ეს თქვენთვის მოულოდნელია, დაუყოვნებლივ წაშალეთ ეს პროგრამული უზრუნველყოფა."), + ("Security Alert", "უსაფრთხოების გაფრთხილება"), + ("My address book", "ჩემი მისამართების წიგნი"), + ("Personal", "პირადი"), + ("Owner", "მფლობელი"), + ("Set shared password", "საზიარო პაროლის დაყენება"), + ("Exist in", "არსებობს"), + ("Read-only", "მხოლოდ წაკითხვა"), + ("Read/Write", "წაკითხვა და ჩაწერა"), + ("Full Control", "სრული კონტროლი"), + ("share_warning_tip", "ზემოთ მოცემული ველები საზიაროა და ხილულია სხვებისთვის."), + ("Everyone", "ყველა"), + ("ab_web_console_tip", "მეტი ვებ-კონსოლში"), + ("allow-only-conn-window-open-tip", "დაშვება მხოლოდ მაშინ, როცა RustDesk-ის ფანჯარა გახსნილია"), + ("no_need_privacy_mode_no_physical_displays_tip", "ფიზიკური ეკრანები არ არის, არ არის საჭირო კონფიდენციალურობის რეჟიმის გამოყენება."), + ("Follow remote cursor", "დისტანციური კურსორის მიყოლა"), + ("Follow remote window focus", "დისტანციური ფანჯრის ფოკუსის მიყოლა"), + ("default_proxy_tip", "ნაგულისხმევი პროტოკოლი და პორტი: Socks5 და 1080"), + ("no_audio_input_device_tip", "აუდიო შეყვანის მოწყობილობა ვერ მოიძებნა."), + ("Incoming", "შემომავალი"), + ("Outgoing", "გამავალი"), + ("Clear Wayland screen selection", "Wayland ეკრანის არჩევანის გაუქმება"), + ("clear_Wayland_screen_selection_tip", "გაუქმების შემდეგ შეგიძლიათ ხელახლა აირჩიოთ ეკრანი გასაზიარებლად."), + ("confirm_clear_Wayland_screen_selection_tip", "გავაუქმოთ Wayland ეკრანის არჩევანი?"), + ("android_new_voice_call_tip", "მიღებულია ახალი ხმოვანი ზარის მოთხოვნა. თუ მიიღებთ, ხმა გადაირთვება ხმოვან კავშირზე."), + ("texture_render_tip", "გამოიყენეთ ტექსტურების ვიზუალიზაცია გამოსახულებების უფრო გლუვად გასაკეთებლად."), + ("Use texture rendering", "ტექსტურების ვიზუალიზაცია"), + ("Floating window", "მოტივტივე ფანჯარა"), + ("floating_window_tip", "ეხმარება RustDesk-ის ფონური სერვისის შენარჩუნებას"), + ("Keep screen on", "ეკრანის ჩართულად შენარჩუნება"), + ("Never", "არასდროს"), + ("During controlled", "მართვისას"), + ("During service is on", "სერვისის მუშაობისას"), + ("Capture screen using DirectX", "ეკრანის გადაღება DirectX-ის გამოყენებით"), + ("Back", "უკან"), + ("Apps", "აპლიკაციები"), + ("Volume up", "ხმის გაზრდა"), + ("Volume down", "ხმის შემცირება"), + ("Power", "კვება"), + ("Telegram bot", "Telegram ბოტი"), + ("enable-bot-tip", "თუ ჩართულია, შეგიძლიათ მიიღოთ ორფაქტორიანი ავთენტიფიკაციის კოდი ბოტისგან. მას ასევე შეუძლია შეასრულოს დაკავშირების შეტყობინების ფუნქცია."), + ("enable-bot-desc", "1) გახსენით ჩატი @BotFather-თან.\n2) გაგზავნეთ ბრძანება \"/newbot\". ამ ნაბიჯის შესრულების შემდეგ მიიღებთ ტოკენს.\n3) დაიწყეთ ჩატი თქვენს ახლად შექმნილ ბოტთან. გაგზავნეთ შეტყობინება, რომელიც იწყება დახრილი ხაზით (\"/\"), მაგალითად, \"/hello\", მის გასააქტიურებლად.\n"), + ("cancel-2fa-confirm-tip", "გამოვრთოთ ორფაქტორიანი ავთენტიფიკაცია?"), + ("cancel-bot-confirm-tip", "გამოვრთოთ Telegram ბოტი?"), + ("About RustDesk", "RustDesk-ის შესახებ"), + ("Send clipboard keystrokes", "გაცვლის ბუფერიდან კლავიშების დაჭერის გაგზავნა"), + ("network_error_tip", "შეამოწმეთ ქსელთან კავშირი, შემდეგ დააჭირეთ \"განმეორება\"."), + ("Unlock with PIN", "PIN-კოდით განბლოკვა"), + ("Requires at least {} characters", "საჭიროა მინიმუმ {} სიმბოლო"), + ("Wrong PIN", "არასწორი PIN-კოდი"), + ("Set PIN", "PIN-კოდის დაყენება"), + ("Enable trusted devices", "სანდო მოწყობილობების ჩართვა"), + ("Manage trusted devices", "სანდო მოწყობილობების მართვა"), + ("Platform", "პლატფორმა"), + ("Days remaining", "დარჩენილი დღეები"), + ("enable-trusted-devices-tip", "სანდო მოწყობილობებს შეუძლიათ გამოტოვონ 2FA ავთენტიფიკაციის შემოწმება"), + ("Parent directory", "მშობელი საქაღალდე"), + ("Resume", "გაგრძელება"), + ("Invalid file name", "არასწორი ფაილის სახელი"), + ("one-way-file-transfer-tip", "მართულ მხარეზე ჩართულია ცალმხრივი ფაილების გადაცემა."), + ("Authentication Required", "საჭიროა ავთენტიფიკაცია"), + ("Authenticate", "ავთენტიფიკაცია"), + ("web_id_input_tip", "შეგიძლიათ შეიყვანოთ ID იმავე სერვერზე, პირდაპირი IP წვდომა ვებ-კლიენტში არ არის მხარდაჭერილი.\nთუ გსურთ წვდომა მოწყობილობაზე სხვა სერვერზე, დაამატეთ სერვერის მისამართი (@<სერვერის_მისამართი>?key=<გასაღები>), მაგალითად,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nთუ გსურთ წვდომა მოწყობილობაზე საჯარო სერვერზე, შეიყვანეთ \"@public\", საჯარო სერვერისთვის გასაღები არ არის საჭირო."), + ("Download", "ჩამოტვირთვა"), + ("Upload folder", "საქაღალდის ატვირთვა"), + ("Upload files", "ფაილების ატვირთვა"), + ("Clipboard is synchronized", "გაცვლის ბუფერი სინქრონიზებულია"), + ("Update client clipboard", "კლიენტის გაცვლის ბუფერის განახლება"), + ("Untagged", "უტეგო"), + ("new-version-of-{}-tip", "ხელმისაწვდომია ახალი ვერსია {}"), + ("Accessible devices", "ხელმისაწვდომი მოწყობილობები"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "განაახლეთ RustDesk კლიენტი ვერსიამდე {} ან უფრო ახალი დისტანციურ მხარეზე!"), + ("d3d_render_tip", "D3D ვიზუალიზაციის ჩართვისას ზოგიერთ მოწყობილობაზე დისტანციური ეკრანი შეიძლება იყოს შავი."), + ("Use D3D rendering", "D3D ვიზუალიზაციის გამოყენება"), + ("Printer", "პრინტერი"), + ("printer-os-requirement-tip", "პრინტერთან გამავალი კავშირის ფუნქციისთვის საჭიროა Windows 10 ან უფრო ახალი ვერსია."), + ("printer-requires-installed-{}-client-tip", "დისტანციური ბეჭდვის გამოსაყენებლად, {} უნდა იყოს დაინსტალირებული ამ მოწყობილობაზე."), + ("printer-{}-not-installed-tip", "პრინტერი {} არ არის დაინსტალირებული."), + ("printer-{}-ready-tip", "პრინტერი {} დაინსტალირებულია და მზად არის გამოსაყენებლად."), + ("Install {} Printer", "დააინსტალირეთ პრინტერი {}"), + ("Outgoing Print Jobs", "გამავალი ბეჭდვის დავალება"), + ("Incoming Print Jobs", "შემომავალი ბეჭდვის დავალება"), + ("Incoming Print Job", "შემომავალი ბეჭდვის დავალება"), + ("use-the-default-printer-tip", "ნაგულისხმევი პრინტერის გამოყენება"), + ("use-the-selected-printer-tip", "არჩეული პრინტერის გამოყენება"), + ("auto-print-tip", "ავტომატურად დაბეჭდეთ არჩეულ პრინტერზე."), + ("print-incoming-job-confirm-tip", "დისტანციური მოწყობილობიდან მიღებულია ბეჭდვის დავალება. გავუშვათ ლოკალურად?"), + ("remote-printing-disallowed-tile-tip", "დისტანციური ბეჭდვა აკრძალულია"), + ("remote-printing-disallowed-text-tip", "მართულ მხარეზე უფლებების პარამეტრები კრძალავს დისტანციურ ბეჭდვას."), + ("save-settings-tip", "პარამეტრების შენახვა"), + ("dont-show-again-tip", "აღარ აჩვენოთ"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "კამერის ნახვა"), + ("Enable camera", "კამერის ჩართვა"), + ("No cameras", "კამერა არ არის"), + ("view_camera_unsupported_tip", "დისტანციური მოწყობილობა არ უჭერს მხარს კამერის ნახვას."), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "{}-ით გაგრძელება"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/gu.rs b/vendor/rustdesk/src/lang/gu.rs new file mode 100644 index 0000000..8b8568c --- /dev/null +++ b/vendor/rustdesk/src/lang/gu.rs @@ -0,0 +1,747 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "સ્થિતિ"), + ("Your Desktop", "તમારું ડેસ્કટોપ"), + ("desk_tip", "તમારું ડેસ્કટોપ આ ID અને પાસવર્ડ દ્વારા એક્સેસ કરી શકાય છે."), + ("Password", "પાસવર્ડ"), + ("Ready", "તૈયાર"), + ("Established", "સ્થાપિત"), + ("connecting_status", "નેટવર્ક સાથે જોડાઈ રહ્યું છે..."), + ("Enable service", "સેવા સક્ષમ કરો"), + ("Start service", "સેવા શરૂ કરો"), + ("Service is running", "સેવા કાર્યરત છે"), + ("Service is not running", "સેવા કાર્યરત નથી"), + ("not_ready_status", "તૈયાર નથી. કૃપા કરીને તમારું કનેક્શન તપાસો"), + ("Control Remote Desktop", "રિમોટ ડેસ્કટોપ નિયંત્રિત કરો"), + ("Transfer file", "ફાઇલ ટ્રાન્સફર"), + ("Connect", "કનેક્ટ કરો"), + ("Recent sessions", "તાજેતરના સત્રો"), + ("Address book", "એડ્રેસ બુક"), + ("Confirmation", "પુષ્ટિકરણ"), + ("TCP tunneling", "TCP ટનલિંગ"), + ("Remove", "દૂર કરો"), + ("Refresh random password", "રેન્ડમ પાસવર્ડ બદલો"), + ("Set your own password", "તમારો પોતાનો પાસવર્ડ સેટ કરો"), + ("Enable keyboard/mouse", "કીબોર્ડ/માઉસ સક્ષમ કરો"), + ("Enable clipboard", "ક્લિપબોર્ડ સક્ષમ કરો"), + ("Enable file transfer", "ફાઇલ ટ્રાન્સફર સક્ષમ કરો"), + ("Enable TCP tunneling", "TCP ટનલિંગ સક્ષમ કરો"), + ("IP Whitelisting", "IP વ્હાઇટલિસ્ટિંગ"), + ("ID/Relay Server", "ID/રિલે સર્વર"), + ("Import server config", "સર્વર કોન્ફિગ ઈમ્પોર્ટ કરો"), + ("Export Server Config", "સર્વર કોન્ફિગ એક્સપોર્ટ કરો"), + ("Import server configuration successfully", "સર્વર કોન્ફિગરેશન સફળતાપૂર્વક ઈમ્પોર્ટ થયું"), + ("Export server configuration successfully", "સર્વર કોન્ફિગરેશન સફળતાપૂર્વક એક્સપોર્ટ થયું"), + ("Invalid server configuration", "અમાન્ય સર્વર કોન્ફિગરેશન"), + ("Clipboard is empty", "ક્લિપબોર્ડ ખાલી છે"), + ("Stop service", "સેવા બંધ કરો"), + ("Change ID", "ID બદલો"), + ("Your new ID", "તમારું નવું ID"), + ("length %min% to %max%", "લંબાઈ %min% થી %max% સુધી"), + ("starts with a letter", "અક્ષરથી શરૂ થાય છે"), + ("allowed characters", "માન્ય અક્ષરો"), + ("id_change_tip", "ID બદલ્યા પછી વર્તમાન કનેક્શન તૂટી જશે."), + ("Website", "વેબસાઇટ"), + ("About", "વિશે"), + ("Slogan_tip", "વધુ સારા અનુભવ માટે બનાવેલ રિમોટ ડેસ્કટોપ સોફ્ટવેર"), + ("Privacy Statement", "ગોપનીયતા નિવેદન"), + ("Mute", "મ્યૂટ કરો"), + ("Build Date", "બિલ્ડ તારીખ"), + ("Version", "સંસ્કરણ (Version)"), + ("Home", "હોમ"), + ("Audio Input", "ઓડિયો ઇનપુટ"), + ("Enhancements", "વધારાની સુવિધાઓ"), + ("Hardware Codec", "હાર્ડવેર કોડેક"), + ("Adaptive bitrate", "એડેપ્ટિવ બિટરેટ"), + ("ID Server", "ID સર્વર"), + ("Relay Server", "રિલે સર્વર"), + ("API Server", "API સર્વર"), + ("invalid_http", "અમાન્ય HTTP લિંક"), + ("Invalid IP", "અમાન્ય IP"), + ("Invalid format", "અમાન્ય ફોર્મેટ"), + ("server_not_support", "સર્વર દ્વારા સમર્થિત નથી"), + ("Not available", "ઉપલબ્ધ નથી"), + ("Too frequent", "ખૂબ વારંવાર"), + ("Cancel", "રદ કરો"), + ("Skip", "રહેવા દો (Skip)"), + ("Close", "બંધ કરો"), + ("Retry", "ફરી પ્રયાસ કરો"), + ("OK", "બરાબર"), + ("Password Required", "પાસવર્ડ જરૂરી છે"), + ("Please enter your password", "કૃપા કરીને તમારો પાસવર્ડ દાખલ કરો"), + ("Remember password", "પાસવર્ડ યાદ રાખો"), + ("Wrong Password", "ખોટો પાસવર્ડ"), + ("Do you want to enter again?", "શું તમે ફરીથી દાખલ કરવા માંગો છો?"), + ("Connection Error", "કનેક્શન ભૂલ"), + ("Error", "ભૂલ"), + ("Reset by the peer", "સામેના છેડેથી રિસેટ કરવામાં આવ્યું"), + ("Connecting...", "જોડાઈ રહ્યું છે..."), + ("Connection in progress. Please wait.", "કનેક્શન ચાલુ છે. કૃપા કરીને રાહ જુઓ."), + ("Please try 1 minute later", "કૃપા કરીને 1 મિનિટ પછી ફરી પ્રયાસ કરો"), + ("Login Error", "લોગિન ભૂલ"), + ("Successful", "સફળ"), + ("Connected, waiting for image...", "જોડાયેલ, ઇમેજની રાહ જોવાય છે..."), + ("Name", "નામ"), + ("Type", "પ્રકાર"), + ("Modified", "સુધારેલ"), + ("Size", "કદ (Size)"), + ("Show Hidden Files", "છુપાયેલી ફાઇલો બતાવો"), + ("Receive", "મેળવો"), + ("Send", "મોકલો"), + ("Refresh File", "ફાઇલ રિફ્રેશ કરો"), + ("Local", "લોકલ"), + ("Remote", "રિમોટ"), + ("Remote Computer", "રિમોટ કોમ્પ્યુટર"), + ("Local Computer", "લોકલ કોમ્પ્યુટર"), + ("Confirm Delete", "કાઢી નાખવાની પુષ્ટિ કરો"), + ("Delete", "કાઢી નાખો"), + ("Properties", "ગુણધર્મો (Properties)"), + ("Multi Select", "બહુ-પસંદગી"), + ("Select All", "બધું પસંદ કરો"), + ("Unselect All", "બધું નાપસંદ કરો"), + ("Empty Directory", "ખાલી ડિરેક્ટરી"), + ("Not an empty directory", "ડિરેક્ટરી ખાલી નથી"), + ("Are you sure you want to delete this file?", "શું તમે ખરેખર આ ફાઇલ કાઢી નાખવા માંગો છો?"), + ("Are you sure you want to delete this empty directory?", "શું તમે ખરેખર આ ખાલી ડિરેક્ટરી કાઢી નાખવા માંગો છો?"), + ("Are you sure you want to delete the file of this directory?", "શું તમે ખરેખર આ ડિરેક્ટરીની ફાઇલ કાઢી નાખવા માંગો છો?"), + ("Do this for all conflicts", "તમામ વિવાદો માટે આ કરો"), + ("This is irreversible!", "આ બદલી શકાશે નહીં!"), + ("Deleting", "કાઢી નાખવામાં આવી રહ્યું છે"), + ("files", "ફાઇલો"), + ("Waiting", "રાહ જુઓ"), + ("Finished", "પૂરું થયું"), + ("Speed", "ગતિ"), + ("Custom Image Quality", "કસ્ટમ ઇમેજ ગુણવત્તા"), + ("Privacy mode", "પ્રાઇવસી મોડ"), + ("Block user input", "યુઝર ઇનપુટ બ્લોક કરો"), + ("Unblock user input", "યુઝર ઇનપુટ અનબ્લોક કરો"), + ("Adjust Window", "વિન્ડો એડજસ્ટ કરો"), + ("Original", "મૂળ (Original)"), + ("Shrink", "સંકોચો (Shrink)"), + ("Stretch", "ખેંચો (Stretch)"), + ("Scrollbar", "સ્ક્રોલબાર"), + ("ScrollAuto", "ઓટો સ્ક્રોલ"), + ("Good image quality", "સારી ઇમેજ ગુણવત્તા"), + ("Balanced", "સંતુલિત"), + ("Optimize reaction time", "પ્રતિક્રિયા સમય શ્રેષ્ઠ બનાવો"), + ("Custom", "કસ્ટમ"), + ("Show remote cursor", "રિમોટ કર્સર બતાવો"), + ("Show quality monitor", "ક્વોલિટી મોનિટર બતાવો"), + ("Disable clipboard", "ક્લિપબોર્ડ અક્ષમ કરો"), + ("Lock after session end", "સત્ર સમાપ્ત થયા પછી લોક કરો"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del દાખલ કરો"), + ("Insert Lock", "લોક દાખલ કરો"), + ("Refresh", "રિફ્રેશ કરો"), + ("ID does not exist", "ID અસ્તિત્વમાં નથી"), + ("Failed to connect to rendezvous server", "Rendezvous સર્વર સાથે જોડવામાં નિષ્ફળ"), + ("Please try later", "કૃપા કરીને પછી પ્રયાસ કરો"), + ("Remote desktop is offline", "રિમોટ ડેસ્કટોપ ઓફલાઇન છે"), + ("Key mismatch", "કી મેળ ખાતી નથી"), + ("Timeout", "સમય સમાપ્ત"), + ("Failed to connect to relay server", "રિલે સર્વર સાથે જોડવામાં નિષ્ફળ"), + ("Failed to connect via rendezvous server", "Rendezvous સર્વર દ્વારા જોડવામાં નિષ્ફળ"), + ("Failed to connect via relay server", "રિલે સર્વર દ્વારા જોડવામાં નિષ્ફળ"), + ("Failed to make direct connection to remote desktop", "રિમોટ ડેસ્કટોપ સાથે સીધું જોડાણ કરવામાં નિષ્ફળ"), + ("Set Password", "પાસવર્ડ સેટ કરો"), + ("OS Password", "OS પાસવર્ડ"), + ("install_tip", "શ્રેષ્ઠ પ્રદર્શન માટે, કૃપા કરીને ઇન્સ્ટોલ કરો."), + ("Click to upgrade", "અપગ્રેડ કરવા માટે ક્લિક કરો"), + ("Configure", "કોન્ફિગર કરો"), + ("config_acc", "એક્સેસિબિલિટી કોન્ફિગર કરો"), + ("config_screen", "સ્ક્રીન કોન્ફિગર કરો"), + ("Installing ...", "ઇન્સ્ટોલ થઈ રહ્યું છે..."), + ("Install", "ઇન્સ્ટોલ કરો"), + ("Installation", "ઇન્સ્ટોલેશન"), + ("Installation Path", "ઇન્સ્ટોલેશન પાથ"), + ("Create start menu shortcuts", "સ્ટાર્ટ મેનૂ શોર્ટકટ બનાવો"), + ("Create desktop icon", "ડેસ્કટોપ આઇકોન બનાવો"), + ("agreement_tip", "ઇન્સ્ટોલ કરીને તમે લાયસન્સ કરાર સ્વીકારો છો."), + ("Accept and Install", "સ્વીકારો અને ઇન્સ્ટોલ કરો"), + ("End-user license agreement", "અંતિમ વપરાશકર્તા લાયસન્સ કરાર"), + ("Generating ...", "જનરેટ થઈ રહ્યું છે..."), + ("Your installation is lower version.", "તમારું ઇન્સ્ટોલેશન જૂનું સંસ્કરણ છે."), + ("not_close_tcp_tip", "ટનલનો ઉપયોગ કરતી વખતે આ વિન્ડો બંધ કરશો નહીં."), + ("Listening ...", "સાંભળી રહ્યું છે..."), + ("Remote Host", "રિમોટ હોસ્ટ"), + ("Remote Port", "રિમોટ પોર્ટ"), + ("Action", "ક્રિયા"), + ("Add", "ઉમેરો"), + ("Local Port", "લોકલ પોર્ટ"), + ("Local Address", "લોકલ સરનામું"), + ("Change Local Port", "લોકલ પોર્ટ બદલો"), + ("setup_server_tip", "ઝડપી કનેક્શન માટે તમારું પોતાનું સર્વર સેટ કરો"), + ("Too short, at least 6 characters.", "ખૂબ ટૂંકું, ઓછામાં ઓછા 6 અક્ષરો હોવા જોઈએ."), + ("The confirmation is not identical.", "પુષ્ટિકરણ સરખું નથી."), + ("Permissions", "પરવાનગીઓ"), + ("Accept", "સ્વીકારો"), + ("Dismiss", "ખારીજ કરો"), + ("Disconnect", "ડિસ્કનેક્ટ કરો"), + ("Enable file copy and paste", "ફાઇલ કોપી અને પેસ્ટ સક્ષમ કરો"), + ("Connected", "જોડાયેલ"), + ("Direct and encrypted connection", "સીધું અને એન્ક્રિપ્ટેડ કનેક્શન"), + ("Relayed and encrypted connection", "રિલે અને એન્ક્રિપ્ટેડ કનેક્શન"), + ("Direct and unencrypted connection", "સીધું અને અનએન્ક્રિપ્ટેડ કનેક્શન"), + ("Relayed and unencrypted connection", "રિલે અને અનએન્ક્રિપ્ટેડ કનેક્શન"), + ("Enter Remote ID", "રિમોટ ID દાખલ કરો"), + ("Enter your password", "તમારો પાસવર્ડ દાખલ કરો"), + ("Logging in...", "લોગિન થઈ રહ્યું છે..."), + ("Enable RDP session sharing", "RDP સત્ર શેરિંગ સક્ષમ કરો"), + ("Auto Login", "ઓટો લોગિન"), + ("Enable direct IP access", "સીધું IP એક્સેસ સક્ષમ કરો"), + ("Rename", "નામ બદલો"), + ("Space", "જગ્યા (Space)"), + ("Create desktop shortcut", "ડેસ્કટોપ શોર્ટકટ બનાવો"), + ("Change Path", "પાથ બદલો"), + ("Create Folder", "ફોલ્ડર બનાવો"), + ("Please enter the folder name", "કૃપા કરીને ફોલ્ડરનું નામ દાખલ કરો"), + ("Fix it", "તેને ઠીક કરો"), + ("Warning", "ચેતવણી"), + ("Login screen using Wayland is not supported", "Wayland ઉપયોગ કરતી લોગિન સ્ક્રીન સમર્થિત નથી"), + ("Reboot required", "રિબૂટ જરૂરી છે"), + ("Unsupported display server", "અસમર્થિત ડિસ્પ્લે સર્વર"), + ("x11 expected", "x11 અપેક્ષિત છે"), + ("Port", "પોર્ટ"), + ("Settings", "સેટિંગ્સ"), + ("Username", "વપરાશકર્તા નામ"), + ("Invalid port", "અમાન્ય પોર્ટ"), + ("Closed manually by the peer", "સામેથી મેન્યુઅલી બંધ કરવામાં આવ્યું"), + ("Enable remote configuration modification", "રિમોટ કોન્ફિગરેશન ફેરફાર સક્ષમ કરો"), + ("Run without install", "ઇન્સ્ટોલ કર્યા વગર ચલાવો"), + ("Connect via relay", "રિલે દ્વારા કનેક્ટ કરો"), + ("Always connect via relay", "હંમેશા રિલે દ્વારા કનેક્ટ કરો"), + ("whitelist_tip", "માત્ર વ્હાઇટલિસ્ટ કરેલ IP જ મને એક્સેસ કરી શકે છે"), + ("Login", "લોગિન"), + ("Verify", "ચકાસો"), + ("Remember me", "મને યાદ રાખો"), + ("Trust this device", "આ ઉપકરણ પર વિશ્વાસ કરો"), + ("Verification code", "વેરિફિકેશન કોડ"), + ("verification_tip", "વેરિફિકેશન કોડ તમારા ઇમેઇલ પર મોકલવામાં આવ્યો છે"), + ("Logout", "લોગઆઉટ"), + ("Tags", "ટેગ્સ"), + ("Search ID", "ID શોધો"), + ("whitelist_sep", "અલ્પવિરામ, અર્ધવિરામ અથવા સ્પેસ દ્વારા અલગ કરો"), + ("Add ID", "ID ઉમેરો"), + ("Add Tag", "ટેગ ઉમેરો"), + ("Unselect all tags", "તમામ ટેગ નાપસંદ કરો"), + ("Network error", "નેટવર્ક ભૂલ"), + ("Username missed", "વપરાશકર્તા નામ બાકી છે"), + ("Password missed", "પાસવર્ડ બાકી છે"), + ("Wrong credentials", "ખોટી વિગતો"), + ("The verification code is incorrect or has expired", "વેરિફિકેશન કોડ ખોટો છે અથવા તેની મર્યાદા પૂરી થઈ ગઈ છે"), + ("Edit Tag", "ટેગ સુધારો"), + ("Forget Password", "પાસવર્ડ ભૂલી ગયા"), + ("Favorites", "પસંદગીના"), + ("Add to Favorites", "પસંદગીમાં ઉમેરો"), + ("Remove from Favorites", "પસંદગીમાંથી દૂર કરો"), + ("Empty", "ખાલી"), + ("Invalid folder name", "અમાન્ય ફોલ્ડર નામ"), + ("Socks5 Proxy", "Socks5 પ્રોક્સી"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) પ્રોક્સી"), + ("Discovered", "શોધાયેલ"), + ("install_daemon_tip", "બૂટ વખતે શરૂ કરવા માટે સેવા ઇન્સ્ટોલ કરો"), + ("Remote ID", "રિમોટ ID"), + ("Paste", "પેસ્ટ કરો"), + ("Paste here?", "અહીં પેસ્ટ કરવું છે?"), + ("Are you sure to close the connection?", "શું તમે ખરેખર કનેક્શન બંધ કરવા માંગો છો?"), + ("Download new version", "નવું સંસ્કરણ ડાઉનલોડ કરો"), + ("Touch mode", "ટચ મોડ"), + ("Mouse mode", "માઉસ મોડ"), + ("One-Finger Tap", "એક આંગળીથી ટેપ"), + ("Left Mouse", "ડાબું માઉસ બટન"), + ("One-Long Tap", "એક લાંબો ટેપ"), + ("Two-Finger Tap", "બે આંગળીથી ટેપ"), + ("Right Mouse", "જમણું માઉસ બટન"), + ("One-Finger Move", "એક આંગળીથી હલનચલન"), + ("Double Tap & Move", "ડબલ ટેપ અને હલનચલન"), + ("Mouse Drag", "માઉસ ડ્રેગ"), + ("Three-Finger vertically", "ત્રણ આંગળી ઊભી રીતે"), + ("Mouse Wheel", "માઉસ વ્હીલ"), + ("Two-Finger Move", "બે આંગળીથી હલનચલન"), + ("Canvas Move", "કેનવાસ ખસેડો"), + ("Pinch to Zoom", "ઝૂમ કરવા માટે પિંચ કરો"), + ("Canvas Zoom", "કેનવાસ ઝૂમ"), + ("Reset canvas", "કેનવાસ રિસેટ કરો"), + ("No permission of file transfer", "ફાઇલ ટ્રાન્સફરની પરવાનગી નથી"), + ("Note", "નોંધ"), + ("Connection", "કનેક્શન"), + ("Share screen", "સ્ક્રીન શેર કરો"), + ("Chat", "ચેટ"), + ("Total", "કુલ"), + ("items", "વસ્તુઓ"), + ("Selected", "પસંદ કરેલ"), + ("Screen Capture", "સ્ક્રીન કેપ્ચર"), + ("Input Control", "ઇનપુટ નિયંત્રણ"), + ("Audio Capture", "ઓડિયો કેપ્ચર"), + ("Do you accept?", "શું તમે સ્વીકારો છો?"), + ("Open System Setting", "સિસ્ટમ સેટિંગ ખોલો"), + ("How to get Android input permission?", "Android ઇનપુટ પરવાનગી કેવી રીતે મેળવવી?"), + ("android_input_permission_tip1", "ઇનપુટ પરવાનગી મેળવવા માટે એક્સેસિબિલિટી સેવા સક્ષમ કરો."), + ("android_input_permission_tip2", "કૃપા કરીને સેટિંગ્સમાં RustDesk શોધો અને તેને ચાલુ કરો."), + ("android_new_connection_tip", "નવો કંટ્રોલ વિનંતી પ્રાપ્ત થઈ છે."), + ("android_service_will_start_tip", "સ્ક્રીન કેપ્ચર ચાલુ કરવાથી સેવા આપમેળે શરૂ થશે."), + ("android_stop_service_tip", "સેવા બંધ કરવાથી તમામ કનેક્શન બંધ થઈ જશે."), + ("android_version_audio_tip", "ઓડિયો કેપ્ચર માત્ર Android 10 કે તેથી ઉપરના વર્ઝનમાં ઉપલબ્ધ છે."), + ("android_start_service_tip", "સ્ક્રીન શેરિંગ સેવા શરૂ કરવા ક્લિક કરો."), + ("android_permission_may_not_change_tip", "પરવાનગીઓ પછીથી બદલી શકાશે નહીં, કૃપા કરીને કાળજીપૂર્વક પસંદ કરો."), + ("Account", "ખાતું"), + ("Overwrite", "ઓવરરાઇટ કરો"), + ("This file exists, skip or overwrite this file?", "આ ફાઇલ અસ્તિત્વમાં છે, રહેવા દેવી છે કે ઓવરરાઇટ કરવી છે?"), + ("Quit", "બહાર નીકળો"), + ("Help", "મદદ"), + ("Failed", "નિષ્ફળ"), + ("Succeeded", "સફળ"), + ("Someone turns on privacy mode, exit", "કોઈએ પ્રાઇવસી મોડ ચાલુ કર્યો છે, બહાર નીકળો"), + ("Unsupported", "અસમર્થિત"), + ("Peer denied", "સામેથી નકારવામાં આવ્યું"), + ("Please install plugins", "કૃપા કરીને પ્લગઇન્સ ઇન્સ્ટોલ કરો"), + ("Peer exit", "સામેથી કોઈ બહાર નીકળી ગયું"), + ("Failed to turn off", "બંધ કરવામાં નિષ્ફળ"), + ("Turned off", "બંધ કરવામાં આવ્યું"), + ("Language", "ભાષા"), + ("Keep RustDesk background service", "RustDesk બેકગ્રાઉન્ડ સેવા ચાલુ રાખો"), + ("Ignore Battery Optimizations", "બેટરી ઓપ્ટિમાઇઝેશન અવગણો"), + ("android_open_battery_optimizations_tip", "ડિસ્કનેક્શન ટાળવા માટે બેટરી ઓપ્ટિમાઇઝેશન સેટિંગ ખોલો"), + ("Start on boot", "બૂટ પર શરૂ કરો"), + ("Start the screen sharing service on boot, requires special permissions", "બૂટ પર સ્ક્રીન શેરિંગ શરૂ કરો, ખાસ પરવાનગીની જરૂર છે"), + ("Connection not allowed", "કનેક્શનની પરવાનગી નથી"), + ("Legacy mode", "લેગસી મોડ"), + ("Map mode", "મેપ મોડ"), + ("Translate mode", "અનુવાદ મોડ"), + ("Use permanent password", "કાયમી પાસવર્ડનો ઉપયોગ કરો"), + ("Use both passwords", "બંને પાસવર્ડનો ઉપયોગ કરો"), + ("Set permanent password", "કાયમી પાસવર્ડ સેટ કરો"), + ("Enable remote restart", "રિમોટ રિસ્ટાર્ટ સક્ષમ કરો"), + ("Restart remote device", "રિમોટ ઉપકરણ રિસ્ટાર્ટ કરો"), + ("Are you sure you want to restart", "શું તમે ખરેખર રિસ્ટાર્ટ કરવા માંગો છો?"), + ("Restarting remote device", "રિમોટ ઉપકરણ રિસ્ટાર્ટ થઈ રહ્યું છે"), + ("remote_restarting_tip", "રિમોટ ઉપકરણ રિસ્ટાર્ટ થઈ રહ્યું છે, કૃપા કરીને રાહ જુઓ..."), + ("Copied", "કોપી થઈ ગયું"), + ("Exit Fullscreen", "ફુલસ્ક્રીનમાંથી બહાર નીકળો"), + ("Fullscreen", "ફુલસ્ક્રીન"), + ("Mobile Actions", "મોબાઇલ ક્રિયાઓ"), + ("Select Monitor", "મોનિટર પસંદ કરો"), + ("Control Actions", "નિયંત્રણ ક્રિયાઓ"), + ("Display Settings", "ડિસ્પ્લે સેટિંગ્સ"), + ("Ratio", "રેશિયો (Ratio)"), + ("Image Quality", "ઇમેજ ગુણવત્તા"), + ("Scroll Style", "સ્ક્રોલ શૈલી"), + ("Show Toolbar", "ટૂલબાર બતાવો"), + ("Hide Toolbar", "ટૂલબાર છુપાવો"), + ("Direct Connection", "સીધું કનેક્શન"), + ("Relay Connection", "રિલે કનેક્શન"), + ("Secure Connection", "સુરક્ષિત કનેક્શન"), + ("Insecure Connection", "અસુરક્ષિત કનેક્શન"), + ("Scale original", "મૂળ સ્કેલ"), + ("Scale adaptive", "એડેપ્ટિવ સ્કેલ"), + ("General", "સામાન્ય"), + ("Security", "સુરક્ષા"), + ("Theme", "થીમ"), + ("Dark Theme", "ડાર્ક થીમ"), + ("Light Theme", "લાઇટ થીમ"), + ("Dark", "ડાર્ક"), + ("Light", "લાઇટ"), + ("Follow System", "સિસ્ટમ મુજબ"), + ("Enable hardware codec", "હાર્ડવેર કોડેક સક્ષમ કરો"), + ("Unlock Security Settings", "સુરક્ષા સેટિંગ્સ અનલોક કરો"), + ("Enable audio", "ઓડિયો સક્ષમ કરો"), + ("Unlock Network Settings", "નેટવર્ક સેટિંગ્સ અનલોક કરો"), + ("Server", "સર્વર"), + ("Direct IP Access", "સીધું IP એક્સેસ"), + ("Proxy", "પ્રોક્સી"), + ("Apply", "લાગુ કરો"), + ("Disconnect all devices?", "તમામ ઉપકરણો ડિસ્કનેક્ટ કરવા છે?"), + ("Clear", "સાફ કરો"), + ("Audio Input Device", "ઓડિયો ઇનપુટ ઉપકરણ"), + ("Use IP Whitelisting", "IP વ્હાઇટલિસ્ટિંગનો ઉપયોગ કરો"), + ("Network", "નેટવર્ક"), + ("Pin Toolbar", "ટૂલબાર પિન કરો"), + ("Unpin Toolbar", "ટૂલબાર અનપિન કરો"), + ("Recording", "રેકોર્ડિંગ"), + ("Directory", "ડિરેક્ટરી"), + ("Automatically record incoming sessions", "આવતા સત્રો આપમેળે રેકોર્ડ કરો"), + ("Automatically record outgoing sessions", "જતા સત્રો આપમેળે રેકોર્ડ કરો"), + ("Change", "બદલો"), + ("Start session recording", "સત્ર રેકોર્ડિંગ શરૂ કરો"), + ("Stop session recording", "સત્ર રેકોર્ડિંગ બંધ કરો"), + ("Enable recording session", "સત્ર રેકોર્ડિંગ સક્ષમ કરો"), + ("Enable LAN discovery", "LAN ડિસ્કવરી સક્ષમ કરો"), + ("Deny LAN discovery", "LAN ડિસ્કવરી નકારો"), + ("Write a message", "સંદેશ લખો"), + ("Prompt", "પ્રોમ્પ્ટ"), + ("Please wait for confirmation of UAC...", "કૃપા કરીને UAC પુષ્ટિની રાહ જુઓ..."), + ("elevated_foreground_window_tip", "રિમોટની વર્તમાન વિન્ડોને વધારે પરવાનગીની જરૂર છે."), + ("Disconnected", "ડિસ્કનેક્ટ થઈ ગયું"), + ("Other", "અન્ય"), + ("Confirm before closing multiple tabs", "બહુવિધ ટેબ્સ બંધ કરતા પહેલા પુષ્ટિ કરો"), + ("Keyboard Settings", "કીબોર્ડ સેટિંગ્સ"), + ("Full Access", "પૂર્ણ એક્સેસ"), + ("Screen Share", "સ્ક્રીન શેર"), + ("ubuntu-21-04-required", "Ubuntu 21.04 કે તેથી ઉપર જરૂરી છે"), + ("wayland-requires-higher-linux-version", "Wayland માટે ઉચ્ચ Linux વર્ઝન જરૂરી છે"), + ("xdp-portal-unavailable", "XDP પોર્ટલ અનુપલબ્ધ છે"), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "કૃપા કરીને શેર કરવાની સ્ક્રીન પસંદ કરો (સામેના છેડે કાર્ય કરો)."), + ("Show RustDesk", "RustDesk બતાવો"), + ("This PC", "આ PC"), + ("or", "અથવા"), + ("Elevate", "એલિવેટ કરો"), + ("Zoom cursor", "ઝૂમ કર્સર"), + ("Accept sessions via password", "પાસવર્ડ દ્વારા સત્રો સ્વીકારો"), + ("Accept sessions via click", "ક્લિક દ્વારા સત્રો સ્વીકારો"), + ("Accept sessions via both", "બંને દ્વારા સત્રો સ્વીકારો"), + ("Please wait for the remote side to accept your session request...", "કૃપા કરીને સામેનો છેડો વિનંતી સ્વીકારે તેની રાહ જુઓ..."), + ("One-time Password", "વન-ટાઇમ પાસવર્ડ (OTP)"), + ("Use one-time password", "વન-ટાઇમ પાસવર્ડનો ઉપયોગ કરો"), + ("One-time password length", "OTP ની લંબાઈ"), + ("Request access to your device", "તમારા ઉપકરણના એક્સેસ માટે વિનંતી"), + ("Hide connection management window", "કનેક્શન મેનેજમેન્ટ વિન્ડો છુપાવો"), + ("hide_cm_tip", "જો પાસવર્ડ દ્વારા કનેક્શન હોય તો જ છુપાવો"), + ("wayland_experiment_tip", "Wayland સપોર્ટ હજુ પ્રાયોગિક ધોરણે છે"), + ("Right click to select tabs", "ટેબ્સ પસંદ કરવા રાઇટ ક્લિક કરો"), + ("Skipped", "રહેવા દીધું (Skipped)"), + ("Add to address book", "એડ્રેસ બુકમાં ઉમેરો"), + ("Group", "ગ્રુપ"), + ("Search", "શોધો"), + ("Closed manually by web console", "વેબ કન્સોલ દ્વારા મેન્યુઅલી બંધ કરવામાં આવ્યું"), + ("Local keyboard type", "લોકલ કીબોર્ડ પ્રકાર"), + ("Select local keyboard type", "લોકલ કીબોર્ડ પ્રકાર પસંદ કરો"), + ("software_render_tip", "જો સ્ક્રીન કાળી દેખાય, તો આ અજમાવો"), + ("Always use software rendering", "હંમેશા સોફ્ટવેર રેન્ડરિંગનો ઉપયોગ કરો"), + ("config_input", "ઇનપુટ કોન્ફિગર કરો"), + ("config_microphone", "માઇક્રોફોન કોન્ફિગર કરો"), + ("request_elevation_tip", "સામેથી ઉચ્ચ પરવાનગી (Elevation) માટે વિનંતી કરો"), + ("Wait", "રાહ જુઓ"), + ("Elevation Error", "એલિવેશન ભૂલ"), + ("Ask the remote user for authentication", "સામેના યુઝરને ઓથેન્ટિકેશન માટે પૂછો"), + ("Choose this if the remote account is administrator", "જો સામેનું ખાતું એડમિનિસ્ટ્રેટર હોય તો આ પસંદ કરો"), + ("Transmit the username and password of administrator", "એડમિનિસ્ટ્રેટરનું નામ અને પાસવર્ડ મોકલો"), + ("still_click_uac_tip", "રિમોટ યુઝરે હજુ પણ UAC વિન્ડોમાં 'હા' ક્લિક કરવું પડશે."), + ("Request Elevation", "એલિવેશન માટે વિનંતી કરો"), + ("wait_accept_uac_tip", "કૃપા કરીને સામેનો યુઝર UAC સ્વીકારે તેની રાહ જુઓ."), + ("Elevate successfully", "સફળતાપૂર્વક એલિવેટ થયું"), + ("uppercase", "મોટા અક્ષરો (Uppercase)"), + ("lowercase", "નાના અક્ષરો (Lowercase)"), + ("digit", "અંક (Digit)"), + ("special character", "ખાસ અક્ષર"), + ("length>=8", "લંબાઈ >= 8"), + ("Weak", "નબળું"), + ("Medium", "મધ્યમ"), + ("Strong", "મજબૂત"), + ("Switch Sides", "બાજુઓ બદલો"), + ("Please confirm if you want to share your desktop?", "શું તમે તમારું ડેસ્કટોપ શેર કરવા માંગો છો?"), + ("Display", "ડિસ્પ્લે"), + ("Default View Style", "ડિફોલ્ટ વ્યુ શૈલી"), + ("Default Scroll Style", "ડિફોલ્ટ સ્ક્રોલ શૈલી"), + ("Default Image Quality", "ડિફોલ્ટ ઇમેજ ગુણવત્તા"), + ("Default Codec", "ડિફોલ્ટ કોડેક"), + ("Bitrate", "બિટરેટ"), + ("FPS", "FPS"), + ("Auto", "ઓટો"), + ("Other Default Options", "અન્ય ડિફોલ્ટ વિકલ્પો"), + ("Voice call", "વોઇસ કોલ"), + ("Text chat", "ટેક્સ્ટ ચેટ"), + ("Stop voice call", "વોઇસ કોલ બંધ કરો"), + ("relay_hint_tip", "સીધું કનેક્શન શક્ય નથી; તમે રિલે દ્વારા પ્રયાસ કરી શકો છો."), + ("Reconnect", "ફરી કનેક્ટ કરો"), + ("Codec", "કોડેક"), + ("Resolution", "રિઝોલ્યુશન"), + ("No transfers in progress", "કોઈ ટ્રાન્સફર ચાલુ નથી"), + ("Set one-time password length", "OTP લંબાઈ સેટ કરો"), + ("RDP Settings", "RDP સેટિંગ્સ"), + ("Sort by", "ક્રમબદ્ધ કરો"), + ("New Connection", "નવું કનેક્શન"), + ("Restore", "રીસ્ટોર"), + ("Minimize", "મિનિમાઇઝ"), + ("Maximize", "મેક્સિમાઇઝ"), + ("Your Device", "તમારું ઉપકરણ"), + ("empty_recent_tip", "તાજેતરના સત્રો અહીં દેખાશે."), + ("empty_favorite_tip", "પસંદગીના ઉપકરણો અહીં દેખાશે."), + ("empty_lan_tip", "નેટવર્ક પરના ઉપકરણો અહીં દેખાશે."), + ("empty_address_book_tip", "તમારી એડ્રેસ બુક ખાલી છે."), + ("Empty Username", "ખાલી યુઝરનેમ"), + ("Empty Password", "ખાલી પાસવર્ડ"), + ("Me", "હું"), + ("identical_file_tip", "આ ફાઇલ પહેલેથી જ અસ્તિત્વમાં છે."), + ("show_monitors_tip", "ટૂલબારમાં મોનિટર બતાવો"), + ("View Mode", "વ્યુ મોડ"), + ("login_linux_tip", "રિમોટ Linux સત્ર માટે તમારે લોગિન કરવું પડશે"), + ("verify_rustdesk_password_tip", "RustDesk પાસવર્ડ ચકાસો"), + ("remember_account_tip", "આ ખાતું યાદ રાખો"), + ("os_account_desk_tip", "એક્સેસ માટે OS ખાતાનો ઉપયોગ કરો"), + ("OS Account", "OS ખાતું"), + ("another_user_login_title_tip", "બીજો યુઝર પહેલેથી લોગિન છે"), + ("another_user_login_text_tip", "ડિસ્કનેક્ટ કરો અને ફરી પ્રયાસ કરો"), + ("xorg_not_found_title_tip", "Xorg મળ્યું નથી"), + ("xorg_not_found_text_tip", "કૃપા કરીને Xorg ઇન્સ્ટોલ કરો"), + ("no_desktop_title_tip", "કોઈ ડેસ્કટોપ ઉપલબ્ધ નથી"), + ("no_desktop_text_tip", "કૃપા કરીને Linux ડેસ્કટોપ ઇન્સ્ટોલ કરો"), + ("No need to elevate", "એલિવેટ કરવાની જરૂર નથી"), + ("System Sound", "સિસ્ટમ સાઉન્ડ"), + ("Default", "ડિફોલ્ટ"), + ("New RDP", "નવું RDP"), + ("Fingerprint", "ફિંગરપ્રિન્ટ"), + ("Copy Fingerprint", "ફિંગરપ્રિન્ટ કોપી કરો"), + ("no fingerprints", "કોઈ ફિંગરપ્રિન્ટ નથી"), + ("Select a peer", "એક પીઅર પસંદ કરો"), + ("Select peers", "પીઅર્સ પસંદ કરો"), + ("Plugins", "પ્લગઇન્સ"), + ("Uninstall", "અનઇન્સ્ટોલ કરો"), + ("Update", "અપડેટ કરો"), + ("Enable", "સક્ષમ કરો"), + ("Disable", "અક્ષમ કરો"), + ("Options", "વિકલ્પો"), + ("resolution_original_tip", "મૂળ રિઝોલ્યુશન"), + ("resolution_fit_local_tip", "સ્ક્રીન મુજબ ફીટ કરો"), + ("resolution_custom_tip", "કસ્ટમ રિઝોલ્યુશન"), + ("Collapse toolbar", "ટૂલબાર નાનું કરો"), + ("Accept and Elevate", "સ્વીકારો અને એલિવેટ કરો"), + ("accept_and_elevate_btn_tooltip", "કનેક્શન સ્વીકારો અને UAC પરવાનગીઓ મેળવો."), + ("clipboard_wait_response_timeout_tip", "ક્લિપબોર્ડ પ્રતિક્રિયા માટે સમય સમાપ્ત થયો."), + ("Incoming connection", "આવતું કનેક્શન"), + ("Outgoing connection", "જતું કનેક્શન"), + ("Exit", "બહાર નીકળો"), + ("Open", "ખોલો"), + ("logout_tip", "શું તમે ખરેખર લોગઆઉટ કરવા માંગો છો?"), + ("Service", "સેવા"), + ("Start", "શરૂ કરો"), + ("Stop", "બંધ કરો"), + ("exceed_max_devices", "તમે ઉપકરણોની મહત્તમ મર્યાદા વટાવી દીધી છે."), + ("Sync with recent sessions", "તાજેતરના સત્રો સાથે સિંક કરો"), + ("Sort tags", "ટેગ્સ ક્રમબદ્ધ કરો"), + ("Open connection in new tab", "નવી ટેબમાં કનેક્શન ખોલો"), + ("Move tab to new window", "ટેબને નવી વિન્ડોમાં ખસેડો"), + ("Can not be empty", "ખાલી ન હોઈ શકે"), + ("Already exists", "પહેલેથી અસ્તિત્વમાં છે"), + ("Change Password", "પાસવર્ડ બદલો"), + ("Refresh Password", "પાસવર્ડ રિફ્રેશ કરો"), + ("ID", "ID"), + ("Grid View", "ગ્રીડ વ્યુ"), + ("List View", "લિસ્ટ વ્યુ"), + ("Select", "પસંદ કરો"), + ("Toggle Tags", "ટેગ્સ ચાલુ/બંધ કરો"), + ("pull_ab_failed_tip", "એડ્રેસ બુક અપડેટ કરવામાં નિષ્ફળ."), + ("push_ab_failed_tip", "એડ્રેસ બુક સિંક કરવામાં નિષ્ફળ."), + ("synced_peer_readded_tip", "તાજેતરના સત્રોના ઉપકરણો એડ્રેસ બુકમાં સિંક થયા."), + ("Change Color", "રંગ બદલો"), + ("Primary Color", "પ્રાથમિક રંગ"), + ("HSV Color", "HSV રંગ"), + ("Installation Successful!", "ઇન્સ્ટોલેશન સફળ!"), + ("Installation failed!", "ઇન્સ્ટોલેશન નિષ્ફળ!"), + ("Reverse mouse wheel", "માઉસ વ્હીલ ઊલટું કરો"), + ("{} sessions", "{} સત્રો"), + ("scam_title", "છેતરપિંડીની ચેતવણી!"), + ("scam_text1", "જો તમે અજાણી વ્યક્તિ સાથે વાત કરી રહ્યા હો અને તેણે RustDesk વાપરવા કહ્યું હોય, તો તરત ડિસ્કનેક્ટ કરો."), + ("scam_text2", "આ એક છેતરપિંડી હોઈ શકે છે. કોઈને પાસવર્ડ આપશો નહીં."), + ("Don't show again", "ફરીથી ના બતાવશો"), + ("I Agree", "હું સહમત છું"), + ("Decline", "અસ્વીકાર"), + ("Timeout in minutes", "મિનિટોમાં ટાઇમઆઉટ"), + ("auto_disconnect_option_tip", "નિષ્ક્રિયતા પર આપમેળે ડિસ્કનેક્ટ કરો"), + ("Connection failed due to inactivity", "નિષ્ક્રિયતાને કારણે કનેક્શન નિષ્ફળ"), + ("Check for software update on startup", "શરૂઆતમાં અપડેટ તપાસો"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "સર્વર પ્રો ને {} માં અપગ્રેડ કરો"), + ("pull_group_failed_tip", "ગ્રુપ ખેંચવામાં (Pull) નિષ્ફળ"), + ("Filter by intersection", "ઇન્ટરસેક્શન દ્વારા ફિલ્ટર કરો"), + ("Remove wallpaper during incoming sessions", "કનેક્શન દરમિયાન વોલપેપર હટાવો"), + ("Test", "ટેસ્ટ"), + ("display_is_plugged_out_msg", "ડિસ્પ્લે કાઢી નાખવામાં આવ્યું છે."), + ("No displays", "કોઈ ડિસ્પ્લે નથી"), + ("Open in new window", "નવી વિન્ડોમાં ખોલો"), + ("Show displays as individual windows", "દરેક ડિસ્પ્લે અલગ વિન્ડોમાં બતાવો"), + ("Use all my displays for the remote session", "તમામ ડિસ્પ્લેનો ઉપયોગ કરો"), + ("selinux_tip", "SELinux ઉપકરણ પર સક્ષમ છે."), + ("Change view", "વ્યુ બદલો"), + ("Big tiles", "મોટી ટાઇલ્સ"), + ("Small tiles", "નાની ટાઇલ્સ"), + ("List", "લિસ્ટ"), + ("Virtual display", "વર્ચ્યુઅલ ડિસ્પ્લે"), + ("Plug out all", "બધું કાઢી નાખો (Plug out)"), + ("True color (4:4:4)", "ટ્રુ કલર (4:4:4)"), + ("Enable blocking user input", "યુઝર ઇનપુટ બ્લોકિંગ સક્ષમ કરો"), + ("id_input_tip", "તમે ID, Alias અથવા IP એડ્રેસ દાખલ કરી શકો છો."), + ("privacy_mode_impl_mag_tip", "મેગ્નિફાયર પ્રાઇવસી મોડ"), + ("privacy_mode_impl_virtual_display_tip", "વર્ચ્યુઅલ ડિસ્પ્લે પ્રાઇવસી મોડ"), + ("Enter privacy mode", "પ્રાઇવસી મોડમાં પ્રવેશ કરો"), + ("Exit privacy mode", "પ્રાઇવસી મોડમાંથી બહાર નીકળો"), + ("idd_not_support_under_win10_2004_tip", "વર્ચ્યુઅલ ડિસ્પ્લે Windows 10 (2004) કે તેથી ઉપર જ સક્ષમ છે."), + ("input_source_1_tip", "ઇનપુટ સ્ત્રોત ૧"), + ("input_source_2_tip", "ઇનપુટ સ્ત્રોત ૨"), + ("Swap control-command key", "Control અને Command કી બદલો"), + ("swap-left-right-mouse", "ડાબું અને જમણું માઉસ બટન બદલો"), + ("2FA code", "2FA કોડ"), + ("More", "વધારે"), + ("enable-2fa-title", "2FA સક્ષમ કરો"), + ("enable-2fa-desc", "તમારું ઓથેન્ટિકેટર એપ સેટ કરો."), + ("wrong-2fa-code", "ખોટો 2FA કોડ."), + ("enter-2fa-title", "2FA કોડ દાખલ કરો"), + ("Email verification code must be 6 characters.", "ઇમેઇલ કોડ 6 અક્ષરનો હોવો જોઈએ."), + ("2FA code must be 6 digits.", "2FA કોડ 6 અંકનો હોવો જોઈએ."), + ("Multiple Windows sessions found", "બહુવિધ Windows સત્રો મળ્યા"), + ("Please select the session you want to connect to", "કૃપા કરીને જે સત્ર સાથે જોડાવું હોય તે પસંદ કરો"), + ("powered_by_me", "મારા દ્વારા સંચાલિત"), + ("outgoing_only_desk_tip", "આ માત્ર આઉટગોઇંગ મોડ છે"), + ("preset_password_warning", "સુરક્ષા માટે પાસવર્ડ બદલો."), + ("Security Alert", "સુરક્ષા ચેતવણી"), + ("My address book", "મારી એડ્રેસ બુક"), + ("Personal", "વ્યક્તિગત"), + ("Owner", "માલિક"), + ("Set shared password", "શેર કરેલ પાસવર્ડ સેટ કરો"), + ("Exist in", "માં અસ્તિત્વ ધરાવે છે"), + ("Read-only", "માત્ર વાંચવા માટે"), + ("Read/Write", "વાંચવા/લખવા માટે"), + ("Full Control", "પૂર્ણ નિયંત્રણ"), + ("share_warning_tip", "તમે તમારો એક્સેસ શેર કરી રહ્યા છો."), + ("Everyone", "દરેક વ્યક્તિ"), + ("ab_web_console_tip", "વેબ કન્સોલ એડ્રેસ બુક"), + ("allow-only-conn-window-open-tip", "માત્ર RustDesk વિન્ડો ખુલ્લી હોય ત્યારે જ કનેક્શનની મંજૂરી આપો"), + ("no_need_privacy_mode_no_physical_displays_tip", "ભૌતિક ડિસ્પ્લે નથી, પ્રાઇવસી મોડની જરૂર નથી."), + ("Follow remote cursor", "રિમોટ કર્સરને અનુસરો"), + ("Follow remote window focus", "રિમોટ વિન્ડો ફોકસને અનુસરો"), + ("default_proxy_tip", "ડિફોલ્ટ પ્રોક્સી સેટિંગ"), + ("no_audio_input_device_tip", "કોઈ ઓડિયો ઇનપુટ મળ્યું નથી."), + ("Incoming", "આવતું"), + ("Outgoing", "જતું"), + ("Clear Wayland screen selection", "Wayland સ્ક્રીન સિલેક્શન સાફ કરો"), + ("clear_Wayland_screen_selection_tip", "સ્ક્રીન સિલેક્શન રીસેટ કરો."), + ("confirm_clear_Wayland_screen_selection_tip", "શું તમે સિલેક્શન સાફ કરવા માંગો છો?"), + ("android_new_voice_call_tip", "નવો વોઇસ કોલ વિનંતી"), + ("texture_render_tip", "ટેક્સચર રેન્ડરિંગ વાપરો"), + ("Use texture rendering", "ટેક્સચર રેન્ડરિંગનો ઉપયોગ કરો"), + ("Floating window", "ફ્લોટિંગ વિન્ડો"), + ("floating_window_tip", "બેકગ્રાઉન્ડમાં હોય ત્યારે RustDesk બતાવો"), + ("Keep screen on", "સ્ક્રીન ચાલુ રાખો"), + ("Never", "ક્યારેય નહીં"), + ("During controlled", "નિયંત્રણ દરમિયાન"), + ("During service is on", "જ્યારે સેવા ચાલુ હોય ત્યારે"), + ("Capture screen using DirectX", "DirectX દ્વારા સ્ક્રીન કેપ્ચર કરો"), + ("Back", "પાછળ"), + ("Apps", "એપ્સ"), + ("Volume up", "અવાજ વધારો"), + ("Volume down", "અવાજ ઘટાડો"), + ("Power", "પાવર"), + ("Telegram bot", "Telegram બોટ"), + ("enable-bot-tip", "સૂચનાઓ માટે બોટ સક્ષમ કરો"), + ("enable-bot-desc", "સૂચનાઓ માટે ટેલિગ્રામ બોટ સેટ કરો."), + ("cancel-2fa-confirm-tip", "શું તમે 2FA રદ કરવા માંગો છો?"), + ("cancel-bot-confirm-tip", "શું તમે બોટ રદ કરવા માંગો છો?"), + ("About RustDesk", "RustDesk વિશે"), + ("Send clipboard keystrokes", "ક્લિપબોર્ડ કી-સ્ટ્રોક્સ મોકલો"), + ("network_error_tip", "નેટવર્ક ભૂલ, ફરી પ્રયાસ કરો."), + ("Unlock with PIN", "PIN થી અનલોક કરો"), + ("Requires at least {} characters", "ઓછામાં ઓછા {} અક્ષર જરૂરી"), + ("Wrong PIN", "ખોટો PIN"), + ("Set PIN", "PIN સેટ કરો"), + ("Enable trusted devices", "વિશ્વાસપાત્ર ઉપકરણો સક્ષમ કરો"), + ("Manage trusted devices", "વિશ્વાસપાત્ર ઉપકરણો સંચાલિત કરો"), + ("Platform", "પ્લેટફોર્મ"), + ("Days remaining", "બાકી દિવસો"), + ("enable-trusted-devices-tip", "માત્ર વિશ્વાસપાત્ર ઉપકરણો જ પાસવર્ડ વગર જોડાઈ શકે"), + ("Parent directory", "પેરન્ટ ડિરેક્ટરી"), + ("Resume", "ફરી શરૂ કરો"), + ("Invalid file name", "અમાન્ય ફાઇલ નામ"), + ("one-way-file-transfer-tip", "માત્ર એકતરફી ફાઇલ ટ્રાન્સફરની મંજૂરી છે"), + ("Authentication Required", "ઓથેન્ટિકેશન જરૂરી"), + ("Authenticate", "ઓથેન્ટિકેટ કરો"), + ("web_id_input_tip", "રિમોટ ID દાખલ કરો"), + ("Download", "ડાઉનલોડ"), + ("Upload folder", "ફોલ્ડર અપલોડ કરો"), + ("Upload files", "ફાઇલો અપલોડ કરો"), + ("Clipboard is synchronized", "ક્લિપબોર્ડ સિંક થયેલ છે"), + ("Update client clipboard", "ક્લાયન્ટ ક્લિપબોર્ડ અપડેટ કરો"), + ("Untagged", "ટેગ વગરનું"), + ("new-version-of-{}-tip", "{} નું નવું વર્ઝન ઉપલબ્ધ છે"), + ("Accessible devices", "એક્સેસિબલ ઉપકરણો"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"), + ("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"), + ("Printer", "પ્રિન્ટર"), + ("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."), + ("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."), + ("printer-{}-not-installed-tip", "પ્રિન્ટર {} ઇન્સ્ટોલ નથી."), + ("printer-{}-ready-tip", "પ્રિન્ટર {} તૈયાર છે."), + ("Install {} Printer", "{} પ્રિન્ટર ઇન્સ્ટોલ કરો"), + ("Outgoing Print Jobs", "જતા પ્રિન્ટ કાર્યો"), + ("Incoming Print Jobs", "આવતા પ્રિન્ટ કાર્યો"), + ("Incoming Print Job", "આવતું પ્રિન્ટ કાર્ય"), + ("use-the-default-printer-tip", "ડિફોલ્ટ પ્રિન્ટર વાપરો"), + ("use-the-selected-printer-tip", "પસંદ કરેલ પ્રિન્ટર વાપરો"), + ("auto-print-tip", "આપમેળે પ્રિન્ટ કરો"), + ("print-incoming-job-confirm-tip", "પ્રિન્ટ કરતા પહેલા પુષ્ટિ કરો"), + ("remote-printing-disallowed-tile-tip", "રિમોટ પ્રિન્ટિંગની મંજૂરી નથી"), + ("remote-printing-disallowed-text-tip", "સેટિંગ્સમાં રિમોટ પ્રિન્ટિંગ સક્ષમ કરો."), + ("save-settings-tip", "સેટિંગ્સ સાચવો"), + ("dont-show-again-tip", "ફરીથી ના બતાવશો"), + ("Take screenshot", "સ્ક્રીનશોટ લો"), + ("Taking screenshot", "સ્ક્રીનશોટ લેવાઈ રહ્યો છે"), + ("screenshot-merged-screen-not-supported-tip", "મર્જ કરેલ સ્ક્રીનશોટ સપોર્ટેડ નથી."), + ("screenshot-action-tip", "સ્ક્રીનશોટ પછીની ક્રિયા"), + ("Save as", "તરીકે સાચવો"), + ("Copy to clipboard", "ક્લિપબોર્ડમાં કોપી કરો"), + ("Enable remote printer", "રિમોટ પ્રિન્ટર સક્ષમ કરો"), + ("Downloading {}", "{} ડાઉનલોડ થઈ રહ્યું છે"), + ("{} Update", "{} અપડેટ"), + ("{}-to-update-tip", "અપડેટ કરવા માટે {}"), + ("download-new-version-failed-tip", "નવું વર્ઝન ડાઉનલોડ કરવામાં નિષ્ફળ."), + ("Auto update", "ઓટો અપડેટ"), + ("update-failed-check-msi-tip", "અપડેટ નિષ્ફળ, MSI ફાઇલ તપાસો."), + ("websocket_tip", "જો પોર્ટ બ્લોક હોય તો WebSocket વાપરો."), + ("Use WebSocket", "WebSocket નો ઉપયોગ કરો"), + ("Trackpad speed", "ટ્રેકપેડ સ્પીડ"), + ("Default trackpad speed", "ડિફોલ્ટ ટ્રેકપેડ સ્પીડ"), + ("Numeric one-time password", "ન્યુમેરિક OTP"), + ("Enable IPv6 P2P connection", "IPv6 P2P કનેક્શન સક્ષમ કરો"), + ("Enable UDP hole punching", "UDP હોલ પંચિંગ સક્ષમ કરો"), + ("View camera", "કેમેરા જુઓ"), + ("Enable camera", "કેમેરા સક્ષમ કરો"), + ("No cameras", "કોઈ કેમેરા મળ્યો નથી"), + ("view_camera_unsupported_tip", "રિમોટ કેમેરા સપોર્ટેડ નથી."), + ("Terminal", "ટર્મિનલ"), + ("Enable terminal", "ટર્મિનલ સક્ષમ કરો"), + ("New tab", "નવી ટેબ"), + ("Keep terminal sessions on disconnect", "ડિસ્કનેક્ટ વખતે ટર્મિનલ ચાલુ રાખો"), + ("Terminal (Run as administrator)", "ટર્મિનલ (એડમિનિસ્ટ્રેટર તરીકે)"), + ("terminal-admin-login-tip", "એડમિન લોગિન જરૂરી છે."), + ("Failed to get user token.", "યુઝર ટોકન મેળવવામાં નિષ્ફળ."), + ("Incorrect username or password.", "ખોટું યુઝરનેમ કે પાસવર્ડ."), + ("The user is not an administrator.", "યુઝર એડમિનિસ્ટ્રેટર નથી."), + ("Failed to check if the user is an administrator.", "યુઝર એડમિન છે કે નહીં તે ચકાસવામાં નિષ્ફળ."), + ("Supported only in the installed version.", "માત્ર ઇન્સ્ટોલ કરેલ વર્ઝનમાં ઉપલબ્ધ."), + ("elevation_username_tip", "એડમિનિસ્ટ્રેટર નામ દાખલ કરો"), + ("Preparing for installation ...", "ઇન્સ્ટોલેશનની તૈયારી..."), + ("Show my cursor", "મારું કર્સર બતાવો"), + ("Scale custom", "કસ્ટમ સ્કેલ"), + ("Custom scale slider", "કસ્ટમ સ્કેલ સ્લાઇડર"), + ("Decrease", "ઘટાડો"), + ("Increase", "વધારો"), + ("Show virtual mouse", "વર્ચ્યુઅલ માઉસ બતાવો"), + ("Virtual mouse size", "વર્ચ્યુઅલ માઉસ કદ"), + ("Small", "નાનું"), + ("Large", "મોટું"), + ("Show virtual joystick", "વર્ચ્યુઅલ જોયસ્ટિક બતાવો"), + ("Edit note", "નોંધ સુધારો"), + ("Alias", "Alias (ઉપનામ)"), + ("ScrollEdge", "સ્ક્રોલ એજ"), + ("Allow insecure TLS fallback", "અસુરક્ષિત TLS ફોલબેકની મંજૂરી આપો"), + ("allow-insecure-tls-fallback-tip", "જૂના સર્વર માટે વાપરો."), + ("Disable UDP", "UDP અક્ષમ કરો"), + ("disable-udp-tip", "કનેક્શન સમસ્યાઓ માટે UDP બંધ કરો."), + ("server-oss-not-support-tip", "OSS સર્વર આને સપોર્ટ કરતું નથી."), + ("input note here", "અહીં નોંધ લખો"), + ("note-at-conn-end-tip", "કનેક્શનના અંતે નોંધ બતાવો"), + ("Show terminal extra keys", "ટર્મિનલની વધારાની કી બતાવો"), + ("Relative mouse mode", "રીલેટિવ માઉસ મોડ"), + ("rel-mouse-not-supported-peer-tip", "સામેથી સપોર્ટેડ નથી."), + ("rel-mouse-not-ready-tip", "તૈયાર નથી."), + ("rel-mouse-lock-failed-tip", "માઉસ લોક નિષ્ફળ."), + ("rel-mouse-exit-{}-tip", "બહાર નીકળવા {} દબાવો"), + ("rel-mouse-permission-lost-tip", "પરવાનગી ગુમાવી દીધી."), + ("Changelog", "Changelog (ફેરફારો)"), + ("keep-awake-during-outgoing-sessions-label", "આઉટગોઇંગ સત્ર વખતે જાગૃત રાખો"), + ("keep-awake-during-incoming-sessions-label", "ઇનકમિંગ સત્ર વખતે જાગૃત રાખો"), + ("Continue with {}", "{} સાથે આગળ વધો"), + ("Display Name", "ડિસ્પ્લે નામ"), + ("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."), + ("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/he.rs b/vendor/rustdesk/src/lang/he.rs new file mode 100644 index 0000000..682ee0c --- /dev/null +++ b/vendor/rustdesk/src/lang/he.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "מצב"), + ("Your Desktop", "שולחן העבודה שלך"), + ("desk_tip", "ניתן לגשת לשולחן העבודה שלך עם מזהה וסיסמה זו."), + ("Password", "סיסמה"), + ("Ready", "מוכן"), + ("Established", "מחובר"), + ("connecting_status", "מתחבר לרשת RustDesk..."), + ("Enable service", "הפעל שירות"), + ("Start service", "התחל שירות"), + ("Service is running", "השירות פעיל"), + ("Service is not running", "השירות איננו רץ"), + ("not_ready_status", "לא מוכן. בדוק את החיבור שלך"), + ("Control Remote Desktop", "שלוט בשולחן עבודה מרוחק"), + ("Transfer file", "העבר קובץ"), + ("Connect", "התחבר"), + ("Recent sessions", "הפעלות אחרונות"), + ("Address book", "ספר כתובות"), + ("Confirmation", "אישור"), + ("TCP tunneling", "TCP tunneling"), + ("Remove", "הסר"), + ("Refresh random password", "רענן סיסמה אקראית"), + ("Set your own password", "הגדר סיסמה משלך"), + ("Enable keyboard/mouse", "אפשר מקלדת/עכבר"), + ("Enable clipboard", "אפשר לוח גזירים"), + ("Enable file transfer", "אפשר העברת קבצים"), + ("Enable TCP tunneling", "אפשר TCP tunneling"), + ("IP Whitelisting", "רשימת IP מורשים"), + ("ID/Relay Server", "שרת ID/Relay"), + ("Import server config", "ייבוא הגדרות שרת"), + ("Export Server Config", "ייצוא הגדרות שרת"), + ("Import server configuration successfully", "ייבוא הגדרות שרת הושלם בהצלחה"), + ("Export server configuration successfully", "ייצוא הגדרות שרת הושלם בהצלחה"), + ("Invalid server configuration", "הגדרות שרת לא תקינות"), + ("Clipboard is empty", "לוח הגזירים ריק"), + ("Stop service", "עצור שירות"), + ("Change ID", "שנה מזהה"), + ("Your new ID", "המזהה החדש שלך"), + ("length %min% to %max%", "אורך בין %min% ל %max%"), + ("starts with a letter", "מתחיל באות"), + ("allowed characters", "תווים מותרים"), + ("id_change_tip", "מותרים רק תווים a-z, A-Z, 0-9, מקף (-) וקו תחתון (_). התו הראשון חייב להיות אות (a-z, A-Z). אורך בין 6 ל-16 תווים."), + ("Website", "דף הבית"), + ("About", "אודות"), + ("Slogan_tip", "נוצר באהבה בעולם הכאוטי הזה!"), + ("Privacy Statement", "הצהרת פרטיות"), + ("Mute", "השתק"), + ("Build Date", "תאריך בנייה"), + ("Version", "גרסה"), + ("Home", "בית"), + ("Audio Input", "קלט שמע"), + ("Enhancements", "שיפורים"), + ("Hardware Codec", "Hardware Codec"), + ("Adaptive bitrate", "Adaptive bitrate"), + ("ID Server", "שרת ID"), + ("Relay Server", "שרת Relay"), + ("API Server", "שרת API"), + ("invalid_http", "חייב להתחיל עם http:// או https://"), + ("Invalid IP", "IP לא תקין"), + ("Invalid format", "פורמט לא תקין"), + ("server_not_support", "לא נתמך על-ידי השרת כרגע"), + ("Not available", "לא זמין"), + ("Too frequent", "תדיר מדי"), + ("Cancel", "ביטול"), + ("Skip", "דלג"), + ("Close", "סגור"), + ("Retry", "נסה שוב"), + ("OK", "אישור"), + ("Password Required", "נדרשת סיסמה"), + ("Please enter your password", "אנא הכנס סיסמה"), + ("Remember password", "זכור סיסמה"), + ("Wrong Password", "סיסמה שגויה"), + ("Do you want to enter again?", "האם אתה רוצה לנסות שוב?"), + ("Connection Error", "שגיאת חיבור"), + ("Error", "שגיאה"), + ("Reset by the peer", "אופס על-ידי הצד השני"), + ("Connecting...", "מתחבר..."), + ("Connection in progress. Please wait.", "מתחבר. אנא המתן."), + ("Please try 1 minute later", "אנא המתן דקה ונסה שוב"), + ("Login Error", "שגיאת התחברות"), + ("Successful", "הצלחה"), + ("Connected, waiting for image...", "מחובר, מחכה לתמונה..."), + ("Name", "שם"), + ("Type", "סוג"), + ("Modified", "שונה"), + ("Size", "גודל"), + ("Show Hidden Files", "הצג קבצים מוסתרים"), + ("Receive", "קבל"), + ("Send", "שלח"), + ("Refresh File", "רענן קובץ"), + ("Local", "מקומי"), + ("Remote", "מרוחק"), + ("Remote Computer", "מחשב מרוחק"), + ("Local Computer", "מחשב מקומי"), + ("Confirm Delete", "אשר מחיקה"), + ("Delete", "מחק"), + ("Properties", "מאפיינים"), + ("Multi Select", "בחירה מרובה"), + ("Select All", "בחר הכל"), + ("Unselect All", "בטל בחירת הכל"), + ("Empty Directory", "תיקייה ריקה"), + ("Not an empty directory", "תיקייה אינה ריקה"), + ("Are you sure you want to delete this file?", "האם אתה בטוח שברצונך למחוק קובץ זה?"), + ("Are you sure you want to delete this empty directory?", "האם אתה בטוח שברצונך למחוק תיקייה ריקה זו?"), + ("Are you sure you want to delete the file of this directory?", "האם אתה בטוח שברצונך למחוק את הקובץ בתקייה זו?"), + ("Do this for all conflicts", "בצע זאת עבור כל ההתנגשויות"), + ("This is irreversible!", "בלתי הפיך"), + ("Deleting", "מוחק"), + ("files", "קבצים"), + ("Waiting", "מחכה"), + ("Finished", "הסתיים"), + ("Speed", "מהירות"), + ("Custom Image Quality", "איכות תמונה מותאמת אישית"), + ("Privacy mode", "מצב פרטיות"), + ("Block user input", "חסום קלט משתמש"), + ("Unblock user input", "אפשר קלט משתמש"), + ("Adjust Window", "התאם חלון"), + ("Original", "מקורי"), + ("Shrink", "הקטן"), + ("Stretch", "מתח"), + ("Scrollbar", "פס גלילה"), + ("ScrollAuto", "גלילה אוטומטית"), + ("Good image quality", "איכות תמונה טובה"), + ("Balanced", "מאוזן"), + ("Optimize reaction time", "מיטוב זמן תגובה"), + ("Custom", "מותאם אישית"), + ("Show remote cursor", "הצג סמן מרוחק"), + ("Show quality monitor", "הצג מד איכות"), + ("Disable clipboard", "השבת את לוח הגזירים"), + ("Lock after session end", "נעל לאחר סיום ההפעלה"), + ("Insert Ctrl + Alt + Del", "לחץ Ctrl + Alt + Delete"), + ("Insert Lock", "הוסף נעילה"), + ("Refresh", "רענן"), + ("ID does not exist", "מזהה אינו קיים"), + ("Failed to connect to rendezvous server", "החיבור לשרת התיאום נכשל"), + ("Please try later", "אנא נסה שוב מאוחר יותר"), + ("Remote desktop is offline", "שולחן העבודה המרוחק אינו מקוון"), + ("Key mismatch", "אי-התאמה במפתח"), + ("Timeout", "תם הזמן"), + ("Failed to connect to relay server", "החיבור לשרת הממסר נכשל"), + ("Failed to connect via rendezvous server", "החיבור דרך שרת התיאום נכשל"), + ("Failed to connect via relay server", "החיבור דרך שרת הממסר נכשל"), + ("Failed to make direct connection to remote desktop", "החיבור למחשב המרוחק נכשל"), + ("Set Password", "הגדר סיסמה"), + ("OS Password", "סיסמת מערכת הפעלה"), + ("install_tip", "בגלל UAC, RustDesk לא יכול לפעול כראוי כצד מרוחק בחלק מהמקרים. כדי להימנע מ-UAC, אנא לחץ על הכפתור למטה כדי להתקין את RustDesk במערכת."), + ("Click to upgrade", "לחץ כדי לשדרג"), + ("Configure", "הגדר"), + ("config_acc", "כדי לשלוט מרחוק בשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"נגישות\"."), + ("config_screen", "כדי לגשת מרחוק לשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"הקלטת מסך\"."), + ("Installing ...", "מתקין ..."), + ("Install", "התקן"), + ("Installation", "התקנה"), + ("Installation Path", "נתיב התקנה"), + ("Create start menu shortcuts", "צור קיצור-דרך לתפריט ההתחלה"), + ("Create desktop icon", "צור סמל בשולחן העבודה"), + ("agreement_tip", "על ידי התחלת ההתקנה, אתה מקבל את הסכם הרישיון."), + ("Accept and Install", "קבל והתקן"), + ("End-user license agreement", "הסכם רישיון משתמש קצה"), + ("Generating ...", "יוצר ..."), + ("Your installation is lower version.", "מותקנת אצלך בגרסה ישנה יותר"), + ("not_close_tcp_tip", "אל תסגור חלון זה בזמן שאתה משתמש בtcp"), + ("Listening ...", "מאזין ..."), + ("Remote Host", "מארח מרוחק"), + ("Remote Port", "פורט מרוחק"), + ("Action", "פעולה"), + ("Add", "הוסף"), + ("Local Port", "פורט מקומי"), + ("Local Address", "כתובת מקומית"), + ("Change Local Port", "שנה פורט מקומי"), + ("setup_server_tip", "לחיבור מהיר יותר, מומלץ להגדיר שרת משלך"), + ("Too short, at least 6 characters.", "קצר מידי, לפחות 6 תווים."), + ("The confirmation is not identical.", "האימות אינו זהה."), + ("Permissions", "הרשאות"), + ("Accept", "קבל"), + ("Dismiss", "התעלם"), + ("Disconnect", "נתק"), + ("Enable file copy and paste", "אפשר העתקה והדבקה עבור קבצים"), + ("Connected", "מחובר"), + ("Direct and encrypted connection", "חיבור ישיר ומוצפן"), + ("Relayed and encrypted connection", "חיבור באמצעות ממסר ומוצפן"), + ("Direct and unencrypted connection", "חיבור ישיר ולא מוצפן"), + ("Relayed and unencrypted connection", "חיבור באמצעות ממסר ולא מוצפן"), + ("Enter Remote ID", "הזן מזהה מרוחק"), + ("Enter your password", "הכנס סיסמה"), + ("Logging in...", "מתחבר..."), + ("Enable RDP session sharing", "אפשר שיתוף סשן RDP"), + ("Auto Login", "התחברות אוטומטית (תקפה רק אם הגדרת \"נעל לאחר סיום הסשן\")"), + ("Enable direct IP access", "אפשר גישה ישירה לפי כתובת IP"), + ("Rename", "שנה שם"), + ("Space", "רווח"), + ("Create desktop shortcut", "צור קיצור דרך בשולחן העבודה"), + ("Change Path", "שנה נתיב"), + ("Create Folder", "צור תיקייה"), + ("Please enter the folder name", "אנא הכנס שם תיקייה"), + ("Fix it", "תקן את זה"), + ("Warning", "אזהרה"), + ("Login screen using Wayland is not supported", "מסך התחברות המשתמש ב-Wayland אינו נתמך"), + ("Reboot required", "נדרש אתחול מחדש"), + ("Unsupported display server", "שרת תצוגה לא נתמך"), + ("x11 expected", "נדרש X11"), + ("Port", "יציאה"), + ("Settings", "הגדרות"), + ("Username", "שם משתמש"), + ("Invalid port", "פורט לא חוקי"), + ("Closed manually by the peer", "נסגר ידנית על ידי הצד השני"), + ("Enable remote configuration modification", "אפשר שינוי הגדרות מרחוק"), + ("Run without install", "הרץ ללא התקנה"), + ("Connect via relay", "התחבר באמצעות ממסר"), + ("Always connect via relay", "התחבר תמיד דרך ממסר"), + ("whitelist_tip", "רק כתובות IP מהרשימה הלבנה יכולות לגשת אלי"), + ("Login", "התחברות"), + ("Verify", "אמת"), + ("Remember me", "זכור אותי"), + ("Trust this device", "סמוך על מכשיר זה"), + ("Verification code", "קוד אימות"), + ("verification_tip", "קוד אימות נשלח לכתובת האימייל הרשומה, הזן את קוד האימות כדי להמשיך בהתחברות."), + ("Logout", "התנתק"), + ("Tags", "תגים"), + ("Search ID", "חפש מזהה"), + ("whitelist_sep", "מופרד על ידי פסיק, נקודה פסיק, רווחים או שורה חדשה"), + ("Add ID", "הוסף מזהה"), + ("Add Tag", "הוסף תג"), + ("Unselect all tags", "בטל בחירת כל התגים"), + ("Network error", "שגיאת רשת"), + ("Username missed", "חסר שם משתמש"), + ("Password missed", "חסרה סיסמה"), + ("Wrong credentials", "פרטי התחברות שגויים"), + ("The verification code is incorrect or has expired", "קוד האימות שגוי או שפג תוקפו"), + ("Edit Tag", "ערוך תג"), + ("Forget Password", "שכחת סיסמה"), + ("Favorites", "מועדפים"), + ("Add to Favorites", "הוסף למועדפים"), + ("Remove from Favorites", "הסר מהמועדפים"), + ("Empty", "ריק"), + ("Invalid folder name", "שם תיקייה אינו תקין"), + ("Socks5 Proxy", "פרוקסי Socks5"), + ("Socks5/Http(s) Proxy", "פרוקסי Socks5/Http(s)"), + ("Discovered", "נמצא"), + ("install_daemon_tip", "לצורך הפעלה בעת הפעלת המחשב, עליך להתקין שירות מערכת."), + ("Remote ID", "מזהה מרוחק"), + ("Paste", "הדבק"), + ("Paste here?", "להדביק כאן?"), + ("Are you sure to close the connection?", "האם אתה בטוח שברצונך לסגור את החיבור?"), + ("Download new version", "הורד גרסה חדשה"), + ("Touch mode", "מצב מגע"), + ("Mouse mode", "מצב עכבר"), + ("One-Finger Tap", "הקשה באצבע אחת"), + ("Left Mouse", "עכבר שמאלי"), + ("One-Long Tap", "הקשה ארוכה באצבע אחת"), + ("Two-Finger Tap", "הקשה בשתי אצבעות"), + ("Right Mouse", "עכבר ימני"), + ("One-Finger Move", "הזזה באצבע אחת"), + ("Double Tap & Move", "הקשה כפולה והזזה"), + ("Mouse Drag", "גרירת עכבר"), + ("Three-Finger vertically", "תנועה אנכית בשלוש אצבעות"), + ("Mouse Wheel", "גלגלת עכבר"), + ("Two-Finger Move", "הזזה בשתי אצבעות"), + ("Canvas Move", "הזזת בד"), + ("Pinch to Zoom", "צביטה לזום"), + ("Canvas Zoom", "זום בד"), + ("Reset canvas", "אפס לוח ציור"), + ("No permission of file transfer", "אין הרשאת העברת קבצים"), + ("Note", "הערה"), + ("Connection", "התחברות"), + ("Share screen", "שיתוף מסך"), + ("Chat", "צ'אט"), + ("Total", "הכל"), + ("items", "פריטים"), + ("Selected", "נבחר"), + ("Screen Capture", "לכידת מסך"), + ("Input Control", "בקרת קלט"), + ("Audio Capture", "לכידת שמע"), + ("Do you accept?", "האם אתה מקבל?"), + ("Open System Setting", "פתח הגדרות מערכת"), + ("How to get Android input permission?", "כיצד לקבל הרשאת קלט באנדרואיד?"), + ("android_input_permission_tip1", "כדי שמכשיר מרוחק יוכל לשלוט במכשיר האנדרואיד שלך באמצעות עכבר או מגע, עליך לאפשר ל-RustDesk להשתמש בשירות \"נגישות\"."), + ("android_input_permission_tip2", "אנא עבור לדף הגדרות המערכת הבא, מצא והכנס ל[שירותים מותקנים], הפעל את שירות [RustDesk Input]."), + ("android_new_connection_tip", "התקבלה בקשת שליטה חדשה, המבקשת לשלוט במכשירך הנוכחי."), + ("android_service_will_start_tip", "הפעלת \"לכידת מסך\" תפעיל את השירות באופן אוטומטי ותאפשר למכשירים אחרים לבקש חיבור למכשירך."), + ("android_stop_service_tip", "סגירת השירות תנתק באופן אוטומטי את כל החיבורים הקיימים."), + ("android_version_audio_tip", "גרסת האנדרואיד הנוכחית אינה תומכת בלכידת שמע. אנא שדרג לאנדרואיד 10 ומעלה."), + ("android_start_service_tip", "הקש על [התחל שירות] או אפשר הרשאת [לכידת מסך] כדי להתחיל את שירות שיתוף המסך."), + ("android_permission_may_not_change_tip", "הרשאות עבור חיבורים קיימים עשויות לא להשתנות באופן מיידי עד להתחברות מחדש."), + ("Account", "חשבון"), + ("Overwrite", "דרוס"), + ("This file exists, skip or overwrite this file?", "הקובץ כבר קיים, לדלג או לדרוס אותו?"), + ("Quit", "צא"), + ("Help", "עזרה"), + ("Failed", "נכשל"), + ("Succeeded", "הצליח"), + ("Someone turns on privacy mode, exit", "מישהו הפעיל מצב פרטיות, מתבצעת יציאה"), + ("Unsupported", "לא נתמך"), + ("Peer denied", "הצד השני סירב"), + ("Please install plugins", "אנא התקן תוספים"), + ("Peer exit", "הצד השני התנתק"), + ("Failed to turn off", "הכיבוי נכשל"), + ("Turned off", "מכובה"), + ("Language", "שפה"), + ("Keep RustDesk background service", "השאר את שירות הרקע של RustDesk פעיל"), + ("Ignore Battery Optimizations", "התעלם מאופטימיזציות סוללה"), + ("android_open_battery_optimizations_tip", "אם ברצונך לבטל תכונה זו, אנא עבור לדף ההגדרות של יישום RustDesk , מצא והיכנס ל[סוללה], ובטל את הסימון מ-[לא מוגבל]"), + ("Start on boot", "התחל בהפעלה"), + ("Start the screen sharing service on boot, requires special permissions", "הפעל את שירות שיתוף המסך בעת אתחול המכשיר (דורש הרשאות מיוחדות)"), + ("Connection not allowed", "חיבור לא מורשה"), + ("Legacy mode", "מצב ישן"), + ("Map mode", "מצב מיפוי מקשים"), + ("Translate mode", "מצב תרגום"), + ("Use permanent password", "השתמש בסיסמה קבועה"), + ("Use both passwords", "השתמש בשתי הסיסמאות"), + ("Set permanent password", "הגדר סיסמה קבועה"), + ("Enable remote restart", "אפשר אתחול מרחוק"), + ("Restart remote device", "אתחל את המכשיר המרוחק"), + ("Are you sure you want to restart", "האם אתה בטוח שברצונך לאתחל"), + ("Restarting remote device", "מאתחל את המכשיר המרוחק"), + ("remote_restarting_tip", "המכשיר המרוחק מאתחל את עצמו, אנא סגור את תיבת ההודעה הזו והתחבר מחדש עם סיסמה קבועה בעוד זמן מה"), + ("Copied", "הועתק"), + ("Exit Fullscreen", "יציאה ממסך מלא"), + ("Fullscreen", "מסך מלא"), + ("Mobile Actions", "פעולות ניידות"), + ("Select Monitor", "בחר מסך"), + ("Control Actions", "פעולות בקרה"), + ("Display Settings", "הגדרות תצוגה"), + ("Ratio", "יחס"), + ("Image Quality", "איכות תמונה"), + ("Scroll Style", "סגנון גלילה"), + ("Show Toolbar", "הצג סרגל כלים"), + ("Hide Toolbar", "הסתר סרגל כלים"), + ("Direct Connection", "חיבור ישיר"), + ("Relay Connection", "חיבור באמצעות ממסר"), + ("Secure Connection", "חיבור מאובטח"), + ("Insecure Connection", "חיבור לא מאובטח"), + ("Scale original", "קנה מידה מקורי"), + ("Scale adaptive", "קנה מידה מותאם"), + ("General", "כללי"), + ("Security", "אבטחה"), + ("Theme", "ערכת נושא"), + ("Dark Theme", "ערכת נושא כהה"), + ("Light Theme", "ערכת נושא בהירה"), + ("Dark", "כהה"), + ("Light", "בהיר"), + ("Follow System", "זהה למערכת"), + ("Enable hardware codec", "אפשר מקודד חומרה"), + ("Unlock Security Settings", "פתח הגדרות אבטחה"), + ("Enable audio", "הפעל שמע"), + ("Unlock Network Settings", "פתח הגדרות רשת"), + ("Server", "שרת"), + ("Direct IP Access", "גישה ישירה ל-IP"), + ("Proxy", "פרוקסי"), + ("Apply", "החל"), + ("Disconnect all devices?", "נתק את כל המכשירים?"), + ("Clear", "נקה"), + ("Audio Input Device", "מכשיר קלט שמע"), + ("Use IP Whitelisting", "השתמש ברשימת לבנה של IP"), + ("Network", "רשת"), + ("Pin Toolbar", "נעץ סרגל כלים"), + ("Unpin Toolbar", "הסר נעיצת סרגל כלים"), + ("Recording", "הקלטה"), + ("Directory", "תיקיה"), + ("Automatically record incoming sessions", "הקלט הפעלות נכנסות באופן אוטומטי"), + ("Automatically record outgoing sessions", "הקלט הפעלות יוצאות באופן אוטומטי"), + ("Change", "שנה"), + ("Start session recording", "התחל הקלטת הפעלה"), + ("Stop session recording", "הפסק הקלטת הפעלה"), + ("Enable recording session", "אפשר הקלטת הפעלה"), + ("Enable LAN discovery", "אפשר זיהוי ברשת מקומית"), + ("Deny LAN discovery", "חסום זיהוי ברשת מקומית"), + ("Write a message", "כתוב הודעה"), + ("Prompt", "הנחיה"), + ("Please wait for confirmation of UAC...", "אנא המתן לאישור בקרת חשבון משתמש (UAC)..."), + ("elevated_foreground_window_tip", "החלון הנוכחי של שולחן העבודה המרוחק דורש הרשאה גבוהה יותר לפעולה, לכן אי אפשר להשתמש בעכבר ובמקלדת באופן זמני. תוכל לבקש מהמשתמש המרוחק למזער את החלון הנוכחי, או ללחוץ על כפתור העלאת הרשאות בחלון ניהול החיבור. כדי להימנע מבעיה זו, מומלץ להתקין את התוכנה במכשיר המרוחק."), + ("Disconnected", "מנותק"), + ("Other", "אחר"), + ("Confirm before closing multiple tabs", "אשר לפני סגירת מספר לשוניות"), + ("Keyboard Settings", "הגדרות מקלדת"), + ("Full Access", "גישה מלאה"), + ("Screen Share", "שיתוף מסך"), + ("ubuntu-21-04-required", "Wayland דורש Ubuntu 21.04 או גרסה גבוהה יותר"), + ("wayland-requires-higher-linux-version", "Wayland דורש גרסת הפצת לינוקס גבוהה יותר. אנא נסה שולחן עבודה מסוג X11 או החלף מערכת הפעלה"), + ("xdp-portal-unavailable", ""), + ("JumpLink", "קישור מהיר"), + ("Please Select the screen to be shared(Operate on the peer side).", "אנא בחר את המסך לשיתוף (פעולה בצד העמית)."), + ("Show RustDesk", "הצג את RustDesk"), + ("This PC", "מחשב זה"), + ("or", "או"), + ("Elevate", "הפעל הרשאות מורחבות"), + ("Zoom cursor", "הגדל סמן"), + ("Accept sessions via password", "קבל הפעלות באמצעות סיסמה"), + ("Accept sessions via click", "קבל הפעלות באמצעות לחיצה"), + ("Accept sessions via both", "קבל הפעלות באמצעות סיסמה או לחיצה"), + ("Please wait for the remote side to accept your session request...", "אנא המתן שהצד המרוחק יאשר את בקשת ההפעלה שלך..."), + ("One-time Password", "סיסמה חד-פעמית"), + ("Use one-time password", "השתמש בסיסמה חד-פעמית"), + ("One-time password length", "אורך סיסמה חד-פעמית"), + ("Request access to your device", "בקשת גישה למכשיר שלך"), + ("Hide connection management window", "הסתר חלון ניהול חיבורים"), + ("hide_cm_tip", "אפשר הסתרה רק אם מקבלים הפעלות דרך סיסמה ומשתמשים בסיסמה קבועה"), + ("wayland_experiment_tip", "תמיכה ב-Wayland נמצאת בשלב ניסיוני, אנא השתמש ב-X11 אם אתה זקוק לגישה ללא ליווי מהצד המרוחק"), + ("Right click to select tabs", "לחץ לחיצה ימנית כדי לבחור לשוניות"), + ("Skipped", "דולג"), + ("Add to address book", "הוסף לספר הכתובות"), + ("Group", "קבוצה"), + ("Search", "חפש"), + ("Closed manually by web console", "נסגר ידנית דרך מסוף האינטרנט"), + ("Local keyboard type", "סוג מקלדת מקומי"), + ("Select local keyboard type", "בחר סוג מקלדת מקומי"), + ("software_render_tip", "אם אתה משתמש בכרטיס גרפיקה של Nvidia תחת Linux וחלון המרוחק נסגר מיד לאחר החיבור, החלפה למנהל ההתקן הפתוח Nouveau ובחירה בשימוש בעיבוד תוכנה עשויה לעזור. נדרשת הפעלה מחדש של התוכנה."), + ("Always use software rendering", "השתמש תמיד בעיבוד תוכנה"), + ("config_input", "כדי לשלוט בשולחן העבודה המרוחק באמצעות מקלדת, עליך להעניק ל-RustDesk הרשאות \"מעקב קלט\"."), + ("config_microphone", "כדי לדבר מרחוק, עליך להעניק ל-RustDesk הרשאות \"הקלטת שמע\"."), + ("request_elevation_tip", "אם יש מישהו בצד המרוחק, ניתן לבקש העלאת הרשאות"), + ("Wait", "המתן"), + ("Elevation Error", "שגיאת העלאת הרשאות"), + ("Ask the remote user for authentication", "בקש מהמשתמש המרוחק אימות"), + ("Choose this if the remote account is administrator", "בחר זאת אם החשבון המרוחק הוא מנהל מערכת"), + ("Transmit the username and password of administrator", "שלח את שם המשתמש והסיסמה של מנהל המערכת"), + ("still_click_uac_tip", "עדיין נדרש מהמשתמש המרוחק לאשר את חלון ה-UAC של RustDesk"), + ("Request Elevation", "בקש העלאת הרשאות"), + ("wait_accept_uac_tip", "אנא המתן שהמשתמש המרוחק יאשר את חלון ה-UAC"), + ("Elevate successfully", "ההרשאות הורחבו בהצלחה"), + ("uppercase", "אותיות גדולות"), + ("lowercase", "אותיות קטנות"), + ("digit", "ספרה"), + ("special character", "תו מיוחד"), + ("length>=8", "לפחות באורך 8"), + ("Weak", "חלש"), + ("Medium", "בינוני"), + ("Strong", "חזק"), + ("Switch Sides", "החלף צדדים"), + ("Please confirm if you want to share your desktop?", "האם לשתף את שולחן העבודה שלך?"), + ("Display", "תצוגה"), + ("Default View Style", "סגנון תצוגה ברירת מחדל"), + ("Default Scroll Style", "סגנון גלילה ברירת מחדל"), + ("Default Image Quality", "איכות תמונה ברירת מחדל"), + ("Default Codec", "קודק ברירת מחדל"), + ("Bitrate", "קצב סיביות"), + ("FPS", "FPS"), + ("Auto", "אוטומטי"), + ("Other Default Options", "אפשרויות ברירת מחדל אחרות"), + ("Voice call", "שיחה קולית"), + ("Text chat", "שיחת טקסט"), + ("Stop voice call", "הפסק שיחה קולית"), + ("relay_hint_tip", "ייתכן שלא ניתן להתחבר ישירות. נסה להתחבר דרך ממסר. כדי להשתמש בממסר כבר מהניסיון הראשון, הוסף את הסיומת /r למזהה או בחר \"התחבר תמיד דרך ממסר\" בכרטיס ההפעלות האחרונות, אם קיים."), + ("Reconnect", "התחברות מחדש"), + ("Codec", "קודק"), + ("Resolution", "רזולוציה"), + ("No transfers in progress", "אין העברות בתהליך"), + ("Set one-time password length", "הגדר אורך סיסמה חד-פעמית"), + ("RDP Settings", "הגדרות RDP"), + ("Sort by", "מיין לפי"), + ("New Connection", "חיבור חדש"), + ("Restore", "שחזור"), + ("Minimize", "מזער"), + ("Maximize", "הגדל"), + ("Your Device", "המכשיר שלך"), + ("empty_recent_tip", "אופס, אין הפעלות אחרונות!\nהגיע הזמן להתחבר למישהו חדש."), + ("empty_favorite_tip", "עדיין אין עמיתים מועדפים?\nבא נמצא מישהו להתחבר אליו ונוסיף אותו למועדפים!"), + ("empty_lan_tip", "אוי לא, נראה שעדיין לא גילינו עמיתים."), + ("empty_address_book_tip", "אבוי, נראה שכרגע אין עמיתים בספר הכתובות שלך."), + ("Empty Username", "שם משתמש ריק"), + ("Empty Password", "סיסמה ריקה"), + ("Me", "אני"), + ("identical_file_tip", "קובץ זה זהה לקובץ שבצד העמית."), + ("show_monitors_tip", "הצג מסכים בסרגל כלים"), + ("View Mode", "מצב תצוגה"), + ("login_linux_tip", "עליך להתחבר לחשבון Linux מרוחק כדי לאפשר פעילות שולחן עבודה X"), + ("verify_rustdesk_password_tip", "אמת סיסמת RustDesk"), + ("remember_account_tip", "זכור חשבון זה"), + ("os_account_desk_tip", "חשבון זה משמש להתחברות למערכת ההפעלה המרוחקת ולהפעלת שולחן עבודה במצב לא מקוון"), + ("OS Account", "חשבון מערכת הפעלה"), + ("another_user_login_title_tip", "משתמש אחר כבר התחבר"), + ("another_user_login_text_tip", "נתק"), + ("xorg_not_found_title_tip", "Xorg לא נמצא"), + ("xorg_not_found_text_tip", "אנא התקן Xorg"), + ("no_desktop_title_tip", "אין שולחן עבודה זמין"), + ("no_desktop_text_tip", "אנא התקן שולחן עבודה GNOME"), + ("No need to elevate", "אין צורך בהעלאת הרשאות"), + ("System Sound", "צליל מערכת"), + ("Default", "ברירת מחדל"), + ("New RDP", "RDP חדש"), + ("Fingerprint", "טביעת אצבע"), + ("Copy Fingerprint", "העתק טביעת אצבע"), + ("no fingerprints", "אין טביעות אצבע"), + ("Select a peer", "בחר עמית"), + ("Select peers", "בחר עמיתים"), + ("Plugins", "תוספים"), + ("Uninstall", "הסר"), + ("Update", "עדכן"), + ("Enable", "פועל"), + ("Disable", "כבוי"), + ("Options", "אפשרויות"), + ("resolution_original_tip", "רזולוציה מקורית"), + ("resolution_fit_local_tip", "התאם לרזולוציה מקומית"), + ("resolution_custom_tip", "רזולוציה מותאמת אישית"), + ("Collapse toolbar", "מזער סרגל כלים"), + ("Accept and Elevate", "אשר והפעל הרשאות מורחבות"), + ("accept_and_elevate_btn_tooltip", "קבל את החיבור והפעל הרשאות מורחבות (UAC)"), + ("clipboard_wait_response_timeout_tip", "המתנה לתגובת העתקה הסתיימה בזמן."), + ("Incoming connection", "חיבור נכנס"), + ("Outgoing connection", "חיבור יוצא"), + ("Exit", "צא"), + ("Open", "פתח"), + ("logout_tip", "האם אתה בטוח שברצונך להתנתק?"), + ("Service", "שירות"), + ("Start", "התחל"), + ("Stop", "עצור"), + ("exceed_max_devices", "הגעת למספר המקסימלי של מכשירים שניתן לנהל."), + ("Sync with recent sessions", "סנכרן עם הפעלות אחרונות"), + ("Sort tags", "מיין תגים"), + ("Open connection in new tab", "פתח חיבור בלשונית חדשה"), + ("Move tab to new window", "העבר לשונית לחלון חדש"), + ("Can not be empty", "לא יכול להיות ריק"), + ("Already exists", "כבר קיים"), + ("Change Password", "שנה סיסמה"), + ("Refresh Password", "רענן סיסמה"), + ("ID", "מזהה"), + ("Grid View", "תצוגת רשת"), + ("List View", "תצוגת רשימה"), + ("Select", "בחר"), + ("Toggle Tags", "החלף תגיות"), + ("pull_ab_failed_tip", "נכשל ברענון ספר הכתובות"), + ("push_ab_failed_tip", "נכשל סנכרון ספר הכתובות עם השרת"), + ("synced_peer_readded_tip", "המכשירים שהיו נוכחים בהפעלות האחרונות יסונכרנו בחזרה לספר הכתובות."), + ("Change Color", "שנה צבע"), + ("Primary Color", "צבע עיקרי"), + ("HSV Color", "צבע HSV"), + ("Installation Successful!", "ההתקנה הצליחה!"), + ("Installation failed!", "התקנה נכשלה!"), + ("Reverse mouse wheel", "הפוך כיוון גלגלת העכבר"), + ("{} sessions", "{} הפעלות"), + ("scam_title", "ייתכן שנפלת להונאה!"), + ("scam_text1", "אם אתה בשיחת טלפון עם מישהו שאינך מכיר ואינך סומך עליו שביקש ממך להשתמש ב-RustDesk ולהתחיל את השירות, אל תמשיך ונתק מיד."), + ("scam_text2", "סביר להניח שמדובר בהונאה שמנסה לגנוב ממך כסף או מידע פרטי אחר."), + ("Don't show again", "אל תראה שוב"), + ("I Agree", "אני מסכים"), + ("Decline", "דחה"), + ("Timeout in minutes", "משך זמן עד התנתקות (בדקות)"), + ("auto_disconnect_option_tip", "סגור באופן אוטומטי הפעלות נכנסות במקרה של חוסר פעילות של המשתמש"), + ("Connection failed due to inactivity", "התנתקות אוטומטית בגלל חוסר פעילות"), + ("Check for software update on startup", "בדוק עדכונים עם ההפעלה"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "אנא שדרג את RustDesk Server Pro לגרסה {} או חדשה יותר!"), + ("pull_group_failed_tip", "נכשל ברענון הקבוצה"), + ("Filter by intersection", "סנן לפי חיתוך"), + ("Remove wallpaper during incoming sessions", "הסר רקע שולחן עבודה במהלך הפעלות נכנסות"), + ("Test", "בדיקה"), + ("display_is_plugged_out_msg", "המסך הופסק, החלף למסך הראשון."), + ("No displays", "אין מסכים"), + ("Open in new window", "פתח בחלון חדש"), + ("Show displays as individual windows", "הצג מסכים כחלונות נפרדים"), + ("Use all my displays for the remote session", "השתמש בכל המסכים שלי עבור ההפעלה המרוחקת"), + ("selinux_tip", "SELinux מופעל במכשיר שלך, מה שעלול למנוע מ-RustDesk לפעול כראוי כצד הנשלט."), + ("Change view", "שנה תצוגה"), + ("Big tiles", "אריחים גדולים"), + ("Small tiles", "אריחים קטנים"), + ("List", "רשימה"), + ("Virtual display", "מסך וירטואלי"), + ("Plug out all", "נתק הכל"), + ("True color (4:4:4)", "צבע מדויק (4:4:4)"), + ("Enable blocking user input", "אפשר חסימת קלט משתמש"), + ("id_input_tip", "ניתן להזין מזהה, IP ישיר, או דומיין עם פורט (:).\nאם ברצונך לגשת למכשיר בשרת אחר, אנא הוסף את כתובת השרת (@?key=), לדוגמה,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nאם ברצונך לגשת למכשיר בשרת ציבורי, אנא הזן \"@public\", המפתח אינו נדרש לשרת ציבורי"), + ("privacy_mode_impl_mag_tip", "מצב 1"), + ("privacy_mode_impl_virtual_display_tip", "מצב 2"), + ("Enter privacy mode", "הכנס למצב פרטיות"), + ("Exit privacy mode", "צא ממצב פרטיות"), + ("idd_not_support_under_win10_2004_tip", "מנהל התצוגה העקיף אינו נתמך. נדרשת גרסת Windows 10, גרסה 2004 או חדשה יותר."), + ("input_source_1_tip", "מקור קלט 1"), + ("input_source_2_tip", "מקור קלט 2"), + ("Swap control-command key", "החלף בין המקשים Control ו־Command"), + ("swap-left-right-mouse", "החלף בין לחצן שמאלי וימני בעכבר"), + ("2FA code", "קוד אימות דו-שלבי"), + ("More", "עוד"), + ("enable-2fa-title", "הפעל אימות דו-שלבי"), + ("enable-2fa-desc", "אנא הגדר כעת את האפליקציה שלך לאימות. תוכל להשתמש באפליקציית אימות כגון Authy, Microsoft או Google Authenticator בטלפון או במחשב שלך.\n\nסרוק את קוד ה-QR עם האפליקציה שלך והזן את הקוד שהאפליקציה מציגה כדי להפעיל את אימות הדו-שלבי."), + ("wrong-2fa-code", "קוד שגוי. בדוק את הקוד ואת הגדרות השעה במכשיר"), + ("enter-2fa-title", "אימות דו-שלבי"), + ("Email verification code must be 6 characters.", "קוד אימות במייל חייב להיות באורך של 6 תווים."), + ("2FA code must be 6 digits.", "קוד אימות דו-שלבי חייב להיות באורך של 6 מספרים."), + ("Multiple Windows sessions found", "נמצאו מספר הפעלות Windows"), + ("Please select the session you want to connect to", "אנא בחר את ההפעלה שברצונך להתחבר אליה"), + ("powered_by_me", "מופעל דרכי"), + ("outgoing_only_desk_tip", "זוהי מהדורה מותאמת אישית.\nניתן להתחבר למכשירים אחרים, אך מכשירים אחרים לא יכולים להתחבר אליך."), + ("preset_password_warning", "שימו לב: שימוש בסיסמה קבועה עלול להפחית את רמת האבטחה"), + ("Security Alert", "התראת אבטחה"), + ("My address book", "ספר הכתובות שלי"), + ("Personal", "אישי"), + ("Owner", "בעלים"), + ("Set shared password", "הגדר סיסמה שיתופית"), + ("Exist in", "קיים ב"), + ("Read-only", "קריאה בלבד"), + ("Read/Write", "קריאה/כתיבה"), + ("Full Control", "שליטה מלאה"), + ("share_warning_tip", "זהירות: כל מי שברשימה יקבל את ההרשאות שנבחרו"), + ("Everyone", "כולם"), + ("ab_web_console_tip", "ספר הכתובות מסונכרן עם ממשק ניהול אינטרנטי"), + ("allow-only-conn-window-open-tip", "אפשר חיבורים רק כאשר חלון הניהול פתוח"), + ("no_need_privacy_mode_no_physical_displays_tip", "אין צורך במצב פרטיות כאשר אין תצוגות פיזיות"), + ("Follow remote cursor", "עקוב אחר מצביע מרוחק"), + ("Follow remote window focus", "עקוב אחר פוקוס בחלון מרוחק"), + ("default_proxy_tip", "ברירת מחדל זו תשתמש בהגדרות הproxy הכלליות של המערכת"), + ("no_audio_input_device_tip", "לא נמצא מכשיר קלט שמע"), + ("Incoming", "נכנס"), + ("Outgoing", "יוצא"), + ("Clear Wayland screen selection", "נקה את בחירת המסך ב־Wayland"), + ("clear_Wayland_screen_selection_tip", "נקה את בחירת המסך שנשמרה בעת שימוש ב־Wayland"), + ("confirm_clear_Wayland_screen_selection_tip", "האם אתה בטוח שברצונך לנקות את בחירת המסך עבור Wayland?"), + ("android_new_voice_call_tip", "בקשת שיחת קול חדשה התקבלה"), + ("texture_render_tip", "השתמש בטכניקת עיבוד מבוססת טקסטורות (ייתכן שיגדיל את הביצועים)"), + ("Use texture rendering", "השתמש בעיבוד טקסטורה"), + ("Floating window", "חלון צף"), + ("floating_window_tip", "הפעלת חלון צף תאפשר גישה נוחה מעל אפליקציות אחרות"), + ("Keep screen on", "השאר מסך דולק"), + ("Never", "אף פעם"), + ("During controlled", "בזמן שליטה"), + ("During service is on", "כאשר השירות פעיל"), + ("Capture screen using DirectX", "לכוד מסך באמצעות DirectX"), + ("Back", "חזור"), + ("Apps", "אפליקציות"), + ("Volume up", "הגבר"), + ("Volume down", "הנמך"), + ("Power", "הפעלה"), + ("Telegram bot", "בוט טלגרם"), + ("enable-bot-tip", "אפשר שליטה או קבלת התראות דרך בוט טלגרם"), + ("enable-bot-desc", "אפשרות זו תאפשר לבוט טלגרם לבצע פעולות או לשלוח התראות עבור חשבונך"), + ("cancel-2fa-confirm-tip", "האם אתה בטוח שברצונך לבטל את האימות הדו-שלבי?"), + ("cancel-bot-confirm-tip", "האם אתה בטוח שברצונך לבטל את קישור הבוט?"), + ("About RustDesk", "אודות RustDesk"), + ("Send clipboard keystrokes", "שלח הקשות לוח גזירים"), + ("network_error_tip", "אירעה שגיאת רשת. אנא בדוק את החיבור שלך ונסה שוב."), + ("Unlock with PIN", "פתח באמצעות קוד PIN"), + ("Requires at least {} characters", "נדרשים לפחות {} תווים"), + ("Wrong PIN", "PIN שגוי"), + ("Set PIN", "הגדר PIN"), + ("Enable trusted devices", "אפשר גישה ממכשירים מהימנים"), + ("Manage trusted devices", "נהל מכשירים מהימנים"), + ("Platform", "פלטפורמה"), + ("Days remaining", "ימים שנשארו"), + ("enable-trusted-devices-tip", "אפשר גישה אוטומטית ממכשירים שסומנו כמהימנים"), + ("Parent directory", "תיקיית אב"), + ("Resume", "המשך"), + ("Invalid file name", "שם קובץ אינו תקין"), + ("one-way-file-transfer-tip", "תכונה זו מאפשרת העברת קבצים בכיוון אחד בלבד – מהמכשיר שלך למרוחק או להפך"), + ("Authentication Required", "הזדהות נדרשת"), + ("Authenticate", "הזדהה"), + ("web_id_input_tip", "הזן את מזהה ההתחברות או כתובת דומיין להתחברות דרך הדפדפן"), + ("Download", "הורדה"), + ("Upload folder", "העלה תיקיה"), + ("Upload files", "העלה קבצים"), + ("Clipboard is synchronized", "לוח הגזירים סונכרן"), + ("Update client clipboard", "עדכן את לוח הגזירים של הלקוח"), + ("Untagged", "לא מתוייג"), + ("new-version-of-{}-tip", "גרסה חדשה של {} זמינה"), + ("Accessible devices", "מכשירים נגישים"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "אנא שדרג את לקוח RustDesk לגרסה {} או חדשה יותר בצד המרוחק!"), + ("d3d_render_tip", "שימוש בעיבוד Direct3D עשוי לשפר ביצועים בחלק מהמקרים"), + ("Use D3D rendering", "השתמש בעיבוד D3D"), + ("Printer", "מדפסת"), + ("printer-os-requirement-tip", "להפעלת מדפסת נדרש מערכת הפעלה תואמת"), + ("printer-requires-installed-{}-client-tip", "נדרש לקוח {} מותקן כדי להשתמש במדפסת"), + ("printer-{}-not-installed-tip", "המדפסת {} אינה מותקנת"), + ("printer-{}-ready-tip", "המדפסת {} מוכנה לשימוש"), + ("Install {} Printer", "התקן מדפסת {}"), + ("Outgoing Print Jobs", "עבודות הדפסה יוצאות"), + ("Incoming Print Jobs", "עבודות הדפסה נכנסות"), + ("Incoming Print Job", "עבודת הדפסה נכנסת"), + ("use-the-default-printer-tip", "השתמש במדפסת ברירת המחדל של המערכת"), + ("use-the-selected-printer-tip", "השתמש במדפסת שנבחרה מתוך הרשימה"), + ("auto-print-tip", "הדפס אוטומטית עבודות הדפסה נכנסות ללא אישור נוסף"), + ("print-incoming-job-confirm-tip", "האם ברצונך להדפיס את עבודת ההדפסה הנכנסת?"), + ("remote-printing-disallowed-tile-tip", "לא ניתן להדפיס מרחוק"), + ("remote-printing-disallowed-text-tip", "המכשיר המרוחק אינו מאפשר הדפסה מרחוק"), + ("save-settings-tip", "שמור את ההגדרות להבא"), + ("dont-show-again-tip", "אל תציג שוב"), + ("Take screenshot", "צלם צילום מסך"), + ("Taking screenshot", "מצלם צילום מסך"), + ("screenshot-merged-screen-not-supported-tip", "צילום מסך משולב מכל המסכים אינו נתמך"), + ("screenshot-action-tip", "בחר פעולה לאחר צילום המסך"), + ("Save as", "שמור בשם"), + ("Copy to clipboard", "העתק ללוח"), + ("Enable remote printer", "אפשר מדפסת מרוחקת"), + ("Downloading {}", "מוריד את {}"), + ("{} Update", "עדכון {}"), + ("{}-to-update-tip", "קיימת גרסה חדשה של {} – מומלץ לעדכן"), + ("download-new-version-failed-tip", "נכשל בהורדת הגרסה החדשה"), + ("Auto update", "עדכון אוטומטי"), + ("update-failed-check-msi-tip", "העדכון נכשל – בדוק אם קובץ ה־MSI מותקן או הוסר"), + ("websocket_tip", "אפשר שימוש בפרוטוקול WebSocket לחיבורים"), + ("Use WebSocket", "השתמש ב־WebSocket"), + ("Trackpad speed", "מהירות משטח מגע"), + ("Default trackpad speed", "מהירות ברירת מחדל של משטח מגע"), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "הצג מצלמה"), + ("Enable camera", "הפעל מצלמה"), + ("No cameras", "אין מצלמות"), + ("view_camera_unsupported_tip", "הצגת מצלמה אינה נתמכת במכשיר המרוחק"), + ("Terminal", "מסוף"), + ("Enable terminal", "אפשר מסוף"), + ("New tab", "טאב חדש"), + ("Keep terminal sessions on disconnect", "שמור על הטרמינל סשן בניתוק"), + ("Terminal (Run as administrator)", "מסוף (הרץ כמנהל)"), + ("terminal-admin-login-tip", "מסוף-טיפ-כניסת-אדמין"), + ("Failed to get user token.", "נכשל בקבלת הטוקן של המשתמש"), + ("Incorrect username or password.", "שם משתמש או סיסמא אינם נכונים"), + ("The user is not an administrator.", "המשתמש אינו מנהל"), + ("Failed to check if the user is an administrator.", "נכשל בבדיקה אם המשתמש הוא מנהל"), + ("Supported only in the installed version.", "נתמך רק בגרסה המותקנת"), + ("elevation_username_tip", "רמז_ליוזר_להעלאת_הרשאה"), + ("Preparing for installation ...", "הכנה להתקנה..."), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "המשך עם {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/hi.rs b/vendor/rustdesk/src/lang/hi.rs new file mode 100644 index 0000000..d35095f --- /dev/null +++ b/vendor/rustdesk/src/lang/hi.rs @@ -0,0 +1,746 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "स्थिति"), + ("Your Desktop", "आपका डेस्कटॉप"), + ("desk_tip", "आपका डेस्कटॉप इस आईडी और पासवर्ड से एक्सेस किया जा सकता है।"), + ("Password", "पासवर्ड"), + ("Ready", "तैयार"), + ("Established", "स्थापित"), + ("connecting_status", "नेटवर्क से जुड़ रहा है..."), + ("Enable service", "सेवा सक्षम करें"), + ("Start service", "सेवा शुरू करें"), + ("Service is running", "सेवा चल रही है"), + ("Service is not running", "सेवा नहीं चल रही है"), + ("not_ready_status", "तैयार नहीं। कृपया अपना कनेक्शन जांचें"), + ("Control Remote Desktop", "रिमोट डेस्कटॉप नियंत्रित करें"), + ("Transfer file", "फ़ाइल स्थानांतरण"), + ("Connect", "जुड़ें"), + ("Recent sessions", "हाल के सत्र"), + ("Address book", "पता पुस्तिका"), + ("Confirmation", "पुष्टि"), + ("TCP tunneling", "TCP टनलिंग"), + ("Remove", "हटाएं"), + ("Refresh random password", "यादृच्छिक (Random) पासवर्ड बदलें"), + ("Set your own password", "अपना पासवर्ड सेट करें"), + ("Enable keyboard/mouse", "कीबोर्ड/माउस सक्षम करें"), + ("Enable clipboard", "क्लिपबोर्ड सक्षम करें"), + ("Enable file transfer", "फ़ाइल स्थानांतरण सक्षम करें"), + ("Enable TCP tunneling", "TCP टनलिंग सक्षम करें"), + ("IP Whitelisting", "IP श्वेतसूची (Whitelisting)"), + ("ID/Relay Server", "ID/रिले सर्वर"), + ("Import server config", "सर्वर कॉन्फ़िगरेशन इम्पोर्ट करें"), + ("Export Server Config", "सर्वर कॉन्फ़िगरेशन एक्सपोर्ट करें"), + ("Import server configuration successfully", "सर्वर कॉन्फ़िगरेशन सफलतापूर्वक इम्पोर्ट किया गया"), + ("Export server configuration successfully", "सर्वर कॉन्फ़िगरेशन सफलतापूर्वक एक्सपोर्ट किया गया"), + ("Invalid server configuration", "अमान्य सर्वर कॉन्फ़िगरेशन"), + ("Clipboard is empty", "क्लिपबोर्ड खाली है"), + ("Stop service", "सेवा रोकें"), + ("Change ID", "ID बदलें"), + ("Your new ID", "आपकी नई ID"), + ("length %min% to %max%", "लंबाई %min% से %max% तक"), + ("starts with a letter", "एक अक्षर से शुरू होता है"), + ("allowed characters", "अनुमत अक्षर"), + ("id_change_tip", "ID बदलने के बाद वर्तमान कनेक्शन टूट जाएगा।"), + ("Website", "वेबसाइट"), + ("About", "के बारे में"), + ("Slogan_tip", "बेहतर अनुभव के लिए बनाया गया रिमोट डेस्कटॉप सॉफ़्टवेयर"), + ("Privacy Statement", "गोपनीयता कथन"), + ("Mute", "म्यूट करें"), + ("Build Date", "निर्माण तिथि"), + ("Version", "संस्करण"), + ("Home", "होम"), + ("Audio Input", "ऑडियो इनपुट"), + ("Enhancements", "वृद्धि (Enhancements)"), + ("Hardware Codec", "हार्डवेयर कोडेक"), + ("Adaptive bitrate", "अनुकूली (Adaptive) बिटरेट"), + ("ID Server", "ID सर्वर"), + ("Relay Server", "रिले सर्वर"), + ("API Server", "API सर्वर"), + ("invalid_http", "अमान्य HTTP लिंक"), + ("Invalid IP", "अमान्य IP"), + ("Invalid format", "अमान्य प्रारूप"), + ("server_not_support", "सर्वर द्वारा समर्थित नहीं"), + ("Not available", "उपलब्ध नहीं"), + ("Too frequent", "बहुत बार-बार"), + ("Cancel", "रद्द करें"), + ("Skip", "छोड़ें"), + ("Close", "बंद करें"), + ("Retry", "पुनः प्रयास करें"), + ("OK", "ठीक है"), + ("Password Required", "पासवर्ड आवश्यक है"), + ("Please enter your password", "कृपया अपना पासवर्ड दर्ज करें"), + ("Remember password", "पासवर्ड याद रखें"), + ("Wrong Password", "गलत पासवर्ड"), + ("Do you want to enter again?", "क्या आप दोबारा दर्ज करना चाहते हैं?"), + ("Connection Error", "कनेक्शन त्रुटि"), + ("Error", "त्रुटि"), + ("Reset by the peer", "दूसरे सिस्टम द्वारा रिसेट किया गया"), + ("Connecting...", "जुड़ रहा है..."), + ("Connection in progress. Please wait.", "कनेक्शन जारी है। कृपया प्रतीक्षा करें।"), + ("Please try 1 minute later", "कृपया 1 मिनट बाद पुनः प्रयास करें"), + ("Login Error", "लॉगिन त्रुटि"), + ("Successful", "सफल"), + ("Connected, waiting for image...", "जुड़ गया, इमेज की प्रतीक्षा कर रहा है..."), + ("Name", "नाम"), + ("Type", "प्रकार"), + ("Modified", "संशोधित"), + ("Size", "आकार"), + ("Show Hidden Files", "छिपी हुई फाइलें दिखाएं"), + ("Receive", "प्राप्त करें"), + ("Send", "भेजें"), + ("Refresh File", "फ़ाइल रिफ्रेश करें"), + ("Local", "स्थानीय (Local)"), + ("Remote", "रिमोट"), + ("Remote Computer", "रिमोट कंप्यूटर"), + ("Local Computer", "स्थानीय कंप्यूटर"), + ("Confirm Delete", "हटाने की पुष्टि करें"), + ("Delete", "हटाएं"), + ("Properties", "गुण (Properties)"), + ("Multi Select", "बहु-चयन"), + ("Select All", "सभी चुनें"), + ("Unselect All", "सभी अचयनित करें"), + ("Empty Directory", "खाली निर्देशिका"), + ("Not an empty directory", "निर्देशिका खाली नहीं है"), + ("Are you sure you want to delete this file?", "क्या आप वाकई इस फ़ाइल को हटाना चाहते हैं?"), + ("Are you sure you want to delete this empty directory?", "क्या आप वाकई इस खाली निर्देशिका को हटाना चाहते हैं?"), + ("Are you sure you want to delete the file of this directory?", "क्या आप वाकई इस निर्देशिका की फ़ाइल को हटाना चाहते हैं?"), + ("Do this for all conflicts", "सभी विवादों के लिए यह करें"), + ("This is irreversible!", "इसे वापस नहीं लिया जा सकता!"), + ("Deleting", "हटाया जा रहा है"), + ("files", "फाइलें"), + ("Waiting", "प्रतीक्षा कर रहा है"), + ("Finished", "पूरा हुआ"), + ("Speed", "गति"), + ("Custom Image Quality", "कस्टम इमेज गुणवत्ता"), + ("Privacy mode", "गोपनीयता मोड"), + ("Block user input", "उपयोगकर्ता इनपुट ब्लॉक करें"), + ("Unblock user input", "उपयोगकर्ता इनपुट अनब्लॉक करें"), + ("Adjust Window", "विंडो समायोजित करें"), + ("Original", "मूल (Original)"), + ("Shrink", "सिकुड़ें"), + ("Stretch", "खिंचाव (Stretch)"), + ("Scrollbar", "स्क्रोलबार"), + ("ScrollAuto", "ऑटो स्क्रॉल"), + ("Good image quality", "अच्छी इमेज गुणवत्ता"), + ("Balanced", "संतुलित"), + ("Optimize reaction time", "प्रतिक्रिया समय अनुकूलित करें"), + ("Custom", "कस्टम"), + ("Show remote cursor", "रिमोट कर्सर दिखाएं"), + ("Show quality monitor", "गुणवत्ता मॉनिटर दिखाएं"), + ("Disable clipboard", "क्लिपबोर्ड अक्षम करें"), + ("Lock after session end", "सत्र समाप्त होने के बाद लॉक करें"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del डालें"), + ("Insert Lock", "लॉक डालें"), + ("Refresh", "रिफ्रेश करें"), + ("ID does not exist", "ID मौजूद नहीं है"), + ("Failed to connect to rendezvous server", "Rendezvous सर्वर से जुड़ने में विफल"), + ("Please try later", "कृपया बाद में प्रयास करें"), + ("Remote desktop is offline", "रिमोट डेस्कटॉप ऑफ़लाइन है"), + ("Key mismatch", "कुंजी बेमेल (Key mismatch)"), + ("Timeout", "समय समाप्त"), + ("Failed to connect to relay server", "रिले सर्वर से जुड़ने में विफल"), + ("Failed to connect via rendezvous server", "Rendezvous सर्वर के माध्यम से जुड़ने में विफल"), + ("Failed to connect via relay server", "रिले सर्वर के माध्यम से जुड़ने में विफल"), + ("Failed to make direct connection to remote desktop", "रिमोट डेस्कटॉप से सीधा कनेक्शन बनाने में विफल"), + ("Set Password", "पासवर्ड सेट करें"), + ("OS Password", "OS पासवर्ड"), + ("install_tip", "सर्वोत्तम प्रदर्शन के लिए, इसे इंस्टॉल करें।"), + ("Click to upgrade", "अपग्रेड करने के लिए क्लिक करें"), + ("Configure", "कॉन्फ़िगर करें"), + ("config_acc", "एक्सेसिबिलिटी कॉन्फ़िगर करें"), + ("config_screen", "स्क्रीन कॉन्फ़िगर करें"), + ("Installing ...", "इंस्टॉल हो रहा है..."), + ("Install", "इंस्टॉल करें"), + ("Installation", "इंस्टॉलेशन"), + ("Installation Path", "इंस्टॉलेशन पाथ"), + ("Create start menu shortcuts", "स्टार्ट मेनू शॉर्टकट बनाएं"), + ("Create desktop icon", "डेस्कटॉप आइकन बनाएं"), + ("agreement_tip", "इंस्टॉल करके आप लाइसेंस समझौते को स्वीकार करते हैं।"), + ("Accept and Install", "स्वीकार करें और इंस्टॉल करें"), + ("End-user license agreement", "अंतिम उपयोगकर्ता लाइसेंस समझौता"), + ("Generating ...", "बनाया जा रहा है..."), + ("Your installation is lower version.", "आपका वर्तमान इंस्टॉलेशन पुराना संस्करण है।"), + ("not_close_tcp_tip", "टनल का उपयोग करते समय इस विंडो को बंद न करें।"), + ("Listening ...", "सुन रहा है (Listening)..."), + ("Remote Host", "रिमोट होस्ट"), + ("Remote Port", "रिमोट पोर्ट"), + ("Action", "कार्य"), + ("Add", "जोड़ें"), + ("Local Port", "स्थानीय पोर्ट"), + ("Local Address", "स्थानीय पता"), + ("Change Local Port", "स्थानीय पोर्ट बदलें"), + ("setup_server_tip", "तेज़ कनेक्शन के लिए अपना खुद का सर्वर सेटअप करें"), + ("Too short, at least 6 characters.", "बहुत छोटा, कम से कम 6 अक्षर होने चाहिए।"), + ("The confirmation is not identical.", "पुष्टि समान नहीं है।"), + ("Permissions", "अनुमतियाँ"), + ("Accept", "स्वीकार करें"), + ("Dismiss", "खारिज करें"), + ("Disconnect", "डिस्कनेक्ट करें"), + ("Enable file copy and paste", "फ़ाइल कॉपी और पेस्ट सक्षम करें"), + ("Connected", "जुड़ गया"), + ("Direct and encrypted connection", "सीधा और एन्क्रिप्टेड कनेक्शन"), + ("Relayed and encrypted connection", "रिले और एन्क्रिप्टेड कनेक्शन"), + ("Direct and unencrypted connection", "सीधा और अनएन्क्रिप्टेड कनेक्शन"), + ("Relayed and unencrypted connection", "रिले और अनएन्क्रिप्टेड कनेक्शन"), + ("Enter Remote ID", "रिमोट ID दर्ज करें"), + ("Enter your password", "अपना पासवर्ड दर्ज करें"), + ("Logging in...", "लॉग इन हो रहा है..."), + ("Enable RDP session sharing", "RDP सत्र साझाकरण सक्षम करें"), + ("Auto Login", "ऑटो लॉगिन"), + ("Enable direct IP access", "सीधी IP पहुंच सक्षम करें"), + ("Rename", "नाम बदलें"), + ("Space", "स्थान (Space)"), + ("Create desktop shortcut", "डेस्कटॉप शॉर्टकट बनाएं"), + ("Change Path", "पाथ बदलें"), + ("Create Folder", "फ़ोल्डर बनाएं"), + ("Please enter the folder name", "कृपया फ़ोल्डर का नाम दर्ज करें"), + ("Fix it", "इसे ठीक करें"), + ("Warning", "चेतावनी"), + ("Login screen using Wayland is not supported", "Wayland का उपयोग करने वाली लॉगिन स्क्रीन समर्थित नहीं है"), + ("Reboot required", "रीबूट आवश्यक है"), + ("Unsupported display server", "असमर्थित डिस्प्ले सर्वर"), + ("x11 expected", "x11 अपेक्षित है"), + ("Port", "पोर्ट"), + ("Settings", "सेटिंग्स"), + ("Username", "उपयोगकर्ता नाम"), + ("Invalid port", "अमान्य पोर्ट"), + ("Closed manually by the peer", "दूसरे सिस्टम द्वारा मैन्युअल रूप से बंद किया गया"), + ("Enable remote configuration modification", "रिमोट कॉन्फ़िगरेशन संशोधन सक्षम करें"), + ("Run without install", "बिना इंस्टॉल किए चलाएं"), + ("Connect via relay", "रिले के माध्यम से जुड़ें"), + ("Always connect via relay", "हमेशा रिले के माध्यम से जुड़ें"), + ("whitelist_tip", "केवल श्वेतसूचीबद्ध IP ही मुझ तक पहुंच सकते हैं"), + ("Login", "लॉगिन"), + ("Verify", "सत्यापित करें"), + ("Remember me", "मुझे याद रखें"), + ("Trust this device", "इस डिवाइस पर भरोसा करें"), + ("Verification code", "सत्यापन कोड"), + ("verification_tip", "एक सत्यापन कोड आपके ईमेल पर भेजा गया है"), + ("Logout", "लॉगआउट"), + ("Tags", "टैग"), + ("Search ID", "ID खोजें"), + ("whitelist_sep", "अल्पविराम, अर्धविराम या रिक्त स्थान द्वारा अलग किया गया"), + ("Add ID", "ID जोड़ें"), + ("Add Tag", "टैग जोड़ें"), + ("Unselect all tags", "सभी टैग अचयनित करें"), + ("Network error", "नेटवर्क त्रुटि"), + ("Username missed", "उपयोगकर्ता नाम छूट गया"), + ("Password missed", "पासवर्ड छूट गया"), + ("Wrong credentials", "गलत क्रेडेंशियल"), + ("The verification code is incorrect or has expired", "सत्यापन कोड गलत है या समाप्त हो गया है"), + ("Edit Tag", "टैग संपादित करें"), + ("Forget Password", "पासवर्ड भूल गए"), + ("Favorites", "पसंदीदा"), + ("Add to Favorites", "पसंदीदा में जोड़ें"), + ("Remove from Favorites", "पसंदीदा से हटाएं"), + ("Empty", "खाली"), + ("Invalid folder name", "अमान्य फ़ोल्डर नाम"), + ("Socks5 Proxy", "Socks5 प्रॉक्सी"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) प्रॉक्सी"), + ("Discovered", "खोजा गया"), + ("install_daemon_tip", "बूट पर शुरू करने के लिए सेवा इंस्टॉल करें"), + ("Remote ID", "रिमोट ID"), + ("Paste", "पेस्ट करें"), + ("Paste here?", "यहाँ पेस्ट करें?"), + ("Are you sure to close the connection?", "क्या आप वाकई कनेक्शन बंद करना चाहते हैं?"), + ("Download new version", "नया संस्करण डाउनलोड करें"), + ("Touch mode", "टच मोड"), + ("Mouse mode", "माउस मोड"), + ("One-Finger Tap", "एक उंगली से टैप"), + ("Left Mouse", "बायां माउस"), + ("One-Long Tap", "एक लंबा टैप"), + ("Two-Finger Tap", "दो उंगलियों से टैप"), + ("Right Mouse", "दायां माउस"), + ("One-Finger Move", "एक उंगली से हिलाएं"), + ("Double Tap & Move", "डबल टैप और हिलाएं"), + ("Mouse Drag", "माउस ड्रैग"), + ("Three-Finger vertically", "तीन उंगलियां लंबवत"), + ("Mouse Wheel", "माउस व्हील"), + ("Two-Finger Move", "दो उंगलियों से हिलाएं"), + ("Canvas Move", "कैनवास मूव"), + ("Pinch to Zoom", "ज़ूम करने के लिए पिंच करें"), + ("Canvas Zoom", "कैनवास ज़ूम"), + ("Reset canvas", "कैनवास रिसेट करें"), + ("No permission of file transfer", "फ़ाइल स्थानांतरण की अनुमति नहीं है"), + ("Note", "नोट"), + ("Connection", "कनेक्शन"), + ("Share screen", "स्क्रीन शेयर करें"), + ("Chat", "चैट"), + ("Total", "कुल"), + ("items", "आइटम"), + ("Selected", "चयनित"), + ("Screen Capture", "स्क्रीन कैप्चर"), + ("Input Control", "इनपुट नियंत्रण"), + ("Audio Capture", "ऑडियो कैप्चर"), + ("Do you accept?", "क्या आप स्वीकार करते हैं?"), + ("Open System Setting", "सिस्टम सेटिंग खोलें"), + ("How to get Android input permission?", "Android इनपुट अनुमति कैसे प्राप्त करें?"), + ("android_input_permission_tip1", "इनपुट अनुमति प्राप्त करने के लिए एक्सेसिबिलिटी सेवा सक्षम करें।"), + ("android_input_permission_tip2", "कृपया सिस्टम सेटिंग में RustDesk खोजें और इसे चालू करें।"), + ("android_new_connection_tip", "एक नया नियंत्रण अनुरोध प्राप्त हुआ है।"), + ("android_service_will_start_tip", "स्क्रीन कैप्चर चालू करने से सेवा अपने आप शुरू हो जाएगी।"), + ("android_stop_service_tip", "सेवा बंद करने से सभी कनेक्शन टूट जाएंगे।"), + ("android_version_audio_tip", "ऑडियो कैप्चर केवल Android 10 या उच्चतर पर समर्थित है।"), + ("android_start_service_tip", "स्क्रीन शेयरिंग सेवा शुरू करने के लिए क्लिक करें।"), + ("android_permission_may_not_change_tip", "अनुमतियाँ बाद में नहीं बदली जा सकती हैं, कृपया ध्यान से चुनें।"), + ("Account", "खाता"), + ("Overwrite", "ओवरराइट (Overwrite) करें"), + ("This file exists, skip or overwrite this file?", "यह फ़ाइल मौजूद है, छोड़ें या ओवरराइट करें?"), + ("Quit", "बाहर निकलें"), + ("Help", "सहायता"), + ("Failed", "विफल"), + ("Succeeded", "सफल"), + ("Someone turns on privacy mode, exit", "किसी ने गोपनीयता मोड चालू किया है, बाहर निकल रहे हैं"), + ("Unsupported", "असमर्थित"), + ("Peer denied", "दूसरे सिस्टम ने मना कर दिया"), + ("Please install plugins", "कृपया प्लगइन्स इंस्टॉल करें"), + ("Peer exit", "दूसरा सिस्टम बाहर निकल गया"), + ("Failed to turn off", "बंद करने में विफल"), + ("Turned off", "बंद कर दिया गया"), + ("Language", "भाषा"), + ("Keep RustDesk background service", "RustDesk बैकग्राउंड सेवा चालू रखें"), + ("Ignore Battery Optimizations", "बैटरी ऑप्टिमाइजेशन को अनदेखा करें"), + ("android_open_battery_optimizations_tip", "डिस्कनेक्शन से बचने के लिए बैटरी ऑप्टिमाइजेशन सेटिंग खोलें"), + ("Start on boot", "बूट पर शुरू करें"), + ("Start the screen sharing service on boot, requires special permissions", "बूट पर स्क्रीन शेयरिंग सेवा शुरू करें, विशेष अनुमतियों की आवश्यकता है"), + ("Connection not allowed", "कनेक्शन की अनुमति नहीं है"), + ("Legacy mode", "लेगेसी (Legacy) मोड"), + ("Map mode", "मैप मोड"), + ("Translate mode", "अनुवाद मोड"), + ("Use permanent password", "स्थायी पासवर्ड का उपयोग करें"), + ("Use both passwords", "दोनों पासवर्ड का उपयोग करें"), + ("Set permanent password", "स्थायी पासवर्ड सेट करें"), + ("Enable remote restart", "रिमोट रीस्टार्ट सक्षम करें"), + ("Restart remote device", "रिमोट डिवाइस रीस्टार्ट करें"), + ("Are you sure you want to restart", "क्या आप वाकई रीस्टार्ट करना चाहते हैं?"), + ("Restarting remote device", "रिमोट डिवाइस रीस्टार्ट हो रहा है"), + ("remote_restarting_tip", "रिमोट डिवाइस रीस्टार्ट हो रहा है, कृपया प्रतीक्षा करें..."), + ("Copied", "कॉपी किया गया"), + ("Exit Fullscreen", "फुलस्क्रीन से बाहर निकलें"), + ("Fullscreen", "फुलस्क्रीन"), + ("Mobile Actions", "मोबाइल क्रियाएं"), + ("Select Monitor", "मॉनिटर चुनें"), + ("Control Actions", "नियंत्रण क्रियाएं"), + ("Display Settings", "डिस्प्ले सेटिंग्स"), + ("Ratio", "अनुपात (Ratio)"), + ("Image Quality", "इमेज गुणवत्ता"), + ("Scroll Style", "स्क्रॉल शैली"), + ("Show Toolbar", "टूलबार दिखाएं"), + ("Hide Toolbar", "टूलबार छुपाएं"), + ("Direct Connection", "सीधा कनेक्शन"), + ("Relay Connection", "रिले कनेक्शन"), + ("Secure Connection", "सुरक्षित कनेक्शन"), + ("Insecure Connection", "असुरक्षित कनेक्शन"), + ("Scale original", "मूल पैमाना"), + ("Scale adaptive", "अनुकूली पैमाना"), + ("General", "सामान्य"), + ("Security", "सुरक्षा"), + ("Theme", "थीम"), + ("Dark Theme", "डार्क थीम"), + ("Light Theme", "लाइट थीम"), + ("Dark", "डार्क"), + ("Light", "लाइट"), + ("Follow System", "सिस्टम का पालन करें"), + ("Enable hardware codec", "हार्डवेयर कोडेक सक्षम करें"), + ("Unlock Security Settings", "सुरक्षा सेटिंग्स अनलॉक करें"), + ("Enable audio", "ऑडियो सक्षम करें"), + ("Unlock Network Settings", "नेटवर्क सेटिंग्स अनलॉक करें"), + ("Server", "सर्वर"), + ("Direct IP Access", "सीधी IP पहुंच"), + ("Proxy", "प्रॉक्सी"), + ("Apply", "लागू करें"), + ("Disconnect all devices?", "सभी डिवाइस डिस्कनेक्ट करें?"), + ("Clear", "साफ करें"), + ("Audio Input Device", "ऑडियो इनपुट डिवाइस"), + ("Use IP Whitelisting", "IP श्वेतसूची का उपयोग करें"), + ("Network", "नेटवर्क"), + ("Pin Toolbar", "टूलबार पिन करें"), + ("Unpin Toolbar", "टूलबार अनपिन करें"), + ("Recording", "रिकॉर्डिंग"), + ("Directory", "निर्देशिका"), + ("Automatically record incoming sessions", "आने वाले सत्रों को स्वचालित रूप से रिकॉर्ड करें"), + ("Automatically record outgoing sessions", "जाने वाले सत्रों को स्वचालित रूप से रिकॉर्ड करें"), + ("Change", "बदलें"), + ("Start session recording", "सत्र रिकॉर्डिंग शुरू करें"), + ("Stop session recording", "सत्र रिकॉर्डिंग रोकें"), + ("Enable recording session", "सत्र रिकॉर्डिंग सक्षम करें"), + ("Enable LAN discovery", "LAN खोज सक्षम करें"), + ("Deny LAN discovery", "LAN खोज अस्वीकार करें"), + ("Write a message", "संदेश लिखें"), + ("Prompt", "प्रॉम्प्ट"), + ("Please wait for confirmation of UAC...", "कृपया UAC की पुष्टि की प्रतीक्षा करें..."), + ("elevated_foreground_window_tip", "रिमोट डेस्कटॉप की वर्तमान विंडो को उच्च अनुमतियों की आवश्यकता है।"), + ("Disconnected", "डिस्कनेक्ट हो गया"), + ("Other", "अन्य"), + ("Confirm before closing multiple tabs", "एकाधिक टैब बंद करने से पहले पुष्टि करें"), + ("Keyboard Settings", "कीबोर्ड सेटिंग्स"), + ("Full Access", "पूर्ण पहुंच (Full Access)"), + ("Screen Share", "स्क्रीन शेयर"), + ("ubuntu-21-04-required", "Ubuntu 21.04 या उच्चतर आवश्यक है"), + ("wayland-requires-higher-linux-version", "Wayland के लिए उच्च Linux संस्करण आवश्यक है"), + ("xdp-portal-unavailable", "XDP पोर्टल अनुपलब्ध है"), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "कृपया साझा की जाने वाली स्क्रीन चुनें (दूसरे सिस्टम पर संचालित करें)।"), + ("Show RustDesk", "RustDesk दिखाएं"), + ("This PC", "यह PC"), + ("or", "या"), + ("Elevate", "एलीवेट (Elevate) करें"), + ("Zoom cursor", "ज़ूम कर्सर"), + ("Accept sessions via password", "पासवर्ड के माध्यम से सत्र स्वीकार करें"), + ("Accept sessions via click", "क्लिक के माध्यम से सत्र स्वीकार करें"), + ("Accept sessions via both", "दोनों के माध्यम से सत्र स्वीकार करें"), + ("Please wait for the remote side to accept your session request...", "कृपया रिमोट साइड द्वारा आपके सत्र अनुरोध को स्वीकार करने की प्रतीक्षा करें..."), + ("One-time Password", "वन-टाइम पासवर्ड"), + ("Use one-time password", "वन-टाइम पासवर्ड का उपयोग करें"), + ("One-time password length", "वन-टाइम पासवर्ड की लंबाई"), + ("Request access to your device", "आपके डिवाइस तक पहुंच का अनुरोध"), + ("Hide connection management window", "कनेक्शन प्रबंधन विंडो छुपाएं"), + ("hide_cm_tip", "केवल तभी छुपाएं जब पासवर्ड से कनेक्शन की अनुमति हो"), + ("wayland_experiment_tip", "Wayland समर्थन अभी परीक्षण मोड में है"), + ("Right click to select tabs", "टैब चुनने के लिए राइट क्लिक करें"), + ("Skipped", "छोड़ दिया गया"), + ("Add to address book", "पता पुस्तिका में जोड़ें"), + ("Group", "समूह"), + ("Search", "खोजें"), + ("Closed manually by web console", "वेब कंसोल द्वारा मैन्युअल रूप से बंद किया गया"), + ("Local keyboard type", "स्थानीय कीबोर्ड प्रकार"), + ("Select local keyboard type", "स्थानीय कीबोर्ड प्रकार चुनें"), + ("software_render_tip", "यदि आपकी स्क्रीन काली है, तो इसे आज़माएं"), + ("Always use software rendering", "हमेशा सॉफ़्टवेयर रेंडरिंग का उपयोग करें"), + ("config_input", "इनपुट कॉन्फ़िगर करें"), + ("config_microphone", "माइक्रोफ़ोन कॉन्फ़िगर करें"), + ("request_elevation_tip", "रिमोट साइड से उच्च अनुमतियों का अनुरोध करें"), + ("Wait", "प्रतीक्षा करें"), + ("Elevation Error", "एलीवेशन (Elevation) त्रुटि"), + ("Ask the remote user for authentication", "रिमोट उपयोगकर्ता से प्रमाणीकरण मांगें"), + ("Choose this if the remote account is administrator", "यदि रिमोट खाता व्यवस्थापक (Admin) है तो इसे चुनें"), + ("Transmit the username and password of administrator", "व्यवस्थापक का उपयोगकर्ता नाम और पासवर्ड भेजें"), + ("still_click_uac_tip", "रिमोट उपयोगकर्ता को अभी भी UAC विंडो पर 'हाँ' क्लिक करना होगा।"), + ("Request Elevation", "एलीवेशन का अनुरोध करें"), + ("wait_accept_uac_tip", "कृपया रिमोट उपयोगकर्ता द्वारा UAC स्वीकार करने की प्रतीक्षा करें।"), + ("Elevate successfully", "सफलतापूर्वक एलीवेट किया गया"), + ("uppercase", "बड़े अक्षर (Uppercase)"), + ("lowercase", "छोटे अक्षर (Lowercase)"), + ("digit", "अंक (Digit)"), + ("special character", "विशेष वर्ण"), + ("length>=8", "लंबाई >= 8"), + ("Weak", "कमजोर"), + ("Medium", "मध्यम"), + ("Strong", "मजबूत"), + ("Switch Sides", "साइड्स बदलें"), + ("Please confirm if you want to share your desktop?", "कृपया पुष्टि करें कि क्या आप अपना डेस्कटॉप साझा करना चाहते हैं?"), + ("Display", "डिस्प्ले"), + ("Default View Style", "डिफ़ॉल्ट व्यू शैली"), + ("Default Scroll Style", "डिफ़ॉल्ट स्क्रॉल शैली"), + ("Default Image Quality", "डिफ़ॉल्ट इमेज गुणवत्ता"), + ("Default Codec", "डिफ़ॉल्ट कोडेक"), + ("Bitrate", "बिटरेट"), + ("FPS", "FPS"), + ("Auto", "ऑटो"), + ("Other Default Options", "अन्य डिफ़ॉल्ट विकल्प"), + ("Voice call", "वॉयस कॉल"), + ("Text chat", "टेक्स्ट चैट"), + ("Stop voice call", "वॉयस कॉल बंद करें"), + ("relay_hint_tip", "सीधा कनेक्शन संभव नहीं हो सकता; आप रिले के माध्यम से जुड़ने का प्रयास कर सकते हैं।"), + ("Reconnect", "पुनः कनेक्ट करें"), + ("Codec", "कोडेक"), + ("Resolution", "रिज़ॉल्यूशन"), + ("No transfers in progress", "कोई स्थानांतरण जारी नहीं है"), + ("Set one-time password length", "वन-टाइम पासवर्ड की लंबाई सेट करें"), + ("RDP Settings", "RDP सेटिंग्स"), + ("Sort by", "इसके अनुसार क्रमबद्ध करें"), + ("New Connection", "नया कनेक्शन"), + ("Restore", "पुनर्स्थापित करें"), + ("Minimize", "मिनिमाइज करें"), + ("Maximize", "मैक्सिमाइज करें"), + ("Your Device", "आपका डिवाइस"), + ("empty_recent_tip", "हाल के सत्र यहाँ दिखाई देंगे।"), + ("empty_favorite_tip", "पसंदीदा डिवाइस यहाँ दिखाई देंगे।"), + ("empty_lan_tip", "खोजे गए डिवाइस यहाँ दिखाई देंगे।"), + ("empty_address_book_tip", "आपके पता पुस्तिका में वर्तमान में कोई डिवाइस नहीं है।"), + ("Empty Username", "खाली उपयोगकर्ता नाम"), + ("Empty Password", "खाली पासवर्ड"), + ("Me", "मैं"), + ("identical_file_tip", "यह फ़ाइल पहले से ही मौजूद है।"), + ("show_monitors_tip", "टूलबार में मॉनिटर दिखाएं"), + ("View Mode", "व्यू मोड"), + ("login_linux_tip", "रिमोट Linux सत्र शुरू करने के लिए आपको लॉगिन करना होगा"), + ("verify_rustdesk_password_tip", "RustDesk पासवर्ड सत्यापित करें"), + ("remember_account_tip", "इस खाते को याद रखें"), + ("os_account_desk_tip", "रिमोट डेस्कटॉप को एक्सेस करने के लिए OS खाते का उपयोग करें"), + ("OS Account", "OS खाता"), + ("another_user_login_title_tip", "एक अन्य उपयोगकर्ता पहले से ही लॉगिन है"), + ("another_user_login_text_tip", "डिस्कनेक्ट करें और पुनः प्रयास करें"), + ("xorg_not_found_title_tip", "Xorg नहीं मिला"), + ("xorg_not_found_text_tip", "कृपया Xorg इंस्टॉल करें"), + ("no_desktop_title_tip", "कोई डेस्कटॉप उपलब्ध नहीं है"), + ("no_desktop_text_tip", "कृपया Linux डेस्कटॉप इंस्टॉल करें"), + ("No need to elevate", "एलीवेट करने की आवश्यकता नहीं है"), + ("System Sound", "सिस्टम साउंड"), + ("Default", "डिफ़ॉल्ट"), + ("New RDP", "नया RDP"), + ("Fingerprint", "फिंगरप्रिंट"), + ("Copy Fingerprint", "फिंगरप्रिंट कॉपी करें"), + ("no fingerprints", "कोई फिंगरप्रिंट नहीं"), + ("Select a peer", "एक पीयर (Peer) चुनें"), + ("Select peers", "पीयर्स चुनें"), + ("Plugins", "प्लगइन्स"), + ("Uninstall", "अनइंस्टॉल करें"), + ("Update", "अपडेट करें"), + ("Enable", "सक्षम करें"), + ("Disable", "अक्षम करें"), + ("Options", "विकल्प"), + ("resolution_original_tip", "मूल रिज़ॉल्यूशन"), + ("resolution_fit_local_tip", "स्थानीय स्क्रीन में फिट करें"), + ("resolution_custom_tip", "कस्टम रिज़ॉल्यूशन"), + ("Collapse toolbar", "टूलबार समेटें"), + ("Accept and Elevate", "स्वीकार करें और एलीवेट करें"), + ("accept_and_elevate_btn_tooltip", "कनेक्शन स्वीकार करें और UAC अनुमतियाँ मांगें।"), + ("clipboard_wait_response_timeout_tip", "क्लिपबोर्ड प्रतिक्रिया के लिए समय समाप्त हो गया।"), + ("Incoming connection", "आने वाला कनेक्शन"), + ("Outgoing connection", "जाने वाला कनेक्शन"), + ("Exit", "बाहर निकलें"), + ("Open", "खोलें"), + ("logout_tip", "क्या आप वाकई लॉगआउट करना चाहते हैं?"), + ("Service", "सेवा"), + ("Start", "शुरू करें"), + ("Stop", "रोकें"), + ("exceed_max_devices", "आप डिवाइस की अधिकतम सीमा को पार कर चुके हैं।"), + ("Sync with recent sessions", "हाल के सत्रों के साथ सिंक करें"), + ("Sort tags", "टैग क्रमबद्ध करें"), + ("Open connection in new tab", "नये टैब में कनेक्शन खोलें"), + ("Move tab to new window", "टैब को नयी विंडो में ले जाएं"), + ("Can not be empty", "खाली नहीं हो सकता"), + ("Already exists", "पहले से मौजूद है"), + ("Change Password", "पासवर्ड बदलें"), + ("Refresh Password", "पासवर्ड रिफ्रेश करें"), + ("ID", "ID"), + ("Grid View", "ग्रिड व्यू"), + ("List View", "लिस्ट व्यू"), + ("Select", "चुनें"), + ("Toggle Tags", "टैग टॉगल करें"), + ("pull_ab_failed_tip", "पता पुस्तिका अपडेट करने में विफल।"), + ("push_ab_failed_tip", "सर्वर पर पता पुस्तिका सिंक करने में विफल।"), + ("synced_peer_readded_tip", "हाल के सत्रों में मौजूद डिवाइस पता पुस्तिका में सिंक किए गए थे।"), + ("Change Color", "रंग बदलें"), + ("Primary Color", "प्राथमिक रंग"), + ("HSV Color", "HSV रंग"), + ("Installation Successful!", "इंस्टॉलेशन सफल रहा!"), + ("Installation failed!", "इंस्टॉलेशन विफल रहा!"), + ("Reverse mouse wheel", "माउस व्हील उल्टा करें"), + ("{} sessions", "{} सत्र"), + ("scam_title", "धोखाधड़ी की चेतावनी!"), + ("scam_text1", "यदि आप किसी ऐसे व्यक्ति से बात कर रहे हैं जिसे आप नहीं जानते और जिसने आपसे RustDesk उपयोग करने को कहा है, तो तुरंत डिस्कनेक्ट कर दें।"), + ("scam_text2", "यह एक घोटाला हो सकता है। अपना आईडी या पासवर्ड किसी को न दें।"), + ("Don't show again", "दोबारा न दिखाएं"), + ("I Agree", "मैं सहमत हूँ"), + ("Decline", "अस्वीकार करें"), + ("Timeout in minutes", "मिनटों में टाइमआउट"), + ("auto_disconnect_option_tip", "निष्क्रियता पर स्वचालित रूप से डिस्कनेक्ट करें"), + ("Connection failed due to inactivity", "निष्क्रियता के कारण कनेक्शन विफल रहा"), + ("Check for software update on startup", "स्टार्टअप पर सॉफ़्टवेयर अपडेट की जांच करें"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "RustDesk सर्वर प्रो को संस्करण {} में अपग्रेड करें"), + ("pull_group_failed_tip", "समूह खींचने (Pull) में विफल"), + ("Filter by intersection", "इंटरसेक्शन द्वारा फ़िल्टर करें"), + ("Remove wallpaper during incoming sessions", "आने वाले सत्रों के दौरान वॉलपेपर हटा दें"), + ("Test", "परीक्षण"), + ("display_is_plugged_out_msg", "डिस्प्ले हटा दिया गया है।"), + ("No displays", "कोई डिस्प्ले नहीं"), + ("Open in new window", "नयी विंडो में खोलें"), + ("Show displays as individual windows", "डिस्प्ले को व्यक्तिगत विंडो के रूप में दिखाएं"), + ("Use all my displays for the remote session", "रिमोट सत्र के लिए मेरे सभी डिस्प्ले का उपयोग करें"), + ("selinux_tip", "डिवाइस पर SELinux सक्षम है।"), + ("Change view", "व्यू बदलें"), + ("Big tiles", "बड़ी टाइलें"), + ("Small tiles", "छोटी टाइलें"), + ("List", "लिस्ट"), + ("Virtual display", "वर्चुअल डिस्प्ले"), + ("Plug out all", "सभी अनप्लग करें"), + ("True color (4:4:4)", "सच्चा रंग (4:4:4)"), + ("Enable blocking user input", "उपयोगकर्ता इनपुट को ब्लॉक करना सक्षम करें"), + ("id_input_tip", "आप ID, उपनाम (Alias) या IP पता दर्ज कर सकते हैं।"), + ("privacy_mode_impl_mag_tip", "मैग्निफायर (Magnifier) गोपनीयता मोड"), + ("privacy_mode_impl_virtual_display_tip", "वर्चुअल डिस्प्ले गोपनीयता मोड"), + ("Enter privacy mode", "गोपनीयता मोड में प्रवेश करें"), + ("Exit privacy mode", "गोपनीयता मोड से बाहर निकलें"), + ("idd_not_support_under_win10_2004_tip", "वर्चुअल डिस्प्ले Windows 10 संस्करण 2004 या उच्चतर पर समर्थित है।"), + ("input_source_1_tip", "इनपुट स्रोत 1"), + ("input_source_2_tip", "इनपुट स्रोत 2"), + ("Swap control-command key", "Control और Command कुंजियों को बदलें"), + ("swap-left-right-mouse", "बाएं और दाएं माउस बटन को बदलें"), + ("2FA code", "2FA कोड"), + ("More", "अधिक"), + ("enable-2fa-title", "द्वि-कारक प्रमाणीकरण (2FA) सक्षम करें"), + ("enable-2fa-desc", "कृपया अपना ऑथेंटिकेटर ऐप सेट करें।"), + ("wrong-2fa-code", "गलत 2FA कोड।"), + ("enter-2fa-title", "2FA कोड दर्ज करें"), + ("Email verification code must be 6 characters.", "ईमेल सत्यापन कोड 6 अक्षरों का होना चाहिए।"), + ("2FA code must be 6 digits.", "2FA कोड 6 अंकों का होना चाहिए।"), + ("Multiple Windows sessions found", "एकाधिक Windows सत्र मिले"), + ("Please select the session you want to connect to", "कृपया वह सत्र चुनें जिससे आप जुड़ना चाहते हैं"), + ("powered_by_me", "मेरे द्वारा संचालित"), + ("outgoing_only_desk_tip", "यह केवल आउटगोइंग मोड है"), + ("preset_password_warning", "सुरक्षा के लिए, कृपया डिफ़ॉल्ट पासवर्ड बदलें।"), + ("Security Alert", "सुरक्षा चेतावनी"), + ("My address book", "मेरी पता पुस्तिका"), + ("Personal", "व्यक्तिगत"), + ("Owner", "स्वामी"), + ("Set shared password", "साझा पासवर्ड सेट करें"), + ("Exist in", "इसमें मौजूद है"), + ("Read-only", "केवल पढ़ने के लिए"), + ("Read/Write", "पढ़ना/लिखना"), + ("Full Control", "पूर्ण नियंत्रण"), + ("share_warning_tip", "सावधानी: आप अपना एक्सेस साझा कर रहे हैं।"), + ("Everyone", "हर कोई"), + ("ab_web_console_tip", "वेब कंसोल पता पुस्तिका"), + ("allow-only-conn-window-open-tip", "केवल तभी कनेक्शन की अनुमति दें जब RustDesk विंडो खुली हो"), + ("no_need_privacy_mode_no_physical_displays_tip", "कोई भौतिक डिस्प्ले नहीं मिला, गोपनीयता मोड की आवश्यकता नहीं है।"), + ("Follow remote cursor", "रिमोट कर्सर का पालन करें"), + ("Follow remote window focus", "रिमोट विंडो फोकस का पालन करें"), + ("default_proxy_tip", "डिफ़ॉल्ट प्रॉक्सी सेटिंग"), + ("no_audio_input_device_tip", "कोई ऑडियो इनपुट डिवाइस नहीं मिला।"), + ("Incoming", "आने वाली"), + ("Outgoing", "जाने वाली"), + ("Clear Wayland screen selection", "Wayland स्क्रीन चयन साफ़ करें"), + ("clear_Wayland_screen_selection_tip", "Wayland के स्क्रीन चयन को रीसेट करें।"), + ("confirm_clear_Wayland_screen_selection_tip", "क्या आप वाकई स्क्रीन चयन साफ़ करना चाहते हैं?"), + ("android_new_voice_call_tip", "नया वॉयस कॉल अनुरोध"), + ("texture_render_tip", "टेक्सचर रेंडरिंग का उपयोग करें"), + ("Use texture rendering", "टेक्सचर रेंडरिंग का उपयोग करें"), + ("Floating window", "फ्लोटिंग विंडो"), + ("floating_window_tip", "बैकग्राउंड में रहने के दौरान RustDesk को दिखाएं"), + ("Keep screen on", "स्क्रीन चालू रखें"), + ("Never", "कभी नहीं"), + ("During controlled", "नियंत्रण के दौरान"), + ("During service is on", "जब सेवा चालू हो"), + ("Capture screen using DirectX", "DirectX का उपयोग करके स्क्रीन कैप्चर करें"), + ("Back", "पीछे"), + ("Apps", "ऐप्स"), + ("Volume up", "आवाज़ बढ़ाएं"), + ("Volume down", "आवाज़ कम करें"), + ("Power", "पावर"), + ("Telegram bot", "Telegram बॉट"), + ("enable-bot-tip", "सूचनाओं के लिए बोट सक्षम करें"), + ("enable-bot-desc", "निर्देशों के लिए हमारे टेलीग्राम बोट को देखें।"), + ("cancel-2fa-confirm-tip", "क्या आप वाकई 2FA रद्द करना चाहते हैं?"), + ("cancel-bot-confirm-tip", "क्या आप वाकई बोट रद्द करना चाहते हैं?"), + ("About RustDesk", "RustDesk के बारे में"), + ("Send clipboard keystrokes", "क्लिपबोर्ड कीस्ट्रोक्स भेजें"), + ("network_error_tip", "नेटवर्क कनेक्शन त्रुटि, कृपया पुनः प्रयास करें।"), + ("Unlock with PIN", "PIN से अनलॉक करें"), + ("Requires at least {} characters", "कम से कम {} अक्षरों की आवश्यकता है"), + ("Wrong PIN", "गलत PIN"), + ("Set PIN", "PIN सेट करें"), + ("Enable trusted devices", "विश्वसनीय डिवाइस सक्षम करें"), + ("Manage trusted devices", "विश्वसनीय डिवाइस प्रबंधित करें"), + ("Platform", "प्लेटफ़ॉर्म"), + ("Days remaining", "शेष दिन"), + ("enable-trusted-devices-tip", "केवल विश्वसनीय डिवाइस ही पासवर्ड के बिना जुड़ सकते हैं"), + ("Parent directory", "पैरेंट निर्देशिका"), + ("Resume", "फिर से शुरू करें"), + ("Invalid file name", "अमान्य फ़ाइल नाम"), + ("one-way-file-transfer-tip", "केवल एकतरफा फ़ाइल स्थानांतरण की अनुमति है"), + ("Authentication Required", "प्रमाणीकरण आवश्यक"), + ("Authenticate", "प्रमाणित करें"), + ("web_id_input_tip", "रिमोट आईडी दर्ज करें"), + ("Download", "डाउनलोड करें"), + ("Upload folder", "फ़ोल्डर अपलोड करें"), + ("Upload files", "फाइलें अपलोड करें"), + ("Clipboard is synchronized", "क्लिपबोर्ड सिंक हो गया है"), + ("Update client clipboard", "क्लाइंट क्लिपबोर्ड अपडेट करें"), + ("Untagged", "बिना टैग वाला"), + ("new-version-of-{}-tip", "{} का नया संस्करण उपलब्ध है"), + ("Accessible devices", "सुलभ डिवाइस"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"), + ("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"), + ("Printer", "प्रिंटर"), + ("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"), + ("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"), + ("printer-{}-not-installed-tip", "प्रिंटर {} इंस्टॉल नहीं है।"), + ("printer-{}-ready-tip", "प्रिंटर {} तैयार है।"), + ("Install {} Printer", "{} प्रिंटर इंस्टॉल करें"), + ("Outgoing Print Jobs", "आउटगोइंग प्रिंट कार्य"), + ("Incoming Print Jobs", "इनकमिंग प्रिंट कार्य"), + ("Incoming Print Job", "इनकमिंग प्रिंट कार्य"), + ("use-the-default-printer-tip", "डिफ़ॉल्ट प्रिंटर का उपयोग करें"), + ("use-the-selected-printer-tip", "चयनित प्रिंटर का उपयोग करें"), + ("auto-print-tip", "स्वचालित रूप से प्रिंट करें"), + ("print-incoming-job-confirm-tip", "प्रिंट कार्य स्वीकार करने से पहले पुष्टि करें"), + ("remote-printing-disallowed-tile-tip", "रिमोट प्रिंटिंग की अनुमति नहीं है"), + ("remote-printing-disallowed-text-tip", "कृपया सेटिंग्स में रिमोट प्रिंटिंग सक्षम करें।"), + ("save-settings-tip", "सेटिंग्स सुरक्षित करें"), + ("dont-show-again-tip", "दोबारा न दिखाएं"), + ("Take screenshot", "स्क्रीनशॉट लें"), + ("Taking screenshot", "स्क्रीनशॉट लिया जा रहा है"), + ("screenshot-merged-screen-not-supported-tip", "मर्ज की गई स्क्रीन के स्क्रीनशॉट समर्थित नहीं हैं।"), + ("screenshot-action-tip", "स्क्रीनशॉट लेने के बाद की कार्रवाई"), + ("Save as", "इस रूप में सहेजें"), + ("Copy to clipboard", "क्लिपबोर्ड पर कॉपी करें"), + ("Enable remote printer", "रिमोट प्रिंटर सक्षम करें"), + ("Downloading {}", "{} डाउनलोड हो रहा है"), + ("{} Update", "{} अपडेट"), + ("{}-to-update-tip", "अपडेट करने के लिए {}"), + ("download-new-version-failed-tip", "नया संस्करण डाउनलोड करने में विफल।"), + ("Auto update", "ऑटो अपडेट"), + ("update-failed-check-msi-tip", "अपडेट विफल, कृपया MSI फ़ाइल की जांच करें।"), + ("websocket_tip", "यदि पोर्ट ब्लॉक हैं, तो WebSocket का उपयोग करें।"), + ("Use WebSocket", "WebSocket का उपयोग करें"), + ("Trackpad speed", "ट्रैकपैड गति"), + ("Default trackpad speed", "डिफ़ॉल्ट ट्रैकपैड गति"), + ("Numeric one-time password", "संख्यात्मक वन-टाइम पासवर्ड"), + ("Enable IPv6 P2P connection", "IPv6 P2P कनेक्शन सक्षम करें"), + ("Enable UDP hole punching", "UDP होल पंचिंग सक्षम करें"), + ("View camera", "कैमरा देखें"), + ("Enable camera", "कैमरा सक्षम करें"), + ("No cameras", "कोई कैमरा नहीं मिला"), + ("view_camera_unsupported_tip", "रिमोट कैमरा समर्थित नहीं है।"), + ("Terminal", "टर्मिनल"), + ("Enable terminal", "टर्मिनल सक्षम करें"), + ("New tab", "नया टैब"), + ("Keep terminal sessions on disconnect", "डिस्कनेक्ट होने पर टर्मिनल सत्र चालू रखें"), + ("Terminal (Run as administrator)", "टर्मिनल (प्रशासक के रूप में चलाएं)"), + ("terminal-admin-login-tip", "प्रशासक लॉगिन आवश्यक है।"), + ("Failed to get user token.", "उपयोगकर्ता टोकन प्राप्त करने में विफल।"), + ("Incorrect username or password.", "गलत उपयोगकर्ता नाम या पासवर्ड।"), + ("The user is not an administrator.", "उपयोगकर्ता प्रशासक नहीं है।"), + ("Failed to check if the user is an administrator.", "जांचने में विफल कि क्या उपयोगकर्ता व्यवस्थापक है।"), + ("Supported only in the installed version.", "केवल इंस्टॉल किए गए संस्करण में समर्थित।"), + ("elevation_username_tip", "प्रशासक उपयोगकर्ता नाम दर्ज करें"), + ("Preparing for installation ...", "स्थापना की तैयारी..."), + ("Show my cursor", "मेरा कर्सर दिखाएं"), + ("Scale custom", "कस्टम पैमाना"), + ("Custom scale slider", "कस्टम स्केल स्लाइडर"), + ("Decrease", "घटाएं"), + ("Increase", "बढ़ाएं"), + ("Show virtual mouse", "वर्चुअल माउस दिखाएं"), + ("Virtual mouse size", "वर्चुअल माउस का आकार"), + ("Small", "छोटा"), + ("Large", "बड़ा"), + ("Show virtual joystick", "वर्चुअल जॉयस्टिक दिखाएं"), + ("Edit note", "नोट संपादित करें"), + ("Alias", "उपनाम (Alias)"), + ("ScrollEdge", "किनारे से स्क्रॉल"), + ("Allow insecure TLS fallback", "असुरक्षित TLS फ़ालबैक की अनुमति दें"), + ("allow-insecure-tls-fallback-tip", "पुराने सर्वर कनेक्शन के लिए उपयोग करें।"), + ("Disable UDP", "UDP अक्षम करें"), + ("disable-udp-tip", "कनेक्शन समस्याओं के लिए UDP बंद करें।"), + ("server-oss-not-support-tip", "OSS सर्वर इसका समर्थन नहीं करता।"), + ("input note here", "यहाँ नोट दर्ज करें"), + ("note-at-conn-end-tip", "कनेक्शन के अंत में नोट दिखाएं"), + ("Show terminal extra keys", "टर्मिनल की अतिरिक्त कुंजियाँ दिखाएं"), + ("Relative mouse mode", "सापेक्ष (Relative) माउस मोड"), + ("rel-mouse-not-supported-peer-tip", "रिमोट साइड पर समर्थित नहीं है।"), + ("rel-mouse-not-ready-tip", "तैयार नहीं है।"), + ("rel-mouse-lock-failed-tip", "माउस लॉक विफल।"), + ("rel-mouse-exit-{}-tip", "बाहर निकलने के लिए {} दबाएं"), + ("rel-mouse-permission-lost-tip", "अनुमति खो गई।"), + ("Changelog", "परिवर्तन सूची (Changelog)"), + ("keep-awake-during-outgoing-sessions-label", "आउटगोइंग सत्र के दौरान जागते रहें"), + ("keep-awake-during-incoming-sessions-label", "इनकमिंग सत्र के दौरान जागते रहें"), + ("Continue with {}", "{} के साथ जारी रखें"), + ("Display Name", "प्रदर्शित नाम"), + ("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"), + ("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/hr.rs b/vendor/rustdesk/src/lang/hr.rs new file mode 100644 index 0000000..505b01d --- /dev/null +++ b/vendor/rustdesk/src/lang/hr.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Vaša radna površina"), + ("desk_tip", "Vašoj radnoj površini se može pristupiti ovim ID i lozinkom."), + ("Password", "Lozinka"), + ("Ready", "Spremno"), + ("Established", "Uspostavljeno"), + ("connecting_status", "Spajanje na RustDesk mrežu..."), + ("Enable service", "Dopusti servis"), + ("Start service", "Pokreni servis"), + ("Service is running", "Servis je pokrenut"), + ("Service is not running", "Servis nije pokrenut"), + ("not_ready_status", "Nije spremno. Provjerite vezu."), + ("Control Remote Desktop", "Upravljanje udaljenom radnom površinom"), + ("Transfer file", "Prijenos datoteke"), + ("Connect", "Spajanje"), + ("Recent sessions", "Nedavne sesije"), + ("Address book", "Adresar"), + ("Confirmation", "Potvrda"), + ("TCP tunneling", "TCP tunel"), + ("Remove", "Ukloni"), + ("Refresh random password", "Osvježi slučajnu lozinku"), + ("Set your own password", "Postavi lozinku"), + ("Enable keyboard/mouse", "Dopusti tipkovnicu/miša"), + ("Enable clipboard", "Dopusti međuspremnik"), + ("Enable file transfer", "Dopusti prijenos datoteka"), + ("Enable TCP tunneling", "Dopusti TCP tunel"), + ("IP Whitelisting", "IP pouzdana lista"), + ("ID/Relay Server", "ID/Posredni poslužitelj"), + ("Import server config", "Uvoz konfiguracije poslužitelja"), + ("Export Server Config", "Izvoz konfiguracije poslužitelja"), + ("Import server configuration successfully", "Uvoz konfiguracije poslužitelja uspješan"), + ("Export server configuration successfully", "Izvoz konfiguracije poslužitelja uspješan"), + ("Invalid server configuration", "Pogrešna konfiguracija poslužitelja"), + ("Clipboard is empty", "Međuspremnik je prazan"), + ("Stop service", "Zaustavi servis"), + ("Change ID", "Promijeni ID"), + ("Your new ID", "Vaš novi ID"), + ("length %min% to %max%", "duljina %min% do %max%"), + ("starts with a letter", "Počinje slovom"), + ("allowed characters", "Dopušteni znakovi"), + ("id_change_tip", "Dopušteni su samo a-z, A-Z, 0-9, - (dash) i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Duljina je od 6 do 16."), + ("Website", "Web stranica"), + ("About", "O programu"), + ("Slogan_tip", "Stvoren srcem u ovom kaotičnom svijetu!"), + ("Privacy Statement", "Izjava o privatnosti"), + ("Mute", "Utišaj"), + ("Build Date", "Datum izrade"), + ("Version", "Verzija"), + ("Home", "Početno"), + ("Audio Input", "Audio ulaz"), + ("Enhancements", "Proširenja"), + ("Hardware Codec", "Hardverski kodek"), + ("Adaptive bitrate", "Prilagodljiva gustoća podataka"), + ("ID Server", "ID poslužitelja"), + ("Relay Server", "Posredni poslužitelj"), + ("API Server", "API poslužitelj"), + ("invalid_http", "Treba početi sa http:// ili https://"), + ("Invalid IP", "Nevažeća IP"), + ("Invalid format", "Pogrešan format"), + ("server_not_support", "Poslužitelj još uvijek ne podržava"), + ("Not available", "Nije dostupno"), + ("Too frequent", "Previše često"), + ("Cancel", "Otkaži"), + ("Skip", "Preskoči"), + ("Close", "Zatvori"), + ("Retry", "Ponovi"), + ("OK", "Ok"), + ("Password Required", "Potrebna lozinka"), + ("Please enter your password", "Molimo unesite svoju lozinku"), + ("Remember password", "Zapamti lozinku"), + ("Wrong Password", "Pogrešna lozinka"), + ("Do you want to enter again?", "Želite li ponovo unijeti lozinku?"), + ("Connection Error", "Greška u spajanju"), + ("Error", "Greška"), + ("Reset by the peer", "Prekinuto sa druge strane"), + ("Connecting...", "Povezivanje..."), + ("Connection in progress. Please wait.", "Povezivanje u tijeku. Molimo pričekajte."), + ("Please try 1 minute later", "Pokušajte minutu kasnije"), + ("Login Error", "Greška kod prijave"), + ("Successful", "Uspješno"), + ("Connected, waiting for image...", "Spojeno, pričekajte sliku..."), + ("Name", "Ime"), + ("Type", "Vrsta"), + ("Modified", "Izmijenjeno"), + ("Size", "Veličina"), + ("Show Hidden Files", "Prikaži skrivene datoteke"), + ("Receive", "Prijem"), + ("Send", "Slanje"), + ("Refresh File", "Osvježi datoteku"), + ("Local", "Lokalno"), + ("Remote", "Udaljeno"), + ("Remote Computer", "Udaljeno računalo"), + ("Local Computer", "Lokalno računalo"), + ("Confirm Delete", "Potvrdite brisanje"), + ("Delete", "Brisanje"), + ("Properties", "Svojstva"), + ("Multi Select", "Višestruki odabir"), + ("Select All", "Odaberi sve"), + ("Unselect All", "Poništi odabir"), + ("Empty Directory", "Prazna mapa"), + ("Not an empty directory", "Nije prazna mapa"), + ("Are you sure you want to delete this file?", "Jeste sigurni da želite obrisati ovu datoteku?"), + ("Are you sure you want to delete this empty directory?", "Jeste sigurni da želite obrisati ovu praznu mapu?"), + ("Are you sure you want to delete the file of this directory?", "Jeste sigurni da želite obrisati datoteku u ovoj mapi?"), + ("Do this for all conflicts", "Učinite to za sve sukobe"), + ("This is irreversible!", "Ovo je nepovratno"), + ("Deleting", "Brisanje"), + ("files", "datoteke"), + ("Waiting", "Čekanje"), + ("Finished", "Završeno"), + ("Speed", "Brzina"), + ("Custom Image Quality", "Korisnička kvaliteta slike"), + ("Privacy mode", "Način privatnosti"), + ("Block user input", "Blokiraj korisnikov unos"), + ("Unblock user input", "Odblokiraj korisnikov unos"), + ("Adjust Window", "Podesi prozor"), + ("Original", "Izvornik"), + ("Shrink", "Skupi"), + ("Stretch", "Raširi"), + ("Scrollbar", "Linija pomaka"), + ("ScrollAuto", "Autom. pomak"), + ("Good image quality", "Dobra kvaliteta slike"), + ("Balanced", "Balansirano"), + ("Optimize reaction time", "Optimizirano vrijeme reakcije"), + ("Custom", "Korisničko"), + ("Show remote cursor", "Prikaži udaljeni kursor"), + ("Show quality monitor", "Prikaži kvalitetu monitora"), + ("Disable clipboard", "Zabrani međuspremnik"), + ("Lock after session end", "Zaključaj po završetku sesije"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del umetanje"), + ("Insert Lock", "Zaključaj umetanje"), + ("Refresh", "Osvježi"), + ("ID does not exist", "ID ne postoji"), + ("Failed to connect to rendezvous server", "Greška u spajanju na poslužitelj za povezivanje"), + ("Please try later", "Molimo pokušajte kasnije"), + ("Remote desktop is offline", "Udaljeni zaslon je isključen"), + ("Key mismatch", "Pogrešan ključ"), + ("Timeout", "Isteklo vrijeme"), + ("Failed to connect to relay server", "Greška u spajanju na posredni poslužitelj"), + ("Failed to connect via rendezvous server", "Greška u spajanju preko poslužitelja za povezivanje"), + ("Failed to connect via relay server", "Greška u spajanju preko posrednog poslužitelja"), + ("Failed to make direct connection to remote desktop", "Greška u direktnom spajanju na udaljenu radnu površinu"), + ("Set Password", "Postavi lozinku"), + ("OS Password", "Lozinka OS-a"), + ("install_tip", "Zbog UAC-a RustDesk ne može u nekim slučajevima raditi pravilno. Da biste prevazišli UAC, kliknite na tipku ispod da instalirate RustDesk na sustav."), + ("Click to upgrade", "Klik za nadogradnju"), + ("Configure", "Konfiguracija"), + ("config_acc", "Da biste daljinski kontrolirali radnu površinu, RustDesk-u trebate dodijeliti prava za \"Pristupačnost\"."), + ("config_screen", "Da biste daljinski pristupili radnoj površini, RustDesk-u trebate dodijeliti prava za \"Snimanje zaslona\"."), + ("Installing ...", "Instaliranje..."), + ("Install", "Instaliraj"), + ("Installation", "Instalacija"), + ("Installation Path", "Putanja za instalaciju"), + ("Create start menu shortcuts", "Stvori prečace u izborniku"), + ("Create desktop icon", "Stvori ikonu na radnoj površini"), + ("agreement_tip", "Pokretanjem instalacije prihvaćate ugovor o licenciranju."), + ("Accept and Install", "Prihvati i instaliraj"), + ("End-user license agreement", "Ugovor sa krajnjim korisnikom"), + ("Generating ...", "Generiranje..."), + ("Your installation is lower version.", "Vaša instalacija je niže verzije"), + ("not_close_tcp_tip", "Ne zatvarajte ovaj prozor dok koristite tunel"), + ("Listening ...", "Na slušanju..."), + ("Remote Host", "Adresa udaljenog uređaja"), + ("Remote Port", "Udaljeni port"), + ("Action", "Postupak"), + ("Add", "Dodaj"), + ("Local Port", "Lokalni port"), + ("Local Address", "Lokalna adresa"), + ("Change Local Port", "Promijeni lokalni port"), + ("setup_server_tip", "Za brže spajanje, molimo da koristite vlastiti poslužitelj"), + ("Too short, at least 6 characters.", "Prekratko, najmanje 6 znakova."), + ("The confirmation is not identical.", "Potvrda nije identična"), + ("Permissions", "Dopuštenja"), + ("Accept", "Prihvati"), + ("Dismiss", "Odbaci"), + ("Disconnect", "Prekini vezu"), + ("Enable file copy and paste", "Dopusti kopiranje i lijepljenje datoteka"), + ("Connected", "Spojeno"), + ("Direct and encrypted connection", "Izravna i kriptirana veza"), + ("Relayed and encrypted connection", "Posredna i kriptirana veza"), + ("Direct and unencrypted connection", "Izravna i nekriptirana veza"), + ("Relayed and unencrypted connection", "Posredna i nekriptirana veza"), + ("Enter Remote ID", "Unesite ID udaljenog uređaja"), + ("Enter your password", "Unesite svoju lozinku"), + ("Logging in...", "Prijava..."), + ("Enable RDP session sharing", "Dopusti dijeljenje RDP sesije"), + ("Auto Login", "Autom. prijava (Vrijedi samo ako je postavljeno \"Zaključavanje nakon završetka sesije\")"), + ("Enable direct IP access", "Dopusti izravan pristup preko IP adrese"), + ("Rename", "Preimenuj"), + ("Space", "Prazno"), + ("Create desktop shortcut", "Stvori prečac na radnoj površini"), + ("Change Path", "Promijeni putanju"), + ("Create Folder", "Svori mapu"), + ("Please enter the folder name", "Unesite ime mape"), + ("Fix it", "Popravi"), + ("Warning", "Upozorenje"), + ("Login screen using Wayland is not supported", "Zaslon za prijavu koji koristi Wayland nije podržan"), + ("Reboot required", "Potrebano je ponovno pokretanje"), + ("Unsupported display server", "Nepodržani poslužitelj za prikaz"), + ("x11 expected", "x11 očekivan"), + ("Port", "Port"), + ("Settings", "Postavke"), + ("Username", "Korisničko ime"), + ("Invalid port", "Pogrešan port"), + ("Closed manually by the peer", "Klijent ručno prekinuo vezu"), + ("Enable remote configuration modification", "Dopusti izmjenu udaljene konfiguracije"), + ("Run without install", "Pokreni bez instalacije"), + ("Connect via relay", "Povezivanje preko relejnog poslužitelja"), + ("Always connect via relay", "Uvek se poveži preko relejnog poslužitelja"), + ("whitelist_tip", "Mogu mi pristupiti samo dozvoljene IP adrese"), + ("Login", "Prijava"), + ("Verify", "Potvrdi"), + ("Remember me", "Zapamti me"), + ("Trust this device", "Vjeruj ovom uređaju"), + ("Verification code", "Kontrolni kôd"), + ("verification_tip", "Kontrolni kôd je poslan na registriranu adresu e-pošte, unesite ga i nastavite s prijavom."), + ("Logout", "Odjava"), + ("Tags", "Oznake"), + ("Search ID", "Traži ID"), + ("whitelist_sep", "Odvojeno zarezima, točka zarezima, praznim mjestima ili novim redovima"), + ("Add ID", "Dodaj ID"), + ("Add Tag", "Dodaj oznaku"), + ("Unselect all tags", "Odznači sve oznake"), + ("Network error", "Greška na mreži"), + ("Username missed", "Korisničko ime propušteno"), + ("Password missed", "Lozinka propuštena"), + ("Wrong credentials", "Pogrešno korisničko ime ili lozinka"), + ("The verification code is incorrect or has expired", "Kôd za provjeru nije točan ili je istekao"), + ("Edit Tag", "Izmjenite oznaku"), + ("Forget Password", "Zaboravi lozinku"), + ("Favorites", "Favoriti"), + ("Add to Favorites", "Dodaj u favorite"), + ("Remove from Favorites", "Ukloni iz favorita"), + ("Empty", "Prazno"), + ("Invalid folder name", "Nevažeći naziv mape"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", ""), + ("Discovered", "Otkriveno"), + ("install_daemon_tip", "Servis sustava mora biti instaliran ako se želi pokrenuti pri pokretanju sustava."), + ("Remote ID", "Udaljeni ID"), + ("Paste", "Zalijepi"), + ("Paste here?", "Zalijepi ovdje?"), + ("Are you sure to close the connection?", "Jeste li sigurni da želite zatvoriti vezu?"), + ("Download new version", "Preuzmi novu verziju"), + ("Touch mode", "Način rada na dodir"), + ("Mouse mode", "Način rada miša"), + ("One-Finger Tap", "Dodir jednim prstom"), + ("Left Mouse", "Lijeva tipka miša"), + ("One-Long Tap", "Jedan dugi dodir"), + ("Two-Finger Tap", "Dodir s dva prsta"), + ("Right Mouse", "Desna tipka miša"), + ("One-Finger Move", "Pomak jednim prstom"), + ("Double Tap & Move", "Dupli dodir i pomak"), + ("Mouse Drag", "Povlačenje mišem"), + ("Three-Finger vertically", "Sa tri prsta okomito"), + ("Mouse Wheel", "Kotačić miša"), + ("Two-Finger Move", "Pomak s dva prsta"), + ("Canvas Move", "Pomak pozadine"), + ("Pinch to Zoom", "Stisnite prste za zumiranje"), + ("Canvas Zoom", "Zumiranje pozadine"), + ("Reset canvas", "Resetiraj pozadinu"), + ("No permission of file transfer", "Nemate pravo prijenosa datoteka"), + ("Note", "Bilješka"), + ("Connection", "Povezivanje"), + ("Share screen", "Podijeli zaslon"), + ("Chat", "Dopisivanje"), + ("Total", "Ukupno"), + ("items", "stavki"), + ("Selected", "Odabrano"), + ("Screen Capture", "Snimanje zaslona"), + ("Input Control", "Kontrola unosa"), + ("Audio Capture", "Snimanje zvuka"), + ("Do you accept?", "Prihvaćate li?"), + ("Open System Setting", "Postavke otvorenog sustava"), + ("How to get Android input permission?", "Kako dobiti pristup za unos na Androidu?"), + ("android_input_permission_tip1", "Da bi ste daljinski uređaj kontrolirali vašim Android uređajem preko miša ili na dodir, trebate dopustiti RustDesk-u da koristi servis \"Pristupačnost\"."), + ("android_input_permission_tip2", "Molimo prijeđite na sljedeću stranicu podešavanja sustava, pronađite i unesite [Instalirani servisi], uključite servis [RustDesk Input]."), + ("android_new_connection_tip", "Primljen je novi zahtjev za upravljanje, koji želi upravljati vašim uređajem."), + ("android_service_will_start_tip", "Omogućavanje \"Snimanje zaslona\" automatski će pokrenuti servis, dopuštajući drugim uređajima da zahtjevaju spajanje na vaš uređaj."), + ("android_stop_service_tip", "Zatvaranje servisa automatski će zatvoriti sve uspostavljene veze."), + ("android_version_audio_tip", "Trenutna Android verzija ne podržava audio snimanje, molimo nadogradite na Android 10 ili veći."), + ("android_start_service_tip", "Pritisnite [Pokreni servis] ili omogućite dopuštenje [Snimanje zaslona] za pokretanje usluge dijeljenja zaslona."), + ("android_permission_may_not_change_tip", "Dopuštenja za uspostavljene veze mogu se promijeniti tek nakon ponovnog povezivanja."), + ("Account", "Račun"), + ("Overwrite", "Prepiši"), + ("This file exists, skip or overwrite this file?", "Ova datoteka postoji, preskoči ju ili prepiši?"), + ("Quit", "Izlaz"), + ("Help", "Pomoć"), + ("Failed", "Neuspješno"), + ("Succeeded", "Uspešno"), + ("Someone turns on privacy mode, exit", "Netko je uključio način privatnosti, izlaz."), + ("Unsupported", "Nepodržano"), + ("Peer denied", "Klijent zabranjen"), + ("Please install plugins", "Molimo instalirajte dodatke"), + ("Peer exit", "Klijent je izašao"), + ("Failed to turn off", "Greška kod isključenja"), + ("Turned off", "Isključeno"), + ("Language", "Jezik"), + ("Keep RustDesk background service", "Zadrži RustDesk kao pozadinski servis"), + ("Ignore Battery Optimizations", "Zanemari optimizaciju baterije"), + ("android_open_battery_optimizations_tip", "Ako želite onemogućiti ovu funkciju, molimo idite na sljedeću stranicu za podešavanje RustDesk aplikacije, pronađite i uđite u [Baterija], onemogućite [Neograničeno]"), + ("Start on boot", "Pokreni pri pokretanju sustava"), + ("Start the screen sharing service on boot, requires special permissions", "Za pokretanje usluge dijeljenja zaslona pri pokretanju sustava potrebna su posebna dopuštenja"), + ("Connection not allowed", "Veza nije dopuštena"), + ("Legacy mode", "Naslijeđeni način"), + ("Map mode", "Način mapiranja"), + ("Translate mode", "Način prevođenja"), + ("Use permanent password", "Koristi trajnu lozinku"), + ("Use both passwords", "Koristi obje lozinke"), + ("Set permanent password", "Postavi trajnu lozinku"), + ("Enable remote restart", "Omogući daljinsko ponovno pokretanje"), + ("Restart remote device", "Ponovno pokreni daljinski uređaj"), + ("Are you sure you want to restart", "Jeste li sigurni da želite ponovno pokretanje"), + ("Restarting remote device", "Ponovno pokretanje daljinskog uređaja"), + ("remote_restarting_tip", "Udaljeni uređaj se ponovno pokreće, molimo zatvorite ovu poruku i ponovo se kasnije povežite trajnom lozinkom"), + ("Copied", "Kopirano"), + ("Exit Fullscreen", "Izlaz iz cijelog zaslona"), + ("Fullscreen", "Cijeli zaslon"), + ("Mobile Actions", "Mobilne akcije"), + ("Select Monitor", "Odabir monitora"), + ("Control Actions", "Kontrolni postupci"), + ("Display Settings", "Postavke prikaza"), + ("Ratio", "Odnos"), + ("Image Quality", "Kvaliteta slike"), + ("Scroll Style", "Način pomaka"), + ("Show Toolbar", "Prikaži alatnu traku"), + ("Hide Toolbar", "Sakrij alatnu traku"), + ("Direct Connection", "Izravna veza"), + ("Relay Connection", "Posredna veza"), + ("Secure Connection", "Sigurna veza"), + ("Insecure Connection", "Nesigurna veza"), + ("Scale original", "Skaliraj izvornik"), + ("Scale adaptive", "Prilagođeno skaliranje"), + ("General", "Općenito"), + ("Security", "Sigurnost"), + ("Theme", "Tema"), + ("Dark Theme", "Tamna tema"), + ("Light Theme", "Svjetla tema"), + ("Dark", "Tamna"), + ("Light", "Svjetla"), + ("Follow System", "Tema sutava"), + ("Enable hardware codec", "Omogući hardverski kodek"), + ("Unlock Security Settings", "Otključaj postavke sigurnosti"), + ("Enable audio", "Dopusti zvuk"), + ("Unlock Network Settings", "Otključaj postavke mreže"), + ("Server", "Poslužitelj"), + ("Direct IP Access", "Izravan IP pristup"), + ("Proxy", "Proxy"), + ("Apply", "Primijeni"), + ("Disconnect all devices?", "Odspojiti sve uređaje?"), + ("Clear", "Obriši"), + ("Audio Input Device", "Uređaj za ulaz zvuka"), + ("Use IP Whitelisting", "Koristi popis pouzdanih IP adresa"), + ("Network", "Mreža"), + ("Pin Toolbar", "Prikači alatnu traku"), + ("Unpin Toolbar", "Otkvači alatnu traku"), + ("Recording", "Snimanje"), + ("Directory", "Mapa"), + ("Automatically record incoming sessions", "Automatski snimi dolazne sesije"), + ("Automatically record outgoing sessions", ""), + ("Change", "Promijeni"), + ("Start session recording", "Započni snimanje sesije"), + ("Stop session recording", "Zaustavi snimanje sesije"), + ("Enable recording session", "Omogući snimanje sesije"), + ("Enable LAN discovery", "Omogući LAN otkrivanje"), + ("Deny LAN discovery", "Onemogući LAN otkrivanje"), + ("Write a message", "Napiši poruku"), + ("Prompt", "Spremno"), + ("Please wait for confirmation of UAC...", "Molimo pričekajte potvrdu UAC-a..."), + ("elevated_foreground_window_tip", "Tekući prozor udaljene radne površine zahtijeva veće privilegije za rad, tako da trenutno nije moguće koristiti miša i tipkovnicu. Možete zatražiti od udaljenog korisnika da minimizira aktivni prozor, ili kliknuti gumb za povećanje privilegija u prozoru za upravljanje vezom. Kako biste izbjegli ovaj problem, preporučujemo da instalirate softver na udaljeni uređaj."), + ("Disconnected", "Odspojeno"), + ("Other", "Ostalo"), + ("Confirm before closing multiple tabs", "Potvrda prije zatvaranja više kartica"), + ("Keyboard Settings", "Postavke tipkovnice"), + ("Full Access", "Potpuni pristup"), + ("Screen Share", "Dijeljenje zaslona"), + ("ubuntu-21-04-required", "Wayland zahtijeva Ubuntu verziju 21.04 ili višu"), + ("wayland-requires-higher-linux-version", "Wayland zahtijeva višu verziju Linux distribucije. Molimo isprobjate X11 ili promijenite OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Vidi"), + ("Please Select the screen to be shared(Operate on the peer side).", "Molimo odaberite zaslon koji će biti podijeljen (Za rad na strani klijenta)"), + ("Show RustDesk", "Prikaži RustDesk"), + ("This PC", "Ovo računalo"), + ("or", "ili"), + ("Elevate", "Izdigni"), + ("Zoom cursor", "Zumiraj kursor"), + ("Accept sessions via password", "Prihvati sesije preko lozinke"), + ("Accept sessions via click", "Prihvati sesije preko klika"), + ("Accept sessions via both", "Prihvati sesije preko oboje"), + ("Please wait for the remote side to accept your session request...", "Molimo pričekajte da udaljena strana prihvati vaš zahtjev za sesijom..."), + ("One-time Password", "Jednokratna lozinka"), + ("Use one-time password", "Koristi jednokratnu lozinku"), + ("One-time password length", "Duljina jednokratne lozinke"), + ("Request access to your device", "Zahtjev za pristup vašem uređaju"), + ("Hide connection management window", "Sakrij prozor za uređivanje veze"), + ("hide_cm_tip", "Skrivanje dozvoljeno samo prihvaćanjem sesije preko lozinke i korištenjem trajne lozinke"), + ("wayland_experiment_tip", "Podrška za Wayland je eksperimentalna, ako trebate pristup bez nadzora, koristite X11."), + ("Right click to select tabs", "Desni klik za odabir kartica"), + ("Skipped", "Preskočeno"), + ("Add to address book", "Dodaj u adresar"), + ("Group", "Grupa"), + ("Search", "Pretraga"), + ("Closed manually by web console", "Zatvoreno ručno pomoću web konzole"), + ("Local keyboard type", "Vrsta lokalne tipkovnice"), + ("Select local keyboard type", "Odabir lokalne vrste tipkovnice"), + ("software_render_tip", "Ako koristite Nvidia grafičku karticu na Linuxu i udaljeni prozor se zatvori odmah nakon povezivanja, prebacivanje na Nouveau upravljački program otvorenog kôda i odabir softverskog renderiranja može pomoći. Potrebno je ponovno pokretanje."), + ("Always use software rendering", "Uvijek koristite softversko renderiranje"), + ("config_input", "Za upravljanje udaljenom radnom površinom pomoću tipkovnice, morate dodijeliti RustDesku dopuštenje \"Nadzor unosa\"."), + ("config_microphone", "Da biste razgovarali na daljinu, morate dopustiti RustDesku \"Snimanje zvuka\"."), + ("request_elevation_tip", "Također možete tražiti podizanje ako je netko na drugoj strani."), + ("Wait", "Pričekaj"), + ("Elevation Error", "Pogreška povećanja"), + ("Ask the remote user for authentication", "Pitajte udaljenog korisnika za autentifikaciju"), + ("Choose this if the remote account is administrator", "Odaberite ovu opciju ako je udaljeni račun administrator"), + ("Transmit the username and password of administrator", "Prijenos administratorskog korisničkog imena i lozinke"), + ("still_click_uac_tip", "Još uvijek zahtijeva da udaljeni korisnik klikne OK u UAC prozoru pokrenutog RustDeska."), + ("Request Elevation", "Zahtjev za podizanje"), + ("wait_accept_uac_tip", "Pričekajte da udaljeni korisnik prihvati UAC dijaloški okvir."), + ("Elevate successfully", "Uspješno podizanje"), + ("uppercase", "velika slova"), + ("lowercase", "mala slova"), + ("digit", "brojka"), + ("special character", "poseban znak"), + ("length>=8", "duljina>=8"), + ("Weak", "Slabo"), + ("Medium", "Srednje"), + ("Strong", "Jako"), + ("Switch Sides", "Promjena strane"), + ("Please confirm if you want to share your desktop?", "Potvrdite ako želite dijeliti svoju radnu površinu?"), + ("Display", "Zaslon"), + ("Default View Style", "Zadani način prikaza"), + ("Default Scroll Style", "Zadani način pomaka"), + ("Default Image Quality", "Zadana kvaliteta slike"), + ("Default Codec", "Izlazni kodek"), + ("Bitrate", "Tok podataka"), + ("FPS", "FPS"), + ("Auto", "Autom."), + ("Other Default Options", "Ostale zadane opcije"), + ("Voice call", "Glasovni poziv"), + ("Text chat", "Tekstni chat"), + ("Stop voice call", "Zaustavi glasovni poziv"), + ("relay_hint_tip", "Izravna veza možda neće biti moguća, možete se pokušati povezati preko relejnog poslužitelja. Osim toga, ako želite koristiti poslužitelj za prosljeđivanje u prvom pokušaju, možete dodati sufiks ID-u \"/r\", ili u kartici nedavnih sesija odaberite opciju \"Uvijek se poveži preko pristupnika\", ako postoji."), + ("Reconnect", "Ponovno se spojite"), + ("Codec", "Kodek"), + ("Resolution", "Razlika"), + ("No transfers in progress", "Nema prijenosa u tijeku"), + ("Set one-time password length", "Postavljanje duljine jednokratne lozinke"), + ("RDP Settings", "Postavljanje RDP-a"), + ("Sort by", "Poredaj po"), + ("New Connection", "Nova veza"), + ("Restore", "Vratiti"), + ("Minimize", "Smanjiti"), + ("Maximize", "Povećati"), + ("Your Device", "Vaš uređaj"), + ("empty_recent_tip", "Nema nedavne sesije!\nVrijeme je da zakažete novu."), + ("empty_favorite_tip", "Još nemate nijednog omiljenog partnera?\nPronađite nekoga s kim se možete povezati i dodajte ga u svoje favorite!"), + ("empty_lan_tip", "Ali ne, izgleda da još nismo otkrili niti jednu drugu stranu."), + ("empty_address_book_tip", "Izgleda da trenutno nemate nijednog kolege navedenog u svom imeniku."), + ("Empty Username", "Prazno korisničko ime"), + ("Empty Password", "Prazna lozinka"), + ("Me", "Ja"), + ("identical_file_tip", "Ova je datoteka identična partnerskoj datoteci."), + ("show_monitors_tip", "Prikažite monitore na alatnoj traci"), + ("View Mode", "Način prikaza"), + ("login_linux_tip", "Da biste omogućili sesiju X radne površine, morate se prijaviti na udaljeni Linux račun."), + ("verify_rustdesk_password_tip", "Provjera lozinke za RustDesk"), + ("remember_account_tip", "Zapamti ovaj račun"), + ("os_account_desk_tip", "Ovaj se račun koristi za prijavu na udaljeni operativni sustav i za omogućavanje sesije radne površine u bezglavom načinu rada."), + ("OS Account", "Račun operativnog sustava"), + ("another_user_login_title_tip", "Drugi korisnik je već prijavljen"), + ("another_user_login_text_tip", "Prekini vezu"), + ("xorg_not_found_title_tip", "Xorg nije pronađen"), + ("xorg_not_found_text_tip", "Molimo instalirajte Xorg"), + ("no_desktop_title_tip", "Nema dostupne radne površine"), + ("no_desktop_text_tip", "Molimo instalirajte GNOME"), + ("No need to elevate", "Nije potrebno povećanje"), + ("System Sound", "Zvuk sustava"), + ("Default", "Zadano"), + ("New RDP", "Novi RDP"), + ("Fingerprint", "Otisak"), + ("Copy Fingerprint", "Kopirat otisak"), + ("no fingerprints", "nema otiska"), + ("Select a peer", "Izbor druge strane"), + ("Select peers", "Odaberite druge strane"), + ("Plugins", "Dodaci"), + ("Uninstall", "Deinstaliraj"), + ("Update", "Ažuriraj"), + ("Enable", "Dopustiti"), + ("Disable", "Zabraniti"), + ("Options", "Mogućnosti"), + ("resolution_original_tip", "Izvorna rezolucija"), + ("resolution_fit_local_tip", "Podesite lokalnu rezoluciju"), + ("resolution_custom_tip", "Prilagođena rezolucija"), + ("Collapse toolbar", "Sažmi alatnu traku"), + ("Accept and Elevate", "Prihvati povećanje"), + ("accept_and_elevate_btn_tooltip", "Prihvatite vezu i povećajte UAC dopuštenja."), + ("clipboard_wait_response_timeout_tip", "Isteklo je vrijeme čekanja na kopiju odgovora."), + ("Incoming connection", "Dolazna veza"), + ("Outgoing connection", "Odlazna veza"), + ("Exit", "Izlaz"), + ("Open", "Otvori"), + ("logout_tip", "Jeste li sigurni da se želite odjaviti?"), + ("Service", "Servis"), + ("Start", "Pokreni"), + ("Stop", "Zaustavi"), + ("exceed_max_devices", "Dosegli ste najveći broj upravljanih uređaja."), + ("Sync with recent sessions", "Sinkronizacija s nedavnim sesijama"), + ("Sort tags", "Razvrstaj oznake"), + ("Open connection in new tab", "Otvorite vezu u novoj kartici"), + ("Move tab to new window", "Premjesti karticu u novi prozor"), + ("Can not be empty", "Ne može biti prazno"), + ("Already exists", "Već postoji"), + ("Change Password", "Promjena lozinke"), + ("Refresh Password", "Poništavanje lozinke"), + ("ID", "ID"), + ("Grid View", "Mreža"), + ("List View", "Imenik"), + ("Select", "Odaberi"), + ("Toggle Tags", "Promijenite oznake"), + ("pull_ab_failed_tip", "Nije uspjelo vraćanje imenika"), + ("push_ab_failed_tip", "Sinkronizacija imenika s poslužiteljem nije uspjela"), + ("synced_peer_readded_tip", "Uređaji koji su bili prisutni u posljednjim sesijama sinkronizirat će se natrag u imenik."), + ("Change Color", "Promjena boje"), + ("Primary Color", "Osnovna boja"), + ("HSV Color", "HSV boja"), + ("Installation Successful!", "Instalacija uspjela!"), + ("Installation failed!", "Instalacija nije uspjela!"), + ("Reverse mouse wheel", "Obrnuti kotačić miša"), + ("{} sessions", "{} sesija"), + ("scam_title", "Možda vas je netko PREVARIO!"), + ("scam_text1", "Ako razgovarate telefonom s nekim koga NE POZNAJETE i NE VJERUJETE tko vas je zamolio da koristite i pokrenete RustDesk, nemojte nastavljati razgovor i odmah spustite slušalicu."), + ("scam_text2", "Ovo je vjerojatno prevarant koji pokušava ukrasti vaš novac ili druge privatne podatke."), + ("Don't show again", "Ne prikazuj opet"), + ("I Agree", "Slažem se"), + ("Decline", "Ne slažem se"), + ("Timeout in minutes", "Istek u minutama"), + ("auto_disconnect_option_tip", "Automatsko prekidanje dolaznih veza kada je korisnik neaktivan"), + ("Connection failed due to inactivity", "Povezivanje nije uspjelo zbog neaktivnosti"), + ("Check for software update on startup", "Provjera ažuriranja softvera pri pokretanju"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Ažurirajte RustDesk Server Pro na verziju {} ili noviju!"), + ("pull_group_failed_tip", "Vraćanje grupe nije uspjelo"), + ("Filter by intersection", "Filtriraj po prosjeku"), + ("Remove wallpaper during incoming sessions", "Uklonite pozadinu tijekom dolaznih sesija"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Zaslon je isključen, prijeđite na prvi zaslon."), + ("No displays", "Nema zaslona"), + ("Open in new window", "Otvori u novom prozoru"), + ("Show displays as individual windows", "Prikaži zaslone kao pojedinačne prozore"), + ("Use all my displays for the remote session", "Koristi sve moje zaslone za udaljenu sesiju"), + ("selinux_tip", "Na vašem uređaju je omogućen SELinux, što može spriječiti RustDesk da pravilno radi kao upravljana strana."), + ("Change view", "Promjena prikaza"), + ("Big tiles", "Velike pločice"), + ("Small tiles", "Male pločice"), + ("List", "Imenik"), + ("Virtual display", "Virtualni zaslon"), + ("Plug out all", "Odspojite sve"), + ("True color (4:4:4)", "Stvarne boje (4:4:4)"), + ("Enable blocking user input", "Omogući blokiranje korisničkog unosa"), + ("id_input_tip", "Možete unijeti ID, izravnu IP adresu ili domenu s portom (:).\nAko želite pristupiti uređaju na drugom poslužitelju, povežite adresu poslužitelja (@?kljuć=), naprimjer,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAko želite pristupiti uređaju na javnom poslužitelju, unesite \"@public\", ključ nije potreban za javni poslužitelj."), + ("privacy_mode_impl_mag_tip", "Način 1"), + ("privacy_mode_impl_virtual_display_tip", "Način 2"), + ("Enter privacy mode", "Uđite u način privatnosti"), + ("Exit privacy mode", "Izađi iz načina privatnosti"), + ("idd_not_support_under_win10_2004_tip", "Neizravni upravljački program za zaslon nije podržan. Potreban je Windows 10 verzija 2004 ili novija."), + ("input_source_1_tip", "Ulazni izvor 1"), + ("input_source_2_tip", "Ulazni izvor 2"), + ("Swap control-command key", "Zamjena tipki control-command"), + ("swap-left-right-mouse", "Zamjena lijeve i desne tipke miša"), + ("2FA code", "2FA kôd"), + ("More", "Više"), + ("enable-2fa-title", "Omogući dvofaktorsku autentifikaciju"), + ("enable-2fa-desc", "Postavite svoj autentifikator. Na telefonu ili računalu možete koristiti aplikaciju za autentifikaciju kao što su Authy, Microsoft ili Google Authenticator.\n\nSkenirajte QR kôd pomoću aplikacije i unesite kôd koji aplikacija prikazuje za aktiviranje dvofaktorske autentifikacije."), + ("wrong-2fa-code", "Kôd se ne može provjeriti. Provjerite jesu li kôd i postavke lokalnog vremena točni"), + ("enter-2fa-title", "Dvofaktorska autentifikacija"), + ("Email verification code must be 6 characters.", "Kôd za provjeru e-pošte mora imati 6 znakova."), + ("2FA code must be 6 digits.", "2FA kôd mora imati 6 znamenki."), + ("Multiple Windows sessions found", "Pronađeno je više Windows sesija"), + ("Please select the session you want to connect to", "Odaberite sesiju kojoj se želite pridružiti"), + ("powered_by_me", "Pokreće RustDesk"), + ("outgoing_only_desk_tip", "Ovo je prilagođeno izdanje.\nMožete se povezati s drugim uređajima, ali se drugi uređaji ne mogu povezati s vašim uređajem."), + ("preset_password_warning", "Ovo modificirano izdanje dolazi s unaprijed postavljenom lozinkom. Svatko tko zna ovu lozinku može dobiti potpunu kontrolu nad vašim uređajem. Ako to niste očekivali, odmah deinstalirajte softver."), + ("Security Alert", "Sigurnosno upozorenje"), + ("My address book", "Moj adresar"), + ("Personal", "Osobni"), + ("Owner", "Vlasnik"), + ("Set shared password", "Postavite zajedničku lozinku"), + ("Exist in", "Postoji u"), + ("Read-only", "Samo za čitanje"), + ("Read/Write", "Način čitanja/pisanja"), + ("Full Control", "Potpuna kontrola"), + ("share_warning_tip", "Gornja polja su podijeljena i vidljiva drugima."), + ("Everyone", "Svatko"), + ("ab_web_console_tip", "Više na web konzoli"), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", ""), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", ""), + ("Floating window", ""), + ("floating_window_tip", ""), + ("Keep screen on", ""), + ("Never", ""), + ("During controlled", ""), + ("During service is on", ""), + ("Capture screen using DirectX", ""), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Molimo ažurirajte RustDesk klijent na verziju {} ili noviju na udaljenoj strani!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Pregled kamere"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Nastavi sa {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/hu.rs b/vendor/rustdesk/src/lang/hu.rs new file mode 100644 index 0000000..7f9b329 --- /dev/null +++ b/vendor/rustdesk/src/lang/hu.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Állapot"), + ("Your Desktop", "Saját számítógép"), + ("desk_tip", "A számítógép ezzel a jelszóval és azonosítóval érhető el távolról."), + ("Password", "Jelszó"), + ("Ready", "Kész"), + ("Established", "Létrejött"), + ("connecting_status", "Kapcsolódás folyamatban ..."), + ("Enable service", "Szolgáltatás engedélyezése"), + ("Start service", "Szolgáltatás indítása"), + ("Service is running", "Szolgáltatás aktív"), + ("Service is not running", "Szolgáltatás inaktív"), + ("not_ready_status", "Kapcsolódási hiba. Ellenőrizze a hálózati beállításokat."), + ("Control Remote Desktop", "Távoli számítógép vezérlése"), + ("Transfer file", "Fájlátvitel"), + ("Connect", "Kapcsolódás"), + ("Recent sessions", "Legutóbbi munkamenetek"), + ("Address book", "Címjegyzék"), + ("Confirmation", "Megerősítés"), + ("TCP tunneling", "TCP-alagút"), + ("Remove", "Eltávolítás"), + ("Refresh random password", "Új véletlenszerű jelszó"), + ("Set your own password", "Saját jelszó beállítása"), + ("Enable keyboard/mouse", "Billentyűzet/egér engedélyezése"), + ("Enable clipboard", "Megosztott vágólap engedélyezése"), + ("Enable file transfer", "Fájlátvitel engedélyezése"), + ("Enable TCP tunneling", "TCP-alagút engedélyezése"), + ("IP Whitelisting", "IP engedélyezési lista"), + ("ID/Relay Server", "ID/Továbbító-kiszolgáló"), + ("Import server config", "Kiszolgáló-konfiguráció importálása"), + ("Export Server Config", "Kiszolgáló-konfiguráció exportálása"), + ("Import server configuration successfully", "Kiszolgáló-konfiguráció sikeresen importálva"), + ("Export server configuration successfully", "Kiszolgáló-konfiguráció sikeresen exportálva"), + ("Invalid server configuration", "Érvénytelen kiszolgáló-konfiguráció"), + ("Clipboard is empty", "A vágólap üres"), + ("Stop service", "Szolgáltatás leállítása"), + ("Change ID", "Azonosító módosítása"), + ("Your new ID", "Új azonosító"), + ("length %min% to %max%", "hossz %min% és %max% között"), + ("starts with a letter", "betűvel kezdődik"), + ("allowed characters", "engedélyezett karakterek"), + ("id_change_tip", "Csak a-z, A-Z, 0-9, - (kötőjel) csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az első karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."), + ("Website", "Weboldal"), + ("About", "Névjegy"), + ("Slogan_tip", "Szenvedéllyel programozva - egy káoszba süllyedő világban!"), + ("Privacy Statement", "Adatvédelmi nyilatkozat"), + ("Mute", "Némítás"), + ("Build Date", "Összeállítás ideje"), + ("Version", "Verzió"), + ("Home", "Kezdőképernyő"), + ("Audio Input", "Hangbemenet"), + ("Enhancements", "Fejlesztések"), + ("Hardware Codec", "Hardveres kodek"), + ("Adaptive bitrate", "Adaptív bitráta"), + ("ID Server", "ID-kiszolgáló"), + ("Relay Server", "Továbbító-kiszolgáló"), + ("API Server", "API-kiszolgáló"), + ("invalid_http", "A címnek mindenképpen http(s)://-rel kell kezdődnie."), + ("Invalid IP", "A megadott IP-cím érvénytelen"), + ("Invalid format", "Érvénytelen formátum"), + ("server_not_support", "A kiszolgáló nem támogatja"), + ("Not available", "Nem érhető el"), + ("Too frequent", "Túl gyakori"), + ("Cancel", "Mégse"), + ("Skip", "Kihagyás"), + ("Close", "Bezárás"), + ("Retry", "Újra"), + ("OK", "OK"), + ("Password Required", "A jelszó megadása kötelező"), + ("Please enter your password", "Adja meg a jelszavát"), + ("Remember password", "Jelszó megjegyzése"), + ("Wrong Password", "Hibás jelszó"), + ("Do you want to enter again?", "Szeretne újra belépni?"), + ("Connection Error", "Kapcsolódási hiba"), + ("Error", "Hiba"), + ("Reset by the peer", "A kapcsolatot a másik fél lezárta."), + ("Connecting...", "Kapcsolódás..."), + ("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kis türelmet ..."), + ("Please try 1 minute later", "Próbálja meg 1 perc múlva"), + ("Login Error", "Bejelentkezési hiba"), + ("Successful", "Sikeres"), + ("Connected, waiting for image...", "Kapcsolódva, várakozás a képadatokra..."), + ("Name", "Név"), + ("Type", "Típus"), + ("Modified", "Módosított"), + ("Size", "Méret"), + ("Show Hidden Files", "Rejtett fájlok megjelenítése"), + ("Receive", "Fogadás"), + ("Send", "Küldés"), + ("Refresh File", "Fájl frissítése"), + ("Local", "Helyi"), + ("Remote", "Távoli"), + ("Remote Computer", "Távoli számítógép"), + ("Local Computer", "Helyi számítógép"), + ("Confirm Delete", "Törlés megerősítése"), + ("Delete", "Törlés"), + ("Properties", "Tulajdonságok"), + ("Multi Select", "Többszörös kijelölés"), + ("Select All", "Összes kijelölése"), + ("Unselect All", "Kijelölések megszüntetése"), + ("Empty Directory", "Üres könyvtár"), + ("Not an empty directory", "Nem egy üres könyvtár"), + ("Are you sure you want to delete this file?", "Biztosan törli ezt a fájlt?"), + ("Are you sure you want to delete this empty directory?", "Biztosan törli ezt az üres könyvtárat?"), + ("Are you sure you want to delete the file of this directory?", "Biztosan törli a könyvtár tartalmát?"), + ("Do this for all conflicts", "Tegye ezt minden ütközés esetén"), + ("This is irreversible!", "Ez a művelet nem vonható vissza!"), + ("Deleting", "Törlés folyamatban"), + ("files", "fájl"), + ("Waiting", "Várakozás"), + ("Finished", "Befejezve"), + ("Speed", "Sebesség"), + ("Custom Image Quality", "Egyéni képminőség"), + ("Privacy mode", "Inkognitó mód"), + ("Block user input", "Felhasználói bevitel letiltása"), + ("Unblock user input", "Felhasználói bevitel engedélyezése"), + ("Adjust Window", "Ablakméret beállítása"), + ("Original", "Eredeti méret"), + ("Shrink", "Kicsinyítés"), + ("Stretch", "Nyújtás"), + ("Scrollbar", "Görgetősáv"), + ("ScrollAuto", "Automatikus görgetés"), + ("Good image quality", "Eredetihez hű"), + ("Balanced", "Kiegyensúlyozott"), + ("Optimize reaction time", "Gyorsan reagáló"), + ("Custom", "Egyéni"), + ("Show remote cursor", "Távoli kurzor megjelenítése"), + ("Show quality monitor", "Kijelző minőségének ellenőrzése"), + ("Disable clipboard", "Közös vágólap kikapcsolása"), + ("Lock after session end", "Távoli fiók zárolása a munkamenet végén"), + ("Insert Ctrl + Alt + Del", "Illessze be a Ctrl + Alt + Del billentyűzetkombinációt"), + ("Insert Lock", "Távoli fiók zárolása"), + ("Refresh", "Frissítés"), + ("ID does not exist", "Az azonosító nem létezik"), + ("Failed to connect to rendezvous server", "Nem sikerült kapcsolódni a kiszolgálóhoz"), + ("Please try later", "Próbálja meg később"), + ("Remote desktop is offline", "A távoli számítógép offline állapotban van"), + ("Key mismatch", "Kulcseltérés"), + ("Timeout", "Időtúllépés"), + ("Failed to connect to relay server", "Nem sikerült kapcsolódni a továbbító-kiszolgálóhoz"), + ("Failed to connect via rendezvous server", "Nem sikerült kapcsolódni a kiszolgálón keresztül"), + ("Failed to connect via relay server", "Nem sikerült kapcsolódni a továbbító-kiszolgálón keresztül"), + ("Failed to make direct connection to remote desktop", "Nem sikerült közvetlen kapcsolatot létesíteni a távoli számítógéppel"), + ("Set Password", "Jelszó beállítása"), + ("OS Password", "Operációs rendszer jelszavának beállítása"), + ("install_tip", "Előfordul, hogy bizonyos esetekben hiba léphet fel a Portable verzió használatakor. A megfelelő működés érdekében, telepítse a RustDesk alkalmazást a számítógépére."), + ("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"), + ("Configure", "Beállítás"), + ("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."), + ("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a „Képernyőfelvétel” jogosultságot."), + ("Installing ...", "Telepítés ..."), + ("Install", "Telepítse"), + ("Installation", "Telepítés"), + ("Installation Path", "Telepítési útvonal"), + ("Create start menu shortcuts", "Start menü parancsikonok létrehozása"), + ("Create desktop icon", "Ikon létrehozása az asztalon"), + ("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licenc szerződés."), + ("Accept and Install", "Elfogadás és telepítés"), + ("End-user license agreement", "Végfelhasználói licenc szerződés"), + ("Generating ...", "Létrehozás ..."), + ("Your installation is lower version.", "A telepített verzió alacsonyabb."), + ("not_close_tcp_tip", "Ne zárja be ezt az ablakot, amíg TCP-alagutat használ"), + ("Listening ...", "Figyelés ..."), + ("Remote Host", "Távoli kiszolgáló"), + ("Remote Port", "Távoli port"), + ("Action", "Indítás"), + ("Add", "Hozzáadás"), + ("Local Port", "Helyi port"), + ("Local Address", "Helyi cím"), + ("Change Local Port", "Helyi port módosítása"), + ("setup_server_tip", "Gyorsabb kapcsolat érdekében, hozzon létre saját kiszolgálót"), + ("Too short, at least 6 characters.", "Túl rövid, legalább 6 karakter."), + ("The confirmation is not identical.", "A megerősítés nem volt azonos"), + ("Permissions", "Engedélyek"), + ("Accept", "Elfogadás"), + ("Dismiss", "Elutasítás"), + ("Disconnect", "Kapcsolat bontása"), + ("Enable file copy and paste", "Fájlmásolás és beillesztés engedélyezése"), + ("Connected", "Kapcsolódva"), + ("Direct and encrypted connection", "Közvetlen, és titkosított kapcsolat"), + ("Relayed and encrypted connection", "Továbbított, és titkosított kapcsolat"), + ("Direct and unencrypted connection", "Közvetlen, és nem titkosított kapcsolat"), + ("Relayed and unencrypted connection", "Továbbított, és nem titkosított kapcsolat"), + ("Enter Remote ID", "Távoli számítógép azonosítója"), + ("Enter your password", "Adja meg a jelszavát"), + ("Logging in...", "Belépés folyamatban..."), + ("Enable RDP session sharing", "RDP-munkamenet-megosztás engedélyezése"), + ("Auto Login", "Automatikus bejelentkezés"), + ("Enable direct IP access", "Közvetlen IP-elérés engedélyezése"), + ("Rename", "Átnevezés"), + ("Space", "Szóköz"), + ("Create desktop shortcut", "Asztali parancsikon létrehozása"), + ("Change Path", "Elérési út módosítása"), + ("Create Folder", "Mappa létrehozás"), + ("Please enter the folder name", "Adja meg a mappa nevét"), + ("Fix it", "Javítás"), + ("Warning", "Figyelmeztetés"), + ("Login screen using Wayland is not supported", "A Wayland használatával történő bejelentkezési képernyő nem támogatott"), + ("Reboot required", "Újraindítás szükséges"), + ("Unsupported display server", "Nem támogatott megjelenítő kiszolgáló"), + ("x11 expected", "x11-re számított"), + ("Port", "Port"), + ("Settings", "Beállítások"), + ("Username", "Felhasználónév"), + ("Invalid port", "Érvénytelen port"), + ("Closed manually by the peer", "A kapcsolatot a másik fél saját kezűleg bezárta"), + ("Enable remote configuration modification", "Távoli konfiguráció-módosítás engedélyezése"), + ("Run without install", "Futtatás telepítés nélkül"), + ("Connect via relay", "Kapcsolódás továbbító-kiszolgálón keresztül"), + ("Always connect via relay", "Kapcsolódás mindig továbbító-kiszolgálón keresztül"), + ("whitelist_tip", "Csak az engedélyezési listán szereplő címek kapcsolódhatnak"), + ("Login", "Belépés"), + ("Verify", "Ellenőrzés"), + ("Remember me", "Emlékezzen rám"), + ("Trust this device", "Megbízom ebben az eszközben"), + ("Verification code", "Ellenőrző kód"), + ("verification_tip", "A regisztrált e-mail-címre egy ellenőrző kód lesz elküldve. Adja meg az ellenőrző kódot az újbóli bejelentkezéshez."), + ("Logout", "Kilépés"), + ("Tags", "Címkék"), + ("Search ID", "Azonosító keresése..."), + ("whitelist_sep", "A címeket vesszővel, pontosvesszővel, szóközzel vagy új sorral kell elválasztani"), + ("Add ID", "Azonosító hozzáadása"), + ("Add Tag", "Címke hozzáadása"), + ("Unselect all tags", "A címkék kijelölésének megszüntetése"), + ("Network error", "Hálózati hiba"), + ("Username missed", "Üres felhasználónév"), + ("Password missed", "Üres jelszó"), + ("Wrong credentials", "Hibás felhasználónév vagy jelszó"), + ("The verification code is incorrect or has expired", "A hitelesítőkód érvénytelen vagy lejárt"), + ("Edit Tag", "Címke szerkesztése"), + ("Forget Password", "Jelszó elfelejtése"), + ("Favorites", "Kedvencek"), + ("Add to Favorites", "Hozzáadás a kedvencekhez"), + ("Remove from Favorites", "Eltávolítás a kedvencekből"), + ("Empty", "Üres"), + ("Invalid folder name", "Helytelen mappa név"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "Felfedezett"), + ("install_daemon_tip", "Automatikus indításhoz szükséges a szolgáltatás telepítése"), + ("Remote ID", "Távoli azonosító"), + ("Paste", "Beillesztés"), + ("Paste here?", "Beillesztés ide?"), + ("Are you sure to close the connection?", "Biztosan bezárja a kapcsolatot?"), + ("Download new version", "Új verzió letöltése"), + ("Touch mode", "Érintési mód bekapcsolása"), + ("Mouse mode", "Egérhasználati mód bekapcsolása"), + ("One-Finger Tap", "Egyujjas érintés"), + ("Left Mouse", "Bal egér gomb"), + ("One-Long Tap", "Hosszú érintés"), + ("Two-Finger Tap", "Kétujjas érintés"), + ("Right Mouse", "Jobb egér gomb"), + ("One-Finger Move", "Egyujjas mozgatás"), + ("Double Tap & Move", "Dupla érintés és mozgatás"), + ("Mouse Drag", "Mozgatás egérrel"), + ("Three-Finger vertically", "Három ujj függőlegesen"), + ("Mouse Wheel", "Egérgörgő"), + ("Two-Finger Move", "Kétujjas mozgatás"), + ("Canvas Move", "Nézet módosítása"), + ("Pinch to Zoom", "Kétujjas nagyítás"), + ("Canvas Zoom", "Nézet nagyítása"), + ("Reset canvas", "Nézet visszaállítása"), + ("No permission of file transfer", "Nincs engedély a fájlátvitelre"), + ("Note", "Megjegyzés"), + ("Connection", "Kapcsolat"), + ("Share screen", "Képernyőmegosztás"), + ("Chat", "Csevegés"), + ("Total", "Összes"), + ("items", "elem"), + ("Selected", "Kijelölve"), + ("Screen Capture", "Képernyőrögzítés"), + ("Input Control", "Távoli vezérlés"), + ("Audio Capture", "Hangrögzítés"), + ("Do you accept?", "Elfogadás?"), + ("Open System Setting", "Rendszerbeállítások megnyitása"), + ("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"), + ("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a „Hozzáférhetőség” szolgáltatás használatát."), + ("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a „RustDesk Input” szolgáltatást."), + ("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"), + ("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."), + ("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."), + ("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."), + ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a „Kapcsolási szolgáltatás indítása” gombra, vagy aktiválja a „Képernyőfelvétel” engedélyt."), + ("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."), + ("Account", "Fiók"), + ("Overwrite", "Felülírás"), + ("This file exists, skip or overwrite this file?", "Ez a fájl már létezik, kihagyja vagy felülírja ezt a fájlt?"), + ("Quit", "Kilépés"), + ("Help", "Súgó"), + ("Failed", "Sikertelen"), + ("Succeeded", "Sikeres"), + ("Someone turns on privacy mode, exit", "Valaki bekacsolta az inkognitó módot, lépjen ki"), + ("Unsupported", "Nem támogatott"), + ("Peer denied", "Elutasítva a távoli fél által"), + ("Please install plugins", "Telepítse a bővítményeket"), + ("Peer exit", "A távoli fél kilépett"), + ("Failed to turn off", "Nem sikerült kikapcsolni"), + ("Turned off", "Kikapcsolva"), + ("Language", "Nyelv"), + ("Keep RustDesk background service", "RustDesk futtatása a háttérben"), + ("Ignore Battery Optimizations", "Akkumulátorkímélő figyelmen kívül hagyása"), + ("android_open_battery_optimizations_tip", "Ha le szeretné tiltani ezt a funkciót, lépjen a RustDesk alkalmazás beállításaiba, keresse meg az [Akkumulátorkímélő] lehetőséget és válassza a nincs korlátozás lehetőséget."), + ("Start on boot", "Indítás bekapcsoláskor"), + ("Start the screen sharing service on boot, requires special permissions", "Indítsa el a képernyőmegosztó szolgáltatást rendszerindításkor, mely speciális engedélyeket is igényel"), + ("Connection not allowed", "A kapcsolódás nem engedélyezett"), + ("Legacy mode", "Kompatibilitási mód"), + ("Map mode", "Hozzárendelési mód"), + ("Translate mode", "Fordító mód"), + ("Use permanent password", "Állandó jelszó használata"), + ("Use both passwords", "Mindkét jelszó használata"), + ("Set permanent password", "Állandó jelszó beállítása"), + ("Enable remote restart", "Távoli újraindítás engedélyezése"), + ("Restart remote device", "Távoli eszköz újraindítása"), + ("Are you sure you want to restart", "Biztosan újra szeretné indítani?"), + ("Restarting remote device", "Távoli eszköz újraindítása..."), + ("remote_restarting_tip", "A távoli eszköz újraindul, zárja be ezt az üzenetet, kapcsolódjon újra az állandó jelszavával"), + ("Copied", "Másolva"), + ("Exit Fullscreen", "Kilépés teljes képernyős módból"), + ("Fullscreen", "Teljes képernyő"), + ("Mobile Actions", "Mobil műveletek"), + ("Select Monitor", "Válasszon képernyőt"), + ("Control Actions", "Irányítási műveletek"), + ("Display Settings", "Megjelenítési beállítások"), + ("Ratio", "Arány"), + ("Image Quality", "Képminőség"), + ("Scroll Style", "Görgetési stílus"), + ("Show Toolbar", "Eszköztár megjelenítése"), + ("Hide Toolbar", "Eszköztár elrejtése"), + ("Direct Connection", "Kapcsolódás közvetlenül"), + ("Relay Connection", "Kapcsolódás továbbító-kiszolgálón keresztül"), + ("Secure Connection", "Biztonságos kapcsolat"), + ("Insecure Connection", "Nem biztonságos kapcsolat"), + ("Scale original", "Eredeti méretarány"), + ("Scale adaptive", "Adaptív méretarány"), + ("General", "Általános"), + ("Security", "Biztonság"), + ("Theme", "Téma"), + ("Dark Theme", "Sötét téma"), + ("Light Theme", "Világos téma"), + ("Dark", "Sötét"), + ("Light", "Világos"), + ("Follow System", "Rendszer beállításainak követése"), + ("Enable hardware codec", "Hardveres kodek engedélyezése"), + ("Unlock Security Settings", "Biztonsági beállítások feloldása"), + ("Enable audio", "Hang engedélyezése"), + ("Unlock Network Settings", "Hálózati beállítások feloldása"), + ("Server", "Kiszolgáló"), + ("Direct IP Access", "Közvetlen IP-hozzáférés"), + ("Proxy", "Proxy"), + ("Apply", "Alkalmaz"), + ("Disconnect all devices?", "Leválasztja az összes eszközt?"), + ("Clear", "Tisztítás"), + ("Audio Input Device", "Hangbemeneti eszköz"), + ("Use IP Whitelisting", "Engedélyezési lista használata"), + ("Network", "Hálózat"), + ("Pin Toolbar", "Eszköztár kitűzése"), + ("Unpin Toolbar", "Eszköztár kitűzésének feloldása"), + ("Recording", "Felvétel"), + ("Directory", "Könyvtár"), + ("Automatically record incoming sessions", "A bejövő munkamenetek automatikus rögzítése"), + ("Automatically record outgoing sessions", "A kimenő munkamenetek automatikus rögzítése"), + ("Change", "Módosítás"), + ("Start session recording", "Munkamenet-rögzítés indítása"), + ("Stop session recording", "Munkamenet-rögzítés leállítása"), + ("Enable recording session", "Munkamenet-rögzítés engedélyezése"), + ("Enable LAN discovery", "Felfedezés engedélyezése"), + ("Deny LAN discovery", "Felfedezés tiltása"), + ("Write a message", "Üzenet írása"), + ("Prompt", "Kérés"), + ("Please wait for confirmation of UAC...", "Várjon az UAC megerősítésére..."), + ("elevated_foreground_window_tip", "A távvezérelt számítógép jelenleg nyitott ablakához magasabb szintű jogok szükségesek. Ezért jelenleg nem lehetséges az egér és a billentyűzet használata. Kérje meg azt a felhasználót, akinek a számítógépét távolról vezérli, hogy minimalizálja az ablakot, vagy növelje a jogokat. A jövőbeni probléma elkerülése érdekében ajánlott a szoftvert a távvezérelt számítógépre telepíteni."), + ("Disconnected", "Kapcsolat bontva"), + ("Other", "Egyéb"), + ("Confirm before closing multiple tabs", "Biztosan bezárja az összes lapot?"), + ("Keyboard Settings", "Billentyűzetbeállítások"), + ("Full Access", "Teljes hozzáférés"), + ("Screen Share", "Képernyőmegosztás"), + ("ubuntu-21-04-required", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."), + ("wayland-requires-higher-linux-version", "A Wayland a Linux disztribúció magasabb verzióját igényli. Próbálja ki az X11 asztali környezetet, vagy változtassa meg az operációs rendszert."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Hiperhivatkozás"), + ("Please Select the screen to be shared(Operate on the peer side).", "Válassza ki a megosztani kívánt képernyőt."), + ("Show RustDesk", "A RustDesk megjelenítése"), + ("This PC", "Ez a számítógép"), + ("or", "vagy"), + ("Elevate", "Hozzáférés engedélyezése"), + ("Zoom cursor", "Kurzor nagyítása"), + ("Accept sessions via password", "Munkamenetek elfogadása jelszóval"), + ("Accept sessions via click", "Munkamenetek elfogadása kattintással"), + ("Accept sessions via both", "Munkamenetek fogadása mindkettőn keresztül"), + ("Please wait for the remote side to accept your session request...", "Várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét..."), + ("One-time Password", "Egyszer használatos jelszó"), + ("Use one-time password", "Használjon ideiglenes jelszót"), + ("One-time password length", "Egyszer használatos jelszó hossza"), + ("Request access to your device", "Hozzáférés kérése az eszközéhez"), + ("Hide connection management window", "Kapcsolatkezelő ablak elrejtése"), + ("hide_cm_tip", "Ez csak akkor lehetséges, ha a hozzáférés állandó jelszóval történik."), + ("wayland_experiment_tip", "A Wayland-támogatás csak kísérleti jellegű. Használja az X11-et, ha felügyelet nélküli hozzáférésre van szüksége."), + ("Right click to select tabs", "Jobb klikk a lapok kiválasztásához"), + ("Skipped", "Kihagyott"), + ("Add to address book", "Hozzáadás a címjegyzékhez"), + ("Group", "Csoport"), + ("Search", "Keresés"), + ("Closed manually by web console", "Saját kezűleg bezárva a webkonzolon keresztül"), + ("Local keyboard type", "Helyi billentyűzet típusa"), + ("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"), + ("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres leképezés alkalmazása segíthet. A szoftvert újra kell indítani."), + ("Always use software rendering", "Mindig szoftveres leképezést használjon"), + ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a „Bemenet figyelése” jogosultságot."), + ("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a „Hangfelvétel” jogosultságot."), + ("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."), + ("Wait", "Várjon"), + ("Elevation Error", "Emelt szintű hozzáférési hiba"), + ("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"), + ("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"), + ("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"), + ("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), + ("Request Elevation", "Emelt szintű jogok igénylése"), + ("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."), + ("Elevate successfully", "Emelt szintű jogok megadva"), + ("uppercase", "NAGYBETŰS"), + ("lowercase", "kisbetűs"), + ("digit", "szám"), + ("special character", "különleges karakter"), + ("length>=8", "hossz>=8"), + ("Weak", "Gyenge"), + ("Medium", "Közepes"), + ("Strong", "Erős"), + ("Switch Sides", "Oldalváltás"), + ("Please confirm if you want to share your desktop?", "Erősítse meg, hogy meg akarja-e osztani az asztalát?"), + ("Display", "Képernyő"), + ("Default View Style", "Alapértelmezett megjelenítés"), + ("Default Scroll Style", "Alapértelmezett görgetés"), + ("Default Image Quality", "Alapértelmezett képminőség"), + ("Default Codec", "Alapértelmezett kodek"), + ("Bitrate", "Bitsebesség"), + ("FPS", "FPS"), + ("Auto", "Automatikus"), + ("Other Default Options", "Egyéb alapértelmezett beállítások"), + ("Voice call", "Hanghívás"), + ("Text chat", "Szöveges csevegés"), + ("Stop voice call", "Hanghívás leállítása"), + ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. Az azonosítóhoz vagy a „Mindig továbbító-kiszolgálón keresztül kapcsolódom” opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), + ("Reconnect", "Újrakapcsolódás"), + ("Codec", "Kodek"), + ("Resolution", "Felbontás"), + ("No transfers in progress", "Nincs folyamatban átvitel"), + ("Set one-time password length", "Állítsa be az egyszeri jelszó hosszát"), + ("RDP Settings", "RDP beállítások"), + ("Sort by", "Rendezés"), + ("New Connection", "Új kapcsolat"), + ("Restore", "Visszaállítás"), + ("Minimize", "Minimalizálás"), + ("Maximize", "Maximalizálás"), + ("Your Device", "Az én eszközöm"), + ("empty_recent_tip", "Nincsenek aktuális munkamenetek!\nIdeje ütemezni egy újat."), + ("empty_favorite_tip", "Még nincs kedvenc távoli állomása?\nHagyja, hogy találjunk valakit, akivel kapcsolatba tud lépni, és adja hozzá a kedvencekhez!"), + ("empty_lan_tip", "Úgy tűnik, még nem adott hozzá egyetlen távoli helyszínt sem."), + ("empty_address_book_tip", "Úgy tűnik, hogy jelenleg nincsenek távoli állomások a címjegyzékében."), + ("Empty Username", "Üres felhasználónév"), + ("Empty Password", "Üres jelszó"), + ("Me", "Ön"), + ("identical_file_tip", "Ez a fájl megegyezik a távoli állomás fájljával."), + ("show_monitors_tip", "Képernyők megjelenítése az eszköztáron"), + ("View Mode", "Nézet mód"), + ("login_linux_tip", "Az X-asztal munkamenet megnyitásához be kell jelentkeznie egy távoli Linux-fiókba."), + ("verify_rustdesk_password_tip", "RustDesk jelszó megerősítése"), + ("remember_account_tip", "Emlékezzen erre a fiókra"), + ("os_account_desk_tip", "Ezzel a fiókkal bejelentkezhet a távoli operációs rendszerbe, és aktiválhatja az asztali munkamenetet fej nélküli módban."), + ("OS Account", "OS fiók"), + ("another_user_login_title_tip", "Egy másik felhasználó már bejelentkezett."), + ("another_user_login_text_tip", "Különálló"), + ("xorg_not_found_title_tip", "Xorg nem található."), + ("xorg_not_found_text_tip", "Telepítse az Xorgot."), + ("no_desktop_title_tip", "Nem áll rendelkezésre asztali környezet."), + ("no_desktop_text_tip", "Telepítse a GNOME asztali környezetet."), + ("No need to elevate", "Nem szükséges megemelni"), + ("System Sound", "Rendszer hangok"), + ("Default", "Alapértelmezett"), + ("New RDP", "Új RDP"), + ("Fingerprint", "Ujjlenyomat"), + ("Copy Fingerprint", "Ujjlenyomat másolása"), + ("no fingerprints", "nincsenek ujjlenyomatok"), + ("Select a peer", "Egy távoli állomás kiválasztása"), + ("Select peers", "Távoli állomások kiválasztása"), + ("Plugins", "Beépülő modulok"), + ("Uninstall", "Eltávolítás"), + ("Update", "Frissítés"), + ("Enable", "Engedélyezés"), + ("Disable", "Letiltás"), + ("Options", "Opciók"), + ("resolution_original_tip", "Eredeti felbontás"), + ("resolution_fit_local_tip", "Helyi felbontás beállítása"), + ("resolution_custom_tip", "Testre szabható felbontás"), + ("Collapse toolbar", "Eszköztár összecsukása"), + ("Accept and Elevate", "Elfogadás és magasabb szintű jogosultságra emelés"), + ("accept_and_elevate_btn_tooltip", "Fogadja el a kapcsolatot, és növelje az UAC-engedélyeket."), + ("clipboard_wait_response_timeout_tip", "Időtúllépés, amíg a másolat válaszára vár."), + ("Incoming connection", "Bejövő kapcsolat"), + ("Outgoing connection", "Kimenő kapcsolat"), + ("Exit", "Kilépés"), + ("Open", "Megnyitás"), + ("logout_tip", "Biztosan ki szeretne lépni?"), + ("Service", "Szolgáltatás"), + ("Start", "Indítás"), + ("Stop", "Leállítás"), + ("exceed_max_devices", "Elérte a felügyelt eszközök maximális számát."), + ("Sync with recent sessions", "Szinkronizálás a legutóbbi munkamenetekkel"), + ("Sort tags", "Címkék rendezése"), + ("Open connection in new tab", "Kapcsolat megnyitása új lapon"), + ("Move tab to new window", "Lap áthelyezése új ablakba"), + ("Can not be empty", "Nem lehet üres"), + ("Already exists", "Már létezik"), + ("Change Password", "Jelszó módosítása"), + ("Refresh Password", "Jelszó frissítése"), + ("ID", "Azonosító"), + ("Grid View", "Mozaik nézet"), + ("List View", "Lista nézet"), + ("Select", "Kiválasztás"), + ("Toggle Tags", "Címkekapcsoló"), + ("pull_ab_failed_tip", "A címjegyzék frissítése nem sikerült"), + ("push_ab_failed_tip", "A címjegyzék szinkronizálása a kiszolgálóval nem sikerült"), + ("synced_peer_readded_tip", "A legutóbbi munkamenetekben jelen lévő eszközök ismét felkerülnek a címjegyzékbe."), + ("Change Color", "Szín módosítása"), + ("Primary Color", "Elsődleges szín"), + ("HSV Color", "HSV szín"), + ("Installation Successful!", "Sikeres telepítés!"), + ("Installation failed!", "A telepítés nem sikerült!"), + ("Reverse mouse wheel", "Fordított egérgörgő"), + ("{} sessions", "{} munkamenet"), + ("scam_title", "Lehet, hogy átverték!"), + ("scam_text1", "Ha olyan valakivel beszél telefonon, akit NEM ISMER, akiben NEM BÍZIK MEG, és aki arra kéri, hogy használja a RustDesket és indítsa el a szolgáltatást, ne folytassa, és azonnal tegye le a telefont."), + ("scam_text2", "Valószínűleg egy csaló próbálja ellopni a pénzét vagy más személyes adatait."), + ("Don't show again", "Ne jelenítse meg újra"), + ("I Agree", "Elfogadás"), + ("Decline", "Elutasítás"), + ("Timeout in minutes", "Időtúllépés percekben"), + ("auto_disconnect_option_tip", "A bejövő munkamenetek automatikus bezárása, ha a felhasználó inaktív"), + ("Connection failed due to inactivity", "A kapcsolat inaktivitás miatt megszakadt"), + ("Check for software update on startup", "Szoftverfrissítés keresése indításkor"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Frissítse a RustDesk Server Prot a(z) {} vagy újabb verzióra!"), + ("pull_group_failed_tip", "A csoport frissítése nem sikerült"), + ("Filter by intersection", "Szűrés metszéspontok szerint"), + ("Remove wallpaper during incoming sessions", "Háttérkép eltávolítása bejövő munkameneteknél"), + ("Test", "Teszt"), + ("display_is_plugged_out_msg", "A képernyő nincs csatlakoztatva, váltson az első képernyőre."), + ("No displays", "Nincsenek kijelzők"), + ("Open in new window", "Megnyitás új ablakban"), + ("Show displays as individual windows", "Kijelzők megjelenítése egyedi ablakokként"), + ("Use all my displays for the remote session", "Összes kijelző használata a távoli munkamenethez"), + ("selinux_tip", "A SELinux engedélyezve van az eszközén, ami azt okozhatja, hogy a RustDesk nem fut megfelelően, mint ellenőrzött."), + ("Change view", "Nézet módosítása"), + ("Big tiles", "Nagy csempék"), + ("Small tiles", "Kis csempék"), + ("List", "Lista"), + ("Virtual display", "Virtuális kijelző"), + ("Plug out all", "Kapcsolja ki az összeset"), + ("True color (4:4:4)", "Valódi szín (4:4:4)"), + ("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"), + ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (:).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „@public” lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például „9123456234/r”."), + ("privacy_mode_impl_mag_tip", "1. mód"), + ("privacy_mode_impl_virtual_display_tip", "2. mód"), + ("Enter privacy mode", "Lépjen be az adatvédelmi módba"), + ("Exit privacy mode", "Lépjen ki az adatvédelmi módból"), + ("idd_not_support_under_win10_2004_tip", "A közvetett grafikus illesztőprogram nem támogatott. Windows 10, 2004-es vagy újabb verzió szükséges."), + ("input_source_1_tip", "1. bemeneti forrás"), + ("input_source_2_tip", "2. bemeneti forrás"), + ("Swap control-command key", "Vezérlő- és parancsgombok cseréje"), + ("swap-left-right-mouse", "Bal és jobb egérgomb felcserélése"), + ("2FA code", "2FA kód"), + ("More", "Továbbiak"), + ("enable-2fa-title", "Kétfaktoros hitelesítés aktiválása"), + ("enable-2fa-desc", "Állítsa be a hitelesítőt. Használhat egy hitelesítő alkalmazást, például az Aegis, Authy, a Microsoft- vagy a Google Authenticator alkalmazást a telefonján vagy az asztali számítógépén.\n\nOlvassa be a QR-kódot az alkalmazással, és adja meg az alkalmazás által megjelenített kódot a kétfaktoros hitelesítés aktiválásához."), + ("wrong-2fa-code", "A kód nem ellenőrizhető. Ellenőrizze, hogy a kód és a helyi idő beállításai helyesek-e."), + ("enter-2fa-title", "Kétfaktoros hitelesítés"), + ("Email verification code must be 6 characters.", "Az e-mailben kapott ellenőrző-kódnak 6 karakterből kell állnia."), + ("2FA code must be 6 digits.", "A 2FA-kódnak 6 számjegyűnek kell lennie."), + ("Multiple Windows sessions found", "Több Windows-munkamenet található"), + ("Please select the session you want to connect to", "Válassza ki a munkamenetet, amelyhez kapcsolódni szeretne"), + ("powered_by_me", "Üzemeltető: RustDesk"), + ("outgoing_only_desk_tip", "Ez a RustDesk testre szabott kimenete.\nMás eszközökhöz kapcsolódhat, de más eszközök nem kapcsolódhatnak az Ön eszközéhez."), + ("preset_password_warning", "Ez egy testre szabott kimenet a RustDeskből egy előre beállított jelszóval. Bárki, aki ismeri ezt a jelszót, teljes irányítást szerezhet a készülék felett. Ha nem kívánja ezt megtenni, azonnal távolítsa el ezt a szoftvert."), + ("Security Alert", "Biztonsági riasztás"), + ("My address book", "Saját címjegyzék"), + ("Personal", "Személyes"), + ("Owner", "Tulajdonos"), + ("Set shared password", "Megosztott jelszó beállítása"), + ("Exist in", "Létezik"), + ("Read-only", "Csak olvasható"), + ("Read/Write", "Olvasás/Írás"), + ("Full Control", "Teljes ellenőrzés"), + ("share_warning_tip", "A fenti mezők megosztottak és mások számára is láthatóak."), + ("Everyone", "Mindenki"), + ("ab_web_console_tip", "További információk a webes konzolról"), + ("allow-only-conn-window-open-tip", "Csak akkor engedélyezze a kapcsolódást, ha a RustDesk ablaka nyitva van."), + ("no_need_privacy_mode_no_physical_displays_tip", "Nincsenek fizikai képernyők; Nincs szükség az adatvédelmi üzemmód használatára."), + ("Follow remote cursor", "Kövesse a távoli kurzort"), + ("Follow remote window focus", "Kövesse a távoli ablakfókuszt"), + ("default_proxy_tip", "A szabványos protokoll és port SOCKS5 és 1080"), + ("no_audio_input_device_tip", "Nem található hangbemeneti eszköz."), + ("Incoming", "Bejövő"), + ("Outgoing", "Kimenő"), + ("Clear Wayland screen selection", "Wayland képernyő kiválasztásának törlése"), + ("clear_Wayland_screen_selection_tip", "A képernyőválasztás törlése után újra kiválaszthatja a megosztandó képernyőt."), + ("confirm_clear_Wayland_screen_selection_tip", "Biztosan törölni szeretné a Wayland képernyő kiválasztását?"), + ("android_new_voice_call_tip", "Új hanghívás-kérés érkezett. Ha elfogadja a megkeresést, a hang átvált hangkommunikációra."), + ("texture_render_tip", "Használja a textúra leképezést a képek simábbá tételéhez. Ezt az opciót kikapcsolhatja, ha leképezési problémái vannak."), + ("Use texture rendering", "Textúra leképezés használata"), + ("Floating window", "Lebegő ablak"), + ("floating_window_tip", "Segít, ha a RustDesk a háttérben fut."), + ("Keep screen on", "Tartsa a képernyőt bekapcsolva"), + ("Never", "Soha"), + ("During controlled", "Amikor ellenőrzött"), + ("During service is on", "Amikor a szolgáltatás fut"), + ("Capture screen using DirectX", "Képernyő rögzítése DirectX használatával"), + ("Back", "Vissza"), + ("Apps", "Alkalmazások"), + ("Volume up", "Hangerő fel"), + ("Volume down", "Hangerő le"), + ("Power", "Főkapcsoló"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."), + ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a „/newbot” parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"), + ("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"), + ("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"), + ("About RustDesk", "A RustDesk névjegye"), + ("Send clipboard keystrokes", "Billentyűleütések küldése a vágólapra"), + ("network_error_tip", "Ellenőrizze a hálózati kapcsolatot, majd próbálja meg újra."), + ("Unlock with PIN", "Feloldás PIN-kóddal"), + ("Requires at least {} characters", "Legalább {} karakter szükséges"), + ("Wrong PIN", "Hibás PIN"), + ("Set PIN", "PIN-kód beállítása"), + ("Enable trusted devices", "Megbízható eszközök engedélyezése"), + ("Manage trusted devices", "Megbízható eszközök kezelése"), + ("Platform", "Platform"), + ("Days remaining", "Hátralévő napok"), + ("enable-trusted-devices-tip", "A 2FA-ellenőrzés kihagyása megbízható eszközökön"), + ("Parent directory", "Szülőkönyvtár"), + ("Resume", "Folytatás"), + ("Invalid file name", "Érvénytelen fájlnév"), + ("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."), + ("Authentication Required", "Hitelesítés szükséges"), + ("Authenticate", "Hitelesítés"), + ("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg az „@public” kulcsot. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."), + ("Download", "Letöltés"), + ("Upload folder", "Mappa feltöltése"), + ("Upload files", "Fájlok feltöltése"), + ("Clipboard is synchronized", "A vágólap szinkronizálva van"), + ("Update client clipboard", "Az ügyfél vágólapjának frissítése"), + ("Untagged", "Címkézetlen"), + ("new-version-of-{}-tip", "A(z) {} új verziója"), + ("Accessible devices", "Hozzáférhető eszközök"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Frissítse a RustDesk klienst {} vagy újabb verziójára a távoli oldalon!"), + ("d3d_render_tip", "D3D leképezés"), + ("Use D3D rendering", "D3D leképezés használata"), + ("Printer", "Nyomtató"), + ("printer-os-requirement-tip", "Nyomtató operációs rendszerének minimális rendszerkövetelménye"), + ("printer-requires-installed-{}-client-tip", "A nyomtatóhoz szükséges a(z) {} kliens telepítése"), + ("printer-{}-not-installed-tip", "A(z) {} nyomtató nincs telepítve"), + ("printer-{}-ready-tip", "A(z) {} nyomtató készen áll"), + ("Install {} Printer", "A(z) {} nyomtató telepítése"), + ("Outgoing Print Jobs", "Kimenő nyomtatási feladatok"), + ("Incoming Print Jobs", "Bejövő nyomtatási feladatok"), + ("Incoming Print Job", "Bejövő nyomtatási feladat"), + ("use-the-default-printer-tip", "Alapértelmezett nyomtató használata"), + ("use-the-selected-printer-tip", "Kiválasztott nyomtató használata"), + ("auto-print-tip", "Automatikus nyomtatás"), + ("print-incoming-job-confirm-tip", "Bejövő nyomtatási feladat megerősítése"), + ("remote-printing-disallowed-tile-tip", "A távoli nyomtatás nincs engedélyezve"), + ("remote-printing-disallowed-text-tip", "A távoli nyomtatás nincs engedélyezve"), + ("save-settings-tip", "Beállítások mentése"), + ("dont-show-again-tip", "Ne jelenítse meg újra"), + ("Take screenshot", "Képernyőkép készítése"), + ("Taking screenshot", "Képernyőkép készítése..."), + ("screenshot-merged-screen-not-supported-tip", "Egyesített képernyőről nem támogatott a képernyőkép készítése"), + ("screenshot-action-tip", "Képernyőkép-művelet"), + ("Save as", "Mentés másként"), + ("Copy to clipboard", "Másolás a vágólapra"), + ("Enable remote printer", "Távoli nyomtatók engedélyezése"), + ("Downloading {}", "{} letöltése"), + ("{} Update", "{} frissítés"), + ("{}-to-update-tip", "{} bezárása és az új verzió telepítése."), + ("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a „Letöltés” gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."), + ("Auto update", "Automatikus frissítés"), + ("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a „Letöltés” gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."), + ("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."), + ("Use WebSocket", "WebSocket használata"), + ("Trackpad speed", "Érintőpad sebessége"), + ("Default trackpad speed", "Alapértelmezett érintőpad sebessége"), + ("Numeric one-time password", "Numerikus, egyszer használatos jelszó"), + ("Enable IPv6 P2P connection", "IPv6 P2P kapcsolat engedélyezése"), + ("Enable UDP hole punching", "UDP résszűrés engedélyezése"), + ("View camera", "Kamera nézet"), + ("Enable camera", "Kamera engedélyezése"), + ("No cameras", "Nincs kamera"), + ("view_camera_unsupported_tip", "A kameranézet nem támogatott"), + ("Terminal", "Terminál"), + ("Enable terminal", "Terminál engedélyezése"), + ("New tab", "Új lap"), + ("Keep terminal sessions on disconnect", "Terminál munkamenetek megtartása leválasztáskor"), + ("Terminal (Run as administrator)", "Terminál (rendszergazdaként futtatva)"), + ("terminal-admin-login-tip", "Adja meg a felügyelt terminál rendszergazdai fiókjának jelszavát."), + ("Failed to get user token.", "Hiba a felhasználói token lekérdezésekor."), + ("Incorrect username or password.", "A felhasználónév vagy a jelszó helytelen."), + ("The user is not an administrator.", "A felhasználó nem rendszergazda."), + ("Failed to check if the user is an administrator.", "Hiba merült fel annak ellenőrzése során, hogy a felhasználó rendszergazda-e."), + ("Supported only in the installed version.", "Csak a telepített változatban támogatott."), + ("elevation_username_tip", "Felhasználónév vagy tartománynév megadása"), + ("Preparing for installation ...", "Felkészülés a telepítésre ..."), + ("Show my cursor", "Kurzor megjelenítése"), + ("Scale custom", "Egyéni méretarány"), + ("Custom scale slider", "Egyéni méretarány-csúszka"), + ("Decrease", "Csökkentés"), + ("Increase", "Növelés"), + ("Show virtual mouse", "Virtuális egér megjelenítése"), + ("Virtual mouse size", "Virtuális egér mérete"), + ("Small", "Kicsi"), + ("Large", "Nagy"), + ("Show virtual joystick", "Virtuális vezérlő megjelenítése"), + ("Edit note", "Megjegyzés szerkesztése"), + ("Alias", "Álnév"), + ("ScrollEdge", "Görgetés az ablak szélein"), + ("Allow insecure TLS fallback", "Nem biztonságos TLS-tartalék engedélyezése"), + ("allow-insecure-tls-fallback-tip", "Alapértelmezés szerint a RustDesk ellenőrzi a kiszolgáló tanúsítványát a TLS-protokollok esetében. Ha ez a beállítás engedélyezve van, a RustDesk kihagyja az ellenőrzési lépést, és az ellenőrzés sikertelensége esetén folytatja a műveletet."), + ("Disable UDP", "UDP letiltása"), + ("disable-udp-tip", "Meghatározza, hogy csak TCP-t használjon-e. Ha ez az beállítás engedélyezve van, a RustDesk nem fogja többé használni a 21116-os UDP-portot, helyette a 21116-os TCP-portot fogja használni."), + ("server-oss-not-support-tip", "MEGJEGYZÉS: Az OSS RustDesk kiszolgáló nem támogatja ezt a funkciót."), + ("input note here", "Megjegyzés beírása"), + ("note-at-conn-end-tip", "Kérjen megjegyzést a kapcsolat végén"), + ("Show terminal extra keys", "További terminálgombok megjelenítése"), + ("Relative mouse mode", "Relatív egér mód"), + ("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egér módot."), + ("rel-mouse-not-ready-tip", "A relatív egér mód még nem elérhető. Próbálja meg újra."), + ("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egér mód le lett tiltva."), + ("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a következő gombot: {}"), + ("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egér mód le lett tilva."), + ("Changelog", "Változáslista"), + ("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"), + ("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"), + ("Continue with {}", "Folytatás ezzel: {}"), + ("Display Name", "Kijelző név"), + ("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."), + ("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/id.rs b/vendor/rustdesk/src/lang/id.rs new file mode 100644 index 0000000..bbd95e7 --- /dev/null +++ b/vendor/rustdesk/src/lang/id.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Desktop Anda"), + ("desk_tip", "Akses desktop kamu dengan ID & Kata sandi ini"), + ("Password", "Kata sandi"), + ("Ready", "Sudah siap"), + ("Established", "Didirikan"), + ("connecting_status", "Menghubungkan ke RustDesk..."), + ("Enable service", "Aktifkan Layanan"), + ("Start service", "Mulai Layanan"), + ("Service is running", "Layanan berjalan"), + ("Service is not running", "Layanan tidak berjalan"), + ("not_ready_status", "Belum siap digunakan. Silakan periksa koneksi"), + ("Control Remote Desktop", "Lakukan Kontrol PC dari jarak jauh"), + ("Transfer file", "Transfer File"), + ("Connect", "Sambungkan"), + ("Recent sessions", "Sesi Terkini"), + ("Address book", "Buku Alamat"), + ("Confirmation", "Konfirmasi"), + ("TCP tunneling", "Tunneling TCP"), + ("Remove", "Hapus"), + ("Refresh random password", "Perbarui kata sandi acak"), + ("Set your own password", "Tetapkan kata sandi"), + ("Enable keyboard/mouse", "Aktifkan Keyboard/Mouse"), + ("Enable clipboard", "Aktifkan Papan Klip"), + ("Enable file transfer", "Aktifkan Transfer file"), + ("Enable TCP tunneling", "Aktifkan TCP tunneling"), + ("IP Whitelisting", "Daftar IP yang diizinkan"), + ("ID/Relay Server", "ID/Relay Server"), + ("Import server config", "Impor Konfigurasi Server"), + ("Export Server Config", "Ekspor Konfigurasi Server"), + ("Import server configuration successfully", "Impor konfigurasi server berhasil"), + ("Export server configuration successfully", "Ekspor konfigurasi server berhasil"), + ("Invalid server configuration", "Konfigurasi server tidak valid"), + ("Clipboard is empty", "Papan klip kosong"), + ("Stop service", "Hentikan Layanan"), + ("Change ID", "Ubah ID"), + ("Your new ID", "ID baru"), + ("length %min% to %max%", "panjang %min% s/d %max%"), + ("starts with a letter", "Dimulai dengan huruf"), + ("allowed characters", "Karakter yang dapat digunakan"), + ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9, - (dash) dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."), + ("Website", "Situs Web"), + ("About", "Tentang"), + ("Slogan_tip", "Dibuat dengan penuh kasih sayang dalam dunia yang penuh kekacauan ini"), + ("Privacy Statement", "Pernyataan Privasi"), + ("Mute", "Bisukan"), + ("Build Date", "Tanggal Build"), + ("Version", "Versi"), + ("Home", ""), + ("Audio Input", "Input Audio"), + ("Enhancements", "Peningkatan"), + ("Hardware Codec", "Kodek Perangkat Keras"), + ("Adaptive bitrate", "Kecepatan Bitrate Adaptif"), + ("ID Server", "Server ID"), + ("Relay Server", "Server Relay"), + ("API Server", "Server API"), + ("invalid_http", "harus dimulai dengan http:// atau https://"), + ("Invalid IP", "IP tidak valid"), + ("Invalid format", "Format tidak valid"), + ("server_not_support", "Belum didukung oleh server"), + ("Not available", "Tidak tersedia"), + ("Too frequent", "Terlalu sering"), + ("Cancel", "Batal"), + ("Skip", "Lanjutkan"), + ("Close", "Tutup"), + ("Retry", "Coba lagi"), + ("OK", "Oke"), + ("Password Required", "Kata sandi tidak boleh kosong"), + ("Please enter your password", "Silahkan masukkan kata sandi"), + ("Remember password", "Ingat kata sandi"), + ("Wrong Password", "Kata sandi Salah"), + ("Do you want to enter again?", "Apakah kamu ingin mencoba lagi?"), + ("Connection Error", "Kesalahan koneksi"), + ("Error", "Kesalahan"), + ("Reset by the peer", "Direset oleh rekan"), + ("Connecting...", "Menghubungkan..."), + ("Connection in progress. Please wait.", "Koneksi sedang berlangsung. Mohon tunggu."), + ("Please try 1 minute later", "Silahkan coba 1 menit lagi"), + ("Login Error", "Kesalahan Login"), + ("Successful", "Berhasil"), + ("Connected, waiting for image...", "Terhubung, menunggu gambar..."), + ("Name", "Nama"), + ("Type", "Tipe"), + ("Modified", "Diperbarui"), + ("Size", "Ukuran"), + ("Show Hidden Files", "Tampilkan File Tersembunyi"), + ("Receive", "Menerima"), + ("Send", "Kirim"), + ("Refresh File", "Segarkan File"), + ("Local", "Lokal"), + ("Remote", "Remote"), + ("Remote Computer", "Remote Komputer"), + ("Local Computer", "Lokal Komputer"), + ("Confirm Delete", "Konfirmasi Hapus"), + ("Delete", "Hapus"), + ("Properties", "Properti"), + ("Multi Select", "Pilih Beberapa"), + ("Select All", "Pilih Semua"), + ("Unselect All", "Batalkan Pilihan Semua"), + ("Empty Directory", "Folder Kosong"), + ("Not an empty directory", "Folder tidak kosong"), + ("Are you sure you want to delete this file?", "Apakah kamu yakin untuk menghapus file ini?"), + ("Are you sure you want to delete this empty directory?", "Apakah yakin yakin untuk menghapus folder ini?"), + ("Are you sure you want to delete the file of this directory?", "Apakah yakin yakin untuk menghapus file dan folder ini?"), + ("Do this for all conflicts", "Lakukan untuk semua konflik"), + ("This is irreversible!", "Ini tidak dapat diubah!"), + ("Deleting", "Menghapus"), + ("files", "file"), + ("Waiting", "Menunggu"), + ("Finished", "Selesai"), + ("Speed", "Kecepatan"), + ("Custom Image Quality", "Sesuaikan Kualitas Gambar"), + ("Privacy mode", "Mode Privasi"), + ("Block user input", "Blokir input pengguna"), + ("Unblock user input", "Jangan blokir input pengguna"), + ("Adjust Window", "Sesuaikan Jendela"), + ("Original", "Asli"), + ("Shrink", "Susutkan"), + ("Stretch", "Regangkan"), + ("Scrollbar", "Scrollbar"), + ("ScrollAuto", "Scroll Otomatis"), + ("Good image quality", "Kualitas Gambar Baik"), + ("Balanced", "Seimbang"), + ("Optimize reaction time", "Optimalkan waktu reaksi"), + ("Custom", "Kustom"), + ("Show remote cursor", "Tampilkan kursor remote"), + ("Show quality monitor", "Tampilkan kualitas monitor"), + ("Disable clipboard", "Matikan papan klip"), + ("Lock after session end", "Kunci setelah sesi berakhir"), + ("Insert Ctrl + Alt + Del", "Menyisipkan Ctrl + Alt + Del"), + ("Insert Lock", "Masukkan Kunci"), + ("Refresh", "Segarkan"), + ("ID does not exist", "ID tidak ada"), + ("Failed to connect to rendezvous server", "Gagal menghubungkan ke rendezvous server"), + ("Please try later", "Silahkan coba lagi nanti"), + ("Remote desktop is offline", "Remote desktop offline"), + ("Key mismatch", "Ketidakcocokan kunci"), + ("Timeout", "Waktu habis"), + ("Failed to connect to relay server", "Gagal terkoneksi ke relay server"), + ("Failed to connect via rendezvous server", "Gagal terkoneksi via rendezvous server"), + ("Failed to connect via relay server", "Gagal terkoneksi via relay server"), + ("Failed to make direct connection to remote desktop", "Gagal membuat koneksi langsung ke desktop jarak jauh"), + ("Set Password", "Tetapkan kata sandi"), + ("OS Password", "Kata Sandi OS"), + ("install_tip", "Karena UAC, RustDesk tidak dapat bekerja dengan baik sebagai sisi remote dalam beberapa kasus. Untuk menghindari UAC, silakan klik tombol di bawah ini untuk menginstal RustDesk ke sistem."), + ("Click to upgrade", "Klik untuk upgrade"), + ("Configure", "Konfigurasi"), + ("config_acc", "Agar bisa mengontrol Desktopmu dari jarak jauh, Kamu harus memberikan izin \"Aksesibilitas\" untuk RustDesk."), + ("config_screen", "Agar bisa mengakses Desktopmu dari jarak jauh, kamu harus memberikan izin \"Perekaman Layar\" untuk RustDesk."), + ("Installing ...", "Menginstall"), + ("Install", "Instal"), + ("Installation", "Instalasi"), + ("Installation Path", "Direktori Instalasi"), + ("Create start menu shortcuts", "Buat pintasan start menu"), + ("Create desktop icon", "Buat icon desktop"), + ("agreement_tip", "Dengan memulai proses instalasi, Kamu menerima perjanjian lisensi."), + ("Accept and Install", "Terima dan Install"), + ("End-user license agreement", "Perjanjian lisensi pengguna"), + ("Generating ...", "Memproses..."), + ("Your installation is lower version.", "Kamu menggunakan versi instalasi yang lebih rendah."), + ("not_close_tcp_tip", "Pastikan jendela ini tetap terbuka saat menggunakan tunnel."), + ("Listening ...", "Menghubungkan..."), + ("Remote Host", "Host Remote"), + ("Remote Port", "Port Remote"), + ("Action", "Tindakan"), + ("Add", "Tambah"), + ("Local Port", "Port Lokal"), + ("Local Address", "Alamat lokal"), + ("Change Local Port", "Ubah Port Lokal"), + ("setup_server_tip", "Untuk koneksi yang lebih baik, silakan konfigurasi di server pribadi"), + ("Too short, at least 6 characters.", "Terlalu pendek, setidaknya 6 karekter."), + ("The confirmation is not identical.", "Konfirmasi tidak identik."), + ("Permissions", "Perizinan"), + ("Accept", "Terima"), + ("Dismiss", "Hentikan"), + ("Disconnect", "Terputus"), + ("Enable file copy and paste", "Izinkan copy dan paste"), + ("Connected", "Terhubung"), + ("Direct and encrypted connection", "Koneksi langsung dan terenkripsi"), + ("Relayed and encrypted connection", "Koneksi relay dan terenkripsi"), + ("Direct and unencrypted connection", "Koneksi langsung dan tanpa enkripsi"), + ("Relayed and unencrypted connection", "Koneksi relay dan tanpa enkripsi"), + ("Enter Remote ID", "Masukkan ID Remote"), + ("Enter your password", "Masukkan kata sandi"), + ("Logging in...", "Masuk..."), + ("Enable RDP session sharing", "Aktifkan berbagi sesi RDP"), + ("Auto Login", "Login Otomatis (Hanya berlaku jika sudah mengatur \"Kunci setelah sesi berakhir\")"), + ("Enable direct IP access", "Aktifkan Akses IP Langsung"), + ("Rename", "Ubah nama"), + ("Space", "Spasi"), + ("Create desktop shortcut", "Buat Pintasan Desktop"), + ("Change Path", "Ubah Direktori"), + ("Create Folder", "Buat Folder"), + ("Please enter the folder name", "Silahkan masukkan nama folder"), + ("Fix it", "Perbaiki"), + ("Warning", "Peringatan"), + ("Login screen using Wayland is not supported", "Login screen dengan Wayland tidak didukung"), + ("Reboot required", "Diperlukan boot ulang"), + ("Unsupported display server", "Server tampilan tidak didukung "), + ("x11 expected", "Diperlukan x11"), + ("Port", "Port"), + ("Settings", "Pengaturan"), + ("Username", "Nama pengguna"), + ("Invalid port", "Kesalahan port"), + ("Closed manually by the peer", "Ditutup secara manual oleh rekan"), + ("Enable remote configuration modification", "Aktifkan modifikasi konfigurasi remotE"), + ("Run without install", "Jalankan tanpa menginstal"), + ("Connect via relay", "Sambungkan via relay"), + ("Always connect via relay", "Selalu terhubung melalui relay"), + ("whitelist_tip", "Hanya IP yang diizikan dapat mengakses"), + ("Login", "Masuk"), + ("Verify", "Verifikasi"), + ("Remember me", "Ingatkan saya"), + ("Trust this device", "Izinkan perangkat ini"), + ("Verification code", "Kode verifikasi"), + ("verification_tip", "Kode verifikasi sudah dikirim ke email yang terdaftar, masukkan kode verifikasi untuk melanjutkan."), + ("Logout", "Keluar"), + ("Tags", "Tag"), + ("Search ID", "Cari ID"), + ("whitelist_sep", "Dipisahkan dengan koma, titik koma, spasi, atau baris baru"), + ("Add ID", "Tambah ID"), + ("Add Tag", "Tambah Tag"), + ("Unselect all tags", "Batalkan pilihan semua tag"), + ("Network error", "Kesalahan Jaringan"), + ("Username missed", "Nama pengguna tidak sesuai"), + ("Password missed", "Kata sandi tidak sesuai"), + ("Wrong credentials", "Nama pengguna atau kata sandi salah"), + ("The verification code is incorrect or has expired", "Kode verifikasi salah atau sudah kadaluarsa"), + ("Edit Tag", "Ubah Tag"), + ("Forget Password", "Lupakan Kata Sandi"), + ("Favorites", "Favorit"), + ("Add to Favorites", "Tambah ke Favorit"), + ("Remove from Favorites", "Hapus dari favorit"), + ("Empty", "Kosong"), + ("Invalid folder name", "Nama folder tidak valid"), + ("Socks5 Proxy", "Proksi Socks5"), + ("Socks5/Http(s) Proxy", "Proksi Socks5/Http(s)"), + ("Discovered", "Telah ditemukan"), + ("install_daemon_tip", "Untuk dapat berjalan saat sistem menyala, kamu perlu menginstal layanan sistem (system service/daemon)."), + ("Remote ID", "ID Remote"), + ("Paste", "Tempel"), + ("Paste here?", "Tempel disini?"), + ("Are you sure to close the connection?", "Apakah kamu yakin akan menutup koneksi?"), + ("Download new version", "Download versi baru"), + ("Touch mode", "Mode Layar Sentuh"), + ("Mouse mode", "Mode Mouse"), + ("One-Finger Tap", "Ketuk Satu Jari"), + ("Left Mouse", "Mouse Kiri"), + ("One-Long Tap", "Ketuk Tahan"), + ("Two-Finger Tap", "Ketuk Dua Jari"), + ("Right Mouse", "Mouse Kanan"), + ("One-Finger Move", "Gerakan Satu Jari"), + ("Double Tap & Move", "Ketuk Dua Kali & Pindah"), + ("Mouse Drag", "Geser Mouse"), + ("Three-Finger vertically", "Tiga Jari secara vertikal"), + ("Mouse Wheel", "Roda mouse"), + ("Two-Finger Move", "Gerakan Dua Jari"), + ("Canvas Move", "Gerakan Kanvas"), + ("Pinch to Zoom", "Cubit untuk Memperbesar"), + ("Canvas Zoom", "Perbesar Canvas"), + ("Reset canvas", "Setel Ulang Canvas"), + ("No permission of file transfer", "Tidak ada izin untuk mengirim file"), + ("Note", "Catatan"), + ("Connection", "Koneksi"), + ("Share screen", "Bagikan Layar"), + ("Chat", "Obrolan"), + ("Total", "Total"), + ("items", "item"), + ("Selected", "Dipilih"), + ("Screen Capture", "Tangkapan Layar"), + ("Input Control", "Kontrol input"), + ("Audio Capture", "Rekam Suara"), + ("Do you accept?", "Apakah kamu setuju?"), + ("Open System Setting", "Buka Pengaturan Sistem"), + ("How to get Android input permission?", "Bagaimana cara mendapatkan izin input dari Android?"), + ("android_input_permission_tip1", "Agar perangkat jarak jauh dapat mengontrol perangkat Android melalui mouse atau sentuhan, Kamu harus memberikan izin/permission kd RustDesk untuk menggunakan layanan \"Aksesibilitas\"."), + ("android_input_permission_tip2", "Silakan buka halaman pengaturan sistem berikutnya, temukan dan masuk ke [Layanan Terinstal], aktifkan layanan [Input RustDesk]."), + ("android_new_connection_tip", "Permintaan akses remote telah diterima"), + ("android_service_will_start_tip", "Mengaktifkan \"Tangkapan Layar\" akan memulai secara otomatis, memungkinkan perangkat lain untuk meminta koneksi ke perangkat Anda."), + ("android_stop_service_tip", "Menutup layanan secara otomatis akan menutup semua koneksi yang dibuat."), + ("android_version_audio_tip", "Versi Android saat ini tidak mendukung pengambilan audio, harap tingkatkan ke Android 10 atau lebih tinggi."), + ("android_start_service_tip", "Tap [Mulai Layanan] atau aktifkan izin [Tangkapan Layar] untuk memulai berbagi layar."), + ("android_permission_may_not_change_tip", "Izin untuk koneksi yang sudah terhubung mungkin tidak dapat diubah secara instan hingga terhubung kembali"), + ("Account", "Akun"), + ("Overwrite", "Ganti"), + ("This file exists, skip or overwrite this file?", "File ini sudah ada, lewati atau ganti file ini?"), + ("Quit", "Keluar"), + ("Help", "Bantuan"), + ("Failed", "Gagal"), + ("Succeeded", "Berhasil"), + ("Someone turns on privacy mode, exit", "Seseorang mengaktifkan mode privasi, keluar"), + ("Unsupported", "Tidak didukung"), + ("Peer denied", "Rekan menolak"), + ("Please install plugins", "Silakan instal plugin"), + ("Peer exit", "Rekan keluar"), + ("Failed to turn off", "Gagal mematikan"), + ("Turned off", "Dimatikan"), + ("Language", "Bahasa"), + ("Keep RustDesk background service", "Pertahankan RustDesk berjalan pada service background"), + ("Ignore Battery Optimizations", "Abaikan Pengoptimalan Baterai"), + ("android_open_battery_optimizations_tip", "Jika anda ingin menonaktifkan fitur ini, buka halam pengaturan, cari dan pilih [Baterai], Uncheck [Tidak dibatasi]"), + ("Start on boot", "Mulai saat dihidupkan"), + ("Start the screen sharing service on boot, requires special permissions", "Mulai layanan berbagi layar saat sistem dinyalakan, memerlukan izin khusus."), + ("Connection not allowed", "Koneksi tidak dizinkan"), + ("Legacy mode", "Mode lawas"), + ("Map mode", "Mode peta"), + ("Translate mode", "Mode terjemahan"), + ("Use permanent password", "Gunakan kata sandi permanaen"), + ("Use both passwords", "Gunakan kedua kata sandi"), + ("Set permanent password", "Setel kata sandi permanen"), + ("Enable remote restart", "Aktifkan Restart Secara Remote"), + ("Restart remote device", "Restart Perangkat Secara Remote"), + ("Are you sure you want to restart", "Apakah Anda yakin ingin merestart"), + ("Restarting remote device", "Merestart Perangkat Remote"), + ("remote_restarting_tip", "Perangkat remote sedang merestart, harap tutup pesan ini dan sambungkan kembali dengan kata sandi permanen setelah beberapa saat."), + ("Copied", "Disalin"), + ("Exit Fullscreen", "Keluar dari Layar Penuh"), + ("Fullscreen", "Layar penuh"), + ("Mobile Actions", "Tindakan Seluler"), + ("Select Monitor", "Pilih Monitor"), + ("Control Actions", "Tindakan Kontrol"), + ("Display Settings", "Pengaturan tampilan"), + ("Ratio", "Rasio"), + ("Image Quality", "Kualitas gambar"), + ("Scroll Style", "Gaya Scroll"), + ("Show Toolbar", "Tampilkan Toolbar"), + ("Hide Toolbar", "Sembunyikan Toolbar"), + ("Direct Connection", "Koneksi langsung"), + ("Relay Connection", "Koneksi Relay"), + ("Secure Connection", "Koneksi aman"), + ("Insecure Connection", "Koneksi Tidak Aman"), + ("Scale original", "Skala asli"), + ("Scale adaptive", "Skala adaptif"), + ("General", "Umum"), + ("Security", "Keamanan"), + ("Theme", "Tema"), + ("Dark Theme", "Tema Gelap"), + ("Light Theme", "Tema Terang"), + ("Dark", "Gelap"), + ("Light", "Terang"), + ("Follow System", "Ikuti Sistem"), + ("Enable hardware codec", "Aktifkan kodek perangkat keras"), + ("Unlock Security Settings", "Buka Keamanan Pengaturan"), + ("Enable audio", "Aktifkan Audio"), + ("Unlock Network Settings", "Buka Keamanan Pengaturan Jaringan"), + ("Server", "Server"), + ("Direct IP Access", "Akses IP Langsung"), + ("Proxy", "Proksi"), + ("Apply", "Terapkan"), + ("Disconnect all devices?", "Putuskan sambungan semua perangkat?"), + ("Clear", "Bersihkan"), + ("Audio Input Device", "Input Perangkat Audio"), + ("Use IP Whitelisting", "Gunakan daftar IP yang diizinkan"), + ("Network", "Jaringan"), + ("Pin Toolbar", "Sematkan Toolbar"), + ("Unpin Toolbar", "Batal sematkan Toolbar"), + ("Recording", "Perekaman"), + ("Directory", "Direktori"), + ("Automatically record incoming sessions", "Otomatis merekam sesi masuk"), + ("Automatically record outgoing sessions", ""), + ("Change", "Ubah"), + ("Start session recording", "Mulai sesi perekaman"), + ("Stop session recording", "Hentikan sesi perekaman"), + ("Enable recording session", "Aktifkan Sesi Perekaman"), + ("Enable LAN discovery", "Aktifkan Pencarian Jaringan Lokal (LAN)"), + ("Deny LAN discovery", "Tolak Pencarian Jaringan Lokal (LAN)"), + ("Write a message", "Tulis pesan"), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", "Harap tunggu konfirmasi UAC"), + ("elevated_foreground_window_tip", "Jendela yang sedang aktif di remote desktop memerlukan hak istimewa yang lebih tinggi untuk beroperasi, sehingga mouse dan keyboard tidak dapat digunakan sementara waktu. Kamu bisa meminta pengguna jarak jauh untuk meminimalkan jendela saat ini, atau klik tombol elevasi di jendela manajemen koneksi. Untuk menghindari masalah ini, disarankan untuk menginstal software di perangkat remote secara permanen."), + ("Disconnected", "Terputus"), + ("Other", "Lainnya"), + ("Confirm before closing multiple tabs", "Konfirmasi sebelum menutup banyak tab"), + ("Keyboard Settings", "Pengaturan Papan Ketik"), + ("Full Access", "Akses penuh"), + ("Screen Share", "Berbagi Layar"), + ("ubuntu-21-04-required", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."), + ("wayland-requires-higher-linux-version", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Tautan Cepat"), + ("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan kepada rekan anda."), + ("Show RustDesk", "Tampilkan RustDesk"), + ("This PC", "PC ini"), + ("or", "atau"), + ("Elevate", "Elevasi"), + ("Zoom cursor", "Perbersar Kursor"), + ("Accept sessions via password", "Izinkan sesi dengan kata sandi"), + ("Accept sessions via click", "Izinkan sesi dengan klik"), + ("Accept sessions via both", "Izinkan sesi dengan keduanya"), + ("Please wait for the remote side to accept your session request...", "Harap tunggu pihak pengguna remote untuk menerima permintaan sesi..."), + ("One-time Password", "Kata sandi sementara"), + ("Use one-time password", "Gunakan kata sandi sementara"), + ("One-time password length", "Panjang kata sandi sementara"), + ("Request access to your device", "Permintaan akses ke perangkat ini"), + ("Hide connection management window", "Sembunyikan jendela pengaturan koneksi"), + ("hide_cm_tip", "Izinkan untuk menyembunyikan hanya jika menerima sesi melalui kata sandi dan menggunakan kata sandi permanen"), + ("wayland_experiment_tip", "Dukungan Wayland masih dalam tahap percobaan, harap gunakan X11 jika Anda memerlukan akses tanpa pengawasan"), + ("Right click to select tabs", "Klik kanan untuk memilih tab"), + ("Skipped", "Dilewati"), + ("Add to address book", "Tambahkan ke Buku Alamat"), + ("Group", "Grup"), + ("Search", "Pencarian"), + ("Closed manually by web console", "Ditutup secara manual dari konsol web."), + ("Local keyboard type", "Tipe papan ketik"), + ("Select local keyboard type", "Pilih tipe papan ketik"), + ("software_render_tip", "Jika anda menggunakan kartu grafis Nvidia pada sistem linux dan jendela windows ditutup secara instan setelah terhung, silahkan ubah ke driver open-source Nouveau, dibutukan untuk merestart aplikasi"), + ("Always use software rendering", "Selalu gunakan software rendering"), + ("config_input", "Untuk menggunakan input keyboard remote, anda perlu memberikan izin \"Pemantauan Input\" pada RustDesk"), + ("config_microphone", "Untuk berbicara secara remote, anda perlu memberikan izin \"Rekam Audio\" pada RustDesk"), + ("request_elevation_tip", "Anda juga bisa meminta izin elevasi jika ada pihak pengguna remote"), + ("Wait", "Tunggu"), + ("Elevation Error", "Kesalahan Elevasi"), + ("Ask the remote user for authentication", "Minta pihak pengguna remote untuk otentikasi"), + ("Choose this if the remote account is administrator", "Pilih ini jika akun adalah \"administrator\""), + ("Transmit the username and password of administrator", "Transmisikan nama pengguna dan kata sandi administrator"), + ("still_click_uac_tip", "Masih memerlukan persetujuan pihak pengguna remote untuk mengklik OK pada jendela UAC RustDesk yang sedang berjalan"), + ("Request Elevation", "Permintaan Elevasi"), + ("wait_accept_uac_tip", "Harap tunggu pihak pengguna remote menerima jendela UAC."), + ("Elevate successfully", "Elevasi berhasil"), + ("uppercase", "Huruf besar"), + ("lowercase", "Huruf kecil"), + ("digit", "angka"), + ("special character", "Karakter spesial"), + ("length>=8", "panjang>=8"), + ("Weak", "Lemah"), + ("Medium", "Sedang"), + ("Strong", "Kuat"), + ("Switch Sides", "Ganti Posisi"), + ("Please confirm if you want to share your desktop?", "Harap konfirmasi apakah Anda ingin berbagi layar?"), + ("Display", "Tampilan"), + ("Default View Style", "Gaya Tampilan Default"), + ("Default Scroll Style", "Gaya Scroll Default"), + ("Default Image Quality", "Kualitas Gambar Default"), + ("Default Codec", "Kodek default"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Otomatis"), + ("Other Default Options", "Opsi Default Lainnya"), + ("Voice call", "Panggilan suara"), + ("Text chat", "Obrolan Teks"), + ("Stop voice call", "Hentikan panggilan suara"), + ("relay_hint_tip", "Tidak memungkinkan untuk terhubung secara langsung; anda bisa mencoba terhubung via relay. Selain itu, jika ingin menggunakan relay pada percobaan pertama, silahkan tambah akhiran \"/r\" pada ID atau pilih \"Selalu terhubung via relay\" di pilihan sesi terbaru."), + ("Reconnect", "Sambungkan ulang"), + ("Codec", "Kodek"), + ("Resolution", "Resolusi"), + ("No transfers in progress", "Tidak ada proses transfer data"), + ("Set one-time password length", "Atur panjang kata sandi sekali pakai"), + ("RDP Settings", "Pengaturan RDP"), + ("Sort by", "Urutkan berdasarkan"), + ("New Connection", "Koneksi baru"), + ("Restore", "Mengembalikan"), + ("Minimize", "Meminimalkan"), + ("Maximize", "Memaksimalkan"), + ("Your Device", "Perangkat anda"), + ("empty_recent_tip", "Tidak ada sesi terbaru!"), + ("empty_favorite_tip", "Belum ada rekan favorit?\nTemukan seseorang untuk terhubung dan tambahkan ke favorit!"), + ("empty_lan_tip", "Sepertinya kami belum memiliki rekan"), + ("empty_address_book_tip", "Tampaknya saat ini tidak ada rekan yang terdaftar dalam buku alamat Anda"), + ("Empty Username", "Nama pengguna kosong"), + ("Empty Password", "Kata sandi kosong"), + ("Me", "Saya"), + ("identical_file_tip", "Data ini identik dengan milik rekan"), + ("show_monitors_tip", "Tampilkan monitor di toolbar"), + ("View Mode", "Mode Tampilan"), + ("login_linux_tip", "Anda harus masuk ke akun remote linux untuk mengaktifkan sesi X desktop"), + ("verify_rustdesk_password_tip", "Verifikasi Kata Sandi RustDesk"), + ("remember_account_tip", "Ingat akun ini"), + ("os_account_desk_tip", "Akun ini digunakan untuk masuk ke sistem operasi remote dan mengaktifkan sesi desktop dalam mode tanpa tampilan (headless)"), + ("OS Account", "Akun OS"), + ("another_user_login_title_tip", "Akun ini sedang digunakan"), + ("another_user_login_text_tip", "Putuskan koneksi diperangkat lain"), + ("xorg_not_found_title_tip", "Xorg tidak ditemukan"), + ("xorg_not_found_text_tip", "Silahkan install Xorg"), + ("no_desktop_title_tip", "Desktop tidak tersedia"), + ("no_desktop_text_tip", "Silahkan install GNOME Desktop"), + ("No need to elevate", "Tidak perlu elevasi"), + ("System Sound", "Suara Sistem"), + ("Default", "Default"), + ("New RDP", "RDP Baru"), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), + ("Select a peer", "Pilih rekan"), + ("Select peers", "Pilih rekan-rekan"), + ("Plugins", "Plugin"), + ("Uninstall", "Hapus instalasi"), + ("Update", "Perbarui"), + ("Enable", "Aktifkan"), + ("Disable", "Nonaktifkan"), + ("Options", "Opsi"), + ("resolution_original_tip", "Resolusi original"), + ("resolution_fit_local_tip", "Sesuaikan resolusi lokal"), + ("resolution_custom_tip", "Resolusi kustom"), + ("Collapse toolbar", ""), + ("Accept and Elevate", "Terima dan Elevasi"), + ("accept_and_elevate_btn_tooltip", "Terima koneksi dan elevasi izin UAC"), + ("clipboard_wait_response_timeout_tip", "Batas waktu habis saat menunggu respons salinan"), + ("Incoming connection", "Koneksi akan masuk"), + ("Outgoing connection", "Koneksi akan keluar"), + ("Exit", "Keluar"), + ("Open", "Buka"), + ("logout_tip", "Apakah Anda yakin ingin keluar?"), + ("Service", "Service"), + ("Start", "Jalankan"), + ("Stop", "Hentikan"), + ("exceed_max_devices", "Anda telah mencapai jumlah maksimal perangkat yang dikelola"), + ("Sync with recent sessions", "Sinkronkan dengan sesi terbaru"), + ("Sort tags", "Urutkan tag"), + ("Open connection in new tab", "Buka koneksi di tab baru"), + ("Move tab to new window", "Pindahkan tab ke jendela baru"), + ("Can not be empty", "Tidak boleh kosong"), + ("Already exists", "Sudah ada"), + ("Change Password", "Ganti kata sandi"), + ("Refresh Password", "Perbarui Kata Sandi"), + ("ID", "ID"), + ("Grid View", "Tampilan Kotak"), + ("List View", "Tampilan Daftar"), + ("Select", "Pilih"), + ("Toggle Tags", "Toggle Tag"), + ("pull_ab_failed_tip", "Gagal memuat ulang buku alamat"), + ("push_ab_failed_tip", "Gagal menyinkronkan buku alamat ke server"), + ("synced_peer_readded_tip", "Perangkat yang terdaftar dalam sesi terbaru akan di-sinkronkan kembali ke buku alamat."), + ("Change Color", "Ganti warna"), + ("Primary Color", "Warna utama"), + ("HSV Color", "Warna HSV"), + ("Installation Successful!", "Instalasi berhasil!"), + ("Installation failed!", "Instalasi gagal!"), + ("Reverse mouse wheel", "Balikkan arah scroll mouse"), + ("{} sessions", "sesi {}"), + ("scam_title", "Kemungkinan anda Sedang DITIPU!"), + ("scam_text1", "Jika anda sedang berbicara di telepon dengan seseorang yang TIDAK dikenal dan mereka meminta anda untuk menggunakan RustDesk, jangan lanjutkan dan segera tutup panggilan."), + ("scam_text2", "Kemungkinan besar mereka adalah komplotan penipu yang berusaha mencuri uang atau informasi pribadi anda."), + ("Don't show again", "Jangan tampilkan lagi"), + ("I Agree", "Saya setuju"), + ("Decline", "Tolak"), + ("Timeout in minutes", "Batasan Waktu dalam Menit"), + ("auto_disconnect_option_tip", "Secara otomatis akan menutup sesi ketika tidak ada aktivitas"), + ("Connection failed due to inactivity", "Secara otomatis akan terputus ketik tidak ada aktivitas."), + ("Check for software update on startup", "Periksa pembaruan aplikasi saat sistem dinyalakan."), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Silahkan perbarui RustDesk Server Pro ke versi {} atau yang lebih baru!"), + ("pull_group_failed_tip", "Gagal memperbarui grup"), + ("Filter by intersection", "Filter berdasarkan persilangan"), + ("Remove wallpaper during incoming sessions", "Hilangkan latar dinding ketika ada sesi yang masuk"), + ("Test", "Tes"), + ("display_is_plugged_out_msg", "Layar terputus, pindah ke layar pertama"), + ("No displays", "Tidak ada tampilan"), + ("Open in new window", "Buka di jendela baru"), + ("Show displays as individual windows", "Tampilkan dengan jendela terpisah"), + ("Use all my displays for the remote session", "Gunakan semua layar untuk sesi remote"), + ("selinux_tip", "pada perangkat anda, SELinux sedang aktif, yang mana itu dapat mencegah RustDesk berfungsi dengan baik sebagai sisi yang dikontrol."), + ("Change view", "Sesuaikan tampilan"), + ("Big tiles", "Kotak besar"), + ("Small tiles", "Kotak kecil"), + ("List", "Daftar"), + ("Virtual display", "Tampilan virtual"), + ("Plug out all", "Lepaskan semua"), + ("True color (4:4:4)", ""), + ("Enable blocking user input", "Aktifkan pemblokiran input pengguna"), + ("id_input_tip", "Anda bisa memasukkan ID, IP langsung, atau domain dengan port kostum yang sudah ditentukan (:).\nJika anda ingin mengakses perangkat lain yang berbeda server, tambahkan alamat server setelah penulisan ID(@?key=), sebagai contoh,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nJika anda ingin mengakses perangkat yang menggunakan server publik, masukkan \"@public\", server public tidak memerlukan key khusus"), + ("privacy_mode_impl_mag_tip", "Mode 1"), + ("privacy_mode_impl_virtual_display_tip", "Mode 2"), + ("Enter privacy mode", "Masuk mode privasi"), + ("Exit privacy mode", "Keluar mode privasi"), + ("idd_not_support_under_win10_2004_tip", "Driver grafis yang Anda gunakan tidak kompatibel dengan versi Windows Anda dan memerlukan Windows 10 versi 2004 atau yang lebih baru"), + ("input_source_1_tip", "Sumber input 1"), + ("input_source_2_tip", "Sumber input 2"), + ("Swap control-command key", "Menukar tombol control-command"), + ("swap-left-right-mouse", "Tukar fungsi tombol kiri dan kanan pada mouse"), + ("2FA code", "Kode 2FA"), + ("More", "Lainnya"), + ("enable-2fa-title", "Aktifkan autentikasi 2FA"), + ("enable-2fa-desc", "Silakan atur autentikator Anda sekarang. Anda dapat menggunakan aplikasi autentikator seperti Authy, Microsoft Authenticator, atau Google Authenticator di ponsel atau desktop Anda\n\nPindai kode QR dengan aplikasi Anda dan masukkan kode yang ditampilkan oleh aplikasi untuk mengaktifkan autentikasi 2FA."), + ("wrong-2fa-code", "Tidak dapat memverifikasi kode. Pastikan bahwa kode dan pengaturan waktu lokal sudah sesuai"), + ("enter-2fa-title", "Autentikasi dua faktor"), + ("Email verification code must be 6 characters.", "Kode verifikasi email harus terdiri dari 6 karakter."), + ("2FA code must be 6 digits.", "Kode 2FA harus terdiri dari 6 digit."), + ("Multiple Windows sessions found", "Terdapat beberapa sesi Windows"), + ("Please select the session you want to connect to", "Silakan pilih sesi yang ingin Anda sambungkan."), + ("powered_by_me", "Didukung oleh RustDesk"), + ("outgoing_only_desk_tip", "Ini adalah edisi yang sudah kustomisasi.\nAnda dapat terhubung ke perangkat lain, tetapi perangkat lain tidak dapat terhubung ke perangkat Anda."), + ("preset_password_warning", "Edisi yang dikustomisasi ini dilengkapi dengan kata sandi bawaan. Siapa pun yang mengetahui kata sandi ini dapat memperoleh kontrol penuh atas perangkat Anda. Jika Anda tidak mengharapkan ini, segera hapus pemasangan aplikasi tersebut."), + ("Security Alert", "Peringatan Keamanan"), + ("My address book", "Daftar Kontak"), + ("Personal", "Personal"), + ("Owner", "Pemilik"), + ("Set shared password", "Atus kata sandi kolaboratif"), + ("Exist in", "Ada di"), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("share_warning_tip", "Informasi di atas bersifat publik dan dapat dilihat oleh orang lain."), + ("Everyone", ""), + ("ab_web_console_tip", "Detail Lain di Konsol Web"), + ("allow-only-conn-window-open-tip", "Koneksi hanya diperbolehkan jika jendela RustDesk sedang terbuka."), + ("no_need_privacy_mode_no_physical_displays_tip", "Karena tidak ada layar fisik, mode privasi tidak perlu diaktifkan."), + ("Follow remote cursor", "Ikuti kursor yang terhubung"), + ("Follow remote window focus", "Ikuti jendela remote yang sedang aktif"), + ("default_proxy_tip", "Pengaturan standar untuk protokol dan port adalah Socks5 dan 1080."), + ("no_audio_input_device_tip", "Perangkat input audio tidak terdeteksi."), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", "Kosongkan pilihan layar Wayland"), + ("clear_Wayland_screen_selection_tip", "Setelah mengosongkan pilihan layar, Kamu bisa memilih kembali layar untuk dibagi"), + ("confirm_clear_Wayland_screen_selection_tip", "Kamu yakin ingin membersihkan pemilihan layar Wayland?"), + ("android_new_voice_call_tip", "Kamu mendapatkan permintaan panggilan suara baru. Jika diterima, audio akan berubah menjadi komunikasi suara."), + ("texture_render_tip", "Aktifkan rendering tekstur untuk membuat tampilan gambar lebih mulus. Kamu dapat menonaktifkan opsi ini jika terjadi masalah saat merender."), + ("Use texture rendering", "Aktifkan rendering tekstur"), + ("Floating window", ""), + ("floating_window_tip", "Untuk menjaga layanan/service RustDesk agar tetap aktif"), + ("Keep screen on", "Biarkan layar tetap menyala"), + ("Never", "Tidak pernah"), + ("During controlled", "Dalam proses pengendalian"), + ("During service is on", ""), + ("Capture screen using DirectX", "Rekam layar dengan DirectX"), + ("Back", "Kembali"), + ("Apps", "App"), + ("Volume up", "Naikkan volume"), + ("Volume down", "Turunkan volume"), + ("Power", ""), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Jika fitur ini diaktifkan, Kamu dapat menerima kode 2FA dari bot, serta mendapatkan notifikasi tentang koneksi."), + ("enable-bot-desc", "1. Buka chat dengan @BotFather.\n2. Kirim perintah \"/newbot\". Setelah menyelesaikan langkah ini, Kamu akan mendapatkan token\n3. Mulai percakapan dengan bot yang baru dibuat. Kirim pesan yang dimulai dengan garis miring (\"/\") seperti \"/hello\" untuk mengaktifkannya."), + ("cancel-2fa-confirm-tip", "Apakah Kamu yakin ingin membatalkan 2FA?"), + ("cancel-bot-confirm-tip", "Apakah Kamu yakin ingin membatalkan bot Telegram?"), + ("About RustDesk", "Tentang RustDesk"), + ("Send clipboard keystrokes", "Kirim keystrokes clipboard"), + ("network_error_tip", "Periksa koneksi internet, lalu klik \"Coba lagi\"."), + ("Unlock with PIN", "Buka menggunakan PIN"), + ("Requires at least {} characters", "Memerlukan setidaknya {} karakter."), + ("Wrong PIN", "PIN salah"), + ("Set PIN", "Atur PIN"), + ("Enable trusted devices", "Izinkan perangkat tepercaya"), + ("Manage trusted devices", "Kelola perangkat tepercaya"), + ("Platform", "Platform"), + ("Days remaining", "Sisa hari"), + ("enable-trusted-devices-tip", "Tidak memerlukan verifikasi 2FA pada perangkat tepercaya."), + ("Parent directory", "Direktori utama"), + ("Resume", "Lanjutkan"), + ("Invalid file name", "Nama file tidak valid"), + ("one-way-file-transfer-tip", "Transfer file satu arah (One-way) telah diaktifkan pada sisi yang dikendalikan."), + ("Authentication Required", "Diperlukan autentikasi"), + ("Authenticate", "Autentikasi"), + ("web_id_input_tip", "Kamu bisa memasukkan ID pada server yang sama, akses IP langsung tidak didukung di klien web.\nJika Anda ingin mengakses perangkat di server lain, silakan tambahkan alamat server (@?key=), contohnya:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nUntuk mengakses perangkat di server publik, cukup masukkan \"@public\", tanpa kunci/key."), + ("Download", "Download"), + ("Upload folder", "Upload folder"), + ("Upload files", "Upload file"), + ("Clipboard is synchronized", "Clipboard disinkronisasi"), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", "Versi {} sudah tersedia."), + ("Accessible devices", "Perangkat yang tersedia"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Silahkan perbarui aplikasi RustDesk ke versi {} atau yang lebih baru pada komputer yang akan terhubung!"), + ("d3d_render_tip", "Ketika rendering D3D diaktifkan, layar kontrol jarak jauh bisa tampak hitam di beberapa komputer"), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", "Printer {} tidak terinstal"), + ("printer-{}-ready-tip", "Printer {} sudah terinstal dan siap digunakan."), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", "Remote Printing tidak diizinkan"), + ("remote-printing-disallowed-text-tip", "Komputer yang diakses tidak mengizinkan Remote Printing."), + ("save-settings-tip", "Simpan pengaturan"), + ("dont-show-again-tip", "Jangan tampilkan lagi"), + ("Take screenshot", "Ambil tangkapan layar"), + ("Taking screenshot", "Mengambil tangkapan layar"), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", "Simpan sebagai"), + ("Copy to clipboard", "Salin ke papan klip"), + ("Enable remote printer", "Aktifkan printer jarak jauh"), + ("Downloading {}", "Mendownload {}"), + ("{} Update", "Perbarui {}"), + ("{}-to-update-tip", "{} akan ditutup dan menginstal versi baru"), + ("download-new-version-failed-tip", "Gagal mendownload. Kamu bisa mencoba lagi nanti atau klik tombol \"Download\" melakukan download dari halaman rilis dan meningkatkan versi secara manual."), + ("Auto update", "Pembaruan otomatis"), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", "Gunakan WebSocket"), + ("Trackpad speed", "Kecepatan trackpad"), + ("Default trackpad speed", "Kecepatan default trackpad"), + ("Numeric one-time password", "Kata sandi sekali pakai numerik"), + ("Enable IPv6 P2P connection", "Aktifkan koneksi P2P IPv6"), + ("Enable UDP hole punching", "Aktifkan UDP hole punching"), + ("View camera", "Lihat Kamera"), + ("Enable camera", "Aktifkan kamera"), + ("No cameras", "Tidak ada kamera"), + ("view_camera_unsupported_tip", "Perangkat yang terhubung tidak mendukung tampilan kamera."), + ("Terminal", "Terminal"), + ("Enable terminal", "Aktifkan terminal"), + ("New tab", "Tab baru"), + ("Keep terminal sessions on disconnect", "Pertahankan sesi terminal saat terputus"), + ("Terminal (Run as administrator)", "Terminal (Jalankan sebagai administrator)"), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", "Gagal mendapatkan token pengguna."), + ("Incorrect username or password.", "Nama pengguna atau kata sandi salah."), + ("The user is not an administrator.", "Pengguna bukanlah administrator."), + ("Failed to check if the user is an administrator.", "Gagal memeriksa apakah pengguna adalah administrator."), + ("Supported only in the installed version.", "Hanya didukung pada versi yang terinstal."), + ("elevation_username_tip", "panduan_elevasi_nama_pengguna"), + ("Preparing for installation ...", "Mempersiapkan instalasi ..."), + ("Show my cursor", "Tampilkan kursor saya"), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Lanjutkan dengan {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/it.rs b/vendor/rustdesk/src/lang/it.rs new file mode 100644 index 0000000..479551f --- /dev/null +++ b/vendor/rustdesk/src/lang/it.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stato"), + ("Your Desktop", "Questo desktop"), + ("desk_tip", "Puoi accedere a questo desktop usando l'ID e la password indicati qui sotto."), + ("Password", "Password"), + ("Ready", "Pronto"), + ("Established", "Stabilita"), + ("connecting_status", "Connessione alla rete RustDesk..."), + ("Enable service", "Abilita servizio"), + ("Start service", "Avvia servizio"), + ("Service is running", "Il servizio è in esecuzione"), + ("Service is not running", "Il servizio non è in esecuzione"), + ("not_ready_status", "Non pronto. Verifica la connessione"), + ("Control Remote Desktop", "Controlla desktop remoto"), + ("Transfer file", "Trasferisci file"), + ("Connect", "Connetti"), + ("Recent sessions", "Sessioni recenti"), + ("Address book", "Rubrica"), + ("Confirmation", "Conferma"), + ("TCP tunneling", "Tunnel TCP"), + ("Remove", "Rimuovi"), + ("Refresh random password", "Nuova password casuale"), + ("Set your own password", "Imposta la password"), + ("Enable keyboard/mouse", "Abilita tastiera/mouse"), + ("Enable clipboard", "Abilita appunti"), + ("Enable file transfer", "Abilita trasferimento file"), + ("Enable TCP tunneling", "Abilita tunnel TCP"), + ("IP Whitelisting", "IP autorizzati"), + ("ID/Relay Server", "Server ID/Relay"), + ("Import server config", "Importa configurazione server dagli appunti"), + ("Export Server Config", "Esporta configurazione server negli appunti"), + ("Import server configuration successfully", "Configurazione server importata con successo"), + ("Export server configuration successfully", "Configurazione Server esportata con successo"), + ("Invalid server configuration", "Configurazione server non valida"), + ("Clipboard is empty", "Gli appunti sono vuoti"), + ("Stop service", "Arresta servizio"), + ("Change ID", "Cambia ID"), + ("Your new ID", "Il nuovo ID"), + ("length %min% to %max%", "lunghezza da %min% a %max%"), + ("starts with a letter", "inizia con una lettera"), + ("allowed characters", "caratteri consentiti"), + ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9, - (dash) e _ (sottolineato).\nIl primo carattere deve essere a-z o A-Z.\nLa lunghezza deve essere fra 6 e 16 caratteri."), + ("Website", "Sito web programma"), + ("About", "Info programma"), + ("Slogan_tip", "Realizzato con il cuore in questo mondo caotico!"), + ("Privacy Statement", "Informativa sulla privacy"), + ("Mute", "Audio disabilitato"), + ("Build Date", "Data build"), + ("Version", "Versione"), + ("Home", "Home"), + ("Audio Input", "Ingresso audio"), + ("Enhancements", "Miglioramenti"), + ("Hardware Codec", "Codec hardware"), + ("Adaptive bitrate", "Bitrate adattivo"), + ("ID Server", "ID server"), + ("Relay Server", "Server relay"), + ("API Server", "Server API"), + ("invalid_http", "deve iniziare con http:// o https://"), + ("Invalid IP", "Indirizzo IP non valido"), + ("Invalid format", "Formato non valido"), + ("server_not_support", "Non ancora supportato dal server"), + ("Not available", "Non disponibile"), + ("Too frequent", "Troppo frequente"), + ("Cancel", "Annulla"), + ("Skip", "Ignora"), + ("Close", "Chiudi"), + ("Retry", "Riprova"), + ("OK", "OK"), + ("Password Required", "Richiesta password"), + ("Please enter your password", "Inserisci la password"), + ("Remember password", "Ricorda password"), + ("Wrong Password", "Password errata"), + ("Do you want to enter again?", "Vuoi riprovare?"), + ("Connection Error", "Errore connessione"), + ("Error", "Errore"), + ("Reset by the peer", "Reimpostata dal dispositivo remoto"), + ("Connecting...", "Connessione..."), + ("Connection in progress. Please wait.", "Connessione..."), + ("Please try 1 minute later", "Riprova fra 1 minuto"), + ("Login Error", "Errore accesso"), + ("Successful", "Completato"), + ("Connected, waiting for image...", "Connesso, in attesa dell'immagine..."), + ("Name", "Nome"), + ("Type", "Tipo"), + ("Modified", "Modificato"), + ("Size", "Dimensione"), + ("Show Hidden Files", "Visualizza file nascosti"), + ("Receive", "Ricevi"), + ("Send", "Invia"), + ("Refresh File", "Aggiorna file"), + ("Local", "Locale"), + ("Remote", "Remoto"), + ("Remote Computer", "Computer remoto"), + ("Local Computer", "Computer locale"), + ("Confirm Delete", "Conferma eliminazione"), + ("Delete", "Elimina"), + ("Properties", "Proprietà"), + ("Multi Select", "Selezione multipla"), + ("Select All", "Seleziona tutto"), + ("Unselect All", "Deseleziona tutto"), + ("Empty Directory", "Cartella vuota"), + ("Not an empty directory", "Non è una cartella vuota"), + ("Are you sure you want to delete this file?", "Vuoi eliminare questo file?"), + ("Are you sure you want to delete this empty directory?", "Vuoi eliminare questa cartella vuota?"), + ("Are you sure you want to delete the file of this directory?", "Vuoi eliminare il file di questa cartella?"), + ("Do this for all conflicts", "Ricorda questa scelta per tutti i conflitti"), + ("This is irreversible!", "Questo è irreversibile!"), + ("Deleting", "Eliminazione di"), + ("files", "file"), + ("Waiting", "In attesa"), + ("Finished", "Completato"), + ("Speed", "Velocità"), + ("Custom Image Quality", "Qualità immagine personalizzata"), + ("Privacy mode", "Modalità privacy"), + ("Block user input", "Blocca input utente"), + ("Unblock user input", "Sblocca input utente"), + ("Adjust Window", "Adatta finestra"), + ("Original", "Originale"), + ("Shrink", "Restringi"), + ("Stretch", "Allarga"), + ("Scrollbar", "Barra scorrimento"), + ("ScrollAuto", "Scorri automaticamente"), + ("Good image quality", "Qualità immagine buona"), + ("Balanced", "Bilanciata qualità/velocità"), + ("Optimize reaction time", "Ottimizza tempo reazione"), + ("Custom", "Profilo personalizzato"), + ("Show remote cursor", "Visualizza cursore remoto"), + ("Show quality monitor", "Visualizza qualità video"), + ("Disable clipboard", "Disabilita appunti"), + ("Lock after session end", "Blocca al termine della sessione"), + ("Insert Ctrl + Alt + Del", "Inserisci Ctrl + Alt + Del"), + ("Insert Lock", "Blocco inserimento"), + ("Refresh", "Aggiorna"), + ("ID does not exist", "L'ID non esiste"), + ("Failed to connect to rendezvous server", "Errore connessione al server rendezvous"), + ("Please try later", "Riprova più tardi"), + ("Remote desktop is offline", "Il desktop remoto è offline"), + ("Key mismatch", "La chiave non corrisponde"), + ("Timeout", "Timeout"), + ("Failed to connect to relay server", "Errore connessione al server relay"), + ("Failed to connect via rendezvous server", "Errore connessione tramite il server rendezvous"), + ("Failed to connect via relay server", "Errore connessione tramite il server relay"), + ("Failed to make direct connection to remote desktop", "Impossibile connettersi direttamente al desktop remoto"), + ("Set Password", "Imposta password"), + ("OS Password", "Password sistema operativo"), + ("install_tip", "A causa del Controllo Account Utente (UAC), RustDesk potrebbe non funzionare correttamente come desktop remoto.\nPer evitare questo problema, fai clic sul tasto qui sotto per installare RustDesk a livello di sistema."), + ("Click to upgrade", "Aggiorna"), + ("Configure", "Configura"), + ("config_acc", "Per controllare il desktop dall'esterno, devi fornire a RustDesk il permesso 'Accessibilità'."), + ("config_screen", "Per controllare il desktop dall'esterno, devi fornire a RustDesk il permesso 'Registrazione schermo'."), + ("Installing ...", "Installazione ..."), + ("Install", "Installa"), + ("Installation", "Installazione"), + ("Installation Path", "Percorso installazione"), + ("Create start menu shortcuts", "Crea i collegamenti nel menu Start"), + ("Create desktop icon", "Crea un'icona sul desktop"), + ("agreement_tip", "Avviando l'installazione, accetti i termini del contratto di licenza."), + ("Accept and Install", "Accetta e installa"), + ("End-user license agreement", "Contratto di licenza utente finale"), + ("Generating ...", "Generazione ..."), + ("Your installation is lower version.", "Questa installazione non è aggiornata."), + ("not_close_tcp_tip", "Non chiudere questa finestra mentre stai usando il tunnel"), + ("Listening ...", "In ascolto ..."), + ("Remote Host", "Host remoto"), + ("Remote Port", "Porta remota"), + ("Action", "Azione"), + ("Add", "Aggiungi"), + ("Local Port", "Porta locale"), + ("Local Address", "Indirizzo locale"), + ("Change Local Port", "Cambia porta locale"), + ("setup_server_tip", "Per una connessione più veloce, configura uno specifico server"), + ("Too short, at least 6 characters.", "Troppo corta, almeno 6 caratteri"), + ("The confirmation is not identical.", "La password di conferma non corrisponde"), + ("Permissions", "Permessi"), + ("Accept", "Accetta"), + ("Dismiss", "Rifiuta"), + ("Disconnect", "Disconnetti"), + ("Enable file copy and paste", "Consenti copia e incolla di file"), + ("Connected", "Connesso"), + ("Direct and encrypted connection", "Connessione diretta e cifrata"), + ("Relayed and encrypted connection", "Connessione tramite relay e cifrata"), + ("Direct and unencrypted connection", "Connessione diretta e non cifrata"), + ("Relayed and unencrypted connection", "Connessione tramite relay e non cifrata"), + ("Enter Remote ID", "Inserisci ID remoto"), + ("Enter your password", "Inserisci la password"), + ("Logging in...", "Autenticazione..."), + ("Enable RDP session sharing", "Abilita condivisione sessione RDP"), + ("Auto Login", "Accesso automatico"), + ("Enable direct IP access", "Abilita accesso diretto tramite IP"), + ("Rename", "Rinomina"), + ("Space", "Spazio"), + ("Create desktop shortcut", "Crea collegamento sul desktop"), + ("Change Path", "Modifica percorso"), + ("Create Folder", "Crea cartella"), + ("Please enter the folder name", "Inserisci il nome della cartella"), + ("Fix it", "Risolvi"), + ("Warning", "Avviso"), + ("Login screen using Wayland is not supported", "La schermata di accesso non è supportata usando Wayland"), + ("Reboot required", "Riavvio necessario"), + ("Unsupported display server", "Display server non supportato"), + ("x11 expected", "necessario xll"), + ("Port", "Porta"), + ("Settings", "Impostazioni"), + ("Username", "Nome utente"), + ("Invalid port", "Numero porta non valido"), + ("Closed manually by the peer", "Chiuso manualmente dal dispositivo remoto"), + ("Enable remote configuration modification", "Abilita modifica remota configurazione"), + ("Run without install", "Esegui senza installare"), + ("Connect via relay", "Collegati tramite relay"), + ("Always connect via relay", "Collegati sempre tramite relay"), + ("whitelist_tip", "Possono connettersi a questo desktop solo gli indirizzi IP autorizzati"), + ("Login", "Accedi"), + ("Verify", "Verifica"), + ("Remember me", "Ricordami"), + ("Trust this device", "Registra questo dispositivo come attendibile"), + ("Verification code", "Codice di verifica"), + ("verification_tip", "È stato inviato un codice di verifica all'indirizzo email registrato, per accedere inserisci il codice di verifica."), + ("Logout", "Esci"), + ("Tags", "Etichette"), + ("Search ID", "Cerca ID"), + ("whitelist_sep", "Separati da virgola, punto e virgola, spazio o a capo"), + ("Add ID", "Aggiungi ID"), + ("Add Tag", "Aggiungi etichetta"), + ("Unselect all tags", "Deseleziona tutte le etichette"), + ("Network error", "Errore di rete"), + ("Username missed", "Nome utente mancante"), + ("Password missed", "Password mancante"), + ("Wrong credentials", "Credenziali errate"), + ("The verification code is incorrect or has expired", "Il codice di verifica non è corretto o è scaduto"), + ("Edit Tag", "Modifica etichetta"), + ("Forget Password", "Dimentica password"), + ("Favorites", "Preferiti"), + ("Add to Favorites", "Aggiungi ai preferiti"), + ("Remove from Favorites", "Rimuovi dai preferiti"), + ("Empty", "Vuoto"), + ("Invalid folder name", "Nome della cartella non valido"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Discovered", "Rilevate"), + ("install_daemon_tip", "Per avviare il programma all'accensione, è necessario installarlo come servizio di sistema."), + ("Remote ID", "ID remoto"), + ("Paste", "Incolla"), + ("Paste here?", "Incollare qui?"), + ("Are you sure to close the connection?", "Vuoi chiudere la connessione?"), + ("Download new version", "Scarica nuova versione"), + ("Touch mode", "Modalità tocco"), + ("Mouse mode", "Modalità mouse"), + ("One-Finger Tap", "Tocca con un dito"), + ("Left Mouse", "Mouse sinistro"), + ("One-Long Tap", "Tocco lungo con un dito"), + ("Two-Finger Tap", "Tocca con due dita"), + ("Right Mouse", "Mouse destro"), + ("One-Finger Move", "Movimento con un dito"), + ("Double Tap & Move", "Tocca due volte e sposta"), + ("Mouse Drag", "Trascina il mouse"), + ("Three-Finger vertically", "Tre dita in verticale"), + ("Mouse Wheel", "Rotellina del mouse"), + ("Two-Finger Move", "Movimento con due dita"), + ("Canvas Move", "Sposta tela"), + ("Pinch to Zoom", "Pizzica per zoomare"), + ("Canvas Zoom", "Zoom tela"), + ("Reset canvas", "Ripristina tela"), + ("No permission of file transfer", "Nessun permesso per il trasferimento file"), + ("Note", "Nota"), + ("Connection", "Connessione"), + ("Share screen", "Condividi schermo"), + ("Chat", "Chat"), + ("Total", "Totale"), + ("items", "Oggetti"), + ("Selected", "Selezionato"), + ("Screen Capture", "Cattura schermo"), + ("Input Control", "Controllo input"), + ("Audio Capture", "Acquisizione audio"), + ("Do you accept?", "Accetti?"), + ("Open System Setting", "Apri impostazioni di sistema"), + ("How to get Android input permission?", "Come ottenere l'autorizzazione input in Android?"), + ("android_input_permission_tip1", "Affinché un dispositivo remoto possa controllare un dispositivo Android tramite mouse o tocco, devi consentire a RustDesk di usare il servizio 'Accessibilità'."), + ("android_input_permission_tip2", "Vai nella pagina delle impostazioni di sistema che si aprirà di seguito, trova e accedi a [Servizi installati], attiva il servizio [RustDesk Input]."), + ("android_new_connection_tip", "È stata ricevuta una nuova richiesta di controllo per il dispositivo attuale."), + ("android_service_will_start_tip", "L'attivazione di Cattura schermo avvierà automaticamente il servizio, consentendo ad altri dispositivi di richiedere una connessione da questo dispositivo."), + ("android_stop_service_tip", "La chiusura del servizio chiuderà automaticamente tutte le connessioni stabilite."), + ("android_version_audio_tip", "L'attuale versione di Android non supporta l'acquisizione audio, esegui l'aggiornamento ad Android 10 o versioni successive."), + ("android_start_service_tip", "Per avviare il servizio di condivisione dello schermo seleziona [Avvia servizio] o abilita l'autorizzazione [Cattura schermo]."), + ("android_permission_may_not_change_tip", "Le autorizzazioni per le connessioni stabilite non possono essere modificate istantaneamente fino alla riconnessione."), + ("Account", "Account"), + ("Overwrite", "Sovrascrivi"), + ("This file exists, skip or overwrite this file?", "Questo file esiste, vuoi ignorarlo o sovrascrivere questo file?"), + ("Quit", "Esci"), + ("Help", "Aiuto"), + ("Failed", "Fallito"), + ("Succeeded", "Completato"), + ("Someone turns on privacy mode, exit", "Qualcuno ha attivato la modalità privacy, uscita"), + ("Unsupported", "Non supportato"), + ("Peer denied", "Accesso negato al dispositivo remoto"), + ("Please install plugins", "Installa i plugin"), + ("Peer exit", "Uscita dal dispostivo remoto"), + ("Failed to turn off", "Impossibile spegnere"), + ("Turned off", "Spegni"), + ("Language", "Lingua"), + ("Keep RustDesk background service", "Mantieni il servizio di RustDesk in background"), + ("Ignore Battery Optimizations", "Ignora le ottimizzazioni della batteria"), + ("android_open_battery_optimizations_tip", "Se vuoi disabilitare questa funzione, vai nelle impostazioni dell'applicazione RustDesk, apri la sezione 'Batteria' e deseleziona 'Senza restrizioni'."), + ("Start on boot", "Avvia all'accensione"), + ("Start the screen sharing service on boot, requires special permissions", "L'avvio del servizio di condivisione dello schermo all'accensione richiede autorizzazioni speciali"), + ("Connection not allowed", "Connessione non consentita"), + ("Legacy mode", "Modalità legacy"), + ("Map mode", "Modalità mappa"), + ("Translate mode", "Modalità traduzione"), + ("Use permanent password", "Usa password permanente"), + ("Use both passwords", "Usa password monouso e permanente"), + ("Set permanent password", "Imposta password permanente"), + ("Enable remote restart", "Abilita riavvio da remoto"), + ("Restart remote device", "Riavvia dispositivo remoto"), + ("Are you sure you want to restart", "Vuoi riavviare?"), + ("Restarting remote device", "Il dispositivo remoto si sta riavviando"), + ("remote_restarting_tip", "Riavvia il dispositivo remoto"), + ("Copied", "Copiato"), + ("Exit Fullscreen", "Esci dalla modalità schermo intero"), + ("Fullscreen", "A schermo intero"), + ("Mobile Actions", "Azioni mobili"), + ("Select Monitor", "Seleziona schermo"), + ("Control Actions", "Azioni controllo"), + ("Display Settings", "Impostazioni visualizzazione"), + ("Ratio", "Rapporto"), + ("Image Quality", "Qualità immagine"), + ("Scroll Style", "Stile scorrimento"), + ("Show Toolbar", "Visualizza barra strumenti"), + ("Hide Toolbar", "Nascondi barra strumenti"), + ("Direct Connection", "Connessione diretta"), + ("Relay Connection", "Connessione relay"), + ("Secure Connection", "Connessione sicura"), + ("Insecure Connection", "Connessione non sicura"), + ("Scale original", "Scala originale"), + ("Scale adaptive", "Scala adattiva"), + ("General", "Generale"), + ("Security", "Sicurezza"), + ("Theme", "Tema"), + ("Dark Theme", "Tema scuro"), + ("Light Theme", "Tema chiaro"), + ("Dark", "Scuro"), + ("Light", "Chiaro"), + ("Follow System", "Sistema"), + ("Enable hardware codec", "Abilita codec hardware"), + ("Unlock Security Settings", "Sblocca impostazioni sicurezza"), + ("Enable audio", "Abilita audio"), + ("Unlock Network Settings", "Sblocca impostazioni di rete"), + ("Server", "Server"), + ("Direct IP Access", "Accesso IP diretto"), + ("Proxy", "Proxy"), + ("Apply", "Applica"), + ("Disconnect all devices?", "Vuoi disconnettere tutti i dispositivi?"), + ("Clear", "Azzera"), + ("Audio Input Device", "Dispositivo ingresso audio"), + ("Use IP Whitelisting", "Usa elenco IP autorizzati"), + ("Network", "Rete"), + ("Pin Toolbar", "Blocca barra strumenti"), + ("Unpin Toolbar", "Sblocca barra strumenti"), + ("Recording", "Registrazione"), + ("Directory", "Cartella"), + ("Automatically record incoming sessions", "Registra automaticamente sessioni in entrata"), + ("Automatically record outgoing sessions", "Registra automaticamente sessioni in uscita"), + ("Change", "Modifica"), + ("Start session recording", "Inizia registrazione sessione"), + ("Stop session recording", "Ferma registrazione sessione"), + ("Enable recording session", "Abilita registrazione sessione"), + ("Enable LAN discovery", "Abilita rilevamento LAN"), + ("Deny LAN discovery", "Non effettuare rilevamento LAN"), + ("Write a message", "Scrivi un messaggio"), + ("Prompt", "Richiedi"), + ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), + ("elevated_foreground_window_tip", "La finestra attuale del desktop remoto richiede per funzionare privilegi più elevati, quindi non è possibile usare temporaneamente il mouse e la tastiera.\nÈ possibile chiedere all'utente remoto di ridurre a icona la finestra attuale o di selezionare il pulsante di elevazione nella finestra di gestione della connessione.\nPer evitare questo problema, ti consigliamo di installare il software nel dispositivo remoto."), + ("Disconnected", "Disconnesso"), + ("Other", "Altro"), + ("Confirm before closing multiple tabs", "Conferma prima di chiudere più schede"), + ("Keyboard Settings", "Impostazioni tastiera"), + ("Full Access", "Accesso completo"), + ("Screen Share", "Condivisione schermo"), + ("ubuntu-21-04-required", "Wayland richiede Ubuntu 21.04 o versione successiva."), + ("wayland-requires-higher-linux-version", "Wayland richiede una versione superiore della distribuzione Linux.\nProva X11 desktop o cambia il sistema operativo."), + ("xdp-portal-unavailable", "Acquisizione dello schermo di Wayland non riuscita. Il portale desktop XDG potrebbe essersi bloccato o non essere disponibile. Prova a riavviarlo con `systemctl --user restart xdg-desktop-portal`."), + ("JumpLink", "Vai a"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seleziona lo schermo da condividere (opera sul lato dispositivo remoto)."), + ("Show RustDesk", "Visualizza RustDesk"), + ("This PC", "Questo PC"), + ("or", "O"), + ("Elevate", "Eleva"), + ("Zoom cursor", "Cursore zoom"), + ("Accept sessions via password", "Accetta sessioni via password"), + ("Accept sessions via click", "Accetta sessioni via clic"), + ("Accept sessions via both", "Accetta sessioni con entrambe le password"), + ("Please wait for the remote side to accept your session request...", "Attendi che il dispositivo remoto accetti la richiesta di sessione..."), + ("One-time Password", "Password monouso"), + ("Use one-time password", "Usa password monouso"), + ("One-time password length", "Lunghezza password monouso"), + ("Request access to your device", "Richiedi accesso al dispositivo"), + ("Hide connection management window", "Nascondi la finestra di gestione delle connessioni"), + ("hide_cm_tip", "Permetti di nascondere solo se si accettano sessioni con password permanente"), + ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, se vuoi un accesso stabile usa X11."), + ("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"), + ("Skipped", "Saltato"), + ("Add to address book", "Aggiungi alla rubrica"), + ("Group", "Gruppo"), + ("Search", "Cerca"), + ("Closed manually by web console", "Chiudi manualmente dalla console web"), + ("Local keyboard type", "Tipo tastiera locale"), + ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), + ("software_render_tip", "Se nel computer con Linux è presente una scheda grafica Nvidia e la finestra remota si chiude immediatamente dopo la connessione, installa il nuovo driver open source e usa il rendering software.\nPotrebbe essere necessario un riavvio del programma."), + ("Always use software rendering", "Usa sempre rendering software"), + ("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk 'Monitoraggio input'."), + ("config_microphone", "Per poter chiamare, è necessario concedere l'autorizzazione 'Registra audio' a RustDesk."), + ("request_elevation_tip", "Se c'è qualcuno nel lato remoto è possibile richiedere l'elevazione."), + ("Wait", "Attendi"), + ("Elevation Error", "Errore durante elevazione dei diritti"), + ("Ask the remote user for authentication", "Chiedi autenticazione all'utente remoto"), + ("Choose this if the remote account is administrator", "Scegli questa opzione se l'account remoto è amministratore"), + ("Transmit the username and password of administrator", "Trasmetti il nome utente e la password dell'amministratore"), + ("still_click_uac_tip", "Richiedi ancora che l'utente remoto selezioni 'OK' nella finestra UAC dell'esecuzione di RustDesk."), + ("Request Elevation", "Richiedi elevazione dei diritti"), + ("wait_accept_uac_tip", "Attendi che l'utente remoto accetti la finestra di dialogo UAC."), + ("Elevate successfully", "Elevazione dei diritti effettuata correttamente"), + ("uppercase", "Maiuscola"), + ("lowercase", "Minuscola"), + ("digit", "Numero"), + ("special character", "Carattere speciale"), + ("length>=8", "Lunghezza >= 8"), + ("Weak", "Debole"), + ("Medium", "Media"), + ("Strong", "Forte"), + ("Switch Sides", "Cambia lato"), + ("Please confirm if you want to share your desktop?", "Vuoi condividere il desktop?"), + ("Display", "Visualizzazione"), + ("Default View Style", "Stile visualizzazione predefinito"), + ("Default Scroll Style", "Stile scorrimento predefinito"), + ("Default Image Quality", "Qualità immagine predefinita"), + ("Default Codec", "Codec predefinito"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Automatico"), + ("Other Default Options", "Altre opzioni predefinite"), + ("Voice call", "Chiamata vocale"), + ("Text chat", "Chat testuale"), + ("Stop voice call", "Interrompi chiamata vocale"), + ("relay_hint_tip", "Se non è possibile connettersi direttamente, puoi provare a farlo tramite relay.\nInoltre, se si vuoi usare il relay al primo tentativo, è possibile aggiungere all'ID il suffisso '/r\' o selezionare nella scheda se esiste l'opzione 'Collegati sempre tramite relay'."), + ("Reconnect", "Riconnetti"), + ("Codec", "Codec"), + ("Resolution", "Risoluzione"), + ("No transfers in progress", "Nessun trasferimento in corso"), + ("Set one-time password length", "Imposta lunghezza password monouso"), + ("RDP Settings", "Impostazioni RDP"), + ("Sort by", "Ordina per"), + ("New Connection", "Nuova connessione"), + ("Restore", "Ripristina"), + ("Minimize", "Minimizza"), + ("Maximize", "Massimizza"), + ("Your Device", "Questo dispositivo"), + ("empty_recent_tip", "Non c'è nessuna sessione recente!\nPianificane una."), + ("empty_favorite_tip", "Ancora nessuna connessione?\nTrova qualcuno con cui connetterti e aggiungilo ai preferiti!"), + ("empty_lan_tip", "Sembra proprio che non sia stata rilevata nessuna connessione."), + ("empty_address_book_tip", "Sembra che per ora nella rubrica non ci siano connessioni."), + ("Empty Username", "Nome utente vuoto"), + ("Empty Password", "Password vuota"), + ("Me", "Io"), + ("identical_file_tip", "Questo file è identico a quello nel dispositivo remoto."), + ("show_monitors_tip", "Visualizza schermi nella barra strumenti"), + ("View Mode", "Modalità visualizzazione"), + ("login_linux_tip", "Accedi all'account Linux remoto"), + ("verify_rustdesk_password_tip", "Conferma password RustDesk"), + ("remember_account_tip", "Ricorda questo account"), + ("os_account_desk_tip", "Questo account viene usato per accedere al sistema operativo remoto e attivare la sessione desktop in modalità non presidiata."), + ("OS Account", "Account sistema operativo"), + ("another_user_login_title_tip", "È già loggato un altro utente."), + ("another_user_login_text_tip", "Separato"), + ("xorg_not_found_title_tip", "Xorg non trovato."), + ("xorg_not_found_text_tip", "Installa Xorg."), + ("no_desktop_title_tip", "Non è presente alcun ambiente desktop disponibile."), + ("no_desktop_text_tip", "Installa il desktop GNOME."), + ("No need to elevate", "Elevazione dei privilegi non richiesta"), + ("System Sound", "Dispositivo audio sistema"), + ("Default", "Predefinita"), + ("New RDP", "Nuovo RDP"), + ("Fingerprint", "Firma digitale"), + ("Copy Fingerprint", "Copia firma digitale"), + ("no fingerprints", "Nessuna firma digitale"), + ("Select a peer", "Seleziona dispositivo remoto"), + ("Select peers", "Seleziona dispositivi remoti"), + ("Plugins", "Plugin"), + ("Uninstall", "Disinstalla"), + ("Update", "Aggiorna"), + ("Enable", "Abilita"), + ("Disable", "Disabilita"), + ("Options", "Opzioni"), + ("resolution_original_tip", "Risoluzione originale"), + ("resolution_fit_local_tip", "Adatta risoluzione locale"), + ("resolution_custom_tip", "Risoluzione personalizzata"), + ("Collapse toolbar", "Comprimi barra strumenti"), + ("Accept and Elevate", "Accetta ed eleva"), + ("accept_and_elevate_btn_tooltip", "Accetta la connessione ed eleva le autorizzazioni UAC."), + ("clipboard_wait_response_timeout_tip", "Timeout attesa risposta della copia."), + ("Incoming connection", "Connessioni in entrata"), + ("Outgoing connection", "Connessioni in uscita"), + ("Exit", "Esci da RustDesk"), + ("Open", "Apri RustDesk"), + ("logout_tip", "Vuoi disconnetterti?"), + ("Service", "Servizio"), + ("Start", "Avvia"), + ("Stop", "Ferma"), + ("exceed_max_devices", "Hai raggiunto il numero massimo di dispositivi gestibili."), + ("Sync with recent sessions", "Sincronizza con le sessioni recenti"), + ("Sort tags", "Ordina etichette"), + ("Open connection in new tab", "Apri connessione in una nuova scheda"), + ("Move tab to new window", "Sposta scheda nella finestra successiva"), + ("Can not be empty", "Non può essere vuoto"), + ("Already exists", "Esiste già"), + ("Change Password", "Modifica password"), + ("Refresh Password", "Aggiorna password"), + ("ID", "ID"), + ("Grid View", "Vista griglia"), + ("List View", "Vista elenco"), + ("Select", "Seleziona"), + ("Toggle Tags", "Attiva/disattiva tag"), + ("pull_ab_failed_tip", "Impossibile aggiornare la rubrica"), + ("push_ab_failed_tip", "Impossibile sincronizzare la rubrica con il server"), + ("synced_peer_readded_tip", "I dispositivi presenti nelle sessioni recenti saranno sincronizzati di nuovo nella rubrica."), + ("Change Color", "Modifica colore"), + ("Primary Color", "Colore primario"), + ("HSV Color", "Colore HSV"), + ("Installation Successful!", "Installazione completata"), + ("Installation failed!", "Installazione fallita"), + ("Reverse mouse wheel", "Funzione rotellina mouse inversa"), + ("{} sessions", "{} sessioni"), + ("scam_title", "Potresti essere stato TRUFFATO!"), + ("scam_text1", "Se sei al telefono con qualcuno che NON conosci NON DI TUA FIDUCIA che ti ha chiesto di usare RustDesk e di avviare il servizio, non procedere e riattacca subito."), + ("scam_text2", "Probabilmente è un truffatore che cerca di rubare i tuoi soldi o altre informazioni private."), + ("Don't show again", "Non visualizzare più"), + ("I Agree", "Accetto"), + ("Decline", "Non accetto"), + ("Timeout in minutes", "Timeout in minuti"), + ("auto_disconnect_option_tip", "Chiudi automaticamente sessioni in entrata per inattività utente"), + ("Connection failed due to inactivity", "Connessione non riuscita a causa di inattività"), + ("Check for software update on startup", "All'avvio verifica presenza aggiornamenti programma"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Aggiorna RustDesk Server Pro alla versione {} o successiva!"), + ("pull_group_failed_tip", "Impossibile aggiornare il gruppo"), + ("Filter by intersection", "Filtra per incrocio"), + ("Remove wallpaper during incoming sessions", "Rimuovi sfondo durante sessioni in entrata"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Lo schermo è scollegato, passo al primo schermo."), + ("No displays", "Nessuno schermo"), + ("Open in new window", "Apri in una nuova finestra"), + ("Show displays as individual windows", "Visualizza schermi come finestre individuali"), + ("Use all my displays for the remote session", "Nella sessione remota usa tutti gli schermi"), + ("selinux_tip", "In questo dispositivo è abilitato SELinux, che potrebbe impedire il corretto funzionamento di RustDesk come lato controllato."), + ("Change view", "Modifica vista"), + ("Big tiles", "Icone grandi"), + ("Small tiles", "Icone piccole"), + ("List", "Elenco"), + ("Virtual display", "Schermo virtuale"), + ("Plug out all", "Scollega tutto"), + ("True color (4:4:4)", "Colore reale (4:4:4)"), + ("Enable blocking user input", "Abilita blocco input utente"), + ("id_input_tip", "Puoi inserire un ID, un IP diretto o un dominio con una porta (:).\nSe vuoi accedere as un dispositivo in un altro server, aggiungi l'indirizzo del server (@?key=), ad esempio\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere as un dispositivo in un server pubblico, inserisci \"@public\", per il server pubblico la chiave non è necessaria\n\nSe vuoi forzare l'uso di una connessione di inoltro alla prima connessione, aggiungi \"/r\" alla fine dell'ID, ad esempio \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Modo 1"), + ("privacy_mode_impl_virtual_display_tip", "Modo 2"), + ("Enter privacy mode", "Entra in modalità privacy"), + ("Exit privacy mode", "Esci dalla modalità privacy"), + ("idd_not_support_under_win10_2004_tip", "Il driver video indiretto non è supportato. È richiesto Windows 10, versione 2004 o successiva."), + ("input_source_1_tip", "Sorgente ingresso (1)"), + ("input_source_2_tip", "Sorgente ingresso (2)"), + ("Swap control-command key", "Scambia tasto controllo-comando"), + ("swap-left-right-mouse", "Scambia pulsante sinistro-destro mouse"), + ("2FA code", "Codice 2FA"), + ("More", "Altro"), + ("enable-2fa-title", "Abilita autenticazione a due fattori"), + ("enable-2fa-desc", "Configura l'autenticatore.\nPuoi usare un'app di autenticazione come Authy, Microsoft o Google Authenticator sul telefono o desktop.\n\nPer abilitare l'autenticazione a due fattori scansiona il codice QR con l'app e inserisci il codice visualizzato dall'app."), + ("wrong-2fa-code", "Impossibile verificare il codice.\nVerifica che le impostazioni del codice e dell'ora locale siano corrette"), + ("enter-2fa-title", "Autenticazione a due fattori"), + ("Email verification code must be 6 characters.", "Il codice di verifica email deve contenere 6 caratteri."), + ("2FA code must be 6 digits.", "Il codice 2FA deve essere composto da 6 cifre."), + ("Multiple Windows sessions found", "Rilevate sessioni Windows multiple"), + ("Please select the session you want to connect to", "Seleziona la sessione a cui connetterti"), + ("powered_by_me", "Alimentato da RustDesk"), + ("outgoing_only_desk_tip", "Questa è un'edizione personalizzata.\nPuoi connetterti ad altri dispositivi, ma gli altri dispositivi non possono connettersi a questo dispositivo."), + ("preset_password_warning", "Questa è un'edizione personalizzata e viene fornita con una password preimpostata.\nChiunque conosca questa password potrebbe ottenere il pieno controllo del dispositivo.\nSe non te lo aspettavi, disinstalla immediatamente il software."), + ("Security Alert", "Avviso sicurezza"), + ("My address book", "Rubrica"), + ("Personal", "Personale"), + ("Owner", "Proprietario"), + ("Set shared password", "Imposta password condivisa"), + ("Exist in", "Esiste in"), + ("Read-only", "Sola lettura"), + ("Read/Write", "Lettura/scrittura"), + ("Full Control", "Controllo completo"), + ("share_warning_tip", "I campi sopra indicati sono condivisi e visibili ad altri."), + ("Everyone", "Everyone"), + ("ab_web_console_tip", "Altre info sulla console web"), + ("allow-only-conn-window-open-tip", "Consenti connessione solo se la finestra RustDesk è aperta"), + ("no_need_privacy_mode_no_physical_displays_tip", "Nessun display fisico, nessuna necessità di usare la modalità privacy."), + ("Follow remote cursor", "Segui cursore remoto"), + ("Follow remote window focus", "Segui focus finestra remota"), + ("default_proxy_tip", "Protocollo e porta predefiniti sono Socks5 e 1080"), + ("no_audio_input_device_tip", "Nessun dispositivo input audio trovato."), + ("Incoming", "In entrata"), + ("Outgoing", "In uscita"), + ("Clear Wayland screen selection", "Annulla selezione schermata Wayland"), + ("clear_Wayland_screen_selection_tip", "Dopo aver annullato la selezione schermo, è possibile selezionare nuovamente lo schermo da condividere."), + ("confirm_clear_Wayland_screen_selection_tip", "Vuoi annullare la selezione schermo Wayland?"), + ("android_new_voice_call_tip", "È stata ricevuta una nuova richiesta di chiamata vocale. Se accetti, l'audio passerà alla comunicazione vocale."), + ("texture_render_tip", "Usa il rendering texture per rendere le immagini più fluide. Se riscontri problemi di rendering prova a disabilitare questa opzione."), + ("Use texture rendering", "Usa rendering texture"), + ("Floating window", "Finestra galleggiante"), + ("floating_window_tip", "Aiuta a mantenere il servizio Rustdesk in background."), + ("Keep screen on", "Mantieni schermo acceso"), + ("Never", "Mai"), + ("During controlled", "Durante il controllo"), + ("During service is on", "Quando il servizio è attivo"), + ("Capture screen using DirectX", "Cattura schermo usando DirectX"), + ("Back", "Indietro"), + ("Apps", "App"), + ("Volume up", "Volume +"), + ("Volume down", "Volume -"), + ("Power", "Alimentazione"), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Se abiliti questa funzione, puoi ricevere il codice 2FA dal tuo bot.\nPuò anche funzionare come notifica di connessione."), + ("enable-bot-desc", "1. apri una chat con @BotFather.\n2. Invia il comando \"/newbot\", dopo aver completato questo passaggio riceverai un token.\n3. Avvia una chat con il tuo bot appena creato. Per attivarlo Invia un messaggio che inizia con una barra (\"/\") tipo \"/hello\".\n"), + ("cancel-2fa-confirm-tip", "Vuoi disabilitare 2FA?"), + ("cancel-bot-confirm-tip", "Vuoi disabilitare il bot Telegram?"), + ("About RustDesk", "Info su RustDesk"), + ("Send clipboard keystrokes", "Invia sequenze tasti appunti"), + ("network_error_tip", "Controlla la connessione di rete, quindi seleziona 'Riprova'."), + ("Unlock with PIN", "Abilita sblocco con PIN"), + ("Requires at least {} characters", "Richiede almeno {} caratteri"), + ("Wrong PIN", "PIN errato"), + ("Set PIN", "Imposta PIN"), + ("Enable trusted devices", "Abilita dispositivi attendibili"), + ("Manage trusted devices", "Gestisci dispositivi attendibili"), + ("Platform", "Piattaforma"), + ("Days remaining", "Giorni rimanenti"), + ("enable-trusted-devices-tip", "Salta verifica 2FA nei dispositivi attendibili"), + ("Parent directory", "Cartella principale"), + ("Resume", "Riprendi"), + ("Invalid file name", "Nome file non valido"), + ("one-way-file-transfer-tip", "Sul lato controllato è abilitato il trasferimento file unidirezionale."), + ("Authentication Required", "Richiesta autenticazione"), + ("Authenticate", "Autentica"), + ("web_id_input_tip", "È possibile inserire un ID nello stesso server, nel client web non è supportato l'accesso con IP diretto.\nSe vuoi accedere ad un dispositivo in un altro server, aggiungi l'indirizzo del server (@?key=), ad esempio,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vuoi accedere ad un dispositivo in un server pubblico, inserisci \"@public\", la chiave non è necessaria per il server pubblico."), + ("Download", "Download"), + ("Upload folder", "Cartella upload"), + ("Upload files", "File upload"), + ("Clipboard is synchronized", "Gli appunti sono sincronizzati"), + ("Update client clipboard", "Aggiorna appunti client"), + ("Untagged", "Senza tag"), + ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), + ("Accessible devices", "Dispositivi accessibili"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Aggiorna il client RustDesk remoto alla versione {} o successiva!"), + ("d3d_render_tip", "Quando è abilitato il rendering D3D, in alcuni computer la schermata del telecomando potrebbe essere nera."), + ("Use D3D rendering", "Usa rendering D3D"), + ("Printer", "Stampante"), + ("printer-os-requirement-tip", "La funzione della stampante richiede Windows 10 o superiore."), + ("printer-requires-installed-{}-client-tip", "Per usare la stampa remota, {} è necessario installare il programma nel dispositivo."), + ("printer-{}-not-installed-tip", "La stampante {} non è installata."), + ("printer-{}-ready-tip", "La stampante {} è installata e pronta all'uso."), + ("Install {} Printer", "Installa la stampante {}"), + ("Outgoing Print Jobs", "Lavori di stampa in uscita"), + ("Incoming Print Jobs", "Lavori di stampa in entrata"), + ("Incoming Print Job", "Lavoro di stampa in entrata"), + ("use-the-default-printer-tip", "Usa la stampante predefinita"), + ("use-the-selected-printer-tip", "Usa la stampante selezionata"), + ("auto-print-tip", "Stampa usando automaticamente la stampante selezionata."), + ("print-incoming-job-confirm-tip", "Hai ricevuto un lavoro di stampa da remoto. Vuoi eseguirlo sul desktop?"), + ("remote-printing-disallowed-tile-tip", "Stampa remota disabilitata"), + ("remote-printing-disallowed-text-tip", "Le impostazioni di autorizzazione del lato controllato negano la stampa remota."), + ("save-settings-tip", "Salva impostazioni"), + ("dont-show-again-tip", "Non visualizzare più questo messaggio"), + ("Take screenshot", "Cattura schermata"), + ("Taking screenshot", "Cattura schermata"), + ("screenshot-merged-screen-not-supported-tip", "L'unione della cattura di schermate di più display non è attualmente supportata.\nPassa ad un singolo display e riprova."), + ("screenshot-action-tip", "Seleziona come continuare con la schermata."), + ("Save as", "Salva come"), + ("Copy to clipboard", "Copia negli appunti"), + ("Enable remote printer", "Abilita stampante remota"), + ("Downloading {}", "Download {}"), + ("{} Update", "Aggiorna {}"), + ("{}-to-update-tip", "{} si chiuderà e installerà la nuova versione"), + ("download-new-version-failed-tip", "Download non riuscito.\nÈ possibile riprovare o selezionare 'Download' per scaricare e aggiornarlo manualmente."), + ("Auto update", "Aggiornamento automatico"), + ("update-failed-check-msi-tip", "Controllo metodo installazione non riuscito.\nSeleziona 'Download' per scaricare il programma e aggiornarlo manualmente."), + ("websocket_tip", "Quando usi WebSocket, sono supportate solo le connessioni relay."), + ("Use WebSocket", "Usa WebSocket"), + ("Trackpad speed", "Velocità trackpad"), + ("Default trackpad speed", "Velocità predefinita trackpad"), + ("Numeric one-time password", "Password numerica monouso"), + ("Enable IPv6 P2P connection", "Abilita connessione P2P IPv6"), + ("Enable UDP hole punching", "Abilita hole punching UDP"), + ("View camera", "Visualizza telecamera"), + ("Enable camera", "Abilita camera"), + ("No cameras", "Nessuna camera"), + ("view_camera_unsupported_tip", "Il dispositivo remoto non supporta la visualizzazione della camera."), + ("Terminal", "Terminale"), + ("Enable terminal", "Abilita terminale"), + ("New tab", "Nuova scheda"), + ("Keep terminal sessions on disconnect", "Quando disconetti mantieni attiva sessione terminale"), + ("Terminal (Run as administrator)", "Terminale (esegui come amministratore)"), + ("terminal-admin-login-tip", "Inserisci il nome utente e la password dell'amministratore del lato controllato."), + ("Failed to get user token.", "Impossibile ottenere il token utente."), + ("Incorrect username or password.", "Nome utente o password non corretti."), + ("The user is not an administrator.", "L'utente non è un amministratore."), + ("Failed to check if the user is an administrator.", "Impossibile verificare se l'utente è un amministratore."), + ("Supported only in the installed version.", "Supportato solo nella versione installata."), + ("elevation_username_tip", "Inserisci Nome utente o dominio sorgente\\nome Utente"), + ("Preparing for installation ...", "Preparazione installazione..."), + ("Show my cursor", "Visualizza il mio cursore"), + ("Scale custom", "Scala personalizzata"), + ("Custom scale slider", "Cursore scala personalizzata"), + ("Decrease", "Diminuisci"), + ("Increase", "Aumenta"), + ("Show virtual mouse", "Visualizza mouse virtuale"), + ("Virtual mouse size", "Dimensione mouse virtuale"), + ("Small", "Piccola"), + ("Large", "Grande"), + ("Show virtual joystick", "Visualizza joystick virtuale"), + ("Edit note", "Modifica nota"), + ("Alias", "Alias"), + ("ScrollEdge", "Bordo scorrimento"), + ("Allow insecure TLS fallback", "Consenti fallback TLS non sicuro"), + ("allow-insecure-tls-fallback-tip", "Per impostazione predefinita, RustDesk verifica il certificato del server per i protocolli usando TLS.\nCon questa opzione abilitata, RustDesk salterà il passaggio di verifica e procederà in caso di errore di verifica."), + ("Disable UDP", "Disabilita UDP"), + ("disable-udp-tip", "Controlla se usare solo TCP.\nQuando questa opzione è abilitata, RustDesk non userà più UDP 21116, verrà invece usato TCP 21116."), + ("server-oss-not-support-tip", "Nota: il sistema operativo del server RustDesk non include questa funzionalità."), + ("input note here", "Inserisci nota qui"), + ("note-at-conn-end-tip", "Visualizza nota alla fine della connessione"), + ("Show terminal extra keys", "Visualizza tasti aggiuntivi terminale"), + ("Relative mouse mode", "Modalità relativa mouse"), + ("rel-mouse-not-supported-peer-tip", "La modalità mouse relativa non è supportata dal peer connesso."), + ("rel-mouse-not-ready-tip", "La modalità mouse relativa non è ancora pronta. Riprova."), + ("rel-mouse-lock-failed-tip", "Impossibile bloccare il cursore. La modalità mouse relativa è stata disabilitata."), + ("rel-mouse-exit-{}-tip", "Premi {} per uscire."), + ("rel-mouse-permission-lost-tip", "È stata revocato l'accesso alla tastiera. La modalità mouse relativa è stata disabilitata."), + ("Changelog", "Novità programma"), + ("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"), + ("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"), + ("Continue with {}", "Continua con {}"), + ("Display Name", "Visualizza nome"), + ("password-hidden-tip", "È impostata una password permanente (nascosta)."), + ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), + ("Enable privacy mode", "Abilita modalità privacy"), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/ja.rs b/vendor/rustdesk/src/lang/ja.rs new file mode 100644 index 0000000..20caca0 --- /dev/null +++ b/vendor/rustdesk/src/lang/ja.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "状態"), + ("Your Desktop", "あなたのコンピューター"), + ("desk_tip", "下記の ID とパスワードでこのコンピューターにアクセスできます。"), + ("Password", "パスワード"), + ("Ready", "準備完了"), + ("Established", "接続完了"), + ("connecting_status", "RustDesk ネットワークに接続中..."), + ("Enable service", "サービスを有効化する"), + ("Start service", "サービスを開始"), + ("Service is running", "サービスが実行されています"), + ("Service is not running", "サービスは停止しています"), + ("not_ready_status", "接続できません。ネットワーク接続を確認してください"), + ("Control Remote Desktop", "リモートデスクトップを操作"), + ("Transfer file", "ファイルを転送"), + ("Connect", "接続"), + ("Recent sessions", "最近のセッション"), + ("Address book", "アドレス帳"), + ("Confirmation", "確認"), + ("TCP tunneling", "TCP トンネリング"), + ("Remove", "削除"), + ("Refresh random password", "ランダムパスワードを再生成"), + ("Set your own password", "パスワードを設定"), + ("Enable keyboard/mouse", "キーボード/マウスを有効化する"), + ("Enable clipboard", "クリップボードを有効化する"), + ("Enable file transfer", "ファイル転送を有効化する"), + ("Enable TCP tunneling", "TCP トンネリングを有効化する"), + ("IP Whitelisting", "IP ホワイトリスト"), + ("ID/Relay Server", "認証/中継サーバー"), + ("Import server config", "サーバー設定をインポート"), + ("Export Server Config", "サーバー設定をエクスポート"), + ("Import server configuration successfully", "サーバー設定のインポートに成功しました"), + ("Export server configuration successfully", "サーバー設定のエクスポートに成功しました"), + ("Invalid server configuration", "無効なサーバー設定です"), + ("Clipboard is empty", "クリップボードは空です"), + ("Stop service", "サービスを停止"), + ("Change ID", "ID を変更"), + ("Your new ID", "新しい ID"), + ("length %min% to %max%", "%min%~%max% 文字の長さ"), + ("starts with a letter", "アルファベットで始まる"), + ("allowed characters", "使用可能な文字"), + ("id_change_tip", "使用できるのは大文字・小文字のアルファベット、数字、アンダースコア (_) のみです。先頭の文字はアルファベット、長さは 6 文字から 16 文字である必要があります。"), + ("Website", "公式サイト"), + ("About", "RustDesk について"), + ("Slogan_tip", "この混沌とした世界から、愛をこめて!"), + ("Privacy Statement", "プライバシーポリシー"), + ("Mute", "ミュート"), + ("Build Date", "ビルド日時"), + ("Version", "バージョン"), + ("Home", "ホーム"), + ("Audio Input", "オーディオ入力"), + ("Enhancements", "拡張機能"), + ("Hardware Codec", "ハードウェアコーデック"), + ("Adaptive bitrate", "可変ビットレートを使用する"), + ("ID Server", "認証サーバー"), + ("Relay Server", "中継サーバー"), + ("API Server", "API サーバー"), + ("invalid_http", "http:// または https:// から始まる必要があります。"), + ("Invalid IP", "無効な IP"), + ("Invalid format", "無効な形式"), + ("server_not_support", "このサーバーには現在対応していません。"), + ("Not available", "利用不可"), + ("Too frequent", "接続の頻度が高すぎます!"), + ("Cancel", "キャンセル"), + ("Skip", "スキップ"), + ("Close", "閉じる"), + ("Retry", "再試行"), + ("OK", "OK"), + ("Password Required", "パスワードが必要です"), + ("Please enter your password", "パスワードを入力してください"), + ("Remember password", "パスワードを記憶する"), + ("Wrong Password", "パスワードが間違っています"), + ("Do you want to enter again?", "もう一度入力しますか?"), + ("Connection Error", "接続エラー"), + ("Error", "エラー"), + ("Reset by the peer", "リモートホストによって接続がリセットされました"), + ("Connecting...", "接続中..."), + ("Connection in progress. Please wait.", "接続中です。しばらくお待ちください。"), + ("Please try 1 minute later", "1 分後にもう一度お試しください"), + ("Login Error", "ログインエラー"), + ("Successful", "成功"), + ("Connected, waiting for image...", "接続完了、映像を待機しています..."), + ("Name", "名前"), + ("Type", "種類"), + ("Modified", "最終更新日"), + ("Size", "サイズ"), + ("Show Hidden Files", "隠しファイルを表示する"), + ("Receive", "受信"), + ("Send", "送信"), + ("Refresh File", "ファイルを更新"), + ("Local", "ローカル"), + ("Remote", "リモート"), + ("Remote Computer", "リモートコンピューター"), + ("Local Computer", "ローカルコンピューター"), + ("Confirm Delete", "削除の確認"), + ("Delete", "削除"), + ("Properties", "プロパティ"), + ("Multi Select", "複数選択"), + ("Select All", "すべて選択"), + ("Unselect All", "選択をすべて解除"), + ("Empty Directory", "空のディレクトリ"), + ("Not an empty directory", "空ではないディレクトリ"), + ("Are you sure you want to delete this file?", "本当にファイルを削除しますか?"), + ("Are you sure you want to delete this empty directory?", "本当に空のディレクトリを削除しますか?"), + ("Are you sure you want to delete the file of this directory?", "本当にディレクトリ内のファイルを削除しますか?"), + ("Do this for all conflicts", "すべてに適用する"), + ("This is irreversible!", "この操作は元に戻せません!"), + ("Deleting", "削除中"), + ("files", "ファイル"), + ("Waiting", "待機中"), + ("Finished", "完了"), + ("Speed", "速度"), + ("Custom Image Quality", "カスタム画質"), + ("Privacy mode", "プライバシーモード"), + ("Block user input", "ユーザーの入力をブロック"), + ("Unblock user input", "ユーザーの入力を許可"), + ("Adjust Window", "ウィンドウを調整"), + ("Original", "オリジナル"), + ("Shrink", "縮小"), + ("Stretch", "伸縮"), + ("Scrollbar", "スクロールバー"), + ("ScrollAuto", "自動スクロール"), + ("Good image quality", "画質を優先"), + ("Balanced", "バランス"), + ("Optimize reaction time", "速度を優先"), + ("Custom", "カスタム"), + ("Show remote cursor", "リモートコンピューターのカーソルを表示する"), + ("Show quality monitor", "ディスプレイの品質を表示する"), + ("Disable clipboard", "クリップボードを無効化する"), + ("Lock after session end", "セッション終了後にロックする"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del を送信"), + ("Insert Lock", "ロック命令を送信"), + ("Refresh", "更新"), + ("ID does not exist", "ID が存在しません"), + ("Failed to connect to rendezvous server", "ランデブーサーバーに接続できませんでした"), + ("Please try later", "後でもう一度お試しください"), + ("Remote desktop is offline", "リモートデスクトップはオフラインです"), + ("Key mismatch", "キーが一致しません"), + ("Timeout", "タイムアウト"), + ("Failed to connect to relay server", "中継サーバーに接続できませんでした"), + ("Failed to connect via rendezvous server", "ランデブーサーバー経由で接続できませんでした"), + ("Failed to connect via relay server", "中継サーバー経由で接続できませんでした"), + ("Failed to make direct connection to remote desktop", "リモートデスクトップと直接接続できませんでした"), + ("Set Password", "パスワードを設定"), + ("OS Password", "OS のパスワード"), + ("install_tip", "UAC の影響により、RustDesk がリモートデスクトップ上で正常に動作しない場合があります。UAC を回避するには、下のボタンをクリックしてシステムに RustDesk をインストールしてください。"), + ("Click to upgrade", "アップグレード"), + ("Configure", "設定"), + ("config_acc", "リモートからあなたのコンピューターを操作するには、RustDesk に「アクセシビリティ」権限を与える必要があります。"), + ("config_screen", "リモートからあなたのコンピューターにアクセスするには、RustDesk に「画面録画」の権限を与える必要があります。"), + ("Installing ...", "インストール中..."), + ("Install", "インストール"), + ("Installation", "インストール"), + ("Installation Path", "インストール先のパス"), + ("Create start menu shortcuts", "スタートメニューにショートカットを作成する"), + ("Create desktop icon", "デスクトップにアイコンを作成する"), + ("agreement_tip", "インストールを開始することで、ライセンス条項に同意したとみなされます。"), + ("Accept and Install", "同意してインストール"), + ("End-user license agreement", "エンドユーザーライセンス条項"), + ("Generating ...", "生成中..."), + ("Your installation is lower version.", "インストールされているバージョンが古くなっています。"), + ("not_close_tcp_tip", "トンネルの使用中はこのウィンドウを閉じないでください"), + ("Listening ...", "リスニング中..."), + ("Remote Host", "リモートホスト"), + ("Remote Port", "リモートポート"), + ("Action", "操作"), + ("Add", "追加"), + ("Local Port", "ローカルポート"), + ("Local Address", "ローカルアドレス"), + ("Change Local Port", "ローカルポートを変更"), + ("setup_server_tip", "より高速に接続したい場合は、自分のサーバーをセットアップすることを推奨します。"), + ("Too short, at least 6 characters.", "文字数が短すぎます。最低文字数は 6 文字です。"), + ("The confirmation is not identical.", "確認欄と入力が一致しません。"), + ("Permissions", "権限"), + ("Accept", "承諾"), + ("Dismiss", "却下"), + ("Disconnect", "切断"), + ("Enable file copy and paste", "ファイルのコピーと貼り付けを許可する"), + ("Connected", "接続済み"), + ("Direct and encrypted connection", "直接接続: 接続は暗号化されています"), + ("Relayed and encrypted connection", "中継接続: 接続は暗号化されています"), + ("Direct and unencrypted connection", "直接接続: 接続が暗号化されていません"), + ("Relayed and unencrypted connection", "中継接続: 接続が暗号化されていません"), + ("Enter Remote ID", "リモート ID を入力"), + ("Enter your password", "パスワードを入力"), + ("Logging in...", "ログイン中..."), + ("Enable RDP session sharing", "RDP セッション共有を有効化する"), + ("Auto Login", "自動ログイン"), + ("Enable direct IP access", "直接 IP アクセスを有効化する"), + ("Rename", "名前の変更"), + ("Space", "スペース"), + ("Create desktop shortcut", "デスクトップにショートカットを作成する"), + ("Change Path", "パスを変更"), + ("Create Folder", "フォルダーを作成"), + ("Please enter the folder name", "フォルダー名を入力してください"), + ("Fix it", "修復する"), + ("Warning", "警告"), + ("Login screen using Wayland is not supported", "Wayland を使用したログインスクリーンはサポートされていません"), + ("Reboot required", "再起動が必要です"), + ("Unsupported display server", "サポートされていないディスプレイサーバー"), + ("x11 expected", "X11 が必要です"), + ("Port", "ポート"), + ("Settings", "設定"), + ("Username", "ユーザー名"), + ("Invalid port", "無効なポート"), + ("Closed manually by the peer", "リモートホストによって切断されました"), + ("Enable remote configuration modification", "リモート設定の変更を有効化する"), + ("Run without install", "インストールせずに実行"), + ("Connect via relay", "中継サーバー経由で接続"), + ("Always connect via relay", "常に中継サーバー経由で接続"), + ("whitelist_tip", "ホワイトリストに登録された IP からのみ接続を許可します"), + ("Login", "ログイン"), + ("Verify", "認証"), + ("Remember me", "入力内容を記憶する"), + ("Trust this device", "このデバイスを信頼する"), + ("Verification code", "認証コード"), + ("verification_tip", "登録されたメールアドレスに認証コードが送信されました。認証コードを入力して、ログインを続行してください。"), + ("Logout", "ログアウト"), + ("Tags", "タグ"), + ("Search ID", "ID を検索"), + ("whitelist_sep", "コンマやセミコロン、空白、改行で区切ってください"), + ("Add ID", "ID を追加"), + ("Add Tag", "タグを追加"), + ("Unselect all tags", "すべてのタグの選択を解除"), + ("Network error", "ネットワークエラー"), + ("Username missed", "ユーザー名がありません"), + ("Password missed", "パスワードがありません"), + ("Wrong credentials", "資格情報が間違っています"), + ("The verification code is incorrect or has expired", "認証コードが間違っているか、有効期限が切れています"), + ("Edit Tag", "タグを編集"), + ("Forget Password", "パスワードを忘れた"), + ("Favorites", "お気に入り"), + ("Add to Favorites", "お気に入りに追加"), + ("Remove from Favorites", "お気に入りから削除"), + ("Empty", "空"), + ("Invalid folder name", "無効なフォルダー名"), + ("Socks5 Proxy", "SOCKS5 プロキシ"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) プロキシ"), + ("Discovered", "発見済み"), + ("install_daemon_tip", "起動時に RustDesk を開始するには、システムサービスをインストールする必要があります。"), + ("Remote ID", "リモート ID"), + ("Paste", "貼り付け"), + ("Paste here?", "ここに貼り付けますか?"), + ("Are you sure to close the connection?", "本当に切断しますか?"), + ("Download new version", "新しいバージョンをダウンロード"), + ("Touch mode", "タッチモード"), + ("Mouse mode", "マウスモード"), + ("One-Finger Tap", "1 本指でタップ"), + ("Left Mouse", "マウス左クリック"), + ("One-Long Tap", "1 本指でロングタップ"), + ("Two-Finger Tap", "2 本指でタップ"), + ("Right Mouse", "マウス右クリック"), + ("One-Finger Move", "1 本指でドラッグ"), + ("Double Tap & Move", "2 本指でタップ&ドラッグ"), + ("Mouse Drag", "マウスドラッグ"), + ("Three-Finger vertically", "3 本指で縦方向"), + ("Mouse Wheel", "マウスホイール"), + ("Two-Finger Move", "2 本指でドラッグ"), + ("Canvas Move", "キャンバスの移動"), + ("Pinch to Zoom", "ピンチして拡大"), + ("Canvas Zoom", "キャンバスの拡大"), + ("Reset canvas", "キャンバスのリセット"), + ("No permission of file transfer", "ファイル転送の権限がありません"), + ("Note", "ノート"), + ("Connection", "接続"), + ("Share screen", "画面を共有"), + ("Chat", "チャット"), + ("Total", "合計"), + ("items", "個のアイテム"), + ("Selected", "選択済み"), + ("Screen Capture", "画面キャプチャ"), + ("Input Control", "入力操作"), + ("Audio Capture", "音声キャプチャ"), + ("Do you accept?", "許可しますか?"), + ("Open System Setting", "システム設定を開く"), + ("How to get Android input permission?", "Android の入力権限を取得するには?"), + ("android_input_permission_tip1", "この Android デバイスをリモートコンピューターからマウスやタッチで操作するには、RustDesk に「ユーザー補助」からサービスの使用を許可する必要があります。"), + ("android_input_permission_tip2", "次の端末設定ページに進み、「インストール済みアプリ」から「RustDesk Input」を有効にしてください。"), + ("android_new_connection_tip", "新しい操作リクエストが届きました。この端末を操作しようとしています。"), + ("android_service_will_start_tip", "「画面キャプチャ」を有効にするとサービスが自動的に開始され、他の端末がこの端末への接続をリクエストできるようになります。"), + ("android_stop_service_tip", "サービスを停止すると、自動的に現在のセッションがすべて閉じられます。"), + ("android_version_audio_tip", "現在の Android バージョンでは音声キャプチャはサポートされていません。Android 10 以降に更新してください。"), + ("android_start_service_tip", "「サービスを開始」をタップするか、「画面キャプチャ」の許可を有効にすると、画面共有サービスが開始されます。"), + ("android_permission_may_not_change_tip", "権限の変更は現在のセッションには適用されません。再接続後に適用されます。"), + ("Account", "アカウント"), + ("Overwrite", "上書き"), + ("This file exists, skip or overwrite this file?", "このファイルは既に存在しています。スキップするか上書きしますか?"), + ("Quit", "終了"), + ("Help", "ヘルプ"), + ("Failed", "失敗"), + ("Succeeded", "成功"), + ("Someone turns on privacy mode, exit", "プライバシーモードがオンになりました。終了します。"), + ("Unsupported", "サポートされていません"), + ("Peer denied", "リモートホストに拒否されました"), + ("Please install plugins", "プラグインをインストールしてください"), + ("Peer exit", "リモートホストが退出しました"), + ("Failed to turn off", "オフにできませんでした"), + ("Turned off", "オフになりました"), + ("Language", "言語"), + ("Keep RustDesk background service", "RustDesk バックグラウンドサービスを維持"), + ("Ignore Battery Optimizations", "バッテリーの最適化を無効にする"), + ("android_open_battery_optimizations_tip", "この機能を使わない場合は、RustDesk アプリの設定ページから「バッテリー」に進み、「制限しない」を選択してください。"), + ("Start on boot", "起動時に自動実行する"), + ("Start the screen sharing service on boot, requires special permissions", "起動時に画面共有サービスを開始します。これには特別な権限が必要です。"), + ("Connection not allowed", "接続が許可されていません"), + ("Legacy mode", "レガシーモード"), + ("Map mode", "マップモード"), + ("Translate mode", "変換モード"), + ("Use permanent password", "固定パスワードを使用する"), + ("Use both passwords", "両方のパスワードを使用する"), + ("Set permanent password", "固定パスワードを設定"), + ("Enable remote restart", "リモートからの再起動を有効化する"), + ("Restart remote device", "リモートの端末を再起動"), + ("Are you sure you want to restart", "本当に再起動しますか"), + ("Restarting remote device", "リモートデバイスを再起動中"), + ("remote_restarting_tip", "リモートコンピューターは再起動中です。このメッセージボックスを閉じて、しばらくした後にパスワードを使用して再接続してください。"), + ("Copied", "コピーしました"), + ("Exit Fullscreen", "全画面表示を終了"), + ("Fullscreen", "全画面表示"), + ("Mobile Actions", "モバイルアクション"), + ("Select Monitor", "ディスプレイを選択"), + ("Control Actions", "コントロールアクション"), + ("Display Settings", "ディスプレイの設定"), + ("Ratio", "比率"), + ("Image Quality", "画質"), + ("Scroll Style", "スクロールスタイル"), + ("Show Toolbar", "ツールバーを表示"), + ("Hide Toolbar", "ツールバーを隠す"), + ("Direct Connection", "直接接続"), + ("Relay Connection", "中継接続"), + ("Secure Connection", "安全な接続"), + ("Insecure Connection", "安全でない接続"), + ("Scale original", "オリジナルのサイズ"), + ("Scale adaptive", "ウィンドウに合わせる"), + ("General", "一般"), + ("Security", "セキュリティ"), + ("Theme", "テーマ"), + ("Dark Theme", "ダークテーマ"), + ("Light Theme", "ライトテーマ"), + ("Dark", "ダーク"), + ("Light", "ライト"), + ("Follow System", "システム設定に従う"), + ("Enable hardware codec", "ハードウェアコーデックを有効化する"), + ("Unlock Security Settings", "セキュリティ設定のロックを解除"), + ("Enable audio", "オーディオを有効化する"), + ("Unlock Network Settings", "ネットワーク設定のロックを解除"), + ("Server", "サーバー"), + ("Direct IP Access", "直接 IP 接続"), + ("Proxy", "プロキシ"), + ("Apply", "適用"), + ("Disconnect all devices?", "すべてのデバイスから切断しますか?"), + ("Clear", "クリア"), + ("Audio Input Device", "音声入力デバイス"), + ("Use IP Whitelisting", "IP ホワイトリストを使用する"), + ("Network", "ネットワーク"), + ("Pin Toolbar", "ツールバーをピン留め"), + ("Unpin Toolbar", "ツールバーのピン留めを解除"), + ("Recording", "録画"), + ("Directory", "ディレクトリ"), + ("Automatically record incoming sessions", "受信したセッションを自動で記録する"), + ("Automatically record outgoing sessions", "送信したセッションを自動で記録する"), + ("Change", "変更"), + ("Start session recording", "セッションの録画を開始"), + ("Stop session recording", "セッションの録画を停止"), + ("Enable recording session", "セッションの録画を有効化する"), + ("Enable LAN discovery", "LAN の探索を有効化する"), + ("Deny LAN discovery", "LAN の探索を拒否する"), + ("Write a message", "メッセージを書き込む"), + ("Prompt", "必須"), + ("Please wait for confirmation of UAC...", "UAC の承認を待機しています..."), + ("elevated_foreground_window_tip", "リモートデスクトップでフォーカスされているウィンドウの操作にはより高い権限が必要なため、マウスとキーボードが一時的に使用できなくなっています。リモートユーザーにウィンドウを最小化、または接続管理画面から権限を昇格するよう要求してください。この問題を回避するには、リモートコンピューターに RustDesk をインストールしてください。"), + ("Disconnected", "切断しました"), + ("Other", "その他"), + ("Confirm before closing multiple tabs", "複数のタブを閉じる前に確認する"), + ("Keyboard Settings", "キーボードの設定"), + ("Full Access", "フルアクセス"), + ("Screen Share", "画面共有"), + ("ubuntu-21-04-required", "Wayland を使用するには、Ubuntu 21.04 以降のバージョンが必要です。"), + ("wayland-requires-higher-linux-version", "Wayland を使用するには、より新しい Linux ディストリビューションが必要です。 X11 デスクトップを試すか、OS を変更してください。"), + ("xdp-portal-unavailable", ""), + ("JumpLink", "表示"), + ("Please Select the screen to be shared(Operate on the peer side).", "共有する画面を選択してください(リモートコンピューターが操作します)"), + ("Show RustDesk", "RustDesk を表示"), + ("This PC", "この PC"), + ("or", "または"), + ("Elevate", "昇格"), + ("Zoom cursor", "カーソルを拡大する"), + ("Accept sessions via password", "パスワードでセッションを承認"), + ("Accept sessions via click", "クリックでセッションを承認"), + ("Accept sessions via both", "両方の方法でセッションを承認"), + ("Please wait for the remote side to accept your session request...", "リモートコンピューターがあなたのセッション要求を受け入れるまでお待ちください..."), + ("One-time Password", "ワンタイムパスワード"), + ("Use one-time password", "ワンタイムパスワードを使用する"), + ("One-time password length", "ワンタイムパスワードの長さ"), + ("Request access to your device", "デバイスへのアクセス要求"), + ("Hide connection management window", "接続管理画面を隠す"), + ("hide_cm_tip", "パスワードによるセッションを許可し、固定パスワードを使用する場合にのみ、管理画面の非表示を許可する。"), + ("wayland_experiment_tip", "Wayland のサポートは試験的なものです。無人アクセスを使用する場合はX11デスクトップをご利用ください。"), + ("Right click to select tabs", "右クリックでタブを選択"), + ("Skipped", "スキップ"), + ("Add to address book", "アドレス帳に追加"), + ("Group", "グループ"), + ("Search", "検索"), + ("Closed manually by web console", "Web コンソールによって閉じられました"), + ("Local keyboard type", "キーボードのタイプ"), + ("Select local keyboard type", "キーボードのタイプを選択"), + ("software_render_tip", "Linux で NVIDIA 製のグラフィックカードを使用していると、接続後すぐにリモートウィンドウが閉じてしまう場合があります。オープンソースの Nouveau ドライバーに切り替えて、ソフトウェアレンダリングを使用するよう設定すると解決するかもしれません。(RustDesk の再起動が必要です)"), + ("Always use software rendering", "常にソフトウェアレンダリングを使用する"), + ("config_input", "リモートコンピューターをキーボードで操作するには、RustDesk に「入力監視」権限を与える必要があります。"), + ("config_microphone", "リモートコンピューターと通話するには、RustDesk に「音声録音」権限を与える必要があります。"), + ("request_elevation_tip", "リモートユーザーがいる場合は、権限の昇格をリクエストできます。"), + ("Wait", "待機"), + ("Elevation Error", "昇格エラー"), + ("Ask the remote user for authentication", "リモートユーザーに認証をリクエストする"), + ("Choose this if the remote account is administrator", "使用中のリモートコンピューター アカウントが管理者の場合はこちらを選択してください"), + ("Transmit the username and password of administrator", "管理者のユーザー名とパスワードを送信"), + ("still_click_uac_tip", "リモートデスクトップユーザーが RustDesk を実行する際に、UACを許可する必要があります。"), + ("Request Elevation", "権限の昇格をリクエストする"), + ("wait_accept_uac_tip", "リモートデスクトップ ユーザーが UAC ダイアログを許可するまでしばらくお待ちください。"), + ("Elevate successfully", "権限の昇格に成功しました"), + ("uppercase", "大文字"), + ("lowercase", "小文字"), + ("digit", "桁数"), + ("special character", "特殊文字"), + ("length>=8", "8 文字以上"), + ("Weak", "脆弱"), + ("Medium", "普通"), + ("Strong", "強力"), + ("Switch Sides", "接続方向の切り替え"), + ("Please confirm if you want to share your desktop?", "デスクトップの共有を許可しますか?"), + ("Display", "ディスプレイ"), + ("Default View Style", "既定の表示スタイル"), + ("Default Scroll Style", "既定のスクロールスタイル"), + ("Default Image Quality", "既定の画質"), + ("Default Codec", "既定のコーデック"), + ("Bitrate", "ビットレート"), + ("FPS", "FPS"), + ("Auto", "自動"), + ("Other Default Options", "その他の既定の設定"), + ("Voice call", "音声通話"), + ("Text chat", "テキストチャット"), + ("Stop voice call", "音声通話を終了"), + ("relay_hint_tip", "直接接続が行えない場合は、リレー経由での接続をお試しください。初回接続で中継接続を行いたい場合は末尾に「/r」を付けるか、最近のセッション履歴に「常に中継サーバー経由で接続」という設定がある場合はそちらを選択してください。"), + ("Reconnect", "再接続"), + ("Codec", "コーデック"), + ("Resolution", "解像度"), + ("No transfers in progress", "進行中の転送はありません"), + ("Set one-time password length", "ワンタイムパスワードの長さを設定する"), + ("RDP Settings", "RDP 設定"), + ("Sort by", "並べ替え"), + ("New Connection", "新規接続"), + ("Restore", "復元"), + ("Minimize", "最小"), + ("Maximize", "最大"), + ("Your Device", "あなたのデバイス"), + ("empty_recent_tip", "おっと、最近のセッションは見つかりませんでした。新しい計画を練る時間です!"), + ("empty_favorite_tip", "お気に入りのリモートコンピュータがないようですね?あなたの接続先を登録しましょう!"), + ("empty_lan_tip", "あらら、まだ近くのコンピューターは発見できていないようです。"), + ("empty_address_book_tip", "驚くべきことに、あなたのアドレス帳には現在コンピューターが登録されていません。"), + ("Empty Username", "空のユーザー名"), + ("Empty Password", "空のパスワード"), + ("Me", "あなた"), + ("identical_file_tip", "このファイルはリモートコンピューターと同一です。"), + ("show_monitors_tip", "ツールバーにディスプレイを表示する"), + ("View Mode", "表示モード"), + ("login_linux_tip", "X デスクトップのセッションにログインするには、リモートコンピューターのLinuxアカウントにログインする必要があります。"), + ("verify_rustdesk_password_tip", "RustDesk のパスワードを確認する"), + ("remember_account_tip", "このアカウントを記憶する"), + ("os_account_desk_tip", "このアカウントは、リモートコンピューターの OS にログインし、ヘッドレスでセッションを有効化するために使用されます。"), + ("OS Account", "OS のアカウント"), + ("another_user_login_title_tip", "他のユーザーがすでにログインしています"), + ("another_user_login_text_tip", "切断しました"), + ("xorg_not_found_title_tip", "Xorg サーバーが見つかりませんでした。"), + ("xorg_not_found_text_tip", "Xorg をインストールしてください"), + ("no_desktop_title_tip", "デスクトップ環境が見つかりませんでした。"), + ("no_desktop_text_tip", "GNOME デスクトップ環境をインストールしてください"), + ("No need to elevate", "権限昇格の必要はありません"), + ("System Sound", "システム音声"), + ("Default", "既定"), + ("New RDP", "新しい RDP"), + ("Fingerprint", "フィンガープリント"), + ("Copy Fingerprint", "フィンガープリントをコピー"), + ("no fingerprints", "フィンガープリントがありません"), + ("Select a peer", "リモートコンピューターを選択"), + ("Select peers", "複数のリモートコンピューターを選択"), + ("Plugins", "プラグイン"), + ("Uninstall", "アンインストール"), + ("Update", "更新"), + ("Enable", "有効"), + ("Disable", "無効"), + ("Options", "設定"), + ("resolution_original_tip", "オリジナルの解像度"), + ("resolution_fit_local_tip", "ローカル解像度に合わせる"), + ("resolution_custom_tip", "カスタム解像度"), + ("Collapse toolbar", "ツールバーを折りたたむ"), + ("Accept and Elevate", "承認して権限を昇格する"), + ("accept_and_elevate_btn_tooltip", "接続を受け入れた上で、UAC 権限を昇格します。"), + ("clipboard_wait_response_timeout_tip", "クリップボードのコピーがタイムアウトしました。"), + ("Incoming connection", "接続の受信"), + ("Outgoing connection", "接続の送信"), + ("Exit", "終了"), + ("Open", "開く"), + ("logout_tip", "本当にログアウトしますか?"), + ("Service", "サービス"), + ("Start", "開始"), + ("Stop", "停止"), + ("exceed_max_devices", "管理対象のデバイスが最大数に達しました。"), + ("Sync with recent sessions", "最近のセッションと同期"), + ("Sort tags", "タグで並べ替え"), + ("Open connection in new tab", "新しいタブで接続を開く"), + ("Move tab to new window", "新しいウィンドウにタブを移動する"), + ("Can not be empty", "空にすることはできません"), + ("Already exists", "すでに存在します"), + ("Change Password", "パスワードを変更"), + ("Refresh Password", "パスワードを更新"), + ("ID", "ID"), + ("Grid View", "グリッド表示"), + ("List View", "リスト表示"), + ("Select", "選択"), + ("Toggle Tags", "タグの切り替え"), + ("pull_ab_failed_tip", "アドレス帳の更新に失敗しました"), + ("push_ab_failed_tip", "サーバーへのアドレス帳の同期に失敗しました"), + ("synced_peer_readded_tip", "最近セッションを行ったデバイスはアドレス帳に同期されます。"), + ("Change Color", "色の変更"), + ("Primary Color", "プライマリカラー"), + ("HSV Color", "HSV カラー"), + ("Installation Successful!", "インストールに成功しました!"), + ("Installation failed!", "インストールに失敗しました。"), + ("Reverse mouse wheel", "マウスホイールを反転する"), + ("{} sessions", "{} 件のセッション"), + ("scam_title", "あなたは詐欺にあっているかもしれません!"), + ("scam_text1", "もし、知らない相手から電話で RustDesk のインストールやサービスの開始を依頼された場合、作業を進めずに、すぐに電話を切ってください。"), + ("scam_text2", "相手はあなたからお金や個人情報を盗もうとする詐欺師である可能性があります。"), + ("Don't show again", "今後表示しない"), + ("I Agree", "同意する"), + ("Decline", "同意しない"), + ("Timeout in minutes", "タイムアウトまでの時間 (分)"), + ("auto_disconnect_option_tip", "ユーザーが非アクティブの場合、自動的に受信したセッションを閉じる"), + ("Connection failed due to inactivity", "リモートデスクトップユーザーが非アクティブなため、接続に失敗しました"), + ("Check for software update on startup", "起動時にソフトウェアの更新を確認する"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "RustDesk Server Pro をバージョン {} 以上にアップグレードしてください!"), + ("pull_group_failed_tip", "グループの更新に失敗しました"), + ("Filter by intersection", "交差位置でフィルター"), + ("Remove wallpaper during incoming sessions", "セッションの受信中、デスクトップ背景を削除する"), + ("Test", "テスト"), + ("display_is_plugged_out_msg", "ディスプレイが接続されていません。最初のディスプレイを選択してください。"), + ("No displays", "ディスプレイがありません"), + ("Open in new window", "新しいウィンドウで開く"), + ("Show displays as individual windows", "ディスプレイを個別のウィンドウとして表示する"), + ("Use all my displays for the remote session", "すべてのディスプレイをセッションで使用する"), + ("selinux_tip", "SELinuxが有効になっているため、RustDesk が正常に動作しない可能性があります。"), + ("Change view", "表示を変更"), + ("Big tiles", "大きなタイル"), + ("Small tiles", "小さなタイル"), + ("List", "リスト"), + ("Virtual display", "仮想ディスプレイ"), + ("Plug out all", "すべて切断"), + ("True color (4:4:4)", "True Color (4:4:4)"), + ("Enable blocking user input", "ユーザー入力のブロックを有効化する"), + ("id_input_tip", "ID、IPアドレス、またはドメインとポート番号(<ドメイン>:<ポート>)を使用できます。\n他のサーバーのデバイスにアクセスしたい場合は、サーバーアドレス(@<サーバーアドレス>?key=<キーの値>)を追加してください。 \n(例: 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=)\nパブリックサーバーのデバイスに接続したい場合は、「@public」のように入力してください。パブリックサーバーの場合、キーは不要です。\n\n初回接続で中継接続を行いたい場合は、「9123456234/r」のように末尾に「/r」を付けてください。"), + ("privacy_mode_impl_mag_tip", "モード 1"), + ("privacy_mode_impl_virtual_display_tip", "モード 2"), + ("Enter privacy mode", "プライバシーモードを起動"), + ("Exit privacy mode", "プライバシーモードを終了"), + ("idd_not_support_under_win10_2004_tip", "Indirect display driver には対応していません。Windows 10 バージョン 2004 以降が必要です。"), + ("input_source_1_tip", "入力ソース 1"), + ("input_source_2_tip", "入力ソース 2"), + ("Swap control-command key", "ctrl と command キーを入れ替える"), + ("swap-left-right-mouse", "マウスのクリックを入れ替える"), + ("2FA code", "二要素認証コード"), + ("More", "詳細"), + ("enable-2fa-title", "二要素認証を有効化する"), + ("enable-2fa-desc", "認証アプリをセットアップします。Authy、Microsoft または Google 認証システムなどが PC またはスマートフォンで利用できます。\n\nQR コードをスキャンし、アプリが表示するコードを入力することで二要素認証が有効になります。"), + ("wrong-2fa-code", "コードが違います。コードと端末の時刻設定が正しいかをご確認ください。"), + ("enter-2fa-title", "二要素認証"), + ("Email verification code must be 6 characters.", "電子メール認証コードは 6 文字である必要があります。"), + ("2FA code must be 6 digits.", "二要素認証コードは 6 文字である必要があります。"), + ("Multiple Windows sessions found", "複数の Windows セッションが見つかりました"), + ("Please select the session you want to connect to", "接続したいセッションを選択してください"), + ("powered_by_me", "Powered by RustDesk"), + ("outgoing_only_desk_tip", "カスタマイズされたエディションを使用しています。\n他のコンピューターに接続できますが、他のコンピューターからのリクエストは受信できません。"), + ("preset_password_warning", "このエディションには、既定で固定パスワードが設定されています。このパスワードを知っているユーザーはあなたのデバイスを完全にコントロールできるため、そのような危険がある場合は直ちに RustDesk をアンインストールして下さい!"), + ("Security Alert", "セキュリティ警告"), + ("My address book", "あなたのアドレス帳"), + ("Personal", "個人"), + ("Owner", "所有者"), + ("Set shared password", "共有パスワードの設定"), + ("Exist in", "既に存在します"), + ("Read-only", "読み取り専用"), + ("Read/Write", "読み取り/書き込み"), + ("Full Control", "フルアクセス"), + ("share_warning_tip", "フィールドは共有され、他の人からも閲覧できます。"), + ("Everyone", "全員"), + ("ab_web_console_tip", "Web コンソールの詳細"), + ("allow-only-conn-window-open-tip", "RustDesk のウィンドウが開いている場合のみ接続を許可する"), + ("no_need_privacy_mode_no_physical_displays_tip", "物理ディスプレイが存在しないため、プライバシーモードは不要です。"), + ("Follow remote cursor", "リモートカーソルに追従する"), + ("Follow remote window focus", "リモートウィンドウのフォーカスに追従する"), + ("default_proxy_tip", "既定のプロトコルとポートは Socks5 と 1080 です。"), + ("no_audio_input_device_tip", "オーディオ入力デバイスが見つかりません。"), + ("Incoming", "受信"), + ("Outgoing", "発信"), + ("Clear Wayland screen selection", "Wayland の画面選択をクリア"), + ("clear_Wayland_screen_selection_tip", "画面選択をクリア後、共有画面を再び選択できます。"), + ("confirm_clear_Wayland_screen_selection_tip", "本当に Wayland の画面選択をクリアしますか?"), + ("android_new_voice_call_tip", "新しい音声通話リクエストを受信しました。承認すると音声通話に切り替わります。"), + ("texture_render_tip", "テクスチャレンダリングを使用し、画像をより滑らかに描画します。レンダリングの問題が発生した場合は無効にしてみてください。"), + ("Use texture rendering", "テクスチャレンダリングを使用する"), + ("Floating window", "フローティングウィンドウ"), + ("floating_window_tip", "RustDesk のバックグラウンドサービスを維持するために使用されます。"), + ("Keep screen on", "常に画面をオン"), + ("Never", "画面をオンにしない"), + ("During controlled", "操作中"), + ("During service is on", "サービスが動作中"), + ("Capture screen using DirectX", "画面キャプチャに DirectX を使用する"), + ("Back", "戻る"), + ("Apps", "アプリ"), + ("Volume up", "音量を上げる"), + ("Volume down", "音量を下げる"), + ("Power", "電源"), + ("Telegram bot", "Telegram ボット"), + ("enable-bot-tip", "この機能を有効にすると、ボットから二要素認証コードを受け取ることができます。また、接続時の通知としても機能します。"), + ("enable-bot-desc", "1. @BotFather のチャットを開きます。\n2. 「/newbot」コマンドを送信します。送信後、トークンを取得できます。\n3. 新しく作成したボットとチャットを開始します。「/hello」のようにスラッシュで始まるメッセージを送信して起動します。\n"), + ("cancel-2fa-confirm-tip", "本当に二要素認証をキャンセルしますか?"), + ("cancel-bot-confirm-tip", "本当に Telegram ボットをキャンセルしますか?"), + ("About RustDesk", "RustDesk について"), + ("Send clipboard keystrokes", "クリップボードの内容をキー入力として送信する"), + ("network_error_tip", "ネットワーク接続を確認し、再度お試しください。"), + ("Unlock with PIN", "PIN でロックを解除する"), + ("Requires at least {} characters", "最低でも {} 文字が必要です"), + ("Wrong PIN", "PIN が間違っています"), + ("Set PIN", "PIN を設定"), + ("Enable trusted devices", "承認済みデバイスを有効化する"), + ("Manage trusted devices", "承認済みデバイスの管理"), + ("Platform", "プラットフォーム"), + ("Days remaining", "残り日数"), + ("enable-trusted-devices-tip", "承認済みのデバイスで 2FA の確認をスキップします。"), + ("Parent directory", "親ディレクトリ"), + ("Resume", "再開"), + ("Invalid file name", "無効なファイル名"), + ("one-way-file-transfer-tip", "コントロールをされる側では一方向のファイル転送が有効になります。"), + ("Authentication Required", "認証が必要です"), + ("Authenticate", "認証"), + ("web_id_input_tip", "同じサーバー内の ID を入力できます。Web クライアントでは直接 IP アドレスによるアクセスはサポートされていません。\n別のサーバー上のデバイスにアクセスする場合は、サーバーアドレス (@?key=) を入力してください。\n 例: 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\nパブリックサーバー上のデバイスにアクセスする場合は、「@public」と入力してください。パブリックサーバーはキーは不要です。"), + ("Download", "ダウンロード"), + ("Upload folder", "フォルダーをアップロード"), + ("Upload files", "ファイルをアップロード"), + ("Clipboard is synchronized", "クリップボードを同期しました"), + ("Update client clipboard", "クライアントのクリップボードを更新"), + ("Untagged", "タグ付けなし"), + ("new-version-of-{}-tip", "{} の新しいバージョンが利用可能です"), + ("Accessible devices", "アクセス可能なデバイス"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "リモート側の RustDesk クライアントをバージョン {} 以上にアップグレードしてください!"), + ("d3d_render_tip", "D3D レンダリングを有効化すると、一部の環境ではリモートコントロール画面が黒くなる場合があります。"), + ("Use D3D rendering", "D3D レンダリングを使用する"), + ("Printer", "プリンター"), + ("printer-os-requirement-tip", "プリンター送信機能は Windows 10 以降が必要です。"), + ("printer-requires-installed-{}-client-tip", "リモート印刷を使用するには、このデバイスに {} がインストールされている必要があります。"), + ("printer-{}-not-installed-tip", "{} のプリンターがインストールされていません。"), + ("printer-{}-ready-tip", "{} のプリンターがインストールされ、使用可能になっています。"), + ("Install {} Printer", " {} のプリンターをインストール"), + ("Outgoing Print Jobs", "印刷ジョブの送信"), + ("Incoming Print Jobs", "印刷ジョブの受信"), + ("Incoming Print Job", "印刷ジョブの受信"), + ("use-the-default-printer-tip", "既定のプリンターを使用する"), + ("use-the-selected-printer-tip", "選択したプリンターを使用する"), + ("auto-print-tip", "選択したプリンターを使用して自動的に印刷する"), + ("print-incoming-job-confirm-tip", "リモートから印刷ジョブを受信しました。こちらで実行しますか?"), + ("remote-printing-disallowed-tile-tip", "リモート印刷は許可されていません"), + ("remote-printing-disallowed-text-tip", "コントロールされる側の権限の設定により、リモート印刷が拒否されました。"), + ("save-settings-tip", "設定を保存します"), + ("dont-show-again-tip", "今後は表示しない"), + ("Take screenshot", "スクリーンショットを撮影"), + ("Taking screenshot", "スクリーンショットを撮影中"), + ("screenshot-merged-screen-not-supported-tip", "複数のディスプレイのスクリーンショットの結合は、現在サポートされていません。単一のディスプレイに切り替えてもう一度お試しください。"), + ("screenshot-action-tip", "スクリーンショットを続行する方法を選択してください。"), + ("Save as", "保存先"), + ("Copy to clipboard", "クリップボードにコピー"), + ("Enable remote printer", "リモートプリンターを有効化する"), + ("Downloading {}", "{} をダウンロード中"), + ("{} Update", "{} を更新"), + ("{}-to-update-tip", "{} を終了して新しいバージョンがインストールされます。"), + ("download-new-version-failed-tip", "ダウンロードに失敗しました。もう一度お試しいただくか、「ダウンロード」ボタンをクリックしてリリースページからダウンロードし、手動でアップグレードしてください。"), + ("Auto update", "ソフトウェアの自動更新を行う"), + ("update-failed-check-msi-tip", "インストール方法の確認に失敗しました。「ダウンロード」ボタンをクリックしてリリースページからダウンロードし、手動でアップグレードしてください。"), + ("websocket_tip", "WebSocket を使用する場合、リレー接続のみがサポートされます。"), + ("Use WebSocket", "WebSocket を使用する"), + ("Trackpad speed", "トラックパッドの速度"), + ("Default trackpad speed", "既定のトラックパッドの速度"), + ("Numeric one-time password", "数字のワンタイムパスワード"), + ("Enable IPv6 P2P connection", "IPv6 P2P 接続を有効化する"), + ("Enable UDP hole punching", "UDP ホールパンチを有効化する"), + ("View camera", "カメラを表示"), + ("Enable camera", "カメラを有効化する"), + ("No cameras", "カメラなし"), + ("view_camera_unsupported_tip", "リモートデバイスはカメラの表示をサポートしていません。"), + ("Terminal", "ターミナル"), + ("Enable terminal", "ターミナルを有効化する"), + ("New tab", "新しいタブ"), + ("Keep terminal sessions on disconnect", "切断時にターミナルセッションを維持する"), + ("Terminal (Run as administrator)", "管理者として実行"), + ("terminal-admin-login-tip", "リモート側の管理者ユーザー名とパスワードを入力してください。"), + ("Failed to get user token.", "ユーザートークンの取得に失敗しました。"), + ("Incorrect username or password.", "ユーザー名またはパスワードが正しくありません。"), + ("The user is not an administrator.", "このユーザーは管理者ではありません。"), + ("Failed to check if the user is an administrator.", "ユーザーが管理者であるかどうかを確認できませんでした。"), + ("Supported only in the installed version.", "インストールされたバージョンでのみサポートされます。"), + ("elevation_username_tip", "ユーザー名またはドメインのユーザー名を入力してください。"), + ("Preparing for installation ...", "インストールの準備中です..."), + ("Show my cursor", "自分のカーソルを表示する"), + ("Scale custom", "カスタムスケール"), + ("Custom scale slider", "カスタムスケールのスライダー"), + ("Decrease", "縮小"), + ("Increase", "拡大"), + ("Show virtual mouse", "仮想マウスを表示する"), + ("Virtual mouse size", "仮想マウスのサイズ"), + ("Small", "小"), + ("Large", "中"), + ("Show virtual joystick", "仮想ジョイスティックを表示する"), + ("Edit note", "メモを編集"), + ("Alias", "エイリアス"), + ("ScrollEdge", "スクロールエッジ"), + ("Allow insecure TLS fallback", "安全ではない TLS フォールバックを許可する"), + ("allow-insecure-tls-fallback-tip", "既定では RustDesk は TLS を使用するプロトコルのサーバー証明書を検証します。\nこのオプションを有効化すると RustDesk は検証の手順をスキップして、検証に失敗した場合の処理を続行します。"), + ("Disable UDP", "UDP を無効化する"), + ("disable-udp-tip", "TCP のみ使用するかどうかを制御します。\nこのオプションを有効化すると、RustDesk は UDP 21116 を使用せずに TCP 21116 を使用するようになります。"), + ("server-oss-not-support-tip", "注意: RustDesk Server OSS にはこの機能が含まれていません。"), + ("input note here", "ここにメモを入力"), + ("note-at-conn-end-tip", "接続終了時にメモを要求する"), + ("Show terminal extra keys", "ターミナルの追加キーを表示する"), + ("Relative mouse mode", "相対マウスモード"), + ("rel-mouse-not-supported-peer-tip", "接続先のデバイスは相対マウスモードに対応していません。"), + ("rel-mouse-not-ready-tip", "相対マウスモードはまだ準備できていません。再度お試しください。"), + ("rel-mouse-lock-failed-tip", "カーソルをロックできませんでした。相対マウスモードは無効化されています。"), + ("rel-mouse-exit-{}-tip", "「{}」を押して終了します。"), + ("rel-mouse-permission-lost-tip", "キーボード操作の権限が取り消されました。相対マウスモードは無効化されています。"), + ("Changelog", "更新履歴"), + ("keep-awake-during-outgoing-sessions-label", "送信セッション中は、画面のスリープを無効化する"), + ("keep-awake-during-incoming-sessions-label", "受信セッション中は、画面のスリープを無効化する"), + ("Continue with {}", "{}で続行する"), + ("Display Name", "表示名"), + ("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"), + ("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/ko.rs b/vendor/rustdesk/src/lang/ko.rs new file mode 100644 index 0000000..7b3ffd9 --- /dev/null +++ b/vendor/rustdesk/src/lang/ko.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "상태"), + ("Your Desktop", "내 데스크탑"), + ("desk_tip", "이 ID와 비밀번호로 데스크탑에 액세스할 수 있습니다."), + ("Password", "비밀번호"), + ("Ready", "준비 완료"), + ("Established", "연결됨"), + ("connecting_status", "RustDesk 네트워크에 연결 중..."), + ("Enable service", "서비스 활성화"), + ("Start service", "서비스 시작"), + ("Service is running", "서비스가 실행 중입니다"), + ("Service is not running", "서비스가 실행되지 않았습니다"), + ("not_ready_status", "준비되지 않았습니다. 연결을 확인해 주세요"), + ("Control Remote Desktop", "원격 데스크탑 제어"), + ("Transfer file", "파일 전송"), + ("Connect", "연결"), + ("Recent sessions", "최근 세션"), + ("Address book", "주소록"), + ("Confirmation", "확인"), + ("TCP tunneling", "TCP 터널링"), + ("Remove", "삭제"), + ("Refresh random password", "임의의 비밀번호 새로 고침"), + ("Set your own password", "자신만의 비밀번호 설정"), + ("Enable keyboard/mouse", "키보드/마우스 허용"), + ("Enable clipboard", "클립보드 허용"), + ("Enable file transfer", "파일 전송 허용"), + ("Enable TCP tunneling", "TCP 터널링 허용"), + ("IP Whitelisting", "IP 화이트리스트"), + ("ID/Relay Server", "ID/릴레이 서버"), + ("Import server config", "서버 구성 가져오기"), + ("Export Server Config", "서버 구성 내보내기"), + ("Import server configuration successfully", "서버 구성 가져오기에 성공했습니다"), + ("Export server configuration successfully", "서버 구성 내보내기가 성공했습니다"), + ("Invalid server configuration", "잘못된 서버 구성입니다"), + ("Clipboard is empty", "클립보드가 비어있습니다"), + ("Stop service", "서비스 중지"), + ("Change ID", "ID 변경"), + ("Your new ID", "새 ID"), + ("length %min% to %max%", "길이 %min% ~ %max%"), + ("starts with a letter", "문자로 시작해야 합니다"), + ("allowed characters", "허용되는 문자"), + ("id_change_tip", "a-z, A-Z, 0-9, -(대시) 및 _(밑줄) 문자만 허용됩니다. 첫 글자는 a-z, A-Z여야 합니다. 길이는 6에서 16 사이여야 합니다."), + ("Website", "웹사이트"), + ("About", "정보"), + ("Slogan_tip", "이 혼란스러운 세상에서 마음을 담아 만들었습니다! - 한국어 번역: 비너스걸"), + ("Privacy Statement", "개인정보 보호정책"), + ("Mute", "음소거"), + ("Build Date", "빌드 날짜"), + ("Version", "버전"), + ("Home", "홈"), + ("Audio Input", "오디오 입력"), + ("Enhancements", "향상된 기능"), + ("Hardware Codec", "하드웨어 코덱"), + ("Adaptive bitrate", "적응형 비트레이트"), + ("ID Server", "ID 서버"), + ("Relay Server", "릴레이 서버"), + ("API Server", "API 서버"), + ("invalid_http", "http:// 또는 https://로 시작해야 합니다"), + ("Invalid IP", "유효하지 않은 IP 주소입니다"), + ("Invalid format", "유효하지 않은 형식입니다"), + ("server_not_support", "아직 서버에서 지원되지 않습니다"), + ("Not available", "사용할 수 없음"), + ("Too frequent", "너무 빈번합니다"), + ("Cancel", "취소"), + ("Skip", "건너뛰기"), + ("Close", "닫기"), + ("Retry", "재시도"), + ("OK", "확인"), + ("Password Required", "비밀번호 필요"), + ("Please enter your password", "비밀번호를 입력하세요"), + ("Remember password", "비밀번호 기억"), + ("Wrong Password", "잘못된 비밀번호"), + ("Do you want to enter again?", "다시 입력하시겠습니까?"), + ("Connection Error", "연결 오류"), + ("Error", "오류"), + ("Reset by the peer", "피어에 의해 초기화"), + ("Connecting...", "연결 중..."), + ("Connection in progress. Please wait.", "연결이 진행 중입니다. 기다려 주세요."), + ("Please try 1 minute later", "1분 후에 다시 시도하세요"), + ("Login Error", "로그인 오류"), + ("Successful", "성공"), + ("Connected, waiting for image...", "연결됨, 화면을 기다리는 중..."), + ("Name", "이름"), + ("Type", "유형"), + ("Modified", "수정 날짜"), + ("Size", "크기"), + ("Show Hidden Files", "숨김 파일 표시"), + ("Receive", "받기"), + ("Send", "보내기"), + ("Refresh File", "파일 새로 고침"), + ("Local", "로컬"), + ("Remote", "원격"), + ("Remote Computer", "원격 컴퓨터"), + ("Local Computer", "로컬 컴퓨터"), + ("Confirm Delete", "삭제 확인"), + ("Delete", "삭제"), + ("Properties", "속성"), + ("Multi Select", "다중 선택"), + ("Select All", "모두 선택"), + ("Unselect All", "모두 선택 해제"), + ("Empty Directory", "빈 디렉터리입니다"), + ("Not an empty directory", "빈 디렉터리가 아닙니다"), + ("Are you sure you want to delete this file?", "이 파일을 삭제하시겠습니까?"), + ("Are you sure you want to delete this empty directory?", "이 빈 디렉터리를 삭제하시겠습니까?"), + ("Are you sure you want to delete the file of this directory?", "이 디렉터리의 파일을 삭제하시겠습니까?"), + ("Do this for all conflicts", "모든 충돌에 대해 이렇게 하세요"), + ("This is irreversible!", "이것은 되돌릴 수 없습니다!"), + ("Deleting", "삭제 중"), + ("files", "파일"), + ("Waiting", "대기 중"), + ("Finished", "완료되었습니다"), + ("Speed", "속도"), + ("Custom Image Quality", "사용자 지정 이미지 품질"), + ("Privacy mode", "개인정보 보호 모드"), + ("Block user input", "사용자 입력 차단"), + ("Unblock user input", "사용자 입력 차단 해제"), + ("Adjust Window", "창 크기 조정"), + ("Original", "원본"), + ("Shrink", "축소"), + ("Stretch", "늘이기"), + ("Scrollbar", "스크롤 막대"), + ("ScrollAuto", "자동 스크롤"), + ("Good image quality", "좋은 이미지 품질"), + ("Balanced", "균형 잡힌"), + ("Optimize reaction time", "반응 시간 최적화"), + ("Custom", "사용자 지정"), + ("Show remote cursor", "원격 커서 표시"), + ("Show quality monitor", "품질 모니터 표시"), + ("Disable clipboard", "클립보드 사용 안 함"), + ("Lock after session end", "세션 종료 후 잠금"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del 삽입"), + ("Insert Lock", "삽입 잠금"), + ("Refresh", "새로 고침"), + ("ID does not exist", "ID가 존재하지 않습니다"), + ("Failed to connect to rendezvous server", "랑데부 서버 연결에 실패했습니다"), + ("Please try later", "나중에 시도해 주세요"), + ("Remote desktop is offline", "원격 데스크탑이 오프라인입니다"), + ("Key mismatch", "키가 일치하지 않습니다"), + ("Timeout", "시간 초과"), + ("Failed to connect to relay server", "릴레이 서버 연결에 실패했습니다"), + ("Failed to connect via rendezvous server", "랑데부 서버를 통한 연결에 실패했습니다"), + ("Failed to connect via relay server", "릴레이 서버를 통한 연결에 실패했습니다"), + ("Failed to make direct connection to remote desktop", "원격 데스크탑 직접 연결에 실패했습니다"), + ("Set Password", "비밀번호 설정"), + ("OS Password", "OS 비밀번호"), + ("install_tip", "UAC로 인해 경우에 따라 RustDesk가 원격 쪽에서 제대로 작동하지 않을 수 있습니다. UAC를 피하려면 아래 버튼을 클릭하여 시스템에 RustDesk를 설치하세요."), + ("Click to upgrade", "업그레이드"), + ("Configure", "구성"), + ("config_acc", "데스크탑을 원격으로 제어하려면 RustDesk에 \"접근성\" 권한을 부여해야 합니다."), + ("config_screen", "데스크탑에 원격으로 액세스하려면 RustDesk에 \"화면 녹화\" 권한을 부여해야 합니다."), + ("Installing ...", "설치 중..."), + ("Install", "설치하기"), + ("Installation", "설치"), + ("Installation Path", "설치 경로"), + ("Create start menu shortcuts", "시작 메뉴에 바로가기 만들기"), + ("Create desktop icon", "바탕 화면 아이콘 만들기"), + ("agreement_tip", "설치를 시작하면 라이선스 계약을 수락하는 것입니다."), + ("Accept and Install", "수락하고 설치"), + ("End-user license agreement", "최종 사용자 라이선스 계약"), + ("Generating ...", "생성 중 ..."), + ("Your installation is lower version.", "설치된 버전이 낮습니다."), + ("not_close_tcp_tip", "터널을 사용하는 동안에는 이 창을 닫지 마세요"), + ("Listening ...", "수신 대기 중 ..."), + ("Remote Host", "원격 호스트"), + ("Remote Port", "원격 포트"), + ("Action", "동작"), + ("Add", "추가"), + ("Local Port", "로컬 포트"), + ("Local Address", "로컬 주소"), + ("Change Local Port", "로컬 포트 변경"), + ("setup_server_tip", "더 빠른 연결을 위해, 자신만의 서버를 설정해 주세요."), + ("Too short, at least 6 characters.", "너무 짧습니다. 최소 6자 이상입니다."), + ("The confirmation is not identical.", "확인이 동일하지 않습니다."), + ("Permissions", "권한"), + ("Accept", "수락"), + ("Dismiss", "거부"), + ("Disconnect", "연결 해제"), + ("Enable file copy and paste", "파일 복사 및 붙여넣기 허용"), + ("Connected", "연결됨"), + ("Direct and encrypted connection", "직접 및 암호화된 연결"), + ("Relayed and encrypted connection", "릴레이 및 암호화된 연결"), + ("Direct and unencrypted connection", "직접 및 암호화되지 않은 연결"), + ("Relayed and unencrypted connection", "릴레이 및 암호화되지 않은 연결"), + ("Enter Remote ID", "원격 ID 입력"), + ("Enter your password", "비밀번호 입력"), + ("Logging in...", "로그인 중..."), + ("Enable RDP session sharing", "RDP 세션 공유 허용"), + ("Auto Login", "자동 로그인"), + ("Enable direct IP access", "직접 IP 액세스 허용"), + ("Rename", "이름 바꾸기"), + ("Space", "공백"), + ("Create desktop shortcut", "바탕 화면 바로가기 만들기"), + ("Change Path", "경로 변경"), + ("Create Folder", "폴더 만들기"), + ("Please enter the folder name", "폴더 이름을 입력해주세요"), + ("Fix it", "문제 해결"), + ("Warning", "경고"), + ("Login screen using Wayland is not supported", "Wayland를 사용한 로그인 화면은 지원되지 않습니다"), + ("Reboot required", "재부팅이 필요합니다"), + ("Unsupported display server", "지원하지 않는 디스플레이 서버"), + ("x11 expected", "x11 환경이 필요합니다"), + ("Port", "포트"), + ("Settings", "설정"), + ("Username", "사용자 이름"), + ("Invalid port", "유효하지 않은 포트입니다"), + ("Closed manually by the peer", "피어가 수동으로 닫았습니다"), + ("Enable remote configuration modification", "원격 구성 수정 허용"), + ("Run without install", "설치 없이 실행"), + ("Connect via relay", "릴레이를 통해 연결"), + ("Always connect via relay", "항상 릴레이를 통해 연결"), + ("whitelist_tip", "화이트리스트에 있는 IP만 나에게 액세스할 수 있음"), + ("Login", "로그인"), + ("Verify", "확인"), + ("Remember me", "기억하기"), + ("Trust this device", "이 장치를 신뢰"), + ("Verification code", "인증 코드"), + ("verification_tip", "등록한 이메일 주소로 인증 코드가 전송되었으니 인증 코드를 입력하여 로그인을 계속하세요."), + ("Logout", "로그아웃"), + ("Tags", "태그"), + ("Search ID", "ID 검색"), + ("whitelist_sep", "쉼표, 세미콜론, 공백 또는 새 줄로 구분합니다."), + ("Add ID", "ID 추가"), + ("Add Tag", "태그 추가"), + ("Unselect all tags", "모든 태그 선택 해제"), + ("Network error", "네트워크 오류"), + ("Username missed", "사용자 이름이 누락되었습니다"), + ("Password missed", "비밀번호가 누락되었습니다"), + ("Wrong credentials", "잘못된 자격 증명"), + ("The verification code is incorrect or has expired", "인증 코드가 올바르지 않거나 만료되었습니다."), + ("Edit Tag", "태그 편집"), + ("Forget Password", "비밀번호 분실"), + ("Favorites", "즐겨찾기"), + ("Add to Favorites", "즐겨찾기에 추가"), + ("Remove from Favorites", "즐겨찾기에서 삭제"), + ("Empty", "비어 있음"), + ("Invalid folder name", "유효하지 않은 폴더 이름"), + ("Socks5 Proxy", "Socks5 프록시"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) 프록시"), + ("Discovered", "발견됨"), + ("install_daemon_tip", "부팅할 때 시작하려면 시스템 서비스를 설치해야 합니다."), + ("Remote ID", "원격 ID"), + ("Paste", "붙여넣기"), + ("Paste here?", "여기에 붙여넣으시겠습니까?"), + ("Are you sure to close the connection?", "연결을 종료하시겠습니까?"), + ("Download new version", "새 버전 다운로드"), + ("Touch mode", "터치 모드"), + ("Mouse mode", "마우스 모드"), + ("One-Finger Tap", "한 손가락 탭"), + ("Left Mouse", "왼쪽 마우스"), + ("One-Long Tap", "한 번 길게 탭"), + ("Two-Finger Tap", "두 손가락 탭"), + ("Right Mouse", "오른쪽 마우스"), + ("One-Finger Move", "한 손가락으로 이동"), + ("Double Tap & Move", "두 번 탭하고 이동"), + ("Mouse Drag", "마우스 끌기"), + ("Three-Finger vertically", "세 손가락으로 수직"), + ("Mouse Wheel", "마우스 휠"), + ("Two-Finger Move", "두 손가락으로 이동"), + ("Canvas Move", "캔버스 이동"), + ("Pinch to Zoom", "찝어서 확대/축소"), + ("Canvas Zoom", "캔버스 확대/축소"), + ("Reset canvas", "캔버스 초기화"), + ("No permission of file transfer", "파일 전송 권한이 없습니다"), + ("Note", "노트"), + ("Connection", "연결"), + ("Share screen", "화면 공유"), + ("Chat", "채팅"), + ("Total", "전체"), + ("items", "항목"), + ("Selected", "선택됨"), + ("Screen Capture", "화면 캡처"), + ("Input Control", "입력 제어"), + ("Audio Capture", "오디오 캡처"), + ("Do you accept?", "수락하시겠습니까?"), + ("Open System Setting", "시스템 설정 열기"), + ("How to get Android input permission?", "Android 입력 권한을 얻는 방법은?"), + ("android_input_permission_tip1", "원격 장치에서 마우스나 터치로 Android 장치를 제어하려면 RustDesk가 \"접근성\" 서비스를 사용하도록 허용해야 합니다."), + ("android_input_permission_tip2", "다음 시스템 설정 페이지로 이동하여 [설치된 서비스]를 찾아 들어가서 [RustDesk 입력] 서비스를 켜세요."), + ("android_new_connection_tip", "현재 장치를 제어하려는 새로운 제어 요청이 수신되었습니다."), + ("android_service_will_start_tip", "\"화면 캡처\"를 켜면 자동으로 서비스가 시작되어 다른 장치가 내 장치에 연결을 요청할 수 있습니다."), + ("android_stop_service_tip", "서비스를 닫으면 설정된 모든 연결이 자동으로 닫힙니다."), + ("android_version_audio_tip", "현재 Android 버전은 오디오 캡처를 지원하지 않으므로 Android 10 이상으로 업그레이드하세요."), + ("android_start_service_tip", "[서비스 시작]을 탭하거나 [화면 캡처] 권한을 활성화하여 화면 공유 서비스를 시작합니다."), + ("android_permission_may_not_change_tip", "설정된 연결에 대한 권한은 다시 연결할 때까지 즉시 변경되지 않을 수 있습니다."), + ("Account", "계정"), + ("Overwrite", "덮어쓰기"), + ("This file exists, skip or overwrite this file?", "이 파일이 이미 존재합니다, 건너뛰거나 덮어쓰시겠습니까?"), + ("Quit", "종료"), + ("Help", "도움말"), + ("Failed", "실패"), + ("Succeeded", "성공"), + ("Someone turns on privacy mode, exit", "누군가 개인정보 보호 모드를 켰습니다, 연결을 종료합니다"), + ("Unsupported", "지원되지 않음"), + ("Peer denied", "연결 거부됨"), + ("Please install plugins", "플러그인을 설치해주세요"), + ("Peer exit", "피어 종료"), + ("Failed to turn off", "끄기 실패"), + ("Turned off", "꺼짐"), + ("Language", "언어"), + ("Keep RustDesk background service", "RustDesk 백그라운드 서비스 유지"), + ("Ignore Battery Optimizations", "배터리 최적화 무시"), + ("android_open_battery_optimizations_tip", "이 기능을 비활성화하려면 다음 RustDesk 응용 프로그램 설정 페이지로 이동하여 [배터리]를 찾아서 입력하고 [제한 없음]을 선택 취소하세요"), + ("Start on boot", "부팅 시 시작"), + ("Start the screen sharing service on boot, requires special permissions", "부팅 시 화면 공유 서비스를 시작하려면 특별 권한이 필요합니다"), + ("Connection not allowed", "연결이 허용되지 않았습니다"), + ("Legacy mode", "레거시 모드"), + ("Map mode", "맵 모드"), + ("Translate mode", "번역 모드"), + ("Use permanent password", "영구 비밀번호 사용"), + ("Use both passwords", "두 가지 비밀번호 모두 사용"), + ("Set permanent password", "영구 비밀번호 설정"), + ("Enable remote restart", "원격 재시작 허용"), + ("Restart remote device", "원격 장치 다시 시작"), + ("Are you sure you want to restart", "다시 시작하시겠습니까"), + ("Restarting remote device", "원격 장치를 다시 시작하는 중"), + ("remote_restarting_tip", "원격 장치가 다시 시작되고 있습니다. 이 메시지 상자를 닫고 잠시 후 영구 비밀번호로 다시 연결해 주세요"), + ("Copied", "복사되었습니다"), + ("Exit Fullscreen", "전체 화면 종료"), + ("Fullscreen", "전체 화면"), + ("Mobile Actions", "모바일 작업"), + ("Select Monitor", "모니터 선택"), + ("Control Actions", "제어 작업"), + ("Display Settings", "디스플레이 설정"), + ("Ratio", "비율"), + ("Image Quality", "이미지 품질"), + ("Scroll Style", "스크롤 스타일"), + ("Show Toolbar", "도구 모음 표시"), + ("Hide Toolbar", "도구 모음 숨기기"), + ("Direct Connection", "직접 연결"), + ("Relay Connection", "릴레이 연결"), + ("Secure Connection", "보안 연결"), + ("Insecure Connection", "보안되지 않은 연결"), + ("Scale original", "원본 크기 조정"), + ("Scale adaptive", "크기 조정 가능"), + ("General", "일반"), + ("Security", "보안"), + ("Theme", "테마"), + ("Dark Theme", "어두운 테마"), + ("Light Theme", "밝은 테마"), + ("Dark", "어두운"), + ("Light", "밝은"), + ("Follow System", "시스템 설정 따름"), + ("Enable hardware codec", "하드웨어 코덱 활성화"), + ("Unlock Security Settings", "보안 설정 잠금 해제"), + ("Enable audio", "오디오 허용"), + ("Unlock Network Settings", "네트워크 설정 잠금 해제"), + ("Server", "서버"), + ("Direct IP Access", "직접 IP 연결"), + ("Proxy", "프록시"), + ("Apply", "적용"), + ("Disconnect all devices?", "모든 장치의 연결을 해제하시겠습니까?"), + ("Clear", "지우기"), + ("Audio Input Device", "오디오 입력 장치"), + ("Use IP Whitelisting", "IP 화이트리스트 사용"), + ("Network", "네트워크"), + ("Pin Toolbar", "도구 모음 고정"), + ("Unpin Toolbar", "도구 모음 고정 해제"), + ("Recording", "녹화"), + ("Directory", "디렉터리"), + ("Automatically record incoming sessions", "수신 세션 자동 녹화"), + ("Automatically record outgoing sessions", "발신 세션 자동 녹화"), + ("Change", "변경"), + ("Start session recording", "세션 녹화 시작"), + ("Stop session recording", "세션 녹화 중지"), + ("Enable recording session", "세션 녹화 허용"), + ("Enable LAN discovery", "LAN 검색 허용"), + ("Deny LAN discovery", "LAN 검색 거부"), + ("Write a message", "메시지 쓰기"), + ("Prompt", "프롬프트"), + ("Please wait for confirmation of UAC...", "UAC 확인을 기다려주세요..."), + ("elevated_foreground_window_tip", "원격 데스크탑의 현재 창을 작동하려면 더 높은 권한이 필요하므로 일시적으로 마우스와 키보드를 사용할 수 없습니다. 원격 사용자에게 현재 창을 최소화하도록 요청하거나 연결 관리 창에서 권한 상승 버튼을 클릭할 수 있습니다. 이 문제를 방지하려면 원격 장치에 소프트웨어를 설치하는 것이 좋습니다."), + ("Disconnected", "연결 끊김"), + ("Other", "기타"), + ("Confirm before closing multiple tabs", "여러 탭을 닫기 전에 확인"), + ("Keyboard Settings", "키보드 설정"), + ("Full Access", "전체 액세스"), + ("Screen Share", "화면 공유"), + ("ubuntu-21-04-required", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."), + ("wayland-requires-higher-linux-version", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크탑을 사용하거나 OS를 변경하세요."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "점프 링크"), + ("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하세요 (피어 측에서 작동)"), + ("Show RustDesk", "RustDesk 표시"), + ("This PC", "이 PC"), + ("or", "또는"), + ("Elevate", "권한 상승"), + ("Zoom cursor", "커서 확대/축소"), + ("Accept sessions via password", "비밀번호를 통해 세션 수락"), + ("Accept sessions via click", "클릭을 통해 세션 수락"), + ("Accept sessions via both", "두 가지 방법을 통해 세션 수락"), + ("Please wait for the remote side to accept your session request...", "원격 측에서 세션 요청을 수락할 때까지 기다려주세요..."), + ("One-time Password", "일회용 비밀번호"), + ("Use one-time password", "일회용 비밀번호 사용"), + ("One-time password length", "일회용 비밀번호 길이"), + ("Request access to your device", "장치에 대한 액세스 권한을 요청"), + ("Hide connection management window", "연결 관리 창 숨기기"), + ("hide_cm_tip", "비밀번호를 통해 세션을 수락하고 영구 비밀번호를 사용하는 경우에만 숨기기 허용"), + ("wayland_experiment_tip", "Wayland 지원은 실험 단계에 있으며, 무인 접근이 필요한 경우 X11을 사용해 주세요."), + ("Right click to select tabs", "마우스 오른쪽 버튼을 클릭하여 탭 선택"), + ("Skipped", "건너뜀"), + ("Add to address book", "주소록에 추가"), + ("Group", "그룹"), + ("Search", "검색"), + ("Closed manually by web console", "웹 콘솔에 의해 수동으로 닫힘"), + ("Local keyboard type", "로컬 키보드 유형"), + ("Select local keyboard type", "로컬 키보드 유형 선택"), + ("software_render_tip", "Linux에서 Nvidia 그래픽 카드를 사용 중인데 원격 창이 연결 즉시 닫히는 경우 오픈 소스 Nouveau 드라이버로 전환하고 소프트웨어 렌더링을 사용하기로 선택하는 것이 도움이 될 수 있습니다. 소프트웨어를 재시작해야 합니다."), + ("Always use software rendering", "항상 소프트웨어 렌더링 사용"), + ("config_input", "키보드로 원격 데스크탑을 제어하려면 RustDesk에 \"입력 모니터링\" 권한을 부여해야 합니다."), + ("config_microphone", "원격으로 통화하려면 RustDesk에 \"오디오 녹음\" 권한을 부여해야 합니다."), + ("request_elevation_tip", "원격 측에 사람이 있는 경우 권한 상승을 요청할 수도 있습니다."), + ("Wait", "대기"), + ("Elevation Error", "권한 상승 오류"), + ("Ask the remote user for authentication", "원격 사용자에게 인증 요청"), + ("Choose this if the remote account is administrator", "원격 계정이 관리자인 경우 이 옵션을 선택합니다"), + ("Transmit the username and password of administrator", "관리자의 사용자 이름과 비밀번호 전송"), + ("still_click_uac_tip", "여전히 원격 사용자가 RustDesk를 실행하는 UAC 창에서 확인을 클릭해야 합니다."), + ("Request Elevation", "권한 상승 요청"), + ("wait_accept_uac_tip", "원격 사용자가 UAC 대화 상자를 수락할 때까지 기다리세요."), + ("Elevate successfully", "권한 상승이 성공하였습니다"), + ("uppercase", "대문자"), + ("lowercase", "소문자"), + ("digit", "숫자"), + ("special character", "특수 문자"), + ("length>=8", "8자 이상"), + ("Weak", "약함"), + ("Medium", "보통"), + ("Strong", "강력"), + ("Switch Sides", "역할 전환"), + ("Please confirm if you want to share your desktop?", "데스크탑을 공유하시겠습니까?"), + ("Display", "디스플레이"), + ("Default View Style", "기본 보기 스타일"), + ("Default Scroll Style", "기본 스크롤 스타일"), + ("Default Image Quality", "기본 이미지 품질"), + ("Default Codec", "기본 코덱"), + ("Bitrate", "비트레이트"), + ("FPS", "FPS"), + ("Auto", "자동"), + ("Other Default Options", "기타 기본 옵션"), + ("Voice call", "음성 통화"), + ("Text chat", "텍스트 채팅"), + ("Stop voice call", "음성 통화 종료"), + ("relay_hint_tip", "직접 연결이 불가능할 수 있으며 릴레이를 통해 연결을 시도할 수 있습니다. 또한 첫 번째 시도에서 릴레이를 사용하려면 아이디에 \"/r\" 접미사를 추가하거나 최근 세션 카드에 \"항상 릴레이를 통해 연결\" 옵션이 있는 경우 이 옵션을 선택하면 됩니다."), + ("Reconnect", "다시 연결"), + ("Codec", "코덱"), + ("Resolution", "해상도"), + ("No transfers in progress", "진행 중인 전송이 없습니다"), + ("Set one-time password length", "일회용 비밀번호 길이 설정"), + ("RDP Settings", "RDP 설정"), + ("Sort by", "정렬 기준"), + ("New Connection", "새 연결"), + ("Restore", "복원"), + ("Minimize", "최소화"), + ("Maximize", "최대화"), + ("Your Device", "내 장치"), + ("empty_recent_tip", "어머나, 최근 세션이 없네요!\n새로운 것을 계획할 시간입니다."), + ("empty_favorite_tip", "아직 즐겨찾는 피어가 없나요?\n연결하고 싶은 피어를 찾아 즐겨찾기에 추가해 보세요!"), + ("empty_lan_tip", "오 아니요, 아직 피어를 발견하지 못한 것 같습니다."), + ("empty_address_book_tip", "오, 이게 무슨 일인지 주소록에 현재 나열된 피어가 없는 것 같습니다."), + ("Empty Username", "사용자 이름이 비어있습니다"), + ("Empty Password", "비밀번호가 비어있습니다"), + ("Me", "나"), + ("identical_file_tip", "이 파일은 상대방의 파일과 일치합니다."), + ("show_monitors_tip", "도구 모음에 모니터 표시"), + ("View Mode", "보기 모드"), + ("login_linux_tip", "X 데스크탑을 활성화하려면 제어되는 터미널의 Linux 계정에 로그인하세요"), + ("verify_rustdesk_password_tip", "RustDesk 비밀번호 확인"), + ("remember_account_tip", "이 계정 기억하기"), + ("os_account_desk_tip", "이 계정은 원격 OS에 로그인하고 헤드리스에서 데스크탑 세션을 활성화하는 데 사용됩니다."), + ("OS Account", "OS 계정"), + ("another_user_login_title_tip", "다른 사용자가 이미 로그인했습니다"), + ("another_user_login_text_tip", "연결 끊기"), + ("xorg_not_found_title_tip", "Xorg를 찾을 수 없습니다"), + ("xorg_not_found_text_tip", "Xorg를 설치해 주세요"), + ("no_desktop_title_tip", "사용 가능한 데스크탑 환경이 없습니다"), + ("no_desktop_text_tip", "GNOME 데스크탑을 설치해 주세요"), + ("No need to elevate", "권한 상승이 필요없습니다"), + ("System Sound", "시스템 소리"), + ("Default", "기본"), + ("New RDP", "새 RDP"), + ("Fingerprint", "지문"), + ("Copy Fingerprint", "지문 복사"), + ("no fingerprints", "지문이 없습니다"), + ("Select a peer", "피어 선택"), + ("Select peers", "피어 선택"), + ("Plugins", "플러그인"), + ("Uninstall", "설치 제거"), + ("Update", "업데이트"), + ("Enable", "허용"), + ("Disable", "사용 안 함"), + ("Options", "옵션"), + ("resolution_original_tip", "원본 해상도"), + ("resolution_fit_local_tip", "로컬 화면에 맞춤"), + ("resolution_custom_tip", "사용자 지정 해상도"), + ("Collapse toolbar", "도구 모음 접기"), + ("Accept and Elevate", "수락 및 권한 상승"), + ("accept_and_elevate_btn_tooltip", "연결을 수락하고 UAC 권한을 높입니다."), + ("clipboard_wait_response_timeout_tip", "복사 응답을 기다리는 동안 시간이 초과되었습니다."), + ("Incoming connection", "수신 연결"), + ("Outgoing connection", "발신 연결"), + ("Exit", "종료"), + ("Open", "열기"), + ("logout_tip", "로그아웃하시겠습니까?"), + ("Service", "서비스"), + ("Start", "시작"), + ("Stop", "중지"), + ("exceed_max_devices", "관리되는 장치의 최대 수에 도달했습니다."), + ("Sync with recent sessions", "최근 세션과 동기화"), + ("Sort tags", "태그 정렬"), + ("Open connection in new tab", "새 탭에서 연결 열기"), + ("Move tab to new window", "새 창으로 탭 이동"), + ("Can not be empty", "비워둘 수 없습니다"), + ("Already exists", "이미 존재합니다"), + ("Change Password", "비밀번호 변경"), + ("Refresh Password", "비밀번호 새로 고침"), + ("ID", "ID"), + ("Grid View", "격자 보기"), + ("List View", "목록 보기"), + ("Select", "선택"), + ("Toggle Tags", "태그 전환"), + ("pull_ab_failed_tip", "주소록을 새로 고치지 못했습니다"), + ("push_ab_failed_tip", "주소록을 서버에 동기화하지 못했습니다"), + ("synced_peer_readded_tip", "최근 세션에 있던 장치들이 주소록으로 다시 동기화될 것입니다."), + ("Change Color", "색상 변경"), + ("Primary Color", "기본 색상"), + ("HSV Color", "HSV 색상"), + ("Installation Successful!", "설치에 성공했습니다!"), + ("Installation failed!", "설치에 실패했습니다!"), + ("Reverse mouse wheel", "마우스 휠 반전"), + ("{} sessions", "{} 세션"), + ("scam_title", "사기를 당하고 있을 수 있습니다!"), + ("scam_text1", "알지 못하고 신뢰할 수 없는 사람이 전화를 걸어 RustDesk를 사용하고 서비스를 시작하라고 요청하는 경우 계속 진행하지 말고 즉시 전화를 끊으세요."), + ("scam_text2", "사기꾼이 귀하의 돈이나 기타 개인 정보를 훔치려 할 가능성이 높습니다."), + ("Don't show again", "다시 표시하지 않음"), + ("I Agree", "동의"), + ("Decline", "거절"), + ("Timeout in minutes", "시간 초과 (분)"), + ("auto_disconnect_option_tip", "사용자가 비활성 상태일 때 수신 세션 자동 종료"), + ("Connection failed due to inactivity", "활동이 없어 자동으로 연결이 끊어졌습니다"), + ("Check for software update on startup", "시작 시 소프트웨어 업데이트 확인"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "RustDesk Server Pro를 {} 버전 이상으로 업그레이드하세요!"), + ("pull_group_failed_tip", "그룹 새로 고침에 실패했습니다"), + ("Filter by intersection", "교차해서 필터링"), + ("Remove wallpaper during incoming sessions", "수신 세션 동안 배경화면 제거"), + ("Test", "테스트"), + ("display_is_plugged_out_msg", "디스플레이가 분리되어 있으면 첫 번째 디스플레이로 전환합니다."), + ("No displays", "디스플레이 없음"), + ("Open in new window", "새 창에서 열기"), + ("Show displays as individual windows", "디스플레이를 개별 창으로 표시"), + ("Use all my displays for the remote session", "원격 세션에 내 모든 디스플레이 사용"), + ("selinux_tip", "SELinux가 장치에서 활성화되어 있어 RustDesk가 제어된 상태로 제대로 작동하지 않을 수 있습니다."), + ("Change view", "보기 변경"), + ("Big tiles", "큰 타일"), + ("Small tiles", "작은 타일"), + ("List", "목록"), + ("Virtual display", "가상 디스플레이"), + ("Plug out all", "모든 플러그를 뽑으세요"), + ("True color (4:4:4)", "트루컬러 (4:4:4)"), + ("Enable blocking user input", "사용자 입력 차단 허용"), + ("id_input_tip", "ID, 직접 IP 또는 포트가 있는 도메인 (:)을 입력할 수 있습니다.\n다른 서버에 있는 장치에 액세스하려면 서버 주소 (@?key=)를 추가하세요. 예를들어 \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\n공용 서버의 장치에 액세스하려면 \"@public\"을 입력하세요. 공용 서버에서는 키가 필요하지 않습니다.\n\n첫 번째 연결에서 릴레이 연결을 강제로 사용하려면 ID 끝에 \"/r\"을 추가합니다, 예를들면 \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "모드 1"), + ("privacy_mode_impl_virtual_display_tip", "모드 2"), + ("Enter privacy mode", "개인정보 보호 모드 시작"), + ("Exit privacy mode", "개인정보 보호 모드 종료"), + ("idd_not_support_under_win10_2004_tip", "간접 디스플레이 드라이버는 지원되지 않습니다. Windows 10 버전 2004 이상이 필요합니다."), + ("input_source_1_tip", "입력 소스 1"), + ("input_source_2_tip", "입력 소스 2"), + ("Swap control-command key", "Control 및 Command 키 교체"), + ("swap-left-right-mouse", "마우스 왼쪽 버튼과 오른쪽 버튼 교체"), + ("2FA code", "이중 인증 코드"), + ("More", "더 많은"), + ("enable-2fa-title", "이중 인증 허용"), + ("enable-2fa-desc", "지금 인증앱을 설정해 주세요. 휴대폰이나 데스크탑에서 Authy, Microsoft 또는 Google 인증기와 같은 인증기 앱을 사용할 수 있습니다.\n\n앱으로 QR 코드를 스캔하고 앱에 표시된 코드를 입력하면 이중 인증이 가능합니다."), + ("wrong-2fa-code", "코드를 확인할 수 없습니다. 코드와 현지 시간 설정이 올바른지 확인합니다"), + ("enter-2fa-title", "이중 인증"), + ("Email verification code must be 6 characters.", "이메일 인증 코드는 6자여야 합니다."), + ("2FA code must be 6 digits.", "이중 인증 코드는 6자리여야 합니다."), + ("Multiple Windows sessions found", "여러 Windows 세션이 발견되었습니다"), + ("Please select the session you want to connect to", "연결할 세션을 선택해 주세요"), + ("powered_by_me", "RustDesk 제공"), + ("outgoing_only_desk_tip", "이것은 맞춤형 에디션입니다.\n다른 장치에 연결할 수는 있지만 귀하의 기기에 연결할 수 없습니다."), + ("preset_password_warning", "이 맞춤형 에디션에는 미리 설정된 비밀번호가 함께 제공됩니다. 이 비밀번호를 아는 사람이라면 누구나 기기를 완전히 제어할 수 있습니다. 예상치 못한 경우 즉시 소프트웨어를 제거하세요."), + ("Security Alert", "보안 경고"), + ("My address book", "내 주소록"), + ("Personal", "개인"), + ("Owner", "소유자"), + ("Set shared password", "공유 비밀번호 설정"), + ("Exist in", "다음 위치 존재"), + ("Read-only", "읽기 전용"), + ("Read/Write", "읽기/쓰기"), + ("Full Control", "전체 제어"), + ("share_warning_tip", "위의 필드는 공유되고 다른 사람들에게 보입니다."), + ("Everyone", "모두"), + ("ab_web_console_tip", "웹 콘솔에 대해 더 알아보기"), + ("allow-only-conn-window-open-tip", "RustDesk 창이 열려 있을 때만 연결 허용"), + ("no_need_privacy_mode_no_physical_displays_tip", "실제 디스플레이가 없으므로 개인 정보 보호 모드를 사용할 필요가 없습니다."), + ("Follow remote cursor", "원격 커서 따라가기"), + ("Follow remote window focus", "원격 창 초점 따라가기"), + ("default_proxy_tip", "기본 프로토콜 및 포트는 Socks5 및 1080입니다"), + ("no_audio_input_device_tip", "오디오 입력 장치를 찾을 수 없습니다."), + ("Incoming", "수신"), + ("Outgoing", "발신"), + ("Clear Wayland screen selection", "Wayland 화면 선택 지우기"), + ("clear_Wayland_screen_selection_tip", "화면 선택을 지운 후, 공유할 화면을 다시 선택할 수 있습니다."), + ("confirm_clear_Wayland_screen_selection_tip", "Wayland 화면 선택을 정말 취소하시겠습니까?"), + ("android_new_voice_call_tip", "새 음성 통화 요청이 수신되었습니다. 수락하면 오디오가 음성 통신으로 전환됩니다."), + ("texture_render_tip", "텍스처 렌더링을 사용하여 사진을 더 부드럽게 만듭니다. 렌더링 문제가 발생하면 이 옵션을 비활성화할 수 있습니다."), + ("Use texture rendering", "텍스처 렌더링 사용"), + ("Floating window", "플로팅 창"), + ("floating_window_tip", "RustDesk 백그라운드 서비스를 유지하는 데 도움이 됩니다"), + ("Keep screen on", "화면 켜짐 유지"), + ("Never", "없음"), + ("During controlled", "제어되는 동안"), + ("During service is on", "서비스 중"), + ("Capture screen using DirectX", "DirectX를 사용하여 화면 캡처"), + ("Back", "뒤로"), + ("Apps", "앱"), + ("Volume up", "볼륨 높이기"), + ("Volume down", "볼륨 낮추기"), + ("Power", "전원"), + ("Telegram bot", "Telegram 봇"), + ("enable-bot-tip", "이 기능을 활성화하면 봇에서 이중 인증 코드를 받을 수 있습니다. 또한 연결 알림 기능도 할 수 있습니다."), + ("enable-bot-desc", "1. @BotFather와 채팅을 시작합니다.\n2. \"/newbot\" 명령을 보내주세요. 이 단계를 완료하면 토큰을 받게 됩니다.\n3. 새로 만든 봇과 채팅을 시작합니다. \"/hello\"와 같이 앞에 슬래시 (\"/\")로 시작하는 메시지를 보내 활성화합니다."), + ("cancel-2fa-confirm-tip", "이중 인증을 취소하시겠습니까?"), + ("cancel-bot-confirm-tip", "Telegram 봇을 취소하시겠습니까?"), + ("About RustDesk", "RustDesk 정보"), + ("Send clipboard keystrokes", "클립보드 키 입력 보내기"), + ("network_error_tip", "네트워크 연결을 확인한 다음 재시도를 클릭하세요."), + ("Unlock with PIN", "PIN으로 잠금 해제"), + ("Requires at least {} characters", "최소 {}자 이상 필요합니다."), + ("Wrong PIN", "잘못된 PIN"), + ("Set PIN", "PIN 설정"), + ("Enable trusted devices", "신뢰할 수 있는 장치 허용"), + ("Manage trusted devices", "신뢰할 수 있는 장치 관리"), + ("Platform", "플랫폼"), + ("Days remaining", "일 남음"), + ("enable-trusted-devices-tip", "신뢰할 수 있는 장치에서 이중 인증 건너뛰기"), + ("Parent directory", "상위 디렉터리"), + ("Resume", "재개"), + ("Invalid file name", "잘못된 파일 이름"), + ("one-way-file-transfer-tip", "제어되는 측에서는 단방향 파일 전송이 가능합니다."), + ("Authentication Required", "인증 필요"), + ("Authenticate", "인증"), + ("web_id_input_tip", "동일한 서버에 ID를 입력할 수 있으며, 웹 클라이언트에서는 다이렉트 IP 액세스가 지원되지 않습니다.\n다른 서버에 있는 장치에 액세스하려면 서버 주소 (@?key=)를 추가해 주세요. 예를 들어 \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\n공용 서버에서 장치에 액세스하려면 \"@public\"을 입력해 주세요. 공용 서버에는 키가 필요하지 않습니다."), + ("Download", "다운로드"), + ("Upload folder", "폴더 업로드"), + ("Upload files", "파일 업로드"), + ("Clipboard is synchronized", "클립보드가 동기화되었습니다"), + ("Update client clipboard", "클라이언트 클립보드 업데이트"), + ("Untagged", "태그 없음"), + ("new-version-of-{}-tip", "{}의 새 버전을 사용할 수 있습니다"), + ("Accessible devices", "액세스 가능한 장치"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "RustDesk 클라이언트를 원격 버전 {} 이상으로 업그레이드해 주세요!"), + ("d3d_render_tip", "D3D 렌더링이 활성화되면 일부 기기에서는 원격 화면이 검은색으로 표시될 수 있습니다."), + ("Use D3D rendering", "D3D 렌더링 사용"), + ("Printer", "프린터"), + ("printer-os-requirement-tip", "프린터 출력 기능은 Windows 10 이상이 필요합니다."), + ("printer-requires-installed-{}-client-tip", "원격 인쇄 기능을 사용하려면 이 장치에 {}를 설치해야 합니다."), + ("printer-{}-not-installed-tip", "{} 프린터가 설치되지 않았습니다."), + ("printer-{}-ready-tip", "{} 프린터가 설치되어 사용할 준비가 되었습니다."), + ("Install {} Printer", "{} 프린터 설치"), + ("Outgoing Print Jobs", "발신 인쇄 작업"), + ("Incoming Print Jobs", "수신 인쇄 작업"), + ("Incoming Print Job", "수신 인쇄 작업"), + ("use-the-default-printer-tip", "기본 프린터 사용"), + ("use-the-selected-printer-tip", "선택한 프린터 사용"), + ("auto-print-tip", "선택한 프린터를 사용하여 자동으로 인쇄합니다."), + ("print-incoming-job-confirm-tip", "원격에서 인쇄 작업을 받았습니다. 옆에서 실행하시겠습니까?"), + ("remote-printing-disallowed-tile-tip", "원격 인쇄 허용 안 함"), + ("remote-printing-disallowed-text-tip", "제어측의 권한 설정에서 원격 인쇄를 거부합니다."), + ("save-settings-tip", "설정 저장"), + ("dont-show-again-tip", "다시 표시하지 않음"), + ("Take screenshot", "스크린샷 찍기"), + ("Taking screenshot", "스크린샷 찍는 중"), + ("screenshot-merged-screen-not-supported-tip", "현재 다중 디스플레이의 스크린샷 병합이 지원되지 않습니다. 단일 디스플레이로 전환한 후 다시 시도해 주세요."), + ("screenshot-action-tip", "스크린샷을 계속 진행할 방법을 선택해 주세요."), + ("Save as", "다른 이름으로 저장"), + ("Copy to clipboard", "클립보드에 복사"), + ("Enable remote printer", "원격 프린터 허용"), + ("Downloading {}", "{} 다운로드 중"), + ("{} Update", "{} 업데이트"), + ("{}-to-update-tip", "{}가 지금 닫히고 새 버전을 설치합니다."), + ("download-new-version-failed-tip", "다운로드에 실패했습니다. 다시 시도하거나 \"다운로드\" 버튼을 클릭하여 릴리스 페이지에서 다운로드하고 수동으로 업그레이드할 수 있습니다."), + ("Auto update", "자동 업데이트"), + ("update-failed-check-msi-tip", "설치 방법 확인에 실패했습니다. \"다운로드\" 버튼을 클릭하여 릴리스 페이지에서 다운로드하고 수동으로 업그레이드하세요."), + ("websocket_tip", "WebSocket을 사용할 때는 릴레이 연결만 지원됩니다."), + ("Use WebSocket", "웹소켓 사용"), + ("Trackpad speed", "트랙패드 속도"), + ("Default trackpad speed", "기본 트랙패드 속도"), + ("Numeric one-time password", "숫자 일회용 비밀번호"), + ("Enable IPv6 P2P connection", "IPv6 P2P 연결 사용"), + ("Enable UDP hole punching", "UDP 홀 펀칭 사용"), + ("View camera", "카메라 보기"), + ("Enable camera", "카메라 허용"), + ("No cameras", "카메라 없음"), + ("view_camera_unsupported_tip", "원격 장치가 카메라 보기를 지원하지 않습니다."), + ("Terminal", "터미널"), + ("Enable terminal", "터미널 허용"), + ("New tab", "새 탭"), + ("Keep terminal sessions on disconnect", "연결이 끊어져도 터미널 세션 유지"), + ("Terminal (Run as administrator)", "터미널 (관리자 권한으로 실행)"), + ("terminal-admin-login-tip", "제어되는 측의 관리자 사용자 이름과 비밀번호를 입력하세요."), + ("Failed to get user token.", "사용자 토큰을 가져오는 데 실패했습니다."), + ("Incorrect username or password.", "사용자 이름이나 비밀번호가 올바르지 않습니다."), + ("The user is not an administrator.", "사용자가 관리자가 아닙니다."), + ("Failed to check if the user is an administrator.", "사용자가 관리자인지 확인하는 데 실패했습니다."), + ("Supported only in the installed version.", "설치된 버전에서만 지원됩니다."), + ("elevation_username_tip", "사용자 이름 또는 도메인\\사용자 이름 입력"), + ("Preparing for installation ...", "설치 준비 중 ..."), + ("Show my cursor", "내 커서 표시"), + ("Scale custom", "사용자 지정 크기 조정"), + ("Custom scale slider", "사용자 지정 크기 조정 슬라이더"), + ("Decrease", "축소"), + ("Increase", "확대"), + ("Show virtual mouse", "가상 마우스 표시"), + ("Virtual mouse size", "가상 마우스 크기"), + ("Small", "작게"), + ("Large", "크게"), + ("Show virtual joystick", "가상 조이스틱 표시"), + ("Edit note", "노트 편집"), + ("Alias", "별명"), + ("ScrollEdge", "가장자리 스크롤"), + ("Allow insecure TLS fallback", "보안되지 않은 TLS 폴백 허용"), + ("allow-insecure-tls-fallback-tip", "기본적으로 RustDesk는 TLS를 사용하여 프로토콜에 대한 서버 인증서를 검증합니다.\n이 옵션을 활성화하면 RustDesk는 인증 단계를 건너뛰고 인증 실패 시 진행합니다."), + ("Disable UDP", "UDP 사용 안 함"), + ("disable-udp-tip", "TCP만 사용할지 여부를 제어합니다.\n이 옵션을 활성화하면 RustDesk는 더 이상 UDP 2116을 사용하지 않고 대신 TCP 2116을 사용합니다."), + ("server-oss-not-support-tip", "참고: RustDesk 서버 OSS에는 이 기능이 포함되어 있지 않습니다."), + ("input note here", "여기에 노트 입력"), + ("note-at-conn-end-tip", "연결이 끝날 때 메모 요청"), + ("Show terminal extra keys", "터미널 추가 키 표시"), + ("Relative mouse mode", "상대 마우스 모드"), + ("rel-mouse-not-supported-peer-tip", "연결된 피어에서 상대 마우스 모드를 지원하지 않습니다."), + ("rel-mouse-not-ready-tip", "상대 마우스 모드가 아직 준비되지 않았습니다. 다시 시도해 주세요."), + ("rel-mouse-lock-failed-tip", "커서 잠금에 실패했습니다. 상대 마우스 모드가 비활성화되었습니다"), + ("rel-mouse-exit-{}-tip", "종료하려면 {}을(를) 누르세요."), + ("rel-mouse-permission-lost-tip", "키보드 권한이 취소되었습니다. 상대 마우스 모드가 비활성화되었습니다."), + ("Changelog", "변경 기록"), + ("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"), + ("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"), + ("Continue with {}", "{}(으)로 계속"), + ("Display Name", "표시 이름"), + ("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."), + ("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/kz.rs b/vendor/rustdesk/src/lang/kz.rs new file mode 100644 index 0000000..a2a1624 --- /dev/null +++ b/vendor/rustdesk/src/lang/kz.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Күй"), + ("Your Desktop", "Сіздің Жұмыс үстеліңіз"), + ("desk_tip", "Сіздің Жұмыс үстеліңіз осы ID мен құпия сөз арқылы қолжетімді"), + ("Password", "Құпия сөз"), + ("Ready", "Дайын"), + ("Established", "Қосылды"), + ("connecting_status", "RustDesk желісіне қосылуда..."), + ("Enable service", "Сербесті қосу"), + ("Start service", "Сербесті іске қосу"), + ("Service is running", "Сербес істеуде"), + ("Service is not running", "Сербес істемеуде"), + ("not_ready_status", "Дайын емес. Қосылымды тексеруді өтінеміз"), + ("Control Remote Desktop", "Қашықтағы Жұмыс үстелін Басқару"), + ("Transfer file", "Файыл Тасымалдау"), + ("Connect", "Қосылу"), + ("Recent sessions", "Соңғы Сештер"), + ("Address book", "Мекенжай Кітабы"), + ("Confirmation", "Мақұлдау"), + ("TCP tunneling", "TCP тунелдеу"), + ("Remove", "Жою"), + ("Refresh random password", "Кездейсоқ құпия сөзді жаңарту"), + ("Set your own password", "Өз құпия сөзіңізді орнатыңыз"), + ("Enable keyboard/mouse", "Пернетақта/Тінтуірді қосу"), + ("Enable clipboard", "Көшіру-тақтасын қосу"), + ("Enable file transfer", "Файыл Тасымалдауды қосу"), + ("Enable TCP tunneling", "TCP тунелдеуді қосу"), + ("IP Whitelisting", "IP Ақ-тізімі"), + ("ID/Relay Server", "ID/Relay сербері"), + ("Import server config", "Серверді импорттау"), + ("Export Server Config", ""), + ("Import server configuration successfully", "Сервердің конфигурациясы сәтті импортталды"), + ("Export server configuration successfully", ""), + ("Invalid server configuration", "Жарамсыз сервердің конфигурациясы"), + ("Clipboard is empty", "Көшіру-тақта бос"), + ("Stop service", "Сербесті тоқтату"), + ("Change ID", "ID ауыстыру"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Тек a-z, A-Z, 0-9, - (dash) және _ (астынғы-сызық) таңбалары рұқсат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 арасы."), + ("Website", "Web-сайт"), + ("About", "Туралы"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Дыбыссыздандыру"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), + ("Audio Input", "Аудио Еңгізу"), + ("Enhancements", "Жақсартулар"), + ("Hardware Codec", "Hardware Codec"), + ("Adaptive bitrate", "Adaptive bitrate"), + ("ID Server", "ID Сербері"), + ("Relay Server", "Relay Сербері"), + ("API Server", "API Сербері"), + ("invalid_http", "http:// немесе https://'пен басталуы қажет"), + ("Invalid IP", "Бұрыс IP-Мекенжай"), + ("Invalid format", "Бұрыс формат"), + ("server_not_support", "Сербер әзірше қолдамайды"), + ("Not available", "Қолжетімсіз"), + ("Too frequent", "Тым жиі"), + ("Cancel", "Болдырмау"), + ("Skip", "Өткізіп жіберу"), + ("Close", "Жабу"), + ("Retry", "Қайтадан көру"), + ("OK", "OK"), + ("Password Required", "Құпия сөз Қажет"), + ("Please enter your password", "Құпия сөзіңізді еңгізуді өтінеміз"), + ("Remember password", "Құпия сөзді есте сақтау"), + ("Wrong Password", "Бұрыс Құпия сөз"), + ("Do you want to enter again?", "Қайтадан кіргіңіз келеді ме?"), + ("Connection Error", "Қосылым Қатесі"), + ("Error", "Қате"), + ("Reset by the peer", "Пир қалпына келтірді"), + ("Connecting...", "Қосылуда..."), + ("Connection in progress. Please wait.", "Қосылым барысында. Күтуді өтінеміз"), + ("Please try 1 minute later", "1 минуттан соң қайта көріңіз"), + ("Login Error", "Кіру Қатесі"), + ("Successful", "Сәтті"), + ("Connected, waiting for image...", "Қосылды, сурет күтілуде..."), + ("Name", "Ат"), + ("Type", "Түр"), + ("Modified", "Өзгертілді"), + ("Size", "Өлшем"), + ("Show Hidden Files", "Жасырын Файылдарды Көрсету"), + ("Receive", "Қабылдау"), + ("Send", "Жіберу"), + ("Refresh File", "Файылды жаңарту"), + ("Local", "Лақал"), + ("Remote", "Қашықтағы"), + ("Remote Computer", "Қашықтағы Қампұтыр"), + ("Local Computer", "Лақал Қампұтыр"), + ("Confirm Delete", "Жоюды Растау"), + ("Delete", "Жою"), + ("Properties", "Қасиеттер"), + ("Multi Select", "Көптік таңдау"), + ("Select All", ""), + ("Unselect All", ""), + ("Empty Directory", "Бос Бума"), + ("Not an empty directory", "Бос бума емес"), + ("Are you sure you want to delete this file?", "Бұл файылды жоюға сенімдісіз бе?"), + ("Are you sure you want to delete this empty directory?", "Бұл бос буманы жоюға сенімдісіз бе?"), + ("Are you sure you want to delete the file of this directory?", "Бұл буманың файылын жоюға сенімдісіз бе?"), + ("Do this for all conflicts", "Мұны барлық қанпілектер үшін жасау"), + ("This is irreversible!", "Бұл қайтымсыз!"), + ("Deleting", "Жойылу"), + ("files", "файылдар"), + ("Waiting", "Күту"), + ("Finished", "Аяқталды"), + ("Speed", "Жылдамдық"), + ("Custom Image Quality", "Теңшеулі Сурет Сапасы"), + ("Privacy mode", "Құпиялылық Модасы"), + ("Block user input", "Қолданушы еңгізуін бұғаттау"), + ("Unblock user input", "Қолданушы еңгізуін бұғаттан шығару"), + ("Adjust Window", "Терезені Реттеу"), + ("Original", "Түпнұсқа"), + ("Shrink", "Қысу"), + ("Stretch", "Созу"), + ("Scrollbar", "Scrollbar"), + ("ScrollAuto", "ScrollAuto"), + ("Good image quality", "Жақсы сурет сапасы"), + ("Balanced", "Теңдестірілген"), + ("Optimize reaction time", "Реакция уақытын оңтайландыру"), + ("Custom", ""), + ("Show remote cursor", "Қашықтағы курсорды көрсету"), + ("Show quality monitor", "Сапа мониторын көрсету"), + ("Disable clipboard", "Көшіру-тақтасын өшіру"), + ("Lock after session end", "Сеш аяқталған соң құлыптау"), + ("Insert Ctrl + Alt + Del", "Кірістіру Ctrl + Alt + Del"), + ("Insert Lock", "Кірістіруді Құлыптау"), + ("Refresh", "Жаңарту"), + ("ID does not exist", "ID табылмады"), + ("Failed to connect to rendezvous server", "Rendezvous серберіне қосылу сәтсіз"), + ("Please try later", "Кейінірек қайта көруді өтінеміз"), + ("Remote desktop is offline", "Қашықтағы жұмыс үстелі офлайн күйінде"), + ("Key mismatch", "Кілт сәйкессіздігі"), + ("Timeout", "Үзіліс"), + ("Failed to connect to relay server", "Relay серберіне қосылу сәтсіз"), + ("Failed to connect via rendezvous server", "Rendezvous сербері арқылы қосылу сәтсіз"), + ("Failed to connect via relay server", "Relay сербері арқылы қосылу сәтсіз"), + ("Failed to make direct connection to remote desktop", "Қашықтағы жұмыс үстеліне тікелей қосылым жасау сәтсіз"), + ("Set Password", "Құпия сөзді Орнату"), + ("OS Password", "OS Құпия сөзі"), + ("install_tip", "UAC кесірінен, RustDesk кейбірде қашықтағы жақ ретінде дұрыс жұмыс істей алмайды. UAC'пен қиындықты болдырмау үшін, төмендегі батырманы басып RustDesk'ті жүйеге орнатыңыз."), + ("Click to upgrade", "Жаңғырту үшін басыңыз"), + ("Configure", "Қалыптау"), + ("config_acc", "Сіздің Жұмыс үстеліңізді қашықтан басқару үшін, RustDesk'ке \"Қолжетімділік\" рұқсаттарын беруіңіз керек."), + ("config_screen", "Сіздің Жұмыс үстеліңізге қашықтан қол жеткізу үшін, RustDesk'ке \"Екіренді Жазу\" рұқсаттарын беруіңіз керек."), + ("Installing ...", "Орнатылу..."), + ("Install", "Орнату"), + ("Installation", "Орнатылу"), + ("Installation Path", "Орнатылу Жолы"), + ("Create start menu shortcuts", "Бастау мәзірі белгішесің жасау"), + ("Create desktop icon", "Жұмыс үстелі белгішесің жасау"), + ("agreement_tip", "Орнатуды бастасаңыз, сіз лисензе келісімін қабылдайсыз."), + ("Accept and Install", "Қабылдау және Орнату"), + ("End-user license agreement", "Түпкі қолданушының лисензе келісімі"), + ("Generating ...", "Генератталуда..."), + ("Your installation is lower version.", "Сіздің орнатуыныз төменгі нұсқа."), + ("not_close_tcp_tip", "Тунел қолдану кезінде бұл терезені жаппаңыз"), + ("Listening ...", "Тыңдау ..."), + ("Remote Host", "Қашықтағы Хост"), + ("Remote Port", "Қашықтағы Порт"), + ("Action", "Әрекет"), + ("Add", "Қосу"), + ("Local Port", "Лақал Порт"), + ("Local Address", ""), + ("Change Local Port", ""), + ("setup_server_tip", "Тез қосылым үшін өз серберіңізді орнатуды өтінеміз"), + ("Too short, at least 6 characters.", "Тым қысқа, кемінде 6 таңба."), + ("The confirmation is not identical.", "Растау сәйкес келмейді."), + ("Permissions", "Рұқсаттар"), + ("Accept", "Қабылдау"), + ("Dismiss", "Босату"), + ("Disconnect", "Ажырату"), + ("Enable file copy and paste", "Файылды көшіру мен қоюды рұқсат ету"), + ("Connected", "Қосылды"), + ("Direct and encrypted connection", "Тікелей және кіриптелген қосылым"), + ("Relayed and encrypted connection", "Релайданған және кіриптелген қосылым"), + ("Direct and unencrypted connection", "Тікелей және кіриптелмеген қосылым"), + ("Relayed and unencrypted connection", "Релайданған және кіриптелмеген қосылым"), + ("Enter Remote ID", "Қашықтағы ID еңгізіңіз"), + ("Enter your password", "Құпия сөзіңізді енгізіңіз"), + ("Logging in...", "Кіруде..."), + ("Enable RDP session sharing", "RDP сешті бөлісуді іске қосу"), + ("Auto Login", "Ауты Кіру (\"Сеш аяқталған соң құлыптау\"'ды орнатқанда ғана жарамды)"), + ("Enable direct IP access", "Тікелей IP Қолжетімді іске қосу"), + ("Rename", "Атын өзгерту"), + ("Space", "Орын"), + ("Create desktop shortcut", "Жұмыс үстелі Таңбашасын Жасау"), + ("Change Path", "Жолды өзгерту"), + ("Create Folder", "Бума жасау"), + ("Please enter the folder name", "Буманың атауын еңгізуді өтінеміз"), + ("Fix it", "Түзету"), + ("Warning", "Ескерту"), + ("Login screen using Wayland is not supported", "Wayland қолданған Кіру екіреніне қолдау көрсетілмейді"), + ("Reboot required", "Қайта-қосу қажет"), + ("Unsupported display server", "Қолдаусыз дисплей сербері"), + ("x11 expected", "x11 күтілген"), + ("Port", "Порт"), + ("Settings", "Орнатпалар"), + ("Username", "Қолданушы аты"), + ("Invalid port", "Бұрыс порт"), + ("Closed manually by the peer", "Пир қолымен жабылған"), + ("Enable remote configuration modification", "Қашықтан қалыптарды өзгертуді іске қосу"), + ("Run without install", "Орнатпай-ақ Іске қосу"), + ("Connect via relay", ""), + ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), + ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), + ("Login", "Кіру"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), + ("Logout", "Шығу"), + ("Tags", "Тақтар"), + ("Search ID", "ID Іздеу"), + ("whitelist_sep", "Үтір, нүктелі үтір, бос орын және жаңа жолал арқылы бөлінеді"), + ("Add ID", "ID Қосу"), + ("Add Tag", "Тақ Қосу"), + ("Unselect all tags", "Барлық тақтардың таңдауын алып тастау"), + ("Network error", "Желі қатесі"), + ("Username missed", "Қолданушы аты бос"), + ("Password missed", "Құпия сөз бос"), + ("Wrong credentials", "Бұрыс тіркелгі деректер"), + ("The verification code is incorrect or has expired", ""), + ("Edit Tag", "Тақты Өндеу"), + ("Forget Password", "Құпия сөзді Ұмыту"), + ("Favorites", "Таңдаулылар"), + ("Add to Favorites", "Таңдаулыларға Қосу"), + ("Remove from Favorites", "Таңдаулылардан алып тастау"), + ("Empty", "Бос"), + ("Invalid folder name", "Бұрыс бума атауы"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "Табылды"), + ("install_daemon_tip", "Бут кезінде қосылу үшін жүйелік сербесті орнатуыныз керек."), + ("Remote ID", "Қашықтағы ID"), + ("Paste", "Қою"), + ("Paste here?", "Осында қою керек пе?"), + ("Are you sure to close the connection?", "Қосылымды жабуға сенімдісіз бе?"), + ("Download new version", "Жаңа нұсқаны жүктеу"), + ("Touch mode", "Жанасатын мода"), + ("Mouse mode", "Тінтуірлі мода"), + ("One-Finger Tap", "Бір-Саусақпен Түрту"), + ("Left Mouse", "Солақ Тінтуір"), + ("One-Long Tap", "Бір-Ұзақ Түрту"), + ("Two-Finger Tap", "Екі-Саусақпен Түрту"), + ("Right Mouse", "Оңақ Тінтуір"), + ("One-Finger Move", "Бір-Саусақпен Жылжыту"), + ("Double Tap & Move", "Екі-рет Түртіп Жылжыту"), + ("Mouse Drag", "Тінтуір Тартуы"), + ("Three-Finger vertically", "Үш-Саусақпен тік-бағытты"), + ("Mouse Wheel", "Тінтуір Дөңгелегі"), + ("Two-Finger Move", "Екі-Саусақпен Жылжыту"), + ("Canvas Move", "Кенеп Жылжуы"), + ("Pinch to Zoom", "Зумдау үшін Шымшыңыз"), + ("Canvas Zoom", "Кенеп Зумы"), + ("Reset canvas", "Кенепті қалпына келтіру"), + ("No permission of file transfer", "Файыл алмасуға рұқсат берілмеген"), + ("Note", "Нота"), + ("Connection", "Қосылым"), + ("Share screen", "Екіренді Бөлісу"), + ("Chat", "Чат"), + ("Total", "Барлығы"), + ("items", "зат"), + ("Selected", "Таңдалған"), + ("Screen Capture", "Екіренді Түсіру"), + ("Input Control", "Еңгізуді Басқару/Қадағалау"), + ("Audio Capture", "Аудио Түсіру"), + ("Do you accept?", "Қабылдайсыз ба?"), + ("Open System Setting", "Жүйе Орнатпаларын Ашу"), + ("How to get Android input permission?", "Android еңгізу рұқсатын қалай алуға болады?"), + ("android_input_permission_tip1", "Қашықтағы құрылғы сіздің Android құрылғыңызды тінтуір немесе түрту арқылы басқару үшін, RustDesk'ке \"Қолжетімділік\" сербесін қолдануға рұқсат беруініз керек."), + ("android_input_permission_tip2", "Келесі Жүйе Орнатпалары бетіне барып, [Орнатылған Сербестер]'ді тауып кіріңіз, сосын [RustDesk Еңгізу] сербесін іске қосыңыз."), + ("android_new_connection_tip", "Сіздің ағымдағы құрылғыңызды басқаруды қалайтын жаңа басқару сұранысы түсті."), + ("android_service_will_start_tip", "\"Екіренді Тұсіру\" қосылған кезде сербес аутыматты іске қосылып, басқа құрылғыларға сіздің құрылғыға қосылым сұраныстауға мүмкіндің береді."), + ("android_stop_service_tip", "Сербесті жабу аутыматты түрде барлық орнатылған қосылымдарды жабады."), + ("android_version_audio_tip", "Ағымдағы Android нұсқасы аудионы түсіруді қолдамайды, Android 10 не жоғарғысына жаңғыртуды өтінеміз."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), + ("Account", "Есепкі"), + ("Overwrite", "Үстінен қайта жазу"), + ("This file exists, skip or overwrite this file?", "Бұл файыл бар, өткізіп жіберу әлде үстінен қайта жазу керек пе?"), + ("Quit", "Шығу"), + ("Help", "Көмек"), + ("Failed", "Сәтсіз"), + ("Succeeded", "Сәтті"), + ("Someone turns on privacy mode, exit", "Біреу құпиялылық модасын қосты, шығу"), + ("Unsupported", "Қолдаусыз"), + ("Peer denied", "Пир қабылдамады"), + ("Please install plugins", "Плагиндерді орнатуды өтінеміз"), + ("Peer exit", "Пирдің шығуы"), + ("Failed to turn off", "Сөндіру сәтсіз болды"), + ("Turned off", "Өшірілген"), + ("Language", "Тіл"), + ("Keep RustDesk background service", "Артжақтағы RustDesk сербесін сақтап тұру"), + ("Ignore Battery Optimizations", "Бәтері Оңтайландыруларын Елемеу"), + ("android_open_battery_optimizations_tip", "Егер де бұл ерекшелікті өшіруді қаласаңыз, келесі RustDesk апылқат орнатпалары бетіне барып, [Бәтері]'ні тауып кіріңіз де [Шектеусіз]'ден құсбелгіні алып тастауды өтінеміз"), + ("Start on boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), + ("Connection not allowed", "Қосылу рұқсат етілмеген"), + ("Legacy mode", ""), + ("Map mode", ""), + ("Translate mode", ""), + ("Use permanent password", "Тұрақты құпия сөзді қолдану"), + ("Use both passwords", "Қос құпия сөзді қолдану"), + ("Set permanent password", "Тұрақты құпия сөзді орнату"), + ("Enable remote restart", "Қашықтан қайта-қосуды іске қосу"), + ("Restart remote device", "Қашықтағы құрылғыны қайта-қосу"), + ("Are you sure you want to restart", "Қайта-қосуға сенімдісіз бе?"), + ("Restarting remote device", "Қашықтағы Құрылғыны қайта-қосуда"), + ("remote_restarting_tip", "Қашықтағы құрылғы қайта-қосылуда, бұл хабар терезесін жабып, біраздан соң тұрақты құпия сөзбен қайта қосылуды өтінеміз"), + ("Copied", "Көшірілді"), + ("Exit Fullscreen", "Толық екіреннен Шығу"), + ("Fullscreen", "Толық екірен"), + ("Mobile Actions", "Мабыл Әрекеттері"), + ("Select Monitor", "Мониторды Таңдау"), + ("Control Actions", "Басқару Әрекеттері"), + ("Display Settings", "Дисплей Орнатпалары"), + ("Ratio", "Арақатынас"), + ("Image Quality", "Сурет Сапасы"), + ("Scroll Style", "Scroll Теңшетұрі"), + ("Show Toolbar", ""), + ("Hide Toolbar", ""), + ("Direct Connection", "Тікелей Қосылым"), + ("Relay Connection", "Релай Қосылым"), + ("Secure Connection", "Қауіпсіз Қосылым"), + ("Insecure Connection", "Қатерлі Қосылым"), + ("Scale original", "Scale original"), + ("Scale adaptive", "Scale adaptive"), + ("General", ""), + ("Security", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Light Theme", ""), + ("Dark", ""), + ("Light", ""), + ("Follow System", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable audio", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Pin Toolbar", ""), + ("Unpin Toolbar", ""), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), + ("Enable recording session", ""), + ("Enable LAN discovery", ""), + ("Deny LAN discovery", ""), + ("Write a message", ""), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), + ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), + ("Full Access", ""), + ("Screen Share", ""), + ("ubuntu-21-04-required", "Wayland Ubuntu 21.04 немесе одан жоғары нұсқасын қажет етеді."), + ("wayland-requires-higher-linux-version", "Wayland linux дистрибутивінің жоғарырақ нұсқасын қажет етеді. X11 жұмыс үстелін қолданып көріңіз немесе операциялық жүйеңізді өзгертіңіз."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Бөлісетін экранды таңдаңыз (бірдей жағынан жұмыс жасаңыз)."), + ("Show RustDesk", ""), + ("This PC", ""), + ("or", ""), + ("Elevate", ""), + ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), + ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), + ("Skipped", ""), + ("Add to address book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), + ("Select a peer", ""), + ("Select peers", ""), + ("Plugins", ""), + ("Uninstall", ""), + ("Update", ""), + ("Enable", ""), + ("Disable", ""), + ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), + ("Collapse toolbar", ""), + ("Accept and Elevate", ""), + ("accept_and_elevate_btn_tooltip", ""), + ("clipboard_wait_response_timeout_tip", ""), + ("Incoming connection", ""), + ("Outgoing connection", ""), + ("Exit", ""), + ("Open", ""), + ("logout_tip", ""), + ("Service", ""), + ("Start", ""), + ("Stop", ""), + ("exceed_max_devices", ""), + ("Sync with recent sessions", ""), + ("Sort tags", ""), + ("Open connection in new tab", ""), + ("Move tab to new window", ""), + ("Can not be empty", ""), + ("Already exists", ""), + ("Change Password", ""), + ("Refresh Password", ""), + ("ID", ""), + ("Grid View", ""), + ("List View", ""), + ("Select", ""), + ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), + ("synced_peer_readded_tip", ""), + ("Change Color", ""), + ("Primary Color", ""), + ("HSV Color", ""), + ("Installation Successful!", ""), + ("Installation failed!", ""), + ("Reverse mouse wheel", ""), + ("{} sessions", ""), + ("scam_title", ""), + ("scam_text1", ""), + ("scam_text2", ""), + ("Don't show again", ""), + ("I Agree", ""), + ("Decline", ""), + ("Timeout in minutes", ""), + ("auto_disconnect_option_tip", ""), + ("Connection failed due to inactivity", ""), + ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), + ("pull_group_failed_tip", ""), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), + ("idd_not_support_under_win10_2004_tip", ""), + ("input_source_1_tip", ""), + ("input_source_2_tip", ""), + ("Swap control-command key", ""), + ("swap-left-right-mouse", ""), + ("2FA code", ""), + ("More", ""), + ("enable-2fa-title", ""), + ("enable-2fa-desc", ""), + ("wrong-2fa-code", ""), + ("enter-2fa-title", ""), + ("Email verification code must be 6 characters.", ""), + ("2FA code must be 6 digits.", ""), + ("Multiple Windows sessions found", ""), + ("Please select the session you want to connect to", ""), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("share_warning_tip", ""), + ("Everyone", ""), + ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", ""), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", ""), + ("Floating window", ""), + ("floating_window_tip", ""), + ("Keep screen on", ""), + ("Never", ""), + ("During controlled", ""), + ("During service is on", ""), + ("Capture screen using DirectX", ""), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Қашықтағы жақтағы RustDesk клиентін {} немесе одан жоғары нұсқаға жаңартуды өтінеміз!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Камераны Көру"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", ""), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/lt.rs b/vendor/rustdesk/src/lang/lt.rs new file mode 100644 index 0000000..82422c3 --- /dev/null +++ b/vendor/rustdesk/src/lang/lt.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Būsena"), + ("Your Desktop", "Jūsų darbalaukis"), + ("desk_tip", "Jūsų darbalaukis pasiekiamas naudojant šį ID ir slaptažodį"), + ("Password", "Slaptažodis"), + ("Ready", "Pasiruošęs"), + ("Established", "Įsteigta"), + ("connecting_status", "Prisijungiama prie RustDesk tinklo..."), + ("Enable service", "Įgalinti paslaugą"), + ("Start service", "Pradėti paslaugą"), + ("Service is running", "Paslauga veikia"), + ("Service is not running", "Paslauga neveikia"), + ("not_ready_status", "Neprisijungęs. Patikrinkite ryšį."), + ("Control Remote Desktop", "Nuotolinio darbalaukio valdymas"), + ("Transfer file", "Perkelti failą"), + ("Connect", "Prisijungti"), + ("Recent sessions", "Seansų istorija"), + ("Address book", "Adresų knyga"), + ("Confirmation", "Patvirtinimas"), + ("TCP tunneling", "TCP tuneliavimas"), + ("Remove", "Pašalinti"), + ("Refresh random password", "Atnaujinti atsitiktinį slaptažodį"), + ("Set your own password", "Nustatykite savo slaptažodį"), + ("Enable keyboard/mouse", "Įgalinti klaviatūrą/pelę"), + ("Enable clipboard", "Įgalinti iškarpinę"), + ("Enable file transfer", "Įgalinti failų perdavimą"), + ("Enable TCP tunneling", "Įgalinti TCP tuneliavimą"), + ("IP Whitelisting", "IP baltasis sąrašas"), + ("ID/Relay Server", "ID / perdavimo serveris"), + ("Import server config", "Importuoti serverio konfigūraciją"), + ("Export Server Config", "Eksportuoti serverio konfigūraciją"), + ("Import server configuration successfully", "Sėkmingai importuoti serverio konfigūraciją"), + ("Export server configuration successfully", "Sėkmingai eksportuoti serverio konfigūraciją"), + ("Invalid server configuration", "Netinkama serverio konfigūracija"), + ("Clipboard is empty", "Iškarpinė tuščia"), + ("Stop service", "Sustabdyti paslaugą"), + ("Change ID", "Keisti ID"), + ("Your new ID", "Jūsų naujasis ID"), + ("length %min% to %max%", "ilgis %min% iki %max%"), + ("starts with a letter", "prasideda raide"), + ("allowed characters", "leistini simboliai"), + ("id_change_tip", "Leidžiami tik simboliai a–z, A–Z, 0–9 ir _ (pabraukimas). Pirmoji raidė turi būti a-z, A-Z. Ilgis nuo 6 iki 16."), + ("Website", "Interneto svetainė"), + ("About", "Apie"), + ("Slogan_tip", "Sukurta su siela šiame beprotiškame pasaulyje!"), + ("Privacy Statement", "Privatumo pareiškimas"), + ("Mute", "Nutildyti"), + ("Build Date", "Sukūrimo data"), + ("Version", "Versija"), + ("Home", "Namai"), + ("Audio Input", "Garso įvestis"), + ("Enhancements", "Patobulinimai"), + ("Hardware Codec", "Aparatinės įrangos paspartinimas"), + ("Adaptive bitrate", "Adaptyvusis pralaidumas"), + ("ID Server", "ID serveris"), + ("Relay Server", "Perdavimo serveris"), + ("API Server", "API serveris"), + ("invalid_http", "Turi prasidėti http:// arba https://"), + ("Invalid IP", "Netinkamas IP"), + ("Invalid format", "Neteisingas formatas"), + ("server_not_support", "Serveris dar nepalaikomas"), + ("Not available", "Nepasiekiamas"), + ("Too frequent", "Per dažnai"), + ("Cancel", "Atšaukti"), + ("Skip", "Praleisti"), + ("Close", "Uždaryti"), + ("Retry", "Bandykite dar kartą"), + ("OK", "GERAI"), + ("Password Required", "Reikalingas slaptažodis"), + ("Please enter your password", "Prašome įvesti savo slaptažodį"), + ("Remember password", "Prisiminti slaptažodį"), + ("Wrong Password", "Neteisingas slaptažodis"), + ("Do you want to enter again?", "Ar norite įeiti dar kartą?"), + ("Connection Error", "Ryšio klaida"), + ("Error", "Klaida"), + ("Reset by the peer", "Atmetė nuotolinis kompiuteris"), + ("Connecting...", "Jungiamasi..."), + ("Connection in progress. Please wait.", "Jungiamasi. Palaukite."), + ("Please try 1 minute later", "Prašome pabandyti po 1 minutės"), + ("Login Error", "Prisijungimo klaida"), + ("Successful", "Sėkmingai"), + ("Connected, waiting for image...", "Prisijungta, laukiama vaizdo..."), + ("Name", "Vardas"), + ("Type", "Tipas"), + ("Modified", "Pakeista"), + ("Size", "Dydis"), + ("Show Hidden Files", "Rodyti paslėptus failus"), + ("Receive", "Gauti"), + ("Send", "Siųsti"), + ("Refresh File", "Atnaujinti failą"), + ("Local", "Vietinis"), + ("Remote", "Nuotolinis"), + ("Remote Computer", "Nuotolinis kompiuteris"), + ("Local Computer", "Šis kompiuteris"), + ("Confirm Delete", "Patvirtinti ištrynimą"), + ("Delete", "Ištrinti"), + ("Properties", "Ypatybės"), + ("Multi Select", "Keli pasirinkimas"), + ("Select All", "Pasirinkti viską"), + ("Unselect All", "Atšaukti visų pasirinkimą"), + ("Empty Directory", "Tuščias katalogas"), + ("Not an empty directory", "Ne tuščias katalogas"), + ("Are you sure you want to delete this file?", "Ar tikrai norite ištrinti šį failą?"), + ("Are you sure you want to delete this empty directory?", "Ar tikrai norite ištrinti šį tuščią katalogą?"), + ("Are you sure you want to delete the file of this directory?", "Ar tikrai norite ištrinti šio katalogo failą?"), + ("Do this for all conflicts", "Taikyti visiems konfliktams"), + ("This is irreversible!", "Tai negrįžtama!"), + ("Deleting", "Ištrinama"), + ("files", "failai"), + ("Waiting", "Laukiu"), + ("Finished", "Baigta"), + ("Speed", "Greitis"), + ("Custom Image Quality", "Tinkinta vaizdo kokybė"), + ("Privacy mode", "Privatumo režimas"), + ("Block user input", "Blokuoti naudotojo įvestį"), + ("Unblock user input", "Atblokuoti naudotojo įvestį"), + ("Adjust Window", "Koreguoti langą"), + ("Original", "Originalas"), + ("Shrink", "Susitraukti"), + ("Stretch", "Ištempti"), + ("Scrollbar", "Slinkties juosta"), + ("ScrollAuto", "Automatinis slinkimas"), + ("Good image quality", "Gera vaizdo kokybė"), + ("Balanced", "Subalansuotas"), + ("Optimize reaction time", "Optimizuoti reakcijos laiką"), + ("Custom", "Tinkintas"), + ("Show remote cursor", "Rodyti nuotolinį žymeklį"), + ("Show quality monitor", "Rodyti kokybės monitorių"), + ("Disable clipboard", "Išjungti mainų sritį"), + ("Lock after session end", "Užrakinti pasibaigus seansui"), + ("Insert Ctrl + Alt + Del", "Įdėti Ctrl + Alt + Del"), + ("Insert Lock", "Įterpti užraktą"), + ("Refresh", "Atnaujinti"), + ("ID does not exist", "ID neegzistuoja"), + ("Failed to connect to rendezvous server", "Nepavyko prisijungti prie susitikimo serverio"), + ("Please try later", "Prašome pabandyti vėliau"), + ("Remote desktop is offline", "Nuotolinis darbalaukis neprisijungęs"), + ("Key mismatch", "Raktų neatitikimas"), + ("Timeout", "Laikas baigėsi"), + ("Failed to connect to relay server", "Nepavyko prisijungti prie perdavimo serverio"), + ("Failed to connect via rendezvous server", "Nepavyko prisijungti per susitikimo serverį"), + ("Failed to connect via relay server", "Nepavyko prisijungti per perdavimo serverį"), + ("Failed to make direct connection to remote desktop", "Nepavyko tiesiogiai prisijungti prie nuotolinio darbalaukio"), + ("Set Password", "Nustatyti slaptažodį"), + ("OS Password", "OS slaptažodis"), + ("install_tip", "Kai kuriais atvejais UAC gali priversti RustDesk netinkamai veikti nuotoliniame pagrindiniame kompiuteryje. Norėdami apeiti UAC, spustelėkite toliau esantį mygtuką, kad įdiegtumėte RustDesk į savo kompiuterį."), + ("Click to upgrade", "Spustelėkite, jei norite atnaujinti"), + ("Configure", "Konfigūruoti"), + ("config_acc", "Norėdami nuotoliniu būdu valdyti darbalaukį, turite suteikti RustDesk \"prieigos\" leidimus"), + ("config_screen", "Norėdami nuotoliniu būdu pasiekti darbalaukį, turite suteikti RustDesk leidimus \"ekrano kopija\""), + ("Installing ...", "Diegiama ..."), + ("Install", "Diegti"), + ("Installation", "Įdiegimas"), + ("Installation Path", "Įdiegimo kelias"), + ("Create start menu shortcuts", "Sukurti pradžios meniu sparčiuosius klavišus"), + ("Create desktop icon", "Sukurti darbalaukio piktogramą"), + ("agreement_tip", "Pradėdami diegimą sutinkate su licencijos sutarties sąlygomis"), + ("Accept and Install", "Priimti ir įdiegti"), + ("End-user license agreement", "Galutinio vartotojo licencijos sutartis"), + ("Generating ...", "Generuojamas..."), + ("Your installation is lower version.", "Jūsų įdiegta versija senesnė."), + ("not_close_tcp_tip", "Naudodami tunelį neuždarykite šio lango"), + ("Listening ...", "Laukimas..."), + ("Remote Host", "Nuotolinis pagrindinis kompiuteris"), + ("Remote Port", "Nuotolinis prievadas"), + ("Action", "Veiksmas"), + ("Add", "Papildyti"), + ("Local Port", "Vietinis prievadas"), + ("Local Address", "Vietinis adresas"), + ("Change Local Port", "Keisti vietinį prievadą"), + ("setup_server_tip", "Kad ryšys būtų greitesnis, nustatykite savo serverį"), + ("Too short, at least 6 characters.", "Per trumpas, mažiausiai 6 simboliai."), + ("The confirmation is not identical.", "Patvirtinimas nėra tapatus."), + ("Permissions", "Leidimai"), + ("Accept", "Priimti"), + ("Dismiss", "Atmesti"), + ("Disconnect", "Atjungti"), + ("Enable file copy and paste", "Leisti kopijuoti ir įklijuoti failus"), + ("Connected", "Prisijungta"), + ("Direct and encrypted connection", "Tiesioginis ir šifruotas ryšys"), + ("Relayed and encrypted connection", "Perduotas ir šifruotas ryšys"), + ("Direct and unencrypted connection", "Tiesioginis ir nešifruotas ryšys"), + ("Relayed and unencrypted connection", "Perduotas ir nešifruotas ryšys"), + ("Enter Remote ID", "Įveskite nuotolinio ID"), + ("Enter your password", "Įveskite savo slaptažodį"), + ("Logging in...", "Prisijungiama..."), + ("Enable RDP session sharing", "Įgalinti RDP seansų bendrinimą"), + ("Auto Login", "Automatinis prisijungimas"), + ("Enable direct IP access", "Įgalinti tiesioginę IP prieigą"), + ("Rename", "Pervardyti"), + ("Space", "Erdvė"), + ("Create desktop shortcut", "Sukurti nuorodą darbalaukyje"), + ("Change Path", "Keisti kelią"), + ("Create Folder", "Sukurti aplanką"), + ("Please enter the folder name", "Įveskite aplanko pavadinimą"), + ("Fix it", "Pataisyk tai"), + ("Warning", "Įspėjimas"), + ("Login screen using Wayland is not supported", "Prisijungimo ekranas naudojant Wayland nepalaikomas"), + ("Reboot required", "Reikia paleisti iš naujo"), + ("Unsupported display server", "Nepalaikomas rodymo serveris"), + ("x11 expected", "reikalingas x11"), + ("Port", "Prievadas"), + ("Settings", "Nustatymai"), + ("Username", "Vartotojo vardas"), + ("Invalid port", "Netinkamas prievadas"), + ("Closed manually by the peer", "Partneris atmetė prašymą prisijungti"), + ("Enable remote configuration modification", "Įgalinti nuotolinį konfigūracijos modifikavimą"), + ("Run without install", "Vykdyti be diegimo"), + ("Connect via relay", "Prisijungti per relę"), + ("Always connect via relay", "Visada prisijunkite per relę"), + ("whitelist_tip", "Mane gali pasiekti tik baltajame sąraše esantys IP adresai"), + ("Login", "Prisijungti"), + ("Verify", "Patvirtinti"), + ("Remember me", "Prisimink mane"), + ("Trust this device", "Pasitikėk šiuo įrenginiu"), + ("Verification code", "Patvirtinimo kodas"), + ("verification_tip", "Aptiktas naujas įrenginys ir registruotu el. pašto adresu išsiųstas patvirtinimo kodas. Įveskite jį norėdami tęsti prisijungimą."), + ("Logout", "Atsijungti"), + ("Tags", "Žymos"), + ("Search ID", "Paieškos ID"), + ("whitelist_sep", "Atskirti kableliu, kabliataškiu, tarpu arba nauja eilute"), + ("Add ID", "Pridėti ID"), + ("Add Tag", "Pridėti žymą"), + ("Unselect all tags", "Atšaukti visų žymų pasirinkimą"), + ("Network error", "Tinklo klaida"), + ("Username missed", "Prarastas vartotojo vardas"), + ("Password missed", "Slaptažodis praleistas"), + ("Wrong credentials", "Klaidingi kredencialai"), + ("The verification code is incorrect or has expired", ""), + ("Edit Tag", "Redaguoti žymą"), + ("Forget Password", "Nebeprisiminti slaptažodžio"), + ("Favorites", "Parankiniai"), + ("Add to Favorites", "Įtraukti į parankinius"), + ("Remove from Favorites", "Pašalinti iš parankinių"), + ("Empty", "Tuščia"), + ("Invalid folder name", "Neteisingas aplanko pavadinimas"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "Aptikta tinkle"), + ("install_daemon_tip", "Norėdami, kad RustDesk startuotų automatiškai, turite ją įdiegti"), + ("Remote ID", "Nuotolinis ID"), + ("Paste", "Įklijuoti"), + ("Paste here?", "Įklijuoti čia?"), + ("Are you sure to close the connection?", "Ar tikrai norite atsijungti?"), + ("Download new version", "Atsisiųsti naują versiją"), + ("Touch mode", "Palietimo režimas"), + ("Mouse mode", "Pelės režimas"), + ("One-Finger Tap", "Palietimas vienu pirštu"), + ("Left Mouse", "Kairysis pelės kl."), + ("One-Long Tap", "Vienas palietimas"), + ("Two-Finger Tap", "Palietimas dviem pirštais"), + ("Right Mouse", "Dešinysis pelės kl."), + ("One-Finger Move", "Vieno piršto judesys"), + ("Double Tap & Move", "Dukart palieskite ir perkelkite"), + ("Mouse Drag", "Pelės vilkimas"), + ("Three-Finger vertically", "Trys pirštai vertikaliai"), + ("Mouse Wheel", "Pelės ratukas"), + ("Two-Finger Move", "Dviejų pirštų judesys"), + ("Canvas Move", "Drobės perkėlimas"), + ("Pinch to Zoom", "Suimkite, kad padidintumėte"), + ("Canvas Zoom", "Drobės mastelis"), + ("Reset canvas", "Atstatyti drobę"), + ("No permission of file transfer", "Nėra leidimo perkelti failus"), + ("Note", "Pastaba"), + ("Connection", "Ryšys"), + ("Share screen", "Bendrinti ekraną"), + ("Chat", "Pokalbis"), + ("Total", "Iš viso"), + ("items", "elementai"), + ("Selected", "Pasirinkta"), + ("Screen Capture", "Ekrano nuotrauka"), + ("Input Control", "Įvesties valdymas"), + ("Audio Capture", "Garso fiksavimas"), + ("Do you accept?", "Ar sutinki?"), + ("Open System Setting", "Atviros sistemos nustatymas"), + ("How to get Android input permission?", "Kaip gauti Android įvesties leidimą?"), + ("android_input_permission_tip1", "Kad nuotolinis įrenginys galėtų valdyti Android įrenginį pele arba liesti, turite leisti RustDesk naudoti \"Prieinamumo\" paslaugą."), + ("android_input_permission_tip2", "Eikite į kitą sistemos nustatymų puslapį, suraskite \"Įdiegtos paslaugos\" ir įgalinkite \"RustDesk įvestis\" paslaugą."), + ("android_new_connection_tip", "Gauta nauja užklausa tvarkyti dabartinį įrenginį."), + ("android_service_will_start_tip", "Įgalinus ekrano fiksavimo paslaugą, kiti įrenginiai gali pateikti užklausą prisijungti prie to įrenginio."), + ("android_stop_service_tip", "Uždarius paslaugą automatiškai bus uždaryti visi užmegzti ryšiai."), + ("android_version_audio_tip", "Dabartinė Android versija nepalaiko garso įrašymo, atnaujinkite į Android 10 ar naujesnę versiją."), + ("android_start_service_tip", "Spustelėkite [Paleisti paslaugą] arba įjunkite [Fiksuoti ekraną], kad paleistumėte ekrano bendrinimo paslaugą."), + ("android_permission_may_not_change_tip", "Užmegztų ryšių leidimų keisti negalima, reikia prisijungti iš naujo."), + ("Account", "Paskyra"), + ("Overwrite", "Perrašyti"), + ("This file exists, skip or overwrite this file?", "Šis failas egzistuoja, praleisti arba perrašyti šį failą?"), + ("Quit", "Išeiti"), + ("Help", "Pagalba"), + ("Failed", "Nepavyko"), + ("Succeeded", "Pavyko"), + ("Someone turns on privacy mode, exit", "Kažkas įjungė privatumo režimą, išeiti"), + ("Unsupported", "Nepalaikomas"), + ("Peer denied", "Atšaukė"), + ("Please install plugins", "Įdiekite papildinius"), + ("Peer exit", "Nuotolinis mazgas neveikia"), + ("Failed to turn off", "Nepavyko išjungti"), + ("Turned off", "Išjungti"), + ("Language", "Kalba"), + ("Keep RustDesk background service", "Palikti RustDesk fonine paslauga"), + ("Ignore Battery Optimizations", "Ignoruoti akumuliatoriaus optimizavimą"), + ("android_open_battery_optimizations_tip", "Eikite į kitą nustatymų puslapį"), + ("Start on boot", "Pradėti paleidžiant"), + ("Start the screen sharing service on boot, requires special permissions", "Paleiskite ekrano bendrinimo paslaugą įkrovos metu, reikia specialių leidimų"), + ("Connection not allowed", "Ryšys neleidžiamas"), + ("Legacy mode", "Senasis režimas"), + ("Map mode", "Žemėlapio režimas"), + ("Translate mode", "Vertimo režimas"), + ("Use permanent password", "Naudoti nuolatinį slaptažodį"), + ("Use both passwords", "Naudoti abu slaptažodžius"), + ("Set permanent password", "Nustatyti nuolatinį slaptažodį"), + ("Enable remote restart", "Įgalinti nuotolinį paleidimą iš naujo"), + ("Restart remote device", "Paleisti nuotolinį kompiuterį iš naujo"), + ("Are you sure you want to restart", "Ar tikrai norite paleisti iš naujo?"), + ("Restarting remote device", "Nuotolinio įrenginio paleidimas iš naujo"), + ("remote_restarting_tip", "Nuotolinis įrenginys paleidžiamas iš naujo. Uždarykite šį pranešimą ir po kurio laiko vėl prisijunkite naudodami nuolatinį slaptažodį."), + ("Copied", "Nukopijuota"), + ("Exit Fullscreen", "Išeiti iš pilno ekrano"), + ("Fullscreen", "Per visą ekraną"), + ("Mobile Actions", "Veiksmai mobiliesiems"), + ("Select Monitor", "Pasirinkite monitorių"), + ("Control Actions", "Valdymo veiksmai"), + ("Display Settings", "Ekrano nustatymai"), + ("Ratio", "Santykis"), + ("Image Quality", "Vaizdo kokybė"), + ("Scroll Style", "Slinkimo stilius"), + ("Show Toolbar", ""), + ("Hide Toolbar", ""), + ("Direct Connection", "Tiesioginis ryšys"), + ("Relay Connection", "Tarpinė jungtis"), + ("Secure Connection", "Saugus ryšys"), + ("Insecure Connection", "Nesaugus ryšys"), + ("Scale original", "Pakeisti originalų mastelį"), + ("Scale adaptive", "Pritaikomas mastelis"), + ("General", "Bendra"), + ("Security", "Sauga"), + ("Theme", "Tema"), + ("Dark Theme", "Tamsioji tema"), + ("Light Theme", "Šviesi tema"), + ("Dark", "Tamsi"), + ("Light", "Šviesi"), + ("Follow System", "Kaip sistemos"), + ("Enable hardware codec", "Įgalinti"), + ("Unlock Security Settings", "Atrakinti saugos nustatymus"), + ("Enable audio", "Įgalinti garsą"), + ("Unlock Network Settings", "Atrakinti tinklo nustatymus"), + ("Server", "Serveris"), + ("Direct IP Access", "Tiesioginė IP prieiga"), + ("Proxy", "Tarpinis serveris"), + ("Apply", "Taikyti"), + ("Disconnect all devices?", "Atjungti visus įrenginius?"), + ("Clear", "Išvalyti"), + ("Audio Input Device", "Garso įvestis"), + ("Use IP Whitelisting", "Naudoti patikimą IP sąrašą"), + ("Network", "Tinklas"), + ("Pin Toolbar", ""), + ("Unpin Toolbar", ""), + ("Recording", "Įrašymas"), + ("Directory", "Katalogas"), + ("Automatically record incoming sessions", "Automatiškai įrašyti įeinančius seansus"), + ("Automatically record outgoing sessions", ""), + ("Change", "Keisti"), + ("Start session recording", "Pradėti seanso įrašinėjimą"), + ("Stop session recording", "Sustabdyti seanso įrašinėjimą"), + ("Enable recording session", "Įgalinti seanso įrašinėjimą"), + ("Enable LAN discovery", "Įgalinti LAN aptikimą"), + ("Deny LAN discovery", "Neleisti LAN aptikimo"), + ("Write a message", "Rašyti žinutę"), + ("Prompt", "Užuomina"), + ("Please wait for confirmation of UAC...", "Palaukite UAC patvirtinimo..."), + ("elevated_foreground_window_tip", "Dabartinis nuotolinio darbalaukio langas reikalauja didesnių privilegijų, todėl laikinai neįmanoma naudoti pelės ir klaviatūros. Galite paprašyti nuotolinio vartotojo sumažinti dabartinį langą arba spustelėti aukščio mygtuką ryšio valdymo lange. Norint išvengti šios problemos ateityje, rekomenduojama programinę įrangą įdiegti nuotoliniame įrenginyje."), + ("Disconnected", "Atsijungęs"), + ("Other", "Kita"), + ("Confirm before closing multiple tabs", "Patvirtinti prieš uždarant kelis skirtukus"), + ("Keyboard Settings", "Klaviatūros nustatymai"), + ("Full Access", "Pilna prieiga"), + ("Screen Share", "Ekrano bendrinimas"), + ("ubuntu-21-04-required", "Wayland reikalauja Ubuntu 21.04 arba naujesnės versijos."), + ("wayland-requires-higher-linux-version", "Wayland reikalinga naujesnės Linux Distro versijos. Išbandykite X11 darbalaukį arba pakeiskite OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Peržiūra"), + ("Please Select the screen to be shared(Operate on the peer side).", "Prašome pasirinkti ekraną, kurį norite bendrinti (veikiantį kitoje pusėje)."), + ("Show RustDesk", "Rodyti RustDesk"), + ("This PC", "Šis kompiuteris"), + ("or", "arba"), + ("Elevate", "Pakelti"), + ("Zoom cursor", "Mastelio keitimo žymeklis"), + ("Accept sessions via password", "Priimti seansus naudojant slaptažodį"), + ("Accept sessions via click", "Priimti seansus spustelėjus"), + ("Accept sessions via both", "Priimti seansus abiem variantais"), + ("Please wait for the remote side to accept your session request...", "Palaukite, kol nuotolinė pusė priims jūsų seanso užklausą..."), + ("One-time Password", "Vienkartinis slaptažodis"), + ("Use one-time password", "Naudoti vienkartinį slaptažodį"), + ("One-time password length", "Vienkartinio slaptažodžio ilgis"), + ("Request access to your device", "Prašo leidimo valdyti jūsų įrenginį"), + ("Hide connection management window", "Slėpti ryšio valdymo langą"), + ("hide_cm_tip", "Leisti paslėpti didžiąją ir mažąją raidę, jei priimamos slaptažodžio sesijos arba naudojamas nuolatinis slaptažodis"), + ("wayland_experiment_tip", "Wayland palaikymas yra eksperimentinis, naudokite X11, jei jums reikalingas automatinis prisijungimas."), + ("Right click to select tabs", "Dešiniuoju pelės mygtuku spustelėkite, kad pasirinktumėte skirtukus"), + ("Skipped", "Praleisti"), + ("Add to address book", "Pridėti prie adresų knygos"), + ("Group", "Grupė"), + ("Search", "Paieška"), + ("Closed manually by web console", "Uždaryta rankiniu būdu naudojant žiniatinklio konsolę"), + ("Local keyboard type", "Vietinės klaviatūros tipas"), + ("Select local keyboard type", "Pasirinkite vietinės klaviatūros tipą"), + ("software_render_tip", "Jei turite Nvidia vaizdo plokštę ir nuotolinis langas iškart užsidaro prisijungus, gali padėti „Nouveau“ tvarkyklės įdiegimas ir programinės įrangos atvaizdavimo pasirinkimas. Būtina paleisti iš naujo."), + ("Always use software rendering", "Visada naudoti programinį spartintuvą"), + ("config_input", "Norėdami valdyti nuotolinį darbalaukį naudodami klaviatūrą, turite suteikti RustDesk leidimus \"Įvesties monitoringas\"."), + ("config_microphone", "Norėdami kalbėtis su nuotoline puse, turite suteikti RustDesk leidimą \"Įrašyti garsą\"."), + ("request_elevation_tip", "Taip pat galite prašyti tesių suteikimo, jeigu kas nors yra nuotolinėje pusėje."), + ("Wait", "Laukti"), + ("Elevation Error", "Teisių suteikimo klaida"), + ("Ask the remote user for authentication", "Klauskite nuotolinio vartotojo autentifikavimo"), + ("Choose this if the remote account is administrator", "Pasirinkite tai, jei nuotolinė paskyra yra administratorius"), + ("Transmit the username and password of administrator", "Persiųsti administratoriaus vartotojo vardą ir slaptažodį"), + ("still_click_uac_tip", "Vis tiek reikia, kad nuotolinis vartotojas paleidžiant RustDesk UAC lange paspaustų \"OK\"."), + ("Request Elevation", "Prašyti teisių"), + ("wait_accept_uac_tip", "Palaukite, kol nuotolinis vartotojas patvirtins UAC užklausą."), + ("Elevate successfully", "Teisės suteiktos"), + ("uppercase", "didžiosios raidės"), + ("lowercase", "mažosios raidės"), + ("digit", "skaitmuo"), + ("special character", "specialusis simbolis"), + ("length>=8", "ilgis>=8"), + ("Weak", "Silpnas"), + ("Medium", "Vidutinis"), + ("Strong", "Stiprus"), + ("Switch Sides", "Perjungti puses"), + ("Please confirm if you want to share your desktop?", "Prašome patvirtinti, jeigu norite bendrinti darbalaukį?"), + ("Display", "Ekranas"), + ("Default View Style", "Numatytasis peržiūros stilius"), + ("Default Scroll Style", "Numatytasis slinkties stilius"), + ("Default Image Quality", "Numatytoji vaizdo kokybė"), + ("Default Codec", "Numatytasis kodekas"), + ("Bitrate", "Sparta"), + ("FPS", "FPS"), + ("Auto", "Automatinis"), + ("Other Default Options", "Kitos numatytosios parinktys"), + ("Voice call", "Balso skambutis"), + ("Text chat", "Tekstinis pokalbis"), + ("Stop voice call", "Sustabdyti balso skambutį"), + ("relay_hint_tip", "Tiesioginis ryšys gali būti neįmanomas. Tokiu atveju galite pabandyti prisijungti per perdavimo serverį. \nArba, jei norite iš karto naudoti perdavimo serverį, prie ID galite pridėti priesagą \"/r\" arba nuotolinio pagrindinio kompiuterio nustatymuose įgalinti \"Visada prisijungti per relę\"."), + ("Reconnect", "Prisijungti iš naujo"), + ("Codec", "Kodekas"), + ("Resolution", "Rezoliucija"), + ("No transfers in progress", "Nevyksta jokių perdavimų"), + ("Set one-time password length", "Nustatyti vienkartinio slaptažodžio ilgį"), + ("RDP Settings", "RDP nustatymai"), + ("Sort by", "Rūšiuoti pagal"), + ("New Connection", "Naujas ryšys"), + ("Restore", "Atkurti"), + ("Minimize", "Sumažinti"), + ("Maximize", "Padidinti"), + ("Your Device", "Jūsų įrenginys"), + ("empty_recent_tip", "Nėra paskutinių seansų!\nLaikas suplanuoti naują."), + ("empty_favorite_tip", "Dar neturite parankinių nuotolinių seansų."), + ("empty_lan_tip", "Nuotolinių mazgų nerasta."), + ("empty_address_book_tip", "Adresų knygelėje nėra nuotolinių kompiuterių."), + ("Empty Username", "Tuščias naudotojo vardas"), + ("Empty Password", "Tuščias slaptažodis"), + ("Me", "Aš"), + ("identical_file_tip", "Failas yra identiškas nuotoliniame kompiuteryje esančiam failui."), + ("show_monitors_tip", "Rodyti monitorius įrankių juostoje"), + ("View Mode", "Peržiūros režimas"), + ("login_linux_tip", "Norėdami įjungti X darbalaukio seansą, turite būti prisijungę prie nuotolinės Linux paskyros."), + ("verify_rustdesk_password_tip", "Įveskite kliento RustDesk slaptažodį"), + ("remember_account_tip", "Prisiminti šią paskyrą"), + ("os_account_desk_tip", "Ši paskyra naudojama norint prisijungti prie nuotolinės OS ir įgalinti darbalaukio seansą režimu headless"), + ("OS Account", "OS paskyra"), + ("another_user_login_title_tip", "Kitas vartotojas jau yra prisijungęs"), + ("another_user_login_text_tip", "Atjungti"), + ("xorg_not_found_title_tip", "Xorg nerastas"), + ("xorg_not_found_text_tip", "Prašom įdiegti Xorg"), + ("no_desktop_title_tip", "Nėra pasiekiamų nuotolinių darbalaukių"), + ("no_desktop_text_tip", "Prašom įdiegti GNOME Desktop"), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), + ("Select a peer", ""), + ("Select peers", ""), + ("Plugins", ""), + ("Uninstall", ""), + ("Update", ""), + ("Enable", ""), + ("Disable", ""), + ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), + ("Collapse toolbar", ""), + ("Accept and Elevate", ""), + ("accept_and_elevate_btn_tooltip", ""), + ("clipboard_wait_response_timeout_tip", ""), + ("Incoming connection", ""), + ("Outgoing connection", ""), + ("Exit", ""), + ("Open", ""), + ("logout_tip", ""), + ("Service", ""), + ("Start", ""), + ("Stop", ""), + ("exceed_max_devices", ""), + ("Sync with recent sessions", ""), + ("Sort tags", ""), + ("Open connection in new tab", ""), + ("Move tab to new window", ""), + ("Can not be empty", ""), + ("Already exists", ""), + ("Change Password", ""), + ("Refresh Password", ""), + ("ID", ""), + ("Grid View", ""), + ("List View", ""), + ("Select", ""), + ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), + ("synced_peer_readded_tip", ""), + ("Change Color", ""), + ("Primary Color", ""), + ("HSV Color", ""), + ("Installation Successful!", ""), + ("Installation failed!", ""), + ("Reverse mouse wheel", ""), + ("{} sessions", ""), + ("scam_title", ""), + ("scam_text1", ""), + ("scam_text2", ""), + ("Don't show again", ""), + ("I Agree", ""), + ("Decline", ""), + ("Timeout in minutes", ""), + ("auto_disconnect_option_tip", ""), + ("Connection failed due to inactivity", ""), + ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), + ("pull_group_failed_tip", ""), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), + ("idd_not_support_under_win10_2004_tip", ""), + ("input_source_1_tip", ""), + ("input_source_2_tip", ""), + ("Swap control-command key", ""), + ("swap-left-right-mouse", ""), + ("2FA code", ""), + ("More", ""), + ("enable-2fa-title", ""), + ("enable-2fa-desc", ""), + ("wrong-2fa-code", ""), + ("enter-2fa-title", ""), + ("Email verification code must be 6 characters.", ""), + ("2FA code must be 6 digits.", ""), + ("Multiple Windows sessions found", ""), + ("Please select the session you want to connect to", ""), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("share_warning_tip", ""), + ("Everyone", ""), + ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", ""), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", ""), + ("Floating window", ""), + ("floating_window_tip", ""), + ("Keep screen on", ""), + ("Never", ""), + ("During controlled", ""), + ("During service is on", ""), + ("Capture screen using DirectX", ""), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Prašome atnaujinti nuotolinės pusės RustDesk klientą į {} ar naujesnę versiją!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Peržiūrėti kamerą"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Tęsti su {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/lv.rs b/vendor/rustdesk/src/lang/lv.rs new file mode 100644 index 0000000..906d056 --- /dev/null +++ b/vendor/rustdesk/src/lang/lv.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Statuss"), + ("Your Desktop", "Jūsu darbvirsma"), + ("desk_tip", "Jūsu darbvirsmai var piekļūt ar šo ID un paroli."), + ("Password", "Parole"), + ("Ready", "Gatavs"), + ("Established", "Izveidots"), + ("connecting_status", "Notiek savienojuma izveide ar RustDesk tīklu..."), + ("Enable service", "Iespējot servisu"), + ("Start service", "Sākt servisu"), + ("Service is running", "Pakalpojums darbojas"), + ("Service is not running", "Pakalpojums nedarbojas"), + ("not_ready_status", "Nav gatavs. Lūdzu, pārbaudiet savienojumu"), + ("Control Remote Desktop", "Vadīt attālo darbvirsmu"), + ("Transfer file", "Pārsūtīt failu"), + ("Connect", "Savienoties"), + ("Recent sessions", "Pēdējās sesijas"), + ("Address book", "Adrešu grāmata"), + ("Confirmation", "Apstiprinājums"), + ("TCP tunneling", "TCP tunelēšana"), + ("Remove", "Noņemt"), + ("Refresh random password", "Atsvaidzināt nejaušo paroli"), + ("Set your own password", "Iestatiet savu paroli"), + ("Enable keyboard/mouse", "Iespējot tastatūru/peli"), + ("Enable clipboard", "Iespējot starpliktuvi"), + ("Enable file transfer", "Iespējot failu pārsūtīšanu"), + ("Enable TCP tunneling", "Iespējot TCP tunelēšanu"), + ("IP Whitelisting", "IP baltais saraksts"), + ("ID/Relay Server", "ID/releja serveris"), + ("Import server config", "Importēt servera konfigurāciju"), + ("Export Server Config", "Eksportēt servera konfigurāciju"), + ("Import server configuration successfully", "Servera konfigurācija veiksmīgi importēta"), + ("Export server configuration successfully", "Servera konfigurācija veiksmīgi eksportēta"), + ("Invalid server configuration", "Nederīga servera konfigurācija"), + ("Clipboard is empty", "Starpliktuve ir tukša"), + ("Stop service", "Apturēt servisu"), + ("Change ID", "Mainīt ID"), + ("Your new ID", "Jūsu jaunais ID"), + ("length %min% to %max%", "garums %min% līdz %max%"), + ("starts with a letter", "sākas ar burtu"), + ("allowed characters", "atļautās rakstzīmes"), + ("id_change_tip", "Atļautas tikai rakstzīmes a-z, A-Z, 0-9, - (domuzīme) un _ (pasvītrojums). Pirmajam burtam ir jābūt a-z, A-Z. Garums no 6 līdz 16."), + ("Website", "Tīmekļa vietne"), + ("About", "Par"), + ("Slogan_tip", "Radīts ar sirdi šajā haotiskajā pasaulē!"), + ("Privacy Statement", "Paziņojums par konfidencialitāti"), + ("Mute", "Izslēgt skaņu"), + ("Build Date", "Būvēšanas datums"), + ("Version", "Versija"), + ("Home", "Sākums"), + ("Audio Input", "Audio ievade"), + ("Enhancements", "Uzlabojumi"), + ("Hardware Codec", "Aparatūras kodeks"), + ("Adaptive bitrate", "Adaptīvais bitu pārraides ātrums"), + ("ID Server", "ID serveris"), + ("Relay Server", "Releja serveris"), + ("API Server", "API serveris"), + ("invalid_http", "jāsākas ar http:// vai https://"), + ("Invalid IP", "Nederīga IP"), + ("Invalid format", "Nederīgs formāts"), + ("server_not_support", "Serveris vēl neatbalsta"), + ("Not available", "Nav pieejams"), + ("Too frequent", "Pārāk bieži"), + ("Cancel", "Atcelt"), + ("Skip", "Izlaist"), + ("Close", "Aizvērt"), + ("Retry", "Mēģināt vēlreiz"), + ("OK", "Labi"), + ("Password Required", "Nepieciešama parole"), + ("Please enter your password", "Lūdzu, ievadiet paroli"), + ("Remember password", "Atcerēties paroli"), + ("Wrong Password", "Nepareiza parole"), + ("Do you want to enter again?", "Vai vēlaties ievadīt vēlreiz?"), + ("Connection Error", "Savienojuma kļūda"), + ("Error", "Kļūda"), + ("Reset by the peer", "Atiestatīts ar sesiju"), + ("Connecting...", "Notiek savienojuma izveide..."), + ("Connection in progress. Please wait.", "Notiek savienošanās. Lūdzu, uzgaidiet."), + ("Please try 1 minute later", "Lūdzu, mēģiniet 1 minūti vēlāk"), + ("Login Error", "Pieteikšanās kļūda"), + ("Successful", "Veiksmīgi"), + ("Connected, waiting for image...", "Savienots, gaida attēlu..."), + ("Name", "Nosaukums"), + ("Type", "Tips"), + ("Modified", "Modificēšanas dat."), + ("Size", "Lielums"), + ("Show Hidden Files", "Rādīt slēptos failus"), + ("Receive", "Saņemt"), + ("Send", "Sūtīt"), + ("Refresh File", "Atsvaidzināt failu"), + ("Local", "Vietējais"), + ("Remote", "Attālais"), + ("Remote Computer", "Attālais dators"), + ("Local Computer", "Vietējais dators"), + ("Confirm Delete", "Apstiprināt dzēšanu"), + ("Delete", "Dzēst"), + ("Properties", "Rekvizīti"), + ("Multi Select", "Vairākatlase"), + ("Select All", "Atlasīt visu"), + ("Unselect All", "Noņemt atlasi visiem"), + ("Empty Directory", "Tukša direktorija"), + ("Not an empty directory", "Nav tukša direktorija"), + ("Are you sure you want to delete this file?", "Vai tiešām vēlaties dzēst šo failu?"), + ("Are you sure you want to delete this empty directory?", "Vai tiešām vēlaties dzēst šo tukšo direktoriju?"), + ("Are you sure you want to delete the file of this directory?", "Vai tiešām vēlaties dzēst šī direktorija failu?"), + ("Do this for all conflicts", "Pielietot visiem konfliktiem"), + ("This is irreversible!", "Tas ir neatgriezeniski!"), + ("Deleting", "Dzēšana"), + ("files", "faili"), + ("Waiting", "Gaida"), + ("Finished", "Pabeigts"), + ("Speed", "Ātrums"), + ("Custom Image Quality", "Pielāgota attēla kvalitāte"), + ("Privacy mode", "Privātuma režīms"), + ("Block user input", "Bloķēt lietotāja ievadi"), + ("Unblock user input", "Atbloķēt lietotāja ievadi"), + ("Adjust Window", "Pielāgot logu"), + ("Original", "Oriģināls"), + ("Shrink", "Saraut"), + ("Stretch", "Izstiept"), + ("Scrollbar", "Ritjosla"), + ("ScrollAuto", "Ritināt automātiski"), + ("Good image quality", "Laba attēla kvalitāte"), + ("Balanced", "Sabalansēts"), + ("Optimize reaction time", "Optimizēt reakcijas laiku"), + ("Custom", "Pielāgots"), + ("Show remote cursor", "Rādīt attālo kursoru"), + ("Show quality monitor", "Rādīt kvalitātes monitoru"), + ("Disable clipboard", "Atspējot starpliktuvi"), + ("Lock after session end", "Bloķēt pēc sesijas beigām"), + ("Insert Ctrl + Alt + Del", "Ievietot Ctrl + Alt + Del"), + ("Insert Lock", "Ievietot Bloķēt"), + ("Refresh", "Atsvaidzināt"), + ("ID does not exist", "ID neeksistē"), + ("Failed to connect to rendezvous server", "Neizdevās izveidot savienojumu ar starpposma serveri"), + ("Please try later", "Lūdzu, mēģiniet vēlāk"), + ("Remote desktop is offline", "Attālā darbvirsma ir bezsaistē"), + ("Key mismatch", "Atslēgu neatbilstība"), + ("Timeout", "Noildze"), + ("Failed to connect to relay server", "Neizdevās izveidot savienojumu ar releja serveri"), + ("Failed to connect via rendezvous server", "Neizdevās izveidot savienojumu, izmantojot starpposma serveri"), + ("Failed to connect via relay server", "Neizdevās izveidot savienojumu, izmantojot releja serveri"), + ("Failed to make direct connection to remote desktop", "Neizdevās izveidot tiešu savienojumu ar attālo darbvirsmu"), + ("Set Password", "Uzstādīt paroli"), + ("OS Password", "OS parole"), + ("install_tip", "UAC dēļ RustDesk dažos gadījumos nevar pareizi darboties kā attālā puse. Lai izvairītos no UAC, lūdzu, noklikšķiniet uz tālāk esošās pogas, lai instalētu RustDesk sistēmā."), + ("Click to upgrade", "Jaunināt"), + ("Configure", "Konfigurēt"), + ("config_acc", "Lai attālināti vadītu savu darbvirsmu, jums ir jāpiešķir RustDesk \"Pieejamība\" atļaujas."), + ("config_screen", "Lai attālināti piekļūtu darbvirsmai, jums ir jāpiešķir RustDesk \"Ekrāna tveršana\" atļaujas."), + ("Installing ...", "Notiek instalēšana..."), + ("Install", "Uzstādīt"), + ("Installation", "Instalēšana"), + ("Installation Path", "Instalācijas ceļš"), + ("Create start menu shortcuts", "Izveidot sākuma izvēlnes īsceļus"), + ("Create desktop icon", "Izveidot darbvirsmas ikonu"), + ("agreement_tip", "Sākot instalēšanu, jūs piekrītat licences līgumam."), + ("Accept and Install", "Pieņemt un instalēt"), + ("End-user license agreement", "Gala lietotāja licences līgums"), + ("Generating ...", "Ģenerēšana..."), + ("Your installation is lower version.", "Jūsu instalācijai ir zemāka versija."), + ("not_close_tcp_tip", "Neaizveriet šo logu, kamēr izmantojat tuneli"), + ("Listening ...", "Klausās..."), + ("Remote Host", "Attālais resursdators"), + ("Remote Port", "Attālais ports"), + ("Action", "Darbība"), + ("Add", "Pievienot"), + ("Local Port", "Vietējais ports"), + ("Local Address", "Vietējā adrese"), + ("Change Local Port", "Mainīt vietējo portu"), + ("setup_server_tip", "Lai iegūtu ātrāku savienojumu, lūdzu, iestatiet savu serveri"), + ("Too short, at least 6 characters.", "Pārāk īss, vismaz 6 rakstzīmes."), + ("The confirmation is not identical.", "Apstiprinājums nav identisks."), + ("Permissions", "Atļaujas"), + ("Accept", "Pieņemt"), + ("Dismiss", "Noraidīt"), + ("Disconnect", "Atvienot"), + ("Enable file copy and paste", "Atļaut failu kopēšanu un ielīmēšanu"), + ("Connected", "Savienots"), + ("Direct and encrypted connection", "Tiešs un šifrēts savienojums"), + ("Relayed and encrypted connection", "Pārslēgts un šifrēts savienojums"), + ("Direct and unencrypted connection", "Tiešs un nešifrēts savienojums"), + ("Relayed and unencrypted connection", "Pārslēgts un nešifrēts savienojums"), + ("Enter Remote ID", "Ievadiet attālo ID"), + ("Enter your password", "Ievadiet savu paroli"), + ("Logging in...", "Ielogoties..."), + ("Enable RDP session sharing", "Iespējot RDP sesiju koplietošanu"), + ("Auto Login", "Automātiskā pieteikšanās (derīga tikai tad, ja esat iestatījis \"Bloķēt pēc sesijas beigām\")"), + ("Enable direct IP access", "Iespējot tiešo IP piekļuvi"), + ("Rename", "Pārdēvēt"), + ("Space", "Vieta"), + ("Create desktop shortcut", "Izveidot saīsni uz darbvirsmas"), + ("Change Path", "Mainīt ceļu"), + ("Create Folder", "Izveidot mapi"), + ("Please enter the folder name", "Lūdzu, ievadiet mapes nosaukumu"), + ("Fix it", "Salabot to"), + ("Warning", "Brīdinājums"), + ("Login screen using Wayland is not supported", "Pieteikšanās ekrāns, izmantojot Wayland netiek atbalstīts"), + ("Reboot required", "Nepieciešama restartēšana"), + ("Unsupported display server", "Neatbalstīts displeja serveris"), + ("x11 expected", "x11 paredzams"), + ("Port", "Ports"), + ("Settings", "Iestatījumi"), + ("Username", "Lietotājvārds"), + ("Invalid port", "Nederīgs ports"), + ("Closed manually by the peer", "Sesija aizvērta manuāli"), + ("Enable remote configuration modification", "Iespējot attālās konfigurācijas modifikāciju"), + ("Run without install", "Palaist bez instalēšanas"), + ("Connect via relay", "Savienot, izmantojot releju"), + ("Always connect via relay", "Vienmēr izveidot savienojumu, izmantojot releju"), + ("whitelist_tip", "Man var piekļūt tikai baltajā sarakstā iekļautās IP adreses"), + ("Login", "Pieslēgties"), + ("Verify", "Pārbaudīt"), + ("Remember me", "Atcerēties mani"), + ("Trust this device", "Uzticēties šai ierīcei"), + ("Verification code", "Verifikācijas kods"), + ("verification_tip", "Verifikācijas kods ir nosūtīts uz reģistrēto e-pasta adresi, ievadiet verifikācijas kodu, lai turpinātu pieslēgšanos."), + ("Logout", "Izlogoties"), + ("Tags", "Tagi"), + ("Search ID", "Meklēt ID"), + ("whitelist_sep", "Atdalīts ar komatu, semikolu, atstarpēm vai jaunu rindiņu"), + ("Add ID", "Pievienot ID"), + ("Add Tag", "Pievienot tagu"), + ("Unselect all tags", "Noņemt visu tagu atlasi"), + ("Network error", "Tīkla kļūda"), + ("Username missed", "Lietotājvārds ir izlaists"), + ("Password missed", "Parole nav ievadīta"), + ("Wrong credentials", "Nepareizs lietotājvārds vai parole"), + ("The verification code is incorrect or has expired", "Verifikācijas kods ir nepareizs vai tam ir beidzies derīguma termiņš"), + ("Edit Tag", "Rediģēt tagu"), + ("Forget Password", "Neatcerēties paroli"), + ("Favorites", "Izlase"), + ("Add to Favorites", "Pievienot pie izlases"), + ("Remove from Favorites", "Noņemt no izlases"), + ("Empty", "Tukšs"), + ("Invalid folder name", "Nederīgs mapes nosaukums"), + ("Socks5 Proxy", "Socks5 starpniekserveris"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) starpniekserveris"), + ("Discovered", "Atklāts"), + ("install_daemon_tip", "Lai palaistu pie startēšanas, ir jāinstalē sistēmas serviss."), + ("Remote ID", "Attālais ID"), + ("Paste", "Ielīmēt"), + ("Paste here?", "Ielīmēt šeit?"), + ("Are you sure to close the connection?", "Vai tiešām vēlaties aizvērt savienojumu?"), + ("Download new version", "Lejupielādēt jauno versiju"), + ("Touch mode", "Skārienrežīms"), + ("Mouse mode", "Peles režīms"), + ("One-Finger Tap", "Pieskāriens ar vienu pirkstu"), + ("Left Mouse", "Kreisā pele"), + ("One-Long Tap", "Viens garš pieskāriens"), + ("Two-Finger Tap", "Pieskāriens ar diviem pirkstiem"), + ("Right Mouse", "Labā pele"), + ("One-Finger Move", "Viena pirksta pārvietošana"), + ("Double Tap & Move", "Dubultskāriens & pārvietošana"), + ("Mouse Drag", "Peles vilkšana"), + ("Three-Finger vertically", "Trīs pirksti vertikāli"), + ("Mouse Wheel", "Peles ritenis"), + ("Two-Finger Move", "Divu pirkstu pārvietošana"), + ("Canvas Move", "Audekla pārvietošana"), + ("Pinch to Zoom", "Saspiediet, lai tuvinātu"), + ("Canvas Zoom", "Audekla tālummaiņa"), + ("Reset canvas", "Atiestatīt audeklu"), + ("No permission of file transfer", "Nav atļaujas failu pārsūtīšanai"), + ("Note", "Piezīme"), + ("Connection", "Savienojums"), + ("Share screen", "Koplietot ekrānu"), + ("Chat", "Tērzēšana"), + ("Total", "Kopā"), + ("items", "vienumi"), + ("Selected", "Atlasīts"), + ("Screen Capture", "Ekrāna tveršana"), + ("Input Control", "Ievades vadība"), + ("Audio Capture", "Audio tveršana"), + ("Do you accept?", "Vai Jūs pieņemat?"), + ("Open System Setting", "Atvērt sistēmas iestatījumus"), + ("How to get Android input permission?", "Kā iegūt Android ievades atļauju?"), + ("android_input_permission_tip1", "Lai attālā ierīce varētu vadīt jūsu Android ierīci, izmantojot peli vai pieskārienu, jums ir jāatļauj RustDesk izmantot servisu \"Pieejamība\"."), + ("android_input_permission_tip2", "Lūdzu, dodieties uz nākamo sistēmas iestatījumu lapu, atrodiet un atveriet [Instalētie pakalpojumi], ieslēdziet servisu [RustDesk Input]."), + ("android_new_connection_tip", "Ir saņemts jauns vadības pieprasījums, kas vēlas kontrolēt jūsu pašreizējo ierīci."), + ("android_service_will_start_tip", "Ieslēdzot \"Ekrāna tveršana\", serviss tiks automātiski startēts, ļaujot citām ierīcēm pieprasīt savienojumu ar jūsu ierīci."), + ("android_stop_service_tip", "Pakalpojuma aizvēršana automātiski aizvērs visus izveidotos savienojumus."), + ("android_version_audio_tip", "Pašreizējā Android versija neatbalsta audio uztveršanu, lūdzu, jauniniet uz Android 10 vai jaunāku versiju."), + ("android_start_service_tip", "Pieskarieties [Sākt servisu] vai iespējojiet [Ekrāna tveršana] atļauju, lai sāktu ekrāna koplietošanas servisu."), + ("android_permission_may_not_change_tip", "Izveidoto savienojumu atļaujas nevar mainīt uzreiz, kamēr nav atkārtoti izveidots savienojums."), + ("Account", "Konts"), + ("Overwrite", "Pārrakstīt"), + ("This file exists, skip or overwrite this file?", "Šis fails pastāv, izlaist vai pārrakstīt šo failu?"), + ("Quit", "Iziet"), + ("Help", "Palīdzība"), + ("Failed", "Neizdevās"), + ("Succeeded", "Izdevās"), + ("Someone turns on privacy mode, exit", "Kāds ieslēdza privātuma režīmu, iziet"), + ("Unsupported", "Neatbalstīts"), + ("Peer denied", "Sesija noraidīta"), + ("Please install plugins", "Lūdzu, instalējiet spraudņus"), + ("Peer exit", "Iziet no attālās ierīces"), + ("Failed to turn off", "Neizdevās izslēgt"), + ("Turned off", "Izslēgts"), + ("Language", "Valoda"), + ("Keep RustDesk background service", "Saglabāt RustDesk fona servisu"), + ("Ignore Battery Optimizations", "Ignorēt akumulatora optimizāciju"), + ("android_open_battery_optimizations_tip", "Ja vēlaties atspējot šo funkciju, lūdzu, dodieties uz nākamo RustDesk lietojumprogrammas iestatījumu lapu, atrodiet un atveriet [Akumulators], noņemiet atzīmi no [Neierobežots]"), + ("Start on boot", "Palaist pie ieslēgšanas"), + ("Start the screen sharing service on boot, requires special permissions", "Startējiet ekrāna koplietošanas pakalpojumu ieslēgšanas laikā, nepieciešamas īpašas atļaujas"), + ("Connection not allowed", "Savienojums nav atļauts"), + ("Legacy mode", "Novecojis režīms"), + ("Map mode", "Kartēšanas režīms"), + ("Translate mode", "Tulkošanas režīms"), + ("Use permanent password", "Izmantot pastāvīgo paroli"), + ("Use both passwords", "Izmantot abas paroles"), + ("Set permanent password", "Iestatīt pastāvīgo paroli"), + ("Enable remote restart", "Iespējot attālo restartēšanu"), + ("Restart remote device", "Restartēt attālo ierīci"), + ("Are you sure you want to restart", "Vai tiešām vēlaties restartēt"), + ("Restarting remote device", "Attālās ierīces restartēšana"), + ("remote_restarting_tip", "Attālā ierīce tiek restartēta, lūdzu, aizveriet šo ziņojuma lodziņu un pēc kāda laika izveidojiet savienojumu ar pastāvīgo paroli"), + ("Copied", "Kopēts"), + ("Exit Fullscreen", "Iziet no pilnekrāna"), + ("Fullscreen", "Pilnekrāna režīms"), + ("Mobile Actions", "Mobilās darbības"), + ("Select Monitor", "Atlasīt monitoru"), + ("Control Actions", "Kontroles darbības"), + ("Display Settings", "Displeja iestatījumi"), + ("Ratio", "Attiecība"), + ("Image Quality", "Attēla kvalitāte"), + ("Scroll Style", "Ritināšanas stils"), + ("Show Toolbar", "Rādīt rīkjoslu"), + ("Hide Toolbar", "Slēpt rīkjoslu"), + ("Direct Connection", "Tiešais savienojums"), + ("Relay Connection", "Releja savienojums"), + ("Secure Connection", "Drošs savienojums"), + ("Insecure Connection", "Nedrošs savienojums"), + ("Scale original", "Mērogs oriģināls"), + ("Scale adaptive", "Mērogs adaptīvs"), + ("General", "Vispārīgi"), + ("Security", "Drošība"), + ("Theme", "Motīvs"), + ("Dark Theme", "Tumšais motīvs"), + ("Light Theme", "Gaišais motīvs"), + ("Dark", "Tumšs"), + ("Light", "Gaišs"), + ("Follow System", "Sekot sistēmai"), + ("Enable hardware codec", "Iespējot aparatūras kodeku"), + ("Unlock Security Settings", "Atbloķēt drošības iestatījumus"), + ("Enable audio", "Iespējot audio"), + ("Unlock Network Settings", "Atbloķēt tīkla iestatījumus"), + ("Server", "Serveris"), + ("Direct IP Access", "Tiešā IP piekļuve"), + ("Proxy", "Starpniekserveris"), + ("Apply", "Lietot"), + ("Disconnect all devices?", "Atvienot visas ierīces?"), + ("Clear", "Notīrīt"), + ("Audio Input Device", "Audio ievades ierīce"), + ("Use IP Whitelisting", "Izmantot balto IP sarakstu"), + ("Network", "Tīkls"), + ("Pin Toolbar", "Piespraust rīkjoslu"), + ("Unpin Toolbar", "Atspraust rīkjoslu"), + ("Recording", "Ierakstīšana"), + ("Directory", "Direktorija"), + ("Automatically record incoming sessions", "Automātiski ierakstīt ienākošās sesijas"), + ("Automatically record outgoing sessions", "Automātiski ierakstīt izejošās sesijas"), + ("Change", "Mainīt"), + ("Start session recording", "Sākt sesijas ierakstīšanu"), + ("Stop session recording", "Apturēt sesijas ierakstīšanu"), + ("Enable recording session", "Iespējot sesijas ierakstīšanu"), + ("Enable LAN discovery", "Iespējot LAN atklāšanu"), + ("Deny LAN discovery", "Liegt LAN atklāšanu"), + ("Write a message", "Rakstīt ziņojumu"), + ("Prompt", "Uzvedne"), + ("Please wait for confirmation of UAC...", "Lūdzu, uzgaidiet UAC apstiprinājumu..."), + ("elevated_foreground_window_tip", "Pašreizējā attālās darbvirsmas loga darbībai ir nepieciešamas lielākas tiesības, tāpēc tas īslaicīgi nevar izmantot peli un tastatūru. Varat pieprasīt attālajam lietotājam samazināt pašreizējo logu vai noklikšķināt uz pacēluma pogas savienojuma pārvaldības logā. Lai izvairītos no šīs problēmas, ieteicams instalēt programmatūru attālajā ierīcē."), + ("Disconnected", "Atvienots"), + ("Other", "Cits"), + ("Confirm before closing multiple tabs", "Apstiprināt pirms vairāku cilņu aizvēršanas"), + ("Keyboard Settings", "Tastatūras iestatījumi"), + ("Full Access", "Pilna piekļuve"), + ("Screen Share", "Ekrāna kopīgošana"), + ("ubuntu-21-04-required", "Wayland nepieciešama Ubuntu 21.04 vai jaunāka versija."), + ("wayland-requires-higher-linux-version", "Wayland nepieciešama augstāka Linux distro versija. Lūdzu, izmēģiniet X11 desktop vai mainiet savu OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Skatīt"), + ("Please Select the screen to be shared(Operate on the peer side).", "Lūdzu, atlasiet kopīgojamo ekrānu (darbojieties sesijas pusē)."), + ("Show RustDesk", "Rādīt RustDesk"), + ("This PC", "Šis dators"), + ("or", "vai"), + ("Elevate", "Pacelt"), + ("Zoom cursor", "Tālummaiņas kursors"), + ("Accept sessions via password", "Pieņemt sesijas, izmantojot paroli"), + ("Accept sessions via click", "Pieņemt sesijas, noklikšķinot"), + ("Accept sessions via both", "Pieņemt sesijas, izmantojot abus"), + ("Please wait for the remote side to accept your session request...", "Lūdzu, uzgaidiet, kamēr attālā puse pieņems jūsu sesijas pieprasījumu..."), + ("One-time Password", "Vienreizējā parole"), + ("Use one-time password", "Izmantot vienreizējo paroli"), + ("One-time password length", "Vienreizējās paroles garums"), + ("Request access to your device", "Pieprasīt piekļuvi savai ierīcei"), + ("Hide connection management window", "Slēpt savienojuma pārvaldības logu"), + ("hide_cm_tip", "Atļaut paslēpšanu tikai tad, ja akceptējat sesijas, izmantojot paroli un pastāvīgo paroli"), + ("wayland_experiment_tip", "Wayland atbalsts ir eksperimentālā stadijā. Ja nepieciešama neuzraudzīta piekļuve, lūdzu, izmantojiet X11."), + ("Right click to select tabs", "Ar peles labo pogu noklikšķiniet, lai atlasītu cilnes"), + ("Skipped", "Izlaists"), + ("Add to address book", "Pievienot adrešu grāmatai"), + ("Group", "Grupa"), + ("Search", "Meklēt"), + ("Closed manually by web console", "Manuāli aizvērta tīmekļa konsole"), + ("Local keyboard type", "Vietējais tastatūras veids"), + ("Select local keyboard type", "Izvēlēties vietējās tastatūras veidu"), + ("software_render_tip", "Ja izmantojat Nvidia grafikas karti operētājsistēmā Linux un attālais logs tiek aizvērts uzreiz pēc savienojuma izveides, var palīdzēt pārslēgšanās uz atvērtā koda Nouveau draiveri un izvēle izmantot programmatūras renderēšanu. Nepieciešama programmatūras restartēšana."), + ("Always use software rendering", "Vienmēr izmantot programmatūras renderēšanu"), + ("config_input", "Lai vadītu attālo darbvirsmu ar tastatūru, jums ir jāpiešķir RustDesk \"Ievades uzraudzība\" atļaujas."), + ("config_microphone", "Lai runātu attālināti, jums ir jāpiešķir RustDesk \"Ierakstīt audio\" atļaujas."), + ("request_elevation_tip", "Paaugstinājumu var pieprasīt arī tad, ja attālajā pusē ir kāds cilvēks."), + ("Wait", "Pagaidiet"), + ("Elevation Error", "Paaugstinājuma kļūda"), + ("Ask the remote user for authentication", "Lūdziet attālajam lietotājam autentifikāciju"), + ("Choose this if the remote account is administrator", "Izvēlieties šo, ja attālais konts ir administrators"), + ("Transmit the username and password of administrator", "Pārsūtīt administratora lietotājvārdu un paroli"), + ("still_click_uac_tip", "Joprojām attālajam lietotājam ir jānoklikšķina uz Labi UAC logā, kurā darbojas RustDesk."), + ("Request Elevation", "Pieprasīt paaugstinājumu"), + ("wait_accept_uac_tip", "Lūdzu, uzgaidiet, līdz attālais lietotājs pieņems UAC dialoglodziņu."), + ("Elevate successfully", "Paaugstināšana veiksmīga"), + ("uppercase", "lielie burti"), + ("lowercase", "mazie burti"), + ("digit", "cipars"), + ("special character", "speciāla rakstzīme"), + ("length>=8", "garums>=8"), + ("Weak", "Vāji"), + ("Medium", "Vidējs"), + ("Strong", "Spēcīgs"), + ("Switch Sides", "Pārslēgt puses"), + ("Please confirm if you want to share your desktop?", "Lūdzu, apstipriniet, vai vēlaties koplietot savu darbvirsmu?"), + ("Display", "Displejs"), + ("Default View Style", "Noklusējuma skata stils"), + ("Default Scroll Style", "Noklusējuma ritināšanas stils"), + ("Default Image Quality", "Noklusējuma attēla kvalitāte"), + ("Default Codec", "Noklusējuma kodeks"), + ("Bitrate", "Bitu pārraides ātrums"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Citas noklusējuma opcijas"), + ("Voice call", "Balss zvans"), + ("Text chat", "Teksta tērzēšana"), + ("Stop voice call", "Apturēt balss zvanu"), + ("relay_hint_tip", "Iespējams, nav iespējams izveidot savienojumu tieši; varat mēģināt izveidot savienojumu, izmantojot releju. Turklāt, ja vēlaties izmantot releju pirmajā mēģinājumā, ID varat pievienot sufiksu \"/r\". vai pēdējo sesiju kartē atlasiet opciju \"Vienmēr izveidot savienojumu, izmantojot releju\", ja tāda pastāv."), + ("Reconnect", "Atkārtoti savienot"), + ("Codec", "Kodeks"), + ("Resolution", "Izšķirtspēja"), + ("No transfers in progress", "Notiek pārsūtīšana"), + ("Set one-time password length", "Iestatīt vienreizējās paroles garumu"), + ("RDP Settings", "RDP iestatījumi"), + ("Sort by", "Kārtot pēc"), + ("New Connection", "Jauns savienojums"), + ("Restore", "Atjaun. uz leju"), + ("Minimize", "Minimizēt"), + ("Maximize", "Maksimizēt"), + ("Your Device", "Jūsu ierīce"), + ("empty_recent_tip", "Hmm, pēdējo sesiju nav!\nLaiks plānot jaunu."), + ("empty_favorite_tip", "Vēl nav iecienītākās sesijas?\nAtradīsim kādu, ar ko sazināties, un pievienosim to jūsu izlasei!"), + ("empty_lan_tip", "Ak nē! Šķiet, ka mēs vēl neesam atklājuši nevienu sesiju."), + ("empty_address_book_tip", "Ak vai, izskatās, ka jūsu adrešu grāmatā šobrīd nav neviena sesija."), + ("Empty Username", "Tukšs lietotājvārds"), + ("Empty Password", "Tukša parole"), + ("Me", "Es"), + ("identical_file_tip", "Šis fails ir identisks sesijas failam."), + ("show_monitors_tip", "Rādīt monitorus rīkjoslā"), + ("View Mode", "Skatīšanas režīms"), + ("login_linux_tip", "Jums ir jāpiesakās attālajā Linux kontā, lai iespējotu X darbvirsmas sesiju"), + ("verify_rustdesk_password_tip", "Pārbaudīt RustDesk paroli"), + ("remember_account_tip", "Atcerēties šo kontu"), + ("os_account_desk_tip", "Šis konts tiek izmantots, lai pieteiktos attālajā operētājsistēmā un iespējotu darbvirsmas sesiju fonā"), + ("OS Account", "OS konts"), + ("another_user_login_title_tip", "Cits lietotājs jau ir pieteicies"), + ("another_user_login_text_tip", "Atvienot"), + ("xorg_not_found_title_tip", "Xorg nav atrasts"), + ("xorg_not_found_text_tip", "Lūdzu, instalējiet Xorg"), + ("no_desktop_title_tip", "Nav pieejama darbvirsma"), + ("no_desktop_text_tip", "Lūdzu, instalējiet GNOME darbvirsmu"), + ("No need to elevate", "Nav nepieciešams paaugstināt"), + ("System Sound", "Sistēmas skaņa"), + ("Default", "Noklusējums"), + ("New RDP", "Jauns RDP"), + ("Fingerprint", "Pirkstu nospiedums"), + ("Copy Fingerprint", "Kopēt pirkstu nospiedumu"), + ("no fingerprints", "nav pirkstu nospiedumu"), + ("Select a peer", "Atlasīt līdzīgu"), + ("Select peers", "Atlasīt līdzīgus"), + ("Plugins", "Spraudņi"), + ("Uninstall", "Atinstalēt"), + ("Update", "Atjaunināt"), + ("Enable", "Iespējot"), + ("Disable", "Atspējot"), + ("Options", "Opcijas"), + ("resolution_original_tip", "Sākotnējā izšķirtspēja"), + ("resolution_fit_local_tip", "Atbilst vietējai izšķirtspējai"), + ("resolution_custom_tip", "Pielāgota izšķirtspēja"), + ("Collapse toolbar", "Sakļaut rīkjoslu"), + ("Accept and Elevate", "Pieņemt un paaugstināt"), + ("accept_and_elevate_btn_tooltip", "Pieņemt savienojumu un paaugstināt UAC atļaujas."), + ("clipboard_wait_response_timeout_tip", "Noildze gaidot atbildi uz kopiju."), + ("Incoming connection", "Ienākošais savienojums"), + ("Outgoing connection", "Izejošais savienojums"), + ("Exit", "Iziet"), + ("Open", "Atvērt"), + ("logout_tip", "Vai tiešām vēlaties iziet?"), + ("Service", "Serviss"), + ("Start", "Sākt"), + ("Stop", "Apturēt"), + ("exceed_max_devices", "Esat sasniedzis maksimālo pārvaldāmo ierīču skaitu."), + ("Sync with recent sessions", "Sinhronizācija ar pēdējām sesijām"), + ("Sort tags", "Kārtot tagus"), + ("Open connection in new tab", "Atvērt savienojumu jaunā cilnē"), + ("Move tab to new window", "Pārvietot cilni uz jaunu logu"), + ("Can not be empty", "Nevar būt tukšs"), + ("Already exists", "Jau eksistē"), + ("Change Password", "Mainīt paroli"), + ("Refresh Password", "Atsvaidzināt paroli"), + ("ID", "ID"), + ("Grid View", "Režģa skats"), + ("List View", "Saraksta skats"), + ("Select", "Atlasīt"), + ("Toggle Tags", "Pārslēgt tagus"), + ("pull_ab_failed_tip", "Neizdevās atsvaidzināt adrešu grāmatu"), + ("push_ab_failed_tip", "Neizdevās sinhronizēt adrešu grāmatu ar serveri"), + ("synced_peer_readded_tip", "Ierīces, kas bija pēdējās sesijās, tiks sinhronizētas atpakaļ ar adrešu grāmatu."), + ("Change Color", "Mainīt krāsu"), + ("Primary Color", "Primārā krāsa"), + ("HSV Color", "HSV krāsa"), + ("Installation Successful!", "Instalēšana veiksmīga!"), + ("Installation failed!", "Instalēšana neizdevās!"), + ("Reverse mouse wheel", "Reversīvs peles ritenis"), + ("{} sessions", "{} sesijas"), + ("scam_title", "Tevi var APKRĀPT!"), + ("scam_text1", "Ja sarunājaties ar kādu, kuru nepazīstat un kurš ir lūdzis izmantot RustDesk, lai sāktu pakalpojumu, neturpiniet un nekavējoties nolieciet klausuli."), + ("scam_text2", "Viņi, visticamāk, ir krāpnieki, kas mēģina nozagt tavu naudu vai citu privātu informāciju."), + ("Don't show again", "Vairs nerādīt"), + ("I Agree", "Es piekrītu"), + ("Decline", "Noraidīt"), + ("Timeout in minutes", "Noildze minūtēs"), + ("auto_disconnect_option_tip", "Automātiski aizvērt ienākošās sesijas lietotāja neaktivitātes gadījumā"), + ("Connection failed due to inactivity", "Automātiski atvienots neaktivitātes dēļ"), + ("Check for software update on startup", "Startējot pārbaudīt, vai nav programmatūras atjauninājumu"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Lūdzu, jauniniet RustDesk Server Pro uz versiju {} vai jaunāku!"), + ("pull_group_failed_tip", "Neizdevās atsvaidzināt grupu"), + ("Filter by intersection", "Filtrēt pēc krustpunkta"), + ("Remove wallpaper during incoming sessions", "Noņemt fona tapeti ienākošo sesiju laikā"), + ("Test", "Pārbaudīt"), + ("display_is_plugged_out_msg", "Displejs ir atvienots, pārslēdzieties uz pirmo displeju."), + ("No displays", "Nav displeju"), + ("Open in new window", "Atvērt jaunā logā"), + ("Show displays as individual windows", "Rādīt displejus kā atsevišķus logus"), + ("Use all my displays for the remote session", "Izmantot visus manus displejus attālajai sesijai"), + ("selinux_tip", "Jūsu ierīcē ir iespējots SELinux, kas var neļaut RustDesk pareizi darboties kā kontrolētajai pusei."), + ("Change view", "Mainīt skatu"), + ("Big tiles", "Lielas flīzes"), + ("Small tiles", "Mazas flīzes"), + ("List", "Saraksts"), + ("Virtual display", "Virtuālais displejs"), + ("Plug out all", "Atvienot visu"), + ("True color (4:4:4)", "Īstā krāsa (4:4:4)"), + ("Enable blocking user input", "Iespējot lietotāja ievades bloķēšanu"), + ("id_input_tip", "Varat ievadīt ID, tiešo IP vai domēnu ar portu (:).\nJa vēlaties piekļūt ierīcei citā serverī, lūdzu, pievienojiet servera adresi (@?key=), piemēram,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nJa vēlaties piekļūt ierīcei publiskajā serverī, lūdzu, ievadiet \"@public\", publiskajam serverim atslēga nav nepieciešama"), + ("privacy_mode_impl_mag_tip", "1. režīms"), + ("privacy_mode_impl_virtual_display_tip", "2. režīms"), + ("Enter privacy mode", "Ieiet privātuma režīmā"), + ("Exit privacy mode", "Iziet no privātuma režīma"), + ("idd_not_support_under_win10_2004_tip", "Netiešā displeja draiveris netiek atbalstīts. Nepieciešama operētājsistēma Windows 10, versija 2004 vai jaunāka."), + ("input_source_1_tip", "Ievades avots 1"), + ("input_source_2_tip", "Ievades avots 2"), + ("Swap control-command key", "Apmainīt vadības un komandas taustiņu"), + ("swap-left-right-mouse", "Apmainīt kreiso un labo peles pogu"), + ("2FA code", "2FA kods"), + ("More", "Vairāk"), + ("enable-2fa-title", "Iespējot divu faktoru autentifikāciju"), + ("enable-2fa-desc", "Lūdzu, iestatiet savu autentifikatoru tūlīt. Tālrunī vai darbvirsmā varat izmantot autentifikācijas lietotni, piemēram, Authy, Microsoft vai Google Authenticator.\n\nIzmantojot lietotni, skenējiet QR kodu un ievadiet lietotnē parādīto kodu, lai iespējotu divu faktoru autentifikāciju."), + ("wrong-2fa-code", "Nevar pārbaudīt kodu. Pārbaudiet, vai kods un vietējā laika iestatījumi ir pareizi"), + ("enter-2fa-title", "Divu faktoru autentifikācija"), + ("Email verification code must be 6 characters.", "E-pasta verifikācijas kodam jābūt ar 6 rakstzīmēm."), + ("2FA code must be 6 digits.", "2FA kodam ir jābūt ar 6 cipariem."), + ("Multiple Windows sessions found", "Atrastas vairākas Windows sesijas"), + ("Please select the session you want to connect to", "Lūdzu, atlasiet sesiju, ar kuru vēlaties izveidot savienojumu"), + ("powered_by_me", "Darbojas ar RustDesk"), + ("outgoing_only_desk_tip", "Šis ir pielāgots izdevums.\nVarat izveidot savienojumu ar citām ierīcēm, taču citas ierīces nevar izveidot savienojumu ar jūsu ierīci."), + ("preset_password_warning", "Šim pielāgotajam izdevumam ir iepriekš iestatīta parole. Ikviens, kurš zina šo paroli, var pilnībā kontrolēt jūsu ierīci. Ja jūs to negaidījāt, nekavējoties atinstalējiet programmatūru."), + ("Security Alert", "Drošības brīdinājums"), + ("My address book", "Mana adrešu grāmata"), + ("Personal", "Personīga"), + ("Owner", "Īpašnieks"), + ("Set shared password", "Iestatīt koplietojamo paroli"), + ("Exist in", "Pastāv iekš"), + ("Read-only", "Tikai lasīt"), + ("Read/Write", "Lasīt/Rakstīt"), + ("Full Control", "Pilnīga kontrole"), + ("share_warning_tip", "Iepriekš minētie lauki ir koplietoti un redzami citiem."), + ("Everyone", "Visi"), + ("ab_web_console_tip", "Vairāk par tīmekļa konsoli"), + ("allow-only-conn-window-open-tip", "Atļaut savienojumu tikai tad, ja ir atvērts RustDesk logs"), + ("no_need_privacy_mode_no_physical_displays_tip", "Nav fizisku displeju, nav jāizmanto privātuma režīms."), + ("Follow remote cursor", "Sekot attālajam kursoram"), + ("Follow remote window focus", "Sekot attālā loga fokusam"), + ("default_proxy_tip", "Noklusējuma protokols un ports ir Socks5 un 1080"), + ("no_audio_input_device_tip", "Nav atrasta neviena audio ievades ierīce."), + ("Incoming", "Ienākošie"), + ("Outgoing", "Izejošie"), + ("Clear Wayland screen selection", "Notīrīt Wayland ekrāna atlasi"), + ("clear_Wayland_screen_selection_tip", "Pēc ekrāna atlases notīrīšanas varat atkārtoti atlasīt ekrānu, ko kopīgot."), + ("confirm_clear_Wayland_screen_selection_tip", "Vai tiešām notīrīt Wayland ekrāna atlasi?"), + ("android_new_voice_call_tip", "Tika saņemts jauns balss zvana pieprasījums. Ja piekrītat, audio pārslēgsies uz balss saziņu."), + ("texture_render_tip", "Izmantojiet tekstūras renderēšanu, lai attēli būtu vienmērīgāki. Varat mēģināt atspējot šo opciju, ja rodas renderēšanas problēmas."), + ("Use texture rendering", "Izmantot tekstūras renderēšanu"), + ("Floating window", "Peldošs logs"), + ("floating_window_tip", "Tas palīdz uzturēt RustDesk fona servisu"), + ("Keep screen on", "Turēt ekrānu ieslēgtu"), + ("Never", "Nekad"), + ("During controlled", "Lietošanas laikā"), + ("During service is on", "Kamēr pakalpojums ir ieslēgts"), + ("Capture screen using DirectX", "Tvert ekrānu, izmantojot DirectX"), + ("Back", "Atpakaļ"), + ("Apps", "Lietotnes"), + ("Volume up", "Skaļāk"), + ("Volume down", "Klusāk"), + ("Power", "Ieslēgšana"), + ("Telegram bot", "Telegram robots"), + ("enable-bot-tip", "Ja iespējojat šo funkciju, varat saņemt 2FA kodu no sava robota. Tas var darboties arī kā savienojuma paziņojums."), + ("enable-bot-desc", "1. Atveriet tērzēšanu ar @BotFather.\n2. Nosūtiet komandu \"/newbot\". Pēc šīs darbības pabeigšanas jūs saņemsit pilnvaru.\n3. Sāciet tērzēšanu ar jaunizveidoto robotprogrammatūru. Lai to aktivizētu, nosūtiet ziņojumu, kas sākas ar slīpsvītru (\"/\"), piemēram, \"/hello\".\n"), + ("cancel-2fa-confirm-tip", "Vai tiešām vēlaties atcelt 2FA?"), + ("cancel-bot-confirm-tip", "Vai tiešām vēlaties atcelt Telegram robotu?"), + ("About RustDesk", "Par RustDesk"), + ("Send clipboard keystrokes", "Nosūtīt starpliktuves taustiņsitienus"), + ("network_error_tip", "Lūdzu, pārbaudiet tīkla savienojumu un pēc tam noklikšķiniet uz Mēģināt vēlreiz."), + ("Unlock with PIN", "Atbloķēt ar PIN"), + ("Requires at least {} characters", "Nepieciešamas vismaz {} rakstzīmes"), + ("Wrong PIN", "Nepareizs PIN"), + ("Set PIN", "Iestatīt PIN"), + ("Enable trusted devices", "Iespējot uzticamas ierīces"), + ("Manage trusted devices", "Pārvaldīt uzticamas ierīces"), + ("Platform", "Platforma"), + ("Days remaining", "Atlikušas dienas"), + ("enable-trusted-devices-tip", "Izlaist 2FA verifikāciju uzticamās ierīcēs"), + ("Parent directory", "Vecākdirektorijs"), + ("Resume", "Atsākt"), + ("Invalid file name", "Nederīgs faila nosaukums"), + ("one-way-file-transfer-tip", "Kontrolējamajā pusē ir iespējota vienvirziena failu pārsūtīšana."), + ("Authentication Required", "Nepieciešama autentifikācija"), + ("Authenticate", "Autentificēt"), + ("web_id_input_tip", "Varat ievadīt ID tajā pašā serverī, tīmekļa klientā tiešā IP piekļuve netiek atbalstīta.\nJa vēlaties piekļūt ierīcei citā serverī, lūdzu, pievienojiet servera adresi (@?key=), piemēram,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nJa vēlaties piekļūt ierīcei publiskajā serverī, lūdzu, ievadiet \"@public\", publiskajam serverim atslēga nav nepieciešama."), + ("Download", "Lejupielādēt"), + ("Upload folder", "Augšupielādēt mapi"), + ("Upload files", "Augšupielādēt failus"), + ("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"), + ("Update client clipboard", "Atjaunināt klienta starpliktuvi"), + ("Untagged", "Neatzīmēts"), + ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), + ("Accessible devices", "Pieejamas ierīces"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Lūdzu, jauniniet attālās puses RustDesk klientu uz versiju {} vai jaunāku!"), + ("d3d_render_tip", "Ja ir iespējota D3D renderēšana, dažās ierīcēs tālvadības pults ekrāns var būt melns."), + ("Use D3D rendering", "Izmantot D3D renderēšanu"), + ("Printer", "Printeris"), + ("printer-os-requirement-tip", "Printera izejošajai funkcijai nepieciešama operētājsistēma Windows 10 vai jaunāka versija."), + ("printer-requires-installed-{}-client-tip", "Lai izmantotu attālo drukāšanu, šajā ierīcē ir jāinstalē {}."), + ("printer-{}-not-installed-tip", "Printeris {} nav instalēts."), + ("printer-{}-ready-tip", "Printeris {} ir instalēts un gatavs lietošanai."), + ("Install {} Printer", "Instalēt {} printeri"), + ("Outgoing Print Jobs", "Izejošie drukas darbi"), + ("Incoming Print Jobs", "Ienākošie drukas darbi"), + ("Incoming Print Job", "Ienākošais drukas darbs"), + ("use-the-default-printer-tip", "Izmantot noklusējuma printeri"), + ("use-the-selected-printer-tip", "Izmantot atlasīto printeri"), + ("auto-print-tip", "Drukājiet automātiski, izmantojot atlasīto printeri."), + ("print-incoming-job-confirm-tip", "Jūs saņēmāt drukas darbu no attālās ierīces. Vai vēlaties to izpildīt savā pusē?"), + ("remote-printing-disallowed-tile-tip", "Attālā drukāšana ir aizliegta"), + ("remote-printing-disallowed-text-tip", "Kontrolētās puses atļauju iestatījumi liedz attālo drukāšanu."), + ("save-settings-tip", "Saglabāt iestatījumus"), + ("dont-show-again-tip", "Nerādīt šo vēlreiz"), + ("Take screenshot", "Uzņemt ekrānuzņēmumu"), + ("Taking screenshot", "Ekrānuzņēmuma uzņemšana"), + ("screenshot-merged-screen-not-supported-tip", "Vairāku displeju ekrānuzņēmumu apvienošana pašlaik netiek atbalstīta. Lūdzu, pārslēdzieties uz vienu displeju un mēģiniet vēlreiz."), + ("screenshot-action-tip", "Lūdzu, atlasiet, kā turpināt darbu ar ekrānuzņēmumu."), + ("Save as", "Saglabāt kā"), + ("Copy to clipboard", "Kopēt starpliktuvē"), + ("Enable remote printer", "Iespējot attālo printeri"), + ("Downloading {}", "Notiek {} lejupielāde"), + ("{} Update", "{} atjauninājums"), + ("{}-to-update-tip", "{} tagad tiks aizvērts un tiks instalēta jaunā versija."), + ("download-new-version-failed-tip", "Lejupielāde neizdevās. Varat mēģināt vēlreiz vai noklikšķināt uz pogas \"Lejupielādēt\", lai lejupielādētu no laidiena lapas un manuāli jauninātu."), + ("Auto update", "Automātiskā atjaunināšana"), + ("update-failed-check-msi-tip", "Instalēšanas metodes pārbaude neizdevās. Lūdzu, noklikšķiniet uz pogas \"Lejupielādēt\", lai lejupielādētu no laidiena lapas un manuāli jauninātu."), + ("websocket_tip", "Izmantojot WebSocket, tiek atbalstīti tikai releja savienojumi."), + ("Use WebSocket", "Lietot WebSocket"), + ("Trackpad speed", "Skārienpaliktņa ātrums"), + ("Default trackpad speed", "Noklusējuma skārienpaliktņa ātrums"), + ("Numeric one-time password", "Vienreiz lietojama ciparu parole"), + ("Enable IPv6 P2P connection", "Iespējot IPv6 P2P savienojumu"), + ("Enable UDP hole punching", "Iespējot UDP caurumu veidošanu"), + ("View camera", "Skatīt kameru"), + ("Enable camera", "Iespējot kameru"), + ("No cameras", "Nav kameru"), + ("view_camera_unsupported_tip", "Attālā ierīce neatbalsta kameras skatīšanos."), + ("Terminal", "Terminālis"), + ("Enable terminal", "Iespējot termināli"), + ("New tab", "Jauna cilne"), + ("Keep terminal sessions on disconnect", "Atvienojoties saglabāt termināļa sesijas"), + ("Terminal (Run as administrator)", "Terminālis (Palaist kā administratoram)"), + ("terminal-admin-login-tip", "Lūdzu, ievadiet kontrolētās puses administratora lietotājvārdu un paroli."), + ("Failed to get user token.", "Neizdevās iegūt lietotāja atļauju."), + ("Incorrect username or password.", "Nepareizs lietotājvārds vai parole."), + ("The user is not an administrator.", "Lietotājs nav administrators."), + ("Failed to check if the user is an administrator.", "Neizdevās pārbaudīt, vai lietotājs ir administrators."), + ("Supported only in the installed version.", "Atbalstīts tikai instalētajā versijā."), + ("elevation_username_tip", "Ievadiet lietotājvārdu vai domēnu\\lietotājvārdu"), + ("Preparing for installation ...", "Gatavošanās instalēšanai..."), + ("Show my cursor", "Rādīt manu kursoru"), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Turpināt ar {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/ml.rs b/vendor/rustdesk/src/lang/ml.rs new file mode 100644 index 0000000..099f1d3 --- /dev/null +++ b/vendor/rustdesk/src/lang/ml.rs @@ -0,0 +1,746 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "നില"), + ("Your Desktop", "നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ്"), + ("desk_tip", "ഈ ഐഡിയും പാസ്‌വേഡും ഉപയോഗിച്ച് നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ് ആക്‌സസ് ചെയ്യാം."), + ("Password", "പാസ്‌വേഡ്"), + ("Ready", "തയ്യാറാണ്"), + ("Established", "ബന്ധം സ്ഥാപിച്ചു"), + ("connecting_status", "നെറ്റ്‌വർക്കുമായി ബന്ധിപ്പിക്കുന്നു..."), + ("Enable service", "സർവീസ് പ്രവർത്തനക്ഷമമാക്കുക"), + ("Start service", "സർവീസ് തുടങ്ങുക"), + ("Service is running", "സർവീസ് പ്രവർത്തിക്കുന്നു"), + ("Service is not running", "സർവീസ് പ്രവർത്തിക്കുന്നില്ല"), + ("not_ready_status", "തയ്യാറായിട്ടില്ല. ദയവായി നിങ്ങളുടെ കണക്ഷൻ പരിശോധിക്കുക"), + ("Control Remote Desktop", "റിമോട്ട് ഡെസ്ക്ടോപ്പ് നിയന്ത്രിക്കുക"), + ("Transfer file", "ഫയൽ കൈമാറുക"), + ("Connect", "കണക്ട് ചെയ്യുക"), + ("Recent sessions", "സമീപകാല സെഷനുകൾ"), + ("Address book", "അഡ്രസ് ബുക്ക്"), + ("Confirmation", "സ്ഥിരീകരണം"), + ("TCP tunneling", "TCP ടണലിംഗ്"), + ("Remove", "നീക്കം ചെയ്യുക"), + ("Refresh random password", "പുതിയ പാസ്‌വേഡ് ജനറേറ്റ് ചെയ്യുക"), + ("Set your own password", "സ്വന്തം പാസ്‌വേഡ് സെറ്റ് ചെയ്യുക"), + ("Enable keyboard/mouse", "കീബോർഡ്/മൗസ് അനുവദിക്കുക"), + ("Enable clipboard", "ക്ലിപ്പ്ബോർഡ് അനുവദിക്കുക"), + ("Enable file transfer", "ഫയൽ കൈമാറ്റം അനുവദിക്കുക"), + ("Enable TCP tunneling", "TCP ടണലിംഗ് അനുവദിക്കുക"), + ("IP Whitelisting", "IP വൈറ്റ്‌ലിസ്റ്റിംഗ്"), + ("ID/Relay Server", "ID/റിലേ സെർവർ"), + ("Import server config", "സെർവർ കോൺഫിഗറേഷൻ ഇമ്പോർട്ട് ചെയ്യുക"), + ("Export Server Config", "സെർവർ കോൺഫിഗറേഷൻ എക്‌സ്‌പോർട്ട് ചെയ്യുക"), + ("Import server configuration successfully", "സെർവർ കോൺഫിഗറേഷൻ വിജയകരമായി ഇമ്പോർട്ട് ചെയ്തു"), + ("Export server configuration successfully", "സെർവർ കോൺഫിഗറേഷൻ വിജയകരമായി എക്‌സ്‌പോർട്ട് ചെയ്തു"), + ("Invalid server configuration", "അസാധുവായ സെർവർ കോൺഫിഗറേഷൻ"), + ("Clipboard is empty", "ക്ലിപ്പ്ബോർഡ് ശൂന്യമാണ്"), + ("Stop service", "സർവീസ് നിർത്തുക"), + ("Change ID", "ഐഡി മാറ്റുക"), + ("Your new ID", "നിങ്ങളുടെ പുതിയ ഐഡി"), + ("length %min% to %max%", "നീളം %min% മുതൽ %max% വരെ"), + ("starts with a letter", "അക്ഷരത്തിൽ തുടങ്ങണം"), + ("allowed characters", "അനുവദനീയമായ അക്ഷരങ്ങൾ"), + ("id_change_tip", "ഐഡി മാറ്റിയാൽ നിലവിലുള്ള കണക്ഷൻ വിച്ഛേദിക്കപ്പെടും."), + ("Website", "വെബ്സൈറ്റ്"), + ("About", "വിവരങ്ങൾ"), + ("Slogan_tip", "മികച്ച അനുഭവത്തിനായി നിർമ്മിച്ച റിമോട്ട് ഡെസ്ക്ടോപ്പ് സോഫ്റ്റ്‌വെയർ"), + ("Privacy Statement", "സ്വകാര്യതാ പ്രസ്താവന"), + ("Mute", "നിശബ്ദമാക്കുക"), + ("Build Date", "നിർമ്മാണ തീയതി"), + ("Version", "പതിപ്പ്"), + ("Home", "ഹോം"), + ("Audio Input", "ഓഡിയോ ഇൻപുട്ട്"), + ("Enhancements", "മെച്ചപ്പെടുത്തലുകൾ"), + ("Hardware Codec", "ഹാർഡ്‌വെയർ കോഡെക്"), + ("Adaptive bitrate", "അഡാപ്റ്റീവ് ബിറ്റ്റേറ്റ്"), + ("ID Server", "ID സെർവർ"), + ("Relay Server", "റിലേ സെർവർ"), + ("API Server", "API സെർവർ"), + ("invalid_http", "അസാധുവായ HTTP ലിങ്ക്"), + ("Invalid IP", "അസാധുവായ IP"), + ("Invalid format", "അസാധുവായ ഫോർമാറ്റ്"), + ("server_not_support", "സെർവർ പിന്തുണയ്ക്കുന്നില്ല"), + ("Not available", "ലഭ്യമല്ല"), + ("Too frequent", "അമിതമായ തവണകൾ"), + ("Cancel", "റദ്ദാക്കുക"), + ("Skip", "ഒഴിവാക്കുക"), + ("Close", "അടയ്ക്കുക"), + ("Retry", "വീണ്ടും ശ്രമിക്കുക"), + ("OK", "ശരി"), + ("Password Required", "പാസ്‌വേഡ് ആവശ്യമാണ്"), + ("Please enter your password", "ദയവായി നിങ്ങളുടെ പാസ്‌വേഡ് നൽകുക"), + ("Remember password", "പാസ്‌വേഡ് ഓർമ്മിക്കുക"), + ("Wrong Password", "തെറ്റായ പാസ്‌വേഡ്"), + ("Do you want to enter again?", "നിങ്ങൾക്ക് വീണ്ടും ശ്രമിക്കണോ?"), + ("Connection Error", "കണക്ഷൻ പിശക്"), + ("Error", "പിശക്"), + ("Reset by the peer", "മറുഭാഗത്തുനിന്ന് റീസെറ്റ് ചെയ്തു"), + ("Connecting...", "ബന്ധിപ്പിക്കുന്നു..."), + ("Connection in progress. Please wait.", "കണക്ഷൻ നടക്കുന്നു. ദയവായി കാത്തിരിക്കുക."), + ("Please try 1 minute later", "ദയവായി ഒരു മിനിറ്റിന് ശേഷം ശ്രമിക്കുക"), + ("Login Error", "ലോഗിൻ പിശക്"), + ("Successful", "വിജയിച്ചു"), + ("Connected, waiting for image...", "ബന്ധിപ്പിച്ചു, ചിത്രത്തിനായി കാത്തിരിക്കുന്നു..."), + ("Name", "പേര്"), + ("Type", "തരം"), + ("Modified", "മാറ്റം വരുത്തിയത്"), + ("Size", "വലിപ്പം"), + ("Show Hidden Files", "മറഞ്ഞിരിക്കുന്ന ഫയലുകൾ കാണിക്കുക"), + ("Receive", "സ്വീകരിക്കുക"), + ("Send", "അയക്കുക"), + ("Refresh File", "ഫയൽ പുതുക്കുക"), + ("Local", "ലോക്കൽ"), + ("Remote", "റിമോട്ട്"), + ("Remote Computer", "റിമോട്ട് കമ്പ്യൂട്ടർ"), + ("Local Computer", "ലോക്കൽ കമ്പ്യൂട്ടർ"), + ("Confirm Delete", "ഡിലീറ്റ് ചെയ്യുന്നത് സ്ഥിരീകരിക്കുക"), + ("Delete", "ഡിലീറ്റ് ചെയ്യുക"), + ("Properties", "പ്രോപ്പർട്ടീസ്"), + ("Multi Select", "ഒന്നിലധികം തിരഞ്ഞെടുക്കുക"), + ("Select All", "എല്ലാം തിരഞ്ഞെടുക്കുക"), + ("Unselect All", "തിരഞ്ഞെടുത്തവ ഒഴിവാക്കുക"), + ("Empty Directory", "ശൂന്യമായ ഡയറക്ടറി"), + ("Not an empty directory", "ഡയറക്ടറി ശൂന്യമല്ല"), + ("Are you sure you want to delete this file?", "ഈ ഫയൽ ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Are you sure you want to delete this empty directory?", "ഈ ശൂന്യമായ ഡയറക്ടറി ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Are you sure you want to delete the file of this directory?", "ഈ ഡയറക്ടറിയിലെ ഫയലുകൾ ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Do this for all conflicts", "എല്ലാ വൈരുദ്ധ്യങ്ങൾക്കും ഇതുതന്നെ ചെയ്യുക"), + ("This is irreversible!", "ഇത് പഴയപടിയാക്കാൻ കഴിയില്ല!"), + ("Deleting", "ഡിലീറ്റ് ചെയ്യുന്നു"), + ("files", "ഫയലുകൾ"), + ("Waiting", "കാത്തിരിക്കുന്നു"), + ("Finished", "പൂർത്തിയായി"), + ("Speed", "വേഗത"), + ("Custom Image Quality", "ഇമേജ് ക്വാളിറ്റി മാറ്റുക"), + ("Privacy mode", "സ്വകാര്യ മോഡ്"), + ("Block user input", "യൂസർ ഇൻപുട്ട് തടയുക"), + ("Unblock user input", "യൂസർ ഇൻപുട്ട് അനുവദിക്കുക"), + ("Adjust Window", "വിൻഡോ ക്രമീകരിക്കുക"), + ("Original", "ഒറിജിനൽ"), + ("Shrink", "ചുരുക്കുക"), + ("Stretch", "വലിപ്പിക്കുക"), + ("Scrollbar", "സ്ക്രോൾബാർ"), + ("ScrollAuto", "ഓട്ടോ സ്ക്രോൾ"), + ("Good image quality", "നല്ല ക്വാളിറ്റി"), + ("Balanced", "സന്തുലിതം"), + ("Optimize reaction time", "പ്രതികരണ സമയം മെച്ചപ്പെടുത്തുക"), + ("Custom", "കസ്റ്റം"), + ("Show remote cursor", "റിമോട്ട് കർസർ കാണിക്കുക"), + ("Show quality monitor", "ക്വാളിറ്റി മോണിറ്റർ കാണിക്കുക"), + ("Disable clipboard", "ക്ലിപ്പ്ബോർഡ് ഒഴിവാക്കുക"), + ("Lock after session end", "സെഷൻ കഴിഞ്ഞാൽ ലോക്ക് ചെയ്യുക"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del നൽകുക"), + ("Insert Lock", "ലോക്ക് ചെയ്യുക"), + ("Refresh", "പുതുക്കുക"), + ("ID does not exist", "ഐഡി നിലവിലില്ല"), + ("Failed to connect to rendezvous server", "സെർവറുമായി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Please try later", "ദയവായി പിന്നീട് ശ്രമിക്കുക"), + ("Remote desktop is offline", "റിമോട്ട് ഡെസ്ക്ടോപ്പ് ഓഫ്‌ലൈനാണ്"), + ("Key mismatch", "കീ പൊരുത്തക്കേട്"), + ("Timeout", "സമയം കഴിഞ്ഞു"), + ("Failed to connect to relay server", "റിലേ സെർവറുമായി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Failed to connect via rendezvous server", "സെർവർ വഴി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Failed to connect via relay server", "റിലേ സെർവർ വഴി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Failed to make direct connection to remote desktop", "നേരിട്ട് ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Set Password", "പാസ്‌വേഡ് നൽകുക"), + ("OS Password", "OS പാസ്‌വേഡ്"), + ("install_tip", "മികച്ച പ്രകടനത്തിനായി ഇൻസ്റ്റാൾ ചെയ്യുക."), + ("Click to upgrade", "അപ്‌ഗ്രേഡ് ചെയ്യാൻ ക്ലിക്ക് ചെയ്യുക"), + ("Configure", "ക്രമീകരിക്കുക"), + ("config_acc", "അക്‌സസിബിലിറ്റി ക്രമീകരിക്കുക"), + ("config_screen", "സ്ക്രീൻ ക്രമീകരിക്കുക"), + ("Installing ...", "ഇൻസ്റ്റാൾ ചെയ്യുന്നു..."), + ("Install", "ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("Installation", "ഇൻസ്റ്റാളേഷൻ"), + ("Installation Path", "ഇൻസ്റ്റാളേഷൻ പാത്ത്"), + ("Create start menu shortcuts", "സ്റ്റാർട്ട് മെനുവിൽ ഷോർട്ട്കട്ട് ഉണ്ടാക്കുക"), + ("Create desktop icon", "ഡെസ്ക്ടോപ്പ് ഐക്കൺ ഉണ്ടാക്കുക"), + ("agreement_tip", "ഇൻസ്റ്റാൾ ചെയ്യുന്നതിലൂടെ നിങ്ങൾ കരാറുകൾ അംഗീകരിക്കുന്നു."), + ("Accept and Install", "അംഗീകരിച്ച് ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("End-user license agreement", "ലൈസൻസ് കരാർ"), + ("Generating ...", "ഉണ്ടാക്കുന്നു..."), + ("Your installation is lower version.", "നിങ്ങളുടെ ഇൻസ്റ്റാളേഷൻ പഴയ പതിപ്പാണ്."), + ("not_close_tcp_tip", "ടണൽ ഉപയോഗിക്കുമ്പോൾ ഈ വിൻഡോ അടയ്ക്കരുത്."), + ("Listening ...", "ശ്രദ്ധിക്കുന്നു..."), + ("Remote Host", "റിമോട്ട് ഹോസ്റ്റ്"), + ("Remote Port", "റിമോട്ട് പോർട്ട്"), + ("Action", "നടപടി"), + ("Add", "ചേർക്കുക"), + ("Local Port", "ലോക്കൽ പോർട്ട്"), + ("Local Address", "ലോക്കൽ അഡ്രസ്"), + ("Change Local Port", "ലോക്കൽ പോർട്ട് മാറ്റുക"), + ("setup_server_tip", "വേഗതയുള്ള കണക്ഷനായി സ്വന്തം സെർവർ സജ്ജമാക്കുക"), + ("Too short, at least 6 characters.", "വളരെ ചെറുതാണ്, കുറഞ്ഞത് 6 അക്ഷരങ്ങൾ വേണം."), + ("The confirmation is not identical.", "സ്ഥിരീകരണം ഒരേപോലെയല്ല."), + ("Permissions", "അനുമതികൾ"), + ("Accept", "സ്വീകരിക്കുക"), + ("Dismiss", "നിരസിക്കുക"), + ("Disconnect", "വിച്ഛേദിക്കുക"), + ("Enable file copy and paste", "ഫയൽ കോപ്പി-പേസ്റ്റ് അനുവദിക്കുക"), + ("Connected", "ബന്ധിപ്പിച്ചു"), + ("Direct and encrypted connection", "നേരിട്ടുള്ളതും എൻക്രിപ്റ്റ് ചെയ്തതുമായ കണക്ഷൻ"), + ("Relayed and encrypted connection", "റിലേ വഴിയുള്ള എൻക്രിപ്റ്റ് ചെയ്ത കണക്ഷൻ"), + ("Direct and unencrypted connection", "നേരിട്ടുള്ളതും എൻക്രിപ്റ്റ് ചെയ്യാത്തതുമായ കണക്ഷൻ"), + ("Relayed and unencrypted connection", "റിലേ വഴിയുള്ള എൻക്രിപ്റ്റ് ചെയ്യാത്ത കണക്ഷൻ"), + ("Enter Remote ID", "റിമോട്ട് ഐഡി നൽകുക"), + ("Enter your password", "നിങ്ങളുടെ പാസ്‌വേഡ് നൽകുക"), + ("Logging in...", "ലോഗിൻ ചെയ്യുന്നു..."), + ("Enable RDP session sharing", "RDP സെഷൻ പങ്കിടൽ അനുവദിക്കുക"), + ("Auto Login", "ഓട്ടോ ലോഗിൻ"), + ("Enable direct IP access", "നേരിട്ടുള്ള IP ആക്‌സസ് അനുവദിക്കുക"), + ("Rename", "പേര് മാറ്റുക"), + ("Space", "സ്പേസ്"), + ("Create desktop shortcut", "ഡെസ്ക്ടോപ്പ് ഷോർട്ട്കട്ട് ഉണ്ടാക്കുക"), + ("Change Path", "പാത്ത് മാറ്റുക"), + ("Create Folder", "ഫോൾഡർ ഉണ്ടാക്കുക"), + ("Please enter the folder name", "ദയവായി ഫോൾഡറിന്റെ പേര് നൽകുക"), + ("Fix it", "പരിഹരിക്കുക"), + ("Warning", "മുന്നറിയിപ്പ്"), + ("Login screen using Wayland is not supported", "Wayland വഴിയുള്ള ലോഗിൻ സപ്പോർട്ട് ചെയ്യുന്നില്ല"), + ("Reboot required", "റീബൂട്ട് ആവശ്യമാണ്"), + ("Unsupported display server", "പിന്തുണയ്ക്കാത്ത ഡിസ്‌പ്ലേ സെർവർ"), + ("x11 expected", "x11 ആവശ്യമാണ്"), + ("Port", "പോർട്ട്"), + ("Settings", "ക്രമീകരണങ്ങൾ"), + ("Username", "യൂസർ നെയിം"), + ("Invalid port", "അസാധുവായ പോർട്ട്"), + ("Closed manually by the peer", "മറുഭാഗത്തുനിന്നും മാനുവലായി അടച്ചു"), + ("Enable remote configuration modification", "റിമോട്ട് കോൺഫിഗറേഷൻ മാറ്റങ്ങൾ അനുവദിക്കുക"), + ("Run without install", "ഇൻസ്റ്റാൾ ചെയ്യാതെ പ്രവർത്തിപ്പിക്കുക"), + ("Connect via relay", "റിലേ വഴി കണക്ട് ചെയ്യുക"), + ("Always connect via relay", "എപ്പോഴും റിലേ വഴി കണക്ട് ചെയ്യുക"), + ("whitelist_tip", "വൈറ്റ്‌ലിസ്റ്റ് ചെയ്ത ഐപികൾക്ക് മാത്രമേ എന്നെ ആക്‌സസ് ചെയ്യാൻ കഴിയൂ"), + ("Login", "ലോഗിൻ"), + ("Verify", "പരിശോധിക്കുക"), + ("Remember me", "എന്നെ ഓർമ്മിക്കുക"), + ("Trust this device", "ഈ ഉപകരണം വിശ്വസിക്കുക"), + ("Verification code", "വെരിഫിക്കേഷൻ കോഡ്"), + ("verification_tip", "വെരിഫിക്കേഷൻ കോഡ് നിങ്ങളുടെ ഇമെയിലിലേക്ക് അയച്ചു"), + ("Logout", "ലോഗൗട്ട്"), + ("Tags", "ടാഗുകൾ"), + ("Search ID", "ഐഡി തിരയുക"), + ("whitelist_sep", "കോമ, സെമി കോളൻ അല്ലെങ്കിൽ സ്പേസ് ഉപയോഗിച്ച് തിരിക്കുക"), + ("Add ID", "ഐഡി ചേർക്കുക"), + ("Add Tag", "ടാഗ് ചേർക്കുക"), + ("Unselect all tags", "എല്ലാ ടാഗുകളും ഒഴിവാക്കുക"), + ("Network error", "നെറ്റ്‌വർക്ക് പിശക്"), + ("Username missed", "യൂസർ നെയിം നൽകിയില്ല"), + ("Password missed", "പാസ്‌വേഡ് നൽകിയില്ല"), + ("Wrong credentials", "തെറ്റായ വിവരങ്ങൾ"), + ("The verification code is incorrect or has expired", "കോഡ് തെറ്റാണ് അല്ലെങ്കിൽ കാലാവധി കഴിഞ്ഞു"), + ("Edit Tag", "ടാഗ് മാറ്റുക"), + ("Forget Password", "പാസ്‌വേഡ് മറന്നു"), + ("Favorites", "പ്രിയപ്പെട്ടവ"), + ("Add to Favorites", "പ്രിയപ്പെട്ടവയിലേക്ക് ചേർക്കുക"), + ("Remove from Favorites", "പ്രിയപ്പെട്ടവയിൽ നിന്ന് നീക്കം ചെയ്യുക"), + ("Empty", "ശൂന്യം"), + ("Invalid folder name", "അസാധുവായ ഫോൾഡർ പേര്"), + ("Socks5 Proxy", "Socks5 പ്രോക്സി"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) പ്രോക്സി"), + ("Discovered", "കണ്ടെത്തിയവ"), + ("install_daemon_tip", "കമ്പ്യൂട്ടർ തുടങ്ങുമ്പോൾ തന്നെ പ്രവർത്തിക്കാൻ സർവീസ് ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("Remote ID", "റിമോട്ട് ഐഡി"), + ("Paste", "പേസ്റ്റ്"), + ("Paste here?", "ഇവിടെ പേസ്റ്റ് ചെയ്യണോ?"), + ("Are you sure to close the connection?", "കണക്ഷൻ നിർത്തണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Download new version", "പുതിയ പതിപ്പ് ഡൗൺലോഡ് ചെയ്യുക"), + ("Touch mode", "ടച്ച് മോഡ്"), + ("Mouse mode", "മൗസ് മോഡ്"), + ("One-Finger Tap", "ഒരു വിരൽ ടാപ്പ്"), + ("Left Mouse", "മൗസ് ഇടത് ബട്ടൺ"), + ("One-Long Tap", "ഒരു നീണ്ട ടാപ്പ്"), + ("Two-Finger Tap", "രണ്ട് വിരൽ ടാപ്പ്"), + ("Right Mouse", "മൗസ് വലത് ബട്ടൺ"), + ("One-Finger Move", "ഒരു വിരൽ നീക്കം"), + ("Double Tap & Move", "രണ്ട് ടാപ്പും നീക്കവും"), + ("Mouse Drag", "മൗസ് ഡ്രാഗ്"), + ("Three-Finger vertically", "മൂന്ന് വിരൽ ലംബമായി"), + ("Mouse Wheel", "മൗസ് വീൽ"), + ("Two-Finger Move", "രണ്ട് വിരൽ നീക്കം"), + ("Canvas Move", "ക്യാൻവാസ് നീക്കുക"), + ("Pinch to Zoom", "സൂം ചെയ്യാൻ പിഞ്ച് ചെയ്യുക"), + ("Canvas Zoom", "ക്യാൻവാസ് സൂം"), + ("Reset canvas", "ക്യാൻവാസ് റീസെറ്റ് ചെയ്യുക"), + ("No permission of file transfer", "ഫയൽ കൈമാറ്റത്തിന് അനുമതിയില്ല"), + ("Note", "കുറിപ്പ്"), + ("Connection", "കണക്ഷൻ"), + ("Share screen", "സ്ക്രീൻ പങ്കിടുക"), + ("Chat", "ചാറ്റ്"), + ("Total", "ആകെ"), + ("items", "ഇനങ്ങൾ"), + ("Selected", "തിഞ്ഞെടുത്തവ"), + ("Screen Capture", "സ്ക്രീൻ ക്യാപ്ചർ"), + ("Input Control", "ഇൻപുട്ട് നിയന്ത്രണം"), + ("Audio Capture", "ഓഡിയോ ക്യാപ്ചർ"), + ("Do you accept?", "നിങ്ങൾ അംഗീകരിക്കുന്നുണ്ടോ?"), + ("Open System Setting", "സിസ്റ്റം സെറ്റിംഗ്സ് തുറക്കുക"), + ("How to get Android input permission?", "ആൻഡ്രോയിഡ് ഇൻപുട്ട് അനുമതി എങ്ങനെ നേടാം?"), + ("android_input_permission_tip1", "ഇൻപുട്ട് അനുമതിക്കായി ആക്‌സസിബിലിറ്റി സർവീസ് ഓൺ ചെയ്യുക."), + ("android_input_permission_tip2", "സെറ്റിംഗ്സിൽ RustDesk കണ്ടെത്തി അത് ഓൺ ചെയ്യുക."), + ("android_new_connection_tip", "പുതിയ കണക്ഷൻ അഭ്യർത്ഥന ലഭിച്ചു."), + ("android_service_will_start_tip", "സ്ക്രീൻ ക്യാപ്ചർ ഓൺ ചെയ്താൽ സർവീസ് താനേ തുടങ്ങും."), + ("android_stop_service_tip", "സർവീസ് നിർത്തുന്നത് എല്ലാ കണക്ഷനുകളും വിച്ഛേദിക്കും."), + ("android_version_audio_tip", "ആൻഡ്രോയിഡ് 10-ൽ കൂടുതൽ വേണം ഓഡിയോ ക്യാപ്ചർ ചെയ്യാൻ."), + ("android_start_service_tip", "സ്ക്രീൻ ഷെയറിംഗ് തുടങ്ങാൻ ക്ലിക്ക് ചെയ്യുക."), + ("android_permission_may_not_change_tip", "അനുമതികൾ പിന്നീട് മാറ്റാൻ കഴിയില്ല, ശ്രദ്ധിച്ച് തിരഞ്ഞെടുക്കുക."), + ("Account", "അക്കൗണ്ട്"), + ("Overwrite", "തിരുത്തിയെഴുതുക (Overwrite)"), + ("This file exists, skip or overwrite this file?", "ഈ ഫയൽ നിലവിലുണ്ട്, ഒഴിവാക്കണോ അതോ തിരുത്തിയെഴുതണോ?"), + ("Quit", "പുറത്തുകടക്കുക"), + ("Help", "സഹായം"), + ("Failed", "പരാജയപ്പെട്ടു"), + ("Succeeded", "വിജയിച്ചു"), + ("Someone turns on privacy mode, exit", "ആരോ പ്രൈവസി മോഡ് ഓൺ ചെയ്തു, പുറത്തുകടക്കുന്നു"), + ("Unsupported", "പിന്തുണയ്ക്കുന്നില്ല"), + ("Peer denied", "മറുഭാഗത്തുനിന്ന് നിരസിച്ചു"), + ("Please install plugins", "ദയവായി പ്ലഗിനുകൾ ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("Peer exit", "മറുഭാഗത്തുനിന്ന് പുറത്തുകടന്നു"), + ("Failed to turn off", "ഓഫ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Turned off", "ഓഫ് ചെയ്തു"), + ("Language", "ഭാഷ"), + ("Keep RustDesk background service", "RustDesk ബാക്ക്ഗ്രൗണ്ടിൽ പ്രവർത്തിപ്പിക്കുക"), + ("Ignore Battery Optimizations", "ബാറ്ററി ഒപ്റ്റിമൈസേഷൻ അവഗണിക്കുക"), + ("android_open_battery_optimizations_tip", "കണക്ഷൻ മുറിയാതിരിക്കാൻ ബാറ്ററി ഒപ്റ്റിമൈസേഷൻ സെറ്റിംഗ്സ് തുറക്കുക"), + ("Start on boot", "തുടങ്ങുമ്പോൾ തന്നെ പ്രവർത്തിക്കുക"), + ("Start the screen sharing service on boot, requires special permissions", "തുടങ്ങുമ്പോൾ തന്നെ സ്ക്രീൻ ഷെയറിംഗ് തുടങ്ങുക, പ്രത്യേക അനുമതി ആവശ്യമാണ്"), + ("Connection not allowed", "കണക്ഷൻ അനുവദനീയമല്ല"), + ("Legacy mode", "ലെഗസി മോഡ്"), + ("Map mode", "മാപ്പ് മോഡ്"), + ("Translate mode", "ട്രാൻസ്ലേറ്റ് മോഡ്"), + ("Use permanent password", "സ്ഥിരമായ പാസ്‌വേഡ് ഉപയോഗിക്കുക"), + ("Use both passwords", "രണ്ട് പാസ്‌വേഡുകളും ഉപയോഗിക്കുക"), + ("Set permanent password", "സ്ഥിരമായ പാസ്‌വേഡ് സജ്ജമാക്കുക"), + ("Enable remote restart", "റിമോട്ട് റീസ്റ്റാർട്ട് അനുവദിക്കുക"), + ("Restart remote device", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുക"), + ("Are you sure you want to restart", "റീസ്റ്റാർട്ട് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Restarting remote device", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുന്നു"), + ("remote_restarting_tip", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുന്നു, ദയവായി കാത്തിരിക്കുക..."), + ("Copied", "കോപ്പി ചെയ്തു"), + ("Exit Fullscreen", "ഫുൾ സ്ക്രീനിൽ നിന്ന് പുറത്തുകടക്കുക"), + ("Fullscreen", "ഫുൾ സ്ക്രീൻ"), + ("Mobile Actions", "മൊബൈൽ നടപടികൾ"), + ("Select Monitor", "മോണിറ്റർ തിരഞ്ഞെടുക്കുക"), + ("Control Actions", "നിയന്ത്രണ നടപടികൾ"), + ("Display Settings", "ഡിസ്‌പ്ലേ ക്രമീകരണങ്ങൾ"), + ("Ratio", "അനുപാതം (Ratio)"), + ("Image Quality", "ചിത്രത്തിന്റെ ഗുണനിലവാരം"), + ("Scroll Style", "സ്ക്രോൾ സ്റ്റൈൽ"), + ("Show Toolbar", "ടൂൾബാർ കാണിക്കുക"), + ("Hide Toolbar", "ടൂൾബാർ മറയ്ക്കുക"), + ("Direct Connection", "നേരിട്ടുള്ള കണക്ഷൻ"), + ("Relay Connection", "റിലേ കണക്ഷൻ"), + ("Secure Connection", "സുരക്ഷിതമായ കണക്ഷൻ"), + ("Insecure Connection", "സുരക്ഷിതമല്ലാത്ത കണക്ഷൻ"), + ("Scale original", "ഒറിജിനൽ വലിപ്പം"), + ("Scale adaptive", "അഡാപ്റ്റീവ് വലിപ്പം"), + ("General", "പൊതുവായവ"), + ("Security", "സുരക്ഷ"), + ("Theme", "തീം"), + ("Dark Theme", "ഡാർക്ക് തീം"), + ("Light Theme", "ലൈറ്റ് തീം"), + ("Dark", "ഡാർക്ക്"), + ("Light", "ലൈറ്റ്"), + ("Follow System", "സിസ്റ്റം അനുസരിച്ച്"), + ("Enable hardware codec", "ഹാർഡ്‌വെയർ കോഡെക് അനുവദിക്കുക"), + ("Unlock Security Settings", "സുരക്ഷാ ക്രമീകരണങ്ങൾ അൺലോക്ക് ചെയ്യുക"), + ("Enable audio", "ശബ്ദം അനുവദിക്കുക"), + ("Unlock Network Settings", "നെറ്റ്‌വർക്ക് ക്രമീകരണങ്ങൾ അൺലോക്ക് ചെയ്യുക"), + ("Server", "സെർവർ"), + ("Direct IP Access", "നേരിട്ടുള്ള IP ആക്‌സസ്"), + ("Proxy", "പ്രോക്സി"), + ("Apply", "പ്രയോഗിക്കുക"), + ("Disconnect all devices?", "എല്ലാ ഉപകരണങ്ങളും വിച്ഛേദിക്കണോ?"), + ("Clear", "വൃത്തിയാക്കുക"), + ("Audio Input Device", "ശബ്ദ ഇൻപുട്ട് ഉപകരണം"), + ("Use IP Whitelisting", "IP വൈറ്റ്‌ലിസ്റ്റിംഗ് ഉപയോഗിക്കുക"), + ("Network", "നെറ്റ്‌വർക്ക്"), + ("Pin Toolbar", "ടൂൾബാർ പിൻ ചെയ്യുക"), + ("Unpin Toolbar", "ടൂൾബാർ അൺപിൻ ചെയ്യുക"), + ("Recording", "റെക്കോർഡിംഗ്"), + ("Directory", "ഡയറക്ടറി"), + ("Automatically record incoming sessions", "വരുന്ന സെഷനുകൾ താനേ റെക്കോർഡ് ചെയ്യുക"), + ("Automatically record outgoing sessions", "പോകുന്ന സെഷനുകൾ താനേ റെക്കോർഡ് ചെയ്യുക"), + ("Change", "മാറ്റുക"), + ("Start session recording", "റെക്കോർഡിംഗ് തുടങ്ങുക"), + ("Stop session recording", "റെക്കോർഡിംഗ് നിർത്തുക"), + ("Enable recording session", "സെഷൻ റെക്കോർഡിംഗ് അനുവദിക്കുക"), + ("Enable LAN discovery", "LAN കണ്ടെത്തൽ അനുവദിക്കുക"), + ("Deny LAN discovery", "LAN കണ്ടെത്തൽ നിരസിക്കുക"), + ("Write a message", "സന്ദേശം എഴുതുക"), + ("Prompt", "പ്രോംപ്റ്റ്"), + ("Please wait for confirmation of UAC...", "UAC സ്ഥിരീകരണത്തിനായി കാത്തിരിക്കുക..."), + ("elevated_foreground_window_tip", "റിമോട്ടിലെ വിൻഡോയ്ക്ക് കൂടുതൽ അനുമതി ആവശ്യമാണ്."), + ("Disconnected", "വിച്ഛേദിച്ചു"), + ("Other", "മറ്റുള്ളവ"), + ("Confirm before closing multiple tabs", "ടാബുകൾ അടയ്ക്കുന്നതിന് മുൻപ് സ്ഥിരീകരിക്കുക"), + ("Keyboard Settings", "കീബോർഡ് ക്രമീകരണങ്ങൾ"), + ("Full Access", "പൂർണ്ണ ആക്‌സസ്"), + ("Screen Share", "സ്ക്രീൻ ഷെയർ"), + ("ubuntu-21-04-required", "Ubuntu 21.04 എങ്കിലും വേണം"), + ("wayland-requires-higher-linux-version", "Wayland-ന് പുതിയ ലിനക്സ് പതിപ്പ് ആവശ്യമാണ്"), + ("xdp-portal-unavailable", "XDP പോർട്ടൽ ലഭ്യമല്ല"), + ("JumpLink", "ജമ്പ്‌ലിങ്ക്"), + ("Please Select the screen to be shared(Operate on the peer side).", "പങ്കിടാനുള്ള സ്ക്രീൻ തിരഞ്ഞെടുക്കുക (മറുഭാഗത്ത് ചെയ്യുക)."), + ("Show RustDesk", "RustDesk കാണിക്കുക"), + ("This PC", "ഈ പിസി"), + ("or", "അല്ലെങ്കിൽ"), + ("Elevate", "എലിവേറ്റ് ചെയ്യുക"), + ("Zoom cursor", "സൂം കർസർ"), + ("Accept sessions via password", "പാസ്‌വേഡ് വഴി സെഷനുകൾ അനുവദിക്കുക"), + ("Accept sessions via click", "ക്ലിക്ക് വഴി സെഷനുകൾ അനുവദിക്കുക"), + ("Accept sessions via both", "രണ്ടും വഴി സെഷനുകൾ അനുവദിക്കുക"), + ("Please wait for the remote side to accept your session request...", "മറുഭാഗം അനുമതി നൽകാനായി കാത്തിരിക്കുക..."), + ("One-time Password", "ഒറ്റത്തവണ പാസ്‌വേഡ്"), + ("Use one-time password", "ഒറ്റത്തവണ പാസ്‌വേഡ് ഉപയോഗിക്കുക"), + ("One-time password length", "ഒറ്റത്തവണ പാസ്‌വേഡ് നീളം"), + ("Request access to your device", "നിങ്ങളുടെ ഉപകരണം ആക്‌സസ് ചെയ്യാൻ അനുമതി ചോദിക്കുന്നു"), + ("Hide connection management window", "കണക്ഷൻ മാനേജ്‌മെന്റ് വിൻഡോ മറയ്ക്കുക"), + ("hide_cm_tip", "പാസ്‌വേഡ് വഴിയുള്ള കണക്ഷൻ ആണെങ്കിൽ മാത്രം മറയ്ക്കുക"), + ("wayland_experiment_tip", "Wayland പിന്തുണ പരീക്ഷണാടിസ്ഥാനത്തിലാണ്"), + ("Right click to select tabs", "ടാബുകൾ തിരഞ്ഞെടുക്കാൻ വലത് ക്ലിക്ക് ചെയ്യുക"), + ("Skipped", "ഒഴിവാക്കി"), + ("Add to address book", "അഡ്രസ് ബുക്കിലേക്ക് ചേർക്കുക"), + ("Group", "ഗ്രൂപ്പ്"), + ("Search", "തിരയുക"), + ("Closed manually by web console", "വെബ് കൺസോൾ വഴി മാനുവലായി അടച്ചു"), + ("Local keyboard type", "ലോക്കൽ കീബോർഡ് തരം"), + ("Select local keyboard type", "ലോക്കൽ കീബോർഡ് തരം തിരഞ്ഞെടുക്കുക"), + ("software_render_tip", "സ്ക്രീൻ കറുത്തിരിക്കുകയാണെങ്കിൽ ഇത് പരീക്ഷിക്കുക"), + ("Always use software rendering", "എപ്പോഴും സോഫ്റ്റ്‌വെയർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"), + ("config_input", "ഇൻപുട്ട് ക്രമീകരിക്കുക"), + ("config_microphone", "മൈക്രോഫോൺ ക്രമീകരിക്കുക"), + ("request_elevation_tip", "മറുഭാഗത്തുനിന്ന് എലവേഷൻ ആവശ്യപ്പെടുക"), + ("Wait", "കാത്തിരിക്കുക"), + ("Elevation Error", "എലവേഷൻ പിശക്"), + ("Ask the remote user for authentication", "റിമോട്ട് ഉപയോക്താവിനോട് അനുമതി ചോദിക്കുക"), + ("Choose this if the remote account is administrator", "റിമോട്ട് അക്കൗണ്ട് അഡ്മിനിസ്ട്രേറ്റർ ആണെങ്കിൽ ഇത് തിരഞ്ഞെടുക്കുക"), + ("Transmit the username and password of administrator", "അഡ്മിനിസ്ട്രേറ്റർ വിവരങ്ങൾ അയക്കുക"), + ("still_click_uac_tip", "റിമോട്ട് ഉപയോക്താവ് UAC വിൻഡോയിൽ 'അതെ' എന്ന് ക്ലിക്ക് ചെയ്യേണ്ടതുണ്ട്."), + ("Request Elevation", "എലവേഷൻ ആവശ്യപ്പെടുക"), + ("wait_accept_uac_tip", "റിമോട്ട് ഉപയോക്താവ് UAC അംഗീകരിക്കാൻ കാത്തിരിക്കുക."), + ("Elevate successfully", "വിജയകരമായി എലവേറ്റ് ചെയ്തു"), + ("uppercase", "വലിയ അക്ഷരം (Uppercase)"), + ("lowercase", "ചെറിയ അക്ഷരം (Lowercase)"), + ("digit", "അക്കം"), + ("special character", "പ്രത്യേക ചിഹ്നം"), + ("length>=8", "നീളം >= 8"), + ("Weak", "ദുർബലം"), + ("Medium", "ഇടത്തരം"), + ("Strong", "ശക്തം"), + ("Switch Sides", "വശങ്ങൾ മാറ്റുക"), + ("Please confirm if you want to share your desktop?", "നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ് പങ്കിടണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Display", "ഡിസ്‌പ്ലേ"), + ("Default View Style", "സാധാരണ വ്യൂ സ്റ്റൈൽ"), + ("Default Scroll Style", "സാധാരണ സ്ക്രോൾ സ്റ്റൈൽ"), + ("Default Image Quality", "സാധാരണ ഇമേജ് ക്വാളിറ്റി"), + ("Default Codec", "സാധാരണ കോഡെക്"), + ("Bitrate", "ബിറ്റ്റേറ്റ്"), + ("FPS", "FPS"), + ("Auto", "ഓട്ടോ"), + ("Other Default Options", "മറ്റ് സാധാരണ ഓപ്ഷനുകൾ"), + ("Voice call", "വോയിസ് കോൾ"), + ("Text chat", "ടെക്സ്റ്റ് ചാറ്റ്"), + ("Stop voice call", "വോയിസ് കോൾ നിർത്തുക"), + ("relay_hint_tip", "നേരിട്ടുള്ള കണക്ഷൻ സാധ്യമല്ല; റിലേ വഴി ശ്രമിക്കാം."), + ("Reconnect", "വീണ്ടും കണക്ട് ചെയ്യുക"), + ("Codec", "കോഡെക്"), + ("Resolution", "റെസല്യൂഷൻ"), + ("No transfers in progress", "കൈമാറ്റങ്ങളൊന്നും നടക്കുന്നില്ല"), + ("Set one-time password length", "ഒറ്റത്തവണ പാസ്‌വേഡ് നീളം നിശ്ചയിക്കുക"), + ("RDP Settings", "RDP ക്രമീകരണങ്ങൾ"), + ("Sort by", "ക്രമീകരിക്കുക"), + ("New Connection", "പുതിയ കണക്ഷൻ"), + ("Restore", "പുനഃസ്ഥാപിക്കുക"), + ("Minimize", "ചുരുക്കുക"), + ("Maximize", "വലുതാക്കുക"), + ("Your Device", "നിങ്ങളുടെ ഉപകരണം"), + ("empty_recent_tip", "സമീപകാല സെഷനുകൾ ഇവിടെ കാണാം."), + ("empty_favorite_tip", "പ്രിയപ്പെട്ടവ ഇവിടെ കാണാം."), + ("empty_lan_tip", "ലോക്കൽ നെറ്റ്‌വർക്കിലെ ഉപകരണങ്ങൾ ഇവിടെ കാണാം."), + ("empty_address_book_tip", "അഡ്രസ് ബുക്ക് ശൂന്യമാണ്."), + ("Empty Username", "യൂസർ നെയിം നൽകിയില്ല"), + ("Empty Password", "പാസ്‌വേഡ് നൽകിയില്ല"), + ("Me", "ഞാൻ"), + ("identical_file_tip", "ഈ ഫയൽ നിലവിലുണ്ട്."), + ("show_monitors_tip", "ടൂൾബാറിൽ മോണിറ്ററുകൾ കാണിക്കുക"), + ("View Mode", "വ്യൂ മോഡ്"), + ("login_linux_tip", "റിമോട്ട് ലിനക്സ് സെഷനായി ലോഗിൻ ചെയ്യണം"), + ("verify_rustdesk_password_tip", "RustDesk പാസ്‌വേഡ് പരിശോധിക്കുക"), + ("remember_account_tip", "ഈ അക്കൗണ്ട് ഓർമ്മിക്കുക"), + ("os_account_desk_tip", "ആക്‌സസിനായി OS അക്കൗണ്ട് ഉപയോഗിക്കുക"), + ("OS Account", "OS അക്കൗണ്ട്"), + ("another_user_login_title_tip", "മറ്റൊരു ഉപയോക്താവ് ലോഗിൻ ചെയ്തിട്ടുണ്ട്"), + ("another_user_login_text_tip", "വിച്ഛേദിച്ച ശേഷം വീണ്ടും ശ്രമിക്കുക"), + ("xorg_not_found_title_tip", "Xorg കണ്ടെത്താനായില്ല"), + ("xorg_not_found_text_tip", "ദയവായി Xorg ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("no_desktop_title_tip", "ഡെസ്ക്ടോപ്പ് ലഭ്യമല്ല"), + ("no_desktop_text_tip", "ദയവായി ലിനക്സ് ഡെസ്ക്ടോപ്പ് ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("No need to elevate", "എലവേറ്റ് ചെയ്യേണ്ടതില്ല"), + ("System Sound", "സിസ്റ്റം സൗണ്ട്"), + ("Default", "ഡിഫോൾട്ട്"), + ("New RDP", "പുതിയ RDP"), + ("Fingerprint", "ഫിംഗർപ്രിന്റ്"), + ("Copy Fingerprint", "ഫിംഗർപ്രിന്റ് കോപ്പി ചെയ്യുക"), + ("no fingerprints", "ഫിംഗർപ്രിന്റുകൾ ഇല്ല"), + ("Select a peer", "ഒരാളെ തിരഞ്ഞെടുക്കുക"), + ("Select peers", "തിരഞ്ഞെടുക്കുക"), + ("Plugins", "പ്ലഗിനുകൾ"), + ("Uninstall", "അൺഇൻസ്റ്റാൾ ചെയ്യുക"), + ("Update", "അപ്ഡേറ്റ് ചെയ്യുക"), + ("Enable", "പ്രവർത്തനക്ഷമമാക്കുക"), + ("Disable", "പ്രവർത്തനരഹിതമാക്കുക"), + ("Options", "ഓപ്ഷനുകൾ"), + ("resolution_original_tip", "ഒറിജിനൽ റെസല്യൂഷൻ"), + ("resolution_fit_local_tip", "ലോക്കൽ സ്ക്രീനിന് അനുയോജ്യം"), + ("resolution_custom_tip", "കസ്റ്റം റെസല്യൂഷൻ"), + ("Collapse toolbar", "ടൂൾബാർ ചുരുക്കുക"), + ("Accept and Elevate", "അംഗീകരിച്ച് എലവേറ്റ് ചെയ്യുക"), + ("accept_and_elevate_btn_tooltip", "കണക്ഷൻ അംഗീകരിച്ച് UAC അനുമതികൾ നൽകുക."), + ("clipboard_wait_response_timeout_tip", "ക്ലിപ്പ്ബോർഡ് മറുപടിക്കായി കാത്തിരുന്നു സമയം കഴിഞ്ഞു."), + ("Incoming connection", "വരുന്ന കണക്ഷൻ"), + ("Outgoing connection", "പോകുന്ന കണക്ഷൻ"), + ("Exit", "പുറത്തുകടക്കുക"), + ("Open", "തുറക്കുക"), + ("logout_tip", "നിങ്ങൾക്ക് ലോഗൗട്ട് ചെയ്യണമെന്ന് ഉറപ്പാണോ?"), + ("Service", "സർവീസ്"), + ("Start", "തുടങ്ങുക"), + ("Stop", "നിർത്തുക"), + ("exceed_max_devices", "നിങ്ങൾ ഉപകരണങ്ങളുടെ പരിധി കവിഞ്ഞു."), + ("Sync with recent sessions", "സമീപകാല സെഷനുകളുമായി സિંക് ചെയ്യുക"), + ("Sort tags", "ടാഗുകൾ ക്രമീകരിക്കുക"), + ("Open connection in new tab", "പുതിയ ടാബിൽ തുറക്കുക"), + ("Move tab to new window", "ടാബ് പുതിയ വിൻഡോയിലേക്ക് മാറ്റുക"), + ("Can not be empty", "ശൂന്യമാകാൻ പാടില്ല"), + ("Already exists", "നിലവിലുണ്ട്"), + ("Change Password", "പാസ്‌വേഡ് മാറ്റുക"), + ("Refresh Password", "പാസ്‌വേഡ് പുതുക്കുക"), + ("ID", "ഐഡി"), + ("Grid View", "ഗ്രിഡ് വ്യൂ"), + ("List View", "ലിസ്റ്റ് വ്യൂ"), + ("Select", "തിരഞ്ഞെടുക്കുക"), + ("Toggle Tags", "ടാഗുകൾ മാറ്റുക"), + ("pull_ab_failed_tip", "അഡ്രസ് ബുക്ക് അപ്‌ഡേറ്റ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."), + ("push_ab_failed_tip", "അഡ്രസ് ബുക്ക് സિંക് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."), + ("synced_peer_readded_tip", "സമീപകാല ഉപകരണം അഡ്രസ് ബുക്കിലേക്ക് സિંക് ചെയ്തു."), + ("Change Color", "നിറം മാറ്റുക"), + ("Primary Color", "പ്രധാന നിറം"), + ("HSV Color", "HSV നിറം"), + ("Installation Successful!", "ഇൻസ്റ്റാളേഷൻ വിജയിച്ചു!"), + ("Installation failed!", "ഇൻസ്റ്റാളേഷൻ പരാജയപ്പെട്ടു!"), + ("Reverse mouse wheel", "മൗസ് വീൽ തിരിക്കുക"), + ("{} sessions", "{} സെഷനുകൾ"), + ("scam_title", "തട്ടിപ്പ് മുന്നറിയിപ്പ്!"), + ("scam_text1", "നിങ്ങൾക്ക് പരിചയമില്ലാത്ത ആരെങ്കിലും RustDesk ഉപയോഗിക്കാൻ ആവശ്യപ്പെട്ടാൽ ഉടൻ കണക്ഷൻ വിച്ഛേദിക്കുക."), + ("scam_text2", "ഇതൊരു തട്ടിപ്പായിരിക്കാം. ആർക്കും പാസ്‌വേഡ് നൽകരുത്."), + ("Don't show again", "വീണ്ടും കാണിക്കരുത്"), + ("I Agree", "ഞാൻ സമ്മതിക്കുന്നു"), + ("Decline", "നിരസിക്കുന്നു"), + ("Timeout in minutes", "മിനിറ്റുകളിൽ സമയം നിശ്ചയിക്കുക"), + ("auto_disconnect_option_tip", "പ്രവർത്തനമില്ലെങ്കിൽ താനേ വിച്ഛേദിക്കുക"), + ("Connection failed due to inactivity", "പ്രവർത്തനമില്ലാത്തതിനാൽ കണക്ഷൻ വിച്ഛേദിച്ചു"), + ("Check for software update on startup", "തുടങ്ങുമ്പോൾ അപ്‌ഡേറ്റ് ഉണ്ടോ എന്ന് പരിശോധിക്കുക"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "സെർവർ പ്രോ {} ലേക്ക് അപ്‌ഗ്രേഡ് ചെയ്യുക"), + ("pull_group_failed_tip", "ഗ്രൂപ്പ് വിവരങ്ങൾ ലഭിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Filter by intersection", "ഇന്റർസെക്ഷൻ വഴി ഫിൽട്ടർ ചെയ്യുക"), + ("Remove wallpaper during incoming sessions", "കണക്ഷൻ സമയത്ത് വാൾപേപ്പർ മാറ്റുക"), + ("Test", "പരിശോധിക്കുക"), + ("display_is_plugged_out_msg", "ഡിസ്‌പ്ലേ ഊരിയിരിക്കുകയാണ്."), + ("No displays", "ഡിസ്‌പ്ലേകൾ ഇല്ല"), + ("Open in new window", "പുതിയ വിൻഡോയിൽ തുറക്കുക"), + ("Show displays as individual windows", "ഓരോ ഡിസ്‌പ്ലേയും ഓരോ വിൻഡോയായി കാണിക്കുക"), + ("Use all my displays for the remote session", "എല്ലാ ഡിസ്‌പ്ലേകളും ഉപയോഗിക്കുക"), + ("selinux_tip", "SELinux പ്രവർത്തനക്ഷമമാണ്."), + ("Change view", "കാഴ്ച മാറ്റുക"), + ("Big tiles", "വലിയ ടൈലുകൾ"), + ("Small tiles", "ചെറിയ ടൈലുകൾ"), + ("List", "ലിസ്റ്റ്"), + ("Virtual display", "വെർച്വൽ ഡിസ്‌പ്ലേ"), + ("Plug out all", "എല്ലാം ഊരുക"), + ("True color (4:4:4)", "ട്രൂ കളർ (4:4:4)"), + ("Enable blocking user input", "യൂസർ ഇൻപുട്ട് തടയുന്നത് അനുവദിക്കുക"), + ("id_input_tip", "നിങ്ങൾക്ക് ഐഡി, ഏലിയാസ് അല്ലെങ്കിൽ ഐപി നൽകാം."), + ("privacy_mode_impl_mag_tip", "മാഗ്നിഫയർ സ്വകാര്യ മോഡ്"), + ("privacy_mode_impl_virtual_display_tip", "വെർച്വൽ ഡിസ്‌പ്ലേ സ്വകാര്യ മോഡ്"), + ("Enter privacy mode", "സ്വകാര്യ മോഡിലേക്ക് കടക്കുക"), + ("Exit privacy mode", "സ്വകാര്യ മോഡിൽ നിന്ന് പുറത്തുകടക്കുക"), + ("idd_not_support_under_win10_2004_tip", "Windows 10 (2004) എങ്കിലും വേണം."), + ("input_source_1_tip", "ഇൻപുട്ട് സോഴ്സ് 1"), + ("input_source_2_tip", "ഇൻപുട്ട് സോഴ്സ് 2"), + ("Swap control-command key", "Control-Command കീകൾ പരസ്പരം മാറ്റുക"), + ("swap-left-right-mouse", "ഇടത്-വലത് മൗസ് ബട്ടണുകൾ മാറ്റുക"), + ("2FA code", "2FA കോഡ്"), + ("More", "കൂടുതൽ"), + ("enable-2fa-title", "2FA ഓൺ ചെയ്യുക"), + ("enable-2fa-desc", "അതന്റിക്കേറ്റർ ആപ്പ് സജ്ജമാക്കുക."), + ("wrong-2fa-code", "തെറ്റായ 2FA കോഡ്."), + ("enter-2fa-title", "2FA കോഡ് നൽകുക"), + ("Email verification code must be 6 characters.", "ഇമെയിൽ കോഡ് 6 അക്ഷരങ്ങൾ വേണം."), + ("2FA code must be 6 digits.", "2FA കോഡ് 6 അക്കങ്ങൾ വേണം."), + ("Multiple Windows sessions found", "ഒന്നിലധികം വിൻഡോസ് സെഷനുകൾ കണ്ടെത്തി"), + ("Please select the session you want to connect to", "ബന്ധിപ്പിക്കേണ്ട സെഷൻ തിരഞ്ഞെടുക്കുക"), + ("powered_by_me", "ഞാൻ നിർമ്മിച്ചത്"), + ("outgoing_only_desk_tip", "ഇതൊരു ഔട്ട്‌ഗോയിംഗ് മോഡ് മാത്രമാണ്"), + ("preset_password_warning", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മാറ്റുക."), + ("Security Alert", "സുരക്ഷാ മുന്നറിയിപ്പ്"), + ("My address book", "എന്റെ അഡ്രസ് ബുക്ക്"), + ("Personal", "വ്യക്തിഗതം"), + ("Owner", "ഉടമസ്ഥൻ"), + ("Set shared password", "പങ്കിട്ട പാസ്‌വേഡ് സജ്ജമാക്കുക"), + ("Exist in", "നിലവിലുള്ളത്"), + ("Read-only", "വായിക്കാൻ മാത്രം"), + ("Read/Write", "വായിക്കാനും എഴുതാനും"), + ("Full Control", "പൂർണ്ണ നിയന്ത്രണം"), + ("share_warning_tip", "നിങ്ങളുടെ വിവരങ്ങൾ പങ്കിടുകയാണ്."), + ("Everyone", "എല്ലാവരും"), + ("ab_web_console_tip", "വെബ് കൺസോൾ അഡ്രസ് ബുക്ക്"), + ("allow-only-conn-window-open-tip", "RustDesk വിൻഡോ തുറന്നിരിക്കുമ്പോൾ മാത്രം കണക്ഷൻ അനുവദിക്കുക"), + ("no_need_privacy_mode_no_physical_displays_tip", "ഡിസ്‌പ്ലേ ഇല്ലാത്തതിനാൽ സ്വകാര്യ മോഡ് ആവശ്യമില്ല."), + ("Follow remote cursor", "റിമോട്ട് കർസറിനെ പിന്തുടരുക"), + ("Follow remote window focus", "റിമോട്ട് വിൻഡോ ഫോക്കസിനെ പിന്തുടരുക"), + ("default_proxy_tip", "ഡിഫോൾട്ട് പ്രോക്സി ക്രമീകരണം"), + ("no_audio_input_device_tip", "ഓഡിയോ ഇൻപുട്ട് ഉപകരണം കണ്ടെത്തിയില്ല."), + ("Incoming", "വരുന്നവ"), + ("Outgoing", "പോകുന്നവ"), + ("Clear Wayland screen selection", "Wayland സ്ക്രീൻ സെലക്ഷൻ മാറ്റുക"), + ("clear_Wayland_screen_selection_tip", "സ്ക്രീൻ സെലക്ഷൻ റീസെറ്റ് ചെയ്യുക."), + ("confirm_clear_Wayland_screen_selection_tip", "സെലക്ഷൻ മാറ്റണമെന്ന് ഉറപ്പാണോ?"), + ("android_new_voice_call_tip", "പുതിയ വോയിസ് കോൾ അഭ്യർത്ഥന"), + ("texture_render_tip", "ടെക്സ്ചർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"), + ("Use texture rendering", "ടെക്സ്ചർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"), + ("Floating window", "ഫ്ലോട്ടിംഗ് വിൻഡോ"), + ("floating_window_tip", "ബാക്ക്ഗ്രൗണ്ടിലാണെങ്കിലും RustDesk കാണിക്കുക"), + ("Keep screen on", "സ്ക്രീൻ ഓഫ് ആകാതെ വെക്കുക"), + ("Never", "ഒരിക്കലുമില്ല"), + ("During controlled", "നിയന്ത്രിക്കുമ്പോൾ"), + ("During service is on", "സർവീസ് ഓൺ ആയിരിക്കുമ്പോൾ"), + ("Capture screen using DirectX", "DirectX ഉപയോഗിച്ച് സ്ക്രീൻ ക്യാപ്ചർ ചെയ്യുക"), + ("Back", "പുറകോട്ട്"), + ("Apps", "ആപ്പുകൾ"), + ("Volume up", "ശബ്ദം കൂട്ടുക"), + ("Volume down", "ശബ്ദം കുറയ്ക്കുക"), + ("Power", "പവർ"), + ("Telegram bot", "ടെലഗ്രാം ബോട്ട്"), + ("enable-bot-tip", "അറിയിപ്പുകൾക്കായി ബോട്ട് ഓൺ ചെയ്യുക"), + ("enable-bot-desc", "ടെലഗ്രാം ബോട്ട് സജ്ജമാക്കുക."), + ("cancel-2fa-confirm-tip", "2FA റദ്ദാക്കണമെന്ന് ഉറപ്പാണോ?"), + ("cancel-bot-confirm-tip", "ബോട്ട് റദ്ദാക്കണമെന്ന് ഉറപ്പാണോ?"), + ("About RustDesk", "RustDesk-നെ കുറിച്ച്"), + ("Send clipboard keystrokes", "ക്ലിപ്പ്ബോർഡ് കീസ്ട്രോക്കുകൾ അയക്കുക"), + ("network_error_tip", "നെറ്റ്‌വർക്ക് പിശക്, വീണ്ടും ശ്രമിക്കുക."), + ("Unlock with PIN", "പിൻ ഉപയോഗിച്ച് അൺലോക്ക് ചെയ്യുക"), + ("Requires at least {} characters", "കുറഞ്ഞത് {} അക്ഷരങ്ങൾ വേണം"), + ("Wrong PIN", "തെറ്റായ പിൻ"), + ("Set PIN", "പിൻ സജ്ജമാക്കുക"), + ("Enable trusted devices", "വിശ്വസനീയമായ ഉപകരണങ്ങൾ അനുവദിക്കുക"), + ("Manage trusted devices", "വിശ്വസനീയമായ ഉപകരണങ്ങൾ നിയന്ത്രിക്കുക"), + ("Platform", "പ്ലാറ്റ്‌ഫോം"), + ("Days remaining", "ബാക്കിയുള്ള ദിവസങ്ങൾ"), + ("enable-trusted-devices-tip", "വിശ്വസനീയമായവയ്ക്ക് പാസ്‌വേഡ് വേണ്ട"), + ("Parent directory", "പ്രധാന ഡയറക്ടറി"), + ("Resume", "തുടരുക"), + ("Invalid file name", "അസാധുവായ ഫയൽ പേര്"), + ("one-way-file-transfer-tip", "ഒരു വശത്തേക്ക് മാത്രമുള്ള ഫയൽ കൈമാറ്റം"), + ("Authentication Required", "അംഗീകാരം ആവശ്യമാണ്"), + ("Authenticate", "അംഗീകരിക്കുക"), + ("web_id_input_tip", "റിമോട്ട് ഐഡി നൽകുക"), + ("Download", "ഡൗൺലോഡ്"), + ("Upload folder", "ഫോൾഡർ അപ്‌ലോഡ് ചെയ്യുക"), + ("Upload files", "ഫയലുകൾ അപ്‌ലോഡ് ചെയ്യുക"), + ("Clipboard is synchronized", "ക്ലിപ്പ്ബോർഡ് സങ്കലനം ചെയ്തു"), + ("Update client clipboard", "ക്ലയന്റ് ക്ലിപ്പ്ബോർഡ് പുതുക്കുക"), + ("Untagged", "ടാഗ് ചെയ്യാത്തവ"), + ("new-version-of-{}-tip", "{} പുതിയ പതിപ്പ് ലഭ്യമാണ്"), + ("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"), + ("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"), + ("Printer", "പ്രിന്റർ"), + ("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."), + ("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."), + ("printer-{}-not-installed-tip", "പ്രിന്റർ {} ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല."), + ("printer-{}-ready-tip", "പ്രിന്റർ {} തയ്യാറാണ്."), + ("Install {} Printer", "{} പ്രിന്റർ ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("Outgoing Print Jobs", "പോകുന്ന പ്രിന്റ് ജോലികൾ"), + ("Incoming Print Jobs", "വരുന്ന പ്രിന്റ് ജോലികൾ"), + ("Incoming Print Job", "വരുന്ന പ്രിന്റ് ജോലി"), + ("use-the-default-printer-tip", "ഡിഫോൾട്ട് പ്രിന്റർ ഉപയോഗിക്കുക"), + ("use-the-selected-printer-tip", "തിഞ്ഞെടുത്ത പ്രിന്റർ ഉപയോഗിക്കുക"), + ("auto-print-tip", "താനേ പ്രിന്റ് ചെയ്യുക"), + ("print-incoming-job-confirm-tip", "പ്രിന്റ് ചെയ്യുന്നതിന് മുൻപ് ചോദിക്കുക"), + ("remote-printing-disallowed-tile-tip", "റിമോട്ട് പ്രിന്റിംഗ് അനുവദനീയമല്ല"), + ("remote-printing-disallowed-text-tip", "സെറ്റിംഗ്സിൽ റിമോട്ട് പ്രിന്റിംഗ് ഓൺ ചെയ്യുക."), + ("save-settings-tip", "സെറ്റിംഗ്സ് സേവ് ചെയ്യുക"), + ("dont-show-again-tip", "വീണ്ടും കാണിക്കരുത്"), + ("Take screenshot", "സ്ക്രീൻഷോട്ട് എടുക്കുക"), + ("Taking screenshot", "സ്ക്രീൻഷോട്ട് എടുക്കുന്നു"), + ("screenshot-merged-screen-not-supported-tip", "മെർജ് ചെയ്ത സ്ക്രീൻഷോട്ട് പിന്തുണയ്ക്കുന്നില്ല."), + ("screenshot-action-tip", "സ്ക്രീൻഷോട്ടിന് ശേഷമുള്ള നടപടി"), + ("Save as", "പേരിൽ സേവ് ചെയ്യുക"), + ("Copy to clipboard", "ക്ലിപ്പ്ബോർഡിലേക്ക് കോപ്പി ചെയ്യുക"), + ("Enable remote printer", "റിമോട്ട് പ്രിന്റർ അനുവദിക്കുക"), + ("Downloading {}", "{} ഡൗൺലോഡ് ചെയ്യുന്നു"), + ("{} Update", "{} അപ്‌ഡേറ്റ്"), + ("{}-to-update-tip", "അപ്‌ഡേറ്റ് ചെയ്യാൻ {}"), + ("download-new-version-failed-tip", "പുതിയ പതിപ്പ് ഡൗൺലോഡ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."), + ("Auto update", "ഓട്ടോ അപ്‌ഡേറ്റ്"), + ("update-failed-check-msi-tip", "അപ്‌ഡേറ്റ് പരാജയപ്പെട്ടു, MSI ഫയൽ പരിശോധിക്കുക."), + ("websocket_tip", "പോട്ടുകൾ തടഞ്ഞിട്ടുണ്ടെങ്കിൽ WebSocket ഉപയോഗിക്കുക."), + ("Use WebSocket", "WebSocket ഉപയോഗിക്കുക"), + ("Trackpad speed", "ട്രാക്ക്പാഡ് വേഗത"), + ("Default trackpad speed", "സാധാരണ ട്രാക്ക്പാഡ് വേഗത"), + ("Numeric one-time password", "അക്കങ്ങൾ മാത്രമുള്ള OTP"), + ("Enable IPv6 P2P connection", "IPv6 P2P കണക്ഷൻ അനുവദിക്കുക"), + ("Enable UDP hole punching", "UDP ഹോൾ പഞ്ചിംഗ് അനുവദിക്കുക"), + ("View camera", "ക്യാമറ കാണുക"), + ("Enable camera", "ക്യാമറ ഓൺ ചെയ്യുക"), + ("No cameras", "ക്യാമറകൾ കണ്ടെത്തിയില്ല"), + ("view_camera_unsupported_tip", "റിമോട്ട് ക്യാമറ പിന്തുണയ്ക്കുന്നില്ല."), + ("Terminal", "ടെർമിനൽ"), + ("Enable terminal", "ടെർമിനൽ അനുവദിക്കുക"), + ("New tab", "പുതിയ ടാബ്"), + ("Keep terminal sessions on disconnect", "വിച്ഛേദിക്കുമ്പോൾ ടെർമിനൽ സെഷൻ നിർത്തരുത്"), + ("Terminal (Run as administrator)", "ടെർമിനൽ (അഡ്മിനിസ്ട്രേറ്ററായി)"), + ("terminal-admin-login-tip", "അഡ്മിൻ ലോഗിൻ ആവശ്യമാണ്."), + ("Failed to get user token.", "യൂസർ ടോക്കൺ ലഭിക്കുന്നതിൽ പരാജയപ്പെട്ടു."), + ("Incorrect username or password.", "തെറ്റായ യൂസർ നെയിം അല്ലെങ്കിൽ പാസ്‌വേഡ്."), + ("The user is not an administrator.", "ഉപയോക്താവ് അഡ്മിനിസ്ട്രേറ്ററല്ല."), + ("Failed to check if the user is an administrator.", "അഡ്മിൻ ആണോ എന്ന് പരിശോധിക്കുന്നതിൽ പരാജയപ്പെട്ടു."), + ("Supported only in the installed version.", "ഇൻസ്റ്റാൾ ചെയ്ത പതിപ്പിൽ മാത്രം ലഭ്യം."), + ("elevation_username_tip", "അഡ്മിനിസ്ട്രേറ്റർ പേര് നൽകുക"), + ("Preparing for installation ...", "ഇൻസ്റ്റാളേഷനായി ഒരുങ്ങുന്നു..."), + ("Show my cursor", "എന്റെ കർസർ കാണിക്കുക"), + ("Scale custom", "കസ്റ്റം സ്കെയിൽ"), + ("Custom scale slider", "കസ്റ്റം സ്കെയിൽ സ്ലൈഡർ"), + ("Decrease", "കുറയ്ക്കുക"), + ("Increase", "കൂട്ടുക"), + ("Show virtual mouse", "വെർച്വൽ മൗസ് കാണിക്കുക"), + ("Virtual mouse size", "വെർച്വൽ മൗസ് വലിപ്പം"), + ("Small", "ചെറുത്"), + ("Large", "വലുത്"), + ("Show virtual joystick", "വെർച്വൽ ജോയ്സ്റ്റിക് കാണിക്കുക"), + ("Edit note", "കുറിപ്പ് മാറ്റുക"), + ("Alias", "ഏലിയാസ് (Alias)"), + ("ScrollEdge", "സ്ക്രോൾ എഡ്ജ്"), + ("Allow insecure TLS fallback", "സുരക്ഷിതമല്ലാത്ത TLS അനുവദിക്കുക"), + ("allow-insecure-tls-fallback-tip", "പഴയ സെർവറുകൾക്കായി ഉപയോഗിക്കുക."), + ("Disable UDP", "UDP ഒഴിവാക്കുക"), + ("disable-udp-tip", "കണക്ഷൻ പ്രശ്നങ്ങൾക്ക് UDP ഒഴിവാക്കുക."), + ("server-oss-not-support-tip", "OSS സെർവർ ഇത് പിന്തുണയ്ക്കുന്നില്ല."), + ("input note here", "ഇവിടെ കുറിപ്പ് എഴുതുക"), + ("note-at-conn-end-tip", "കണക്ഷൻ കഴിയുമ്പോൾ കുറിപ്പ് കാണിക്കുക"), + ("Show terminal extra keys", "ടെർമിനൽ കീകൾ കാണിക്കുക"), + ("Relative mouse mode", "റിലേറ്റീവ് മൗസ് മോഡ്"), + ("rel-mouse-not-supported-peer-tip", "മറുഭാഗം പിന്തുണയ്ക്കുന്നില്ല."), + ("rel-mouse-not-ready-tip", "തയ്യാറായിട്ടില്ല."), + ("rel-mouse-lock-failed-tip", "മൗസ് ലോക്ക് പരാജയപ്പെട്ടു."), + ("rel-mouse-exit-{}-tip", "പുറത്തുകടക്കാൻ {} അമർത്തുക"), + ("rel-mouse-permission-lost-tip", "അനുമതി നഷ്ടപ്പെട്ടു."), + ("Changelog", "മാറ്റങ്ങൾ (Changelog)"), + ("keep-awake-during-outgoing-sessions-label", "സെഷൻ നടക്കുമ്പോൾ ഉറക്കത്തിലാകരുത്"), + ("keep-awake-during-incoming-sessions-label", "സെഷൻ വരുമ്പോൾ ഉറക്കത്തിലാകരുത്"), + ("Continue with {}", "{} ഉപയോഗിച്ച് തുടരുക"), + ("Display Name", "ഡിസ്‌പ്ലേ പേര്"), + ("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."), + ("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/nb.rs b/vendor/rustdesk/src/lang/nb.rs new file mode 100644 index 0000000..5795b9e --- /dev/null +++ b/vendor/rustdesk/src/lang/nb.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Ditt skrivebord"), + ("desk_tip", "Du kan få adgang til ditt skrivebord med denne ID og passord."), + ("Password", "Passord"), + ("Ready", "Klar"), + ("Established", "Etablert"), + ("connecting_status", "Opretter tilkobling til RustDesk-nettverket..."), + ("Enable service", "Aktiver tjenesten"), + ("Start service", "Start tjenesten"), + ("Service is running", "Tjenesten kjører"), + ("Service is not running", " tilknyttede tjenesten kjører ikke"), + ("not_ready_status", "Ikke klar. Vennligst sjekk din tilkobling"), + ("Control Remote Desktop", "Kontroller fjernskrivebord"), + ("Transfer file", "Overfør fil"), + ("Connect", "Koble til"), + ("Recent sessions", "Siste sesjoner"), + ("Address book", "Adressebok"), + ("Confirmation", "Bekreftelse"), + ("TCP tunneling", "TCP tunnelering"), + ("Remove", "Fjern"), + ("Refresh random password", "Oppdater tilfeldig passord"), + ("Set your own password", "Sett ditt eget passord"), + ("Enable keyboard/mouse", "Aktiver tastatur/mus"), + ("Enable clipboard", "Aktiver utklipstavle"), + ("Enable file transfer", "Aktiver filoverførsel"), + ("Enable TCP tunneling", "Aktiver TCP-tunnelering"), + ("IP Whitelisting", "IP Hvitelisting"), + ("ID/Relay Server", "ID/Tilkoblingsserver"), + ("Import server config", "Importer serverkonfigurasjon"), + ("Export Server Config", "Eksporter serverkonfigurasjon"), + ("Import server configuration successfully", "Import av serverkonfigurasjonen var vellykket"), + ("Export server configuration successfully", "Eksport av serverkonfigurasjonen var vellykket"), + ("Invalid server configuration", "Ugyldig serverkonfigurasjon"), + ("Clipboard is empty", "Utklipstavlen er tom"), + ("Stop service", "Stopp tilkoblingsserveren"), + ("Change ID", "Endre ID"), + ("Your new ID", "Din nye ID"), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9, - (dash) og _ (understrek) er tillat. Den første bokstaven skal være a-z, A-Z. Lengde mellom 6 og 16."), + ("Website", "Hjemmeside"), + ("About", "Om"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Deaktiver mikrofonen"), + ("Build Date", ""), + ("Version", "Versjon"), + ("Home", "Hjem"), + ("Audio Input", "Lydinput"), + ("Enhancements", "Forbedringer"), + ("Hardware Codec", "Hardware-codec"), + ("Adaptive bitrate", "Adaptiv bitrate"), + ("ID Server", "ID Server"), + ("Relay Server", "Relay Server"), + ("API Server", "API Server"), + ("invalid_http", "Skal starte med http:// eller https://"), + ("Invalid IP", "Ugyldig IP-adresse"), + ("Invalid format", "Ugyldig format"), + ("server_not_support", "Enda ikke støttet av serveren"), + ("Not available", "Ikke tilgengelig"), + ("Too frequent", "For hyppig"), + ("Cancel", "Avbryt"), + ("Skip", "Hopp over"), + ("Close", "Lukk"), + ("Retry", "Prøv igjen"), + ("OK", "OK"), + ("Password Required", "passord påkrevd"), + ("Please enter your password", "Tast inn ditt passord"), + ("Remember password", "Husk passord"), + ("Wrong Password", "Feil passord"), + ("Do you want to enter again?", "Ønsker du å forsøke igen?"), + ("Connection Error", "Tilkoblingsfeil"), + ("Error", "Feil"), + ("Reset by the peer", "Nulstilt av motparten"), + ("Connecting...", "Opretter tilkobling..."), + ("Connection in progress. Please wait.", "Tilkobler. Vennligst vent."), + ("Please try 1 minute later", "Prøv igen om et minutt"), + ("Login Error", "Login feil"), + ("Successful", "Vellykket"), + ("Connected, waiting for image...", "Tilkoblet, venter på bilde..."), + ("Name", "Navn"), + ("Type", "Type"), + ("Modified", "Endret"), + ("Size", "Størrelse"), + ("Show Hidden Files", "Vis skjulte filer"), + ("Receive", "Motta"), + ("Send", "Send"), + ("Refresh File", "Oppdater fil"), + ("Local", "Lokalt"), + ("Remote", "Remote"), + ("Remote Computer", "fjerntilkoblet maskin"), + ("Local Computer", "Lokal maskin"), + ("Confirm Delete", "Bekreft sletting"), + ("Delete", "Slett"), + ("Properties", "Egenskaper"), + ("Multi Select", "Flere valg"), + ("Select All", "Velg alle"), + ("Unselect All", "Fravelg alle"), + ("Empty Directory", "Tomt bibliotek"), + ("Not an empty directory", "Ikke et tomt bibliotek"), + ("Are you sure you want to delete this file?", "Er du sikker på, at du vil slette denne filen?"), + ("Are you sure you want to delete this empty directory?", "Er du sikker på, at du vil slette dette tomme biblioteket?"), + ("Are you sure you want to delete the file of this directory?", "Er du sikker på, at du vil slette filen til dette biblioteket?"), + ("Do this for all conflicts", "Gjør dette for alle konflikter"), + ("This is irreversible!", "Dette kan ikke reverseres!"), + ("Deleting", "Sletter"), + ("files", "Filer"), + ("Waiting", "Venter"), + ("Finished", "Ferdig"), + ("Speed", "Hastighet"), + ("Custom Image Quality", "Brukerdefinert bildekvalitet"), + ("Privacy mode", "Privatlivsmodus"), + ("Block user input", "Blokker brukerinput"), + ("Unblock user input", "Fjern blokkering av brukerinput"), + ("Adjust Window", "Juster vinduet"), + ("Original", "Original"), + ("Shrink", "Krymp"), + ("Stretch", "Strekk ut"), + ("Scrollbar", "Rullebar"), + ("ScrollAuto", "Auto-rull"), + ("Good image quality", "God bildekvalitet"), + ("Balanced", "Balansert"), + ("Optimize reaction time", "Optimert responstid"), + ("Custom", "Tilpasset"), + ("Show remote cursor", "Vis fjernstyrt musepeker"), + ("Show quality monitor", "Vis bildekvalitet"), + ("Disable clipboard", "Deaktiver utklipstavle"), + ("Lock after session end", "Lås etter avsluttet fjernstyring"), + ("Insert Ctrl + Alt + Del", "Sett inn Ctrl + Alt + Del"), + ("Insert Lock", "Sett inn lås"), + ("Refresh", "Oppdater"), + ("ID does not exist", "ID finnes ikke"), + ("Failed to connect to rendezvous server", "tilkobling til serveren mislykktes"), + ("Please try later", "Prøv igjen senere"), + ("Remote desktop is offline", "Fjernskrivebord er offline"), + ("Key mismatch", "Nøkkel mismatch"), + ("Timeout", "Timeout"), + ("Failed to connect to relay server", "tilkobling til rele-serveren mislykktes"), + ("Failed to connect via rendezvous server", "tilkobling via Rendezvous-server mislykktes"), + ("Failed to connect via relay server", "tilkobling via rele-serveren mislykktes"), + ("Failed to make direct connection to remote desktop", "Direkte tilkobling til fjernskrivebord kunne ikke etableres"), + ("Set Password", "Sett passord"), + ("OS Password", "Operativsystempassord"), + ("install_tip", "På grunn av UAC kan RustDesk ikke fungere korrekt i enkelte tillfeller på fjernskrivebordet. For å unngå UAC klikker du på knappen nedenfor for å installere RustDesk på systemet"), + ("Click to upgrade", "Klikk for å oppgradere"), + ("Configure", "Konfigurer"), + ("config_acc", "For å kontrollere ditt skrivebord med fjernstyring må du gi RustDesk \"Access \" Rettigheter."), + ("config_screen", "For å kunne få adgang til ditt skrivebord med fjernstyring, må du gi RustDesk \"skjerstøtte \" tillatelser."), + ("Installing ...", "Installerer ..."), + ("Install", "installer"), + ("Installation", "Installasjon"), + ("Installation Path", "Installasjonssti"), + ("Create start menu shortcuts", "Oppret start meny snarvei"), + ("Create desktop icon", "Oppret skrivebords-snarvei"), + ("agreement_tip", "Hvis du starter installasjonen, må du akseptere lisensavtalen"), + ("Accept and Install", "Aksepter og installer"), + ("End-user license agreement", "Lisensavtale for sluttbrukere"), + ("Generating ...", "Genererer kode ..."), + ("Your installation is lower version.", "Din installasjon er en eldre versjon."), + ("not_close_tcp_tip", "Ikke lukk dette vinduet, mens du bruker tunnelen."), + ("Listening ...", "Lytter ..."), + ("Remote Host", "Fjern-Host"), + ("Remote Port", "Fjern-Port"), + ("Action", "Handling"), + ("Add", "Tilføy"), + ("Local Port", "Lokal Port"), + ("Local Address", "Lokal adresse"), + ("Change Local Port", "Skift lokal port"), + ("setup_server_tip", "For en hurtigere tilkobling må du bruke din egen tilkoblingsserver"), + ("Too short, at least 6 characters.", "For kort, bruk minst 6 tegn."), + ("The confirmation is not identical.", "bekreftelsen er ikke identisk."), + ("Permissions", "Tillatelser"), + ("Accept", "Aksepter"), + ("Dismiss", "Avvis"), + ("Disconnect", "Koble fra"), + ("Enable file copy and paste", "Tillat kopiering og innliming av filer"), + ("Connected", "Tilkoblet"), + ("Direct and encrypted connection", "Direkte og kryptert tilkobling"), + ("Relayed and encrypted connection", "Viderekoblet og kryptert tilkobling"), + ("Direct and unencrypted connection", "Direkte og ukryptert tilkobling"), + ("Relayed and unencrypted connection", "Viderekoblet og ukryptert tilkobling"), + ("Enter Remote ID", "Tast inn Remote-ID"), + ("Enter your password", "Skriv ditt passord"), + ("Logging in...", "Logger inn..."), + ("Enable RDP session sharing", "Aktiver RDP sesjonsgodkjennelse"), + ("Auto Login", "Automatisk login (kun gyldig hvis du har konfigurert \"Lås etter avslutting av sesjonen\")"), + ("Enable direct IP access", "Aktiver direkte IP-adgang"), + ("Rename", "Gi nytt navn"), + ("Space", "Plass"), + ("Create desktop shortcut", "Opprett skrivebords-snarvei"), + ("Change Path", "Skift sti"), + ("Create Folder", "Opprett mappe"), + ("Please enter the folder name", "Tast inn mappens navn"), + ("Fix it", "Kjør reparasjon"), + ("Warning", "Advarsel"), + ("Login screen using Wayland is not supported", "Login skjerm med Wayland støttes ikke"), + ("Reboot required", "Omstart kreves"), + ("Unsupported display server", "Ikke-understøttet displayserver"), + ("x11 expected", "X11 Forventet"), + ("Port", "Port"), + ("Settings", "Innstillinger"), + ("Username", " Brukernavn"), + ("Invalid port", "Ugyldig port"), + ("Closed manually by the peer", "Manuelt lukket av peer"), + ("Enable remote configuration modification", "Tillat fjernkonfigurering"), + ("Run without install", "Kjør uten installasjon"), + ("Connect via relay", "Koble til via viderekobling"), + ("Always connect via relay", "tilkobling via viderekoblings-server"), + ("whitelist_tip", "Kun IP'er på hvitelisten kan få adgang til meg"), + ("Login", "Logg inn"), + ("Verify", "Verifiser"), + ("Remember me", "Husk meg"), + ("Trust this device", "Husk denne enheten"), + ("Verification code", "Verifikasjonskode"), + ("verification_tip", ""), + ("Logout", "Logger av"), + ("Tags", "Tagger"), + ("Search ID", "Søk etter ID"), + ("whitelist_sep", "Adskilt etter komma, semikolon, mellemrom eller linjeskift"), + ("Add ID", "Legg til ID"), + ("Add Tag", "Legg til tagg"), + ("Unselect all tags", "Fravelg alle passord"), + ("Network error", "Nettverksfeil"), + ("Username missed", "Glemt brukernavn"), + ("Password missed", "Glemt passord"), + ("Wrong credentials", "Feil brukernavn og/eller passord"), + ("The verification code is incorrect or has expired", ""), + ("Edit Tag", "Rediger tagg"), + ("Forget Password", "Glem passord"), + ("Favorites", "Favoritter"), + ("Add to Favorites", "Legg til favoritter"), + ("Remove from Favorites", "Fjern favoritter"), + ("Empty", "Tom"), + ("Invalid folder name", "Ugyldig mappenavn"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "Oppdaget"), + ("install_daemon_tip", "For å starte når PC'en har startet opp, må du installere systemtjenesten"), + ("Remote ID", "Fjern-ID"), + ("Paste", "Sett inn"), + ("Paste here?", "Sett inn her?"), + ("Are you sure to close the connection?", "Er du sikker på at du vil lukke tilkoblingn?"), + ("Download new version", "Last ned ny versjon"), + ("Touch mode", "Touch-modus"), + ("Mouse mode", "Muse-modus"), + ("One-Finger Tap", "En-finger-trykk"), + ("Left Mouse", "Venstre mus"), + ("One-Long Tap", "Trykk og hold med en finger"), + ("Two-Finger Tap", "Trykk med to fingre"), + ("Right Mouse", "Høyre mus"), + ("One-Finger Move", "En-finger bevegelse"), + ("Double Tap & Move", "Dobbeltklikk og flytt"), + ("Mouse Drag", "Dra med musen"), + ("Three-Finger vertically", "Tre fingre lodrett"), + ("Mouse Wheel", "Musehjul"), + ("Two-Finger Move", "To-finger bevegelse"), + ("Canvas Move", "Flytt lerret"), + ("Pinch to Zoom", "Knip for å zoome inn"), + ("Canvas Zoom", "Lerret zoom"), + ("Reset canvas", "Nullstill lerret"), + ("No permission of file transfer", "Ingen tillatelse til å overføre filen"), + ("Note", "Notat"), + ("Connection", "Tilkobling"), + ("Share screen", "Del skjermen"), + ("Chat", "Chat"), + ("Total", "Total"), + ("items", "Objekter"), + ("Selected", "Valgte"), + ("Screen Capture", "Skjermopptak"), + ("Input Control", "Input kontroll"), + ("Audio Capture", "Lydopptak"), + ("Do you accept?", "Akepterer du?"), + ("Open System Setting", "Åpne systeminnstillinger"), + ("How to get Android input permission?", "Hvordan får jeg en Android-input tillatelse?"), + ("android_input_permission_tip1", "For at en ekstern enhet kan kontrollere din Android-enhet via mus eller berøring, må du gi RustDesk mulighet til å bruke tjenesten \"tilgjengelighet \"."), + ("android_input_permission_tip2", "Gå til den neste systeminnstillingssiden, søk og tast inn [installerte tjenester], aktiver [RustDesk Input] tjenesten."), + ("android_new_connection_tip", "En ny forespørsel ble mottatt, som ønsker å kontrollere din nåværende enhet."), + ("android_service_will_start_tip", "Ved å aktivere skjermopptak startes tjenesten automatisk, så andre enheter kan forespørre en tilkobling fra denne enheten."), + ("android_stop_service_tip", "Ved å lukke tjenesten lukkes alle tilkoblinger automatisk."), + ("android_version_audio_tip", "Den aktuelle Android-versjonen støtter ikke lydopptak. Android 10 eller nyere kreves."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), + ("Account", "Konto"), + ("Overwrite", "Overskriv"), + ("This file exists, skip or overwrite this file?", "Denne filen finnes allerede, vil du hoppe over eller overskrive denne filen?"), + ("Quit", "Avslutt"), + ("Help", "Hjelp"), + ("Failed", "Mislykket"), + ("Succeeded", "Vellykket"), + ("Someone turns on privacy mode, exit", "Noen aktiverte privatlivsmodus, avslutt"), + ("Unsupported", "Ikke støttet"), + ("Peer denied", "Motpart nektet"), + ("Please install plugins", "Installer plugins"), + ("Peer exit", "Motpart-Avslutt"), + ("Failed to turn off", "Klarte ikke å skru av"), + ("Turned off", "Avslått"), + ("Language", "Språk"), + ("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"), + ("Ignore Battery Optimizations", "Ignorer batteri optimalisering"), + ("android_open_battery_optimizations_tip", ""), + ("Start on boot", "Start under oppstart"), + ("Start the screen sharing service on boot, requires special permissions", "Start skjermdelingstjenesten under oppstart, krever spesielle tillatelser"), + ("Connection not allowed", "tilkobling ikke tillat"), + ("Legacy mode", "Tilbakekompatibilitetstilstand"), + ("Map mode", "Kartmodus"), + ("Translate mode", "Oversettelsesmodus"), + ("Use permanent password", "Bruk permanent passord"), + ("Use both passwords", "Bruk begge passord"), + ("Set permanent password", "Sett permanent passord"), + ("Enable remote restart", "Aktiver fjerngomstart"), + ("Restart remote device", "Restart fjernenhed"), + ("Are you sure you want to restart", "Er du sikker på at du vil restarte"), + ("Restarting remote device", "Restarter fjernenhet"), + ("remote_restarting_tip", "Enheten starter på nytt - Lukker denne beskjeden og kobler til igjen om et øyeblikk"), + ("Copied", "Kopiert"), + ("Exit Fullscreen", "Avslutt fullskjerm"), + ("Fullscreen", "Fullskjerm"), + ("Mobile Actions", "Mobile handlinger"), + ("Select Monitor", "velg skjerm"), + ("Control Actions", "Kontrollhandlinger"), + ("Display Settings", "Skjerminnstillinger"), + ("Ratio", "Forhold"), + ("Image Quality", "Bildekvalitet"), + ("Scroll Style", "Rullestil"), + ("Show Toolbar", "Vis Verktøylinje"), + ("Hide Toolbar", "Skjul Verktøylinje"), + ("Direct Connection", "Direkte tilkobling"), + ("Relay Connection", "Viderekoblet tilkobling"), + ("Secure Connection", "Sikker tilkobling"), + ("Insecure Connection", "Usikker tilkobling"), + ("Scale original", "Original skalering"), + ("Scale adaptive", "Adaptiv skalering"), + ("General", "Generelt"), + ("Security", "Sikkerhet"), + ("Theme", "Tema"), + ("Dark Theme", "Mørkt Tema"), + ("Light Theme", "Lyst Tema"), + ("Dark", "Mørk"), + ("Light", "Lys"), + ("Follow System", "Følg System"), + ("Enable hardware codec", "Aktiver hardware-codec"), + ("Unlock Security Settings", "Lås opp Sikkerhetsinnstillinger"), + ("Enable audio", "Aktiver Lyd"), + ("Unlock Network Settings", "Lås opp Nettverksinnstillinger"), + ("Server", "Server"), + ("Direct IP Access", "Direkte IP Adgang"), + ("Proxy", "Proxy"), + ("Apply", "Bruk"), + ("Disconnect all devices?", "Koble fra alle enheter?"), + ("Clear", "Nullstill"), + ("Audio Input Device", "Lydinngangsenhet"), + ("Use IP Whitelisting", "Bruk IP hvitelisting"), + ("Network", "Nettverk"), + ("Pin Toolbar", "Fest Hurtiglinje"), + ("Unpin Toolbar", "Avfest Hurtiglinje"), + ("Recording", "Opptak"), + ("Directory", "Mappe"), + ("Automatically record incoming sessions", "Ta opp innkommende sesjoner automatisk"), + ("Automatically record outgoing sessions", ""), + ("Change", "Rediger"), + ("Start session recording", "Start sesjonsopptak"), + ("Stop session recording", "Stopp sesjonsopptak"), + ("Enable recording session", "Aktiver opptakssesjon"), + ("Enable LAN discovery", "Aktiver LAN Discovery"), + ("Deny LAN discovery", "Avvis LAN Discovery"), + ("Write a message", "Skriv en beskjed"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Vennligst vent på UAC-bekreftelse..."), + ("elevated_foreground_window_tip", ""), + ("Disconnected", "Frakoblet"), + ("Other", "Andre"), + ("Confirm before closing multiple tabs", "Bekreft før du lukker flere faner"), + ("Keyboard Settings", "Tastaturinnstillinger"), + ("Full Access", "Full tilgang"), + ("Screen Share", "Skjermdeling"), + ("ubuntu-21-04-required", "Wayland krever Ubuntu version 21.04 eller nyere."), + ("wayland-requires-higher-linux-version", "Wayland krever en nyere versjon av Linux. Prøv X11 desktop eller skift OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "vennligst velg den skjermen, som skal deles (fjernstyres)."), + ("Show RustDesk", "Vis RustDesk"), + ("This PC", "Denne PC"), + ("or", "eller"), + ("Elevate", "Elever"), + ("Zoom cursor", "Zoom markør"), + ("Accept sessions via password", "Aksepter sesjoner via passord"), + ("Accept sessions via click", "Aksepter sesjoner via klikk"), + ("Accept sessions via both", "Aksepter sesjoner via begge"), + ("Please wait for the remote side to accept your session request...", "Vennligst vent på at fjernklienten aksepterer din sesjonsforespørsel..."), + ("One-time Password", "Engangskode"), + ("Use one-time password", "Bruk engangskode"), + ("One-time password length", "Engangskode lengde"), + ("Request access to your device", "Etterspør adgang til din enhet"), + ("Hide connection management window", "Skjul tilkoblingshåndteringsvinduet"), + ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), + ("Right click to select tabs", "Høyreklikk for å velge faner"), + ("Skipped", "Hoppet over"), + ("Add to address book", "Legg til adresseboken"), + ("Group", "Gruppe"), + ("Search", "Søk"), + ("Closed manually by web console", "Lukket ned manuelt av webkonsollet"), + ("Local keyboard type", "Lokal tastatur type"), + ("Select local keyboard type", "velg lokal tastatur type"), + ("software_render_tip", ""), + ("Always use software rendering", "Bruk alltid programvare rendering"), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", "Vent"), + ("Elevation Error", "Eleveringsfeil"), + ("Ask the remote user for authentication", "Spør fjernbrukeren om godkjennelse"), + ("Choose this if the remote account is administrator", "velg dette hvis fjernbrukeren er en administrator"), + ("Transmit the username and password of administrator", "Send brukernavnet og passordet for administrator"), + ("still_click_uac_tip", ""), + ("Request Elevation", "Etterspørr elevering"), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", "Elevering vellykket"), + ("uppercase", "store bokstaver"), + ("lowercase", "små bokstaver"), + ("digit", "siffer"), + ("special character", "spesialtegn"), + ("length>=8", "lengde>=8"), + ("Weak", "Svak"), + ("Medium", "Medium"), + ("Strong", "Sterk"), + ("Switch Sides", "Skift sider"), + ("Please confirm if you want to share your desktop?", "Bekreft at du ønsker å dele skrivebordet ditt?"), + ("Display", "Visning"), + ("Default View Style", "Standard visningsstil"), + ("Default Scroll Style", "Standard rulle stil"), + ("Default Image Quality", "Standard bildekvalitet"), + ("Default Codec", "Standard codec"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Andre standardinnstillinger"), + ("Voice call", "Stemmeoppkald"), + ("Text chat", "Tekstchat"), + ("Stop voice call", "Stopp stemmeoppkald"), + ("relay_hint_tip", ""), + ("Reconnect", "Gjenopprett"), + ("Codec", "Codec"), + ("Resolution", "Oppløsning"), + ("No transfers in progress", "Ingen aktive overførsler"), + ("Set one-time password length", "Sett engangspassord lengde"), + ("RDP Settings", "RDP innstillinger"), + ("Sort by", "Sorter etter"), + ("New Connection", "Ny tilkobling"), + ("Restore", "gjenopprett"), + ("Minimize", "Minimer"), + ("Maximize", "Maksimer"), + ("Your Device", "Din enhet"), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("Empty Username", "Tøm brukernavn"), + ("Empty Password", "Tøm passord"), + ("Me", "Meg"), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), + ("Select a peer", ""), + ("Select peers", ""), + ("Plugins", ""), + ("Uninstall", ""), + ("Update", ""), + ("Enable", ""), + ("Disable", ""), + ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), + ("Collapse toolbar", ""), + ("Accept and Elevate", ""), + ("accept_and_elevate_btn_tooltip", ""), + ("clipboard_wait_response_timeout_tip", ""), + ("Incoming connection", ""), + ("Outgoing connection", ""), + ("Exit", ""), + ("Open", ""), + ("logout_tip", ""), + ("Service", ""), + ("Start", ""), + ("Stop", ""), + ("exceed_max_devices", ""), + ("Sync with recent sessions", ""), + ("Sort tags", ""), + ("Open connection in new tab", ""), + ("Move tab to new window", ""), + ("Can not be empty", ""), + ("Already exists", ""), + ("Change Password", ""), + ("Refresh Password", ""), + ("ID", ""), + ("Grid View", ""), + ("List View", ""), + ("Select", ""), + ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), + ("synced_peer_readded_tip", ""), + ("Change Color", ""), + ("Primary Color", ""), + ("HSV Color", ""), + ("Installation Successful!", ""), + ("Installation failed!", ""), + ("Reverse mouse wheel", ""), + ("{} sessions", ""), + ("scam_title", ""), + ("scam_text1", ""), + ("scam_text2", ""), + ("Don't show again", ""), + ("I Agree", ""), + ("Decline", ""), + ("Timeout in minutes", ""), + ("auto_disconnect_option_tip", ""), + ("Connection failed due to inactivity", ""), + ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), + ("pull_group_failed_tip", ""), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), + ("idd_not_support_under_win10_2004_tip", ""), + ("input_source_1_tip", ""), + ("input_source_2_tip", ""), + ("Swap control-command key", ""), + ("swap-left-right-mouse", ""), + ("2FA code", ""), + ("More", ""), + ("enable-2fa-title", ""), + ("enable-2fa-desc", ""), + ("wrong-2fa-code", ""), + ("enter-2fa-title", ""), + ("Email verification code must be 6 characters.", ""), + ("2FA code must be 6 digits.", ""), + ("Multiple Windows sessions found", ""), + ("Please select the session you want to connect to", ""), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("share_warning_tip", ""), + ("Everyone", ""), + ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", ""), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", ""), + ("Floating window", ""), + ("floating_window_tip", ""), + ("Keep screen on", ""), + ("Never", ""), + ("During controlled", ""), + ("During service is on", ""), + ("Capture screen using DirectX", ""), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Vis kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Fortsett med {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/nl.rs b/vendor/rustdesk/src/lang/nl.rs new file mode 100644 index 0000000..833c947 --- /dev/null +++ b/vendor/rustdesk/src/lang/nl.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Uw Bureaublad"), + ("desk_tip", "Uw bureaublad is toegankelijk met dit ID en wachtwoord."), + ("Password", "Wachtwoord"), + ("Ready", "Klaar"), + ("Established", "Opgezet"), + ("connecting_status", "Verbinding maken met het RustDesk netwerk..."), + ("Enable service", "Service inschakelen"), + ("Start service", "Start service"), + ("Service is running", "De service loopt."), + ("Service is not running", "De service loopt niet"), + ("not_ready_status", "Niet verbonden met de server, controleer de netwerkverbinding"), + ("Control Remote Desktop", "Beheer Extern Bureaublad"), + ("Transfer file", "Bestand overzetten"), + ("Connect", "Verbinden"), + ("Recent sessions", "Recente sessies"), + ("Address book", "Adresboek"), + ("Confirmation", "Bevestiging"), + ("TCP tunneling", "TCP-tunneling"), + ("Remove", "Verwijder"), + ("Refresh random password", "Vernieuw willekeurig wachtwoord"), + ("Set your own password", "Stel uw eigen wachtwoord in"), + ("Enable keyboard/mouse", "Toetsenbord/muis inschakelen"), + ("Enable clipboard", "Klembord inschakelen"), + ("Enable file transfer", "Bestandsoverdracht inschakelen"), + ("Enable TCP tunneling", "TCP-tunneling inschakelen"), + ("IP Whitelisting", "IP Witte Lijst"), + ("ID/Relay Server", "ID-/Relayserver"), + ("Import server config", "Importeer serverconfiguratie"), + ("Export Server Config", "Exporteer serverconfiguratie"), + ("Import server configuration successfully", "Importeren serverconfiguratie is geslaagd"), + ("Export server configuration successfully", "Exporteren serverconfiguratie is geslaagd"), + ("Invalid server configuration", "Ongeldige serverconfiguratie"), + ("Clipboard is empty", "Klembord is leeg"), + ("Stop service", "Stop service"), + ("Change ID", "Wijzig ID"), + ("Your new ID", "Uw nieuwe ID"), + ("length %min% to %max%", "lengte %min% tot %max%"), + ("starts with a letter", "begint met een letter"), + ("allowed characters", "toegestane tekens"), + ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, - (dash), _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), + ("Website", "Website"), + ("About", "Over"), + ("Slogan_tip", "Met hart en ziel gemaakt in deze chaotische wereld!"), + ("Privacy Statement", "Privacyverklaring"), + ("Mute", "Geluid uit"), + ("Build Date", "Datum"), + ("Version", "Versie"), + ("Home", "Startpagina"), + ("Audio Input", "Audioingang"), + ("Enhancements", "Verbeteringen"), + ("Hardware Codec", "Hardwarecodec"), + ("Adaptive bitrate", "Bitrate automatisch aanpassen"), + ("ID Server", "ID-server"), + ("Relay Server", "Relay-server"), + ("API Server", "API-server"), + ("invalid_http", "Moet beginnen met http:// of https://"), + ("Invalid IP", "Ongeldig IP"), + ("Invalid format", "Ongeldig formaat"), + ("server_not_support", "Nog niet ondersteund door de server"), + ("Not available", "Niet beschikbaar"), + ("Too frequent", "Te vaak"), + ("Cancel", "Annuleer"), + ("Skip", "Overslaan"), + ("Close", "Sluit"), + ("Retry", "Probeer opnieuw"), + ("OK", "OK"), + ("Password Required", "Wachtwoord Vereist"), + ("Please enter your password", "Geef uw wachtwoord in"), + ("Remember password", "Wachtwoord onthouden"), + ("Wrong Password", "Verkeerd Wachtwoord"), + ("Do you want to enter again?", "Wilt u het opnieuw invoeren?"), + ("Connection Error", "Fout bij verbinding"), + ("Error", "Fout"), + ("Reset by the peer", "Door de peer gereset"), + ("Connecting...", "Verbinding maken..."), + ("Connection in progress. Please wait.", "Verbinding wordt gemaakt. Even geduld a.u.b."), + ("Please try 1 minute later", "Probeer 1 minuut later"), + ("Login Error", "Loginfout"), + ("Successful", "Geslaagd"), + ("Connected, waiting for image...", "Verbonden, wacht op beeld..."), + ("Name", "Naam"), + ("Type", "Type"), + ("Modified", "Gewijzigd"), + ("Size", "Grootte"), + ("Show Hidden Files", "Toon Verborgen Bestanden"), + ("Receive", "Ontvang"), + ("Send", "Verzend"), + ("Refresh File", "Bestand Verversen"), + ("Local", "Lokaal"), + ("Remote", "Op Afstand"), + ("Remote Computer", "Externe Computer"), + ("Local Computer", "Lokale Computer"), + ("Confirm Delete", "Bevestig Verwijderen"), + ("Delete", "Verwijder"), + ("Properties", "Eigenschappen"), + ("Multi Select", "Meervoudig Selecteren"), + ("Select All", "Selecteer Alle"), + ("Unselect All", "De-selecteer Alle"), + ("Empty Directory", "Lege Map"), + ("Not an empty directory", "Geen lege map"), + ("Are you sure you want to delete this file?", "Weet u zeker dat u dit bestand wilt verwijderen?"), + ("Are you sure you want to delete this empty directory?", "Weet u zeker dat u deze lege map wilt verwijderen?"), + ("Are you sure you want to delete the file of this directory?", "Weet u zeker dat u de bestanden uit deze map wilt verwijderen?"), + ("Do this for all conflicts", "Doe dit voor alle conflicten"), + ("This is irreversible!", "Dit is onomkeerbaar!"), + ("Deleting", "Verwijderen"), + ("files", "bestanden"), + ("Waiting", "Wachten"), + ("Finished", "Voltooid"), + ("Speed", "Snelheid"), + ("Custom Image Quality", "Aangepaste Beeldkwaliteit"), + ("Privacy mode", "Privacymodus"), + ("Block user input", "Gebruikersinvoer blokkeren"), + ("Unblock user input", "Gebruikersinvoer deblokkeren"), + ("Adjust Window", "Venster Aanpassen"), + ("Original", "Origineel"), + ("Shrink", "Verkleinen"), + ("Stretch", "Uitrekken"), + ("Scrollbar", "Schuifbalk"), + ("ScrollAuto", "Automatisch schuiven"), + ("Good image quality", "Goede beeldkwaliteit"), + ("Balanced", "Gebalanceerd"), + ("Optimize reaction time", "Optimaliseer reactietijd"), + ("Custom", "Aangepast"), + ("Show remote cursor", "Toon cursor van extern bureaublad"), + ("Show quality monitor", "Kwaliteitsmonitor tonen"), + ("Disable clipboard", "Klembord uitschakelen"), + ("Lock after session end", "Vergrendelen na einde sessie"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Invoeren"), + ("Insert Lock", "Vergrendelen"), + ("Refresh", "Vernieuwen"), + ("ID does not exist", "ID bestaat niet"), + ("Failed to connect to rendezvous server", "Verbinding met rendez-vous-server mislukt"), + ("Please try later", "Probeer later opnieuw"), + ("Remote desktop is offline", "Extern bureaublad is offline"), + ("Key mismatch", "Code onjuist"), + ("Timeout", "Time-out"), + ("Failed to connect to relay server", "Verbinden met relayserver mislukt"), + ("Failed to connect via rendezvous server", "Verbinden via rendez-vous-server mislukt"), + ("Failed to connect via relay server", "Verbinden via relaisserver mislukt"), + ("Failed to make direct connection to remote desktop", "Direct verbinden met extern bureaublad is mislukt"), + ("Set Password", "Wachtwoord Instellen"), + ("OS Password", "OS Wachtwoord"), + ("install_tip", "Door UAC-beperkingen lukt het niet altijd om uw bureaublad op afstand te bedienen. Installeer RustDesk op het systeem om dit probleem te voorkomen."), + ("Click to upgrade", "Klik voor upgrade"), + ("Configure", "Configureren"), + ("config_acc", "Om uw apparaat op afstand te kunnen bedienen, moet u RustDesk toestemming voor Toegankelijkheid geven."), + ("config_screen", "Om uw apparaat op afstand te kunnen bedienen, moet u RustDesk toestemming voor Schermopname geven."), + ("Installing ...", "Installeren ..."), + ("Install", "Installeer"), + ("Installation", "Installatie"), + ("Installation Path", "Locatie"), + ("Create start menu shortcuts", "Startmenu-snelkoppelingen maken"), + ("Create desktop icon", "Bureaubladpictogram maken"), + ("agreement_tip", "Het starten van de installatie betekent het accepteren van de licentieovereenkomst."), + ("Accept and Install", "Accepteren en installeren"), + ("End-user license agreement", "Licentieovereenkomst eindgebruiker"), + ("Generating ...", "Genereert ..."), + ("Your installation is lower version.", "Uw installatie is een lagere versie."), + ("not_close_tcp_tip", "Sluit dit venster niet zolang u de tunnel gebruikt"), + ("Listening ...", "Luistert ..."), + ("Remote Host", "Externe Host"), + ("Remote Port", "Externe Poort"), + ("Action", "Actie"), + ("Add", "Toevoegen"), + ("Local Port", "Lokale Poort"), + ("Local Address", "Lokaal Adres"), + ("Change Local Port", "Wijzig Lokale Poort"), + ("setup_server_tip", "Als u een hogere verbindingssnelheid nodig heeft, kunt u ervoor kiezen om uw eigen server aan te maken"), + ("Too short, at least 6 characters.", "Te kort, minstens 6 tekens."), + ("The confirmation is not identical.", "De bevestiging is niet identiek."), + ("Permissions", "Machtigingen"), + ("Accept", "Accepteren"), + ("Dismiss", "Afwijzen"), + ("Disconnect", "Verbinding verbreken"), + ("Enable file copy and paste", "Kopiëren en plakken van bestanden toestaan"), + ("Connected", "Verbonden"), + ("Direct and encrypted connection", "Directe en versleutelde verbinding"), + ("Relayed and encrypted connection", "Doorgeschakelde en versleutelde verbinding"), + ("Direct and unencrypted connection", "Directe en niet-versleutelde verbinding"), + ("Relayed and unencrypted connection", "Doorgeschakelde en niet-versleutelde verbinding"), + ("Enter Remote ID", "Voer Extern ID in"), + ("Enter your password", "Voer uw wachtwoord in"), + ("Logging in...", "Aanmelden..."), + ("Enable RDP session sharing", "Delen van RDP-sessie inschakelen"), + ("Auto Login", "Automatisch Aanmelden"), + ("Enable direct IP access", "Directe IP-toegang inschakelen"), + ("Rename", "Naam wijzigen"), + ("Space", "Spatie"), + ("Create desktop shortcut", "Snelkoppeling op bureaublad maken"), + ("Change Path", "Pad Wijzigen"), + ("Create Folder", "Map Maken"), + ("Please enter the folder name", "Geef de mapnaam op"), + ("Fix it", "Repareer"), + ("Warning", "Waarschuwing"), + ("Login screen using Wayland is not supported", "Aanmeldingsscherm via Wayland wordt niet ondersteund"), + ("Reboot required", "Opnieuw opstarten vereist"), + ("Unsupported display server", "Niet-ondersteunde weergaveserver"), + ("x11 expected", "x11 verwacht"), + ("Port", "Poort"), + ("Settings", "Instellingen"), + ("Username", "Gebruiker"), + ("Invalid port", "Ongeldige poort"), + ("Closed manually by the peer", "Handmatig gesloten door de peer"), + ("Enable remote configuration modification", "Configuratiewijziging op afstand inschakelen"), + ("Run without install", "Uitvoeren zonder installatie"), + ("Connect via relay", "Verbinden via relay"), + ("Always connect via relay", "Altijd verbinden via relay"), + ("whitelist_tip", "Alleen IP-adressen op de witte lijst krijgen toegang tot mijn toestel"), + ("Login", "Log In"), + ("Verify", "Controleer"), + ("Remember me", "Herinner mij"), + ("Trust this device", "Vertrouw dit apparaat"), + ("Verification code", "Verificatiecode"), + ("verification_tip", "Er is een verificatiecode naar het geregistreerde e-mailadres gestuurd, voer de verificatiecode in om de verbinding voort te zetten."), + ("Logout", "Log Uit"), + ("Tags", "Labels"), + ("Search ID", "Zoek ID"), + ("whitelist_sep", "Gescheiden door komma, puntkomma, spatie of nieuwe regel"), + ("Add ID", "ID Toevoegen"), + ("Add Tag", "Label Toevoegen"), + ("Unselect all tags", "Alle labels verwijderen"), + ("Network error", "Netwerkfout"), + ("Username missed", "Gebruikersnaam gemist"), + ("Password missed", "Wachtwoord vergeten"), + ("Wrong credentials", "Verkeerde inloggegevens"), + ("The verification code is incorrect or has expired", "De verificatiecode is onjuist of verlopen"), + ("Edit Tag", "Label Bewerken"), + ("Forget Password", "Wachtwoord vergeten"), + ("Favorites", "Favorieten"), + ("Add to Favorites", "Toevoegen aan Favorieten"), + ("Remove from Favorites", "Verwijderen uit Favorieten"), + ("Empty", "Leeg"), + ("Invalid folder name", "Ongeldige mapnaam"), + ("Socks5 Proxy", "SOCKS5 Proxy"), + ("Socks5/Http(s) Proxy", "SOCKS5/HTTP(S) Proxy"), + ("Discovered", "Ontdekt"), + ("install_daemon_tip", "Om te starten bij het opstarten van de computer, moet u de systeemservice installeren."), + ("Remote ID", "Extern ID"), + ("Paste", "Plakken"), + ("Paste here?", "Hier plakken?"), + ("Are you sure to close the connection?", "Weet u zeker dat u de verbinding wilt sluiten?"), + ("Download new version", "Download nieuwe versie"), + ("Touch mode", "Aanraakmodus"), + ("Mouse mode", "Muismodus"), + ("One-Finger Tap", "Een-Vinger Tik"), + ("Left Mouse", "Linkermuis"), + ("One-Long Tap", "Een-Vinger-Lange-Tik"), + ("Two-Finger Tap", "Twee-Vingers-Tik"), + ("Right Mouse", "Rechtermuis"), + ("One-Finger Move", "Een-Vinger-Verplaatsing"), + ("Double Tap & Move", "Dubbel-Tik en Verplaatsen"), + ("Mouse Drag", "Muis Slepen"), + ("Three-Finger vertically", "Drie-Vinger verticaal"), + ("Mouse Wheel", "Muiswiel"), + ("Two-Finger Move", "Twee-Vingers Verplaatsen"), + ("Canvas Move", "Canvas Verplaatsen"), + ("Pinch to Zoom", "Knijp om te Zoomen"), + ("Canvas Zoom", "Canvas Zoom"), + ("Reset canvas", "Reset canvas"), + ("No permission of file transfer", "Geen toestemming voor bestandsoverdracht"), + ("Note", "Opmerking"), + ("Connection", "Verbinding"), + ("Share screen", "Scherm Delen"), + ("Chat", "Chat"), + ("Total", "Totaal"), + ("items", "items"), + ("Selected", "Geselecteerd"), + ("Screen Capture", "Schermopname"), + ("Input Control", "Invoercontrole"), + ("Audio Capture", "Audio Opnemen"), + ("Do you accept?", "Geeft u toestemming?"), + ("Open System Setting", "Systeeminstelling Openen"), + ("How to get Android input permission?", "Hoe krijg ik Android invoer toestemming?"), + ("android_input_permission_tip1", "Om ervoor te zorgen dat een extern apparaat uw Android-apparaat kan besturen via muis of aanraking, moet u RustDesk toestaan om de \"Toegankelijkheid\" service te gebruiken."), + ("android_input_permission_tip2", "Ga naar de volgende pagina met systeeminstellingen, zoek en ga naar [Geïnstalleerde Services], schakel de service [RustDesk Input] in."), + ("android_new_connection_tip", "Er is een nieuw controleverzoek binnengekomen, dat uw huidige apparaat wil controleren."), + ("android_service_will_start_tip", "Als u \"Schermopname\" inschakelt, wordt de service automatisch gestart, zodat andere apparaten een verbinding met uw apparaat kunnen aanvragen."), + ("android_stop_service_tip", "Het sluiten van de service zal automatisch alle gemaakte verbindingen sluiten."), + ("android_version_audio_tip", "De huidige versie van Android ondersteunt geen audio-opname, upgrade naar Android 10 of hoger."), + ("android_start_service_tip", "Druk op [Start service] of activeer de autorisatie [Scherm opnemen] om de schermdelingsservice te starten."), + ("android_permission_may_not_change_tip", "Toestemmingen voor tot stand gebrachte verbindingen kunnen niet onmiddellijk worden gewijzigd totdat er opnieuw verbinding wordt gemaakt."), + ("Account", "Account"), + ("Overwrite", "Overschrijven"), + ("This file exists, skip or overwrite this file?", "Dit bestand bestaat reeds, overslaan of overschrijven?"), + ("Quit", "Afsluiten"), + ("Help", "Help"), + ("Failed", "Mislukt"), + ("Succeeded", "Geslaagd"), + ("Someone turns on privacy mode, exit", "Iemand schakelt privacymodus in, afsluiten"), + ("Unsupported", "Niet Ondersteund"), + ("Peer denied", "Peer geweigerd"), + ("Please install plugins", "Installeer plugins"), + ("Peer exit", "Peer afgesloten"), + ("Failed to turn off", "Uitschakelen mislukt"), + ("Turned off", "Uitgeschakeld"), + ("Language", "Taal"), + ("Keep RustDesk background service", "RustDesk achtergronddienst behouden"), + ("Ignore Battery Optimizations", "Negeer Batterij-optimalisaties"), + ("android_open_battery_optimizations_tip", "Ga naar de volgende pagina met instellingen"), + ("Start on boot", "Starten bij Opstarten"), + ("Start the screen sharing service on boot, requires special permissions", "Start de schermdelingsservice bij het opstarten, vereist speciale rechten"), + ("Connection not allowed", "Verbinding niet toegestaan"), + ("Legacy mode", "Legacymodus"), + ("Map mode", "Mapmodus"), + ("Translate mode", "Vertaalmodus"), + ("Use permanent password", "Gebruik permanent wachtwoord"), + ("Use both passwords", "Gebruik beide wachtwoorden"), + ("Set permanent password", "Stel permanent wachtwoord in"), + ("Enable remote restart", "Herstart op afstand inschakelen"), + ("Restart remote device", "Apparaat op afstand herstarten"), + ("Are you sure you want to restart", "Weet u zeker dat u wilt herstarten"), + ("Restarting remote device", "Apparaat op afstand herstarten"), + ("remote_restarting_tip", "Apparaat op afstand wordt opnieuw opgestart, sluit dit bericht en maak na een ogenblik opnieuw verbinding met het permanente wachtwoord."), + ("Copied", "Gekopieerd"), + ("Exit Fullscreen", "Volledig Scherm sluiten"), + ("Fullscreen", "Volledig Scherm"), + ("Mobile Actions", "Mobiele Acties"), + ("Select Monitor", "Selecteer Monitor"), + ("Control Actions", "Controleacties"), + ("Display Settings", "Beeldscherminstellingen"), + ("Ratio", "Verhouding"), + ("Image Quality", "Beeldkwaliteit"), + ("Scroll Style", "Scroll Stijl"), + ("Show Toolbar", "Werkbalk Weergeven"), + ("Hide Toolbar", "Verberg Werkbalk"), + ("Direct Connection", "Directe Verbinding"), + ("Relay Connection", "Relaisverbinding"), + ("Secure Connection", "Beveiligde Verbinding"), + ("Insecure Connection", "Onveilige Verbinding"), + ("Scale original", "Oorspronkelijk formaat"), + ("Scale adaptive", "Automatisch schalen"), + ("General", "Algemeen"), + ("Security", "Beveiliging"), + ("Theme", "Thema"), + ("Dark Theme", "Donker Thema"), + ("Light Theme", "Licht Thema"), + ("Dark", "Donker"), + ("Light", "Licht"), + ("Follow System", "Volg systeem"), + ("Enable hardware codec", "Hardwarecodec inschakelen"), + ("Unlock Security Settings", "Beveiligingsinstellingen vrijgeven"), + ("Enable audio", "Audio inschakelen"), + ("Unlock Network Settings", "Netwerkinstellingen Vrijgeven"), + ("Server", "Server"), + ("Direct IP Access", "Directe IP toegang"), + ("Proxy", "Proxy"), + ("Apply", "Toepassen"), + ("Disconnect all devices?", "Alle apparaten uitschakelen?"), + ("Clear", "Wis"), + ("Audio Input Device", "Audio-invoerapparaat"), + ("Use IP Whitelisting", "Gebruik een witte lijst van IP-adressen"), + ("Network", "Netwerk"), + ("Pin Toolbar", "Werkbalk Vastzetten"), + ("Unpin Toolbar", "Werkbalk Losmaken"), + ("Recording", "Opnemen"), + ("Directory", "Map"), + ("Automatically record incoming sessions", "Inkomende sessies automatisch opnemen"), + ("Automatically record outgoing sessions", "Uitgaande sessies automatisch opnemen"), + ("Change", "Aanpassen"), + ("Start session recording", "Start de sessieopname"), + ("Stop session recording", "Stop de sessieopname"), + ("Enable recording session", "Sessieopname activeren"), + ("Enable LAN discovery", "LAN-detectie inschakelen"), + ("Deny LAN discovery", "LAN-detectie weigeren"), + ("Write a message", "Schrijf een bericht"), + ("Prompt", "Melding"), + ("Please wait for confirmation of UAC...", "Wacht op bevestiging van UAC..."), + ("elevated_foreground_window_tip", "Het momenteel geopende venster van de op afstand bediende computer vereist hogere rechten. Daarom is het momenteel niet mogelijk de muis en het toetsenbord te gebruiken. Vraag de gebruiker wiens computer u op afstand bedient om het venster te minimaliseren of de rechten te verhogen. Om dit probleem in de toekomst te voorkomen, wordt aanbevolen de software te installeren op de op afstand bediende computer."), + ("Disconnected", "Afgesloten"), + ("Other", "Andere"), + ("Confirm before closing multiple tabs", "Bevestig voordat u meerdere tabbladen sluit"), + ("Keyboard Settings", "Toetsenbordinstellingen"), + ("Full Access", "Volledige Toegang"), + ("Screen Share", "Scherm Delen"), + ("ubuntu-21-04-required", "Wayland vereist Ubuntu 21.04 of hoger."), + ("wayland-requires-higher-linux-version", "Wayland vereist een hogere versie van Linux distro. Probeer X11 desktop of verander van OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Selecteer het scherm dat moet worden gedeeld (Bediening aan de kant van de peer)."), + ("Show RustDesk", "Toon RustDesk"), + ("This PC", "Deze PC"), + ("or", "of"), + ("Elevate", "Verhoog"), + ("Zoom cursor", "Zoom cursor"), + ("Accept sessions via password", "Sessies accepteren via wachtwoord"), + ("Accept sessions via click", "Sessies accepteren via klik"), + ("Accept sessions via both", "Accepteer sessies via klik of wachtwoord"), + ("Please wait for the remote side to accept your session request...", "Wacht tot de andere kant uw sessieverzoek accepteert..."), + ("One-time Password", "Eenmalig Wachtwoord"), + ("Use one-time password", "Gebruik een eenmalig wachtwoord"), + ("One-time password length", "Lengte eenmalig wachtwoord"), + ("Request access to your device", "Toegang tot uw toestel aanvragen"), + ("Hide connection management window", "Verberg het venster voor verbindingsbeheer"), + ("hide_cm_tip", "Dit kan alleen als de toegang via een permanent wachtwoord verloopt."), + ("wayland_experiment_tip", "Wayland ondersteuning is slechts experimenteel. Gebruik alstublieft X11 als u onbeheerde toegang nodig heeft."), + ("Right click to select tabs", "Rechts klikken om tabbladen te selecteren"), + ("Skipped", "Overgeslagen"), + ("Add to address book", "Toevoegen aan Adresboek"), + ("Group", "Groep"), + ("Search", "Zoek"), + ("Closed manually by web console", "Handmatig gesloten door webconsole"), + ("Local keyboard type", "Lokaal toetsenbord"), + ("Select local keyboard type", "Selecteer lokaal toetsenbord"), + ("software_render_tip", "Als u een NVIDIA grafische kaart hebt en het externe venster sluit onmiddellijk na verbinding, kan het helpen om het nieuwe stuurprogramma te installeren en te kiezen voor software rendering. Een software herstart is vereist."), + ("Always use software rendering", "Gebruik altijd software rendering"), + ("config_input", "Om een extern apparaat met uw toetsenbord te kunnen bedienen, moet u RustDesk toestemming voor Invoer Vastleggen geven."), + ("config_microphone", "Om te kunnen chatten moet u RustDesk toestemming voor Microfoon geven."), + ("request_elevation_tip", "U kunt ook meer rechten vragen als iemand aan de andere kant aanwezig is."), + ("Wait", "Wacht"), + ("Elevation Error", "Verhogingsfout"), + ("Ask the remote user for authentication", "Vraag de gebruiker op afstand om bevestiging"), + ("Choose this if the remote account is administrator", "Kies dit als het externe account de beheerder is"), + ("Transmit the username and password of administrator", "Verzend de gebruikersnaam en het wachtwoord van de beheerder"), + ("still_click_uac_tip", "De gebruiker op afstand moet altijd bevestigen via het UAC-venster van de werkende RustDesk."), + ("Request Elevation", "Verzoek om meer rechten"), + ("wait_accept_uac_tip", "Wacht tot de gebruiker op afstand het UAC-dialoogvenster accepteert."), + ("Elevate successfully", "Succesvolle verhoging van privileges"), + ("uppercase", "Hoofdletter"), + ("lowercase", "kleine letter"), + ("digit", "cijfer"), + ("special character", "speciaal teken"), + ("length>=8", "lengte>=8"), + ("Weak", "Zwak"), + ("Medium", "Middelmatig"), + ("Strong", "Sterk"), + ("Switch Sides", "Wissel van kant"), + ("Please confirm if you want to share your desktop?", "Bevestig dat u uw bureaublad wilt delen?"), + ("Display", "Weergave"), + ("Default View Style", "Standaard Weergavestijl"), + ("Default Scroll Style", "Standaard Scrollstijl"), + ("Default Image Quality", "Standaard Beeldkwaliteit"), + ("Default Codec", "Standaard Codec"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Overige Standaardinstellingen"), + ("Voice call", "Spraakoproep"), + ("Text chat", "Tekstchat"), + ("Stop voice call", "Stop spraakoproep"), + ("relay_hint_tip", "Indien een directe verbinding niet mogelijk is, kunt u proberen verbinding te maken via een Relay Server.\nAls u bij de eerste poging een relaisverbinding tot stand wilt brengen, kunt u het achtervoegsel \"/r\" toevoegen aan het ID of de optie \"Altijd verbinden via relaisserver\" selecteren op de externe terminal."), + ("Reconnect", "Opnieuw verbinden"), + ("Codec", "Codec"), + ("Resolution", "Resolutie"), + ("No transfers in progress", "Geen overdrachten in uitvoering"), + ("Set one-time password length", "Stel de lengte van het eenmalige wachtwoord in"), + ("RDP Settings", "RDP Instellingen"), + ("Sort by", "Sorteren op"), + ("New Connection", "Nieuwe Verbinding"), + ("Restore", "Herstel"), + ("Minimize", "Minimaliseren"), + ("Maximize", "Maximaliseren"), + ("Your Device", "Uw Apparaat"), + ("empty_recent_tip", "Oeps, geen recente sessies!\nTijd om een nieuwe te plannen."), + ("empty_favorite_tip", "Nog geen favoriete stations op afstand? Laat ons iemand vinden om mee te verbinden en voeg hem toe aan uw favorieten!"), + ("empty_lan_tip", "Oh nee, het lijkt erop dat we nog geen extern station hebben ontdekt."), + ("empty_address_book_tip", "Oh jee, het lijkt erop dat er momenteel geen externe stations in uw adresboek staan."), + ("Empty Username", "Gebruikersnaam Leeg"), + ("Empty Password", "Wachtwoord Leeg"), + ("Me", "Ik"), + ("identical_file_tip", "Dit bestand is identiek aan het bestand van het externe station."), + ("show_monitors_tip", "Monitoren weergeven in de werkbalk"), + ("View Mode", "Toeschouwermodus"), + ("login_linux_tip", "Toegang tot het externe Linux-account"), + ("verify_rustdesk_password_tip", "Bevestiging wachtwoord RustDesk"), + ("remember_account_tip", "Herinner dit account"), + ("os_account_desk_tip", "Dit account wordt gebruikt om toegang te krijgen tot het externe besturingssysteem en de bureaubladsessie in onbeheerde modus te activeren."), + ("OS Account", "Besturingssysteem account"), + ("another_user_login_title_tip", "Een andere gebruiker is al ingelogd."), + ("another_user_login_text_tip", "Afzonderlijk"), + ("xorg_not_found_title_tip", "Xorg niet gevonden."), + ("xorg_not_found_text_tip", "Installeer Xorg."), + ("no_desktop_title_tip", "Er is geen desktop beschikbaar."), + ("no_desktop_text_tip", "Installeer de GNOME desktop."), + ("No need to elevate", "Niet nodig om te verhogen"), + ("System Sound", "Systeemgeluid"), + ("Default", "Standaard"), + ("New RDP", "Nieuwe RDP"), + ("Fingerprint", "Vingerafdruk"), + ("Copy Fingerprint", "Kopieer Vingerafdruk"), + ("no fingerprints", "geen vingerafdrukken"), + ("Select a peer", "Selecteer een peer"), + ("Select peers", "Selecteer peers"), + ("Plugins", "Plugins"), + ("Uninstall", "Verwijder"), + ("Update", "Bijwerken"), + ("Enable", "Activeer"), + ("Disable", "Deactiveer"), + ("Options", "Opties"), + ("resolution_original_tip", "Oorspronkelijke resolutie"), + ("resolution_fit_local_tip", "Lokale resolutie aanpassen"), + ("resolution_custom_tip", "Aangepaste resolutie"), + ("Collapse toolbar", "Werkbalk samenvouwen"), + ("Accept and Elevate", "Accepteren en Verheffen"), + ("accept_and_elevate_btn_tooltip", "Accepteer de verbinding en verhoog de UAC-machtigingen."), + ("clipboard_wait_response_timeout_tip", "Time-out in afwachting van kopieer-antwoord."), + ("Incoming connection", "Inkomende verbinding"), + ("Outgoing connection", "Uitgaande verbinding"), + ("Exit", "Afsluiten"), + ("Open", "Open"), + ("logout_tip", "Weet u zeker dat u zich wilt afmelden?"), + ("Service", "Achtergrondservice"), + ("Start", "Start"), + ("Stop", "Stop"), + ("exceed_max_devices", "Het maximum aantal gecontroleerde apparaten is bereikt."), + ("Sync with recent sessions", "Recente sessies synchroniseren"), + ("Sort tags", "Labels sorteren"), + ("Open connection in new tab", "Verbinding openen in een nieuw tabblad"), + ("Move tab to new window", "Tabblad verplaatsen naar nieuw venster"), + ("Can not be empty", "Mag niet leeg zijn"), + ("Already exists", "Bestaat al"), + ("Change Password", "Wijzig Wachtwoord"), + ("Refresh Password", "Wachtwoord Vernieuwen"), + ("ID", "ID"), + ("Grid View", "Rasterweergave"), + ("List View", "Lijstweergave"), + ("Select", "Selecteer"), + ("Toggle Tags", "Schakel Tags"), + ("pull_ab_failed_tip", "Adresboek kan niet worden bijgewerkt"), + ("push_ab_failed_tip", "Synchronisatie van adresboek mislukt"), + ("synced_peer_readded_tip", "Apparaten die aanwezig waren in recente sessies worden gesynchroniseerd met het adresboek."), + ("Change Color", "Kleur Aanpassen"), + ("Primary Color", "Hoofdkleur"), + ("HSV Color", "HSV Kleur"), + ("Installation Successful!", "Installatie geslaagd!"), + ("Installation failed!", "Installatie mislukt!"), + ("Reverse mouse wheel", "Muiswiel omkeren"), + ("{} sessions", "{} sessies"), + ("scam_title", "U wordt misschien opgelicht!"), + ("scam_text1", "Als u aan de telefoon bent met iemand die u NIET kent EN VERTROUWT en die u heeft gevraagd om RustDesk te gebruiken en de service te starten, ga dan niet verder en hang onmiddellijk op."), + ("scam_text2", "Het is waarschijnlijk een oplichter die probeert uw geld of andere privégegevens te stelen."), + ("Don't show again", "Niet opnieuw tonen"), + ("I Agree", "Ik ga akkoord"), + ("Decline", "Afwijzen"), + ("Timeout in minutes", "Time-out in minuten"), + ("auto_disconnect_option_tip", "Inkomende sessies automatisch sluiten bij inactiviteit van de gebruiker"), + ("Connection failed due to inactivity", "Automatisch verbinding verbroken wegens inactiviteit"), + ("Check for software update on startup", "Controleer op updates bij opstarten"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Upgrade RustDesk Server Pro naar versie {} of nieuwer!"), + ("pull_group_failed_tip", "Vernieuwen van groep mislukt"), + ("Filter by intersection", "Filter op kruising"), + ("Remove wallpaper during incoming sessions", "Achtergrond verwijderen tijdens inkomende sessies"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Beeldscherm is uitgeschakeld, schakel over naar het primaire beeldscherm."), + ("No displays", "Geen beeldschermen"), + ("Open in new window", "Open in een nieuw venster"), + ("Show displays as individual windows", "Beeldschermen weergeven als afzonderlijke vensters"), + ("Use all my displays for the remote session", "Gebruik al mijn beeldschermen voor de externe sessie"), + ("selinux_tip", "SELinux is ingeschakeld op dit apparaat, waardoor RustDesk mogelijk niet goed functioneert als een gecontroleerde kant."), + ("Change view", "Weergave wijzigen"), + ("Big tiles", "Grote tegels"), + ("Small tiles", "Kleine tegels"), + ("List", "Overzicht"), + ("Virtual display", "Virtuele weergave"), + ("Plug out all", "Sluit alle"), + ("True color (4:4:4)", "Ware kleur (4:4:4)"), + ("Enable blocking user input", "Blokkeren van gebruikersinvoer inschakelen"), + ("id_input_tip", "U kunt een ID, een direct IP of een domein met poort (:) invoeren. Als u toegang wilt tot een apparaat op een andere server, voeg dan een serveradres en public key toe (@?key=), bijvoorbeeld \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.Als je toegang wilt als apparaat op een openbare server, voer dan \"@public\" in, voor de openbare server is de sleutel niet nodig."), + ("privacy_mode_impl_mag_tip", "Modus 1: Overlayscherm"), + ("privacy_mode_impl_virtual_display_tip", "Modus 2: Monitor slaapstand"), + ("Enter privacy mode", "Privacymodus openen"), + ("Exit privacy mode", "Privacymodus afsluiten"), + ("idd_not_support_under_win10_2004_tip", "Het indirecte displaystuurprogramma wordt niet ondersteund. Windows 10 versie 2004 of later is vereist."), + ("input_source_1_tip", "Invoerbron 1: Standaard"), + ("input_source_2_tip", "Invoerbron 2: Verouderd"), + ("Swap control-command key", "Wissel controle-commando toets"), + ("swap-left-right-mouse", "Wissel linker- en rechtermuisknop"), + ("2FA code", "2FA-code"), + ("More", "Meer"), + ("enable-2fa-title", "Tweefactorauthenticatie inschakelen"), + ("enable-2fa-desc", "Stel nu uw authenticator in. U kunt een authenticator-app zoals Authy, Microsoft of Google Authenticator op uw telefoon of desktop gebruiken.\n\nScan de QR-code met uw app en voer de code in die uw app toont om tweefactorauthenticatie in te schakelen."), + ("wrong-2fa-code", "Kan de code niet verifiëren. Controleer of de code en lokale tijdinstellingen correct zijn."), + ("enter-2fa-title", "Tweefactorauthenticatie (2FA)"), + ("Email verification code must be 6 characters.", "E-mailverificatiecode moet 6 tekens lang zijn."), + ("2FA code must be 6 digits.", "2FA-code moet 6 cijfers lang zijn."), + ("Multiple Windows sessions found", "Meerdere Windows-sessies gevonden"), + ("Please select the session you want to connect to", "Selecteer de sessie waarmee u verbinding wilt maken"), + ("powered_by_me", "Werkt met Rustdesk"), + ("outgoing_only_desk_tip", "U kan verbinding maken met andere apparaten, maar andere apparaten kunnen geen verbinding maken met u."), + ("preset_password_warning", "Dit is een aangepaste editie en wordt geleverd met een vooraf ingesteld wachtwoord. Iedereen die dit wachtwoord kent, kan de volledige controle over het apparaat krijgen."), + ("Security Alert", "Beveiligingswaarschuwing"), + ("My address book", "Mijn adresboek"), + ("Personal", "Persoonijk"), + ("Owner", "Eigenaar"), + ("Set shared password", "Gedeeld wachtwoord instellen"), + ("Exist in", "Bestaat in"), + ("Read-only", "Alleen-lezen"), + ("Read/Write", "Lezen/Schrijven"), + ("Full Control", "Volledige Controle"), + ("share_warning_tip", "De bovenstaande velden worden gedeeld en zijn zichtbaar voor anderen."), + ("Everyone", "Iedereen"), + ("ab_web_console_tip", "Meer over de webconsole"), + ("allow-only-conn-window-open-tip", "Alleen verbindingen toestaan als het RustDesk-venster geopend is"), + ("no_need_privacy_mode_no_physical_displays_tip", "Geen fysieke schermen, geen privémodus nodig."), + ("Follow remote cursor", "Volg de cursor op afstand"), + ("Follow remote window focus", "Volg de focus van het venster op afstand"), + ("default_proxy_tip", "Standaard protocol en poort: Socks5 en 1080"), + ("no_audio_input_device_tip", "Er is geen invoerapparaat gevonden."), + ("Incoming", "Inkomend"), + ("Outgoing", "Uitgaand"), + ("Clear Wayland screen selection", "Wayland-scherm wissen"), + ("clear_Wayland_screen_selection_tip", "Nadat u de schermselectie heeft gewist, kunt u het scherm dat u wilt delen opnieuw selecteren."), + ("confirm_clear_Wayland_screen_selection_tip", "Weet u zeker dat u de Wayland-schermselectie wilt wissen?"), + ("android_new_voice_call_tip", "Er is een nieuwe spraakoproep ontvangen. Als u het aanvaardt, schakelt de audio over naar spraakcommunicatie."), + ("texture_render_tip", "Pas textuurrendering toe om afbeeldingen vloeiender te maken."), + ("Use texture rendering", "Textuurweergave gebruiken"), + ("Floating window", "Zwevend venster"), + ("floating_window_tip", "Helpt RustDesk op de achtergrond actief te houden"), + ("Keep screen on", "Scherm ingeschakeld laten"), + ("Never", "Nooit"), + ("During controlled", "Tijdens gecontroleerde"), + ("During service is on", "Tijdens actieve service"), + ("Capture screen using DirectX", "Scherm opnemen via DirectX"), + ("Back", "Terug"), + ("Apps", "Apps"), + ("Volume up", "Volume verhogen"), + ("Volume down", "Volume verlagen"), + ("Power", "Stroom"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Als u deze functie inschakelt, kunt u een 2FA-code ontvangen van uw bot. Het kan ook fungeren als een verbindingsmelding."), + ("enable-bot-desc", "1, Open een chat met @BotFather.\n2, Verzend het commando \"/newbot\". Als deze stap voltooid is, ontvangt u een token.\n3, Start een chat met de nieuw aangemaakte bot. Om hem te activeren stuurt u een bericht dat begint met een schuine streep (\"/\"), bijvoorbeeld \"/hello\".\n"), + ("cancel-2fa-confirm-tip", "Weet u zeker dat u 2FA wilt annuleren?"), + ("cancel-bot-confirm-tip", "Weet u zeker dat u de Telegram-bot wilt annuleren?"), + ("About RustDesk", "Over RustDesk"), + ("Send clipboard keystrokes", "Klembord toetsaanslagen verzenden"), + ("network_error_tip", "Controleer de netwerkverbinding en selecteer 'Opnieuw proberen'."), + ("Unlock with PIN", "Ontgrendelen met PIN"), + ("Requires at least {} characters", "Vereist minstens {} tekens"), + ("Wrong PIN", "Verkeerde PIN-code"), + ("Set PIN", "PIN-code instellen"), + ("Enable trusted devices", "Vertrouwde apparaten inschakelen"), + ("Manage trusted devices", "Vertrouwde apparaten beheren"), + ("Platform", "Platform"), + ("Days remaining", "Resterende dagen"), + ("enable-trusted-devices-tip", "2FA-verificatie overslaan op vertrouwde apparaten"), + ("Parent directory", "Hoofdmap"), + ("Resume", "Hervatten"), + ("Invalid file name", "Ongeldige bestandsnaam"), + ("one-way-file-transfer-tip", "Eenzijdige bestandsoverdracht is ingeschakeld aan de gecontroleerde kant."), + ("Authentication Required", "Verificatie vereist"), + ("Authenticate", "Verificatie"), + ("web_id_input_tip", "Je kunt een ID invoeren op dezelfde server, directe IP-toegang wordt niet ondersteund in de webclient.\nAls u toegang wilt tot een apparaat op een andere server, voegt u het serveradres toe (@?key=), bijvoorbeeld,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls u toegang wilt krijgen tot een apparaat op een publieke server, voer dan \"@public\" in, sleutel is niet nodig voor de publieke server."), + ("Download", "Downloaden"), + ("Upload folder", "Map uploaden"), + ("Upload files", "Bestanden uploaden"), + ("Clipboard is synchronized", "Klembord is gesynchroniseerd"), + ("Update client clipboard", "Klembord van client bijwerken"), + ("Untagged", "Ongemarkeerd"), + ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), + ("Accessible devices", "Toegankelijke apparaten"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgrade de RustDesk client naar versie {} of nieuwer op de externe computer!"), + ("d3d_render_tip", "Wanneer D3D-rendering is ingeschakeld kan het externe scherm op sommige apparaten, zwart zijn."), + ("Use D3D rendering", "Gebruik D3D-rendering"), + ("Printer", "Printer"), + ("printer-os-requirement-tip", "Windows 10 of hoger is vereist om de uitgaande functie met de printer te laten werken."), + ("printer-requires-installed-{}-client-tip", "Om afdrukken op afstand te gebruiken, moet {} geïnstalleerd zijn op dit apparaat."), + ("printer-{}-not-installed-tip", "De printer {} is niet geïnstalleerd."), + ("printer-{}-ready-tip", "De printer {} is geïnstalleerd en klaar voor gebruik."), + ("Install {} Printer", "Installeer {} Printer"), + ("Outgoing Print Jobs", "Uitgaande Afdruktaken"), + ("Incoming Print Jobs", "Inkomende Afdruktaken"), + ("Incoming Print Job", "Inkomende Afdruktaak"), + ("use-the-default-printer-tip", "Gebruik de standaard printer"), + ("use-the-selected-printer-tip", "Gebruik de geselecteerde printer"), + ("auto-print-tip", "Automatisch afdrukken op de geselecteerde printer."), + ("print-incoming-job-confirm-tip", "Er werd een afdruktaak ontvangen van een extern apparaat. Moet ik deze lokaal afdrukken?"), + ("remote-printing-disallowed-tile-tip", "Afdruk op afstand is verboden"), + ("remote-printing-disallowed-text-tip", "Machtigingsinstellingen aan beheerde zijde verhinderen afdrukken op afstand."), + ("save-settings-tip", "Instellingen opslaan"), + ("dont-show-again-tip", "Dit bericht wordt niet meer weergegeven"), + ("Take screenshot", "Maak een schermafbeelding"), + ("Taking screenshot", "Schermafbeelding maken"), + ("screenshot-merged-screen-not-supported-tip", "Schermafbeeldingen van meerdere schermen samenvoegen wordt momenteel niet ondersteund. Schakel over naar een enkel scherm en herhaal de actie."), + ("screenshot-action-tip", "Kies wat je met de gemaakte schermafbeelding wilt doen."), + ("Save as", "Opslaan als"), + ("Copy to clipboard", "Kopiëren naar het klembord"), + ("Enable remote printer", "Printer op afstand inschakelen"), + ("Downloading {}", "Downloaden {}"), + ("{} Update", "{} Updaten"), + ("{}-to-update-tip", "{} zal sluiten en de nieuwe versie installeren."), + ("download-new-version-failed-tip", "Fout bij het downloaden. Je kunt het opnieuw proberen of op de knop Downloaden klikken om de applicatie van de officiële website te downloaden en handmatig bij te werken."), + ("Auto update", "Automatisch updaten"), + ("update-failed-check-msi-tip", "Kan de installatiemethode niet bepalen. Klik op “Downloaden” om de applicatie van de officiële website te downloaden en handmatig bij te werken."), + ("websocket_tip", "Het WebSocketprotocol ondersteunt alleen verbindingen met de repeater."), + ("Use WebSocket", "Gebruik het WebSocketprotocol"), + ("Trackpad speed", "Snelheid Trackpad"), + ("Default trackpad speed", "Standaardsnelheid Trackpad"), + ("Numeric one-time password", "Eenmalig numeriek wachtwoord"), + ("Enable IPv6 P2P connection", "IPv6 P2P-verbinding inschakelen"), + ("Enable UDP hole punching", "UDP-hole punching inschakelen"), + ("View camera", "Camera bekijken"), + ("Enable camera", "Camera inschakelen"), + ("No cameras", "Geen camera's"), + ("view_camera_unsupported_tip", "Het externe apparaat ondersteunt geen cameraweergave."), + ("Terminal", "Terminal"), + ("Enable terminal", "Terminal inschakelen"), + ("New tab", "Nieuw tabblad"), + ("Keep terminal sessions on disconnect", "Terminalsessies bij verbreking van de verbinding behouden"), + ("Terminal (Run as administrator)", "Terminal (Als administrator uitvoeren)"), + ("terminal-admin-login-tip", "Voer de gebruikersnaam en het wachtwoord in van de beheerder van het gecontroleerde apparaat."), + ("Failed to get user token.", "Kan geen gebruikerstoken krijgen."), + ("Incorrect username or password.", "Foutieve gebruikersnaam of wachtwoord."), + ("The user is not an administrator.", "De gebruiker is geen beheerder."), + ("Failed to check if the user is an administrator.", "Fout bij het controleren of de gebruiker een beheerder is."), + ("Supported only in the installed version.", "Alleen ondersteund in de geïnstalleerde versie."), + ("elevation_username_tip", "Voer je gebruikersnaam of domeinnaam in"), + ("Preparing for installation ...", "Installatie voorbereiden ..."), + ("Show my cursor", "Toon mijn cursor"), + ("Scale custom", "Aangepaste schaal"), + ("Custom scale slider", "Aangepaste schuifregelaar voor schaal"), + ("Decrease", "Verlagen"), + ("Increase", "Verhogen"), + ("Show virtual mouse", "Virtuele muis weergeven"), + ("Virtual mouse size", "Virtuele muis grootte"), + ("Small", "Klein"), + ("Large", "Groot"), + ("Show virtual joystick", "Virtuele joystick weergeven"), + ("Edit note", "Opmerking bewerken"), + ("Alias", "Alias"), + ("ScrollEdge", "Schuifbalk"), + ("Allow insecure TLS fallback", "Onbeveiligde TLS-terugval toestaan"), + ("allow-insecure-tls-fallback-tip", "Standaard controleert RustDesk het certificaat van de server bij het gebruik van protocollen die TLS gebruiken. Wanneer deze optie is ingeschakeld, laat RustDesk verbindingen toe, zelfs als de verificatiestap mislukt."), + ("Disable UDP", "UDP uitschakelen"), + ("disable-udp-tip", "Controleert of alleen TCP moet worden gebruikt. Als deze optie is ingeschakeld, gebruikt RustDesk niet langer UDP 21116, maar TCP 21116."), + ("server-oss-not-support-tip", "Opmerking: Deze functie is niet beschikbaar in de open-sourceversie van de RustDesk-server."), + ("input note here", "voeg hier een opmerking toe"), + ("note-at-conn-end-tip", "Vraag om een opmerking aan het einde van de verbinding"), + ("Show terminal extra keys", "Toon extra toetsen voor terminal"), + ("Relative mouse mode", "Relatieve muismodus"), + ("rel-mouse-not-supported-peer-tip", "De relatieve muismodus wordt niet ondersteund door het externe apparaat."), + ("rel-mouse-not-ready-tip", "De relatieve muismodus was nog niet klaar, probeer het later opnieuw."), + ("rel-mouse-lock-failed-tip", "Het vergrendelen van de cursor is mislukt. De relatieve muismodus is uitgeschakeld."), + ("rel-mouse-exit-{}-tip", "Druk op {} om af te sluiten."), + ("rel-mouse-permission-lost-tip", "De toetsenbordcontrole is uitgeschakeld. De relatieve muismodus is uitgeschakeld."), + ("Changelog", "Wijzigingenlogboek"), + ("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."), + ("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."), + ("Continue with {}", "Ga verder met {}"), + ("Display Name", "Naam Weergeven"), + ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), + ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/pl.rs b/vendor/rustdesk/src/lang/pl.rs new file mode 100644 index 0000000..972afc1 --- /dev/null +++ b/vendor/rustdesk/src/lang/pl.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Twój pulpit"), + ("desk_tip", "Aby połączyć się z tym urządzeniem, użyj poniższego ID i hasła"), + ("Password", "Hasło"), + ("Ready", "Gotowe"), + ("Established", "Nawiązano"), + ("connecting_status", "Łączenie"), + ("Enable service", "Włącz usługę"), + ("Start service", "Uruchom usługę"), + ("Service is running", "Usługa uruchomiona"), + ("Service is not running", "Usługa nie jest uruchomiona"), + ("not_ready_status", "Brak gotowości"), + ("Control Remote Desktop", "Steruj pulpitem zdalnym"), + ("Transfer file", "Transfer plików"), + ("Connect", "Połącz"), + ("Recent sessions", "Ostatnie sesje"), + ("Address book", "Książka adresowa"), + ("Confirmation", "Potwierdzenie"), + ("TCP tunneling", "Tunelowanie TCP"), + ("Remove", "Usuń"), + ("Refresh random password", "Odśwież losowe hasło"), + ("Set your own password", "Ustaw własne hasło"), + ("Enable keyboard/mouse", "Włącz klawiaturę/mysz"), + ("Enable clipboard", "Włącz schowek"), + ("Enable file transfer", "Włącz transfer pliku"), + ("Enable TCP tunneling", "Włącz tunelowanie TCP"), + ("IP Whitelisting", "Biała lista IP"), + ("ID/Relay Server", "Serwer ID/Pośredniczący"), + ("Import server config", "Importuj konfigurację serwera"), + ("Export Server Config", "Eksportuj konfigurację serwera"), + ("Import server configuration successfully", "Import konfiguracji serwera zakończono pomyślnie"), + ("Export server configuration successfully", "Eksport konfiguracji serwera zakończono pomyślnie"), + ("Invalid server configuration", "Nieprawidłowa konfiguracja serwera"), + ("Clipboard is empty", "Schowek jest pusty"), + ("Stop service", "Zatrzymaj usługę"), + ("Change ID", "Zmień ID"), + ("Your new ID", "Twój nowy ID"), + ("length %min% to %max%", "o długości od %min% do %max%"), + ("starts with a letter", "rozpoczyna się literą"), + ("allowed characters", "dozwolone znaki"), + ("id_change_tip", "Nowy ID może być złożony z małych i dużych liter a-zA-z, cyfry 0-9, - (dash) oraz _ (podkreślenie). Pierwszym znakiem powinna być litera a-zA-Z, a całe ID powinno składać się z 6 do 16 znaków."), + ("Website", "Strona internetowa"), + ("About", "O aplikacji"), + ("Slogan_tip", "Tworzone z miłością w tym pełnym chaosu świecie!"), + ("Privacy Statement", "Oświadczenie o ochronie prywatności"), + ("Mute", "Wycisz"), + ("Build Date", "Zbudowano"), + ("Version", "Wersja"), + ("Home", "Pulpit"), + ("Audio Input", "Wejście audio"), + ("Enhancements", "Ulepszenia"), + ("Hardware Codec", "Kodek sprzętowy"), + ("Adaptive bitrate", "Adaptacyjny bitrate"), + ("ID Server", "Serwer ID"), + ("Relay Server", "Serwer pośredniczący"), + ("API Server", "Serwer API"), + ("invalid_http", "Nieprawidłowe żądanie http"), + ("Invalid IP", "Nieprawidłowe IP"), + ("Invalid format", "Nieprawidłowy format"), + ("server_not_support", "Serwer nie obsługuje tej funkcji"), + ("Not available", "Niedostępne"), + ("Too frequent", "Zbyt często"), + ("Cancel", "Anuluj"), + ("Skip", "Pomiń"), + ("Close", "Zamknij"), + ("Retry", "Ponów"), + ("OK", "OK"), + ("Password Required", "Wymagane jest hasło"), + ("Please enter your password", "Wpisz proszę Twoje hasło"), + ("Remember password", "Zapamiętaj hasło"), + ("Wrong Password", "Błędne hasło"), + ("Do you want to enter again?", "Czy chcesz wprowadzić ponownie?"), + ("Connection Error", "Błąd połączenia"), + ("Error", "Błąd"), + ("Reset by the peer", "Połączenie zresetowane przez zdalne urządzenie"), + ("Connecting...", "Łączenie..."), + ("Connection in progress. Please wait.", "Trwa łączenie. Proszę czekać."), + ("Please try 1 minute later", "Spróbuj za minutę"), + ("Login Error", "Błąd logowania"), + ("Successful", "Sukces"), + ("Connected, waiting for image...", "Połączono, oczekiwanie na obraz..."), + ("Name", "Nazwa"), + ("Type", "Typ"), + ("Modified", "Zmodyfikowany"), + ("Size", "Rozmiar"), + ("Show Hidden Files", "Pokaż ukryte pliki"), + ("Receive", "Pobierz"), + ("Send", "Wyślij"), + ("Refresh File", "Odśwież plik"), + ("Local", "Lokalny"), + ("Remote", "Zdalny"), + ("Remote Computer", "Komputer zdalny"), + ("Local Computer", "Komputer lokalny"), + ("Confirm Delete", "Potwierdź usunięcie"), + ("Delete", "Usuń"), + ("Properties", "Właściwości"), + ("Multi Select", "Wielokrotny wybór"), + ("Select All", "Zaznacz wszystko"), + ("Unselect All", "Odznacz wszystko"), + ("Empty Directory", "Pusty katalog"), + ("Not an empty directory", "Katalog nie jest pusty"), + ("Are you sure you want to delete this file?", "Czy na pewno chcesz usunąć ten plik?"), + ("Are you sure you want to delete this empty directory?", "Czy na pewno chcesz usunąć ten pusty katalog?"), + ("Are you sure you want to delete the file of this directory?", "Czy na pewno chcesz usunąć pliki z tego katalogu?"), + ("Do this for all conflicts", "Wykonaj dla wszystkich konfliktów"), + ("This is irreversible!", "To jest nieodwracalne!"), + ("Deleting", "Usuwanie"), + ("files", "pliki"), + ("Waiting", "Oczekiwanie"), + ("Finished", "Zakończono"), + ("Speed", "Prędkość"), + ("Custom Image Quality", "Niestandardowa jakość obrazu"), + ("Privacy mode", "Tryb prywatny"), + ("Block user input", "Blokuj peryferia użytkownika"), + ("Unblock user input", "Odblokuj peryferia użytkownika"), + ("Adjust Window", "Dostosuj okno"), + ("Original", "Oryginalny"), + ("Shrink", "Zmniejsz"), + ("Stretch", "Rozciągnij"), + ("Scrollbar", "Pasek przewijania"), + ("ScrollAuto", "Przewijanie automatyczne"), + ("Good image quality", "Wysoka jakość obrazu"), + ("Balanced", "Tryb zbalansowany"), + ("Optimize reaction time", "Wysoka wydajność"), + ("Custom", "Tryb niestandardowy"), + ("Show remote cursor", "Pokazuj zdalny kursor"), + ("Show quality monitor", "Parametry połączenia"), + ("Disable clipboard", "Wyłącz schowek"), + ("Lock after session end", "Zablokuj po zakończeniu sesji"), + ("Insert Ctrl + Alt + Del", "Wyślij Ctrl + Alt + Del"), + ("Insert Lock", "Zablokuj zdalne urządzenie"), + ("Refresh", "Odśwież"), + ("ID does not exist", "ID nie istnieje"), + ("Failed to connect to rendezvous server", "Nie udało się połączyć z serwerem połączeń"), + ("Please try later", "Spróbuj później"), + ("Remote desktop is offline", "Zdalny pulpit jest offline"), + ("Key mismatch", "Niezgodność klucza"), + ("Timeout", "Przekroczono czas oczekiwania"), + ("Failed to connect to relay server", "Nie udało się połączyć z serwerem pośredniczącym"), + ("Failed to connect via rendezvous server", "Nie udało się połączyć przez serwer połączeń"), + ("Failed to connect via relay server", "Nie udało się połączyć przez serwer pośredniczący"), + ("Failed to make direct connection to remote desktop", "Nie udało się nawiązać bezpośredniego połączenia z pulpitem zdalnym"), + ("Set Password", "Ustaw hasło"), + ("OS Password", "Hasło systemu operacyjnego"), + ("install_tip", "RustDesk może nie działać poprawnie na maszynie zdalnej z przyczyn związanych z UAC. W celu uniknięcia problemów z UAC, kliknij poniższy przycisk by zainstalować RustDesk w swoim systemie."), + ("Click to upgrade", "Zaktualizuj"), + ("Configure", "Konfiguruj"), + ("config_acc", "Konfiguracja konta"), + ("config_screen", "Konfiguracja ekranu"), + ("Installing ...", "Instalowanie..."), + ("Install", "Zainstaluj"), + ("Installation", "Instalacja"), + ("Installation Path", "Ścieżka instalacji"), + ("Create start menu shortcuts", "Utwórz skrót w menu"), + ("Create desktop icon", "Utwórz skrót na pulpicie"), + ("agreement_tip", "Wskazówki do umowy/zgody"), + ("Accept and Install", "Akceptuj i zainstaluj"), + ("End-user license agreement", "Umowa licencyjna użytkownika końcowego"), + ("Generating ...", "Trwa generowanie..."), + ("Your installation is lower version.", "Twoja instalacja jest w niższej wersji"), + ("not_close_tcp_tip", "Podczas korzystania z tunelowania, nie zamykaj tego okna."), + ("Listening ...", "Nasłuchiwanie..."), + ("Remote Host", "Host zdalny"), + ("Remote Port", "Port zdalny"), + ("Action", "Akcja"), + ("Add", "Dodaj"), + ("Local Port", "Lokalny port"), + ("Local Address", "Lokalny adres"), + ("Change Local Port", "Zmień lokalny port"), + ("setup_server_tip", "W celu uzyskania szybszego połączenia, skorzystaj z własnego serwera połączeń."), + ("Too short, at least 6 characters.", "Za krótkie, min. 6 znaków"), + ("The confirmation is not identical.", "Potwierdzenie nie jest identyczne."), + ("Permissions", "Uprawnienia"), + ("Accept", "Akceptuj"), + ("Dismiss", "Odrzuć"), + ("Disconnect", "Rozłącz"), + ("Enable file copy and paste", "Zezwalaj na kopiowanie i wklejanie plików"), + ("Connected", "Połączony"), + ("Direct and encrypted connection", "Połączenie bezpośrednie i szyfrowane"), + ("Relayed and encrypted connection", "Połączenie pośrednie i szyfrowane"), + ("Direct and unencrypted connection", "Połączenie bezpośrednie i nieszyfrowane"), + ("Relayed and unencrypted connection", "Połączenie pośrednie i nieszyfrowane"), + ("Enter Remote ID", "Wprowadź zdalne ID"), + ("Enter your password", "Wprowadź hasło"), + ("Logging in...", "Trwa logowanie..."), + ("Enable RDP session sharing", "Włącz udostępnianie sesji RDP"), + ("Auto Login", "Automatyczne logowanie"), + ("Enable direct IP access", "Włącz bezpośredni dostęp IP"), + ("Rename", "Zmień nazwę"), + ("Space", "Przestrzeń"), + ("Create desktop shortcut", "Utwórz skrót na pulpicie"), + ("Change Path", "Zmień ścieżkę"), + ("Create Folder", "Utwórz folder"), + ("Please enter the folder name", "Wpisz nazwę folderu"), + ("Fix it", "Napraw to"), + ("Warning", "Ostrzeżenie"), + ("Login screen using Wayland is not supported", "Ekran logowania korzystający z Wayland nie jest obsługiwany"), + ("Reboot required", "Wymagane ponowne uruchomienie"), + ("Unsupported display server", "Nieobsługiwany serwer wyświetlania"), + ("x11 expected", "Wymagany jest X11"), + ("Port", "Port"), + ("Settings", "Ustawienia"), + ("Username", "Nazwa użytkownika"), + ("Invalid port", "Nieprawidłowy port"), + ("Closed manually by the peer", "Połączenie zakończone ręcznie przez zdalne urządzenie"), + ("Enable remote configuration modification", "Włącz zdalną modyfikację konfiguracji"), + ("Run without install", "Uruchom bez instalacji"), + ("Connect via relay", "Połącz bezpośrednio"), + ("Always connect via relay", "Zawsze łącz pośrednio"), + ("whitelist_tip", "Zezwalaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), + ("Login", "Zaloguj"), + ("Verify", "Zweryfikuj"), + ("Remember me", "Zapamiętaj mnie"), + ("Trust this device", "Dodaj to urządzenie do zaufanych"), + ("Verification code", "Kod weryfikacyjny"), + ("verification_tip", "Nastąpiło logowanie z nowego urządzenia, kod weryfikacyjny został wysłany na podany adres email, wprowadź kod by kontynuować proces logowania"), + ("Logout", "Wyloguj"), + ("Tags", "Tagi"), + ("Search ID", "Szukaj ID"), + ("whitelist_sep", "Oddzielone przecinkiem, średnikiem, spacją lub w nowej linii"), + ("Add ID", "Dodaj ID"), + ("Add Tag", "Dodaj Tag"), + ("Unselect all tags", "Odznacz wszystkie tagi"), + ("Network error", "Błąd sieci"), + ("Username missed", "Nieprawidłowa nazwa użytkownika"), + ("Password missed", "Nieprawidłowe hasło"), + ("Wrong credentials", "Błędne dane uwierzytelniające"), + ("The verification code is incorrect or has expired", "Kod weryfikacyjny jest niepoprawny lub wygasł"), + ("Edit Tag", "Edytuj tag"), + ("Forget Password", "Zapomnij hasło"), + ("Favorites", "Ulubione"), + ("Add to Favorites", "Dodaj do ulubionych"), + ("Remove from Favorites", "Usuń z ulubionych"), + ("Empty", "Pusto"), + ("Invalid folder name", "Nieprawidłowa nazwa folderu"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Discovered", "Wykryte"), + ("install_daemon_tip", "By uruchomić RustDesk przy starcie systemu, musisz zainstalować usługę systemową."), + ("Remote ID", "Zdalne ID"), + ("Paste", "Wklej"), + ("Paste here?", "Wkleić tutaj?"), + ("Are you sure to close the connection?", "Czy na pewno chcesz zakończyć połączenie?"), + ("Download new version", "Pobierz nową wersję"), + ("Touch mode", "Tryb dotykowy"), + ("Mouse mode", "Tryb myszy"), + ("One-Finger Tap", "Dotknij jednym palcem"), + ("Left Mouse", "Lewy klik myszy"), + ("One-Long Tap", "Przytrzymaj jednym palcem"), + ("Two-Finger Tap", "Dotknij dwoma palcami"), + ("Right Mouse", "Prawy przycisk myszy"), + ("One-Finger Move", "Przesuń jednym palcem"), + ("Double Tap & Move", "Dotknij dwukrotnie i przesuń"), + ("Mouse Drag", "Przeciągnij myszą"), + ("Three-Finger vertically", "Trzy palce pionowo"), + ("Mouse Wheel", "Kółko myszy"), + ("Two-Finger Move", "Ruch dwoma palcami"), + ("Canvas Move", "Ruch ekranu"), + ("Pinch to Zoom", "Uszczypnij, aby powiększyć"), + ("Canvas Zoom", "Powiększanie ekranu"), + ("Reset canvas", "Reset ekranu"), + ("No permission of file transfer", "Brak uprawnień na przesyłanie plików"), + ("Note", "Notatka"), + ("Connection", "Połączenie"), + ("Share screen", "Udostępnianie ekranu"), + ("Chat", "Czat"), + ("Total", "Łącznie"), + ("items", "elementów"), + ("Selected", "zaznaczonych"), + ("Screen Capture", "Przechwytywanie ekranu"), + ("Input Control", "Kontrola wejścia"), + ("Audio Capture", "Przechwytywanie dźwięku"), + ("Do you accept?", "Akceptujesz?"), + ("Open System Setting", "Otwórz ustawienia systemowe"), + ("How to get Android input permission?", "Jak uzyskać uprawnienia do wprowadzania danych w systemie Android?"), + ("android_input_permission_tip1", "Aby można było sterować Twoim urządzeniem za pomocą myszy lub dotyku, musisz zezwolić RustDesk na korzystanie z usługi \"Ułatwienia dostępu\"."), + ("android_input_permission_tip2", "Przejdź do następnej strony ustawień systemowych, znajdź i wejdź w [Zainstalowane usługi], włącz usługę [RustDesk Input]."), + ("android_new_connection_tip", "Otrzymano nowe żądanie zdalnego dostępu, które chce przejąć kontrolę nad Twoim urządzeniem."), + ("android_service_will_start_tip", "Włączenie opcji „Przechwytywanie ekranu” spowoduje automatyczne uruchomienie usługi, umożliwiając innym urządzeniom żądanie połączenia z Twoim urządzeniem."), + ("android_stop_service_tip", "Zamknięcie usługi spowoduje automatyczne zamknięcie wszystkich nawiązanych połączeń."), + ("android_version_audio_tip", "Bieżąca wersja systemu Android nie obsługuje przechwytywania dźwięku, zaktualizuj system do wersji Android 10 lub nowszej."), + ("android_start_service_tip", "Kliknij [Uruchom serwis] lub włącz uprawnienia [Zrzuty ekranu], aby uruchomić usługę udostępniania ekranu."), + ("android_permission_may_not_change_tip", "Uprawnienia do nawiązanych połączeń nie mogą być zmieniane automatycznie, dopiero po ponownym połączeniu."), + ("Account", "Konto"), + ("Overwrite", "Nadpisz"), + ("This file exists, skip or overwrite this file?", "Ten plik istnieje, pominąć czy nadpisać ten plik?"), + ("Quit", "Zrezygnuj"), + ("Help", "Pomoc"), + ("Failed", "Niepowodzenie"), + ("Succeeded", "Udało się"), + ("Someone turns on privacy mode, exit", "Ktoś włącza tryb prywatności, wyjdź"), + ("Unsupported", "Niewspierane"), + ("Peer denied", "Odmowa dostępu"), + ("Please install plugins", "Zainstaluj wtyczkę"), + ("Peer exit", "Wyjście ze zdalnego urządzenia"), + ("Failed to turn off", "Nie udało się wyłączyć"), + ("Turned off", "Wyłączony"), + ("Language", "Język"), + ("Keep RustDesk background service", "Zachowaj usługę RustDesk w tle"), + ("Ignore Battery Optimizations", "Ignoruj optymalizację baterii"), + ("android_open_battery_optimizations_tip", "Jeśli chcesz wyłączyć tę funkcję, przejdź do następnej strony ustawień aplikacji RustDesk, znajdź i wprowadź [Bateria], odznacz [Bez ograniczeń]"), + ("Start on boot", "Autostart"), + ("Start the screen sharing service on boot, requires special permissions", "Uruchom usługę udostępniania ekranu podczas startu, wymaga specjalnych uprawnień"), + ("Connection not allowed", "Połączenie niedozwolone"), + ("Legacy mode", "Tryb kompatybilności wstecznej (legacy)"), + ("Map mode", "Tryb mapowania"), + ("Translate mode", "Tryb translacji"), + ("Use permanent password", "Użyj hasła permanentnego"), + ("Use both passwords", "Użyj obu haseł"), + ("Set permanent password", "Ustaw hasło permanentne"), + ("Enable remote restart", "Włącz zdalne restartowanie"), + ("Restart remote device", "Zrestartuj zdalne urządzenie"), + ("Are you sure you want to restart", "Czy na pewno uruchomić ponownie"), + ("Restarting remote device", "Trwa restartowanie zdalnego urządzenia"), + ("remote_restarting_tip", "Trwa ponownie uruchomienie zdalnego urządzenia, zamknij ten komunikat i ponownie nawiąż za chwilę połączenie używając hasła permanentnego"), + ("Copied", "Skopiowano"), + ("Exit Fullscreen", "Wyłącz tryb pełnoekranowy"), + ("Fullscreen", "Tryb pełnoekranowy"), + ("Mobile Actions", "Dostępne mobilne polecenia"), + ("Select Monitor", "Wybierz ekran"), + ("Control Actions", "Dostępne polecenia"), + ("Display Settings", "Ustawienia wyświetlania"), + ("Ratio", "Proporcje"), + ("Image Quality", "Jakość obrazu"), + ("Scroll Style", "Styl przewijania"), + ("Show Toolbar", "Pokaż pasek narzędzi"), + ("Hide Toolbar", "Ukryj pasek narzędzi"), + ("Direct Connection", "Połączenie bezpośrednie"), + ("Relay Connection", "Połączenie przez bramkę"), + ("Secure Connection", "Połączenie szyfrowane"), + ("Insecure Connection", "Połączenie nieszyfrowane"), + ("Scale original", "Skalowanie oryginalne"), + ("Scale adaptive", "Dopasuj do wyświetlacza"), + ("General", "Ogólne"), + ("Security", "Zabezpieczenia"), + ("Theme", "Motyw"), + ("Dark Theme", "Ciemny motyw"), + ("Light Theme", "Jasny motyw"), + ("Dark", "Ciemny"), + ("Light", "Jasny"), + ("Follow System", "Zgodny z systemem"), + ("Enable hardware codec", "Włącz akcelerację sprzętową kodeków"), + ("Unlock Security Settings", "Odblokuj ustawienia zabezpieczeń"), + ("Enable audio", "Włącz dźwięk"), + ("Unlock Network Settings", "Odblokuj ustawienia sieciowe"), + ("Server", "Serwer"), + ("Direct IP Access", "Bezpośredni adres IP"), + ("Proxy", "Proxy"), + ("Apply", "Zastosuj"), + ("Disconnect all devices?", "Czy rozłączyć wszystkie urządzenia?"), + ("Clear", "Wyczyść"), + ("Audio Input Device", "Urządzenie wejściowe Audio"), + ("Use IP Whitelisting", "Użyj białej listy IP"), + ("Network", "Sieć"), + ("Pin Toolbar", "Przypnij pasek narzędzi"), + ("Unpin Toolbar", "Odepnij pasek narzędzi"), + ("Recording", "Nagrywanie"), + ("Directory", "Folder"), + ("Automatically record incoming sessions", "Automatycznie nagrywaj sesje przychodzące"), + ("Automatically record outgoing sessions", "Automatycznie nagrywaj sesje wychodzące"), + ("Change", "Zmień"), + ("Start session recording", "Zacznij nagrywać sesję"), + ("Stop session recording", "Zatrzymaj nagrywanie sesji"), + ("Enable recording session", "Włącz nagrywanie sesji"), + ("Enable LAN discovery", "Włącz wykrywanie urządzenia w sieci LAN"), + ("Deny LAN discovery", "Zablokuj wykrywanie urządzenia w sieci LAN"), + ("Write a message", "Napisz wiadomość"), + ("Prompt", "Monit"), + ("Please wait for confirmation of UAC...", "Poczekaj na potwierdzenie uprawnień UAC"), + ("elevated_foreground_window_tip", "Aktualne okno zdalnego urządzenia wymaga wyższych uprawnień by poprawnie działać, chwilowo niemożliwym jest korzystanie z myszy i klawiatury. Możesz poprosić zdalnego użytkownika o minimalizację okna, lub nacisnąć przycisk podniesienia uprawnień w oknie zarządzania połączeniami. By uniknąć tego problemu, rekomendujemy instalację oprogramowania na urządzeniu zdalnym."), + ("Disconnected", "Rozłączone"), + ("Other", "Inne"), + ("Confirm before closing multiple tabs", "Potwierdź przed zamknięciem wielu kart"), + ("Keyboard Settings", "Ustawienia klawiatury"), + ("Full Access", "Pełny dostęp"), + ("Screen Share", "Udostępnianie ekranu"), + ("ubuntu-21-04-required", "Wayland wymaga Ubuntu 21.04 lub nowszego."), + ("wayland-requires-higher-linux-version", "Wayland wymaga nowszej dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), + ("xdp-portal-unavailable", "Nie udało się przechwycić ekranu Wayland. Portal XDG Desktop mógł ulec awarii lub jest niedostępny. Spróbuj go ponownie uruchomić poleceniem `systemctl --user restart xdg-desktop-portal`."), + ("JumpLink", "Podgląd"), + ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po zdalnego urządzenia)."), + ("Show RustDesk", "Pokaż RustDesk"), + ("This PC", "Ten komputer"), + ("or", "lub"), + ("Elevate", "Uzyskaj uprawnienia"), + ("Zoom cursor", "Powiększenie kursora"), + ("Accept sessions via password", "Uwierzytelnij sesję używając hasła"), + ("Accept sessions via click", "Uwierzytelnij sesję poprzez kliknięcie"), + ("Accept sessions via both", "Uwierzytelnij sesję za pomocą obu sposobów"), + ("Please wait for the remote side to accept your session request...", "Oczekiwanie, na zatwierdzenie sesji przez host zdalny..."), + ("One-time Password", "Hasło jednorazowe"), + ("Use one-time password", "Użyj hasła jednorazowego"), + ("One-time password length", "Długość hasła jednorazowego"), + ("Request access to your device", "Żądanie dostępu do Twojego urządzenia"), + ("Hide connection management window", "Ukryj okno zarządzania połączeniem"), + ("hide_cm_tip", "Pozwalaj na ukrycie tylko, gdy akceptujesz sesje za pośrednictwem hasła i używasz hasła permanentnego"), + ("wayland_experiment_tip", "Wsparcie dla Wayland jest niekompletne, użyj X11 jeżeli chcesz korzystać z dostępu nienadzorowanego"), + ("Right click to select tabs", "Kliknij prawym przyciskiem myszy, aby wybrać zakładkę"), + ("Skipped", "Pominięte"), + ("Add to address book", "Dodaj do Książki Adresowej"), + ("Group", "Grupy"), + ("Search", "Szukaj"), + ("Closed manually by web console", "Zakończone ręcznie z poziomu konsoli webowej"), + ("Local keyboard type", "Lokalny typ klawiatury"), + ("Select local keyboard type", "Wybierz lokalny typ klawiatury"), + ("software_render_tip", "Jeżeli posiadasz kartę graficzną Nvidia i okno zamyka się natychmiast po nawiązaniu połączenia, instalacja sterownika nouveau i wybór renderowania programowego mogą pomóc. Restart aplikacji jest wymagany."), + ("Always use software rendering", "Zawsze używaj renderowania programowego"), + ("config_input", "By kontrolować zdalne urządzenie przy pomocy klawiatury, musisz udzielić aplikacji RustDesk uprawnień do \"Urządzeń Wejściowych\"."), + ("config_microphone", "Aby umożliwić zdalne rozmowy należy przyznać RustDesk uprawnienia do \"Nagrań audio\"."), + ("request_elevation_tip", "Możesz poprosić o podniesienie uprawnień jeżeli ktoś posiada dostęp do zdalnego urządzenia."), + ("Wait", "Czekaj"), + ("Elevation Error", "Błąd przy podnoszeniu uprawnień"), + ("Ask the remote user for authentication", "Poproś użytkownika zdalnego o uwierzytelnienie"), + ("Choose this if the remote account is administrator", "Wybierz to jeżeli zdalne konto jest administratorem"), + ("Transmit the username and password of administrator", "Prześlij nazwę użytkownika i hasło administratora"), + ("still_click_uac_tip", "Nadal wymaga od zdalnego użytkownika potwierdzenia uprawnień UAC."), + ("Request Elevation", "Poproś o podniesienie uprawnień"), + ("wait_accept_uac_tip", "Prosimy czekać aż zdalny użytkownik potwierdzi uprawnienia UAC."), + ("Elevate successfully", "Pomyślnie podniesiono uprawnienia"), + ("uppercase", "wielkie litery"), + ("lowercase", "małe litery"), + ("digit", "cyfry"), + ("special character", "znaki specjalne"), + ("length>=8", "długość>=8"), + ("Weak", "Słabe"), + ("Medium", "Średnie"), + ("Strong", "Mocne"), + ("Switch Sides", "Zamień Strony"), + ("Please confirm if you want to share your desktop?", "Czy na pewno chcesz udostępnić swój ekran?"), + ("Display", "Wyświetlanie"), + ("Default View Style", "Domyślny styl wyświetlania"), + ("Default Scroll Style", "Domyślny styl przewijania"), + ("Default Image Quality", "Domyślna jakość obrazu"), + ("Default Codec", "Domyślny kodek"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Inne opcje domyślne"), + ("Voice call", "Rozmowa głosowa"), + ("Text chat", "Chat tekstowy"), + ("Stop voice call", "Rozłącz"), + ("relay_hint_tip", "Bezpośrednie połączenie może nie być możliwe, możesz spróbować połączyć się przez serwer przekazujący. \nDodatkowo, jeśli chcesz użyć serwera przekazującego przy pierwszej próbie, możesz dodać sufiks \"/r\" do identyfikatora lub wybrać opcję \"Zawsze łącz przez serwer przekazujący\" na karcie peer-ów."), + ("Reconnect", "Połącz ponownie"), + ("Codec", "Kodek"), + ("Resolution", "Rozdzielczość"), + ("No transfers in progress", "Brak transferów w toku"), + ("Set one-time password length", "Ustaw długość jednorazowego hasła"), + ("RDP Settings", "Ustawienia RDP"), + ("Sort by", "Sortuj wg"), + ("New Connection", "Nowe połączenie"), + ("Restore", "Przywróć"), + ("Minimize", "Minimalizuj"), + ("Maximize", "Maksymalizuj"), + ("Your Device", "Twoje urządzenie"), + ("empty_recent_tip", "Ups, żadnych nowych sesji!\nCzas zaplanować nową."), + ("empty_favorite_tip", "Brak ulubionych?\nZnajdźmy kogoś, z kim możesz się połączyć i dodaj Go do ulubionych!"), + ("empty_lan_tip", "Ojej, wygląda na to, że nie odkryliśmy żadnych urządzeń z RustDesk w Twojej sieci."), + ("empty_address_book_tip", "Ojej, wygląda na to, że nie ma żadnych wpisów w Twojej książce adresowej."), + ("Empty Username", "Pole nazwy użytkownika jest puste"), + ("Empty Password", "Pole hasła jest puste"), + ("Me", "Ja"), + ("identical_file_tip", "Ten plik jest identyczny z plikiem na drugim komputerze."), + ("show_monitors_tip", "Pokaż monitory w zasobniku"), + ("View Mode", "Tylko podgląd (wyłącza możliwość interakcji)"), + ("login_linux_tip", "Musisz zalogować się na zdalne konto, by zezwolić na sesję pulpitu X"), + ("verify_rustdesk_password_tip", "Weryfikuj hasło RustDesk"), + ("remember_account_tip", "Zapamiętaj to konto"), + ("os_account_desk_tip", "To konto jest używane do logowania do zdalnych systemów i włącza bezobsługowe sesje pulpitu"), + ("OS Account", "Konto systemowe"), + ("another_user_login_title_tip", "Inny użytkownik jest już zalogowany"), + ("another_user_login_text_tip", "Rozłącz"), + ("xorg_not_found_title_tip", "Nie znaleziono Xorg"), + ("xorg_not_found_text_tip", "Proszę zainstalować Xorg"), + ("no_desktop_title_tip", "Żaden pulpit nie jest dostępny"), + ("no_desktop_text_tip", "Proszę zainstalować pulpit GNOME"), + ("No need to elevate", "Podniesienie uprawnień nie jest wymagane"), + ("System Sound", "Dźwięk systemowy"), + ("Default", "Domyślne"), + ("New RDP", "Nowe RDP"), + ("Fingerprint", "Sygnatura"), + ("Copy Fingerprint", "Skopiuj sygnaturę"), + ("no fingerprints", "brak sygnatur"), + ("Select a peer", "Wybierz zdalne urządzenie"), + ("Select peers", "Wybierz zdalne urządzenia"), + ("Plugins", "Wtyczki"), + ("Uninstall", "Odinstaluj"), + ("Update", "Aktualizuj"), + ("Enable", "Włącz"), + ("Disable", "Wyłącz"), + ("Options", "Opcje"), + ("resolution_original_tip", "Oryginalna rozdzielczość"), + ("resolution_fit_local_tip", "Dostosuj rozdzielczość lokalną"), + ("resolution_custom_tip", "Rozdzielczość niestandardowa"), + ("Collapse toolbar", "Zwiń pasek narzędzi"), + ("Accept and Elevate", "Akceptuj i Podnieś uprawnienia"), + ("accept_and_elevate_btn_tooltip", "Zaakceptuj połączenie i podnieś uprawnienia UAC"), + ("clipboard_wait_response_timeout_tip", "Upłynął limit czasu oczekiwania na schowek."), + ("Incoming connection", "Połączenie przychodzące"), + ("Outgoing connection", "Połączenie wychodzące"), + ("Exit", "Wyjście"), + ("Open", "Otwórz"), + ("logout_tip", "Na pewno chcesz się wylogować?"), + ("Service", "Usługa"), + ("Start", "Uruchom"), + ("Stop", "Zatrzymaj"), + ("exceed_max_devices", "Przekroczona maks. liczba urządzeń"), + ("Sync with recent sessions", "Synchronizacja z ostatnimi sesjami"), + ("Sort tags", "Znaczniki sortowania"), + ("Open connection in new tab", "Otwórz połączenie w nowej zakładce"), + ("Move tab to new window", "Przenieś zakładkę do nowego okna"), + ("Can not be empty", "Nie może być puste"), + ("Already exists", "Już istnieje"), + ("Change Password", "Zmień hasło"), + ("Refresh Password", "Odśwież hasło"), + ("ID", "ID"), + ("Grid View", "Widok siatki"), + ("List View", "Widok listy"), + ("Select", "Wybierz"), + ("Toggle Tags", "Przełącz tagi"), + ("pull_ab_failed_tip", "Aktualizacja książki adresowej nie powiodła się"), + ("push_ab_failed_tip", "Nie udało się zsynchronizować książki adresowej z serwerem"), + ("synced_peer_readded_tip", "Urządzenia, które były obecne w ostatnich sesjach, zostaną ponownie dodane do książki adresowej"), + ("Change Color", "Zmień kolor"), + ("Primary Color", "Kolor podstawowy"), + ("HSV Color", "Kolor HSV"), + ("Installation Successful!", "Instalacja zakończona!"), + ("Installation failed!", "Instalacja nie powiodła się"), + ("Reverse mouse wheel", "Odwróć rolkę myszki"), + ("{} sessions", "{} sesji"), + ("scam_title", "Możesz być ofiarą OSZUSTWA!"), + ("scam_text1", "Jeżeli rozmawiasz przez telefon z osobą której NIE ZNASZ i NIE UFASZ, która prosi Cię o uruchomienie programu RustDesk - nie rób tego i natychmiast się rozłącz."), + ("scam_text2", "Jest to prawdopodobnie oszust, który próbuje ukraść Twoje pieniądze lub inne prywatne informacje."), + ("Don't show again", "Nie pokazuj więcej"), + ("I Agree", "Zgadzam się"), + ("Decline", "Odmawiam"), + ("Timeout in minutes", "Czas bezczynności w minutach"), + ("auto_disconnect_option_tip", "Automatycznie rozłącz sesje przychodzące przy braku aktywności użytkownika"), + ("Connection failed due to inactivity", "Automatycznie rozłącz przy bezczynności"), + ("Check for software update on startup", "Sprawdź aktualizacje przy starcie programu"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Proszę zaktualizować RustDesk Server Pro do wersji {} lub nowszej!"), + ("pull_group_failed_tip", "Błąd odświeżania grup"), + ("Filter by intersection", "Filtruj wg przecięcia"), + ("Remove wallpaper during incoming sessions", "Usuń tapetę podczas sesji przychodzących"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Ekran został odłączony, przełącz się na pierwszy ekran."), + ("No displays", "Brak ekranów"), + ("Open in new window", "Otwórz w nowym oknie"), + ("Show displays as individual windows", "Pokaż ekrany w osobnych oknach"), + ("Use all my displays for the remote session", "Użyj wszystkich moich ekranów do zdalnej sesji"), + ("selinux_tip", "SELinux jest włączony na Twoim urządzeniu, co może przeszkodzić w uruchomieniu RustDesk po stronie kontrolowanej."), + ("Change view", "Zmień widok"), + ("Big tiles", "Duże kafelki"), + ("Small tiles", "Małe kafelki"), + ("List", "Lista"), + ("Virtual display", "Wirtualne ekrany"), + ("Plug out all", "Odłącz wszystko"), + ("True color (4:4:4)", "True color (4:4:4)"), + ("Enable blocking user input", "Zablokuj wprowadzanie danych przez użytkownika"), + ("id_input_tip", "Możesz wprowadzić identyfikator, bezpośredni adres IP lub domenę z portem (:).\nJeżeli chcesz uzyskać dostęp do urządzenia na innym serwerze, dołącz adres serwera (@?key=, np. \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nJeżeli chcesz uzyskać dostęp do urządzenia na serwerze publicznym, wpisz \"@public\", klucz nie jest potrzebny dla serwera publicznego."), + ("privacy_mode_impl_mag_tip", "Tryb 1"), + ("privacy_mode_impl_virtual_display_tip", "Tryb 2"), + ("Enter privacy mode", "Wejdź w tryb prywatności"), + ("Exit privacy mode", "Wyjdź z trybu prywatności"), + ("idd_not_support_under_win10_2004_tip", "Pośredni sterownik ekranu nie jest obsługiwany. Wymagany jest system Windows 10 w wersji 2004 lub nowszej."), + ("input_source_1_tip", "Wejście źródła 1"), + ("input_source_2_tip", "Wejście źródła 2"), + ("Swap control-command key", "Zamiana przycisków sterujących myszki"), + ("swap-left-right-mouse", "Zamień przyciski myszki (LPM - RPM)"), + ("2FA code", "Kod 2FA"), + ("More", "Więcej"), + ("enable-2fa-title", "Włącz autoryzację dwuskładnikową (2FA)"), + ("enable-2fa-desc", "Skonfiguruj teraz swój moduł uwierzytelniający. Na telefonie lub komputerze możesz używać aplikacji autoryzującej, takiej jak Authy, Microsoft lub Google Authenticator.\n\nZeskanuj kod QR za pomocą aplikacji i wprowadź kod wyświetlany przez aplikację, aby włączyć uwierzytelnianie dwuskładnikowe."), + ("wrong-2fa-code", "Nie można zweryfikować kodu. Sprawdź, czy kod oraz ustawienia lokalnego czasu są prawidłowe."), + ("enter-2fa-title", "Autoryzacja dwuskładnikowa"), + ("Email verification code must be 6 characters.", "Kod weryfikacyjny wysłany e-mailem musi mieć 6 znaków."), + ("2FA code must be 6 digits.", "Kod 2FA musi zawierać 6 cyfr."), + ("Multiple Windows sessions found", "Znaleziono wiele sesji Windows"), + ("Please select the session you want to connect to", "Wybierz sesję, do której chcesz się podłączyć"), + ("powered_by_me", "Obsługiwane przez RustDesk"), + ("outgoing_only_desk_tip", "To jest spersonalizowana edycja. Możesz łączyć się z innymi urządzeniami, ale inne urządzenia nie mogą połączyć się z urządzeniem."), + ("preset_password_warning", "Ta spersonalizowana edycja jest wyposażona w wstępnie ustawione hasło. Każdy, kto zna to hasło, może uzyskać pełną kontrolę nad Twoim urządzeniem. Jeśli się tego nie spodziewałeś, natychmiast odinstaluj oprogramowanie."), + ("Security Alert", "Alert bezpieczeństwa"), + ("My address book", "Moja książka adresowa"), + ("Personal", "Osobiste"), + ("Owner", "Właściciel"), + ("Set shared password", "Ustaw hasło udostępniania"), + ("Exist in", "Istnieje w"), + ("Read-only", "Tylko do odczytu"), + ("Read/Write", "Odczyt/Zapis"), + ("Full Control", "Pełna kontrola"), + ("share_warning_tip", "Powyższe pola są udostępniane i widoczne dla innych."), + ("Everyone", "Wszyscy"), + ("ab_web_console_tip", "Więcej w konsoli web"), + ("allow-only-conn-window-open-tip", "Zezwalaj na połączenie tylko wtedy, gdy okno RustDesk jest otwarte"), + ("no_need_privacy_mode_no_physical_displays_tip", "Brak fizycznych wyświetlaczy, tryb prywatny nie jest potrzebny."), + ("Follow remote cursor", "Podążaj za zdalnym kursorem"), + ("Follow remote window focus", "Podążaj za aktywnością zdalnych okien"), + ("default_proxy_tip", "Domyślny protokół i port to Socks5 i 1080"), + ("no_audio_input_device_tip", "Nie znaleziono urządzenia audio."), + ("Incoming", "Przychodzące"), + ("Outgoing", "Wychodzące"), + ("Clear Wayland screen selection", "Wyczyść wybór ekranu Wayland"), + ("clear_Wayland_screen_selection_tip", "Po wyczyszczeniu wyboru ekranu, możesz wybrać, który ekran chcesz udostępnić."), + ("confirm_clear_Wayland_screen_selection_tip", "Na pewno wyczyścić wybór ekranu Wayland?"), + ("android_new_voice_call_tip", "Otrzymano nowe żądanie połączenia głosowego. Jeżeli je zaakceptujesz, dźwięk przełączy się na komunikację głosową."), + ("texture_render_tip", "Użyj renderowania tekstur, aby wygładzić zdjęcia. Możesz spróbować wyłączyć tę opcję, jeżeli napotkasz problemy z renderowaniem."), + ("Use texture rendering", "Użyj renderowania tekstur"), + ("Floating window", "Okno pływające"), + ("floating_window_tip", "Pozwala zachować usługę RustDesk w tle"), + ("Keep screen on", "Pozostaw ekran włączony"), + ("Never", "Nigdy"), + ("During controlled", "Podczas sterowania"), + ("During service is on", "Gdy usługa jest uruchomiona"), + ("Capture screen using DirectX", "Przechwytuj ekran używając DirectX"), + ("Back", "Wstecz"), + ("Apps", "Aplikacje"), + ("Volume up", "Głośniej"), + ("Volume down", "Ciszej"), + ("Power", "Zasilanie"), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Jeżeli włączysz tę funkcję, możesz otrzymać kod 2FA od swojego bota. Może on również działać jako powiadomienie o połączeniu."), + ("enable-bot-desc", "1. Otwórz czat z @BotFather.\n2. Wyślij polecenie \"/newbot\". Otrzymasz token do po wykonaniu tego kroku.\n3. Rozpocznij czat z nowo utworzonym botem. Wyślij wiadomość zaczynającą się od ukośnika (\"/\"),np. \"/hello\", aby go aktywować.\n"), + ("cancel-2fa-confirm-tip", "Na pewno chcesz anulować 2FA?"), + ("cancel-bot-confirm-tip", "Na pewno chcesz anulować bot Telegram?"), + ("About RustDesk", "O programie"), + ("Send clipboard keystrokes", "Wysyła naciśnięcia klawiszy ze schowka"), + ("network_error_tip", "Sprawdź swoje połączenie sieciowe, następnie kliknij Ponów."), + ("Unlock with PIN", "Odblokuj za pomocą PIN"), + ("Requires at least {} characters", "Wymaga co najmniej {} znaków"), + ("Wrong PIN", "Niewłaściwy PIN"), + ("Set PIN", "Ustaw PIN"), + ("Enable trusted devices", "Włącz zaufane urządzenia"), + ("Manage trusted devices", "Zarządzaj zaufanymi urządzeniami"), + ("Platform", "Platforma"), + ("Days remaining", "Pozostało dni"), + ("enable-trusted-devices-tip", "Omiń weryfikację 2FA dla zaufanych urządzeń"), + ("Parent directory", "Folder nadrzędny"), + ("Resume", "Wznów"), + ("Invalid file name", "Nieprawidłowa nazwa pliku"), + ("one-way-file-transfer-tip", "Jednokierunkowy transfer plików jest włączony po stronie kontrolowanej."), + ("Authentication Required", "Wymagana autoryzacja"), + ("Authenticate", "Uwierzytelnienie"), + ("web_id_input_tip", "Jeśli chcesz uzyskać dostęp do urządzenia na innym serwerze, dodaj adres serwera (@?key=) na przykład, \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nJeśli chcesz uzyskać dostęp do urządzenia na serwerze publicznym, wprowadź \"@public\", klucz nie jest wymagany dla serwera publicznego."), + ("Download", "Pobierz"), + ("Upload folder", "Wyślij folder"), + ("Upload files", "Wyślij pliki"), + ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), + ("Update client clipboard", "Uaktualnij schowek klienta"), + ("Untagged", "Bez etykiety"), + ("new-version-of-{}-tip", "Dostępna jest nowa wersja {}"), + ("Accessible devices", "Dostępne urządzenia"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Proszę zaktualizować zdalny klient RustDesk do wersji {} lub nowszej!"), + ("d3d_render_tip", "Kiedy włączenie renderowania D3D jest włączone, ekran zdalnej kontroli może być czarny w niektórych przypadkach"), + ("Use D3D rendering", "Użyj renderowania D3D"), + ("Printer", "Drukarka"), + ("printer-os-requirement-tip", "Funkcja drukowania zdalnego wymaga Windows 10 lub nowszego"), + ("printer-requires-installed-{}-client-tip", "Aby włączyć funkcję zdalnego drukowania, {} musi być zainstalowany na tym urządzeniu."), + ("printer-{}-not-installed-tip", "Drukarka {} nie jest zainstalowana."), + ("printer-{}-ready-tip", "Drukarka {} jest zainstalowana i gotowa do użycia."), + ("Install {} Printer", "Zainstaluj drukarkę {}"), + ("Outgoing Print Jobs", "Wychodzące zadania drukowania"), + ("Incoming Print Jobs", "Przychodzące zadania drukowania"), + ("Incoming Print Job", "Przychodzące zadanie drukowania"), + ("use-the-default-printer-tip", "Użyj domyślnej drukarki"), + ("use-the-selected-printer-tip", "Użyj wybranej drukarki"), + ("auto-print-tip", "Drukuj automatycznie używając wybranej drukarki"), + ("print-incoming-job-confirm-tip", "Otrzymałeś zadanie zdalnego drukowania. Chcesz wykonać je po swojej stronie?"), + ("remote-printing-disallowed-tile-tip", "Zdalne drukowanie niedozwolone"), + ("remote-printing-disallowed-text-tip", "Ustawienia uprawnień po zdalnej stronie uniemożliwiają zdalne drukowanie."), + ("save-settings-tip", "Zapisz ustawienia"), + ("dont-show-again-tip", "Nie pokazuj więcej"), + ("Take screenshot", "Zrób zrzut ekranu"), + ("Taking screenshot", "Tworzenie zrzutu ekranu"), + ("screenshot-merged-screen-not-supported-tip", "Łączenie zrzutów ekranu z wielu wyświetlaczy nie jest obecnie obsługiwane. Przełącz się na pojedynczy wyświetlacz i spróbuj ponownie."), + ("screenshot-action-tip", "Wybierz sposób kontynuacji zrzutu ekranu."), + ("Save as", "Zapisz jako"), + ("Copy to clipboard", "Kopiuj do schowka"), + ("Enable remote printer", "Włącz zdalne drukowanie"), + ("Downloading {}", "Pobieranie {}"), + ("{} Update", "Aktualizacja {}"), + ("{}-to-update-tip", "{} zostanie teraz zamknięty i zostanie zainstalowana nowa wersja."), + ("download-new-version-failed-tip", "Pobieranie nie powiodło się. Możesz spróbować ponownie lub kliknąć przycisk \"Pobierz\", aby pobrać ze strony programu i uaktualnić ręcznie."), + ("Auto update", "Automatyczna aktualizacja"), + ("update-failed-check-msi-tip", "Sprawdzenie metody instalacji nie powiodło się. Kliknij przycisk \"Pobierz\", aby pobrać ze strony wydania i uaktualnić ręcznie."), + ("websocket_tip", "Gdy używasz WebSocket, obsługiwane są tylko połączenia przekaźnikowe."), + ("Use WebSocket", "Użyj WebSocket"), + ("Trackpad speed", "Szybkość gładzika"), + ("Default trackpad speed", "Domyślna szybkość gładzika"), + ("Numeric one-time password", "Jednorazowe hasło cyfrowe"), + ("Enable IPv6 P2P connection", "Włącz połączenie P2P IPv6"), + ("Enable UDP hole punching", "Włącz tworzenie tunelu UDP"), + ("View camera", "Podgląd kamery"), + ("Enable camera", "Włącz kamerę"), + ("No cameras", "Brak kamer"), + ("view_camera_unsupported_tip", "Zdalne urządzenie nie obsługuje podglądu kamery."), + ("Terminal", "Terminal"), + ("Enable terminal", "Włącz terminal"), + ("New tab", "Nowa zakładka"), + ("Keep terminal sessions on disconnect", "Utrzymaj sesję terminala przy rozłączeniu"), + ("Terminal (Run as administrator)", "Terminal (uruchom jako administrator)"), + ("terminal-admin-login-tip", "Proszę wprowadzić użytkownika i hasło administratora kontrolowanego urządzenia."), + ("Failed to get user token.", "Błąd pobierania tokenu użytkownika."), + ("Incorrect username or password.", "Nieprawidłowy użytkownik lub hasło."), + ("The user is not an administrator.", "Użytkownik nie posiada praw administratora."), + ("Failed to check if the user is an administrator.", "Błąd sprawdzania, czy użytkownik jest administratorem."), + ("Supported only in the installed version.", "Wspierane tylko dla zainstalowanej aplikacji."), + ("elevation_username_tip", "Podaj nazwę użytkownika lub domena\\użytkownik"), + ("Preparing for installation ...", "Przygotowywanie do instalacji ..."), + ("Show my cursor", "Pokaż mój kursor"), + ("Scale custom", "Skala użytkownika"), + ("Custom scale slider", "Suwak skali użytkownika"), + ("Decrease", "Zmniejsz"), + ("Increase", "Zwiększ"), + ("Show virtual mouse", "Pokaż wirtualną mysz"), + ("Virtual mouse size", "Wielkość wirtualnego kursora myszy"), + ("Small", "Mały"), + ("Large", "Duży"), + ("Show virtual joystick", "Pokaz wirtualny joystick"), + ("Edit note", "Edytuj notatkę"), + ("Alias", "Alias"), + ("ScrollEdge", "Przewijanie na krawędzi"), + ("Allow insecure TLS fallback", "Zezwól na nie zweryfikowane połączenia TLS"), + ("allow-insecure-tls-fallback-tip", "Domyślnie RustDesk weryfikuje certyfikat serwera dla protokołów korzystających z TLS.\n Po włączeniu tej opcji, RustDesk pominie etap weryfikacji i będzie kontynuował działanie w przypadku negatywnej weryfikacji."), + ("Disable UDP", "Wyłącz protokół UDP"), + ("disable-udp-tip", "Kontroluje, czy używać wyłącznie protokołu TCP.\nPo włączeniu tej opcji, RustDesk nie będzie używać protokołu UDP 21116, zamiast niego będzie używać protokołu TCP 21116."), + ("server-oss-not-support-tip", "UWAGA: Serwer OSS RustDesk nie obsługuje tej funkcji."), + ("input note here", "Wstaw tutaj notatkę"), + ("note-at-conn-end-tip", "Poproś o notatkę po zakończeniu połączenia."), + ("Show terminal extra keys", "Pokaż dodatkowe klawisze terminala"), + ("Relative mouse mode", "Tryb przechwytywania myszy"), + ("rel-mouse-not-supported-peer-tip", "Zdalne urządzenie nie obsługuje trybu przechwytywania myszy"), + ("rel-mouse-not-ready-tip", "Tryb przechwytywania myszy nie jest gotowy"), + ("rel-mouse-lock-failed-tip", "Nie udało się przechwycić kursora myszy"), + ("rel-mouse-exit-{}-tip", "Aby wyłączyć tryb przechwytywania myszy, naciśnij {}"), + ("rel-mouse-permission-lost-tip", "Utracono uprawnienia do trybu przechwytywania myszy"), + ("Changelog", "Dziennik zmian"), + ("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"), + ("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"), + ("Continue with {}", "Kontynuuj z {}"), + ("Display Name", "Nazwa wyświetlana"), + ("password-hidden-tip", "Ustawiono (ukryto) stare hasło."), + ("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/pt_PT.rs b/vendor/rustdesk/src/lang/pt_PT.rs new file mode 100644 index 0000000..899c8da --- /dev/null +++ b/vendor/rustdesk/src/lang/pt_PT.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Estado"), + ("Your Desktop", "Ambiente de Trabalho"), + ("desk_tip", "O seu Ambiente de Trabalho pode ser acedido com este ID e palavra-passe."), + ("Password", "Senha"), + ("Ready", "Pronto"), + ("Established", "Estabelecido"), + ("connecting_status", "A ligar à rede do RustDesk..."), + ("Enable service", "Activar Serviço"), + ("Start service", "Iniciar Serviço"), + ("Service is running", "Serviço está activo"), + ("Service is not running", "Serviço não está activo"), + ("not_ready_status", "Indisponível. Por favor verifique a sua ligação"), + ("Control Remote Desktop", "Controle o Ambiente de Trabalho à distância"), + ("Transfer file", "Transferir Ficheiro"), + ("Connect", "Ligar"), + ("Recent sessions", "Sessões recentes"), + ("Address book", "Lista de Endereços"), + ("Confirmation", "Confirmação"), + ("TCP tunneling", "Túnel TCP"), + ("Remove", "Remover"), + ("Refresh random password", "Actualizar palavra-chave"), + ("Set your own password", "Configure a sua palavra-passe"), + ("Enable keyboard/mouse", "Activar Teclado/Rato"), + ("Enable clipboard", "Activar Área de Transferência"), + ("Enable file transfer", "Activar Transferência de Ficheiros"), + ("Enable TCP tunneling", "Activar Túnel TCP"), + ("IP Whitelisting", "Whitelist de IP"), + ("ID/Relay Server", "Servidor ID/Relay"), + ("Import server config", "Importar Configuração do Servidor"), + ("Export Server Config", "Exportar Configuração do Servidor"), + ("Import server configuration successfully", "Configuração do servidor importada com sucesso"), + ("Export server configuration successfully", ""), + ("Invalid server configuration", "Configuração do servidor inválida"), + ("Clipboard is empty", "A área de transferência está vazia"), + ("Stop service", "Parar serviço"), + ("Change ID", "Alterar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9, - (dash) e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), + ("Website", "Website"), + ("About", "Sobre"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Silenciar"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), + ("Audio Input", "Entrada de Áudio"), + ("Enhancements", "Melhorias"), + ("Hardware Codec", ""), + ("Adaptive bitrate", ""), + ("ID Server", "Servidor de ID"), + ("Relay Server", "Servidor de Relay"), + ("API Server", "Servidor da API"), + ("invalid_http", "deve iniciar com http:// ou https://"), + ("Invalid IP", "IP inválido"), + ("Invalid format", "Formato inválido"), + ("server_not_support", "Ainda não suportado pelo servidor"), + ("Not available", "Indisponível"), + ("Too frequent", "Muito frequente"), + ("Cancel", "Cancelar"), + ("Skip", "Passar"), + ("Close", "Fechar"), + ("Retry", "Tentar novamente"), + ("OK", "Confirmar"), + ("Password Required", "Palavra-chave Necessária"), + ("Please enter your password", "Por favor introduza a sua palavra-chave"), + ("Remember password", "Memorizar palavra-chave"), + ("Wrong Password", "Palavra-chave inválida"), + ("Do you want to enter again?", "Deseja tentar novamente??"), + ("Connection Error", "Erro de Ligação"), + ("Error", "Erro"), + ("Reset by the peer", "Reiniciado pelo destino"), + ("Connecting...", "A Ligar..."), + ("Connection in progress. Please wait.", "Ligação em progresso. Aguarde por favor."), + ("Please try 1 minute later", "Por favor tente após 1 minuto"), + ("Login Error", "Erro de Login"), + ("Successful", "Sucesso"), + ("Connected, waiting for image...", "Ligado. A aguardar pela imagem..."), + ("Name", "Nome"), + ("Type", "Tipo"), + ("Modified", "Modificado"), + ("Size", "Tamanho"), + ("Show Hidden Files", "Mostrar Ficheiros Ocultos"), + ("Receive", "Receber"), + ("Send", "Enviar"), + ("Refresh File", "Actualizar Ficheiro"), + ("Local", "Local"), + ("Remote", "Remoto"), + ("Remote Computer", "Computador Remoto"), + ("Local Computer", "Computador Local"), + ("Confirm Delete", "Confirmar Apagar"), + ("Delete", "Apagar"), + ("Properties", "Propriedades"), + ("Multi Select", "Selecção Múltipla"), + ("Select All", "Selecionar tudo"), + ("Unselect All", "Desmarcar todos"), + ("Empty Directory", "Directório Vazio"), + ("Not an empty directory", "Directório não está vazio"), + ("Are you sure you want to delete this file?", "Tem certeza que deseja apagar este ficheiro?"), + ("Are you sure you want to delete this empty directory?", "Tem certeza que deseja apagar este directório vazio?"), + ("Are you sure you want to delete the file of this directory?", "Tem certeza que deseja apagar este ficheiro deste directório?"), + ("Do this for all conflicts", "Fazer isto para todos os conflictos"), + ("This is irreversible!", "Isto é irreversível!"), + ("Deleting", "A apagar"), + ("files", "ficheiros"), + ("Waiting", "A aguardar"), + ("Finished", "Completo"), + ("Speed", "Velocidade"), + ("Custom Image Quality", "Qualidade Visual Personalizada"), + ("Privacy mode", "Modo privado"), + ("Block user input", "Bloquear entrada de utilizador"), + ("Unblock user input", "Desbloquear entrada de utilizador"), + ("Adjust Window", "Ajustar Janela"), + ("Original", "Original"), + ("Shrink", "Reduzir"), + ("Stretch", "Aumentar"), + ("Scrollbar", ""), + ("ScrollAuto", ""), + ("Good image quality", "Qualidade visual boa"), + ("Balanced", "Equilibrada"), + ("Optimize reaction time", "Optimizar tempo de reacção"), + ("Custom", ""), + ("Show remote cursor", "Mostrar cursor remoto"), + ("Show quality monitor", ""), + ("Disable clipboard", "Desabilitar área de transferência"), + ("Lock after session end", "Bloquear após o fim da sessão"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Inserir"), + ("Insert Lock", "Bloquear Inserir"), + ("Refresh", "Actualizar"), + ("ID does not exist", "ID não existente"), + ("Failed to connect to rendezvous server", "Falha ao ligar ao servidor de rendezvous"), + ("Please try later", "Por favor tente mais tarde"), + ("Remote desktop is offline", "Ambiente de trabalho remoto está desligado"), + ("Key mismatch", "Chaves incompatíveis"), + ("Timeout", "Tempo esgotado"), + ("Failed to connect to relay server", "Falha ao ligar ao servidor de relay"), + ("Failed to connect via rendezvous server", "Falha ao ligar ao servidor de rendezvous"), + ("Failed to connect via relay server", "Falha ao ligar através do servidor de relay"), + ("Failed to make direct connection to remote desktop", "Falha ao fazer ligação directa ao desktop remoto"), + ("Set Password", "Definir palavra-chave"), + ("OS Password", "Senha do SO"), + ("install_tip", "Devido ao UAC, o RustDesk não funciona correctamente em alguns casos. Para evitar o UAC, por favor clique no botão abaixo para instalar o RustDesk no sistema."), + ("Click to upgrade", "Clique para atualizar"), + ("Configure", "Configurar"), + ("config_acc", "Para controlar o seu Ambiente de Trabalho remotamente, é preciso conceder ao RustDesk permissões de \"Acessibilidade\"."), + ("config_screen", "Para aceder ao seu Ambiente de Trabalho remotamente, é preciso conceder ao RustDesk permissões de \"Gravar a Tela\"/"), + ("Installing ...", "A Instalar ..."), + ("Install", "Instalar"), + ("Installation", "Instalação"), + ("Installation Path", "Caminho da Instalação"), + ("Create start menu shortcuts", "Criar atalhos no menu iniciar"), + ("Create desktop icon", "Criar ícone no ambiente de trabalho"), + ("agreement_tip", "Ao iniciar a instalação, você concorda com o acordo de licença."), + ("Accept and Install", "Aceitar e Instalar"), + ("End-user license agreement", "Acordo de licença do utilizador final"), + ("Generating ...", "A Gerar ..."), + ("Your installation is lower version.", "A sua instalação é de uma versão anterior."), + ("not_close_tcp_tip", "Não feche esta janela enquanto estiver a utilizar o túnel"), + ("Listening ...", "A escuta ..."), + ("Remote Host", "Host Remoto"), + ("Remote Port", "Porta Remota"), + ("Action", "Acção"), + ("Add", "Adicionar"), + ("Local Port", "Porta Local"), + ("Local Address", "Endereço local"), + ("Change Local Port", "Alterar porta local"), + ("setup_server_tip", "Para uma ligação mais rápida, por favor configure seu próprio servidor"), + ("Too short, at least 6 characters.", "Muito curto, pelo menos 6 caracteres."), + ("The confirmation is not identical.", "A confirmação não é idêntica."), + ("Permissions", "Permissões"), + ("Accept", "Aceitar"), + ("Dismiss", "Dispensar"), + ("Disconnect", "Desconectar"), + ("Enable file copy and paste", "Permitir copiar e mover ficheiros"), + ("Connected", "Ligado"), + ("Direct and encrypted connection", "Ligação directa e encriptada"), + ("Relayed and encrypted connection", "Ligação via relay e encriptada"), + ("Direct and unencrypted connection", "Ligação direta e não encriptada"), + ("Relayed and unencrypted connection", "Ligação via relay e não encriptada"), + ("Enter Remote ID", "Introduza o ID Remoto"), + ("Enter your password", "Introduza a sua palavra-chave"), + ("Logging in...", "A efectuar Login..."), + ("Enable RDP session sharing", "Activar partilha de sessão RDP"), + ("Auto Login", "Login Automático (Somente válido se você activou \"Bloquear após o fim da sessão\")"), + ("Enable direct IP access", "Activar Acesso IP Directo"), + ("Rename", "Renomear"), + ("Space", "Espaço"), + ("Create desktop shortcut", "Criar Atalho no Ambiente de Trabalho"), + ("Change Path", "Alterar Caminho"), + ("Create Folder", "Criar Diretório"), + ("Please enter the folder name", "Por favor introduza o nome do diretório"), + ("Fix it", "Reparar"), + ("Warning", "Aviso"), + ("Login screen using Wayland is not supported", "Tela de Login com Wayland não é suportada"), + ("Reboot required", "Reinicialização necessária"), + ("Unsupported display server", "Servidor de display não suportado"), + ("x11 expected", "x11 em falha"), + ("Port", ""), + ("Settings", "Configurações"), + ("Username", "Nome de utilizador"), + ("Invalid port", "Porta inválida"), + ("Closed manually by the peer", "Fechada manualmente pelo destino"), + ("Enable remote configuration modification", "Habilitar modificações de configuração remotas"), + ("Run without install", "Executar sem instalar"), + ("Connect via relay", ""), + ("Always connect via relay", "Sempre conectar via relay"), + ("whitelist_tip", "Somente IPs na whitelist podem me acessar"), + ("Login", "Login"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), + ("Logout", "Sair"), + ("Tags", "Tags"), + ("Search ID", "Procurar ID"), + ("whitelist_sep", "Separado por vírcula, ponto-e-vírgula, espaços ou nova linha"), + ("Add ID", "Adicionar ID"), + ("Add Tag", "Adicionar Tag"), + ("Unselect all tags", "Desselecionar todas as tags"), + ("Network error", "Erro de rede"), + ("Username missed", "Nome de utilizador em falta"), + ("Password missed", "Palavra-chave em falta"), + ("Wrong credentials", "Nome de utilizador ou palavra-chave incorrectos"), + ("The verification code is incorrect or has expired", ""), + ("Edit Tag", "Editar Tag"), + ("Forget Password", "Esquecer Palavra-chave"), + ("Favorites", "Favoritos"), + ("Add to Favorites", "Adicionar aos Favoritos"), + ("Remove from Favorites", "Remover dos Favoritos"), + ("Empty", "Vazio"), + ("Invalid folder name", "Nome de diretório inválido"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Discovered", "Descoberto"), + ("install_daemon_tip", "Para inicialização junto do sistema, deve instalar o serviço de sistema."), + ("Remote ID", "ID Remoto"), + ("Paste", "Colar"), + ("Paste here?", "Colar aqui?"), + ("Are you sure to close the connection?", "Tem certeza que deseja fechar a ligação?"), + ("Download new version", "Transferir nova versão"), + ("Touch mode", "Modo toque"), + ("Mouse mode", "Modo rato"), + ("One-Finger Tap", "Toque com um dedo"), + ("Left Mouse", "Botão esquerdo do rato"), + ("One-Long Tap", "Um toque longo"), + ("Two-Finger Tap", "Toque com dois dedos"), + ("Right Mouse", "Botão direito do rato"), + ("One-Finger Move", "Mover com um dedo"), + ("Double Tap & Move", "Toque duplo & mover"), + ("Mouse Drag", "Arrastar com o rato"), + ("Three-Finger vertically", "Três dedos verticalmente"), + ("Mouse Wheel", "Roda do rato"), + ("Two-Finger Move", "Mover com dois dedos"), + ("Canvas Move", "Mover Tela"), + ("Pinch to Zoom", "Clique para ampliar"), + ("Canvas Zoom", "Zoom na Tela"), + ("Reset canvas", "Reiniciar tela"), + ("No permission of file transfer", "Sem permissões de transferência de ficheiro"), + ("Note", "Nota"), + ("Connection", "Ligação"), + ("Share screen", "Partilhar ecrã"), + ("Chat", "Conversar"), + ("Total", "Total"), + ("items", "itens"), + ("Selected", "Seleccionado"), + ("Screen Capture", "Captura de Ecran"), + ("Input Control", "Controle de Entrada"), + ("Audio Capture", "Captura de Áudio"), + ("Do you accept?", "Aceita?"), + ("Open System Setting", "Abrir Configurações do Sistema"), + ("How to get Android input permission?", "Como activar a permissão de entrada do Android?"), + ("android_input_permission_tip1", "Para que um dispositivo remoto controle o seu dispositivo Android via rato ou toque, você precisa permitir que o RustDesk use o serviço \"Acessibilidade\"."), + ("android_input_permission_tip2", "Por favor vá para a próxima página de configuração do sistema, encontre e entre [Serviços Instalados], ACTIVE o serviço [RustDesk Input]."), + ("android_new_connection_tip", "Nova requisição de controle recebida, solicita o controle do seu dispositivo atual."), + ("android_service_will_start_tip", "Activar a Captura de Ecran irá automaticamente inicializar o serviço, permitindo que outros dispositivos solicitem uma ligação deste dispositivo."), + ("android_stop_service_tip", "Fechar o serviço irá automaticamente fechar todas as ligações estabelecidas."), + ("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor actualize para o Android 10 ou maior."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), + ("Account", ""), + ("Overwrite", "Substituir"), + ("This file exists, skip or overwrite this file?", "Este ficheiro já existe, ignorar ou substituir este ficheiro?"), + ("Quit", "Saída"), + ("Help", "Ajuda"), + ("Failed", "Falhou"), + ("Succeeded", "Conseguiu"), + ("Someone turns on privacy mode, exit", "Alguém activou o modo de privacidade, desligue"), + ("Unsupported", "Sem suporte"), + ("Peer denied", "Remoto negado"), + ("Please install plugins", "Por favor instale plugins"), + ("Peer exit", "Saída do Remoto"), + ("Failed to turn off", "Falha ao desligar"), + ("Turned off", "Desligado"), + ("Language", "Linguagem"), + ("Keep RustDesk background service", "Manter o serviço RustDesk em funcionamento"), + ("Ignore Battery Optimizations", "Ignorar optimizações de Bateria"), + ("android_open_battery_optimizations_tip", ""), + ("Start on boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), + ("Connection not allowed", "Ligação não autorizada"), + ("Legacy mode", ""), + ("Map mode", ""), + ("Translate mode", ""), + ("Use permanent password", "Utilizar palavra-chave permanente"), + ("Use both passwords", "Utilizar ambas as palavras-chave"), + ("Set permanent password", "Definir palavra-chave permanente"), + ("Enable remote restart", "Activar reiniciar remoto"), + ("Restart remote device", "Reiniciar Dispositivo Remoto"), + ("Are you sure you want to restart", "Tem a certeza que pretende reiniciar"), + ("Restarting remote device", "A reiniciar sistema remoto"), + ("remote_restarting_tip", ""), + ("Copied", ""), + ("Exit Fullscreen", "Sair da tela cheia"), + ("Fullscreen", "Tela cheia"), + ("Mobile Actions", "Ações para celular"), + ("Select Monitor", "Selecionar monitor"), + ("Control Actions", "Ações de controle"), + ("Display Settings", "Configurações do visor"), + ("Ratio", "Razão"), + ("Image Quality", "Qualidade da imagem"), + ("Scroll Style", "Estilo de rolagem"), + ("Show Toolbar", ""), + ("Hide Toolbar", ""), + ("Direct Connection", "Conexão direta"), + ("Relay Connection", "Conexão de relé"), + ("Secure Connection", "Conexão segura"), + ("Insecure Connection", "Conexão insegura"), + ("Scale original", "Escala original"), + ("Scale adaptive", "Escala adaptável"), + ("General", ""), + ("Security", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Light Theme", ""), + ("Dark", ""), + ("Light", ""), + ("Follow System", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable audio", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Pin Toolbar", ""), + ("Unpin Toolbar", ""), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), + ("Enable recording session", ""), + ("Enable LAN discovery", ""), + ("Deny LAN discovery", ""), + ("Write a message", ""), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), + ("Disconnected", "Desconectado"), + ("Other", "Outro"), + ("Confirm before closing multiple tabs", "Confirme antes de fechar vários separadores"), + ("Keyboard Settings", "Configurações do teclado"), + ("Full Access", "Controlo total"), + ("Screen Share", ""), + ("ubuntu-21-04-required", "Wayland requer Ubuntu 21.04 ou versão superior."), + ("wayland-requires-higher-linux-version", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do peer)."), + ("Show RustDesk", ""), + ("This PC", ""), + ("or", ""), + ("Elevate", ""), + ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), + ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), + ("Skipped", ""), + ("Add to address book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), + ("Select a peer", ""), + ("Select peers", ""), + ("Plugins", ""), + ("Uninstall", ""), + ("Update", ""), + ("Enable", ""), + ("Disable", ""), + ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), + ("Collapse toolbar", ""), + ("Accept and Elevate", ""), + ("accept_and_elevate_btn_tooltip", ""), + ("clipboard_wait_response_timeout_tip", ""), + ("Incoming connection", ""), + ("Outgoing connection", ""), + ("Exit", ""), + ("Open", ""), + ("logout_tip", ""), + ("Service", ""), + ("Start", ""), + ("Stop", ""), + ("exceed_max_devices", ""), + ("Sync with recent sessions", ""), + ("Sort tags", ""), + ("Open connection in new tab", ""), + ("Move tab to new window", ""), + ("Can not be empty", ""), + ("Already exists", ""), + ("Change Password", ""), + ("Refresh Password", ""), + ("ID", ""), + ("Grid View", ""), + ("List View", ""), + ("Select", ""), + ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), + ("synced_peer_readded_tip", ""), + ("Change Color", ""), + ("Primary Color", ""), + ("HSV Color", ""), + ("Installation Successful!", ""), + ("Installation failed!", ""), + ("Reverse mouse wheel", ""), + ("{} sessions", ""), + ("scam_title", ""), + ("scam_text1", ""), + ("scam_text2", ""), + ("Don't show again", ""), + ("I Agree", ""), + ("Decline", ""), + ("Timeout in minutes", ""), + ("auto_disconnect_option_tip", ""), + ("Connection failed due to inactivity", ""), + ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), + ("pull_group_failed_tip", ""), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), + ("idd_not_support_under_win10_2004_tip", ""), + ("input_source_1_tip", ""), + ("input_source_2_tip", ""), + ("Swap control-command key", ""), + ("swap-left-right-mouse", ""), + ("2FA code", ""), + ("More", ""), + ("enable-2fa-title", ""), + ("enable-2fa-desc", ""), + ("wrong-2fa-code", ""), + ("enter-2fa-title", ""), + ("Email verification code must be 6 characters.", ""), + ("2FA code must be 6 digits.", ""), + ("Multiple Windows sessions found", ""), + ("Please select the session you want to connect to", ""), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("share_warning_tip", ""), + ("Everyone", ""), + ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", ""), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", ""), + ("Floating window", ""), + ("floating_window_tip", ""), + ("Keep screen on", ""), + ("Never", ""), + ("During controlled", ""), + ("During service is on", ""), + ("Capture screen using DirectX", ""), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Ver câmara"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", "Escala personalizada"), + ("Custom scale slider", "Controlo deslizante de escala personalizada"), + ("Decrease", "Diminuir"), + ("Increase", "Aumentar"), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", ""), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/ptbr.rs b/vendor/rustdesk/src/lang/ptbr.rs new file mode 100644 index 0000000..4eb2c15 --- /dev/null +++ b/vendor/rustdesk/src/lang/ptbr.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Seu Computador"), + ("desk_tip", "Seu computador pode ser acessado com este ID e senha."), + ("Password", "Senha"), + ("Ready", "Pronto"), + ("Established", "Estabelecido"), + ("connecting_status", "Conectando à rede do RustDesk..."), + ("Enable service", "Habilitar Serviço"), + ("Start service", "Iniciar Serviço"), + ("Service is running", "Serviço está em execução"), + ("Service is not running", "Serviço não está em execução"), + ("not_ready_status", "Não está pronto. Por favor verifique sua conexão"), + ("Control Remote Desktop", "Controle um Computador Remoto"), + ("Transfer file", "Transferir Arquivos"), + ("Connect", "Conectar"), + ("Recent sessions", "Sessões Recentes"), + ("Address book", "Lista de Endereços"), + ("Confirmation", "Confirmação"), + ("TCP tunneling", "Tunelamento TCP"), + ("Remove", "Remover"), + ("Refresh random password", "Atualizar senha aleatória"), + ("Set your own password", "Configure sua própria senha"), + ("Enable keyboard/mouse", "Habilitar teclado/mouse"), + ("Enable clipboard", "Habilitar Área de Transferência"), + ("Enable file transfer", "Habilitar Transferência de Arquivos"), + ("Enable TCP tunneling", "Habilitar Tunelamento TCP"), + ("IP Whitelisting", "Lista de IPs Confiáveis"), + ("ID/Relay Server", "Servidor ID/Relay"), + ("Import server config", "Importar Configuração do Servidor"), + ("Export Server Config", "Exportar Configuração do Servidor"), + ("Import server configuration successfully", "Configuração do servidor importada com sucesso"), + ("Export server configuration successfully", "Configuração do servidor exportada com sucesso"), + ("Invalid server configuration", "Configuração do servidor inválida"), + ("Clipboard is empty", "A área de transferência está vazia"), + ("Stop service", "Parar serviço"), + ("Change ID", "Alterar ID"), + ("Your new ID", "Seu novo ID"), + ("length %min% to %max%", "tamanho %min% para %max%"), + ("starts with a letter", "começa com uma letra"), + ("allowed characters", "caracteres permitidos"), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9, - (dash) e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), + ("Website", "Website"), + ("About", "Sobre"), + ("Slogan_tip", "Feito de coração neste mundo caótico!"), + ("Privacy Statement", "Declaração de Privacidade"), + ("Mute", "Desativar som"), + ("Build Date", "Data da Build"), + ("Version", "Versão"), + ("Home", "Início"), + ("Audio Input", "Entrada de Áudio"), + ("Enhancements", "Melhorias"), + ("Hardware Codec", "Codec de hardware"), + ("Adaptive bitrate", "Taxa de bits adaptável"), + ("ID Server", "Servidor de ID"), + ("Relay Server", "Servidor de Relay"), + ("API Server", "Servidor da API"), + ("invalid_http", "deve iniciar com http:// ou https://"), + ("Invalid IP", "IP inválido"), + ("Invalid format", "Formato inválido"), + ("server_not_support", "Ainda não suportado pelo servidor"), + ("Not available", "Indisponível"), + ("Too frequent", "Muito frequente"), + ("Cancel", "Cancelar"), + ("Skip", "Pular"), + ("Close", "Fechar"), + ("Retry", "Tentar novamente"), + ("OK", "OK"), + ("Password Required", "Senha necessária"), + ("Please enter your password", "Por favor informe sua senha"), + ("Remember password", "Lembrar senha"), + ("Wrong Password", "Senha incorreta"), + ("Do you want to enter again?", "Você deseja conectar novamente?"), + ("Connection Error", "Erro de conexão"), + ("Error", "Erro"), + ("Reset by the peer", "Reiniciado pelo parceiro"), + ("Connecting...", "Conectando..."), + ("Connection in progress. Please wait.", "Conexão em progresso. Aguarde por favor."), + ("Please try 1 minute later", "Por favor tente após 1 minuto"), + ("Login Error", "Erro de login"), + ("Successful", "Sucesso"), + ("Connected, waiting for image...", "Conectado. Aguardando pela imagem..."), + ("Name", "Nome"), + ("Type", "Tipo"), + ("Modified", "Modificado"), + ("Size", "Tamanho"), + ("Show Hidden Files", "Mostrar Arquivos Ocultos"), + ("Receive", "Receber"), + ("Send", "Enviar"), + ("Refresh File", "Atualizar Arquivo"), + ("Local", "Local"), + ("Remote", "Remoto"), + ("Remote Computer", "Computador Remoto"), + ("Local Computer", "Computador Local"), + ("Confirm Delete", "Confirmar Apagar"), + ("Delete", "Apagar"), + ("Properties", "Propriedades"), + ("Multi Select", "Seleção múltipla"), + ("Select All", "Selecionar tudo"), + ("Unselect All", "Desmarcar tudo"), + ("Empty Directory", "Diretório vazio"), + ("Not an empty directory", "Diretório não está vazio"), + ("Are you sure you want to delete this file?", "Tem certeza que deseja apagar este arquivo?"), + ("Are you sure you want to delete this empty directory?", "Tem certeza que deseja apagar este diretório vazio?"), + ("Are you sure you want to delete the file of this directory?", "Tem certeza que deseja apagar este arquivo deste diretório?"), + ("Do this for all conflicts", "Fazer isto para todos os conflitos"), + ("This is irreversible!", "Isso é irreversível!"), + ("Deleting", "Apagando"), + ("files", "arquivos"), + ("Waiting", "Aguardando"), + ("Finished", "Completo"), + ("Speed", "Velocidade"), + ("Custom Image Quality", "Qualidade Visual Personalizada"), + ("Privacy mode", "Modo privado"), + ("Block user input", "Bloquear entrada de usuário"), + ("Unblock user input", "Desbloquear entrada de usuário"), + ("Adjust Window", "Ajustar Janela"), + ("Original", "Original"), + ("Shrink", "Reduzir"), + ("Stretch", "Aumentar"), + ("Scrollbar", "Barra de rolagem"), + ("ScrollAuto", "Rolagem automática"), + ("Good image quality", "Qualidade visual boa"), + ("Balanced", "Balanceada"), + ("Optimize reaction time", "Otimizar tempo de resposta"), + ("Custom", "Personalizado"), + ("Show remote cursor", "Mostrar cursor remoto"), + ("Show quality monitor", "Exibir monitor de qualidade"), + ("Disable clipboard", "Desabilitar área de transferência"), + ("Lock after session end", "Bloquear após o fim da sessão"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Inserir"), + ("Insert Lock", "Bloquear computador"), + ("Refresh", "Atualizar"), + ("ID does not exist", "ID não existe"), + ("Failed to connect to rendezvous server", "Falha ao conectar ao servidor de rendezvous"), + ("Please try later", "Por favor tente mais tarde"), + ("Remote desktop is offline", "O computador remoto está offline"), + ("Key mismatch", "Chaves incompatíveis"), + ("Timeout", "Tempo esgotado"), + ("Failed to connect to relay server", "Falha ao conectar ao servidor de relay"), + ("Failed to connect via rendezvous server", "Falha ao conectar ao servidor de rendezvous"), + ("Failed to connect via relay server", "Falha ao conectar através do servidor de relay"), + ("Failed to make direct connection to remote desktop", "Falha ao fazer conexão direta ao desktop remoto"), + ("Set Password", "Definir Senha"), + ("OS Password", "Senha do SO"), + ("install_tip", "Devido ao UAC, o RustDesk não funciona corretamente como o lado remoto em alguns casos. Para evitar o UAC, por favor clique no botão abaixo para instalar o RustDesk no sistema."), + ("Click to upgrade", "Clique para fazer o upgrade"), + ("Configure", "Configurar"), + ("config_acc", "Para controlar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Acessibilidade\"."), + ("config_screen", "Para acessar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\"/"), + ("Installing ...", "Instalando ..."), + ("Install", "Instalar"), + ("Installation", "Instalação"), + ("Installation Path", "Caminho da Instalação"), + ("Create start menu shortcuts", "Criar atalhos no Menu Iniciar"), + ("Create desktop icon", "Criar ícone na Área de Trabalho"), + ("agreement_tip", "Ao iniciar a instalação, você concorda com o acordo de licença."), + ("Accept and Install", "Aceitar e Instalar"), + ("End-user license agreement", "Acordo de licença do usuário final"), + ("Generating ...", "Gerando ..."), + ("Your installation is lower version.", "Sua instalação é de uma versão menor."), + ("not_close_tcp_tip", "Não feche esta janela enquanto estiver utilizando o túnel"), + ("Listening ...", "Escutando ..."), + ("Remote Host", "Host Remoto"), + ("Remote Port", "Porta Remota"), + ("Action", "Ação"), + ("Add", "Adicionar"), + ("Local Port", "Porta Local"), + ("Local Address", "Endereço Local"), + ("Change Local Port", "Alterar Porta Local"), + ("setup_server_tip", "Para uma conexão mais rápida, por favor configure seu próprio servidor"), + ("Too short, at least 6 characters.", "Muito curto, pelo menos 6 caracteres."), + ("The confirmation is not identical.", "A confirmação não é idêntica."), + ("Permissions", "Permissões"), + ("Accept", "Aceitar"), + ("Dismiss", "Dispensar"), + ("Disconnect", "Desconectar"), + ("Enable file copy and paste", "Permitir copiar e colar arquivos"), + ("Connected", "Conectado"), + ("Direct and encrypted connection", "Conexão direta e criptografada"), + ("Relayed and encrypted connection", "Conexão via relay e criptografada"), + ("Direct and unencrypted connection", "Conexão direta e não criptografada"), + ("Relayed and unencrypted connection", "Conexão via relay e não criptografada"), + ("Enter Remote ID", "Informe o ID Remoto"), + ("Enter your password", "Informe sua senha"), + ("Logging in...", "Fazendo Login..."), + ("Enable RDP session sharing", "Habilitar compartilhamento de sessão RDP"), + ("Auto Login", "Login Automático (Somente válido se você habilitou \"Bloquear após o fim da sessão\")"), + ("Enable direct IP access", "Habilitar Acesso IP Direto"), + ("Rename", "Renomear"), + ("Space", "Espaço"), + ("Create desktop shortcut", "Criar Atalho na Área de Trabalho"), + ("Change Path", "Alterar Caminho"), + ("Create Folder", "Criar Diretório"), + ("Please enter the folder name", "Por favor informe o nome do diretório"), + ("Fix it", "Corrigir"), + ("Warning", "Aviso"), + ("Login screen using Wayland is not supported", "Tela de Login utilizando Wayland não é suportada"), + ("Reboot required", "Reinicialização necessária"), + ("Unsupported display server", "Servidor de display não suportado"), + ("x11 expected", "x11 esperado"), + ("Port", "Porta"), + ("Settings", "Configurações"), + ("Username", "Nome de usuário"), + ("Invalid port", "Porta inválida"), + ("Closed manually by the peer", "Fechada manualmente pelo parceiro"), + ("Enable remote configuration modification", "Habilitar modificações de configuração remotas"), + ("Run without install", "Executar sem instalar"), + ("Connect via relay", "Conectar via relay"), + ("Always connect via relay", "Sempre conectar via relay"), + ("whitelist_tip", "Somente IPs confiáveis podem me acessar"), + ("Login", "Login"), + ("Verify", "Verificar"), + ("Remember me", "Lembrar de mim"), + ("Trust this device", "Confiar neste dispositivo"), + ("Verification code", "Código de verificação"), + ("verification_tip", "Um novo dispositivo foi detectado e um código de verificação foi enviado para o endereço de e-mail registrado, insira o código de verificação para continuar o login."), + ("Logout", "Sair"), + ("Tags", "Tags"), + ("Search ID", "Pesquisar ID"), + ("whitelist_sep", "Separado por vírcula, ponto-e-vírgula, espaços ou nova linha"), + ("Add ID", "Adicionar ID"), + ("Add Tag", "Adicionar Tag"), + ("Unselect all tags", "Desmarcar todas as tags"), + ("Network error", "Erro de rede"), + ("Username missed", "Nome de usuário requerido"), + ("Password missed", "Senha requerida"), + ("Wrong credentials", "Nome de usuário ou senha incorretos"), + ("The verification code is incorrect or has expired", "O código de verificação está incorreto ou expirou"), + ("Edit Tag", "Editar Tag"), + ("Forget Password", "Esquecer Senha"), + ("Favorites", "Favoritos"), + ("Add to Favorites", "Adicionar aos Favoritos"), + ("Remove from Favorites", "Remover dos Favoritos"), + ("Empty", "Vazio"), + ("Invalid folder name", "Nome de diretório inválido"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Discovered", "Descoberto"), + ("install_daemon_tip", "Para inicialização junto ao sistema, você deve instalar o serviço de sistema."), + ("Remote ID", "ID Remoto"), + ("Paste", "Colar"), + ("Paste here?", "Colar aqui?"), + ("Are you sure to close the connection?", "Tem certeza que deseja fechar a conexão?"), + ("Download new version", "Baixar nova versão"), + ("Touch mode", "Modo toque"), + ("Mouse mode", "Modo mouse"), + ("One-Finger Tap", "Toque com um dedo"), + ("Left Mouse", "Botão esquerdo do mouse"), + ("One-Long Tap", "Um toque longo"), + ("Two-Finger Tap", "Toque com dois dedos"), + ("Right Mouse", "Botão direito do mouse"), + ("One-Finger Move", "Mover com um dedo"), + ("Double Tap & Move", "Toque duplo & mover"), + ("Mouse Drag", "Arrastar com o mouse"), + ("Three-Finger vertically", "Três dedos verticalmente"), + ("Mouse Wheel", "Roda do Mouse"), + ("Two-Finger Move", "Mover com dois dedos"), + ("Canvas Move", "Mover Tela"), + ("Pinch to Zoom", "Pinçar para Zoom"), + ("Canvas Zoom", "Zoom na tela"), + ("Reset canvas", "Reiniciar tela"), + ("No permission of file transfer", "Sem permissão para transferência de arquivo"), + ("Note", "Nota"), + ("Connection", "Conexão"), + ("Share screen", "Compartilhar Tela"), + ("Chat", "Chat"), + ("Total", "Total"), + ("items", "itens"), + ("Selected", "Selecionado"), + ("Screen Capture", "Captura de Tela"), + ("Input Control", "Controle de Entrada"), + ("Audio Capture", "Captura de Áudio"), + ("Do you accept?", "Você aceita?"), + ("Open System Setting", "Abrir Configurações do Sistema"), + ("How to get Android input permission?", "Como habilitar a permissão de entrada do Android?"), + ("android_input_permission_tip1", "Para que um dispositivo remoto controle seu dispositivo Android via mouse ou toque, você precisa permitir que o RustDesk use o serviço \"Acessibilidade\"."), + ("android_input_permission_tip2", "Por favor vá para a próxima página de configuração do sistema, encontre e entre [Serviços Instalados], HABILITE o serviço [RustDesk Input]."), + ("android_new_connection_tip", "Nova requisição de controle recebida, solicita o controle de seu dispositivo atual."), + ("android_service_will_start_tip", "Habilitar a Captura de Tela irá automaticamente inicalizar o serviço, permitindo que outros dispositivos solicitem uma conexão deste dispositivo."), + ("android_stop_service_tip", "Fechar o serviço irá automaticamente fechar todas as conexões estabelecidas."), + ("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor atualize para o Android 10 ou superior."), + ("android_start_service_tip", "Toque em [Iniciar serviço] ou habilite a permissão [Captura de tela] para iniciar o serviço de compartilhamento de tela."), + ("android_permission_may_not_change_tip", "As permissões para conexões estabelecidas podem não ser alteradas instantaneamente até que seja reconectado."), + ("Account", "Conta"), + ("Overwrite", "Substituir"), + ("This file exists, skip or overwrite this file?", "Este arquivo existe, pular ou substituir este arquivo?"), + ("Quit", "Sair"), + ("Help", "Ajuda"), + ("Failed", "Falhou"), + ("Succeeded", "Sucesso"), + ("Someone turns on privacy mode, exit", "Alguém habilitou o modo de privacidade, sair"), + ("Unsupported", "Não suportado"), + ("Peer denied", "Parceiro negou"), + ("Please install plugins", "Por favor instale plugins"), + ("Peer exit", "Parceiro saiu"), + ("Failed to turn off", "Falha ao desligar"), + ("Turned off", "Desligado"), + ("Language", "Idioma"), + ("Keep RustDesk background service", "Manter o serviço do RustDesk executando em segundo plano"), + ("Ignore Battery Optimizations", "Ignorar otimizações de bateria"), + ("android_open_battery_optimizations_tip", "Abrir otimizações de bateria"), + ("Start on boot", "Iniciar na Inicialização"), + ("Start the screen sharing service on boot, requires special permissions", "Inicie o serviço de compartilhamento de tela na inicialização, requer permissões especiais"), + ("Connection not allowed", "Conexão não permitida"), + ("Legacy mode", "Modo legado"), + ("Map mode", "Modo mapa"), + ("Translate mode", "Modo traduzido"), + ("Use permanent password", "Utilizar senha permanente"), + ("Use both passwords", "Utilizar ambas as senhas"), + ("Set permanent password", "Configurar senha permanente"), + ("Enable remote restart", "Habilitar Reinicialização Remota"), + ("Restart remote device", "Reiniciar Dispositivo Remoto"), + ("Are you sure you want to restart", "Você tem certeza que deseja reiniciar?"), + ("Restarting remote device", "Reiniciando dispositivo remoto"), + ("remote_restarting_tip", "O dispositivo remoto está reiniciando, feche esta caixa de mensagem e reconecte com a senha permanente depois de um tempo"), + ("Copied", "Copiado"), + ("Exit Fullscreen", "Sair da Tela Cheia"), + ("Fullscreen", "Tela Cheia"), + ("Mobile Actions", "Ações móveis"), + ("Select Monitor", "Selecionar monitor"), + ("Control Actions", "Controlar ações"), + ("Display Settings", "Configurações de exibição"), + ("Ratio", "Proporção"), + ("Image Quality", "Qualidade de Imagem"), + ("Scroll Style", "Estilo de Rolagem"), + ("Show Toolbar", "Mostrar Barra de Ferramentas"), + ("Hide Toolbar", "Ocultar Barra de Ferramentas"), + ("Direct Connection", "Conexão Direta"), + ("Relay Connection", "Conexão via Relay"), + ("Secure Connection", "Conexão Segura"), + ("Insecure Connection", "Conexão Insegura"), + ("Scale original", "Escala original"), + ("Scale adaptive", "Escala adaptada"), + ("General", "Geral"), + ("Security", "Segurança"), + ("Theme", "Tema"), + ("Dark Theme", "Tema Escuro"), + ("Light Theme", "Tema Claro"), + ("Dark", "Escuro"), + ("Light", "Claro"), + ("Follow System", "Padrão do sistema"), + ("Enable hardware codec", "Habilitar codec de hardware"), + ("Unlock Security Settings", "Desbloquear configurações de segurança"), + ("Enable audio", "Habilitar áudio"), + ("Unlock Network Settings", "Desbloquear configurações de rede"), + ("Server", "Servidor"), + ("Direct IP Access", "Acesso direto por IP"), + ("Proxy", "Proxy"), + ("Apply", "Aplicar"), + ("Disconnect all devices?", "Desconectar todos os dispositivos?"), + ("Clear", "Limpar"), + ("Audio Input Device", "Dispositivo de entrada de áudio"), + ("Use IP Whitelisting", "Utilizar lista de IPs confiáveis"), + ("Network", "Rede"), + ("Pin Toolbar", "Fixar Barra de Ferramentas"), + ("Unpin Toolbar", "Desafixar Barra de Ferramentas"), + ("Recording", "Gravando"), + ("Directory", "Diretório"), + ("Automatically record incoming sessions", "Gravar automaticamente sessões de entrada"), + ("Automatically record outgoing sessions", "Gravar automaticamente sessões de saída"), + ("Change", "Alterar"), + ("Start session recording", "Iniciar gravação da sessão"), + ("Stop session recording", "Parar gravação da sessão"), + ("Enable recording session", "Habilitar gravação da sessão"), + ("Enable LAN discovery", "Habilitar descoberta da LAN"), + ("Deny LAN discovery", "Negar descoberta da LAN"), + ("Write a message", "Escrever uma mensagem"), + ("Prompt", "Prompt de comando"), + ("Please wait for confirmation of UAC...", "Favor aguardar a confirmação do UAC..."), + ("elevated_foreground_window_tip", "A janela atual da área de trabalho remota requer privilégios mais altos para operar, portanto, não é possível usar o mouse e o teclado temporariamente. Você pode solicitar ao usuário remoto para minimizar a janela atual ou clicar no botão de elevação na janela de gerenciamento de conexão. Para evitar esse problema, é recomendável instalar o software no dispositivo remoto."), + ("Disconnected", "Desconectado"), + ("Other", "Outro"), + ("Confirm before closing multiple tabs", "Confirmar antes de fechar múltiplas abas"), + ("Keyboard Settings", "Configurações de teclado"), + ("Full Access", "Acesso completo"), + ("Screen Share", "Compartilhamento de tela"), + ("ubuntu-21-04-required", "Wayland requer Ubuntu 21.04 ou versão superior."), + ("wayland-requires-higher-linux-version", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do parceiro)."), + ("Show RustDesk", "Exibir RustDesk"), + ("This PC", "Este Computador"), + ("or", "ou"), + ("Elevate", "Elevar"), + ("Zoom cursor", "Aumentar cursor"), + ("Accept sessions via password", "Aceitar sessões via senha"), + ("Accept sessions via click", "Aceitar sessões via clique"), + ("Accept sessions via both", "Aceitar sessões de ambos os modos"), + ("Please wait for the remote side to accept your session request...", "Por favor aguarde enquanto o cliente remoto aceita seu pedido de sessão..."), + ("One-time Password", "Senha de uso único"), + ("Use one-time password", "Usar senha de uso único"), + ("One-time password length", "Comprimento da senha de uso único"), + ("Request access to your device", "Solicitar acesso ao seu dispositivo"), + ("Hide connection management window", "Ocultar janela de gerenciamento de conexão"), + ("hide_cm_tip", "Permitir ocultação somente se aceitar sessões via senha e usar senha permanente"), + ("wayland_experiment_tip", "O suporte ao Wayland está em estágio experimental, use o X11 se precisar de acesso autônomo."), + ("Right click to select tabs", "Clique com o botão direito para selecionar as guias"), + ("Skipped", "Ignorado"), + ("Add to address book", "Adicionar ao livro de endereços"), + ("Group", "Grupo"), + ("Search", "Buscar"), + ("Closed manually by web console", "Fechado manualmente pelo console da web"), + ("Local keyboard type", "Tipo de teclado local"), + ("Select local keyboard type", "Selecione o tipo de teclado local"), + ("software_render_tip", "Se você tiver uma placa gráfica Nvidia e a janela remota fechar imediatamente após a conexão, instalar o driver nouveau e optar por usar a renderização de software pode ajudar. É necessário reiniciar o software."), + ("Always use software rendering", "Sempre utilizar renderização de software"), + ("config_input", "Para controlar a área de trabalho remota com teclado, você precisa conceder a permissão \"Monitoramento de entrada\" do RustDesk."), + ("config_microphone", "Para falar remotamente, você precisa conceder a permissão \"Gravar áudio\" do RustDesk."), + ("request_elevation_tip", "Você também pode solicitar elevação se houver alguém do lado remoto."), + ("Wait", "Espera"), + ("Elevation Error", "Erro de Elevação"), + ("Ask the remote user for authentication", "Peça autenticação ao usuário remoto"), + ("Choose this if the remote account is administrator", "Escolha isto se a conta remota for administrador"), + ("Transmit the username and password of administrator", "Transmita o nome de usuário e a senha do administrador"), + ("still_click_uac_tip", "Ainda requer que o usuário remoto clique em OK na janela UAC da execução do RustDesk."), + ("Request Elevation", "Pedir Elevação"), + ("wait_accept_uac_tip", "Aguarde até que o usuário remoto aceite a caixa de diálogo do UAC."), + ("Elevate successfully", "Elevado com sucesso"), + ("uppercase", "maiúsculo"), + ("lowercase", "minúsculo"), + ("digit", "dígito"), + ("special character", "caractere especial"), + ("length>=8", "tamanho>=8"), + ("Weak", "Fraco"), + ("Medium", "Médio"), + ("Strong", "Forte"), + ("Switch Sides", "Trocar de Lado"), + ("Please confirm if you want to share your desktop?", "Por favor, confirme se você deseja compartilhar sua área de trabalho?"), + ("Display", "Display"), + ("Default View Style", "Estilo de Visualização Padrão"), + ("Default Scroll Style", "Estilo de Rolagem Padrão"), + ("Default Image Quality", "Qualidade de Imagem Padrão"), + ("Default Codec", "Codec Padrão"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Automático"), + ("Other Default Options", "Outras Opções Padrão"), + ("Voice call", "Chamada de voz"), + ("Text chat", "Chat de texto"), + ("Stop voice call", "Parar chamada de voz"), + ("relay_hint_tip", "Pode não ser possível conectar diretamente, você pode tentar conectar via relay. \nAlém disso, se você quiser usar o relay em sua primeira tentativa, pode adicionar o sufixo \"/r\" ao ID ou selecionar a opção \"Sempre conectar via relay\" no cartão do parceiro."), + ("Reconnect", "Reconectar"), + ("Codec", "Codec"), + ("Resolution", "Resolução"), + ("No transfers in progress", "Nenhuma transferência em andamento"), + ("Set one-time password length", "Definir comprimento de senha descartável"), + ("RDP Settings", "Configurações RDP"), + ("Sort by", "Ordenar por"), + ("New Connection", "Nova Conexão"), + ("Restore", "Restaurar"), + ("Minimize", "Minimizar"), + ("Maximize", "Maximizar"), + ("Your Device", "Seu Dispositivo"), + ("empty_recent_tip", "Ops, não há sessões recentes!\nHora de planejar uma nova."), + ("empty_favorite_tip", "Ainda não há parceiros favoritos?\nVamos encontrar alguém para se conectar e adicioná-lo aos seus favoritos!"), + ("empty_lan_tip", "Ah não, parece que ainda não descobrimos nenhum parceiro."), + ("empty_address_book_tip", "Oh céus, parece que atualmente não há parceiros listados em seu catálogo de endereços."), + ("Empty Username", "Nome de Usuário vazio"), + ("Empty Password", "Senha Vazia"), + ("Me", "Eu"), + ("identical_file_tip", "Este arquivo é idêntico ao do parceiro."), + ("show_monitors_tip", "Mostrar monitores na barra de ferramentas"), + ("View Mode", "Modo de Visualização"), + ("login_linux_tip", "Você precisa fazer login na conta Linux remota para habilitar uma sessão de desktop X"), + ("verify_rustdesk_password_tip", "Verifique a senha do RustDesk"), + ("remember_account_tip", "Lembrar desta conta"), + ("os_account_desk_tip", "Esta conta é usada para fazer login no Sistema Operacional remoto e habilitar a sessão da área de trabalho em headless"), + ("OS Account", "Conta do Sistema Operacional"), + ("another_user_login_title_tip", "Outro usuário já está logado"), + ("another_user_login_text_tip", "Desconectar"), + ("xorg_not_found_title_tip", "Xorg não encontrado"), + ("xorg_not_found_text_tip", "Por favor, instale o Xorg"), + ("no_desktop_title_tip", "Nenhuma área de trabalho está disponível"), + ("no_desktop_text_tip", "Por favor, instale a área de trabalho do GNOME"), + ("No need to elevate", "Não há necessidade de elevar"), + ("System Sound", "Som do Sistema"), + ("Default", "Padrão"), + ("New RDP", "Novo RDP"), + ("Fingerprint", "Impressão Digital"), + ("Copy Fingerprint", "Copiar Impressão Digital"), + ("no fingerprints", "sem Impressões Digitais"), + ("Select a peer", "Selecione um parceiro"), + ("Select peers", "Selecione parceiros"), + ("Plugins", "Plugins"), + ("Uninstall", "Desinstalar"), + ("Update", "Atualizar"), + ("Enable", "Habilitar"), + ("Disable", "Desabilitar"), + ("Options", "Opções"), + ("resolution_original_tip", "Resolução original"), + ("resolution_fit_local_tip", "Adequar a resolução local"), + ("resolution_custom_tip", "Customizar resolução"), + ("Collapse toolbar", "Ocultar barra de ferramentas"), + ("Accept and Elevate", "Aceitar e elevar"), + ("accept_and_elevate_btn_tooltip", "Aceitar a conexão e elevar os privilégios do UAC."), + ("clipboard_wait_response_timeout_tip", "Tempo de espera para a resposta da área de transferência expirado."), + ("Incoming connection", "Conexão de entrada"), + ("Outgoing connection", "Conexão de saída"), + ("Exit", "Sair"), + ("Open", "Abrir"), + ("logout_tip", "Tem certeza que deseja sair?"), + ("Service", "Serviço"), + ("Start", "Iniciar"), + ("Stop", "Parar"), + ("exceed_max_devices", "Você atingiu o número máximo de dispositivos gerenciados."), + ("Sync with recent sessions", "Sincronizar com sessões recentes"), + ("Sort tags", "Classificar tags"), + ("Open connection in new tab", "Abrir conexão em uma nova aba"), + ("Move tab to new window", "Mover aba para uma nova janela"), + ("Can not be empty", "Não pode estar vazio"), + ("Already exists", "Já existe"), + ("Change Password", "Alterar senha"), + ("Refresh Password", "Atualizar senha"), + ("ID", "ID"), + ("Grid View", "Visualização em grade"), + ("List View", "Visualização em lista"), + ("Select", "Selecionar"), + ("Toggle Tags", "Alternar etiquetas"), + ("pull_ab_failed_tip", "Não foi possível atualizar o diretório"), + ("push_ab_failed_tip", "Não foi possível sincronizar o diretório com o servidor"), + ("synced_peer_readded_tip", "Os dispositivos presentes em sessões recentes serão sincronizados com o diretório."), + ("Change Color", "Alterar cor"), + ("Primary Color", "Cor principal"), + ("HSV Color", "Cor HSV"), + ("Installation Successful!", "Instalação bem-sucedida!"), + ("Installation failed!", "A instalação falhou!"), + ("Reverse mouse wheel", "Inverter rolagem do mouse"), + ("{} sessions", "{} sessões"), + ("scam_title", "Você pode estar sendo ENGANADO!"), + ("scam_text1", "Se você estiver ao telefone com alguém que NÃO conhece e em quem NÃO confia e essa pessoa pedir para você usar o RustDesk e iniciar o serviço, NÃO faça isso !! e desligue imediatamente."), + ("scam_text2", "Provavelmente são golpistas tentando roubar seu dinheiro ou informações privadas."), + ("Don't show again", "Não mostrar novamente"), + ("I Agree", "Eu concordo"), + ("Decline", "Recusar"), + ("Timeout in minutes", "Tempo limite em minutos"), + ("auto_disconnect_option_tip", "Encerrar sessões entrantes automaticamente por inatividade do usuário."), + ("Connection failed due to inactivity", "Conexão encerrada automaticamente por inatividade."), + ("Check for software update on startup", "Verificar atualizações do software ao iniciar"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Atualize o RustDesk Server Pro para a versão {} ou superior."), + ("pull_group_failed_tip", "Não foi possível atualizar o grupo."), + ("Filter by intersection", "Filtrar por interseção"), + ("Remove wallpaper during incoming sessions", "Remover papel de parede durante sessão remota"), + ("Test", "Teste"), + ("display_is_plugged_out_msg", "A tela está desconectada. Mudando para a principal."), + ("No displays", "Nenhum display encontrado"), + ("Open in new window", "Abrir em uma nova janela"), + ("Show displays as individual windows", "Mostrar as telas como janelas individuais"), + ("Use all my displays for the remote session", "Usar todas as minhas telas para a sessão remota"), + ("selinux_tip", "O SELinux está ativado em seu dispositivo, o que pode impedir que o RustDesk funcione corretamente como dispositivo controlado."), + ("Change view", "Alterar visualização"), + ("Big tiles", "Ícones grandes"), + ("Small tiles", "Ícones pequenos"), + ("List", "Lista"), + ("Virtual display", "Display Virtual"), + ("Plug out all", "Desconectar tudo"), + ("True color (4:4:4)", "Cor verdadeira (4:4:4)"), + ("Enable blocking user input", "Habilitar bloqueio da entrada do usuário"), + ("id_input_tip", "Você pode inserir um ID, um IP direto ou um domínio com uma porta (:).\nPara acessar um dispositivo em outro servidor, adicione o IP do servidor (@?key=), por exemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nPara acessar um dispositivo em um servidor público, insira \"@public\", a chave não é necessária para um servidor público."), + ("privacy_mode_impl_mag_tip", "Modo 1"), + ("privacy_mode_impl_virtual_display_tip", "Modo 2"), + ("Enter privacy mode", "Entrar no modo privado"), + ("Exit privacy mode", "Sair do modo privado"), + ("idd_not_support_under_win10_2004_tip", "O driver de tela indireto não é suportado. É necessário o Windows 10, versão 2004 ou superior."), + ("input_source_1_tip", "Fonte de entrada 1"), + ("input_source_2_tip", "Fonte de entrada 2"), + ("Swap control-command key", "Trocar teclas Control e Comando"), + ("swap-left-right-mouse", "Trocar botões esquerdo e direito do mouse"), + ("2FA code", "Código 2FA"), + ("More", "Mais"), + ("enable-2fa-title", "Habilitar autenticação em duas etapas"), + ("enable-2fa-desc", "Configure seu autenticador agora. Você pode usar um aplicativo de autenticação como Authy, Microsoft ou Google Authenticator em seu telefone ou computador. Escaneie o código QR com seu aplicativo e insira o código mostrado para habilitar a autenticação em duas etapas."), + ("wrong-2fa-code", "Código inválido. Verifique se o código e as configurações de horário estão corretas."), + ("enter-2fa-title", "Autenticação em duas etapas"), + ("Email verification code must be 6 characters.", "O código de verificação por e-mail deve ter 6 caracteres."), + ("2FA code must be 6 digits.", "O código 2FA deve ter 6 dígitos."), + ("Multiple Windows sessions found", "Múltiplas sessões de janela encontradas"), + ("Please select the session you want to connect to", "Por favor, selecione a sessão que você deseja se conectar"), + ("powered_by_me", "Desenvolvido por RustDesk"), + ("outgoing_only_desk_tip", "Esta é uma edição personalizada.\nVocê pode se conectar a outros dispositivos, mas eles não podem se conectar ao seu."), + ("preset_password_warning", "Atenção: esta edição personalizada vem com uma senha predefinida. Qualquer pessoa que a conhecer poderá controlar totalmente seu dispositivo. Se isso não for o que você deseja, desinstale o software imediatamente."), + ("Security Alert", "Alerta de Segurança"), + ("My address book", "Minha lista de contatos"), + ("Personal", "Pessoal"), + ("Owner", "Proprietário"), + ("Set shared password", "Definir senha compartilhada"), + ("Exist in", "Existe em"), + ("Read-only", "Apenas leitura"), + ("Read/Write", "Leitura/escrita"), + ("Full Control", "Controle total"), + ("share_warning_tip", "Os campos mostrados acima são compartilhados e visíveis por outras pessoas."), + ("Everyone", "Todos"), + ("ab_web_console_tip", "Mais opções no console web"), + ("allow-only-conn-window-open-tip", "Permitir conexões apenas quando a janela do RustDesk estiver aberta"), + ("no_need_privacy_mode_no_physical_displays_tip", "Sem telas físicas, o modo privado não é necessário"), + ("Follow remote cursor", "Seguir cursor remoto"), + ("Follow remote window focus", "Seguir janela remota ativa"), + ("default_proxy_tip", "O protocolo e a porta padrão são Socks5 e 1080"), + ("no_audio_input_device_tip", "Nenhum dispositivo de entrada de áudio encontrado"), + ("Incoming", "Entrada"), + ("Outgoing", "Saída"), + ("Clear Wayland screen selection", "Limpar seleção de tela do Wayland"), + ("clear_Wayland_screen_selection_tip", "Depois de limpar a seleção de tela, você pode selecioná-la novamente para compartilhar."), + ("confirm_clear_Wayland_screen_selection_tip", "Tem certeza que deseja limpar a seleção da tela do Wayland?"), + ("android_new_voice_call_tip", "Uma nova solicitação de chamada de voz foi recebida. Se você aceitar, o áudio será alternado para comunicação por voz."), + ("texture_render_tip", "Use renderização de textura para tornar as imagens mais suaves"), + ("Use texture rendering", "Usar renderização de textura"), + ("Floating window", "Janela flutuante"), + ("floating_window_tip", "Ajuda a manter o serviço RustDesk em segundo plano"), + ("Keep screen on", "Manter tela ligada"), + ("Never", "Nunca"), + ("During controlled", "Durante controle"), + ("During service is on", "Enquanto o serviço estiver ativo"), + ("Capture screen using DirectX", "Capturar tela usando DirectX"), + ("Back", "Voltar"), + ("Apps", "Apps"), + ("Volume up", "Aumentar volume"), + ("Volume down", "Diminuir volume"), + ("Power", "Energia"), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Se você ativar este recurso, poderá receber o código 2FA do seu bot. Ele também pode funcionar como uma notificação de conexão."), + ("enable-bot-desc", "1. Abra um chat com @BotFather.\n2. Envie o comando \"/newbot\". Você receberá um token após completar esta etapa.\n3. Inicie um chat com o seu bot recém-criado. Envie uma mensagem começando com uma barra invertida (\"/\"), como \"/hello\", para ativá-lo.\n"), + ("cancel-2fa-confirm-tip", "Tem certeza de que deseja cancelar a 2FA?"), + ("cancel-bot-confirm-tip", "Tem certeza de que deseja cancelar o bot do Telegram?"), + ("About RustDesk", "Sobre RustDesk"), + ("Send clipboard keystrokes", "Colar área de transferência"), + ("network_error_tip", "Por favor, verifique sua conexão de rede e clique em tentar novamente."), + ("Unlock with PIN", "Desbloquear com PIN"), + ("Requires at least {} characters", "São necessários pelo menos {} caracteres"), + ("Wrong PIN", "PIN Errado"), + ("Set PIN", "Definir PIN"), + ("Enable trusted devices", "Habilitar dispositivos confiáveis"), + ("Manage trusted devices", "Gerenciar dispositivos confiáveis"), + ("Platform", "Plataforma"), + ("Days remaining", "Dias restantes"), + ("enable-trusted-devices-tip", "Ignore a verificação de dois fatores em dispositivos confiáveis"), + ("Parent directory", "Diretório pai"), + ("Resume", "Continuar"), + ("Invalid file name", "Nome de arquivo inválido"), + ("one-way-file-transfer-tip", "A transferência de arquivos unidirecional está ativada no dispositivo controlado."), + ("Authentication Required", "Autenticação necessária"), + ("Authenticate", "Autenticar"), + ("web_id_input_tip", "Você pode inserir um ID no mesmo servidor; o acesso direto por IP não é suportado no cliente web.\nSe desejar acessar um dispositivo em outro servidor, por favor, adicione o endereço do servidor (@?key=), por exemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe desejar acessar um dispositivo em um servidor público, por favor, insira \"@public\", a chave não é necessária para servidores públicos."), + ("Download", "Baixar"), + ("Upload folder", "Carregar pasta"), + ("Upload files", "Carregar arquivos"), + ("Clipboard is synchronized", "A área de transferência está sincronizada"), + ("Update client clipboard", "Atualizar a área de transferência do cliente"), + ("Untagged", "Sem etiqueta"), + ("new-version-of-{}-tip", "Uma nova versão de {} está disponível"), + ("Accessible devices", "Dispositivos acessíveis"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualize o cliente RustDesk para a versão {} ou superior no lado remoto."), + ("d3d_render_tip", "Em algumas máquinas, a tela do controle remoto pode ficar preta ao usar a renderização D3D."), + ("Use D3D rendering", "Usar renderização D3D"), + ("Printer", "Impressora"), + ("printer-os-requirement-tip", "A função de impressão de saída requer Windows 10 ou superior."), + ("printer-requires-installed-{}-client-tip", "{} deve ser instalado neste dispositivo antes que você possa usar a impressão remota."), + ("printer-{}-not-installed-tip", "A impressora {} não está instalada."), + ("printer-{}-ready-tip", "A impressora {} está instalada e operacional."), + ("Install {} Printer", "Instalar impressora {}"), + ("Outgoing Print Jobs", "Trabalhos de impressão enviados"), + ("Incoming Print Jobs", "Trabalhos de impressão recebidos"), + ("Incoming Print Job", "Impressão recebida"), + ("use-the-default-printer-tip", "Usar impressora padrão"), + ("use-the-selected-printer-tip", "Usar impressora selecionada"), + ("auto-print-tip", "Imprimir automaticamente usando a impressora selecionada."), + ("print-incoming-job-confirm-tip", "O dispositivo remoto enviou uma impressão. Deseja imprimir?"), + ("remote-printing-disallowed-tile-tip", "Impressão remota não permitida"), + ("remote-printing-disallowed-text-tip", "As configurações do dispositivo controlado não permitem impressão remota."), + ("save-settings-tip", "Salvar configurações"), + ("dont-show-again-tip", "Não mostrar novamente"), + ("Take screenshot", "Capturar de tela"), + ("Taking screenshot", "Capturando tela"), + ("screenshot-merged-screen-not-supported-tip", "Mesclar a captura de tela de múltiplos monitores não é suportada no momento. Por favor, alterne para um único monitor e tente novamente."), + ("screenshot-action-tip", "Por favor, selecione como seguir com a captura de tela."), + ("Save as", "Salvar como"), + ("Copy to clipboard", "Copiar para área de transferência"), + ("Enable remote printer", "Habilitar impressora remota"), + ("Downloading {}", "Baixando {}"), + ("{} Update", "Atualização do {}"), + ("{}-to-update-tip", "{} será fechado agora para instalar a nova versão."), + ("download-new-version-failed-tip", "Falha no download. Você pode tentar novamente ou clicar no botão \"Download\" para baixar da página releases e atualizar manualmente."), + ("Auto update", "Atualização automática"), + ("update-failed-check-msi-tip", "Falha na verificação do método de instalação. Clique no botão \"Download\" para baixar da página releases e atualizar manualmente."), + ("websocket_tip", "Usando WebSocket, apenas conexões via relay são suportadas."), + ("Use WebSocket", "Usar WebSocket"), + ("Trackpad speed", "Velocidade do trackpad"), + ("Default trackpad speed", "Velocidade padrão do trackpad"), + ("Numeric one-time password", "Senha numérica de uso único"), + ("Enable IPv6 P2P connection", "Habilitar conexão IPv6 P2P"), + ("Enable UDP hole punching", "Habilitar UDP hole punching"), + ("View camera", "Visualizar câmera"), + ("Enable camera", "Ativar câmera"), + ("No cameras", "Sem câmeras"), + ("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."), + ("Terminal", "Terminal"), + ("Enable terminal", "Habilitar Terminal"), + ("New tab", "Nova aba"), + ("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"), + ("Terminal (Run as administrator)", "Terminal (Executar como administrador)"), + ("terminal-admin-login-tip", "Insira o nome do usuário e senha de administrador do dispositivo controlado."), + ("Failed to get user token.", "Falha ao obter token do usuário."), + ("Incorrect username or password.", "Usuário ou senha incorretos"), + ("The user is not an administrator.", "O usuário não é administrador"), + ("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"), + ("Supported only in the installed version.", "Funciona somente na versão instalada"), + ("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"), + ("Preparing for installation ...", "Preparando para instalação ..."), + ("Show my cursor", "Mostrar meu cursor"), + ("Scale custom", "Escala personalizada"), + ("Custom scale slider", "Controle deslizante de escala personalizada"), + ("Decrease", "Diminuir"), + ("Increase", "Aumentar"), + ("Show virtual mouse", "Mostrar mouse virtual"), + ("Virtual mouse size", "Tamanho do mouse virtual"), + ("Small", "Pequeno"), + ("Large", "Grande"), + ("Show virtual joystick", "Mostrar joystick virtual"), + ("Edit note", "Editar nota"), + ("Alias", "Apelido"), + ("ScrollEdge", "Rolagem nas bordas"), + ("Allow insecure TLS fallback", "Permitir fallback TLS inseguro"), + ("allow-insecure-tls-fallback-tip", "Por padrão, o RustDesk verifica o certificado do servidor para protocolos que usam TLS.\nCom esta opção habilitada, o RustDesk ignorará a verificação e prosseguirá em caso de falha."), + ("Disable UDP", "Desabilitar UDP"), + ("disable-udp-tip", "Controla se deve usar somente TCP.\nCom esta opção habilitada, o RustDesk não usará mais UDP 21116, TCP 21116 será usado no lugar."), + ("server-oss-not-support-tip", "NOTA: O servidor RustDesk OSS não inclui este recurso."), + ("input note here", "Insira uma nota aqui"), + ("note-at-conn-end-tip", "Solicitar nota ao final da conexão"), + ("Show terminal extra keys", "Mostrar teclas extras do terminal"), + ("Relative mouse mode", "Modo de Mouse Relativo"), + ("rel-mouse-not-supported-peer-tip", "O Modo de Mouse Relativo não é suportado pelo parceiro conectado."), + ("rel-mouse-not-ready-tip", "O Modo de Mouse Relativo ainda não está pronto. Por favor, tente novamente."), + ("rel-mouse-lock-failed-tip", "Falha ao bloquear o cursor. O Modo de Mouse Relativo foi desabilitado."), + ("rel-mouse-exit-{}-tip", "Pressione {} para sair."), + ("rel-mouse-permission-lost-tip", "Permissão de teclado revogada. O Modo Mouse Relativo foi desabilitado."), + ("Changelog", "Registro de alterações"), + ("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"), + ("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"), + ("Continue with {}", "Continuar com {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/ro.rs b/vendor/rustdesk/src/lang/ro.rs new file mode 100644 index 0000000..45b2268 --- /dev/null +++ b/vendor/rustdesk/src/lang/ro.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stare"), + ("Your Desktop", "Desktopul tău"), + ("desk_tip", "Desktopul tău poate fi accesat folosind ID-ul și parola de mai jos."), + ("Password", "Parolă"), + ("Ready", "Pregătit"), + ("Established", "Stabilit"), + ("connecting_status", "În curs de conectare la rețeaua RustDesk..."), + ("Enable service", "Activează serviciul"), + ("Start service", "Pornește serviciul"), + ("Service is running", "Serviciul rulează..."), + ("Service is not running", "Serviciul nu funcționează"), + ("not_ready_status", "Nepregătit. Verifică conexiunea la rețea."), + ("Control Remote Desktop", "Controlează desktopul la distanță"), + ("Transfer file", "Transferă fișiere"), + ("Connect", "Conectează-te"), + ("Recent sessions", "Sesiuni recente"), + ("Address book", "Agendă"), + ("Confirmation", "Confirmare"), + ("TCP tunneling", "Tunel TCP"), + ("Remove", "Elimină"), + ("Refresh random password", "Actualizează parola aleatorie"), + ("Set your own password", "Setează propria parolă"), + ("Enable keyboard/mouse", "Activează control tastatură/mouse"), + ("Enable clipboard", "Activează clipboard"), + ("Enable file transfer", "Activează transferul de fișiere"), + ("Enable TCP tunneling", "Activează tunelul TCP"), + ("IP Whitelisting", "Listă de IP-uri autorizate"), + ("ID/Relay Server", "Server de ID/retransmisie"), + ("Import server config", "Importă configurație server"), + ("Export Server Config", "Exportă configurație server"), + ("Import server configuration successfully", "Configurație server importată cu succes"), + ("Export server configuration successfully", "Configurație server exportată cu succes"), + ("Invalid server configuration", "Configurație server nevalidă"), + ("Clipboard is empty", "Clipboard gol"), + ("Stop service", "Oprește serviciul"), + ("Change ID", "Schimbă ID"), + ("Your new ID", "Noul tău ID"), + ("length %min% to %max%", "lungime între %min% și %max%"), + ("starts with a letter", "începe cu o literă"), + ("allowed characters", "caractere permise"), + ("id_change_tip", "Pot fi utilizate doar caractere a-z, A-Z, 0-9, - (dash), _ (bară jos). Primul caracter trebuie să fie a-z, A-Z. Lungimea trebuie să fie între 6 și 16 caractere."), + ("Website", "Site web"), + ("About", "Despre"), + ("Slogan_tip", "Făcut din inimă în lumea aceasta haotică!"), + ("Privacy Statement", "Politică de confidențialitate"), + ("Mute", "Dezactivează sunet"), + ("Build Date", "Dată build"), + ("Version", "Versiune"), + ("Home", "Acasă"), + ("Audio Input", "Intrări audio"), + ("Enhancements", "Îmbunătățiri"), + ("Hardware Codec", "Codec hardware"), + ("Adaptive bitrate", "Rată de biți adaptabilă"), + ("ID Server", "Server de ID"), + ("Relay Server", "Server de retransmisie"), + ("API Server", "Server API"), + ("invalid_http", "Trebuie să înceapă cu http:// sau https://"), + ("Invalid IP", "IP nevalid"), + ("Invalid format", "Format nevalid"), + ("server_not_support", "Încă nu este compatibil cu serverul"), + ("Not available", "Indisponibil"), + ("Too frequent", "Prea frecvent"), + ("Cancel", "Anulează"), + ("Skip", "Omite"), + ("Close", "Închide"), + ("Retry", "Reîncearcă"), + ("OK", "OK"), + ("Password Required", "Parolă necesară"), + ("Please enter your password", "Introdu parola"), + ("Remember password", "Memorează parola"), + ("Wrong Password", "Parolă incorectă"), + ("Do you want to enter again?", "Vrei să intri din nou?"), + ("Connection Error", "Eroare de conexiune"), + ("Error", "Eroare"), + ("Reset by the peer", "Conexiunea a fost închisă de dispozitivul pereche"), + ("Connecting...", "Se conectează..."), + ("Connection in progress. Please wait.", "Conectare în curs. Te rugăm așteaptă."), + ("Please try 1 minute later", "Reîncearcă într-un minut"), + ("Login Error", "Eroare de autentificare"), + ("Successful", "Succes"), + ("Connected, waiting for image...", "Conectat, se așteaptă transmiterea imaginii..."), + ("Name", "Denumire"), + ("Type", "Tip"), + ("Modified", "Modificat"), + ("Size", "Dimensiune"), + ("Show Hidden Files", "Afișează fișiere ascunse"), + ("Receive", "Primește"), + ("Send", "Trimite"), + ("Refresh File", "Actualizează fișier"), + ("Local", "Local"), + ("Remote", "La distanță"), + ("Remote Computer", "Computer la distanță"), + ("Local Computer", "Computer local"), + ("Confirm Delete", "Confirmă ștergerea"), + ("Delete", "Șterge"), + ("Properties", "Caracteristici"), + ("Multi Select", "Alegere multiplă"), + ("Select All", "Selectează tot"), + ("Unselect All", "Deselectează tot"), + ("Empty Directory", "Director gol"), + ("Not an empty directory", "Directorul nu este gol"), + ("Are you sure you want to delete this file?", "Sigur vrei să ștergi acest fișier?"), + ("Are you sure you want to delete this empty directory?", "Sigur vrei să ștergi acest director gol?"), + ("Are you sure you want to delete the file of this directory?", "Sigur vrei să ștergi fișierul din acest director?"), + ("Do this for all conflicts", "Aplică la toate conflictele"), + ("This is irreversible!", "Această acțiune este ireversibilă!"), + ("Deleting", "În curs de ștergere..."), + ("files", "fișiere"), + ("Waiting", "În așteptare..."), + ("Finished", "Finalizat"), + ("Speed", "Viteză"), + ("Custom Image Quality", "Setează calitatea imaginii"), + ("Privacy mode", "Mod privat"), + ("Block user input", "Blochează intervenție utilizator"), + ("Unblock user input", "Deblochează intervenție utilizator"), + ("Adjust Window", "Ajustează fereastra"), + ("Original", "Dimensiune originală"), + ("Shrink", "Micșorează"), + ("Stretch", "Extinde"), + ("Scrollbar", "Bară de derulare"), + ("ScrollAuto", "Derulare automată"), + ("Good image quality", "Calitate bună a imaginii"), + ("Balanced", "Calitate normală a imaginii"), + ("Optimize reaction time", "Timp de reacție optimizat"), + ("Custom", "Personalizat"), + ("Show remote cursor", "Afișează cursor la distanță"), + ("Show quality monitor", "Afișează detalii despre conexiune"), + ("Disable clipboard", "Dezactivează clipboard"), + ("Lock after session end", "Blochează după deconectare"), + ("Insert Ctrl + Alt + Del", "Introdu Ctrl + Alt + Del"), + ("Insert Lock", "Blochează computer"), + ("Refresh", "Reîmprospătează"), + ("ID does not exist", "ID neexistent"), + ("Failed to connect to rendezvous server", "Conectare la server rendezvous eșuată"), + ("Please try later", "Încearcă mai târziu"), + ("Remote desktop is offline", "Desktopul la distanță este offline"), + ("Key mismatch", "Nepotrivire cheie"), + ("Timeout", "Conexiune expirată"), + ("Failed to connect to relay server", "Conectare la server de retransmisie eșuată"), + ("Failed to connect via rendezvous server", "Conectare prin intermediul serverului rendezvous eșuată"), + ("Failed to connect via relay server", "Conectare prin intermediul serverului de retransmisie eșuată"), + ("Failed to make direct connection to remote desktop", "Imposibil de stabilit o conexiune directă cu desktopul la distanță"), + ("Set Password", "Setează parola"), + ("OS Password", "Parolă sistem"), + ("install_tip", "Din cauza restricțiilor CCU, este posibil ca RustDesk să nu funcționeze corespunzător. Pentru a evita acest lucru, dă clic pe butonul de mai jos pentru a instala RustDesk."), + ("Click to upgrade", "Dă clic pentru a face upgrade"), + ("Configure", "Configurează"), + ("config_acc", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Accesibilitate."), + ("config_screen", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Înregistrare ecran."), + ("Installing ...", "Se instalează..."), + ("Install", "Instalează"), + ("Installation", "Instalare"), + ("Installation Path", "Cale de instalare"), + ("Create start menu shortcuts", "Creează comenzi rapide în meniul Start"), + ("Create desktop icon", "Creează pictogramă pe desktop"), + ("agreement_tip", "Începerea procesului de instalare înseamnă acceptarea acordului de licență."), + ("Accept and Install", "Acceptă și instalează"), + ("End-user license agreement", "Acord de licență pentru utilizatorul final"), + ("Generating ...", "Se generează..."), + ("Your installation is lower version.", "Versiunea instalată este una inferioară."), + ("not_close_tcp_tip", "Nu închide această fereastră în timp ce folosești tunelul"), + ("Listening ...", "În așteptarea conexiunii tunel..."), + ("Remote Host", "Gazdă la distanță"), + ("Remote Port", "Port la distanță"), + ("Action", "Acțiune"), + ("Add", "Adaugă"), + ("Local Port", "Port local"), + ("Local Address", "Adresă locală"), + ("Change Local Port", "Modifică port local"), + ("setup_server_tip", "Pentru o conexiune mai rapidă, îți poți configura propriul server."), + ("Too short, at least 6 characters.", "Prea scurtă; trebuie cel puțin 6 caractere."), + ("The confirmation is not identical.", "Cele două intrări nu corespund."), + ("Permissions", "Permisiuni"), + ("Accept", "Acceptă"), + ("Dismiss", "Respinge"), + ("Disconnect", "Deconectează-te"), + ("Enable file copy and paste", "Permite copierea și lipirea fișierelor"), + ("Connected", "Conectat"), + ("Direct and encrypted connection", "Conexiune directă criptată"), + ("Relayed and encrypted connection", "Conexiune retransmisă criptată"), + ("Direct and unencrypted connection", "Conexiune directă necriptată"), + ("Relayed and unencrypted connection", "Conexiune retransmisă necriptată"), + ("Enter Remote ID", "Introdu ID-ul dispozitivului la distanță"), + ("Enter your password", "Introdu parola"), + ("Logging in...", "Se conectează..."), + ("Enable RDP session sharing", "Activează partajarea sesiunii RDP"), + ("Auto Login", "Conectare automată (validă doar dacă opțiunea Blocare după deconectare este selectată)"), + ("Enable direct IP access", "Activează accesul direct cu IP"), + ("Rename", "Redenumește"), + ("Space", "Spațiu"), + ("Create desktop shortcut", "Creează comandă rapidă de desktop"), + ("Change Path", "Schimbă calea"), + ("Create Folder", "Creează folder"), + ("Please enter the folder name", "Introdu numele folderului"), + ("Fix it", "Repară"), + ("Warning", "Avertisment"), + ("Login screen using Wayland is not supported", "Ecranele de conectare care folosesc Wayland nu sunt acceptate"), + ("Reboot required", "Repornire necesară"), + ("Unsupported display server", "Tipul de server de afișaj nu este acceptat"), + ("x11 expected", "Este necesar X11"), + ("Port", "Port"), + ("Settings", "Setări"), + ("Username", "Nume utilizator"), + ("Invalid port", "Port nevalid"), + ("Closed manually by the peer", "Conexiune închisă manual de dispozitivul pereche"), + ("Enable remote configuration modification", "Activează modificarea configurației de la distanță"), + ("Run without install", "Rulează fără a instala"), + ("Connect via relay", "Conectează-te prin retransmisie"), + ("Always connect via relay", "Conectează-te mereu prin retransmisie"), + ("whitelist_tip", "Doar adresele IP autorizate pot accesa acest dispozitiv"), + ("Login", "Conectează-te"), + ("Verify", "Verificare"), + ("Remember me", "Reține-mă"), + ("Trust this device", "Acest dispozitiv este de încredere"), + ("Verification code", "Cod de verificare"), + ("verification_tip", "Introdu codul de verificare trimis la adresa ta de e-mail sau generat de aplicația de autentificare."), + ("Logout", "Deconectează-te"), + ("Tags", "Etichete"), + ("Search ID", "Caută după ID"), + ("whitelist_sep", "Poți folosi ca separator virgula, punctul și virgula, spațiul sau linia nouă"), + ("Add ID", "Adaugă ID"), + ("Add Tag", "Adaugă etichetă"), + ("Unselect all tags", "Deselectează toate etichetele"), + ("Network error", "Eroare de rețea"), + ("Username missed", "Lipsește numele de utilizator"), + ("Password missed", "Lipsește parola"), + ("Wrong credentials", "Nume sau parolă greșită"), + ("The verification code is incorrect or has expired", "Codul de verificare este incorect sau a expirat"), + ("Edit Tag", "Modifică etichetă"), + ("Forget Password", "Parolă uitată"), + ("Favorites", "Favorite"), + ("Add to Favorites", "Adaugă la Favorite"), + ("Remove from Favorites", "Șterge din Favorite"), + ("Empty", "Gol"), + ("Invalid folder name", "Denumire folder nevalidă"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Discovered", "Descoperite"), + ("install_daemon_tip", "Pentru executare la pornirea sistemului, instalează serviciul de sistem."), + ("Remote ID", "ID dispozitiv la distanță"), + ("Paste", "Lipește"), + ("Paste here?", "Lipește aici?"), + ("Are you sure to close the connection?", "Sigur vrei să închizi conexiunea?"), + ("Download new version", "Descarcă noua versiune"), + ("Touch mode", "Mod tactil"), + ("Mouse mode", "Mod mouse"), + ("One-Finger Tap", "Apasă cu un deget"), + ("Left Mouse", "Clic stânga"), + ("One-Long Tap", "Apasă lung"), + ("Two-Finger Tap", "Apasă cu două degete"), + ("Right Mouse", "Clic dreapta"), + ("One-Finger Move", "Mișcă cu un deget"), + ("Double Tap & Move", "Apasă dublu și mișcă"), + ("Mouse Drag", "Tragere mouse"), + ("Three-Finger vertically", "Trei degete vertical"), + ("Mouse Wheel", "Rotiță mouse"), + ("Two-Finger Move", "Mișcă cu două degete"), + ("Canvas Move", "Mută ecran"), + ("Pinch to Zoom", "Apropie degetele pentru a mări"), + ("Canvas Zoom", "Mărire ecran"), + ("Reset canvas", "Reinițializează ecranul"), + ("No permission of file transfer", "Nicio permisiune pentru transferul de fișiere"), + ("Note", "Notă"), + ("Connection", "Conexiune"), + ("Share screen", "Partajează ecran"), + ("Chat", "Mesaje"), + ("Total", "Total"), + ("items", "elemente"), + ("Selected", "Selectat"), + ("Screen Capture", "Capturare ecran"), + ("Input Control", "Control intrări"), + ("Audio Capture", "Capturare audio"), + ("Do you accept?", "Accepți?"), + ("Open System Setting", "Deschide setări sistem"), + ("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe Android?"), + ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilizeze serviciul „Accesibilitate\"."), + ("android_input_permission_tip2", "Accesează următoarea pagină din Setări, deschide [Aplicații instalate] și pornește serviciul [RustDesk Input]."), + ("android_new_connection_tip", "Ai primit o nouă solicitare de controlare a dispozitivului actual."), + ("android_service_will_start_tip", "Activarea setării de capturare a ecranului va porni automat serviciul, permițând altor dispozitive să solicite conectarea la dispozitivul tău."), + ("android_stop_service_tip", "Închiderea serviciului va închide automat toate conexiunile stabilite."), + ("android_version_audio_tip", "Versiunea actuală de Android nu suportă captura audio. Fă upgrade la Android 10 sau la o versiune superioară."), + ("android_start_service_tip", "Apasă [Pornește serviciu] sau DESCHIDE [Capturare ecran] pentru a porni serviciul de partajare a ecranului."), + ("android_permission_may_not_change_tip", "Este posibil ca unele permisiuni să nu poată fi modificate în funcție de versiunea de Android."), + ("Account", "Cont"), + ("Overwrite", "Suprascrie"), + ("This file exists, skip or overwrite this file?", "Fișier deja existent. Omite sau suprascrie?"), + ("Quit", "Ieși"), + ("Help", "Ajutor"), + ("Failed", "Nereușit"), + ("Succeeded", "Reușit"), + ("Someone turns on privacy mode, exit", "Cineva activează modul privat, ieși din"), + ("Unsupported", "Neacceptat"), + ("Peer denied", "Dispozitiv pereche refuzat"), + ("Please install plugins", "Instalează pluginuri"), + ("Peer exit", "Ieșire dispozitiv pereche"), + ("Failed to turn off", "Dezactivare nereușită"), + ("Turned off", "Închis"), + ("Language", "Limbă"), + ("Keep RustDesk background service", "Rulează serviciul RustDesk în fundal"), + ("Ignore Battery Optimizations", "Ignoră optimizările de baterie"), + ("android_open_battery_optimizations_tip", "Pentru dezactivarea acestei funcții, accesează setările aplicației RustDesk, deschide secțiunea [Baterie] și deselectează [Fără restricții]."), + ("Start on boot", "Pornește la boot"), + ("Start the screen sharing service on boot, requires special permissions", "Pornește serviciul de partajare a ecranului la boot; necesită permisiuni speciale"), + ("Connection not allowed", "Conexiune neautorizată"), + ("Legacy mode", "Mod legacy"), + ("Map mode", "Mod hartă"), + ("Translate mode", "Mod traducere"), + ("Use permanent password", "Folosește parola permanentă"), + ("Use both passwords", "Folosește ambele parole"), + ("Set permanent password", "Setează parola permanentă"), + ("Enable remote restart", "Activează repornirea la distanță"), + ("Restart remote device", "Repornește dispozitivul la distanță"), + ("Are you sure you want to restart", "Sigur vrei să repornești dispozitivul?"), + ("Restarting remote device", "Se repornește dispozitivul la distanță"), + ("remote_restarting_tip", "Dispozitivul este în curs de repornire. Închide acest mesaj și reconectează-te cu parola permanentă după un timp."), + ("Copied", "Copiat"), + ("Exit Fullscreen", "Ieși din modul ecran complet"), + ("Fullscreen", "Ecran complet"), + ("Mobile Actions", "Bară de navigare"), + ("Select Monitor", "Selectează monitor"), + ("Control Actions", "Acțiuni de control"), + ("Display Settings", "Setări afișaj"), + ("Ratio", "Raport"), + ("Image Quality", "Calitate imagine"), + ("Scroll Style", "Stil de derulare"), + ("Show Toolbar", "Arată bară de instrumente"), + ("Hide Toolbar", "Ascunde bară de instrumente"), + ("Direct Connection", "Conexiune directă"), + ("Relay Connection", "Conexiune prin retransmisie"), + ("Secure Connection", "Conexiune securizată"), + ("Insecure Connection", "Conexiune nesecurizată"), + ("Scale original", "Dimensiune originală"), + ("Scale adaptive", "Scalare automată"), + ("General", "General"), + ("Security", "Securitate"), + ("Theme", "Temă"), + ("Dark Theme", "Temă întunecată"), + ("Light Theme", "Temă luminoasă"), + ("Dark", "Întunecată"), + ("Light", "Luminoasă"), + ("Follow System", "Temă sistem"), + ("Enable hardware codec", "Activează codec hardware"), + ("Unlock Security Settings", "Deblochează setările de securitate"), + ("Enable audio", "Activează audio"), + ("Unlock Network Settings", "Deblochează setările de rețea"), + ("Server", "Server"), + ("Direct IP Access", "Acces direct IP"), + ("Proxy", "Proxy"), + ("Apply", "Aplică"), + ("Disconnect all devices?", "Vrei să deconectezi toate dispozitivele?"), + ("Clear", "Golește"), + ("Audio Input Device", "Dispozitiv de intrare audio"), + ("Use IP Whitelisting", "Folosește lista de IP-uri autorizate"), + ("Network", "Rețea"), + ("Pin Toolbar", "Fixează bara de instrumente"), + ("Unpin Toolbar", "Detașează bara de instrumente"), + ("Recording", "Înregistrare"), + ("Directory", "Director"), + ("Automatically record incoming sessions", "Înregistrează automat sesiunile primite"), + ("Automatically record outgoing sessions", "Înregistrează automat sesiunile de ieșire"), + ("Change", "Modifică"), + ("Start session recording", "Începe înregistrarea"), + ("Stop session recording", "Oprește înregistrarea"), + ("Enable recording session", "Activează înregistrarea sesiunii"), + ("Enable LAN discovery", "Activează descoperirea LAN"), + ("Deny LAN discovery", "Interzice descoperirea LAN"), + ("Write a message", "Scrie un mesaj"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Așteaptă confirmarea CCU..."), + ("elevated_foreground_window_tip", "Fereastra actuală a dispozitivului la distanță necesită privilegii sporite pentru a funcționa, astfel că mouse-ul și tastatura nu pot fi folosite. Poți cere utilizatorului la distanță să minimizeze fereastra actuală sau să facă clic pe butonul de sporire a privilegiilor din fereastra de gestionare a conexiunilor. Pentru a evita această problemă, recomandăm instalarea software-ului pe dispozitivul la distanță."), + ("Disconnected", "Deconectat"), + ("Other", "Altele"), + ("Confirm before closing multiple tabs", "Confirmă înainte de a închide mai multe file"), + ("Keyboard Settings", "Setări tastatură"), + ("Full Access", "Acces total"), + ("Screen Share", "Partajare ecran"), + ("ubuntu-21-04-required", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."), + ("wayland-requires-higher-linux-version", "Wayland necesită o versiune superioară a distribuției Linux. Încearcă desktopul X11 sau schimbă sistemul de operare."), + ("xdp-portal-unavailable", "Portalul XDG Desktop nu este disponibil. Asigură-te că rulezi o sesiune Wayland cu suport pentru portal."), + ("JumpLink", "Afișează"), + ("Please Select the screen to be shared(Operate on the peer side).", "Partajează ecranul care urmează să fie partajat (operează din partea dispozitivului pereche)."), + ("Show RustDesk", "Afișează RustDesk"), + ("This PC", "Acest PC"), + ("or", "sau"), + ("Elevate", "Sporește privilegii"), + ("Zoom cursor", "Cursor lupă"), + ("Accept sessions via password", "Acceptă începerea sesiunii folosind parola"), + ("Accept sessions via click", "Acceptă începerea sesiunii dând clic"), + ("Accept sessions via both", "Acceptă începerea sesiunii folosind ambele moduri"), + ("Please wait for the remote side to accept your session request...", "Așteaptă ca solicitarea ta de conectare la distanță să fie acceptată..."), + ("One-time Password", "Parolă unică"), + ("Use one-time password", "Folosește parola unică"), + ("One-time password length", "Lungimea parolei unice"), + ("Request access to your device", "Solicitare de acces la dispozitivul tău"), + ("Hide connection management window", "Ascunde fereastra de gestionare a conexiunilor"), + ("hide_cm_tip", "Permite ascunderea ferestrei de gestionare doar dacă accepți începerea sesiunilor folosind parola permanentă"), + ("wayland_experiment_tip", "Wayland este acceptat doar într-o formă experimentală. Folosește X11 dacă nu ai nevoie de acces supravegheat."), + ("Right click to select tabs", "Dă clic dreapta pentru a selecta file"), + ("Skipped", "Ignorat"), + ("Add to address book", "Adaugă la agendă"), + ("Group", "Grup"), + ("Search", "Caută"), + ("Closed manually by web console", "Conexiune închisă manual de consola web"), + ("Local keyboard type", "Tastatură locală"), + ("Select local keyboard type", "Selectează tastatura locală"), + ("software_render_tip", "Dacă ai o placă video Nvidia și folosești Linux, iar fereastra cu conexiunea la distanță se închide imediat după conectare, îți sugerăm să instalezi driverul gratuit Nouveau și să folosești randarea de software. Este necesară repornirea."), + ("Always use software rendering", "Utilizează mereu randarea de software"), + ("config_input", "Pentru a controla desktopul la distanță folosind tastatura, trebuie să acorzi RustDesk permisiunea Monitorizare intrare"), + ("config_microphone", "Pentru a desfășura un apel vocal, este nevoie să acorzi RustDesk permisiunea Înregistrare audio."), + ("request_elevation_tip", "Poți solicita sporirea privilegiilor și dacă este cineva la desktopul la distanță."), + ("Wait", "În curs..."), + ("Elevation Error", "Eroare la sporirea privilegiilor"), + ("Ask the remote user for authentication", "Solicită utilizatorului de la distanță să se autentifice"), + ("Choose this if the remote account is administrator", "Alege asta dacă contul la distanță este un cont de administrator"), + ("Transmit the username and password of administrator", "Transmite numele de utilizator și parola administratorului"), + ("still_click_uac_tip", "Este necesar ca utilizatorul la distanță să confirme în fereastra CCU din RustDesk care rulează."), + ("Request Elevation", "Solicită sporirea privilegiilor"), + ("wait_accept_uac_tip", "Așteaptă ca utilizatorul la distanță să accepte dialogul CCU."), + ("Elevate successfully", "Sporirea privilegiilor realizată cu succes"), + ("uppercase", "majuscule"), + ("lowercase", "minuscule"), + ("digit", "cifre"), + ("special character", "caractere speciale"), + ("length>=8", "lungime>=8"), + ("Weak", "Slabă"), + ("Medium", "Medie"), + ("Strong", "Puternică"), + ("Switch Sides", "Inversează controlul"), + ("Please confirm if you want to share your desktop?", "Confirmi că dorești să îți partajezi desktopul?"), + ("Display", "Afișare"), + ("Default View Style", "Stilul implicit de vizualizare"), + ("Default Scroll Style", "Stilul implicit de derulare"), + ("Default Image Quality", "Calitatea implicită a imaginii"), + ("Default Codec", "Codec implicit"), + ("Bitrate", "Rată de biți"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Alte opțiuni implicite"), + ("Voice call", "Apel vocal"), + ("Text chat", "Conversație text"), + ("Stop voice call", "Încheie apel vocal"), + ("relay_hint_tip", "Este posibil să nu te poți conecta direct; poți încerca să te conectezi prin retransmisie. De asemenea, dacă dorești să te conectezi direct prin retransmisie, poți adăuga sufixul „/r\" la ID sau să bifezi opțiunea Conectează-te mereu prin retransmisie."), + ("Reconnect", "Reconectează-te"), + ("Codec", "Codec"), + ("Resolution", "Rezoluție"), + ("No transfers in progress", "Niciun transfer nu este în desfășurare"), + ("Set one-time password length", "Definește lungimea parolei unice"), + ("RDP Settings", "Setări RDP"), + ("Sort by", "Sortează după"), + ("New Connection", "Conexiune nouă"), + ("Restore", "Restaurează"), + ("Minimize", "Minimizează"), + ("Maximize", "Maximizează"), + ("Your Device", "Dispozitivul tău"), + ("empty_recent_tip", "Hopa! Nu există nicio sesiune recentă.\nPoate ar trebui să plănuiești una chiar acum!"), + ("empty_favorite_tip", "Încă nu ai niciun dispozitiv pereche favorit?\nHai să-ți găsim pe cineva cu care să te conectezi, iar apoi poți adăuga dispozitivul la Favorite!"), + ("empty_lan_tip", "Of! S-ar părea că încă nu am descoperit niciun dispozitiv."), + ("empty_address_book_tip", "Măi să fie! Se pare că deocamdată nu figurează niciun dispozitiv în agenda ta."), + ("Empty Username", "Nume utilizator nespecificat"), + ("Empty Password", "Parolă nespecificată"), + ("Me", "Eu"), + ("identical_file_tip", "Acest fișier este identic cu cel al dispozitivului pereche."), + ("show_monitors_tip", "Afișează monitoare în bara de instrumente"), + ("View Mode", "Mod vizualizare"), + ("login_linux_tip", "Este necesar să te conectezi la contul de Linux de la distanță pentru a începe o sesiune cu un desktop care folosește X11"), + ("verify_rustdesk_password_tip", "Verifică parola RustDesk"), + ("remember_account_tip", "Reține contul"), + ("os_account_desk_tip", "Acest cont este utilizat pentru conectarea la sistemul de operare la distanță și începerea sesiunii cu desktopul în modul fără afișaj."), + ("OS Account", "Cont OS"), + ("another_user_login_title_tip", "Un alt utilizator este deja conectat"), + ("another_user_login_text_tip", "Deconectare"), + ("xorg_not_found_title_tip", "Xorg nu a fost găsit"), + ("xorg_not_found_text_tip", "Instalează Xorg"), + ("no_desktop_title_tip", "Nu este disponibil niciun mediu desktop"), + ("no_desktop_text_tip", "Instalează mediul desktop GNOME"), + ("No need to elevate", "Nu sunt necesare permisiuni de administrator"), + ("System Sound", "Sunet sistem"), + ("Default", "Implicit"), + ("New RDP", "RDP nou"), + ("Fingerprint", "Amprentă digitală"), + ("Copy Fingerprint", "Copiază amprenta digitală"), + ("no fingerprints", "Nicio amprentă digitală"), + ("Select a peer", "Selectează un dispozitiv pereche"), + ("Select peers", "Selectează dispozitive pereche"), + ("Plugins", "Pluginuri"), + ("Uninstall", "Dezinstalează"), + ("Update", "Actualizează"), + ("Enable", "Activează"), + ("Disable", "Dezactivează"), + ("Options", "Opțiuni"), + ("resolution_original_tip", "Rezoluție originală"), + ("resolution_fit_local_tip", "Adaptează la rezoluția locală"), + ("resolution_custom_tip", "Rezoluție personalizată"), + ("Collapse toolbar", "Restrânge bara de instrumente"), + ("Accept and Elevate", "Acceptă și sporește privilegii"), + ("accept_and_elevate_btn_tooltip", "Acceptă conectarea și sporește privilegiile CCU"), + ("clipboard_wait_response_timeout_tip", "Procesul a expirat așteptând un răspuns la copiere"), + ("Incoming connection", "Conexiune de intrare"), + ("Outgoing connection", "Conexiune de ieșire"), + ("Exit", "Ieși"), + ("Open", "Deschide"), + ("logout_tip", "Sigur vrei să te deconectezi?"), + ("Service", "Serviciu"), + ("Start", "Pornește"), + ("Stop", "Oprește"), + ("exceed_max_devices", "Numărul maxim de dispozitive a fost depășit"), + ("Sync with recent sessions", "Sincronizează cu sesiunile recente"), + ("Sort tags", "Sortează etichete"), + ("Open connection in new tab", "Deschide conexiunea într-o filă nouă"), + ("Move tab to new window", "Mută fila într-o fereastră nouă"), + ("Can not be empty", "Nu poate fi gol"), + ("Already exists", "Există deja"), + ("Change Password", "Schimbă parola"), + ("Refresh Password", "Reîmprospătează parola"), + ("ID", "ID"), + ("Grid View", "Vizualizare grilă"), + ("List View", "Vizualizare listă"), + ("Select", "Selectează"), + ("Toggle Tags", "Comută etichete"), + ("pull_ab_failed_tip", "Sincronizarea agendei a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), + ("push_ab_failed_tip", "Salvarea agendei pe server a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), + ("synced_peer_readded_tip", "Dispozitivele pereche eliminate au fost re-adăugate automat din sesiunile recente."), + ("Change Color", "Schimbă culoarea"), + ("Primary Color", "Culoare principală"), + ("HSV Color", "Culoare HSV"), + ("Installation Successful!", "Instalare reușită!"), + ("Installation failed!", "Instalare eșuată!"), + ("Reverse mouse wheel", "Inversează rotiță mouse"), + ("{} sessions", "{} sesiuni"), + ("scam_title", "Avertisment de securitate"), + ("scam_text1", "Escrocii se pot da drept angajați ai asistenței tehnice și îți pot solicita să instalezi sau să rulezi RustDesk pentru a-ți accesa dispozitivul."), + ("scam_text2", "Dacă nu ai contactat tu primul asistența tehnică, te rugăm să închizi această aplicație imediat."), + ("Don't show again", "Nu mai afișa"), + ("I Agree", "Sunt de acord"), + ("Decline", "Refuză"), + ("Timeout in minutes", "Timp de expirare în minute"), + ("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."), + ("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"), + ("Check for software update on startup", "Verifică actualizări la pornire"), + ("upgrade_rustdesk_server_pro_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."), + ("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), + ("Filter by intersection", "Filtrează prin intersecție"), + ("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Monitorul selectat a fost deconectat. Sesiunea continuă pe monitorul disponibil."), + ("No displays", "Niciun monitor"), + ("Open in new window", "Deschide în fereastră nouă"), + ("Show displays as individual windows", "Afișează monitoarele ca ferestre individuale"), + ("Use all my displays for the remote session", "Folosește toate monitoarele mele pentru sesiunea la distanță"), + ("selinux_tip", "SELinux este activat pe acest sistem. Este posibil ca unele funcții să nu funcționeze corect. Te rugăm să consulți documentația pentru instrucțiuni de configurare."), + ("Change view", "Schimbă vizualizarea"), + ("Big tiles", "Dale mari"), + ("Small tiles", "Dale mici"), + ("List", "Listă"), + ("Virtual display", "Monitor virtual"), + ("Plug out all", "Deconectează toate"), + ("True color (4:4:4)", "Culori reale (4:4:4)"), + ("Enable blocking user input", "Activează blocarea intrărilor utilizatorului"), + ("id_input_tip", "Introdu ID-ul sau adresa IP a dispozitivului la distanță"), + ("privacy_mode_impl_mag_tip", "Modul privat prin Magnificare — nu este suportat pe toate sistemele"), + ("privacy_mode_impl_virtual_display_tip", "Modul privat prin monitor virtual — necesită driverul de monitor virtual"), + ("Enter privacy mode", "Intră în modul privat"), + ("Exit privacy mode", "Ieși din modul privat"), + ("idd_not_support_under_win10_2004_tip", "Driverul de monitor virtual nu este suportat pe versiuni de Windows anterioare versiunii 2004 (build 19041)."), + ("input_source_1_tip", "Sursă de intrare 1 — folosește metodele standard de simulare a tastaturii și mouse-ului"), + ("input_source_2_tip", "Sursă de intrare 2 — folosește driver-ul RustDesk pentru simulare la nivel de kernel"), + ("Swap control-command key", "Schimbă tastele Control și Command"), + ("swap-left-right-mouse", "Schimbă butoanele stâng și drept ale mouse-ului"), + ("2FA code", "Cod 2FA"), + ("More", "Mai mult"), + ("enable-2fa-title", "Activează autentificarea în doi pași (2FA)"), + ("enable-2fa-desc", "Scanează codul QR cu o aplicație de autentificare (de ex. Google Authenticator) și introdu codul generat pentru a confirma activarea."), + ("wrong-2fa-code", "Cod 2FA incorect"), + ("enter-2fa-title", "Introdu codul de autentificare în doi pași"), + ("Email verification code must be 6 characters.", "Codul de verificare prin e-mail trebuie să aibă 6 caractere."), + ("2FA code must be 6 digits.", "Codul 2FA trebuie să conțină 6 cifre."), + ("Multiple Windows sessions found", "Au fost găsite mai multe sesiuni Windows"), + ("Please select the session you want to connect to", "Selectează sesiunea la care vrei să te conectezi"), + ("powered_by_me", "Realizat cu RustDesk"), + ("outgoing_only_desk_tip", "Acest dispozitiv este configurat doar pentru conexiuni de ieșire și nu acceptă conexiuni de intrare."), + ("preset_password_warning", "Parola prestabilită nu este recomandată din motive de securitate. Te rugăm să o schimbi cât mai curând posibil."), + ("Security Alert", "Alertă de securitate"), + ("My address book", "Agenda mea"), + ("Personal", "Personal"), + ("Owner", "Proprietar"), + ("Set shared password", "Setează parola partajată"), + ("Exist in", "Există în"), + ("Read-only", "Doar citire"), + ("Read/Write", "Citire/Scriere"), + ("Full Control", "Control total"), + ("share_warning_tip", "Datele partajate vor fi vizibile pentru toți membrii grupului selectat. Asigură-te că partajezi doar informații adecvate."), + ("Everyone", "Toată lumea"), + ("ab_web_console_tip", "Gestionează agenda prin consola web RustDesk Pro."), + ("allow-only-conn-window-open-tip", "Permite conexiunile numai atunci când fereastra de gestionare a conexiunilor este deschisă"), + ("no_need_privacy_mode_no_physical_displays_tip", "Modul privat nu este necesar deoarece nu există monitoare fizice conectate."), + ("Follow remote cursor", "Urmărește cursorul de la distanță"), + ("Follow remote window focus", "Urmărește fereastra activă de la distanță"), + ("default_proxy_tip", "Proxy-ul implicit este utilizat pentru toate conexiunile dacă nu este specificat altul."), + ("no_audio_input_device_tip", "Nu a fost găsit niciun dispozitiv de intrare audio. Conectează un microfon și reîncearcă."), + ("Incoming", "Intrare"), + ("Outgoing", "Ieșire"), + ("Clear Wayland screen selection", "Șterge selecția de ecran Wayland"), + ("clear_Wayland_screen_selection_tip", "Șterge selecția de ecran Wayland salvată, astfel încât să poți alege un alt ecran la următoarea conexiune."), + ("confirm_clear_Wayland_screen_selection_tip", "Sigur vrei să ștergi selecția de ecran Wayland?"), + ("android_new_voice_call_tip", "Ai primit un nou apel vocal. Apasă pentru a accepta sau respinge."), + ("texture_render_tip", "Randarea prin textură poate îmbunătăți performanța grafică pe unele dispozitive. Repornește aplicația dacă apar probleme de afișare."), + ("Use texture rendering", "Folosește randarea prin textură"), + ("Floating window", "Fereastră flotantă"), + ("floating_window_tip", "Fereastra flotantă ajută la menținerea serviciului de partajare a ecranului activ în fundal pe Android."), + ("Keep screen on", "Menține ecranul pornit"), + ("Never", "Niciodată"), + ("During controlled", "În timpul controlului"), + ("During service is on", "Cât timp serviciul este activ"), + ("Capture screen using DirectX", "Capturează ecranul folosind DirectX"), + ("Back", "Înapoi"), + ("Apps", "Aplicații"), + ("Volume up", "Mărește volumul"), + ("Volume down", "Micșorează volumul"), + ("Power", "Alimentare"), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Activează botul Telegram pentru a primi notificări și a gestiona conexiunile."), + ("enable-bot-desc", "Configurează un bot Telegram pentru notificări RustDesk. Introdu token-ul botului și ID-ul chat-ului."), + ("cancel-2fa-confirm-tip", "Sigur vrei să dezactivezi autentificarea în doi pași? Aceasta va reduce securitatea contului tău."), + ("cancel-bot-confirm-tip", "Sigur vrei să dezactivezi botul Telegram?"), + ("About RustDesk", "Despre RustDesk"), + ("Send clipboard keystrokes", "Trimite conținutul clipboard-ului ca apăsări de taste"), + ("network_error_tip", "Eroare de rețea. Verifică conexiunea la internet și încearcă din nou."), + ("Unlock with PIN", "Deblochează cu PIN"), + ("Requires at least {} characters", "Necesită cel puțin {} caractere"), + ("Wrong PIN", "PIN incorect"), + ("Set PIN", "Setează PIN"), + ("Enable trusted devices", "Activează dispozitive de încredere"), + ("Manage trusted devices", "Gestionează dispozitivele de încredere"), + ("Platform", "Platformă"), + ("Days remaining", "Zile rămase"), + ("enable-trusted-devices-tip", "Dispozitivele de încredere pot accesa contul fără verificare suplimentară."), + ("Parent directory", "Director părinte"), + ("Resume", "Reia"), + ("Invalid file name", "Nume de fișier nevalid"), + ("one-way-file-transfer-tip", "Transferul de fișiere în sens unic permite doar trimiterea sau primirea de fișiere, nu ambele direcții simultan."), + ("Authentication Required", "Autentificare necesară"), + ("Authenticate", "Autentifică-te"), + ("web_id_input_tip", "Introdu ID-ul RustDesk al dispozitivului la care vrei să te conectezi"), + ("Download", "Descarcă"), + ("Upload folder", "Încarcă folder"), + ("Upload files", "Încarcă fișiere"), + ("Clipboard is synchronized", "Clipboard-ul este sincronizat"), + ("Update client clipboard", "Actualizează clipboard-ul clientului"), + ("Untagged", "Neetichetat"), + ("new-version-of-{}-tip", "Este disponibilă o nouă versiune a {}. Fă clic pentru a actualiza."), + ("Accessible devices", "Dispozitive accesibile"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Versiunea clientului RustDesk de la distanță este mai mică decât {}. Te rugăm să o actualizezi pentru o compatibilitate completă."), + ("d3d_render_tip", "Randarea Direct3D poate îmbunătăți performanța pe sistemele Windows cu suport hardware adecvat."), + ("Use D3D rendering", "Folosește randarea D3D"), + ("Printer", "Imprimantă"), + ("printer-os-requirement-tip", "Imprimarea la distanță necesită Windows 10 sau o versiune superioară."), + ("printer-requires-installed-{}-client-tip", "Imprimarea la distanță necesită instalarea clientului {} pe dispozitivul local."), + ("printer-{}-not-installed-tip", "Imprimanta {} nu este instalată. Instalează driverul imprimantei pentru a continua."), + ("printer-{}-ready-tip", "Imprimanta {} este pregătită pentru utilizare."), + ("Install {} Printer", "Instalează imprimanta {}"), + ("Outgoing Print Jobs", "Lucrări de imprimare de ieșire"), + ("Incoming Print Jobs", "Lucrări de imprimare de intrare"), + ("Incoming Print Job", "Lucrare de imprimare de intrare"), + ("use-the-default-printer-tip", "Folosește imprimanta implicită a sistemului pentru lucrările de imprimare primite."), + ("use-the-selected-printer-tip", "Folosește imprimanta selectată pentru lucrările de imprimare primite."), + ("auto-print-tip", "Imprimă automat lucrările primite fără confirmare."), + ("print-incoming-job-confirm-tip", "Ai primit o lucrare de imprimare. Vrei să o imprimești?"), + ("remote-printing-disallowed-tile-tip", "Imprimare la distanță nepermisă"), + ("remote-printing-disallowed-text-tip", "Dispozitivul la distanță nu permite imprimarea. Contactează administratorul pentru a activa această funcție."), + ("save-settings-tip", "Salvează setările curente ca implicite pentru sesiunile viitoare."), + ("dont-show-again-tip", "Nu mai afișa acest mesaj"), + ("Take screenshot", "Fă captură de ecran"), + ("Taking screenshot", "Se face captura de ecran..."), + ("screenshot-merged-screen-not-supported-tip", "Captura de ecran a ecranului combinat nu este suportată în prezent."), + ("screenshot-action-tip", "Selectează acțiunea pentru captura de ecran: salvează ca fișier sau copiază în clipboard."), + ("Save as", "Salvează ca"), + ("Copy to clipboard", "Copiază în clipboard"), + ("Enable remote printer", "Activează imprimanta la distanță"), + ("Downloading {}", "Se descarcă {}"), + ("{} Update", "Actualizare {}"), + ("{}-to-update-tip", "Este disponibilă o actualizare pentru {}. Fă clic pentru a descărca și instala."), + ("download-new-version-failed-tip", "Descărcarea noii versiuni a eșuat. Verifică conexiunea la internet și încearcă din nou."), + ("Auto update", "Actualizare automată"), + ("update-failed-check-msi-tip", "Actualizarea a eșuat. Încearcă să descarci și să instalezi manual fișierul MSI."), + ("websocket_tip", "WebSocket oferă o conexiune mai stabilă în unele medii de rețea restrictive."), + ("Use WebSocket", "Folosește WebSocket"), + ("Trackpad speed", "Viteza touchpad-ului"), + ("Default trackpad speed", "Viteza implicită a touchpad-ului"), + ("Numeric one-time password", "Parolă unică numerică"), + ("Enable IPv6 P2P connection", "Activează conexiunea P2P prin IPv6"), + ("Enable UDP hole punching", "Activează traversarea UDP (hole punching)"), + ("View camera", "Vezi camera"), + ("Enable camera", "Activează camera"), + ("No cameras", "Nicio cameră disponibilă"), + ("view_camera_unsupported_tip", "Vizualizarea camerei nu este suportată pe dispozitivul la distanță."), + ("Terminal", "Terminal"), + ("Enable terminal", "Activează terminalul"), + ("New tab", "Filă nouă"), + ("Keep terminal sessions on disconnect", "Păstrează sesiunile de terminal la deconectare"), + ("Terminal (Run as administrator)", "Terminal (Rulează ca administrator)"), + ("terminal-admin-login-tip", "Introdu datele de autentificare ale administratorului pentru a rula terminalul cu privilegii sporite."), + ("Failed to get user token.", "Obținerea tokenului de utilizator a eșuat."), + ("Incorrect username or password.", "Nume de utilizator sau parolă incorectă."), + ("The user is not an administrator.", "Utilizatorul nu este administrator."), + ("Failed to check if the user is an administrator.", "Verificarea privilegiilor de administrator a eșuat."), + ("Supported only in the installed version.", "Suportat doar în versiunea instalată."), + ("elevation_username_tip", "Introdu numele de utilizator al contului de administrator pentru a solicita sporirea privilegiilor."), + ("Preparing for installation ...", "Se pregătește instalarea..."), + ("Show my cursor", "Afișează cursorul meu"), + ("Scale custom", "Scalare personalizată"), + ("Custom scale slider", "Glisor pentru scalare personalizată"), + ("Decrease", "Micșorează"), + ("Increase", "Mărește"), + ("Show virtual mouse", "Afișează mouse virtual"), + ("Virtual mouse size", "Dimensiunea mouse-ului virtual"), + ("Small", "Mic"), + ("Large", "Mare"), + ("Show virtual joystick", "Afișează joystick virtual"), + ("Edit note", "Editează notă"), + ("Alias", "Alias"), + ("ScrollEdge", "Derulare la margine"), + ("Allow insecure TLS fallback", "Permite revenirea la TLS nesecurizat"), + ("allow-insecure-tls-fallback-tip", "Permite conexiunile cu certificate TLS nevalide sau expirate. Nu este recomandat din motive de securitate."), + ("Disable UDP", "Dezactivează UDP"), + ("disable-udp-tip", "Dezactivează conexiunile UDP și folosește doar TCP. Poate reduce performanța conexiunii."), + ("server-oss-not-support-tip", "Serverul open-source nu suportă această funcție. Folosește RustDesk Pro pentru funcționalitate completă."), + ("input note here", "Introdu o notă aici"), + ("note-at-conn-end-tip", "Afișează această notă la sfârșitul sesiunii de conexiune."), + ("Show terminal extra keys", "Afișează taste suplimentare pentru terminal"), + ("Relative mouse mode", "Mod mouse relativ"), + ("rel-mouse-not-supported-peer-tip", "Dispozitivul pereche nu suportă modul mouse relativ."), + ("rel-mouse-not-ready-tip", "Modul mouse relativ nu este pregătit. Încearcă din nou."), + ("rel-mouse-lock-failed-tip", "Blocarea mouse-ului în modul relativ a eșuat."), + ("rel-mouse-exit-{}-tip", "Apasă {} pentru a ieși din modul mouse relativ."), + ("rel-mouse-permission-lost-tip", "Permisiunea pentru modul mouse relativ a fost pierdută."), + ("Changelog", "Jurnal de modificări"), + ("keep-awake-during-outgoing-sessions-label", "Menține ecranul activ în timpul sesiunilor de ieșire"), + ("keep-awake-during-incoming-sessions-label", "Menține ecranul activ în timpul sesiunilor de intrare"), + ("Continue with {}", "Continuă cu {}"), + ("Display Name", "Nume afișat"), + ("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."), + ("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/ru.rs b/vendor/rustdesk/src/lang/ru.rs new file mode 100644 index 0000000..3917c6f --- /dev/null +++ b/vendor/rustdesk/src/lang/ru.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Статус"), + ("Your Desktop", "Ваш рабочий стол"), + ("desk_tip", "Ваш рабочий стол доступен с этим ID и паролем."), + ("Password", "Пароль"), + ("Ready", "Готов"), + ("Established", "Установлено"), + ("connecting_status", "Подключение к сети RustDesk..."), + ("Enable service", "Включить службу"), + ("Start service", "Запустить службу"), + ("Service is running", "Служба запущена"), + ("Service is not running", "Служба не запущена"), + ("not_ready_status", "Не подключено. Проверьте соединение."), + ("Control Remote Desktop", "Новое соединение"), + ("Transfer file", "Передать файлы"), + ("Connect", "Подключиться"), + ("Recent sessions", "Последние сеансы"), + ("Address book", "Адресная книга"), + ("Confirmation", "Подтверждение"), + ("TCP tunneling", "TCP-туннелирование"), + ("Remove", "Удалить"), + ("Refresh random password", "Обновить случайный пароль"), + ("Set your own password", "Установить свой пароль"), + ("Enable keyboard/mouse", "Использовать клавиатуру/мышь"), + ("Enable clipboard", "Использовать буфер обмена"), + ("Enable file transfer", "Использовать передачу файлов"), + ("Enable TCP tunneling", "Использовать туннелирование TCP"), + ("IP Whitelisting", "Список разрешённых IP-адресов"), + ("ID/Relay Server", "ID/Ретранслятор"), + ("Import server config", "Импортировать конфигурацию сервера"), + ("Export Server Config", "Экспортировать конфигурацию сервера"), + ("Import server configuration successfully", "Конфигурация сервера успешно импортирована"), + ("Export server configuration successfully", "Конфигурация сервера успешно экспортирована"), + ("Invalid server configuration", "Неправильная конфигурация сервера"), + ("Clipboard is empty", "Буфер обмена пуст"), + ("Stop service", "Остановить службу"), + ("Change ID", "Изменить ID"), + ("Your new ID", "Новый ID"), + ("length %min% to %max%", "длина %min%...%max%"), + ("starts with a letter", "начинается с буквы"), + ("allowed characters", "допустимые символы"), + ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9, - (dash) и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), + ("Website", "Сайт"), + ("About", "О приложении"), + ("Slogan_tip", "Сделано с душой в этом безумном мире!"), + ("Privacy Statement", "Заявление о конфиденциальности"), + ("Mute", "Отключить звук"), + ("Build Date", "Дата сборки"), + ("Version", "Версия"), + ("Home", "Главная"), + ("Audio Input", "Аудиовход"), + ("Enhancements", "Улучшения"), + ("Hardware Codec", "Аппаратный кодек"), + ("Adaptive bitrate", "Адаптивный битрейт"), + ("ID Server", "Сервер ID"), + ("Relay Server", "Ретранслятор"), + ("API Server", "Сервер API"), + ("invalid_http", "Адрес должен начинаться с http:// или https://"), + ("Invalid IP", "Неправильный IP-адрес"), + ("Invalid format", "Неправильный формат"), + ("server_not_support", "Пока не поддерживается сервером"), + ("Not available", "Недоступно"), + ("Too frequent", "Слишком часто"), + ("Cancel", "Отмена"), + ("Skip", "Пропустить"), + ("Close", "Закрыть"), + ("Retry", "Повтор"), + ("OK", "ОК"), + ("Password Required", "Требуется пароль"), + ("Please enter your password", "Введите пароль"), + ("Remember password", "Запомнить пароль"), + ("Wrong Password", "Неправильный пароль"), + ("Do you want to enter again?", "Повторить вход?"), + ("Connection Error", "Ошибка подключения"), + ("Error", "Ошибка"), + ("Reset by the peer", "Сброшено удалённым узлом"), + ("Connecting...", "Подключение..."), + ("Connection in progress. Please wait.", "Выполняется подключение. Подождите."), + ("Please try 1 minute later", "Попробуйте через минуту"), + ("Login Error", "Ошибка входа"), + ("Successful", "Успешно"), + ("Connected, waiting for image...", "Подключено, ожидание изображения..."), + ("Name", "Имя"), + ("Type", "Тип"), + ("Modified", "Изменено"), + ("Size", "Размер"), + ("Show Hidden Files", "Показать скрытые файлы"), + ("Receive", "Получить"), + ("Send", "Отправить"), + ("Refresh File", "Обновить файл"), + ("Local", "Локальный"), + ("Remote", "Удалённый"), + ("Remote Computer", "Удалённый компьютер"), + ("Local Computer", "Локальный компьютер"), + ("Confirm Delete", "Подтвердить удаление"), + ("Delete", "Удалить"), + ("Properties", "Свойства"), + ("Multi Select", "Множественный выбор"), + ("Select All", "Выбрать все"), + ("Unselect All", "Снять все"), + ("Empty Directory", "Пустая папка"), + ("Not an empty directory", "Папка не пуста"), + ("Are you sure you want to delete this file?", "Удалить этот файл?"), + ("Are you sure you want to delete this empty directory?", "Удалить пустую папку?"), + ("Are you sure you want to delete the file of this directory?", "Удалить файл из этой папки?"), + ("Do this for all conflicts", "Применить ко всем конфликтам"), + ("This is irreversible!", "Это необратимо!"), + ("Deleting", "Удаление"), + ("files", "файлы"), + ("Waiting", "Ожидание"), + ("Finished", "Завершено"), + ("Speed", "Скорость"), + ("Custom Image Quality", "Заданное пользователем качество изображения"), + ("Privacy mode", "Режим конфиденциальности"), + ("Block user input", "Заблокировать ввод на удалённом устройстве"), + ("Unblock user input", "Разблокировать ввод на удалённом устройстве"), + ("Adjust Window", "Настроить окно"), + ("Original", "Оригинал"), + ("Shrink", "Уменьшить"), + ("Stretch", "Растянуть"), + ("Scrollbar", "Полоса прокрутки"), + ("ScrollAuto", "Автопрокрутка"), + ("Good image quality", "Лучшее качество изображения"), + ("Balanced", "Баланс между качеством и откликом"), + ("Optimize reaction time", "Лучшее время отклика"), + ("Custom", "Заданное пользователем"), + ("Show remote cursor", "Показывать удалённый курсор"), + ("Show quality monitor", "Показывать монитор качества"), + ("Disable clipboard", "Отключить буфер обмена"), + ("Lock after session end", "Заблокировать учётную запись после сеанса"), + ("Insert Ctrl + Alt + Del", "Вставить Ctrl + Alt + Del"), + ("Insert Lock", "Заблокировать учётную запись"), + ("Refresh", "Обновить"), + ("ID does not exist", "ID не существует"), + ("Failed to connect to rendezvous server", "Невозможно подключиться к промежуточному серверу"), + ("Please try later", "Попробуйте позже"), + ("Remote desktop is offline", "Удалённое устройство не в сети"), + ("Key mismatch", "Несоответствие ключей"), + ("Timeout", "Истекло время ожидания"), + ("Failed to connect to relay server", "Невозможно подключиться к ретранслятору"), + ("Failed to connect via rendezvous server", "Невозможно подключиться через промежуточный сервер"), + ("Failed to connect via relay server", "Невозможно подключиться через ретранслятор"), + ("Failed to make direct connection to remote desktop", "Невозможно установить прямое подключение к удалённому устройству"), + ("Set Password", "Установить пароль"), + ("OS Password", "Пароль входа в ОС"), + ("install_tip", "В некоторых случаях из-за UAC RustDesk может работать неправильно на удалённом узле. Чтобы избежать возможных проблем с UAC, нажмите кнопку ниже для установки RustDesk в системе."), + ("Click to upgrade", "Нажмите, чтобы обновить"), + ("Configure", "Настроить"), + ("config_acc", "Чтобы удалённо управлять своим рабочим столом, вы должны предоставить RustDesk права \"доступа\""), + ("config_screen", "Для удалённого доступа к рабочему столу вы должны предоставить RustDesk права \"снимок экрана\""), + ("Installing ...", "Установка..."), + ("Install", "Установить"), + ("Installation", "Установка"), + ("Installation Path", "Путь установки"), + ("Create start menu shortcuts", "Создать ярлыки в меню \"Пуск\""), + ("Create desktop icon", "Создать значок на рабочем столе"), + ("agreement_tip", "Начиная установку, вы принимаете условия лицензионного соглашения."), + ("Accept and Install", "Принять и установить"), + ("End-user license agreement", "Лицензионное соглашение с конечным пользователем"), + ("Generating ...", "Генерация..."), + ("Your installation is lower version.", "Установлена более ранняя версия"), + ("not_close_tcp_tip", "Не закрывать это окно при использовании туннеля."), + ("Listening ...", "Ожидание..."), + ("Remote Host", "Удалённый узел"), + ("Remote Port", "Удалённый порт"), + ("Action", "Действие"), + ("Add", "Добавить"), + ("Local Port", "Локальный порт"), + ("Local Address", "Локальный адрес"), + ("Change Local Port", "Изменить локальный порт"), + ("setup_server_tip", "Для более быстрого подключения настройте собственный сервер."), + ("Too short, at least 6 characters.", "Слишком короткий, минимум 6 символов."), + ("The confirmation is not identical.", "Подтверждение не совпадает"), + ("Permissions", "Разрешения"), + ("Accept", "Принять"), + ("Dismiss", "Отклонить"), + ("Disconnect", "Отключить"), + ("Enable file copy and paste", "Разрешить копирование и вставку файлов"), + ("Connected", "Подключено"), + ("Direct and encrypted connection", "Прямое и зашифрованное подключение"), + ("Relayed and encrypted connection", "Ретранслируемое и зашифрованное подключение"), + ("Direct and unencrypted connection", "Прямое и незашифрованное подключение"), + ("Relayed and unencrypted connection", "Ретранслируемое и незашифрованное подключение"), + ("Enter Remote ID", "Введите удалённый ID"), + ("Enter your password", "Введите пароль"), + ("Logging in...", "Вход..."), + ("Enable RDP session sharing", "Использовать общий доступ к сеансу RDP"), + ("Auto Login", "Автоматический вход в учётную запись"), + ("Enable direct IP access", "Использовать прямой IP-доступ"), + ("Rename", "Переименовать"), + ("Space", "Место"), + ("Create desktop shortcut", "Создать ярлык на рабочем столе"), + ("Change Path", "Изменить путь"), + ("Create Folder", "Создать папку"), + ("Please enter the folder name", "Введите имя папки"), + ("Fix it", "Исправить"), + ("Warning", "Предупреждение"), + ("Login screen using Wayland is not supported", "Вход в систему с использованием Wayland не поддерживается"), + ("Reboot required", "Требуется перезагрузка"), + ("Unsupported display server", "Неподдерживаемый сервер отображения"), + ("x11 expected", "Ожидается X11"), + ("Port", "Порт"), + ("Settings", "Настройки"), + ("Username", "Имя пользователя"), + ("Invalid port", "Неправильный порт"), + ("Closed manually by the peer", "Закрыто удалённым узлом вручную"), + ("Enable remote configuration modification", "Разрешить удалённое изменение конфигурации"), + ("Run without install", "Запустить без установки"), + ("Connect via relay", "Подключится через ретранслятор"), + ("Always connect via relay", "Всегда подключаться через ретранслятор"), + ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ к моему устройству."), + ("Login", "Войти"), + ("Verify", "Проверить"), + ("Remember me", "Запомнить"), + ("Trust this device", "Доверенное устройство"), + ("Verification code", "Проверочный код"), + ("verification_tip", "Обнаружено новое устройство, на зарегистрированный адрес электронной почты отправлен проверочный код. Введите его, чтобы продолжить вход в систему."), + ("Logout", "Выйти"), + ("Tags", "Метки"), + ("Search ID", "Поиск по ID"), + ("whitelist_sep", "Разделение запятой, точкой с запятой, пробелом или новой строкой."), + ("Add ID", "Добавить ID"), + ("Add Tag", "Добавить ключевое слово"), + ("Unselect all tags", "Отменить выбор всех меток"), + ("Network error", "Ошибка сети"), + ("Username missed", "Имя пользователя отсутствует"), + ("Password missed", "Забыли пароль"), + ("Wrong credentials", "Неправильные учётные данные"), + ("The verification code is incorrect or has expired", "Проверочный код неправильный или устарел"), + ("Edit Tag", "Изменить метку"), + ("Forget Password", "Не сохранять пароль"), + ("Favorites", "Избранное"), + ("Add to Favorites", "Добавить в избранное"), + ("Remove from Favorites", "Удалить из избранного"), + ("Empty", "Пусто"), + ("Invalid folder name", "Недопустимое имя папки"), + ("Socks5 Proxy", "SOCKS5-прокси"), + ("Socks5/Http(s) Proxy", "SOCKS5/HTTP(S)-прокси"), + ("Discovered", "Найдено"), + ("install_daemon_tip", "Для запуска при загрузке необходимо установить системную службу"), + ("Remote ID", "Удалённый ID"), + ("Paste", "Вставить"), + ("Paste here?", "Вставить сюда?"), + ("Are you sure to close the connection?", "Завершить подключение?"), + ("Download new version", "Скачать новую версию"), + ("Touch mode", "Сенсорный режим"), + ("Mouse mode", "Режим мыши/тачпада"), + ("One-Finger Tap", "Нажатие одним пальцем"), + ("Left Mouse", "Левая кнопка мыши"), + ("One-Long Tap", "Долгое нажатие одним пальцем"), + ("Two-Finger Tap", "Нажатие двумя пальцами"), + ("Right Mouse", "Правая кнопка мыши"), + ("One-Finger Move", "Перемещение одним пальцем"), + ("Double Tap & Move", "Двойное нажатие и перемещение"), + ("Mouse Drag", "Перетаскивание мышью"), + ("Three-Finger vertically", "Тремя пальцами по вертикали"), + ("Mouse Wheel", "Колесо мыши"), + ("Two-Finger Move", "Перемещение двумя пальцами"), + ("Canvas Move", "Перемещение холста"), + ("Pinch to Zoom", "Масштабирование щипком"), + ("Canvas Zoom", "Масштаб холста"), + ("Reset canvas", "Сбросить масштаб холста"), + ("No permission of file transfer", "Нет разрешения на передачу файлов"), + ("Note", "Заметка"), + ("Connection", "Подключение"), + ("Share screen", "Демонстрация экрана"), + ("Chat", "Чат"), + ("Total", "Всего"), + ("items", "элементы"), + ("Selected", "Выбрано"), + ("Screen Capture", "Захват экрана"), + ("Input Control", "Управление вводом"), + ("Audio Capture", "Захват аудио"), + ("Do you accept?", "Вы согласны?"), + ("Open System Setting", "Открыть настройки системы"), + ("How to get Android input permission?", "Как получить разрешение на ввод Android?"), + ("android_input_permission_tip1", "Чтобы удалённое устройство могло управлять вашим Android-устройством с помощью мыши или нажатий, необходимо разрешить RustDesk использовать службу \"Специальные возможности\"."), + ("android_input_permission_tip2", "Перейдите на соответствующую страницу системных настроек, найдите и войдите в \"Установленные службы\", включите службу \"RustDesk Input\"."), + ("android_new_connection_tip", "Получен новый запрос на управление вашим текущим устройством."), + ("android_service_will_start_tip", "Включение захвата экрана автоматически запускает службу, позволяя другим устройствам запрашивать подключение к этому устройству."), + ("android_stop_service_tip", "Закрытие службы автоматически закроет все установленные подключения."), + ("android_version_audio_tip", "Текущая версия Android не поддерживает захват звука, обновите её до Android 10 или выше."), + ("android_start_service_tip", "Нажмите [Запустить службу] или разрешите [Захват экрана], чтобы запустить службу демонстрации экрана."), + ("android_permission_may_not_change_tip", "Разрешения для установленных подключений не могут быть изменены, необходимо переподключение."), + ("Account", "Аккаунт"), + ("Overwrite", "Перезаписать"), + ("This file exists, skip or overwrite this file?", "Файл существует, пропустить или перезаписать его?"), + ("Quit", "Выйти"), + ("Help", "Помощь"), + ("Failed", "Не выполнено"), + ("Succeeded", "Выполнено"), + ("Someone turns on privacy mode, exit", "Кто-то включил режим конфиденциальности, выход"), + ("Unsupported", "Не поддерживается"), + ("Peer denied", "Отклонено удалённым узлом"), + ("Please install plugins", "Установите плагины"), + ("Peer exit", "Отключено пользователем"), + ("Failed to turn off", "Невозможно отключить"), + ("Turned off", "Отключён"), + ("Language", "Язык"), + ("Keep RustDesk background service", "Держать в фоне службу RustDesk"), + ("Ignore Battery Optimizations", "Игнорировать оптимизацию потребления батареи"), + ("android_open_battery_optimizations_tip", "Перейдите на следующую страницу настроек"), + ("Start on boot", "Запускать при загрузке"), + ("Start the screen sharing service on boot, requires special permissions", "Запускать службу демонстрации экрана при загрузке (требуются специальные разрешения)"), + ("Connection not allowed", "Подключение не разрешено"), + ("Legacy mode", "Устаревший режим"), + ("Map mode", "Режим сопоставления"), + ("Translate mode", "Режим перевода"), + ("Use permanent password", "Использовать постоянный пароль"), + ("Use both passwords", "Использовать оба пароля"), + ("Set permanent password", "Установить постоянный пароль"), + ("Enable remote restart", "Разрешить удалённую перезагрузку"), + ("Restart remote device", "Перезапустить удалённое устройство"), + ("Are you sure you want to restart", "Вы уверены, что хотите выполнить перезагрузку?"), + ("Restarting remote device", "Перезагрузка удалённого устройства"), + ("remote_restarting_tip", "Удалённое устройство перезапускается. Закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."), + ("Copied", "Скопировано"), + ("Exit Fullscreen", "Выйти из полноэкранного режима"), + ("Fullscreen", "Полноэкранный режим"), + ("Mobile Actions", "Мобильные действия"), + ("Select Monitor", "Выберите монитор"), + ("Control Actions", "Действия по управлению"), + ("Display Settings", "Настройки отображения"), + ("Ratio", "Соотношение"), + ("Image Quality", "Качество изображения"), + ("Scroll Style", "Стиль прокрутки"), + ("Show Toolbar", "Показать панель инструментов"), + ("Hide Toolbar", "Скрыть панель инструментов"), + ("Direct Connection", "Прямая связь"), + ("Relay Connection", "Ретранслируемое подключение"), + ("Secure Connection", "Безопасное подключение"), + ("Insecure Connection", "Небезопасное подключение"), + ("Scale original", "Оригинальный масштаб"), + ("Scale adaptive", "Адаптивный масштаб"), + ("General", "Общие"), + ("Security", "Безопасность"), + ("Theme", "Тема"), + ("Dark Theme", "Тёмная тема"), + ("Light Theme", "Светлая тема"), + ("Dark", "Тёмная"), + ("Light", "Светлая"), + ("Follow System", "Системная"), + ("Enable hardware codec", "Использовать аппаратный кодек"), + ("Unlock Security Settings", "Разблокировать настройки безопасности"), + ("Enable audio", "Включить передачу звука"), + ("Unlock Network Settings", "Разблокировать сетевые настройки"), + ("Server", "Сервер"), + ("Direct IP Access", "Прямой IP-доступ"), + ("Proxy", "Прокси"), + ("Apply", "Применить"), + ("Disconnect all devices?", "Отключить все устройства?"), + ("Clear", "Очистить"), + ("Audio Input Device", "Источник звука"), + ("Use IP Whitelisting", "Использовать белый список IP"), + ("Network", "Сеть"), + ("Pin Toolbar", "Закрепить панель инструментов"), + ("Unpin Toolbar", "Открепить панель инструментов"), + ("Recording", "Запись"), + ("Directory", "Папка"), + ("Automatically record incoming sessions", "Автоматически записывать входящие сеансы"), + ("Automatically record outgoing sessions", "Автоматически записывать исходящие сеансы"), + ("Change", "Изменить"), + ("Start session recording", "Начать запись сеанса"), + ("Stop session recording", "Остановить запись сеанса"), + ("Enable recording session", "Включить запись сеанса"), + ("Enable LAN discovery", "Включить обнаружение в локальной сети"), + ("Deny LAN discovery", "Запретить обнаружение в локальной сети"), + ("Write a message", "Написать сообщение"), + ("Prompt", "Подсказка"), + ("Please wait for confirmation of UAC...", "Дождитесь подтверждения UAC..."), + ("elevated_foreground_window_tip", "Текущее окно удалённого рабочего стола требует более высоких привилегий для работы, поэтому временно невозможно использовать мышь и клавиатуру. Можно попросить удалённого пользователя свернуть текущее окно или нажать кнопку повышения прав в окне управления подключением. Чтобы избежать этой проблемы в дальнейшем, рекомендуется выполнить установку программного обеспечения на удалённом устройстве."), + ("Disconnected", "Отключено"), + ("Other", "Другое"), + ("Confirm before closing multiple tabs", "Подтверждать закрытие нескольких вкладок"), + ("Keyboard Settings", "Настройки клавиатуры"), + ("Full Access", "Полный доступ"), + ("Screen Share", "Демонстрация экрана"), + ("ubuntu-21-04-required", "Wayland требуется Ubuntu версии 21.04 или новее."), + ("wayland-requires-higher-linux-version", "Для Wayland требуется более поздняя версия дистрибутива Linux. Используйте рабочий стол X11 или смените ОС."), + ("xdp-portal-unavailable", "Невозможно сделать снимок экрана Wayland. Возможно, в XDG Desktop Portal сбой или он недоступен. Попробуйте перезапустить его с помощью `systemctl --user restart xdg-desktop-portal`."), + ("JumpLink", "Просмотр"), + ("Please Select the screen to be shared(Operate on the peer side).", "Выберите экран для демонстрации (работайте на одноранговой стороне)."), + ("Show RustDesk", "Показать RustDesk"), + ("This PC", "Этот компьютер"), + ("or", "или"), + ("Elevate", "Повысить"), + ("Zoom cursor", "Масштабировать курсор"), + ("Accept sessions via password", "Принимать сеансы по паролю"), + ("Accept sessions via click", "Принимать сеансы нажатием кнопки"), + ("Accept sessions via both", "Принимать сеансы по паролю и нажатию кнопки"), + ("Please wait for the remote side to accept your session request...", "Подождите, пока удалённая сторона примет ваш запрос на сеанс..."), + ("One-time Password", "Одноразовый пароль"), + ("Use one-time password", "Использовать одноразовый пароль"), + ("One-time password length", "Длина одноразового пароля"), + ("Request access to your device", "Запрос доступа к вашему устройству"), + ("Hide connection management window", "Скрывать окно управления подключениями"), + ("hide_cm_tip", "Разрешать скрытие в случае, если принимаются сеансы по паролю или используется постоянный пароль"), + ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), + ("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"), + ("Skipped", "Пропущено"), + ("Add to address book", "Добавить в адресную книгу"), + ("Group", "Группа"), + ("Search", "Поиск"), + ("Closed manually by web console", "Закрыто вручную через веб-консоль"), + ("Local keyboard type", "Тип локальной клавиатуры"), + ("Select local keyboard type", "Выберите тип локальной клавиатуры"), + ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), + ("Always use software rendering", "Использовать программную визуализацию"), + ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), + ("config_microphone", "Чтобы разговаривать с удалённой стороной, необходимо предоставить RustDesk разрешение \"Запись аудио\"."), + ("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."), + ("Wait", "Ждите"), + ("Elevation Error", "Ошибка повышения прав"), + ("Ask the remote user for authentication", "Запросить аутентификацию у удалённого пользователя"), + ("Choose this if the remote account is administrator", "Выберите это, если удалённый аккаунт является администратором"), + ("Transmit the username and password of administrator", "Передать имя пользователя и пароль администратора"), + ("still_click_uac_tip", "По-прежнему требуется, чтобы удалённый пользователь нажал \"OK\" в окне UAC при запуске RustDesk."), + ("Request Elevation", "Запросить повышение"), + ("wait_accept_uac_tip", "Подождите, пока удалённый пользователь подтвердит запрос UAC."), + ("Elevate successfully", "Права повышены"), + ("uppercase", "заглавные"), + ("lowercase", "строчные"), + ("digit", "цифры"), + ("special character", "спецсимволы"), + ("length>=8", "8+ символов"), + ("Weak", "Слабый"), + ("Medium", "Средний"), + ("Strong", "Стойкий"), + ("Switch Sides", "Переключить стороны"), + ("Please confirm if you want to share your desktop?", "Подтверждаете, что разрешаете демонстрацию рабочего стола?"), + ("Display", "Отображение"), + ("Default View Style", "Стиль отображения по умолчанию"), + ("Default Scroll Style", "Стиль прокрутки по умолчанию"), + ("Default Image Quality", "Качество изображения по умолчанию"), + ("Default Codec", "Кодек по умолчанию"), + ("Bitrate", "Битрейт"), + ("FPS", "Частота кадров"), + ("Auto", "Авто"), + ("Other Default Options", "Другие параметры по умолчанию"), + ("Voice call", "Голосовой вызов"), + ("Text chat", "Текстовый чат"), + ("Stop voice call", "Завершить голосовой вызов"), + ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через ретранслятор.\nКроме того, если вы хотите сразу использовать ретранслятор, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), + ("Reconnect", "Переподключить"), + ("Codec", "Кодек"), + ("Resolution", "Разрешение"), + ("No transfers in progress", "Передача не осуществляется"), + ("Set one-time password length", "Установить длину одноразового пароля"), + ("RDP Settings", "Настройки RDP"), + ("Sort by", "Сортировка"), + ("New Connection", "Новое подключение"), + ("Restore", "Восстановить"), + ("Minimize", "Свернуть"), + ("Maximize", "Развернуть"), + ("Your Device", "Ваше устройство"), + ("empty_recent_tip", "Нет последних сеансов!\nПора спланировать новый."), + ("empty_favorite_tip", "Ещё нет избранных удалённых узлов?\nДавайте найдём, кого можно добавить в избранное!"), + ("empty_lan_tip", "Не найдено удалённых узлов."), + ("empty_address_book_tip", "В адресной книге нет удалённых узлов."), + ("Empty Username", "Пустое имя пользователя"), + ("Empty Password", "Пустой пароль"), + ("Me", "Я"), + ("identical_file_tip", "Файл идентичен файлу на удалённом узле"), + ("show_monitors_tip", "Показывать мониторы на панели инструментов"), + ("View Mode", "Режим просмотра"), + ("login_linux_tip", "Чтобы включить сеанс рабочего стола X, необходимо войти в удалённый аккаунт Linux."), + ("verify_rustdesk_password_tip", "Подтвердить пароль RustDesk"), + ("remember_account_tip", "Запомнить этот аккаунт"), + ("os_account_desk_tip", "Этот аккаунт используется для входа в удалённую ОС и включения сеанса рабочего стола в режиме headless."), + ("OS Account", "Аккаунт ОС"), + ("another_user_login_title_tip", "Другой пользователь уже вошёл в систему"), + ("another_user_login_text_tip", "Отключить"), + ("xorg_not_found_title_tip", "Xorg не найден"), + ("xorg_not_found_text_tip", "Установите Xorg"), + ("no_desktop_title_tip", "Нет доступных рабочих столов"), + ("no_desktop_text_tip", "Установите GNOME Desktop"), + ("No need to elevate", "Повышение прав не требуется"), + ("System Sound", "Системный звук"), + ("Default", "По умолчанию"), + ("New RDP", "Новый RDP"), + ("Fingerprint", "Отпечаток"), + ("Copy Fingerprint", "Копировать отпечаток"), + ("no fingerprints", "отпечатки отсутствуют"), + ("Select a peer", "Выберите удалённый узел"), + ("Select peers", "Выберите удалённые узлы"), + ("Plugins", "Плагины"), + ("Uninstall", "Удалить"), + ("Update", "Обновить"), + ("Enable", "Включить"), + ("Disable", "Отключить"), + ("Options", "Настройки"), + ("resolution_original_tip", "Исходное разрешение"), + ("resolution_fit_local_tip", "Соответствие локальному разрешению"), + ("resolution_custom_tip", "Произвольное разрешение"), + ("Collapse toolbar", "Свернуть панель инструментов"), + ("Accept and Elevate", "Принять и повысить"), + ("accept_and_elevate_btn_tooltip", "Разрешить подключение и повысить права UAC."), + ("clipboard_wait_response_timeout_tip", "Время ожидания копирования буфера обмена истекло"), + ("Incoming connection", "Входящее подключение"), + ("Outgoing connection", "Исходящее подключение"), + ("Exit", "Выход"), + ("Open", "Открыть"), + ("logout_tip", "Вы действительно хотите выйти?"), + ("Service", "Служба"), + ("Start", "Запустить"), + ("Stop", "Остановить"), + ("exceed_max_devices", "Достигнуто максимальное количество управляемых устройств."), + ("Sync with recent sessions", "Синхронизация последних сеансов"), + ("Sort tags", "Сортировка меток"), + ("Open connection in new tab", "Открыть подключение в новой вкладке"), + ("Move tab to new window", "Переместить вкладку в отдельное окно"), + ("Can not be empty", "Не может быть пустым"), + ("Already exists", "Уже существует"), + ("Change Password", "Изменить пароль"), + ("Refresh Password", "Обновить пароль"), + ("ID", "ID"), + ("Grid View", "Сетка"), + ("List View", "Список"), + ("Select", "Выбор"), + ("Toggle Tags", "Переключить метки"), + ("pull_ab_failed_tip", "Невозможно обновить адресную книгу"), + ("push_ab_failed_tip", "Невозможно синхронизировать адресную книгу с сервером"), + ("synced_peer_readded_tip", "Устройства, присутствовавшие в последних сеансах, будут синхронизированы с адресной книгой."), + ("Change Color", "Изменить цвет"), + ("Primary Color", "Основной цвет"), + ("HSV Color", "Цвет HSV"), + ("Installation Successful!", "Установка выполнена успешно!"), + ("Installation failed!", "Установка не выполнена!"), + ("Reverse mouse wheel", "Реверсировать колесо мыши"), + ("{} sessions", "{} сеансов"), + ("scam_title", "Вы можете быть ОБМАНУТЫ!"), + ("scam_text1", "Если вы разговариваете по телефону с кем-то, кого вы НЕ ЗНАЕТЕ и НЕ ДОВЕРЯЕТЕ, и он просит вас использовать RustDesk и запустить его службу, не продолжайте и немедленно прервите разговор."), + ("scam_text2", "Скорее всего, это мошенник, пытающийся украсть ваши деньги или другую личную информацию."), + ("Don't show again", "Больше не показывать"), + ("I Agree", "Принимаю"), + ("Decline", "Отказ"), + ("Timeout in minutes", "Время ожидания (минут)"), + ("auto_disconnect_option_tip", "Автоматически закрывать входящие сеансы при неактивности пользователя"), + ("Connection failed due to inactivity", "Подключение не выполнено из-за неактивности"), + ("Check for software update on startup", "Проверять обновления программы при запуске"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Обновите RustDesk Server Pro до версии {} или новее!"), + ("pull_group_failed_tip", "Невозможно обновить группу"), + ("Filter by intersection", "Фильтровать по пересечению"), + ("Remove wallpaper during incoming sessions", "Скрывать обои рабочего стола при входящем сеансе"), + ("Test", "Тест"), + ("display_is_plugged_out_msg", "Дисплей отключён, переключитесь на первый дисплей."), + ("No displays", "Нет дисплеев"), + ("Open in new window", "Открыть в новом окне"), + ("Show displays as individual windows", "Показывать дисплеи в отдельных окнах"), + ("Use all my displays for the remote session", "Использовать все мои дисплеи для удалённого сеанса"), + ("selinux_tip", "На вашем устройстве включён SELinux, что может помешать правильной работе RustDesk на управляемой стороне."), + ("Change view", "Вид"), + ("Big tiles", "Большие значки"), + ("Small tiles", "Маленькие значки"), + ("List", "Список"), + ("Virtual display", "Виртуальный дисплей"), + ("Plug out all", "Отключить все"), + ("True color (4:4:4)", "True color (4:4:4)"), + ("Enable blocking user input", "Разрешить блокировать ввод на устройстве"), + ("id_input_tip", "Можно ввести идентификатор, прямой IP-адрес или домен с портом (<домен>:<порт>).\nЕсли необходимо получить доступ к устройству на другом сервере, добавьте адрес сервера (@<адрес_сервера>?key=<ключ_значение>), например:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЕсли необходимо получить доступ к устройству на общедоступном сервере, введите \"@public\", ключ для публичного сервера не требуется."), + ("privacy_mode_impl_mag_tip", "Режим 1"), + ("privacy_mode_impl_virtual_display_tip", "Режим 2"), + ("Enter privacy mode", "Режим конфиденциальности включён"), + ("Exit privacy mode", "Режим конфиденциальности отключён"), + ("idd_not_support_under_win10_2004_tip", "Драйвер непрямого отображения не поддерживается. Требуется Windows 10 версии 2004 или новее."), + ("input_source_1_tip", "Источник ввода 1"), + ("input_source_2_tip", "Источник ввода 2"), + ("Swap control-command key", "Поменять местами значения кнопок Ctrl и Command"), + ("swap-left-right-mouse", "Поменять местами значения левой и правой кнопок мыши"), + ("2FA code", "Код двухфакторной аутентификации"), + ("More", "Ещё"), + ("enable-2fa-title", "Использовать двухфакторную аутентификацию"), + ("enable-2fa-desc", "Настройте приложение аутентификации. Используйте, например, Authy, Microsoft или Google Authenticator, на телефоне или компьютере.\n\nОтсканируйте QR-код с помощью приложения аутентификации и введите код, который отобразит это приложение, чтобы включить двухфакторную аутентификацию."), + ("wrong-2fa-code", "Невозможно подтвердить код. Проверьте код и настройки местного времени."), + ("enter-2fa-title", "Двухфакторная аутентификация"), + ("Email verification code must be 6 characters.", "Код подтверждения электронной почты должен состоять из 6 символов."), + ("2FA code must be 6 digits.", "Код двухфакторной аутентификации должен состоять из 6 цифр."), + ("Multiple Windows sessions found", "Обнаружено несколько сеансов Windows"), + ("Please select the session you want to connect to", "Выберите сеанс, к которому хотите подключиться"), + ("powered_by_me", "Основано на RustDesk"), + ("outgoing_only_desk_tip", "Это специализированная версия.\nВы можете подключаться к другим устройствам, но другие устройства не могут подключиться к вашему."), + ("preset_password_warning", "Это специализированная версия с предустановленным паролем. Любой, кто знает этот пароль, может получить полный контроль над вашим устройством. Если это для вас неожиданно, немедленно удалите данное программное обеспечение."), + ("Security Alert", "Предупреждение о безопасности"), + ("My address book", "Моя адресная книга"), + ("Personal", "Личная"), + ("Owner", "Владелец"), + ("Set shared password", "Установить общий пароль"), + ("Exist in", "Существует в"), + ("Read-only", "Только чтение"), + ("Read/Write", "Чтение и запись"), + ("Full Control", "Полный доступ"), + ("share_warning_tip", "Поля выше являются общими и видны другим."), + ("Everyone", "Все"), + ("ab_web_console_tip", "Больше в веб-консоли"), + ("allow-only-conn-window-open-tip", "Разрешать подключение только при открытом окне RustDesk"), + ("no_need_privacy_mode_no_physical_displays_tip", "Физические дисплеи отсутствуют, нет необходимости использовать режим конфиденциальности."), + ("Follow remote cursor", "Следовать за удалённым курсором"), + ("Follow remote window focus", "Следовать за фокусом удалённого окна"), + ("default_proxy_tip", "Протокол и порт по умолчанию: Socks5 и 1080"), + ("no_audio_input_device_tip", "Устройство аудиовхода не найдено."), + ("Incoming", "Входящие"), + ("Outgoing", "Исходящие"), + ("Clear Wayland screen selection", "Отменить выбор экрана Wayland"), + ("clear_Wayland_screen_selection_tip", "После отмены можно заново выбрать экран для демонстрации."), + ("confirm_clear_Wayland_screen_selection_tip", "Отменить выбор экрана Wayland?"), + ("android_new_voice_call_tip", "Получен новый запрос на голосовой вызов. Если вы его примите, звук переключится на голосовую связь."), + ("texture_render_tip", "Использовать визуализацию текстур, чтобы сделать изображения более плавными."), + ("Use texture rendering", "Визуализация текстур"), + ("Floating window", "Плавающее окно"), + ("floating_window_tip", "Помогает поддерживать фоновую службу RustDesk"), + ("Keep screen on", "Держать экран включённым"), + ("Never", "Нет"), + ("During controlled", "При управлении"), + ("During service is on", "При запущенной службе"), + ("Capture screen using DirectX", "Захват экрана с помощью DirectX"), + ("Back", "Назад"), + ("Apps", "Приложения"), + ("Volume up", "Громкость+"), + ("Volume down", "Громкость-"), + ("Power", "Питание"), + ("Telegram bot", "Telegram-бот"), + ("enable-bot-tip", "Если включено, можно получать код двухфакторной аутентификации от бота. Он также может выполнять функцию уведомления о подключении."), + ("enable-bot-desc", "1) Откройте чат с @BotFather.\n2) Отправьте команду \"/newbot\". После выполнения этого шага вы получите токен.\n3) Начните чат с вашим только что созданным ботом. Отправьте сообщение, начинающееся с прямой косой черты (\"/\"), например, \"/hello\", чтобы его активировать.\n"), + ("cancel-2fa-confirm-tip", "Отключить двухфакторную аутентификацию?"), + ("cancel-bot-confirm-tip", "Отключить Telegram-бота?"), + ("About RustDesk", "О RustDesk"), + ("Send clipboard keystrokes", "Отправлять нажатия клавиш в буфер обмена"), + ("network_error_tip", "Проверьте подключение к сети, затем нажмите \"Повтор\"."), + ("Unlock with PIN", "Разблокировать PIN-кодом"), + ("Requires at least {} characters", "Требуется не менее {} символов"), + ("Wrong PIN", "Неправильный PIN-код"), + ("Set PIN", "Установить PIN-код"), + ("Enable trusted devices", "Включение доверенных устройств"), + ("Manage trusted devices", "Управление доверенными устройствами"), + ("Platform", "Платформа"), + ("Days remaining", "Дней осталось"), + ("enable-trusted-devices-tip", "Разрешить доверенным устройствам пропускать проверку подлинности 2FA"), + ("Parent directory", "Родительская папка"), + ("Resume", "Продолжить"), + ("Invalid file name", "Неправильное имя файла"), + ("one-way-file-transfer-tip", "На управляемой стороне включена односторонняя передача файлов."), + ("Authentication Required", "Требуется аутентификация"), + ("Authenticate", "Аутентификация"), + ("web_id_input_tip", "Можно ввести ID на том же сервере, прямой доступ по IP в веб-клиенте не поддерживается.\nЕсли вы хотите получить доступ к устройству на другом сервере, добавьте адрес сервера (@<адрес_сервера>?key=<ключ>), например,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЕсли вы хотите получить доступ к устройству на публичном сервере, введите \"@public\", для публичного сервера ключ не нужен."), + ("Download", "Скачать"), + ("Upload folder", "Загрузить папку"), + ("Upload files", "Загрузить файлы"), + ("Clipboard is synchronized", "Буфер обмена синхронизирован"), + ("Update client clipboard", "Обновить буфер обмена клиента"), + ("Untagged", "Без метки"), + ("new-version-of-{}-tip", "Доступна новая версия {}"), + ("Accessible devices", "Доступные устройства"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Обновите клиент RustDesk до версии {} или новее на удалённой стороне!"), + ("d3d_render_tip", "При включении визуализации D3D на некоторых устройствах удалённый экран может быть чёрным."), + ("Use D3D rendering", "Использовать визуализацию D3D"), + ("Printer", "Принтер"), + ("printer-os-requirement-tip", "Для работы функции исходящей связи с принтером требуется Windows 10 или более поздней версии."), + ("printer-requires-installed-{}-client-tip", "Чтобы использовать удалённую печать, {} должен быть установлен на этом устройстве."), + ("printer-{}-not-installed-tip", "Принтер {} не установлен."), + ("printer-{}-ready-tip", "Принтер {} установлен и готов к использованию."), + ("Install {} Printer", "Установить принтер {}"), + ("Outgoing Print Jobs", "Исходящее задание печати"), + ("Incoming Print Jobs", "Входящее задание печати"), + ("Incoming Print Job", "Входящее задание печати"), + ("use-the-default-printer-tip", "Использовать принтер по умолчанию"), + ("use-the-selected-printer-tip", "Использовать выбранный принтер"), + ("auto-print-tip", "Автоматически выполнять печать на выбранном принтере"), + ("print-incoming-job-confirm-tip", "Получено задание на печать с удалённого устройства. Выполнить его локально?"), + ("remote-printing-disallowed-tile-tip", "Удалённая печать запрещена"), + ("remote-printing-disallowed-text-tip", "Настройки разрешений на управляемой стороне запрещают удалённую печать."), + ("save-settings-tip", "Сохранить настройки"), + ("dont-show-again-tip", "Больше не показывать"), + ("Take screenshot", "Сделать снимок экрана"), + ("Taking screenshot", "Получение снимка экрана"), + ("screenshot-merged-screen-not-supported-tip", "Объединение снимков экранов с нескольких дисплеев в настоящее время не поддерживается. Переключитесь на один дисплей и повторите действие."), + ("screenshot-action-tip", "Выберите, что делать с полученным снимком экрана."), + ("Save as", "Сохранить в файл"), + ("Copy to clipboard", "Копировать в буфер обмена"), + ("Enable remote printer", "Использовать удалённый принтер"), + ("Downloading {}", "Скачивание"), + ("{} Update", "Обновить {}"), + ("{}-to-update-tip", "{} закроется и установит новую версию."), + ("download-new-version-failed-tip", "Ошибка загрузки. Можно повторить попытку или нажать кнопку \"Скачать\", чтобы скачать приложение с официального сайта и обновить вручную."), + ("Auto update", "Автоматическое обновление"), + ("update-failed-check-msi-tip", "Невозможно определить метод установки. Нажмите кнопку \"Скачать\", чтобы скачать приложение с официального сайта и обновить его вручную."), + ("websocket_tip", "WebSocket поддерживает только подключения к ретранслятору."), + ("Use WebSocket", "Использовать WebSocket"), + ("Trackpad speed", "Скорость трекпада"), + ("Default trackpad speed", "Скорость трекпада по умолчанию"), + ("Numeric one-time password", "Цифровой одноразовый пароль"), + ("Enable IPv6 P2P connection", "Использовать подключение IPv6 P2P"), + ("Enable UDP hole punching", "Использовать UDP hole punching"), + ("View camera", "Просмотр камеры"), + ("Enable camera", "Включить камеру"), + ("No cameras", "Камера отсутствует"), + ("view_camera_unsupported_tip", "Удалённое устройство не поддерживает просмотр камеры."), + ("Terminal", "Терминал"), + ("Enable terminal", "Включить терминал"), + ("New tab", "Новая вкладка"), + ("Keep terminal sessions on disconnect", "Сохранять сеансы терминала при отключении"), + ("Terminal (Run as administrator)", "Терминал (администратор)"), + ("terminal-admin-login-tip", "Введите имя пользователя и пароль администратора управляемой стороны."), + ("Failed to get user token.", "Невозможно получить токен пользователя."), + ("Incorrect username or password.", "Неправильное имя пользователя или пароль."), + ("The user is not an administrator.", "Пользователь не является администратором."), + ("Failed to check if the user is an administrator.", "Невозможно проверить, является ли пользователь администратором."), + ("Supported only in the installed version.", "Поддерживается только в установочной версии."), + ("elevation_username_tip", "Введите пользователя или домен\\пользователя"), + ("Preparing for installation ...", "Подготовка к установке..."), + ("Show my cursor", "Показывать мой курсор"), + ("Scale custom", "Пользовательский масштаб"), + ("Custom scale slider", "Ползунок пользовательского масштаба"), + ("Decrease", "Уменьшить"), + ("Increase", "Увеличить"), + ("Show virtual mouse", "Показать виртуальную мышь"), + ("Virtual mouse size", "Размер виртуальной мыши"), + ("Small", "Маленький"), + ("Large", "Большой"), + ("Show virtual joystick", "Показать виртуальный джойстик"), + ("Edit note", "Изменить заметку"), + ("Alias", "Псевдоним"), + ("ScrollEdge", "Прокрутка по краю"), + ("Allow insecure TLS fallback", "Разрешать небезопасные TLS"), + ("allow-insecure-tls-fallback-tip", "По умолчанию RustDesk проверяет сертификат сервера на наличие протоколов, использующих TLS.\nЕсли эта функция включена, RustDesk пропустит данный этап и продолжит работу в случае неудачной проверки."), + ("Disable UDP", "Отключить UDP"), + ("disable-udp-tip", "Определяет, следует ли использовать только TCP.\nЕсли включено, RustDesk не будет использовать UDP 21116, вместо него будет использоваться TCP 21116."), + ("server-oss-not-support-tip", "ПРИМЕЧАНИЕ: в OSS-сервере RustDesk эта функция отсутствует."), + ("input note here", "введите заметку"), + ("note-at-conn-end-tip", "Запрашивать заметку в конце соединения"), + ("Show terminal extra keys", "Показывать дополнительные кнопки терминала"), + ("Relative mouse mode", "Режим относительного перемещения мыши"), + ("rel-mouse-not-supported-peer-tip", "Режим относительного перемещения мыши не поддерживается подключённым узлом."), + ("rel-mouse-not-ready-tip", "Режим относительного перемещения мыши ещё не готов. Попробуйте снова."), + ("rel-mouse-lock-failed-tip", "Невозможно заблокировать курсор. Режим относительного перемещения мыши отключён."), + ("rel-mouse-exit-{}-tip", "Нажмите {} для выхода."), + ("rel-mouse-permission-lost-tip", "Разрешение на использование клавиатуры отменено. Режим относительного перемещения мыши отключён."), + ("Changelog", "Журнал изменений"), + ("keep-awake-during-outgoing-sessions-label", "Не отключать экран во время исходящих сеансов"), + ("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"), + ("Continue with {}", "Продолжить с {}"), + ("Display Name", "Отображаемое имя"), + ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), + ("preset-password-in-use-tip", "Установленный пароль сейчас используется."), + ("Enable privacy mode", "Использовать режим конфиденциальности"), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/sc.rs b/vendor/rustdesk/src/lang/sc.rs new file mode 100644 index 0000000..68ce541 --- /dev/null +++ b/vendor/rustdesk/src/lang/sc.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Istadu"), + ("Your Desktop", "Custu elaboradore"), + ("desk_tip", "Podes atzèdere a custu elaboradore impreende s'ID e sa crae de intrada inditados inoghe in suta."), + ("Password", "Crae"), + ("Ready", "Prontu"), + ("Established", "Istabilida"), + ("connecting_status", "Connessione a sa rete RustDesk..."), + ("Enable service", "Abìlita servìtziu"), + ("Start service", "Allughe su servìtziu"), + ("Service is running", "Su servìtziu est in funtzione"), + ("Service is not running", "Su servìtziu no est in funtzione"), + ("not_ready_status", "Non prontu. Verìfica sa connessione"), + ("Control Remote Desktop", "Controlla s'elaboradore remotu"), + ("Transfer file", "Tràmuda documentos"), + ("Connect", "Cunnete·ti"), + ("Recent sessions", "Sessiones reghentes"), + ("Address book", "Rubrica"), + ("Confirmation", "Cunfirma"), + ("TCP tunneling", "Tunnel TCP"), + ("Remove", "Boga"), + ("Refresh random password", "Crae casuale noa"), + ("Set your own password", "Imposta sa crae"), + ("Enable keyboard/mouse", "Abìlita tecladu/ratu"), + ("Enable clipboard", "Abìlita punta de billete"), + ("Enable file transfer", "Abìlita su tramudòngiu de documentos"), + ("Enable TCP tunneling", "Abìlita tunnel TCP"), + ("IP Whitelisting", "IP autorizados"), + ("ID/Relay Server", "Serbidore ID/Tràmuda"), + ("Import server config", "Importa configuratzione serbidore dae sa punta de billete"), + ("Export Server Config", "Esporta configurazione serbidore a sa punta de billete"), + ("Import server configuration successfully", "Configuratzione serbidore importada cumprida"), + ("Export server configuration successfully", "Configuratzione serbidore esportada cumprida"), + ("Invalid server configuration", "Configuratzione serbidore non vàlida"), + ("Clipboard is empty", "Sa punta de billete est bòida"), + ("Stop service", "Firma su servìtziu"), + ("Change ID", "Càmbia ID"), + ("Your new ID", "S'ID nou"), + ("length %min% to %max%", "longària dae %min% a %max%"), + ("starts with a letter", "incumintza cun una lìtera"), + ("allowed characters", "caràteres cunsentidos"), + ("id_change_tip", "Podes impreare petzi sos caràteres a-z, A-Z, 0-9, - (tratigheddu) e _ (sutaliniadu).\nSu primu caràtere depet èssere a-z o A-Z.\nSa longària depet èssere de intre 6 e 16 caràteres."), + ("Website", "Situ web programma"), + ("About", "Info programma"), + ("Slogan_tip", "Fatu cun su coro in custu mundu caòticu!"), + ("Privacy Statement", "Informativa subra de sa riservadesa"), + ("Mute", "Sonu istudadu"), + ("Build Date", "Data build"), + ("Version", "Versione"), + ("Home", "Pàgina printzipale"), + ("Audio Input", "Intrada àudio"), + ("Enhancements", "Megioros"), + ("Hardware Codec", "Codificadore fìsicu (hardware)"), + ("Adaptive bitrate", "Velotzidade de bits adativa"), + ("ID Server", "ID serbidore"), + ("Relay Server", "Serbidore de tràmuda"), + ("API Server", "Serbidore API"), + ("invalid_http", "depet incumintzare cun http:// o https://"), + ("Invalid IP", "Indiritzu IP non vàlidu"), + ("Invalid format", "Formadu non vàlidu"), + ("server_not_support", "Galu non suportadu dae su serbidore"), + ("Not available", "No a disponimentu"), + ("Too frequent", "Tropu fitianu"), + ("Cancel", "Annulla"), + ("Skip", "Ignora"), + ("Close", "Serra"), + ("Retry", "Torra a proare"), + ("OK", "AB"), + ("Password Required", "Bisòngiat sa crae"), + ("Please enter your password", "Inserta sa crae tua"), + ("Remember password", "Ammenta sa crae"), + ("Wrong Password", "Crae isballiada"), + ("Do you want to enter again?", "Boles torrare a intrare?"), + ("Connection Error", "Errore de connessione"), + ("Error", "Errore"), + ("Reset by the peer", "Resetada dae su dispositivu de s'àtera parte"), + ("Connecting...", "Connetende..."), + ("Connection in progress. Please wait.", "Connetende. Iseta."), + ("Please try 1 minute later", "Torra a proare a pustis de 1 minutu"), + ("Login Error", "Faddina de atzessu"), + ("Successful", "Cumpridu"), + ("Connected, waiting for image...", "Connessu, isetende s'immàgine..."), + ("Name", "Nùmene"), + ("Type", "Casta"), + ("Modified", "Modificadu"), + ("Size", "Mannària"), + ("Show Hidden Files", "Mustra sos documentos cuados"), + ("Receive", "Retzi"), + ("Send", "Imbia"), + ("Refresh File", "Annoa sos documentos"), + ("Local", "Locale"), + ("Remote", "Remotu"), + ("Remote Computer", "Elaboradore remotu"), + ("Local Computer", "Elaboradore locale"), + ("Confirm Delete", "Cunfirma s'iscantzelladura"), + ("Delete", "Iscantzella"), + ("Properties", "Propiedades"), + ("Multi Select", "Seletzione mùltipla"), + ("Select All", "Seletziona totu"), + ("Unselect All", "Deseletziona totu"), + ("Empty Directory", "Cartella bòida"), + ("Not an empty directory", "No est una cartella bòida"), + ("Are you sure you want to delete this file?", "Ses seguru de bòlere iscantzellare custu documentu?"), + ("Are you sure you want to delete this empty directory?", "Ses seguru de bòlere iscantzellare custa cartella bòida?"), + ("Are you sure you want to delete the file of this directory?", "Ses seguru de bòlere iscantzellare su documentu de custa cartella?"), + ("Do this for all conflicts", "Ammenta custu issèberu pro totu sos cunflitos"), + ("This is irreversible!", "Custu non si podet annullare!"), + ("Deleting", "Iscantzellende"), + ("files", "documentos"), + ("Waiting", "Isetende"), + ("Finished", "Acabadu"), + ("Speed", "Lestresa"), + ("Custom Image Quality", "Calidade immàgine personalizada"), + ("Privacy mode", "Modalidade de riservadesa"), + ("Block user input", "Bloca sas atziones de utente"), + ("Unblock user input", "Isbloca sas atziones de utente"), + ("Adjust Window", "Adata sa ventana"), + ("Original", "Originale"), + ("Shrink", "Astringhe"), + ("Stretch", "Illàrghia"), + ("Scrollbar", "Istanga de iscurrimentu"), + ("ScrollAuto", "Iscurre in automàticu"), + ("Good image quality", "Calidade bona de s'immàgine"), + ("Balanced", "Bilantziada"), + ("Optimize reaction time", "Otimiza su tempus de reatzione"), + ("Custom", "Profilu personalizadu"), + ("Show remote cursor", "Mustra su cursore remotu"), + ("Show quality monitor", "Mustra sa calidade vìdeu"), + ("Disable clipboard", "Disabìlita sa punta de billete"), + ("Lock after session end", "Bloca a sa fine de sa sessione"), + ("Insert Ctrl + Alt + Del", "Inserta Ctrl + Alt + Del"), + ("Insert Lock", "Blocu insertada"), + ("Refresh", "Annoa"), + ("ID does not exist", "S'ID no esistit"), + ("Failed to connect to rendezvous server", "Errore connessione a su sebidore de atòbiu"), + ("Please try later", "Torra a proare prus a a tardu"), + ("Remote desktop is offline", "S'iscrivania remota no est in lìnia"), + ("Key mismatch", "Sa crae non currispondet"), + ("Timeout", "Tempus iscadidu"), + ("Failed to connect to relay server", "Connessione a su serbidore de tràmuda fallida"), + ("Failed to connect via rendezvous server", "Connessione pro mèdiu de su serbidore de atòbiu fallida"), + ("Failed to connect via relay server", "Connessione pro mèdiu de su serbidore de tràmuda fallida"), + ("Failed to make direct connection to remote desktop", "Connessione direta a s'iscrivania remota fallida"), + ("Set Password", "Imposta sa crae"), + ("OS Password", "Crae sistema operativu"), + ("install_tip", "Pro neghe de su Controllu Contu Utente (UAC), RustDesk diat pòdere non funtzionare comente si tocat comente iscrivania remota.\nPro evitare custu problema, incarca in su butone inoghe in suta pro installare RustDesk a livellu de sistema."), + ("Click to upgrade", "Atualiza"), + ("Configure", "Cunfigura"), + ("config_acc", "Pro controllare s'iscrivania dae foras, depes frunire a RustDesk su permissu 'Atzessibilidade'."), + ("config_screen", "Pro controllare s'iscrivania dae foras, depes frunire a RustDesk su permissu 'Registratzione ischermu'."), + ("Installing ...", "Installatzione ..."), + ("Install", "Installa"), + ("Installation", "Installatzione"), + ("Installation Path", "Àndala de installatzione"), + ("Create start menu shortcuts", "Crea sos ligàmenes in su menù de incumintzu"), + ("Create desktop icon", "Crea un'icona in s'iscrivania"), + ("agreement_tip", "Incaminende s'installazione, atzetas sos tèrmines de su cuntratu de lissèntzia."), + ("Accept and Install", "Atzeta e installa"), + ("End-user license agreement", "Cuntratu de lissèntzia utente finale"), + ("Generating ...", "Ingendrende ..."), + ("Your installation is lower version.", "Cuta installazione no est atualizada."), + ("not_close_tcp_tip", "Non Serres custa ventana in su mentres chi ses impreende su tunnel"), + ("Listening ...", "Ascurtende ..."), + ("Remote Host", "Istrangiaore (host) remotu"), + ("Remote Port", "Ghenna remota"), + ("Action", "Atzione"), + ("Add", "Annanghe"), + ("Local Port", "Ghenna locale"), + ("Local Address", "Indiritzu locale"), + ("Change Local Port", "Càmbia ghenna locale"), + ("setup_server_tip", "Pro una connessione prus lestra, cunfigura unu serbidore ispetzìficu"), + ("Too short, at least 6 characters.", "Tropu curtza, a su nessi 6 caràteres"), + ("The confirmation is not identical.", "Sa crae de cunfirma non currispondet"), + ("Permissions", "Permissos"), + ("Accept", "Atzeta"), + ("Dismiss", "Naga"), + ("Disconnect", "Iscollega·ti"), + ("Enable file copy and paste", "Permite sa còpia e s'incollòngiu de documentos"), + ("Connected", "Connessu"), + ("Direct and encrypted connection", "Connessione direta e tzifrada"), + ("Relayed and encrypted connection", "Connessione inoltrada (relayed) e tzifrada"), + ("Direct and unencrypted connection", "Connessione direta e non tzifrada"), + ("Relayed and unencrypted connection", "Connessione inoltrada (relayed) e non tzifrada"), + ("Enter Remote ID", "Inserta ID remotu"), + ("Enter your password", "Inserta sa crae tua"), + ("Logging in...", "Intrende..."), + ("Enable RDP session sharing", "Abìlita sa cumpartzidura sessione RDP"), + ("Auto Login", "Atzessu automàticu"), + ("Enable direct IP access", "Abìlita s'intrada direta pro mèdiu de s'IP"), + ("Rename", "Càmbia de nùmene"), + ("Space", "Ispàtziu"), + ("Create desktop shortcut", "Crea unu ligàmene in s'iscrivania"), + ("Change Path", "Modìfica s'àndala"), + ("Create Folder", "Crea una cartella"), + ("Please enter the folder name", "Inserta su nùmene de sa cartella"), + ("Fix it", "Risolve"), + ("Warning", "Avisu"), + ("Login screen using Wayland is not supported", "S'ischemada de intrada no est suportada impreende Wayland"), + ("Reboot required", "B'at bisòngiu de una torrada a aviare"), + ("Unsupported display server", "Serbidore de visualizatzione non suportadu"), + ("x11 expected", "bisòngiat xll"), + ("Port", "Ghenna"), + ("Settings", "Impostatziones"), + ("Username", "Nùmene utente"), + ("Invalid port", "Nùmeru ghenna non vàlidu"), + ("Closed manually by the peer", "Serradu a manu dae su dispositivu remotu"), + ("Enable remote configuration modification", "Abìlita sa modìfica remota de sa cunfiguratzione"), + ("Run without install", "Allughe chene installare"), + ("Connect via relay", "Collega·ti impreende una tràmuda relay"), + ("Always connect via relay", "Collega·ti semper impreende una tràmuda relay"), + ("whitelist_tip", "Si podent connètere a custa iscrivania petzi sos indiritzos IP autorizados"), + ("Login", "Intra"), + ("Verify", "Avèrgua"), + ("Remember me", "Ammenta·ti de mene"), + ("Trust this device", "Registra custu dispositivu comente de fidùtzia"), + ("Verification code", "Còdighe de verìfica"), + ("verification_tip", "Amus imbiadu unu còdighe de averguada a s'indiritzu de posta eletrònica registradu, pro intrare inserta·lu."), + ("Logout", "Essi"), + ("Tags", "Etichetas"), + ("Search ID", "Chirca ID"), + ("whitelist_sep", "Separados dae vìrgulas, puntu e vìrgula, ispatziu o riga a suta"), + ("Add ID", "Annanghe ID"), + ("Add Tag", "Annanghe eticheta"), + ("Unselect all tags", "Deseletziona totu sas etichetas"), + ("Network error", "Errore de rete"), + ("Username missed", "Mancat su nùmene utente"), + ("Password missed", "Mancat sa crae de intrada"), + ("Wrong credentials", "Credentziales isballiadas"), + ("The verification code is incorrect or has expired", "Su còdighe de verìfica no est curretu o est iscadidu"), + ("Edit Tag", "Modìfica eticheta"), + ("Forget Password", "Ismèntiga sa crae"), + ("Favorites", "Preferidos"), + ("Add to Favorites", "Annanghe a sos preferidos"), + ("Remove from Favorites", "Boga dae sos preferidos"), + ("Empty", "Bòidu"), + ("Invalid folder name", "Nùmene de sa cartella non vàlidu"), + ("Socks5 Proxy", "Serbidore intermediàriu Socks5"), + ("Socks5/Http(s) Proxy", "Serbidore intermediàriu Socks5/Http(s)"), + ("Discovered", "Rileva"), + ("install_daemon_tip", "Pro aviare su programma a s'allughìngiu, tocat a l'installare comente servìtziu de sistema."), + ("Remote ID", "ID remotu"), + ("Paste", "Incolla"), + ("Paste here?", "Incollare inoghe?"), + ("Are you sure to close the connection?", "Ses seguru de bòlere serrare sa connessione?"), + ("Download new version", "Iscàrriga sa versione noa"), + ("Touch mode", "Modalidade tocu"), + ("Mouse mode", "Modalidade ratu"), + ("One-Finger Tap", "Tocu cun unu pòddighe"), + ("Left Mouse", "Butone de manca de su ratu"), + ("One-Long Tap", "Tocu longu cun unu pòddighe"), + ("Two-Finger Tap", "Tocu cun duos pòddighes"), + ("Right Mouse", "Butone de destra de su ratu"), + ("One-Finger Move", "Movimentu cun unu pòddighe"), + ("Double Tap & Move", "Tocu dòpiu e movimentu"), + ("Mouse Drag", "Trisinada de su ratu"), + ("Three-Finger vertically", "Tres pòddighes in verticale"), + ("Mouse Wheel", "Rodedda de su ratu"), + ("Two-Finger Move", "Movimentu cun duos pòddighes"), + ("Canvas Move", "Isposta sa tela"), + ("Pinch to Zoom", "Pìtziga pro ismanniare"), + ("Canvas Zoom", "Ismanniamentu tela"), + ("Reset canvas", "Reseta sa tela"), + ("No permission of file transfer", "Perunu permissu pro sa tràmuda de documentos"), + ("Note", "Nota"), + ("Connection", "Connessione"), + ("Share screen", "Cumpartzi ischermu"), + ("Chat", "Tzarrada"), + ("Total", "Totale"), + ("items", "Elementos"), + ("Selected", "Seletzionadu"), + ("Screen Capture", "Catura de ischermu"), + ("Input Control", "Controllu atziones"), + ("Audio Capture", "Catura de s'àudio"), + ("Do you accept?", "Atzetas?"), + ("Open System Setting", "Aberi sas impostatziones de sistema"), + ("How to get Android input permission?", "Comente otènnere s'autorizatzione de intrada (input) in Android?"), + ("android_input_permission_tip1", "Pro chi unu dispositivu remotu potzat controllare unu dispositivu Android pro mèdiu de unu ratu o cun su tocu, depes cunsentire a RustDesk de impreare su servìtziu 'Atzessibilidade'."), + ("android_input_permission_tip2", "Bae a sa pàgina de sas impostatziones de sistema chi s'at a abèrrere a pustis, busca e intra a [Servìtzios installados], allughe su servìtziu [Intrada RustDesk]."), + ("android_new_connection_tip", "Est istada retzida una dimanda noa de controllu pro su dispositivu atuale."), + ("android_service_will_start_tip", "S'ativatzione de Catura ischermu at a aviare in automàticu su servìtziu, permitende a àteros dispositivos de pedire una connessione dae custu dispositivu."), + ("android_stop_service_tip", "Sa serrada de su servìtziu at a tancare in automàticu totu sas connessiones istabilidas."), + ("android_version_audio_tip", "Sa versione atuale de Android non suportat s'achirimentu àudio, faghe s'atualizatzione a Android 10 o versiones prus noas."), + ("android_start_service_tip", "Pro aviare su servìtziu de cumpartzidura de s'ischermu seletziona [Avia su servìtziu] o abìlita s'autorizatzione [Catura de ischermu]."), + ("android_permission_may_not_change_tip", "Sas autorizatziones pro sas connessiones istabilidas non si podent modificare in manera istantànea finas a sa riconnessione."), + ("Account", "Contu"), + ("Overwrite", "Subraiscrie"), + ("This file exists, skip or overwrite this file?", "Custu documentu esistit, boles ignorare o subraiscìere custu archìviu?"), + ("Quit", "Essi"), + ("Help", "Agiudu"), + ("Failed", "Fallidu"), + ("Succeeded", "Cumpridu"), + ("Someone turns on privacy mode, exit", "Calicunu at allutu sa modalidade de riservadesa, essida"), + ("Unsupported", "Non suportadu"), + ("Peer denied", "Atzessu negadu a su dispositivu remotu"), + ("Please install plugins", "Installa sos cumplementos"), + ("Peer exit", "Essida dae su dispostivu remotu"), + ("Failed to turn off", "Non faghet a istudare"), + ("Turned off", "Istuda"), + ("Language", "Limba"), + ("Keep RustDesk background service", "Mantene su servìtziu de RustDesk in s'isfundu"), + ("Ignore Battery Optimizations", "Ignora sas otimizatziones de sa bateria"), + ("android_open_battery_optimizations_tip", "Si boles disabilitare custa funtzione, bae a sas impostatziones de s'aplicatzione RustDesk, aberi sa setzione 'Bateria' e boga sa seletzione a 'Chene restritziones'."), + ("Start on boot", "Avia a s'allughidura"), + ("Start the screen sharing service on boot, requires special permissions", "S'aviu de su servìtziu de cumpartzidura de s'ischermu a s'allughidura tenet bisòngiu de permissos ispetziales"), + ("Connection not allowed", "Connessione non permìtida"), + ("Legacy mode", "Modalidade antiga"), + ("Map mode", "Modalidade mapa"), + ("Translate mode", "Modalidade tradutzione"), + ("Use permanent password", "Imprea una crae de intrada permanente"), + ("Use both passwords", "Imprea craes de intrada monoimpreu e permanente"), + ("Set permanent password", "Imposta sa crae permanente"), + ("Enable remote restart", "Abìlita riaviu dae remotu"), + ("Restart remote device", "Torra a aviare su dispositivu remotu"), + ("Are you sure you want to restart", "Ses seguru de bòlere torrare a allùghere?"), + ("Restarting remote device", "Su dispositivu remotu s'est torrende a allùghere"), + ("remote_restarting_tip", "Torra a allùghere su dispositivu remotu"), + ("Copied", "Copiadu"), + ("Exit Fullscreen", "Essi dae sa modalidade a ischermu intreu"), + ("Fullscreen", "A ischermu intreu"), + ("Mobile Actions", "Atziones mòbiles"), + ("Select Monitor", "Seleziona ischermu"), + ("Control Actions", "Atziones de controllu"), + ("Display Settings", "Impostatziones de visualizatzione"), + ("Ratio", "Raportu"), + ("Image Quality", "Calidade de s'immàgine"), + ("Scroll Style", "Istile de iscurrimentu"), + ("Show Toolbar", "Mustra s'istanga de trastes"), + ("Hide Toolbar", "Cua s'istanga de trastes"), + ("Direct Connection", "Connessione direta"), + ("Relay Connection", "Connessione tramudada (relay)"), + ("Secure Connection", "Connessione segura"), + ("Insecure Connection", "Connessione non segura"), + ("Scale original", "Iscala originale"), + ("Scale adaptive", "Iscala adativa"), + ("General", "Generale"), + ("Security", "Seguresa"), + ("Theme", "Tema"), + ("Dark Theme", "Tema iscuru"), + ("Light Theme", "Tema craru"), + ("Dark", "Iscuru"), + ("Light", "Craru"), + ("Follow System", "Sistema"), + ("Enable hardware codec", "Abìlita codificadore fìsicu"), + ("Unlock Security Settings", "Isbloca sas impostatziones de seguresa"), + ("Enable audio", "Abìlita àudio"), + ("Unlock Network Settings", "Isbloca impostatziones de rete"), + ("Server", "Serbidore"), + ("Direct IP Access", "Atzessu IP diretu"), + ("Proxy", "Serbidore intermediàriu"), + ("Apply", "Àplica"), + ("Disconnect all devices?", "Boles iscollegare totu sos dispositivos?"), + ("Clear", "Isbòida"), + ("Audio Input Device", "Dispositivu intrada àudio"), + ("Use IP Whitelisting", "Imprea elencu IP autorizados"), + ("Network", "Rete"), + ("Pin Toolbar", "Bloca s'istanga de trastes"), + ("Unpin Toolbar", "Isbloca s'istanga de trastes"), + ("Recording", "Registratzione"), + ("Directory", "Cartella"), + ("Automatically record incoming sessions", "Registra in automàticu sas sessiones in intrada"), + ("Automatically record outgoing sessions", "Registra in automàticu sas sessiones in essida"), + ("Change", "Modìfica"), + ("Start session recording", "Incumintza sa registrazione de sa sessione"), + ("Stop session recording", "Firma sa registrazione de sa sessione"), + ("Enable recording session", "Abìlita sa registrazione de sa sessione"), + ("Enable LAN discovery", "Abìlita su rilevamentu LAN"), + ("Deny LAN discovery", "Disabìlita su rilevamentu LAN"), + ("Write a message", "Iscrie unu messàgiu"), + ("Prompt", "Pedi"), + ("Please wait for confirmation of UAC...", "Iseta sa cunfirma de s'UAC..."), + ("elevated_foreground_window_tip", "Sa ventana atuale de s'elaboradore remotu tenet bisòngiu, pro funtzionare, de privilègios prus mannos, duncas non faghet a impreare in manera temporànea su ratu e su tecladu.\nSi podet pedire a s'utente remotu de minimare a icona sa ventana atuale o de seletzionare su pulsante de artària in sa ventana de gestione de sa connessione.\nPro evitare custu problema, ti cussigiamus de installare su programma in su dispositivu remotu."), + ("Disconnected", "Iscollegadu"), + ("Other", "Àteru"), + ("Confirm before closing multiple tabs", "Cunfirma in antis de serrare prus ischedas"), + ("Keyboard Settings", "Impostatziones de tecladu"), + ("Full Access", "Atzessu cumpridu"), + ("Screen Share", "Cumpartzidura de ischermu"), + ("ubuntu-21-04-required", "Wayland tenet bisòngiu de Ubuntu 21.04 o versione prus noa."), + ("wayland-requires-higher-linux-version", "Wayland tenet bisòngiu de una versione prus noa de sa distributzione Linux.\nProa X11 pro elaboradores o càmbia su sistema operativu."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Bae a"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seletziona s'ischermu de cumpartzire (òpera dae s'ala de su dispositivu remotu)."), + ("Show RustDesk", "Mustra RustDesk"), + ("This PC", "Custu PC"), + ("or", "O"), + ("Elevate", "Cresche"), + ("Zoom cursor", "Cursore de ismanniamentu"), + ("Accept sessions via password", "Atzeta sessiones cun sa crae"), + ("Accept sessions via click", "Atzeta sessiones cun sas incarcadas"), + ("Accept sessions via both", "Atzeta sessiones cun totu sas duas craes"), + ("Please wait for the remote side to accept your session request...", "Iseta chi su dispositivu remotu atzetet sa dimanda de sessione..."), + ("One-time Password", "Crae monoimpreu"), + ("Use one-time password", "Imprea crae monoimpreu"), + ("One-time password length", "Longària crae monoimpreu"), + ("Request access to your device", "Pedi s'atzessu a su dispositivu"), + ("Hide connection management window", "Cua sa ventana de gestione de sas connessiones"), + ("hide_cm_tip", "Permite de cuare petzi si s'atzetant sessiones cun crae permanente"), + ("wayland_experiment_tip", "Su suportu Wayland est in fase isperimentale, si boles un'atzessu istàbile imprea X11."), + ("Right click to select tabs", "Incarca cun su pulsante destru pro seletzionare sas ischedas"), + ("Skipped", "Brincadu"), + ("Add to address book", "Annanghe a sa rubrica"), + ("Group", "Grupu"), + ("Search", "Chirca"), + ("Closed manually by web console", "Serra in manera manuale dae sa console web"), + ("Local keyboard type", "Casta de tecladu locale"), + ("Select local keyboard type", "Seletziona sa casta de tecladu locale"), + ("software_render_tip", "Si in s'elaboradore cun Linux b'at un'ischeda grafica Nvidia e sa ventana remota si serrat deretu a pustis de sa connessione, installa su driver nou a còdighe abertu e imprea sa renderitzatzione tràmite programma (software).\nDiat pòdere bisongiare a torrare a allùghere su programma."), + ("Always use software rendering", "Imprea semper sa renderizatzione tràmite programma"), + ("config_input", "Pro controllare s'elaboradore remotu cun su tecladu bisòngiat a frunire a RustDesk sos permissos de 'Monitoràgiu insertada'."), + ("config_microphone", "Per pòdere mutire, bisòngiat a frunire su premissu 'Registra àudio' a RustDesk."), + ("request_elevation_tip", "Si b'at calicunu in s'ala remota si podet pedire sa crèschida."), + ("Wait", "Iseta"), + ("Elevation Error", "Faddina durante sa crèschida de sos deretos"), + ("Ask the remote user for authentication", "Pedi s'autenticatzione a s'utente remotu"), + ("Choose this if the remote account is administrator", "Issèbera custa optzione si su contu remotu est amministradore"), + ("Transmit the username and password of administrator", "Trasmite su nùmene utente e sa crae de intrada de s'amministradore"), + ("still_click_uac_tip", "Torra a pedire chi s'utente remotu seletziones 'AB' in sa ventana UAC de s'esecutzione de RustDesk."), + ("Request Elevation", "Pedi sa crèschida de sos deretos"), + ("wait_accept_uac_tip", "Iseta chi s'utente remotu atzetet sa ventana de diàlogu UAC."), + ("Elevate successfully", "Crèschida de sos deretos cumprida"), + ("uppercase", "Majùscula"), + ("lowercase", "Minùscula"), + ("digit", "Nùmeru"), + ("special character", "Caràtere ispetziale"), + ("length>=8", "Lunghezza >= 8"), + ("Weak", "Dèbile"), + ("Medium", "Mesana"), + ("Strong", "Forte"), + ("Switch Sides", "Càmbia ala"), + ("Please confirm if you want to share your desktop?", "Boles cumpartzire s'elaboradore?"), + ("Display", "Visualizatzione"), + ("Default View Style", "Istile de visualiztazione predefinidu"), + ("Default Scroll Style", "Istile de iscurrimentu predefinidu"), + ("Default Image Quality", "Calidade de s'immàgine predefinida"), + ("Default Codec", "Codificadore predefinidu"), + ("Bitrate", "Tassu de bits"), + ("FPS", "FPS"), + ("Auto", "Automàticu"), + ("Other Default Options", "Àteras optziones predefinidas"), + ("Voice call", "Mutida vocale"), + ("Text chat", "Tzarrada de testu"), + ("Stop voice call", "Interrumpe sa mutida vocale"), + ("relay_hint_tip", "Si non faghet a si connètere in manera direta, podes proare a ti collegare impreende unu serbidore de tràmuda.\nIn prus, si boles imprearevsu serbidore de tràmuda in su primu tentativu, podes annànghere a s'ID su suffissu '/r\' o seletzionare in s'ischeda si esistit s'optzione 'Collega·ti semper impreende una tràmuda relay'."), + ("Reconnect", "Collega·ti torra"), + ("Codec", "Codificadore"), + ("Resolution", "Risolutzione"), + ("No transfers in progress", "Peruna tràmuda in cursu"), + ("Set one-time password length", "Imposta sa longària de sa crae monoimpreu"), + ("RDP Settings", "Impostatziones RDP"), + ("Sort by", "Òrdina pro"), + ("New Connection", "Connessione noa"), + ("Restore", "Riprìstina"), + ("Minimize", "Mìnima"), + ("Maximize", "Massimiza"), + ("Your Device", "Custu dispositivu"), + ("empty_recent_tip", "Non b'at galu peruna sessione reghente!\nPianifica·nde una."), + ("empty_favorite_tip", "Galu peruna connessione?\nBusca calicunu cun chie ti collegare e annanghe·lu a sos preferidos!"), + ("empty_lan_tip", "Paret a beru chi non siat istada atzapada peruna connessione."), + ("empty_address_book_tip", "Paret chi pro como in sa rubrica non b'apat connessiones."), + ("Empty Username", "Nùmene utente bòidu"), + ("Empty Password", "Crae bòida"), + ("Me", "Deo"), + ("identical_file_tip", "Custu archìviu est pretzisu a su chi b'at in su dispositivu remotu."), + ("show_monitors_tip", "Mustra sos ischermos in s'istanga de sos trastes"), + ("View Mode", "Modalidade de visualizatzione"), + ("login_linux_tip", "Intra a su contu de Linux remotu"), + ("verify_rustdesk_password_tip", "Cunfirma sa crae de RustDesk"), + ("remember_account_tip", "Ammenta custu contu"), + ("os_account_desk_tip", "Custu contu s'impreat pro intrare a su sistema operativu remotu e ativare sa sessione de s'elaboradore in modalidade non presidiada."), + ("OS Account", "Contu sistema operativu"), + ("another_user_login_title_tip", "Un'àteru utente at giai fatu s'atzessu."), + ("another_user_login_text_tip", "Separadu"), + ("xorg_not_found_title_tip", "Xorg no atzapadu."), + ("xorg_not_found_text_tip", "Installa Xorg."), + ("no_desktop_title_tip", "Non b'at perunu ambiente de elaboradore a disponimentu."), + ("no_desktop_text_tip", "Installa s'ambiente de elaboradore GNOME."), + ("No need to elevate", "Crèschida de sos privilègios non pedida"), + ("System Sound", "Dispositivu àudio de sistema"), + ("Default", "Predefinida"), + ("New RDP", "RDP nou"), + ("Fingerprint", "Firma digitale"), + ("Copy Fingerprint", "Còpia firma digitale"), + ("no fingerprints", "Peruna firma digitale"), + ("Select a peer", "Seletziona su dispositivu remotu"), + ("Select peers", "Seletziona sos dispositivos remotos"), + ("Plugins", "Cumplementos"), + ("Uninstall", "Disinstalla"), + ("Update", "Atualiza"), + ("Enable", "Abìlita"), + ("Disable", "Disabìlita"), + ("Options", "Optziones"), + ("resolution_original_tip", "Risolutzione originale"), + ("resolution_fit_local_tip", "Adata sa risolutzione locale"), + ("resolution_custom_tip", "Risolutzione personalizada"), + ("Collapse toolbar", "Mìnima s'istanga de sos trastes"), + ("Accept and Elevate", "Atzeta e cresche"), + ("accept_and_elevate_btn_tooltip", "Atzeta sa connessione e cresche sos permissos UAC."), + ("clipboard_wait_response_timeout_tip", "Tempus de isetu de rispota dae sa còpia iscadidu."), + ("Incoming connection", "Connessiones in intrada"), + ("Outgoing connection", "Connessiones in essida"), + ("Exit", "Essi dae RustDesk"), + ("Open", "Aberi RustDesk"), + ("logout_tip", "Ses seguru de bòlere essire?"), + ("Service", "Servìtziu"), + ("Start", "Allughe"), + ("Stop", "Firma"), + ("exceed_max_devices", "Ses arribbadu a su nùmeru màssimu de dispositivos chi podes manigiare."), + ("Sync with recent sessions", "Sincroniza cun sas sessiones reghentes"), + ("Sort tags", "Òrdina sas etichetas"), + ("Open connection in new tab", "Aberi sa connessione in un'ischeda noa"), + ("Move tab to new window", "Move s'ischeda a sa ventana imbeniente"), + ("Can not be empty", "Non podet èssere bòidu"), + ("Already exists", "Esistit giai"), + ("Change Password", "Modìfica sa crae"), + ("Refresh Password", "Annoa sa crae"), + ("ID", "ID"), + ("Grid View", "Vista grìllia"), + ("List View", "Vista elencu"), + ("Select", "Seletziona"), + ("Toggle Tags", "Allughe/istuda eticheta"), + ("pull_ab_failed_tip", "Non faghet a annoare sa rubrica"), + ("push_ab_failed_tip", "Non faghet a sincronizare sa rubrica cun su serbidore"), + ("synced_peer_readded_tip", "Sos dispositivos chi bi sunt in sas sessiones reghentes s'ant a torrare a sincronizare in sa rubrica."), + ("Change Color", "Modìfica colore"), + ("Primary Color", "Colore primàriu"), + ("HSV Color", "Colore HSV"), + ("Installation Successful!", "Installatzione cumprida"), + ("Installation failed!", "Installtazione fallida"), + ("Reverse mouse wheel", "Funtzione rodedda ratu furriada"), + ("{} sessions", "{} sessiones"), + ("scam_title", "Ti diant pòdere àere TRAMPADU!"), + ("scam_text1", "Si ses in su telèfonu cun calicunu chi NON connosches NON FIDADU chi t'at pedidu de impreare RustDesk e de allùghere su servìtziu, non sigas e tanca deretu."), + ("scam_text2", "Est dàbile chi siat unu trampadore chi chircat de furare su dinare tuo o àteras informatziones privadas tuas."), + ("Don't show again", "Non mustres prus"), + ("I Agree", "Atzeto"), + ("Decline", "No atzeto"), + ("Timeout in minutes", "Tempus de iscadèntzia in minutos"), + ("auto_disconnect_option_tip", "Serra in automàticu sas sessiones in intrada pro inatividade de s'utente"), + ("Connection failed due to inactivity", "Connessione non resèssida pro neghe de inatividade"), + ("Check for software update on startup", "A s'allughìngiu avèrgua sa presèntzia de atualizatziones pro su programma"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Atualiza RustDesk Server Pro a sa versione {} o prus noa!"), + ("pull_group_failed_tip", "Non faghet a annoare su grupu"), + ("Filter by intersection", "Filtra pro rugrada"), + ("Remove wallpaper during incoming sessions", "Boga s'isfundu durante sas sessiones in intrada"), + ("Test", "Proa"), + ("display_is_plugged_out_msg", "S'ischermu est iscollegadu, colo a su primu ischermu."), + ("No displays", "Perunu ischermu"), + ("Open in new window", "Aberi in una ventana noa"), + ("Show displays as individual windows", "Mustra sos ischermos comente ventanas individuales"), + ("Use all my displays for the remote session", "In sa sessione remota imprea totu sos ischermos"), + ("selinux_tip", "In custu dispositivu est abilitadu SELinux, chi diat pòdere su funtzionamentu curretu de RustDesk comente ala controllada."), + ("Change view", "Modìfica vista"), + ("Big tiles", "Iconas mannas"), + ("Small tiles", "Iconas minores"), + ("List", "Elencu"), + ("Virtual display", "Ischermu virtuale"), + ("Plug out all", "Iscollega totu"), + ("True color (4:4:4)", "Colore reale (4:4:4)"), + ("Enable blocking user input", "Abìlita blocu insertada utente"), + ("id_input_tip", "Podes insertare un'ID, un'IP diretu o unu domìniu cun una ghenna (:).\nSi boles atzèdere a unu dispositivu in un'àteru serbidore, annanghe s'indiritzu de su serbidore (@?key=), a esèmpiu\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi boles atzèdere a unu dispositivu in unu serbidore pùblicu, inserta \"@public\", pro su serbidore pùblicu sa crae non serbit\n\nSi boles fortzare s'impreu de una connessione de inoltru a sa prima connessione, annanghe \"/r\" a sa fine de s'ID, a esèmpiu \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Manera 1"), + ("privacy_mode_impl_virtual_display_tip", "Manera 2"), + ("Enter privacy mode", "Intra in modalidade de riservadesa"), + ("Exit privacy mode", "Essi dae sa modalidade de riservadesa"), + ("idd_not_support_under_win10_2004_tip", "Su driver vìdeu indiretu no est suportadu. Bisòngiat Windows 10, versione 2004 o prus noa."), + ("input_source_1_tip", "Fonte intrada (1)"), + ("input_source_2_tip", "Fonte intrada (2)"), + ("Swap control-command key", "Cuncàmbia tecla controllu-cumandu"), + ("swap-left-right-mouse", "Cuncàmbia pulsante mancu-destru ratu"), + ("2FA code", "Còdighe 2FA"), + ("More", "Àteru"), + ("enable-2fa-title", "Abìlita s'autenticatzione a duos fases"), + ("enable-2fa-desc", "Cunfigura s'autenticadore.\nPodes impreare un'aplicatzione de autenticatzione che a Authy, Microsoft o Google Authenticator in su telèfonu o elaboredore.\n\nPro abilitare s'autenticatzione a duas fases iscansi su còdighe QR cun s'aplicatzione e inserta su còdighe mustradu dae s'aplicatzione."), + ("wrong-2fa-code", "Non faghet a averguare su còdighe.\nVerìfica chi sas impostatziones de su còdighe e de s'ora locale siant curretas"), + ("enter-2fa-title", "Autenticatzione a duas fases"), + ("Email verification code must be 6 characters.", "Su còdighe de verìfica posta eletrònica depet cuntènnere 6 caràteres."), + ("2FA code must be 6 digits.", "Su còdighe 2FA depet èssere fatu de 6 tzifras."), + ("Multiple Windows sessions found", "Sessiones de Windows mùltiplas atzapadas"), + ("Please select the session you want to connect to", "Seletziona sa sessione cun chi ti boles cunnètere"), + ("powered_by_me", "Alimentadu dae RustDesk"), + ("outgoing_only_desk_tip", "Custa est un'editzione personalizada.\nTi podes connètere a àteros dispositivos, ma sos àteros dispositivos non si podent connètere a custu dispositivu."), + ("preset_password_warning", "Custa est un'editzione personalizada e benit frunida cun una crae de intrada pre-impostada.\nTotu sos chi connoschent custa crae diant pòdere otènnere su controllu totale de su dispositivu.\nSi non ti l'isetaias, disinstalla deretu su programma."), + ("Security Alert", "Avisu de seguresa"), + ("My address book", "Rubrica"), + ("Personal", "Personale"), + ("Owner", "Proprietàriu"), + ("Set shared password", "Imposta una crae cumpartzida"), + ("Exist in", "Esistit in"), + ("Read-only", "Leghidura ebbia"), + ("Read/Write", "Leghidura/iscritura"), + ("Full Control", "Controllu totale"), + ("share_warning_tip", "Sos campos inoghe in subra sunt cumpartzidos e sos àteros los pòdent bìdere."), + ("Everyone", "Totus"), + ("ab_web_console_tip", "Àteras informatziones subra de sa console web"), + ("allow-only-conn-window-open-tip", "Permite sa connessione petzi si sa ventana RustDesk est aberta"), + ("no_need_privacy_mode_no_physical_displays_tip", "Perunu ischermu fìsicu, peruna netzessidade de impreare sa modalidade de riservadesa."), + ("Follow remote cursor", "Sighi su cursore remotu"), + ("Follow remote window focus", "Sighi su focus de sa ventana remota"), + ("default_proxy_tip", "Protocollu e ghenna predefinidos sunt Socks5 e 1080"), + ("no_audio_input_device_tip", "Perunu dispositivu de intrada àudio atzapadu."), + ("Incoming", "In intrada"), + ("Outgoing", "In essida"), + ("Clear Wayland screen selection", "Annulla seletzione ischermada Wayland"), + ("clear_Wayland_screen_selection_tip", "A pustis de àere annulladu sa seletzione de ischermu, podes torrare a seletzionare s'ischermu de cumpartzire."), + ("confirm_clear_Wayland_screen_selection_tip", "Ses seguru de bòlere annullare sa seletzione de ischermu Wayland?"), + ("android_new_voice_call_tip", "As retzidu una rechuesta noa de mutida vocale. Si l'atzetas, sàudio at a colare a sa comunicatzione vocale."), + ("texture_render_tip", "Imprea sa tessidura de renderizatzione pro fàghere sas immàgines prus flùidas. Si atzapas problemas, proa a disabilitare custa optzione."), + ("Use texture rendering", "Imprea sa tessidura de renderizatzione"), + ("Floating window", "Ventana gallegiante"), + ("floating_window_tip", "Agiudat a mantènnere su servìtziu in s'isfundu de RustDesk"), + ("Keep screen on", "Mantene s'ischermu allutu"), + ("Never", "Mai"), + ("During controlled", "Durante su controllu"), + ("During service is on", "Cando su servìtziu est ativu"), + ("Capture screen using DirectX", "Catura s'ischermu impreende DirectX"), + ("Back", "In segus"), + ("Apps", "Aplicatziones"), + ("Volume up", "Volume +"), + ("Volume down", "Volume -"), + ("Power", "Alimentatzione"), + ("Telegram bot", "Bot de Telegram"), + ("enable-bot-tip", "Si abilitas custa funtzione, podes retzire su còdighe 2FA dae su bot tuo.\nPodes funtzionare fintzas comente notìfica de connessione."), + ("enable-bot-desc", "1. aberi una tzarrada cun @BotFather.\n2. Inbia su cumandu \"/newbot\", a pustis de àere fatu custu passàgiu as a retzire unu getone.\n3. Incumintza una tzarrada cun su bot tuo creadu como. Imbia unu messàgiu chi incumintzat cun un'istanga (\"/\") a tipu \"/salude\".\n"), + ("cancel-2fa-confirm-tip", "Ses seguru de bòlere annullare sa 2FA?"), + ("cancel-bot-confirm-tip", "Ses seguru de bòlere annullare Telegram?"), + ("About RustDesk", "Informatziones subra de RustDesk"), + ("Send clipboard keystrokes", "Imbia fileras teclas puntas de billete"), + ("network_error_tip", "Controlla sa connessione de rete, e a pustis seletziona 'Torra a proare'."), + ("Unlock with PIN", "Abìlita s'isblocu cun PIN"), + ("Requires at least {} characters", "Bisòngiant a su nessi {} caràteres"), + ("Wrong PIN", "PIN isballiadu"), + ("Set PIN", "Imposta su PIN"), + ("Enable trusted devices", "Abìlita dispositivos fidados"), + ("Manage trusted devices", "Manìgia sos dispositivos fidados"), + ("Platform", "Prataforma"), + ("Days remaining", "Dies chi abarrant"), + ("enable-trusted-devices-tip", "Brinca sa verìfica 2FA in sos dispositivos fidados"), + ("Parent directory", "Cartella printzipale"), + ("Resume", "Sighi"), + ("Invalid file name", "Nùmene archìviu non vàlidu"), + ("one-way-file-transfer-tip", "In s'ala controllada est abilitada sa tràmuda de archìvios a una diretzione ebbia."), + ("Authentication Required", "Dimanda de autenticatzione"), + ("Authenticate", "Autèntica"), + ("web_id_input_tip", "Podes insertare un'ID in su matessi serbidore, in su cliente web no est suportadu s'atzessu cun IP diretu.\nSi boles atzèdere a unu dispositivu in un'àteru serbidore, annanghe s'indiritzu de su serbidore (@?key=), a esèmpiu,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi boles intrare a unu dispositivu in unu serbidore pùblicu, inserta \"@public\", non b'at bisòngiu de sa crae pro su serbidore pùblicu."), + ("Download", "Iscàrriga"), + ("Upload folder", "Cartella de carrigamentu"), + ("Upload files", "Carrigamentu de archìvios upload"), + ("Clipboard is synchronized", "Sa punta de billete est sincronizada"), + ("Update client clipboard", "Annoa sa punta de billete de su cliente"), + ("Untagged", "Chene tag"), + ("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"), + ("Accessible devices", "Dispositivos atzessìbiles"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualiza su cliente RustDesk remotu a sa versione {} o prus noa!"), + ("d3d_render_tip", "Cando sa renderizatzione D3D est abilitada, s'ischermu de controllu remotu diat pòdere èssere nieddu in unas cantas màchinas"), + ("Use D3D rendering", "Imprea sa renderizatzione D3D"), + ("Printer", "Imprentadora"), + ("printer-os-requirement-tip", "Pro pòdere impreare s'imprentadora in seddida bisòngiat a installare {} in custu dispositivu."), + ("printer-requires-installed-{}-client-tip", "Pro sa funtzionalidade de imprenta in essida b'at bisòngiu de Window 10 o prus nou."), + ("printer-{}-not-installed-tip", "S'imprentadora {} no est installada"), + ("printer-{}-ready-tip", "S'imprentadora {} est installada e pronta pro s'impreu."), + ("Install {} Printer", "Installa s'imprentadora {}"), + ("Outgoing Print Jobs", "Traballos de imprenta in essida"), + ("Incoming Print Jobs", "Traballos de imprenta in intrada"), + ("Incoming Print Job", "Traballu de imprenta in intrada"), + ("use-the-default-printer-tip", "Imprea s'imprentadora predefinida"), + ("use-the-selected-printer-tip", "Imprea s'imprentadora seletzionada"), + ("auto-print-tip", "Imprenta in automàticu impreende s'imprentadora seletzionada."), + ("print-incoming-job-confirm-tip", "As retzidu unu traballu de imprenta dae remotu. Lu boles esecutare dae s'ala tua?"), + ("remote-printing-disallowed-tile-tip", "Imprenta remota disabilitada"), + ("remote-printing-disallowed-text-tip", "Sas impostatziones de sos permissos de s'ala controllada negant s'imprenta remota."), + ("save-settings-tip", "Sarva sas impostatziones"), + ("dont-show-again-tip", "Non mustres prus custu messàgiu"), + ("Take screenshot", "Faghe un'ischermada"), + ("Taking screenshot", "Faghende un'ischermada"), + ("screenshot-merged-screen-not-supported-tip", "S'unione de sa catura de ischermadas de prus ischermos como no est suportada.\nCola a un'ischermu ebbia e torra a proare."), + ("screenshot-action-tip", "Seletziona comente sighire cun s'ischermada."), + ("Save as", "Sarva comente"), + ("Copy to clipboard", "Còpia in punta de billete"), + ("Enable remote printer", "Abìlita imprentadora remota"), + ("Downloading {}", "Iscarrighende {}"), + ("{} Update", "Atualiza {}"), + ("{}-to-update-tip", "{} s'at a serrare e a installare sa versione nova"), + ("download-new-version-failed-tip", "Iscarrigamentu fallidu.\nPodes torrare a proare o seletzionare 'Iscàrriga' pro iscarrigare e atualizare a manera manuale."), + ("Auto update", "Atualizatzione automàtica"), + ("update-failed-check-msi-tip", "Controllu de sa manera de installatzione fallidu.\nSeletziona 'Iscàrriga' pro iscarrigare su programma e l'atualizare a manera manuale."), + ("websocket_tip", "Cando impreas WebSocket, sunt suportadas petzi sas connessiones de tràmuda relay"), + ("Use WebSocket", "Imprea WebSocket"), + ("Trackpad speed", "Velotzidade de su pannellu tàtile"), + ("Default trackpad speed", "Velotzidade predefinida de su pannellu tàtile"), + ("Numeric one-time password", "Crae numèrica monoimpreu"), + ("Enable IPv6 P2P connection", "Abìlita connessione P2P IPv6"), + ("Enable UDP hole punching", "Abìlita s'istampadura UDP"), + ("View camera", "Mustra sa càmera"), + ("Enable camera", "Abìlita sa càmera"), + ("No cameras", "Peruna càmera"), + ("view_camera_unsupported_tip", "Su dispositivu remotu non suportat sa visualizatzione de sa càmera"), + ("Terminal", "Terminale"), + ("Enable terminal", "Abìlita su terminale"), + ("New tab", "Ischeda noa"), + ("Keep terminal sessions on disconnect", "Cando ti disconnetes mantene aberta sa sessione de terminale"), + ("Terminal (Run as administrator)", "Terminale (imprea comente amministradore)"), + ("terminal-admin-login-tip", "Inserta su nùmene utente e sa crae de intrada de s'amministradore de s'ala controllada."), + ("Failed to get user token.", "Otenimentu de su getone de utente fallidu."), + ("Incorrect username or password.", "Nùmene utente o crae de intrada isballiados."), + ("The user is not an administrator.", "S'utente no est un'amministradore."), + ("Failed to check if the user is an administrator.", "Non faghet a verificare si s'utente est un'amministradore."), + ("Supported only in the installed version.", "Suportadu petzi in sa versione installada."), + ("elevation_username_tip", "Inserta Nùmene utente o domìniu de fonte\\nùmene Utente"), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Sighi cun {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/sk.rs b/vendor/rustdesk/src/lang/sk.rs new file mode 100644 index 0000000..6b4e166 --- /dev/null +++ b/vendor/rustdesk/src/lang/sk.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stav"), + ("Your Desktop", "Vaša plocha"), + ("desk_tip", "K svojej ploche sa môžete pripojiť pomocou zobrazeného ID a hesla."), + ("Password", "Heslo"), + ("Ready", "Pripravené"), + ("Established", "Nadviazané"), + ("connecting_status", "Pripájam sa na RustDesk server..."), + ("Enable service", "Povoliť službu"), + ("Start service", "Spustiť službu"), + ("Service is running", "Služba je aktívna"), + ("Service is not running", "Služba je vypnutá"), + ("not_ready_status", "Nepripravené. Skontrolujte svoje sieťové pripojenie."), + ("Control Remote Desktop", "Ovládať vzdialenú plochu"), + ("Transfer file", "Prenos súborov"), + ("Connect", "Pripojiť"), + ("Recent sessions", "Nedávne pripojenie"), + ("Address book", "Adresár kontaktov"), + ("Confirmation", "Potvrdenie"), + ("TCP tunneling", "TCP tunelovanie"), + ("Remove", "Odstrániť"), + ("Refresh random password", "Aktualizovať náhodné heslo"), + ("Set your own password", "Nastavte si svoje vlastné heslo"), + ("Enable keyboard/mouse", "Povoliť klávesnicu/myš"), + ("Enable clipboard", "Povoliť schránku"), + ("Enable file transfer", "Povoliť prenos súborov"), + ("Enable TCP tunneling", "Povoliť TCP tunelovanie"), + ("IP Whitelisting", "Zoznam povolených IP adries"), + ("ID/Relay Server", "ID/Prepojovací server"), + ("Import server config", "Importovať konfiguráciu servera"), + ("Export Server Config", "Exportovať konfiguráciu servera"), + ("Import server configuration successfully", "Konfigurácia servera bola úspešne importovaná"), + ("Export server configuration successfully", "Konfigurácia servera bola úspešne exportovaná"), + ("Invalid server configuration", "Neplatná konfigurácia servera"), + ("Clipboard is empty", "Schránka je prázdna"), + ("Stop service", "Zastaviť službu"), + ("Change ID", "Zmeniť ID"), + ("Your new ID", "Vaše nové ID"), + ("length %min% to %max%", "dĺžka medzi %min% a %max%"), + ("starts with a letter", "začína písmenom"), + ("allowed characters", "povolené znaky"), + ("id_change_tip", "Povolené sú len znaky a-z, A-Z, 0-9, - (dash) a _ (podčiarkovník). Prvý znak musí byť a-z, A-Z. Dĺžka musí byť medzi 6 a 16 znakmi."), + ("Website", "Webová stránka"), + ("About", "O RustDesk"), + ("Slogan_tip", "Stvorené srdcom v tomto chaotickom svete!"), + ("Privacy Statement", "Vyhlásenie o ochrane osobných údajov"), + ("Mute", "Stíšiť"), + ("Build Date", "Dátum zostavenia"), + ("Version", "Verzia"), + ("Home", "Domov"), + ("Audio Input", "Zvukový vstup"), + ("Enhancements", "Vylepšenia"), + ("Hardware Codec", "Hardvérový kodek"), + ("Adaptive bitrate", "Adaptívny dátový tok"), + ("ID Server", "ID server"), + ("Relay Server", "Prepojovací server"), + ("API Server", "API server"), + ("invalid_http", "Musí začínať http:// alebo https://"), + ("Invalid IP", "Neplatná IP adresa"), + ("Invalid format", "Neplatný formát"), + ("server_not_support", "Zatiaľ serverom nepodporované"), + ("Not available", "Nie je k dispozícii"), + ("Too frequent", "Príliš často"), + ("Cancel", "Zrušiť"), + ("Skip", "Preskočiť"), + ("Close", "Zatvoriť"), + ("Retry", "Zopakovať"), + ("OK", "OK"), + ("Password Required", "Vyžaduje sa heslo"), + ("Please enter your password", "Zadajte vaše heslo"), + ("Remember password", "Zapamätať heslo"), + ("Wrong Password", "Chybné heslo"), + ("Do you want to enter again?", "Chcete ho znova zadať?"), + ("Connection Error", "Chyba spojenia"), + ("Error", "Chyba"), + ("Reset by the peer", "Odmietnuté druhou stranou spojenia"), + ("Connecting...", "Pripájanie sa..."), + ("Connection in progress. Please wait.", "Pokúšam sa pripojiť. Počkajte chvíľu."), + ("Please try 1 minute later", "Skúte znova za minútu, alebo ešte neskôr"), + ("Login Error", "Chyba prihlásenia"), + ("Successful", "Úspech"), + ("Connected, waiting for image...", "Pripojené, čakám na obraz..."), + ("Name", "Názov"), + ("Type", "Typ"), + ("Modified", "Zmenené"), + ("Size", "Veľkosť"), + ("Show Hidden Files", "Zobraziť skryté súbory"), + ("Receive", "Prijať"), + ("Send", "Odoslať"), + ("Refresh File", "Aktualizovať súbor"), + ("Local", "Miestne"), + ("Remote", "Vzdialené"), + ("Remote Computer", "Vzdialený počítač"), + ("Local Computer", "Miestny počítač"), + ("Confirm Delete", "Potvrdenie zmazania"), + ("Delete", "Zmazať"), + ("Properties", "Vlastnosti"), + ("Multi Select", "Viacnásobný výber"), + ("Select All", "Vybrať všetko"), + ("Unselect All", "Zrušiť výber všetkého"), + ("Empty Directory", "Prázdny adresár"), + ("Not an empty directory", "Nie prázdny adresár"), + ("Are you sure you want to delete this file?", "Ste si istý, že chcete zmazať tento súbor?"), + ("Are you sure you want to delete this empty directory?", "Ste si istý, že chcete zmazať tento adresár?"), + ("Are you sure you want to delete the file of this directory?", "Ste si istý, že chcete zmazať tento súbor alebo adresár?"), + ("Do this for all conflicts", "Všetky konflikty riešiť týmto spôsobom"), + ("This is irreversible!", "Toto je nezvratná operácia!"), + ("Deleting", "Mazanie"), + ("files", "súbory"), + ("Waiting", "Čaká sa"), + ("Finished", "Ukončené"), + ("Speed", "Rýchlosť"), + ("Custom Image Quality", "Vlastná kvalita obrazu"), + ("Privacy mode", "Režim súkromia"), + ("Block user input", "Blokovať vstupné zariadenia užívateľa"), + ("Unblock user input", "Odblokovať vstupné zariadenia užívateľa"), + ("Adjust Window", "Prispôsobiť okno"), + ("Original", "Pôvodný"), + ("Shrink", "Zmenšené"), + ("Stretch", "Roztiahnuté"), + ("Scrollbar", "Posuvník"), + ("ScrollAuto", "Rolovať Auto"), + ("Good image quality", "Dobrá kvalita obrazu"), + ("Balanced", "Vyvážené"), + ("Optimize reaction time", "Optimalizované pre čas odozvy"), + ("Custom", "Vlastné"), + ("Show remote cursor", "Zobrazovať vzdialený ukazovateľ myši"), + ("Show quality monitor", "Zobraziť monitor kvality"), + ("Disable clipboard", "Vypnúť schránku"), + ("Lock after session end", "Po skončení uzamknúť plochu"), + ("Insert Ctrl + Alt + Del", "Vložiť Ctrl + Alt + Del"), + ("Insert Lock", "Uzamknúť"), + ("Refresh", "Aktualizovať"), + ("ID does not exist", "ID neexistuje"), + ("Failed to connect to rendezvous server", "Nepodarilo sa pripojiť k zoznamovaciemu serveru"), + ("Please try later", "Vyskúšajte neskôr"), + ("Remote desktop is offline", "Vzdialená plocha nie je pripojená"), + ("Key mismatch", "Kľúče sa nezhodujú"), + ("Timeout", "Čas pre nadviazanie pripojenia vypršal"), + ("Failed to connect to relay server", "Nepodarilo sa pripojiť k prepojovaciemu serveru"), + ("Failed to connect via rendezvous server", "Nepodarilo sa pripojiť cez zoznamovací server"), + ("Failed to connect via relay server", "Nepodarilo sa pripojiť cez prepojovací server"), + ("Failed to make direct connection to remote desktop", "Nepodarilo sa nadviazať priamu komunikáciu so vzdialenou plochou"), + ("Set Password", "Nastaviť heslo"), + ("OS Password", "Heslo do operačného systému"), + ("install_tip", "V niektorých prípadoch RustDesk nefunguje správne z dôvodu riadenia užívateľských oprávnení (UAC). Vyhnete sa tomu kliknutím na nižšie zobrazene tlačítko a nainštalovaním RuskDesk do systému."), + ("Click to upgrade", "Kliknutím nainštalujete aktualizáciu"), + ("Configure", "Nastaviť"), + ("config_acc", "Aby bolo možné na diaľku ovládať vašu plochu, je potrebné aplikácii RustDesk udeliť práva \"Dostupnosť\"."), + ("config_screen", "Aby bolo možné na diaľku sledovať vašu obrazovku, je potrebné aplikácii RustDesk udeliť práva \"Zachytávanie obsahu obrazovky\"."), + ("Installing ...", "Inštaluje sa"), + ("Install", "Inštalovať"), + ("Installation", "Inštalácia"), + ("Installation Path", "Inštalačný adresár"), + ("Create start menu shortcuts", "Vytvoriť zástupcu do ponuky Štart"), + ("Create desktop icon", "Vytvoriť ikonu na ploche"), + ("agreement_tip", "Spustením inštalácie prijímate licenčné podmienky."), + ("Accept and Install", "Prijať a inštalovať"), + ("End-user license agreement", "Licenčné podmienky dohodnuté s koncovým užívateľom"), + ("Generating ...", "Generujem ..."), + ("Your installation is lower version.", "Vaša inštalácia je staršia"), + ("not_close_tcp_tip", "Nezatvárajte toto okno po celý čas, kedy používate TCP tunel"), + ("Listening ...", "Čakám na pripojenie ..."), + ("Remote Host", "Vzdialený počítač"), + ("Remote Port", "Vzdialený port"), + ("Action", "Akcia"), + ("Add", "Pridať"), + ("Local Port", "Lokálny port"), + ("Local Address", "Lokálna adresa"), + ("Change Local Port", "Zmena lokálneho portu"), + ("setup_server_tip", "Pre zrýchlenie pripojenia si nainštalujte svoj vlastný server"), + ("Too short, at least 6 characters.", "Príliš krátke, vyžaduje sa aspoň 6 znakov."), + ("The confirmation is not identical.", "Potvrdenie nie je zhodné."), + ("Permissions", "Práva"), + ("Accept", "Prijať"), + ("Dismiss", "Odmietnuť"), + ("Disconnect", "Odpojiť"), + ("Enable file copy and paste", "Povoliť kopírovanie a vkladanie súborov"), + ("Connected", "Pripojené"), + ("Direct and encrypted connection", "Priame a šifrované spojenie"), + ("Relayed and encrypted connection", "Sprostredkované a šifrované spojenie"), + ("Direct and unencrypted connection", "Priame a nešifrované spojenie"), + ("Relayed and unencrypted connection", "Sprostredkované a nešifrované spojenie"), + ("Enter Remote ID", "Zadajte ID vzdialenej plochy"), + ("Enter your password", "Zadajte svoje heslo"), + ("Logging in...", "Prihlasovanie sa...."), + ("Enable RDP session sharing", "Povoliť zdieľanie RDP relácie"), + ("Auto Login", "Automatické prihlásenie"), + ("Enable direct IP access", "Povoliť priame pripojenie cez IP"), + ("Rename", "Premenovať"), + ("Space", "Medzera"), + ("Create desktop shortcut", "Vytvoriť zástupcu na ploche"), + ("Change Path", "Zmeniť adresár"), + ("Create Folder", "Vytvoriť adresár"), + ("Please enter the folder name", "Zadajte názov adresára"), + ("Fix it", "Opraviť to"), + ("Warning", "Upozornenie"), + ("Login screen using Wayland is not supported", "Prihlasovacia obrazovka prostredníctvom Wayland nie je podporovaná"), + ("Reboot required", "Vyžaduje sa reštart"), + ("Unsupported display server", "Nepodporovaný zobrazovací (display) server"), + ("x11 expected", "očakáva sa x11"), + ("Port", "Port"), + ("Settings", "Nastavenia"), + ("Username", "Uživateľské meno"), + ("Invalid port", "Neplatný port"), + ("Closed manually by the peer", "Manuálne ukončené opačnou stranou pripojenia"), + ("Enable remote configuration modification", "Povoliť zmeny konfigurácie zo vzdialeného PC"), + ("Run without install", "Spustiť bez inštalácie"), + ("Connect via relay", "Pripojenie prostredníctvom relay servera"), + ("Always connect via relay", "Vždy pripájať cez prepájací server"), + ("whitelist_tip", "Len vymenované IP adresy majú oprávnenie sa pripojiť k vzdialenej správe"), + ("Login", "Prihlásenie"), + ("Verify", "Overiť"), + ("Remember me", "Zapamätať si"), + ("Trust this device", "Dôverovať tomuto zariadeniu"), + ("Verification code", "Overovací kód"), + ("verification_tip", "Na vašu registrovanú e-mailovú adresu bol odoslaný overovací kód, zadajte ho a pokračujte v prihlasovaní."), + ("Logout", "Odhlásenie"), + ("Tags", "Štítky"), + ("Search ID", "Hľadať ID"), + ("whitelist_sep", "Oddelené čiarkou, bodkočiarkou, medzerou alebo koncom riadku"), + ("Add ID", "Pridať ID"), + ("Add Tag", "Pridať štítok"), + ("Unselect all tags", "Zrušiť výber všetkých štítkov"), + ("Network error", "Chyba siete"), + ("Username missed", "Chýba užívateľské meno"), + ("Password missed", "Chýba heslo"), + ("Wrong credentials", "Nesprávne prihlasovacie údaje"), + ("The verification code is incorrect or has expired", "Overovací kód je nesprávny alebo jeho platnosť vypršala"), + ("Edit Tag", "Upraviť štítok"), + ("Forget Password", "Zabudnúť heslo"), + ("Favorites", "Obľúbené"), + ("Add to Favorites", "Pridať medzi obľúbené"), + ("Remove from Favorites", "Odstrániť z obľúbených"), + ("Empty", "Prázdne"), + ("Invalid folder name", "Neplatný názov adresára"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "Objavené"), + ("install_daemon_tip", "Ak chcete, aby sa spúšťal pri štarte systému, musíte nainštalovať systémovú službu."), + ("Remote ID", "Vzdialené ID"), + ("Paste", "Vložiť"), + ("Paste here?", "Vložiť sem?"), + ("Are you sure to close the connection?", "Ste si istý, že chcete ukončiť spojenie?"), + ("Download new version", "Stiahnuť novú verziu"), + ("Touch mode", "Dotykový režim"), + ("Mouse mode", "Režim ovládania myšou"), + ("One-Finger Tap", "Klepnutie jedným prstom"), + ("Left Mouse", "Ľavé tlačidlo myši"), + ("One-Long Tap", "Jedno dlhé klepnutie"), + ("Two-Finger Tap", "Klepnutie dvoma prstami"), + ("Right Mouse", "Pravé tlačidlo myši"), + ("One-Finger Move", "Presúvanie jedným prstom"), + ("Double Tap & Move", "Dvojité klepnutie a presun"), + ("Mouse Drag", "Presun myšou"), + ("Three-Finger vertically", "Pohyb tromi prstami zvisle"), + ("Mouse Wheel", "Koliesko myši"), + ("Two-Finger Move", "Pohyb dvoma prstami"), + ("Canvas Move", "Pohyb zobrazenia"), + ("Pinch to Zoom", "Roztiahnutím prstov priblížiť"), + ("Canvas Zoom", "Priblíženie zobrazenia"), + ("Reset canvas", "Obnoviť zobrazenie"), + ("No permission of file transfer", "Prenos súborov nie je povolený"), + ("Note", "Poznámka"), + ("Connection", "Pripojenie"), + ("Share screen", "Zdielať obrazovku"), + ("Chat", "Chat"), + ("Total", "Celkom"), + ("items", "položiek"), + ("Selected", "Vybrané"), + ("Screen Capture", "Snímanie obrazovky"), + ("Input Control", "Ovládanie vstupných zariadení"), + ("Audio Capture", "Snímanie zvuku"), + ("Do you accept?", "Súhlasíte?"), + ("Open System Setting", "Otvorenie nastavení systému"), + ("How to get Android input permission?", "Ako v systéme Android povoliť oprávnenie písať zo vstupného zariadenia?"), + ("android_input_permission_tip1", "Aby bolo možné na diaľku ovládať vašu plochu pomocou myši alebo dotykov, je potrebné aplikácii RustDesk udeliť práva \"Dostupnosť\"."), + ("android_input_permission_tip2", "Prejdite na stránku nastavení systému, nájdite a vstúpte do [Stiahnuté služby], zapnite [RustDesk Input] službu."), + ("android_new_connection_tip", "Bola prijatá nová požiadavka na ovládanie vášho zariadenia."), + ("android_service_will_start_tip", "Zapnutie \"Zachytávanie obsahu obrazovky\" automaticky spistí službu, čo iným zariadeniam umožní požiadať o pripojenie k tomuto zariadeniu."), + ("android_stop_service_tip", "Zastavenie služby automaticky ukončí všetky naviazané spojenia."), + ("android_version_audio_tip", "Vaša verzia Androidu neumožňuje zaznamenávanie zvuku. Prejdite na verziu Android 10 alebo vyššiu."), + ("android_start_service_tip", "Ťuknutím na položku [Spustiť službu] alebo povolením povolenia [Snímanie obrazovky] spustite službu zdieľania obrazovky."), + ("android_permission_may_not_change_tip", "Oprávnenia pre vytvorené pripojenia možno zmeniť až po opätovnom pripojení."), + ("Account", "Účet"), + ("Overwrite", "Prepísať"), + ("This file exists, skip or overwrite this file?", "Preskočiť alebo prepísať existujúci súbor?"), + ("Quit", "Ukončiť"), + ("Help", "Nápoveda"), + ("Failed", "Nepodarilo sa"), + ("Succeeded", "Podarilo sa"), + ("Someone turns on privacy mode, exit", "Niekto zapne režim súkromia, ukončite ho"), + ("Unsupported", "Nepodporované"), + ("Peer denied", "Peer poprel"), + ("Please install plugins", "Nainštalujte si prosím pluginy"), + ("Peer exit", "Peer exit"), + ("Failed to turn off", "Nepodarilo sa vypnúť"), + ("Turned off", "Vypnutý"), + ("Language", "Jazyk"), + ("Keep RustDesk background service", "Ponechať službu RustDesk na pozadí"), + ("Ignore Battery Optimizations", "Ignorovať optimalizácie batérie"), + ("android_open_battery_optimizations_tip", "Ak chcete túto funkciu vypnúť, prejdite na ďalšiu stránku nastavení RustDesku, vyhľadajte a zadajte položku [Batéria], zrušte začiarknutie položky [Neobmedzené]."), + ("Start on boot", "Spustenie po štarte"), + ("Start the screen sharing service on boot, requires special permissions", "Spustenie služby zdieľania obrazovky pri štarte systému, vyžaduje špeciálne oprávnenia"), + ("Connection not allowed", "Spojenie nie je povolené"), + ("Legacy mode", "Režim Legacy"), + ("Map mode", "Režim mapovania"), + ("Translate mode", "Režim prekladania"), + ("Use permanent password", "Použitie trvalého hesla"), + ("Use both passwords", "Používanie oboch hesiel"), + ("Set permanent password", "Nastaviť trvalé heslo"), + ("Enable remote restart", "Povoliť vzdialený reštart"), + ("Restart remote device", "Reštartovať vzdialené zariadenie"), + ("Are you sure you want to restart", "Ste si istý, že chcete reštartovať"), + ("Restarting remote device", "Reštartovanie vzdialeného zariadenia"), + ("remote_restarting_tip", "Vzdialené zariadenie sa reštartuje, zatvorte toto okno a po chvíli sa znovu pripojte pomocou trvalého hesla."), + ("Copied", "Skopírované"), + ("Exit Fullscreen", "Ukončiť celú obrazovku"), + ("Fullscreen", "Celá obrazovka"), + ("Mobile Actions", "Mobilné akcie"), + ("Select Monitor", "Vyberte možnosť Monitor"), + ("Control Actions", "Kontrolné akcie"), + ("Display Settings", "Nastavenia displeja"), + ("Ratio", "Pomer"), + ("Image Quality", "Kvalita obrazu"), + ("Scroll Style", "Štýl posúvania"), + ("Show Toolbar", "Zobrazenie panela nástrojov"), + ("Hide Toolbar", "Skrytie panela nástrojov"), + ("Direct Connection", "Priame pripojenie"), + ("Relay Connection", "Reléové pripojenie"), + ("Secure Connection", "Zabezpečené pripojenie"), + ("Insecure Connection", "Nezabezpečené pripojenie"), + ("Scale original", "Pôvodná mierka"), + ("Scale adaptive", "Prispôsobivá mierka"), + ("General", "Všeobecné"), + ("Security", "Zabezpečenie"), + ("Theme", "Motív"), + ("Dark Theme", "Tmavý motív"), + ("Light Theme", "Svetlý motív"), + ("Dark", "Tmavý"), + ("Light", "Svetlý"), + ("Follow System", "Podľa systému"), + ("Enable hardware codec", "Povoliť hardwarový kodek"), + ("Unlock Security Settings", "Odomknúť nastavenie zabezpečenia"), + ("Enable audio", "Povoliť zvuk"), + ("Unlock Network Settings", "Odomknúť nastavenie siete"), + ("Server", "Server"), + ("Direct IP Access", "Priamy IP prístup"), + ("Proxy", "Proxy"), + ("Apply", "Použiť"), + ("Disconnect all devices?", "Odpojiť všetky zariadenia?"), + ("Clear", "Zmazať"), + ("Audio Input Device", "Vstupné zvukové zariadenie"), + ("Use IP Whitelisting", "Použiť IP whitelisting"), + ("Network", "Sieť"), + ("Pin Toolbar", "Pripnúť panel nástrojov"), + ("Unpin Toolbar", "Odpojiť panel nástrojov"), + ("Recording", "Nahrávanie"), + ("Directory", "Adresár"), + ("Automatically record incoming sessions", "Automaticky nahrávať prichádzajúce relácie"), + ("Automatically record outgoing sessions", ""), + ("Change", "Zmeniť"), + ("Start session recording", "Spustiť záznam relácie"), + ("Stop session recording", "Zastaviť záznam relácie"), + ("Enable recording session", "Povoliť nahrávanie relácie"), + ("Enable LAN discovery", "Povolenie zisťovania siete LAN"), + ("Deny LAN discovery", "Zakázať zisťovania siete LAN"), + ("Write a message", "Napísať správu"), + ("Prompt", "Výzva"), + ("Please wait for confirmation of UAC...", "Počkajte, prosím, na potvrdenie UAC..."), + ("elevated_foreground_window_tip", "Aktuálne okno vzdialenej plochy vyžaduje vyššie oprávnenia, takže dočasne nemôže používať myš a klávesnicu. Môžete požiadať vzdialeného používateľa, aby minimalizoval aktuálne okno, alebo kliknúť na tlačidlo povýšiť v okne správy pripojenia. Ak sa chcete vyhnúť tomuto problému, odporúčame nainštalovať softvér na vzdialené zariadenie."), + ("Disconnected", "Odpojené"), + ("Other", "Iné"), + ("Confirm before closing multiple tabs", "Potvrdiť pred zatvorením viacerých kariet"), + ("Keyboard Settings", "Nastavenia klávesnice"), + ("Full Access", "Úplný prístup"), + ("Screen Share", "Zdielanie obrazovky"), + ("ubuntu-21-04-required", "Wayland vyžaduje Ubuntu 21.04 alebo vyššiu verziu."), + ("wayland-requires-higher-linux-version", "Wayland vyžaduje vyššiu verziu linuxovej distribúcie. Skúste X11 desktop alebo zmeňte OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte obrazovku, ktorú chcete zdieľať (Ovládajte na strane partnera)."), + ("Show RustDesk", "Zobraziť RustDesk"), + ("This PC", "Tento počítač"), + ("or", "alebo"), + ("Elevate", "Zvýšiť"), + ("Zoom cursor", "Kurzor priblíženia"), + ("Accept sessions via password", "Prijímanie relácií pomocou hesla"), + ("Accept sessions via click", "Prijímanie relácií kliknutím"), + ("Accept sessions via both", "Prijímanie relácií prostredníctvom oboch"), + ("Please wait for the remote side to accept your session request...", "Počkajte, kým vzdialená strana prijme vašu žiadosť o reláciu..."), + ("One-time Password", "Jednorázové heslo"), + ("Use one-time password", "Použiť jednorázové heslo"), + ("One-time password length", "Dĺžka jednorázového hesla"), + ("Request access to your device", "Žiadosť o prístup k vášmu zariadeniu"), + ("Hide connection management window", "Skryť okno správy pripojenia"), + ("hide_cm_tip", "Skrývanie povoľte len vtedy, ak relácie prijímate pomocou hesla a používate trvalé heslo."), + ("wayland_experiment_tip", "Podpora Waylandu je v experimentálnej fáze, ak potrebujete bezobslužný prístup, použite X11."), + ("Right click to select tabs", "Výber karty kliknutím pravým tlačidlom myši"), + ("Skipped", "Vynechané"), + ("Add to address book", "Pridať do adresára"), + ("Group", "Skupina"), + ("Search", "Vyhľadávanie"), + ("Closed manually by web console", "Zatvorené ručne pomocou webovej konzoly"), + ("Local keyboard type", "Typ lokálnej klávesnice"), + ("Select local keyboard type", "Výber typu lokálnej klávesnice"), + ("software_render_tip", "Ak používate grafickú kartu Nvidia v systéme Linux a vzdialené okno sa po pripojení okamžite zatvorí, môže vám pomôcť prepnutie na ovládač Nouveau s otvoreným zdrojovým kódom a výber softvérového vykresľovania. Vyžaduje sa softvérový reštart."), + ("Always use software rendering", "Vždy použiť softvérové vykresľovanie"), + ("config_input", "Ak chcete ovládať vzdialenú plochu pomocou klávesnice, musíte udeliť oprávnenie RustDesk \"Sledovanie vstupu\"."), + ("config_microphone", "Ak chcete hovoriť na diaľku, musíte RustDesku udeliť povolenie \"Nahrávať zvuk\"."), + ("request_elevation_tip", "Môžete tiež požiadať o zvýšenie, ak je niekto na vzdialenej strane."), + ("Wait", "Počkajte"), + ("Elevation Error", "Chyba navýšenia"), + ("Ask the remote user for authentication", "Požiadať vzdialeného používateľa o overenie"), + ("Choose this if the remote account is administrator", "Túto možnosť vyberte, ak je účet vzdialeného správcu"), + ("Transmit the username and password of administrator", "Prenos používateľského mena a hesla správcu"), + ("still_click_uac_tip", "Stále sa vyžaduje, aby vzdialený používateľ klikol na tlačidlo OK v okne UAC spusteného programu RustDesk."), + ("Request Elevation", "Žiadosť o navýšenie"), + ("wait_accept_uac_tip", "Počkajte, kým vzdialený používateľ prijme dialógové okno UAC."), + ("Elevate successfully", "Úspešné navýšenie"), + ("uppercase", "velké písmená"), + ("lowercase", "malé písmená"), + ("digit", "číslice"), + ("special character", "špeciálny znak"), + ("length>=8", "dĺžka>=8"), + ("Weak", "Slabé"), + ("Medium", "Stredné"), + ("Strong", "Silné"), + ("Switch Sides", "Prepínanie strán"), + ("Please confirm if you want to share your desktop?", "Potvrďte, prosím, či chcete zdieľať svoju plochu?"), + ("Display", "Obrazovka"), + ("Default View Style", "Predvolený štýl zobrazenia"), + ("Default Scroll Style", "Predvolený štýl posúvania"), + ("Default Image Quality", "Predvolená kvalita obrazu"), + ("Default Codec", "Predvolený kodek"), + ("Bitrate", "Dátový tok"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Ďalšie predvolené možnosti"), + ("Voice call", "Hlasový hovor"), + ("Text chat", "Textový chat"), + ("Stop voice call", "Zastaviť hlasový hovor"), + ("relay_hint_tip", "Priame pripojenie nemusí byť možné, môžete sa pokúsiť pripojiť prostredníctvom presmerovacieho servera. Okrem toho, ak chcete pri prvom pokuse použiť presmerovací server, môžete k ID pridať príponu \"/r\" alebo vybrať možnosť \"Vždy sa pripájať cez bránu\" na karte posledných relácií, ak existuje."), + ("Reconnect", "Znovu pripojiť"), + ("Codec", "Kodek"), + ("Resolution", "Rozlíšenie"), + ("No transfers in progress", "Žiadne prebiehajúce presuny"), + ("Set one-time password length", "Nastaviť dĺžku jednorazového hesla"), + ("RDP Settings", "Nastavenia RDP"), + ("Sort by", "Usporiadať podľa"), + ("New Connection", "Nové pripojenie"), + ("Restore", "Obnoviť"), + ("Minimize", "Minimalizovať"), + ("Maximize", "Maximalizovať"), + ("Your Device", "Vaše zariadenie"), + ("empty_recent_tip", "Ups, žiadna nedávna relácia!\nČas naplánovať novú."), + ("empty_favorite_tip", "Ešte nemáte obľúbeného partnera?\nNájdite niekoho, s kým sa môžete spojiť, a pridajte si ho do obľúbených!"), + ("empty_lan_tip", "Ale nie, zdá sa, že sme zatiaľ neobjavili žiadnu protistranu."), + ("empty_address_book_tip", "Ach bože, zdá sa, že vo vašom adresári momentálne nie sú uvedení žiadni kolegovia."), + ("Empty Username", "Prázdne používateľské meno"), + ("Empty Password", "Prázdne heslo"), + ("Me", "Ja"), + ("identical_file_tip", "Tento súbor je identický so súborom partnera."), + ("show_monitors_tip", "Zobraziť monitory na paneli nástrojov"), + ("View Mode", "Režim zobrazenia"), + ("login_linux_tip", "Ak chcete povoliť reláciu Desktop X, musíte sa prihlásiť do vzdialeného konta Linuxu."), + ("verify_rustdesk_password_tip", "Overenie hesla RustDesk"), + ("remember_account_tip", "Zapamätať si tento účet"), + ("os_account_desk_tip", "Toto konto sa používa na prihlásenie do vzdialeného operačného systému a na povolenie relácie pracovnej plochy v režime headless."), + ("OS Account", "Účet operačného systému"), + ("another_user_login_title_tip", "Ďalší používateľ je už prihlásený"), + ("another_user_login_text_tip", "Odpojiť"), + ("xorg_not_found_title_tip", "Xorg nebol nájdený"), + ("xorg_not_found_text_tip", "Prosím, nainštalujte Xorg"), + ("no_desktop_title_tip", "Nie je k dispozícii žiadna plocha"), + ("no_desktop_text_tip", "Nainštalujte si prostredie GNOME"), + ("No need to elevate", "Navýšenie nie je potrebné"), + ("System Sound", "Systémový zvuk"), + ("Default", "Predvolené"), + ("New RDP", "Nové RDP"), + ("Fingerprint", "Odtlačok prsta"), + ("Copy Fingerprint", "Kopírovať odtlačok prsta"), + ("no fingerprints", "žiadne odtlačky prstov"), + ("Select a peer", "Výber partnera"), + ("Select peers", "Výber partnerov"), + ("Plugins", "Pluginy"), + ("Uninstall", "Odinštalovať"), + ("Update", "Aktualizovať"), + ("Enable", "Povoliť"), + ("Disable", "Zakázať"), + ("Options", "Možnosti"), + ("resolution_original_tip", "Pôvodné rozlíšenie"), + ("resolution_fit_local_tip", "Prispôsobiť miestne rozlíšenie"), + ("resolution_custom_tip", "Vlastné rozlíšenie"), + ("Collapse toolbar", "Zbaliť panel nástrojov"), + ("Accept and Elevate", "Prijať navýšenie"), + ("accept_and_elevate_btn_tooltip", "Prijmite pripojenie a zvýšte oprávnenia UAC."), + ("clipboard_wait_response_timeout_tip", "Čas na čakanie na kópiu odpovede uplynul."), + ("Incoming connection", "Prichádzajúce pripojenie"), + ("Outgoing connection", "Odchádzajúce pripojenie"), + ("Exit", "Ukončiť"), + ("Open", "Otvoriť"), + ("logout_tip", "Naozaj sa chcete odhlásiť?"), + ("Service", "Služba"), + ("Start", "Spustiť"), + ("Stop", "Zastaviť"), + ("exceed_max_devices", "Dosiahli ste maximálny počet spravovaných zariadení."), + ("Sync with recent sessions", "Synchronizovať s poslednými reláciami"), + ("Sort tags", "Zoradiť štítky"), + ("Open connection in new tab", "Otvoriť pripojenie v novej karte"), + ("Move tab to new window", "Presunúť kartu do nového okna"), + ("Can not be empty", "Nemôže byť prázdne"), + ("Already exists", "Už existuje"), + ("Change Password", "Zmeniť heslo"), + ("Refresh Password", "Obnoviť heslo"), + ("ID", "ID"), + ("Grid View", "Mriežka"), + ("List View", "Zoznam"), + ("Select", "Vybrať"), + ("Toggle Tags", "Prepnúť štítky"), + ("pull_ab_failed_tip", "Nepodarilo sa obnoviť adresár"), + ("push_ab_failed_tip", "Nepodarilo sa synchronizovať adresár so serverom"), + ("synced_peer_readded_tip", "Zariadenia, ktoré boli prítomné v posledných reláciách, budú synchronizované späť do adresára."), + ("Change Color", "Zmeniť farbu"), + ("Primary Color", "Hlavná farba"), + ("HSV Color", "HSV farba"), + ("Installation Successful!", "Inštalácia úspešná!"), + ("Installation failed!", "Inštalácia zlyhala!"), + ("Reverse mouse wheel", "Reverzné koliesko myši"), + ("{} sessions", "{} relácií"), + ("scam_title", "Možno ste boli oklamaní!"), + ("scam_text1", "Ak telefonujete s niekým, koho nepoznáte a komu nedôverujete a kto vás požiadal o použitie a spustenie aplikácie RustDesk, nepokračujte v hovore a okamžite zaveste."), + ("scam_text2", "Pravdepodobne ide o podvodníka, ktorý sa snaží ukradnúť vaše peniaze alebo iné súkromné informácie."), + ("Don't show again", "Nezobrazovať znova"), + ("I Agree", "Súhlasím"), + ("Decline", "Odmietnuť"), + ("Timeout in minutes", "Časový limit v minútach"), + ("auto_disconnect_option_tip", "Automatické ukončenie prichádzajúcich relácií, keď je používateľ nečinný"), + ("Connection failed due to inactivity", "Pripojenie zlyhalo z dôvodu nečinnosti"), + ("Check for software update on startup", "Kontrola aktualizácií softvéru pri spustení"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Aktualizujte RustDesk Server Pro na verziu {} alebo novšiu!"), + ("pull_group_failed_tip", "Nepodarilo sa obnoviť skupinu"), + ("Filter by intersection", "Filtrovať podľa križovatky"), + ("Remove wallpaper during incoming sessions", "Odstrániť tapetu počas prichádzajúcich relácií"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Obrazovka je odpojená, prepnite na prvú obrazovku."), + ("No displays", "Žiadne obrazovky"), + ("Open in new window", "Otvoriť v novom okne"), + ("Show displays as individual windows", "Zobraziť obrazovky ako jednotlivé okná"), + ("Use all my displays for the remote session", "Použiť všetky moje obrazovky pre vzdialenú reláciu"), + ("selinux_tip", "Na vašom zariadení je povolený SELinux, čo môže brániť správnemu spusteniu RustDesku ako spravovanej strany."), + ("Change view", "Zmeniť pohľad"), + ("Big tiles", "Veľké dlaždice"), + ("Small tiles", "Malé dlaždice"), + ("List", "Zoznam"), + ("Virtual display", "Virtuálny displej"), + ("Plug out all", "Odpojiť všetky"), + ("True color (4:4:4)", "Skutočná farba (4:4:4)"), + ("Enable blocking user input", "Povoliť blokovanie vstupu od používateľa"), + ("id_input_tip", "Môžete zadať ID, priamu IP adresu alebo doménu s portom (:).\nAk chcete získať prístup k zariadeniu na inom serveri, doplňte adresu servera (@?key=), napríklad,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAk chcete získať prístup k zariadeniu na verejnom serveri, zadajte \"@public\", kľúč nie je potrebný pre verejný server."), + ("privacy_mode_impl_mag_tip", "Režim 1"), + ("privacy_mode_impl_virtual_display_tip", "Režim 2"), + ("Enter privacy mode", "Vstup do režimu súkromia"), + ("Exit privacy mode", "Ukončiť režim súkromia"), + ("idd_not_support_under_win10_2004_tip", "Ovládač nepriameho zobrazenia nie je podporovaný. Vyžaduje sa systém Windows 10, verzia 2004 alebo novšia."), + ("input_source_1_tip", "Vstupný zdroj 1"), + ("input_source_2_tip", "Vstupný zdroj 2"), + ("Swap control-command key", "Vymeniť kláves ovládania a príkazu"), + ("swap-left-right-mouse", "Prehodiť ľavé a pravé tlačidlo myši"), + ("2FA code", "2FA kód"), + ("More", "Viac"), + ("enable-2fa-title", "Povoliť dvojfaktorové overenie"), + ("enable-2fa-desc", "Prosím, nastavte si svoj autentifikátor. Na svojom telefóne alebo počítači môžete použiť autentifikačnú aplikáciu, ako je Authy, Microsoft alebo Google Authenticator.\n\nNaskenujte QR kód pomocou svojej aplikácie a zadajte kód, ktorý aplikácia zobrazí, aby ste povolili dvojfaktorové overenie."), + ("wrong-2fa-code", "Kód sa nepodarilo overiť. Skontrolujte, či sú nastavenia kódu a miestneho času správne"), + ("enter-2fa-title", "Dvojfaktorové overenie"), + ("Email verification code must be 6 characters.", "Overovací kód e-mailu musí mať 6 znakov."), + ("2FA code must be 6 digits.", "Kód 2FA musí obsahovať 6 číslic."), + ("Multiple Windows sessions found", "Našlo sa viacero relácií systému Windows"), + ("Please select the session you want to connect to", "Vyberte reláciu, ku ktorej sa chcete pripojiť"), + ("powered_by_me", "Poháňané aplikáciou RustDesk"), + ("outgoing_only_desk_tip", "Toto je prispôsobené vydanie.\nMôžete sa pripojiť k iným zariadeniam, ale iné zariadenia sa k vášmu zariadeniu pripojiť nemôžu."), + ("preset_password_warning", "Toto prispôsobené vydanie sa dodáva s prednastaveným heslom. Každý, kto pozná toto heslo, môže získať plnú kontrolu nad vaším zariadením. Ak ste to neočakávali, okamžite softvér odinštalujte."), + ("Security Alert", "Bezpečnostné upozornenie"), + ("My address book", "Môj adresár"), + ("Personal", "Osobné"), + ("Owner", "Vlastník"), + ("Set shared password", "Nastaviť zdieľané heslo"), + ("Exist in", "Existovať v"), + ("Read-only", "len na čítanie"), + ("Read/Write", "Režim čítania/zápisu"), + ("Full Control", "Úplná kontrola"), + ("share_warning_tip", "Vyššie uvedené polia sú zdieľané a viditeľné pre ostatných."), + ("Everyone", "Každý"), + ("ab_web_console_tip", "Viac na webovej konzole"), + ("allow-only-conn-window-open-tip", "Povoliť pripojenie iba vtedy, ak je otvorené okno aplikácie RustDesk"), + ("no_need_privacy_mode_no_physical_displays_tip", "Žiadne fyzické displeje, nie je potrebné používať režim ochrany osobných údajov."), + ("Follow remote cursor", "Nasledovať vzdialený kurzor"), + ("Follow remote window focus", "Nasledovať vzdialené zameranie okna"), + ("default_proxy_tip", "Predvolený protokol a port sú Socks5 a 1080"), + ("no_audio_input_device_tip", "Nenašlo sa žiadne vstupné zvukové zariadenie."), + ("Incoming", "Prichádzajúci"), + ("Outgoing", "Odchádzajúci"), + ("Clear Wayland screen selection", "Vyčistiť výber obrazovky Wayland"), + ("clear_Wayland_screen_selection_tip", "Po vymazaní výberu obrazovky môžete znova vybrať obrazovku, ktorú chcete zdieľať."), + ("confirm_clear_Wayland_screen_selection_tip", "Určite ste si istý, že chcete vyčistiť výber obrazovky Wayland?"), + ("android_new_voice_call_tip", "Bola prijatá nová žiadosť o hlasový hovor. Ak ho prijmete, zvuk sa prepne na hlasovú komunikáciu."), + ("texture_render_tip", "Použiť vykresľovanie textúr, aby boli obrázky hladšie."), + ("Use texture rendering", "Použiť vykresľovanie textúr"), + ("Floating window", "Plávajúce okno"), + ("floating_window_tip", "Pomáha udržiavať službu RustDesk na pozadí"), + ("Keep screen on", "Ponechať obrazovku zapnutú"), + ("Never", "Nikdy"), + ("During controlled", "Počas kontrolovaného"), + ("During service is on", "Počas služby je v prevádzke"), + ("Capture screen using DirectX", "Snímanie obrazovky pomocou DirectX"), + ("Back", "Naspäť"), + ("Apps", "Aplikácie"), + ("Volume up", "Zvýšiť hlasitosť"), + ("Volume down", "Znížiť hlasitosť"), + ("Power", "Napájanie"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Ak túto funkciu povolíte, kód 2FA môžete dostať od svojho bota. Môže fungovať aj ako upozornenie na pripojenie."), + ("enable-bot-desc", "1, Otvorte chat s @BotFather.\n2, Odošlite príkaz \"/newbot\". Po dokončení tohto kroku dostanete token.\n3, Spustite chat s novo vytvoreným botom. Odošlite správu začínajúcu lomítkom vpred (\"/\"), napríklad \"/hello\", aby ste ho aktivovali.\n"), + ("cancel-2fa-confirm-tip", "Ste si istí, že chcete zrušiť službu 2FA?"), + ("cancel-bot-confirm-tip", "Ste si istí, že chcete zrušiť bota Telegramu?"), + ("About RustDesk", "O RustDesk"), + ("Send clipboard keystrokes", "Odoslať stlačenia klávesov zo schránky"), + ("network_error_tip", "Skontrolujte svoje sieťové pripojenie a potom kliknite na tlačidlo Opakovať."), + ("Unlock with PIN", "Odomknutie pomocou PIN kódu"), + ("Requires at least {} characters", "Vyžaduje aspoň {} znakov"), + ("Wrong PIN", "Nesprávny PIN kód"), + ("Set PIN", "Nastavenie PIN kódu"), + ("Enable trusted devices", "Povolenie dôveryhodných zariadení"), + ("Manage trusted devices", "Správa dôveryhodných zariadení"), + ("Platform", "Platforma"), + ("Days remaining", "Zostávajúce dni"), + ("enable-trusted-devices-tip", "Vynechanie overovania 2FA na dôveryhodných zariadeniach"), + ("Parent directory", "Rodičovský adresár"), + ("Resume", "Obnoviť"), + ("Invalid file name", "Nesprávny názov súboru"), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Aktualizujte klienta RustDesk na verziu {} alebo novšiu na vzdialenej strane!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Zobraziť kameru"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Pokračovať s {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/sl.rs b/vendor/rustdesk/src/lang/sl.rs new file mode 100755 index 0000000..3f35dea --- /dev/null +++ b/vendor/rustdesk/src/lang/sl.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stanje"), + ("Your Desktop", "Vaše namizje"), + ("desk_tip", "S spodnjim IDjem in geslom omogočite oddaljeni nadzor vašega računalnika"), + ("Password", "Geslo"), + ("Ready", "Pripravljen"), + ("Established", "Povezava vzpostavljena"), + ("connecting_status", "Vzpostavljanje povezave z omrežjem RustDesk..."), + ("Enable service", "Omogoči storitev"), + ("Start service", "Zaženi storitev"), + ("Service is running", "Storitev se izvaja"), + ("Service is not running", "Storitev se ne izvaja"), + ("not_ready_status", "Ni pripravljeno, preverite vašo mrežno povezavo"), + ("Control Remote Desktop", "Nadzoruj oddaljeno namizje"), + ("Transfer file", "Prenos datotek"), + ("Connect", "Poveži"), + ("Recent sessions", "Nedavne seje"), + ("Address book", "Adresar"), + ("Confirmation", "Potrditev"), + ("TCP tunneling", "TCP tuneliranje"), + ("Remove", "Odstrani"), + ("Refresh random password", "Osveži naključno geslo"), + ("Set your own password", "Nastavi lastno geslo"), + ("Enable keyboard/mouse", "Omogoči tipkovnico in miško"), + ("Enable clipboard", "Omogoči odložišče"), + ("Enable file transfer", "Omogoči prenos datotek"), + ("Enable TCP tunneling", "Omogoči TCP tuneliranje"), + ("IP Whitelisting", "Omogoči seznam dovoljenih IPjev"), + ("ID/Relay Server", "Strežnik za ID/posredovanje"), + ("Import server config", "Uvozi nastavitve strežnika"), + ("Export Server Config", "Izvozi nastavitve strežnika"), + ("Import server configuration successfully", "Nastavitve strežnika uspešno uvožene"), + ("Export server configuration successfully", "Nastavitve strežnika uspešno izvožene"), + ("Invalid server configuration", "Neveljavne nastavitve strežnika"), + ("Clipboard is empty", "Odložišče je prazno"), + ("Stop service", "Ustavi storitev"), + ("Change ID", "Spremeni ID"), + ("Your new ID", "Vaš nov ID"), + ("length %min% to %max%", "dolžina od %min% do %max%"), + ("starts with a letter", "začne se s črko"), + ("allowed characters", "dovoljeni znaki"), + ("id_change_tip", "Dovoljeni znaki so a-z, A-Z (brez šumnikov), 0-9, - (dash) in _. Prvi znak mora biti črka, dolžina od 6 do 16 znakov."), + ("Website", "Spletna stran"), + ("About", "O programu"), + ("Slogan_tip", ""), + ("Privacy Statement", "Izjava o zasebnosti"), + ("Mute", "Izklopi zvok"), + ("Build Date", "Datum graditve"), + ("Version", "Različica"), + ("Home", "Začetek"), + ("Audio Input", "Avdio vhod"), + ("Enhancements", "Izboljšave"), + ("Hardware Codec", "Strojni kodek"), + ("Adaptive bitrate", "Prilagodljiva bitna hitrost"), + ("ID Server", "ID strežnik"), + ("Relay Server", "Posredniški strežnik"), + ("API Server", "API strežnik"), + ("invalid_http", "mora se začeti s http:// ali https://"), + ("Invalid IP", "Neveljaven IP"), + ("Invalid format", "Neveljavna oblika"), + ("server_not_support", "Strežnik še ne podpira"), + ("Not available", "Ni na voljo"), + ("Too frequent", "Prepogosto"), + ("Cancel", "Prekliči"), + ("Skip", "Izpusti"), + ("Close", "Zapri"), + ("Retry", "Ponovi"), + ("OK", "V redu"), + ("Password Required", "Potrebno je geslo"), + ("Please enter your password", "Vnesite vaše geslo"), + ("Remember password", "Zapomni si geslo"), + ("Wrong Password", "Napačno geslo"), + ("Do you want to enter again?", "Želite znova vnesti?"), + ("Connection Error", "Napaka pri povezavi"), + ("Error", "Napaka"), + ("Reset by the peer", "Povezava prekinjena"), + ("Connecting...", "Povezovanje..."), + ("Connection in progress. Please wait.", "Vzpostavljanje povezave, prosim počakajte."), + ("Please try 1 minute later", "Poizkusite čez 1 minuto"), + ("Login Error", "Napaka pri prijavi"), + ("Successful", "Uspešno"), + ("Connected, waiting for image...", "Povezava vzpostavljena, čakam na sliko..."), + ("Name", "Ime"), + ("Type", "Vrsta"), + ("Modified", "Čas spremembe"), + ("Size", "Velikost"), + ("Show Hidden Files", "Prikaži skrite datoteke"), + ("Receive", "Prejmi"), + ("Send", "Pošlji"), + ("Refresh File", "Osveži datoteko"), + ("Local", "Lokalno"), + ("Remote", "Oddaljeno"), + ("Remote Computer", "Oddaljeni računalnik"), + ("Local Computer", "Lokalni računalnik"), + ("Confirm Delete", "Potrdi izbris"), + ("Delete", "Izbriši"), + ("Properties", "Lastnosti"), + ("Multi Select", "Večkratna izbira"), + ("Select All", "Izberi vse"), + ("Unselect All", "Počisti vse"), + ("Empty Directory", "Prazen imenik"), + ("Not an empty directory", "Imenik ni prazen"), + ("Are you sure you want to delete this file?", "Ali res želite izbrisati to datoteko?"), + ("Are you sure you want to delete this empty directory?", "Ali res želite izbrisati to prazno mapo?"), + ("Are you sure you want to delete the file of this directory?", "Ali res želite datoteko iz mape?"), + ("Do this for all conflicts", "Naredi to za vse"), + ("This is irreversible!", "Tega dejanja ni mogoče razveljaviti!"), + ("Deleting", "Brisanje"), + ("files", "datoteke"), + ("Waiting", "Čakanje"), + ("Finished", "Opravljeno"), + ("Speed", "Hitrost"), + ("Custom Image Quality", "Kakovost slike po meri"), + ("Privacy mode", "Zasebni način"), + ("Block user input", "Onemogoči uporabnikov vnos"), + ("Unblock user input", "Omogoči uporabnikov vnos"), + ("Adjust Window", "Prilagodi okno"), + ("Original", "Originalno"), + ("Shrink", "Skrči"), + ("Stretch", "Raztegni"), + ("Scrollbar", "Drsenje z drsniki"), + ("ScrollAuto", "Samodejno drsenje"), + ("Good image quality", "Visoka kakovost slike"), + ("Balanced", "Uravnoteženo"), + ("Optimize reaction time", "Optimiraj odzivni čas"), + ("Custom", "Po meri"), + ("Show remote cursor", "Prikaži oddaljeni kazalec miške"), + ("Show quality monitor", "Prikaži nadzornik kakovosti"), + ("Disable clipboard", "Onemogoči odložišče"), + ("Lock after session end", "Zakleni ob koncu seje"), + ("Insert Ctrl + Alt + Del", "Vstavi Ctrl + Alt + Del"), + ("Insert Lock", "Zakleni oddaljeni računalnik"), + ("Refresh", "Osveži"), + ("ID does not exist", "ID ne obstaja"), + ("Failed to connect to rendezvous server", "Ni se bilo mogoče povezati na povezovalni strežnik"), + ("Please try later", "Poizkusite znova kasneje"), + ("Remote desktop is offline", "Oddaljeno namizje ni dosegljivo"), + ("Key mismatch", "Ključ ni ustrezen"), + ("Timeout", "Časovna omejitev"), + ("Failed to connect to relay server", "Ni se bilo mogoče povezati na posredniški strežnik"), + ("Failed to connect via rendezvous server", "Ni se bilo mogoče povezati preko povezovalnega strežnika"), + ("Failed to connect via relay server", "Ni se bilo mogoče povezati preko posredniškega strežnika"), + ("Failed to make direct connection to remote desktop", "Ni bilo mogoče vzpostaviti neposredne povezave z oddaljenim namizjem"), + ("Set Password", "Nastavi geslo"), + ("OS Password", "Geslo operacijskega sistema"), + ("install_tip", "Zaradi nadzora uporabniškega računa, RustDesk v nekaterih primerih na oddaljeni strani ne deluje pravilno. Temu se lahko izognete z namestitvijo."), + ("Click to upgrade", "Klikni za nadgradnjo"), + ("Configure", "Nastavi"), + ("config_acc", "Za oddaljeni nadzor namizja morate RustDesku dodeliti pravico za dostopnost"), + ("config_screen", "Za oddaljeni dostop do namizja morate RustDesku dodeliti pravico snemanje zaslona"), + ("Installing ...", "Nameščanje..."), + ("Install", "Namesti"), + ("Installation", "Namestitev"), + ("Installation Path", "Pot za namestitev"), + ("Create start menu shortcuts", "Ustvari bližnjice v meniju Začetek"), + ("Create desktop icon", "Ustvari ikono na namizju"), + ("agreement_tip", "Z namestitvijo se strinjate z licenčno pogodbo"), + ("Accept and Install", "Sprejmi in namesti"), + ("End-user license agreement", "Licenčna pogodba za končnega uporabnika"), + ("Generating ...", "Ustvarjanje ..."), + ("Your installation is lower version.", "Vaša namestitev je starejša"), + ("not_close_tcp_tip", "Med uporabo tunela ne zaprite tega okna"), + ("Listening ...", "Poslušam ..."), + ("Remote Host", "Oddaljeni gostitelj"), + ("Remote Port", "Oddaljena vrata"), + ("Action", "Dejanje"), + ("Add", "Dodaj"), + ("Local Port", "Lokalna vrata"), + ("Local Address", "Lokalni naslov"), + ("Change Local Port", "Spremeni lokalna vrata"), + ("setup_server_tip", "Za hitrejšo povezavo uporabite lasten strežnik"), + ("Too short, at least 6 characters.", "Prekratek, mora biti najmanj 6 znakov."), + ("The confirmation is not identical.", "Potrditev ni enaka."), + ("Permissions", "Dovoljenja"), + ("Accept", "Sprejmi"), + ("Dismiss", "Opusti"), + ("Disconnect", "Prekini povezavo"), + ("Enable file copy and paste", "Dovoli kopiranje in lepljenje datotek"), + ("Connected", "Povezan"), + ("Direct and encrypted connection", "Neposredna šifrirana povezava"), + ("Relayed and encrypted connection", "Posredovana šifrirana povezava"), + ("Direct and unencrypted connection", "Neposredna nešifrirana povezava"), + ("Relayed and unencrypted connection", "Posredovana šifrirana povezava"), + ("Enter Remote ID", "Vnesi oddaljeni ID"), + ("Enter your password", "Vnesi geslo"), + ("Logging in...", "Prijavljanje..."), + ("Enable RDP session sharing", "Omogoči deljenje RDP seje"), + ("Auto Login", "Samodejna prijava"), + ("Enable direct IP access", "Omogoči neposredni dostop preko IP naslova"), + ("Rename", "Preimenuj"), + ("Space", "Prazno"), + ("Create desktop shortcut", "Ustvari bližnjico na namizju"), + ("Change Path", "Spremeni pot"), + ("Create Folder", "Ustvari mapo"), + ("Please enter the folder name", "Vnesite ime mape"), + ("Fix it", "Popravi"), + ("Warning", "Opozorilo"), + ("Login screen using Wayland is not supported", "Prijava z Waylandom ni podprta"), + ("Reboot required", "Potreben je ponovni zagon"), + ("Unsupported display server", "Nepodprt zaslonski strežnik"), + ("x11 expected", "Pričakovan X11"), + ("Port", "Vrata"), + ("Settings", "Nastavitve"), + ("Username", "Uporabniško ime"), + ("Invalid port", "Neveljavno geslo"), + ("Closed manually by the peer", "Povezavo ročno prekinil odjemalec"), + ("Enable remote configuration modification", "Omogoči oddaljeno spreminjanje nastavitev"), + ("Run without install", "Zaženi brez namestitve"), + ("Connect via relay", "Poveži preko posrednika"), + ("Always connect via relay", "Vedno poveži preko posrednika"), + ("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"), + ("Login", "Prijavi"), + ("Verify", "Preveri"), + ("Remember me", "Zapomni si me"), + ("Trust this device", "Zaupaj tej napravi"), + ("Verification code", "Koda za preverjanje"), + ("verification_tip", "Kodo za preverjanje prejmete na registrirani e-poštni naslov"), + ("Logout", "Odjavi"), + ("Tags", "Oznake"), + ("Search ID", "Išči ID"), + ("whitelist_sep", "Naslovi ločeni z vejico, podpičjem, presledkom ali novo vrstico"), + ("Add ID", "Dodaj ID"), + ("Add Tag", "Dodaj oznako"), + ("Unselect all tags", "Odznači vse oznake"), + ("Network error", "Omrežna napaka"), + ("Username missed", "Up. ime izpuščeno"), + ("Password missed", "Geslo izpuščeno"), + ("Wrong credentials", "Napačne poverilnice"), + ("The verification code is incorrect or has expired", "Koda za preverjanje je napačna, ali pa je potekla"), + ("Edit Tag", "Uredi oznako"), + ("Forget Password", "Pozabi geslo"), + ("Favorites", "Priljubljene"), + ("Add to Favorites", "Dodaj med priljubljene"), + ("Remove from Favorites", "Odstrani iz priljubljenih"), + ("Empty", "Prazno"), + ("Invalid folder name", "Napačno ime mape"), + ("Socks5 Proxy", "Socks5 posredniški strežnik"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) posredniški strežnik"), + ("Discovered", "Odkriti"), + ("install_daemon_tip", "Za samodejni zagon ob vklopu računalnika je potrebno dodati sistemsko storitev"), + ("Remote ID", "Oddaljeni ID"), + ("Paste", "Prilepi"), + ("Paste here?", "Prilepi tu?"), + ("Are you sure to close the connection?", "Ali želite prekiniti povezavo?"), + ("Download new version", "Prenesi novo različico"), + ("Touch mode", "Način dotika"), + ("Mouse mode", "Način miške"), + ("One-Finger Tap", "Tap z enim prstom"), + ("Left Mouse", "Leva tipka miške"), + ("One-Long Tap", "Dolg tap z enim prstom"), + ("Two-Finger Tap", "Tap z dvema prstoma"), + ("Right Mouse", "Desna tipka miške"), + ("One-Finger Move", "Premik z enim prstom"), + ("Double Tap & Move", "Dvojni tap in premik"), + ("Mouse Drag", "Vlečenje z miško"), + ("Three-Finger vertically", "Triprstno navpično"), + ("Mouse Wheel", "Miškino kolesce"), + ("Two-Finger Move", "Premik z dvema prstoma"), + ("Canvas Move", "Premik platna"), + ("Pinch to Zoom", "Povečava s približevanjem prstov"), + ("Canvas Zoom", "Povečava platna"), + ("Reset canvas", "Ponastavi platno"), + ("No permission of file transfer", "Ni pravic za prenos datotek"), + ("Note", "Opomba"), + ("Connection", "Povezava"), + ("Share screen", "Deli zaslon"), + ("Chat", "Pogovor"), + ("Total", "Skupaj"), + ("items", "elementi"), + ("Selected", "Izbrano"), + ("Screen Capture", "Zajem zaslona"), + ("Input Control", "Nadzor vnosa"), + ("Audio Capture", "Zajem zvoka"), + ("Do you accept?", "Ali sprejmete?"), + ("Open System Setting", "Odpri sistemske nastavitve"), + ("How to get Android input permission?", "Kako pridobiti dovoljenje za vnos na Androidu?"), + ("android_input_permission_tip1", "Za oddaljeni nadzor vaše naprave Android, je potrebno RustDesku dodeliti pravico za dostopnost."), + ("android_input_permission_tip2", "Pojdite v sistemske nastavitve, poiščite »Nameščene storitve« in vklopite storitev »RustDesk Input«."), + ("android_new_connection_tip", "Prejeta je bila zahteva za oddaljeni nadzor vaše naprave."), + ("android_service_will_start_tip", "Z vklopom zajema zaslona se bo samodejno zagnala storitev, ki omogoča da oddaljene naprave pošljejo zahtevo za povezavo na vašo napravo."), + ("android_stop_service_tip", "Z zaustavitvijo storitve bodo samodejno prekinjene vse oddaljene povezave."), + ("android_version_audio_tip", "Trenutna različica Androida ne omogoča zajema zvoka. Za zajem zvoka nadgradite na Android 10 ali novejši."), + ("android_start_service_tip", "Tapnite [Zaženi storitev] ali pa omogočite pravico [Zajemanje zaslona] za zagon storitve deljenja zaslona."), + ("android_permission_may_not_change_tip", "Pravic za že vzpostavljene povezave ne morete spremeniti brez ponovne vzpostavitve povezave."), + ("Account", "Račun"), + ("Overwrite", "Prepiši"), + ("This file exists, skip or overwrite this file?", "Datoteka obstaja, izpusti ali prepiši?"), + ("Quit", "Izhod"), + ("Help", "Pomoč"), + ("Failed", "Ni uspelo"), + ("Succeeded", "Uspelo"), + ("Someone turns on privacy mode, exit", "Vklopljen je zasebni način, izhod"), + ("Unsupported", "Ni podprto"), + ("Peer denied", "Odjemalec zavrnil"), + ("Please install plugins", "Namestite vključke"), + ("Peer exit", "Odjemalec se je zaprl"), + ("Failed to turn off", "Ni bilo mogoče izklopiti"), + ("Turned off", "Izklopljeno"), + ("Language", "Jezik"), + ("Keep RustDesk background service", "Ohrani RustDeskovo storitev v ozadju"), + ("Ignore Battery Optimizations", "Prezri optimizacije baterije"), + ("android_open_battery_optimizations_tip", "Če želite izklopiti to možnost, pojdite v nastavitve aplikacije RustDesk, poiščite »Baterija« in izklopite »Neomejeno«"), + ("Start on boot", "Zaženi ob vklopu"), + ("Start the screen sharing service on boot, requires special permissions", "Zaženi storitev deljenja zaslona ob vklopu, zahteva posebna dovoljenja"), + ("Connection not allowed", "Povezava ni dovoljena"), + ("Legacy mode", "Stari način"), + ("Map mode", "Način preslikave"), + ("Translate mode", "Način prevajanja"), + ("Use permanent password", "Uporabi stalno geslo"), + ("Use both passwords", "Uporabi obe gesli"), + ("Set permanent password", "Nastavi stalno geslo"), + ("Enable remote restart", "Omogoči oddaljeni ponovni zagon"), + ("Restart remote device", "Znova zaženi oddaljeno napravo"), + ("Are you sure you want to restart", "Ali ste prepričani, da želite znova zagnati"), + ("Restarting remote device", "Ponovni zagon oddaljene naprave"), + ("remote_restarting_tip", "Oddaljena naprava se znova zaganja, prosim zaprite to sporočilo in se čez nekaj časa povežite s stalnim geslom."), + ("Copied", "Kopirano"), + ("Exit Fullscreen", "Izhod iz celozaslonskega načina"), + ("Fullscreen", "Celozaslonski način"), + ("Mobile Actions", "Dejanja za prenosne naprave"), + ("Select Monitor", "Izberite zaslon"), + ("Control Actions", "Dejanja za nadzor"), + ("Display Settings", "Nastavitve zaslona"), + ("Ratio", "Razmerje"), + ("Image Quality", "Kakovost slike"), + ("Scroll Style", "Način drsenja"), + ("Show Toolbar", "Prikaži orodno vrstico"), + ("Hide Toolbar", "Skrij orodno vrstico"), + ("Direct Connection", "Neposredna povezava"), + ("Relay Connection", "Posredovana povezava"), + ("Secure Connection", "Zavarovana povezava"), + ("Insecure Connection", "Nezavarovana povezava"), + ("Scale original", "Originalna velikost"), + ("Scale adaptive", "Prilagojena velikost"), + ("General", "Splošno"), + ("Security", "Varnost"), + ("Theme", "Tema"), + ("Dark Theme", "Temna tema"), + ("Light Theme", "Svetla tema"), + ("Dark", "Temna"), + ("Light", "Svetla"), + ("Follow System", "Sistemska"), + ("Enable hardware codec", "Omogoči strojno pospeševanje"), + ("Unlock Security Settings", "Odkleni varnostne nastavitve"), + ("Enable audio", "Omogoči zvok"), + ("Unlock Network Settings", "Odkleni mrežne nastavitve"), + ("Server", "Strežnik"), + ("Direct IP Access", "Neposredni dostop preko IPja"), + ("Proxy", "Posredniški strežnik"), + ("Apply", "Uveljavi"), + ("Disconnect all devices?", "Odklopi vse naprave?"), + ("Clear", "Počisti"), + ("Audio Input Device", "Vhodna naprava za zvok"), + ("Use IP Whitelisting", "Omogoči seznam dovoljenih IP naslovov"), + ("Network", "Mreža"), + ("Pin Toolbar", "Pripni orodno vrstico"), + ("Unpin Toolbar", "Odpni orodno vrstico"), + ("Recording", "Snemanje"), + ("Directory", "Imenik"), + ("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"), + ("Automatically record outgoing sessions", "Samodejno snemaj odhodne seje"), + ("Change", "Spremeni"), + ("Start session recording", "Začni snemanje seje"), + ("Stop session recording", "Ustavi snemanje seje"), + ("Enable recording session", "Omogoči snemanje seje"), + ("Enable LAN discovery", "Omogoči odkrivanje lokalnega omrežja"), + ("Deny LAN discovery", "Onemogoči odkrivanje lokalnega omrežja"), + ("Write a message", "Napiši spoorčilo"), + ("Prompt", "Poziv"), + ("Please wait for confirmation of UAC...", "Počakajte za potrditev nadzora uporabniškega računa"), + ("elevated_foreground_window_tip", "Trenutno aktivno okno na oddaljenem računalniku zahteva višje pravice za upravljanje. Oddaljenega uporabnika lahko prosite, da okno minimizira, ali pa kliknite gumb za povzdig pravic v oknu za upravljanje povezave. Če se želite izogniti temu problemu, na oddaljenem računalniku RustDesk namestite."), + ("Disconnected", "Brez povezave"), + ("Other", "Drugo"), + ("Confirm before closing multiple tabs", "Zahtevajte potrditev pred zapiranjem večih zavihkov"), + ("Keyboard Settings", "Nastavitve tipkovnice"), + ("Full Access", "Poln dostop"), + ("Screen Share", "Deljenje zaslona"), + ("ubuntu-21-04-required", "Wayland zahteva Ubuntu 21.04 ali novejši"), + ("wayland-requires-higher-linux-version", "Zahtevana je novejša različica Waylanda. Posodobite vašo distribucijo ali pa uporabite X11."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Pogled"), + ("Please Select the screen to be shared(Operate on the peer side).", "Izberite zaslon za delitev (na oddaljeni strani)."), + ("Show RustDesk", "Prikaži RustDesk"), + ("This PC", "Ta računalnik"), + ("or", "ali"), + ("Elevate", "Povzdig pravic"), + ("Zoom cursor", "Prilagodi velikost miškinega kazalca"), + ("Accept sessions via password", "Sprejmi seje z geslom"), + ("Accept sessions via click", "Sprejmi seje s potrditvijo"), + ("Accept sessions via both", "Sprejmi seje z geslom ali potrditvijo"), + ("Please wait for the remote side to accept your session request...", "Počakajte, da oddaljeni računalnik sprejme povezavo..."), + ("One-time Password", "Enkratno geslo"), + ("Use one-time password", "Uporabi enkratno geslo"), + ("One-time password length", "Dolžina enkratnega gesla"), + ("Request access to your device", "Zahtevaj dostop do svoje naprave"), + ("Hide connection management window", "Skrij okno za upravljanje povezave"), + ("hide_cm_tip", "Dovoli skrivanje samo pri sprejemanju sej z geslom"), + ("wayland_experiment_tip", "Podpora za Wayland je v preizkusni fazi. Uporabite X11, če rabite nespremljan dostop."), + ("Right click to select tabs", "Desno-kliknite za izbiro zavihkov"), + ("Skipped", "Izpuščeno"), + ("Add to address book", "Dodaj v adresar"), + ("Group", "Skupina"), + ("Search", "Iskanje"), + ("Closed manually by web console", "Ročno zaprto iz spletne konzole"), + ("Local keyboard type", "Lokalna vrsta tipkovnice"), + ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), + ("software_render_tip", "Če na Linuxu uporabljate Nvidino grafično kartico in se oddaljeno okno zapre takoj po vzpostavitvi povezave, lahko pomaga preklop na odprtokodni gonilnik Nouveau in uporaba programskega upodabljanja. Potreben je ponovni zagon programa."), + ("Always use software rendering", "Vedno uporabi programsko upodabljanje"), + ("config_input", "RustDesk potrebuje pravico »Nadzor vnosa« za nadzor oddaljenega namizja s tipkovnico."), + ("config_microphone", "RustDesk potrebuje pravico »Snemanje zvoka« za zajemanje zvoka."), + ("request_elevation_tip", "Lahko tudi zaprosite za dvig pravic, če je kdo na oddaljeni strani."), + ("Wait", "Čakaj"), + ("Elevation Error", "Napaka pri povzdigovanju"), + ("Ask the remote user for authentication", "Vprašaj oddaljenega uporabnika za prijavo"), + ("Choose this if the remote account is administrator", "Izberite to, če ima oddaljeni uporabnik skrbniške pravice"), + ("Transmit the username and password of administrator", "Vnesite poverilnice za skrbnika"), + ("still_click_uac_tip", "Oddaljeni uporabnik mora klikniti »Da« v oknu za nadzor uporabniškega računa."), + ("Request Elevation", "Zahtevaj povzdig pravic"), + ("wait_accept_uac_tip", "Počakajte na potrditev oddaljenega uporabnika v oknu za nadzor uporabniškega računa."), + ("Elevate successfully", "Povzdig pravic uspešen"), + ("uppercase", "velike črke"), + ("lowercase", "male črke"), + ("digit", "številke"), + ("special character", "posebni znaki"), + ("length>=8", "dolžina>=8"), + ("Weak", "Šibko"), + ("Medium", "Srednje"), + ("Strong", "Močno"), + ("Switch Sides", "Zamenjaj strani"), + ("Please confirm if you want to share your desktop?", "Potrdite, če želite deliti vaše namizje"), + ("Display", "Zaslon"), + ("Default View Style", "Privzeti način prikaza"), + ("Default Scroll Style", "Privzeti način drsenja"), + ("Default Image Quality", "Privzeta kakovost slike"), + ("Default Codec", "Privzeti kodek"), + ("Bitrate", "Bitna hitrost"), + ("FPS", "Sličice/sekundo"), + ("Auto", "Samodejno"), + ("Other Default Options", "Ostale privzete možnosti"), + ("Voice call", "Glasovni klic"), + ("Text chat", "Besedilni klepet"), + ("Stop voice call", "Prekini glasovni klic"), + ("relay_hint_tip", "Morda neposredna povezava ni možna; lahko se poizkusite povezati preko posrednika. Če želite uporabiti posrednika ob prvem poizkusu vzpotavljanja povezave, lahko na konec IDja dodate »/r«, ali pa izberete možnost »Vedno poveži preko posrednika« v kartici nedavnih sej, če le-ta obstja."), + ("Reconnect", "Ponovna povezava"), + ("Codec", "Kodek"), + ("Resolution", "Ločljivost"), + ("No transfers in progress", "Trenutno ni prenosov"), + ("Set one-time password length", "Nastavi dolžino enkratnega gesla"), + ("RDP Settings", "Nastavitve za RDP"), + ("Sort by", "Razvrsti po"), + ("New Connection", "Nova povezava"), + ("Restore", "Obnovi"), + ("Minimize", "Minimiziraj"), + ("Maximize", "Maksimiziraj"), + ("Your Device", "Vaša naprava"), + ("empty_recent_tip", "Oops, ni nedavnih sej.\nPripravite novo."), + ("empty_favorite_tip", "Nimate še priljubljenih partnerjev?\nVzpostavite povezavo, in jo dodajte med priljubljene."), + ("empty_lan_tip", "Nismo našli še nobenih partnerjev."), + ("empty_address_book_tip", "Vaš adresar je prazen."), + ("Empty Username", "Prazno uporabniško ime"), + ("Empty Password", "Prazno geslo"), + ("Me", "Jaz"), + ("identical_file_tip", "Datoteka je enaka partnerjevi"), + ("show_monitors_tip", "Prikaži monitorje v orodni vrstici"), + ("View Mode", "Način prikazovanja"), + ("login_linux_tip", "Prijaviti se morate v oddaljeni Linux račun in omogočiti namizno sejo X."), + ("verify_rustdesk_password_tip", "Preveri geslo za RustDesk"), + ("remember_account_tip", "Zapomni si ta račun"), + ("os_account_desk_tip", "Ta račun se uporabi za prijavo v oddaljeni sistem in omogči namizno sejo v napravi brez monitorja."), + ("OS Account", "Račun operacijskega sistema"), + ("another_user_login_title_tip", "Prijavljen je že drug uporabnik"), + ("another_user_login_text_tip", "Prekini"), + ("xorg_not_found_title_tip", "Xorg ni najden"), + ("xorg_not_found_text_tip", "Namestite Xorg"), + ("no_desktop_title_tip", "Namizno okolje ni na voljo"), + ("no_desktop_text_tip", "Namestite GNOME"), + ("No need to elevate", "Povzdig pravic ni potreben"), + ("System Sound", "Sistemski zvok"), + ("Default", "Privzeto"), + ("New RDP", "Nova RDP povezava"), + ("Fingerprint", "Prstni odtis"), + ("Copy Fingerprint", "Kopiraj prstni odtis"), + ("no fingerprints", "ni prstnega odtisa"), + ("Select a peer", "Izberite partnerja"), + ("Select peers", "Izberite partnerje"), + ("Plugins", "Vključki"), + ("Uninstall", "Odstrani"), + ("Update", "Posodobi"), + ("Enable", "Omogoči"), + ("Disable", "Onemogoči"), + ("Options", "Možnosti"), + ("resolution_original_tip", "Izvirna ločljivost"), + ("resolution_fit_local_tip", "Prilagodi lokalni ločljivosti"), + ("resolution_custom_tip", "Ločljivost po meri"), + ("Collapse toolbar", "Strni orodno vrstico"), + ("Accept and Elevate", "Sprejmi in povzdigni pravice"), + ("accept_and_elevate_btn_tooltip", "Sprejmi povezavo in preko nadzora uporabniškera računa povišaj pravice"), + ("clipboard_wait_response_timeout_tip", "Časovna omejitev pri kopiranju je potekla"), + ("Incoming connection", "Dohodna povezava"), + ("Outgoing connection", "Odhodna povezava"), + ("Exit", "Izhod"), + ("Open", "Odpri"), + ("logout_tip", "Ali ste prepričani, da se želite odjaviti?"), + ("Service", "Storitev"), + ("Start", "Zaženi"), + ("Stop", "Ustavi"), + ("exceed_max_devices", "Dosegli ste največje dovoljeno število upravljanih naprav."), + ("Sync with recent sessions", "Sinhroniziraj z nedavnimi sejami"), + ("Sort tags", "Uredi oznake"), + ("Open connection in new tab", "Odpri povezavo na novem zavihku"), + ("Move tab to new window", "Premakni zavihek v novo okno"), + ("Can not be empty", "Ne more biti prazno"), + ("Already exists", "Že obstaja"), + ("Change Password", "Spremeni geslo"), + ("Refresh Password", "Osveži geslo"), + ("ID", "ID"), + ("Grid View", "Mrežni pogled"), + ("List View", "Pogled seznama"), + ("Select", "Izberi"), + ("Toggle Tags", "Preklopi oznake"), + ("pull_ab_failed_tip", "Adresarja ni bilo mogoče osvežiti"), + ("push_ab_failed_tip", "Adresarja ni bilo mogoče poslati na strežnik"), + ("synced_peer_readded_tip", "Naprave, ki so bile prisotne v nedavnih sejah bodo sinhronizirane z adresarjem."), + ("Change Color", "Spremeni barvo"), + ("Primary Color", "Osnovne barve"), + ("HSV Color", "Barve HSV"), + ("Installation Successful!", "Namestitev uspešna"), + ("Installation failed!", "Namestitev ni uspela"), + ("Reverse mouse wheel", "Obrni smer drsenja miškinega kolesca"), + ("{} sessions", "{} sej"), + ("scam_title", "Lahko gre za prevaro!"), + ("scam_text1", "V primeru, da vas je nekdo, ki ga ne poznate in mu zaupate prosil, da uporabite RustDesk, prekinite klic in program zaprite."), + ("scam_text2", "RustDesk omogoča popoln nadzor nad vašim računalnikom in telefonom, in se lahko uporabi za krajo vašega denarja ali pa zasebnih podatkov."), + ("Don't show again", "Ne prikaži znova"), + ("I Agree", "Strinjam se"), + ("Decline", "Zavrni"), + ("Timeout in minutes", "Časovna omejitev v minutah"), + ("auto_disconnect_option_tip", "Samodejno prekini neaktivne seje"), + ("Connection failed due to inactivity", "Povezava je bila prekinjena zaradi neaktivnosti"), + ("Check for software update on startup", "Preveri za posodobitve ob zagonu"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Prosimo, nadgradite RustDesk Server Pro na različico {} ali novejšo."), + ("pull_group_failed_tip", "Osveževanje skupine ni uspelo"), + ("Filter by intersection", "Filtriraj po preseku"), + ("Remove wallpaper during incoming sessions", "Odstrani sliko ozadja ob dohodnih povezavah"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Zaslon je bil odklopljen, preklop na primarni zaslon."), + ("No displays", "Ni zaslonov"), + ("Open in new window", "Odpri v novem oknu"), + ("Show displays as individual windows", "Prikaži zaslone kot ločena okna"), + ("Use all my displays for the remote session", "Uporabi vse zaslone za oddaljeno sejo"), + ("selinux_tip", "Na vaši napravi je omogčen SELinux, kar lahko povzroča težave pri oddaljenem nadzoru"), + ("Change view", "Spremeni pogled"), + ("Big tiles", "Velike ploščice"), + ("Small tiles", "Majhne ploščice"), + ("List", "Seznam"), + ("Virtual display", "Navidezni zaslon"), + ("Plug out all", "Odklopi vse"), + ("True color (4:4:4)", "Popolne barve (4:4:4)"), + ("Enable blocking user input", "Omogoči blokiranje vnosa"), + ("id_input_tip", "Vnesete lahko ID, neposredni IP naslov, ali pa domeno in vrata (:)\nČe želite dostopati do naprave na drugem strežniku, pripnite naslov strežnika (@?key=), npr. 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nČe želite dostopati do naprave na javnem strežniku, vnesite »@public«; ključ za javni strežnik ni potreben.\nČe želite vsiliti povezavo preko posrednika, pripnite »/r« na konec IDja, npr. »9123456234/r«."), + ("privacy_mode_impl_mag_tip", "Način 1"), + ("privacy_mode_impl_virtual_display_tip", "Način 2"), + ("Enter privacy mode", "Vstopi v zasebni način"), + ("Exit privacy mode", "Izstopi iz zasebnega načina"), + ("idd_not_support_under_win10_2004_tip", "Posredni gonilnik ni podprt. Za uporabo rabite Windows 10 2004 ali novejšo različico."), + ("input_source_1_tip", "Vir vnosa 1"), + ("input_source_2_tip", "Vir vnosa 2"), + ("Swap control-command key", "Zamenjaj tipki Ctrl-Command"), + ("swap-left-right-mouse", "Zamenjaj levo in desno tipko miške"), + ("2FA code", "Koda za dvostopenjsko preverjanje"), + ("More", "Več"), + ("enable-2fa-title", "Omogoči dvostopenjsko preverjanje"), + ("enable-2fa-desc", "Pripravite vaš TOTP avtentikator. Uporabite lahko programe kot so Authy, Microsoft ali Google Authenticator, na vašem telefonu ali računalniku.\n\nZa omogočanje dvostopenjskega preverjanja, skenirajte QR kodo in vnesite kodo, ki jo prikaže aplikacija."), + ("wrong-2fa-code", "Kode ni bilo mogoče preveriti. Preverite, da je koda pravilna, in da je nastavitev ure točna."), + ("enter-2fa-title", "Dvostopenjsko preverjanje"), + ("Email verification code must be 6 characters.", "E-poštna koda za preverjanje mora imeti 6 znakov."), + ("2FA code must be 6 digits.", "Koda za dvostopenjsko preverjanje mora imeti 6 znakov."), + ("Multiple Windows sessions found", "Najdenih je bilo več Windows sej"), + ("Please select the session you want to connect to", "Izberite sejo, v katero se želite povezati"), + ("powered_by_me", "Uporablja tehnologijo RustDesk"), + ("outgoing_only_desk_tip", "To je prilagojena različica.\nLahko se povežete na druge naprave, druge naprave pa se k vam ne morejo povezati."), + ("preset_password_warning", "Ta prilagojena različica ima prednastavljeno geslo. Kdorkoli, ki pozna to geslo, lahko prevzame popoln nadzor nad vašim računalnikom. Če tega niste pričakovali, takoj odstranite program."), + ("Security Alert", "Varnostno opozorilo"), + ("My address book", "Moj adresar"), + ("Personal", "Osebni"), + ("Owner", "Lastnik"), + ("Set shared password", "Nastavi deljeno geslo"), + ("Exist in", "Obstaja v"), + ("Read-only", "Samo za branje"), + ("Read/Write", "Branje/pisanje"), + ("Full Control", "Popoln nadzor"), + ("share_warning_tip", "Zgornja polja so deljena, in vidna vsem"), + ("Everyone", "Vsi"), + ("ab_web_console_tip", "Več na spletni konzoli"), + ("allow-only-conn-window-open-tip", "Dovoli povezavo samo če je okno RustDeska odprto"), + ("no_need_privacy_mode_no_physical_displays_tip", "Ni fizičnih zaslonov, zasebni način ni potreben"), + ("Follow remote cursor", "Sledi oddaljenemu kazalcu"), + ("Follow remote window focus", "Sledi oddaljenemu fokusu"), + ("default_proxy_tip", "Privzeti protokol je Socks5 na vratih 1080"), + ("no_audio_input_device_tip", "Ni bilo možno najti vhodne zvočne naprave"), + ("Incoming", "Dohodno"), + ("Outgoing", "Odhodno"), + ("Clear Wayland screen selection", "Počisti izbiro Wayland zaslona"), + ("clear_Wayland_screen_selection_tip", "Po čiščenju izbire Wayland zaslona lahko ponovno izberete zaslon za delitev"), + ("confirm_clear_Wayland_screen_selection_tip", "Ali res želite počistiti izbiro Wayland zaslona?"), + ("android_new_voice_call_tip", "Prejeli ste prošnjo za nov glasovni klic. Če sprejmete, bo zvok preklopljen na glasovno komunikacijo."), + ("texture_render_tip", "Uporabi upodabljanje tekstur, za gladkejše slike. Izklopite, če imate težave pri upodabljanju."), + ("Use texture rendering", "Uporabi upodabljanje tekstur"), + ("Floating window", "Plavajoče okno"), + ("floating_window_tip", "Pomaga pri RustDesk storitvi v ozadju"), + ("Keep screen on", "Ohranite zaslon prižgan"), + ("Never", "Nikoli"), + ("During controlled", "Med nadzorom"), + ("During service is on", "Med vklopljeno storitvijo"), + ("Capture screen using DirectX", "Uporabi DirectX za zajem zaslona"), + ("Back", "Nazaj"), + ("Apps", "Aplikacije"), + ("Volume up", "Glasneje"), + ("Volume down", "Tišje"), + ("Power", "Vklop/izklop"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Če vklopite to možnost, lahko dobite kodo za dvostopenjsko preverjanje od bota. Lahko se uporabi tudi za obveščanje o povezavi."), + ("enable-bot-desc", "1. Odprite pogovor z @BotFather.\n2. Pošljite ukaz »/newbot« in prejeli boste žeton.\n3. Začnite pogovor z na novo narejenim botom. Pošljite sporočilo z desno poševnico (/) kot npr. »/hello« za aktivacijo."), + ("cancel-2fa-confirm-tip", "Ali ste prepričani, da želite ukiniti dvostopenjsko preverjanje?"), + ("cancel-bot-confirm-tip", "Ali ste prepričani, da želite ukiniti Telegram bota?"), + ("About RustDesk", "O RustDesku"), + ("Send clipboard keystrokes", "Vtipkaj vsebino odložišča"), + ("network_error_tip", "Preverite vašo mrežno povezavo, nato kliknite Ponovi."), + ("Unlock with PIN", "Odkleni s PINom"), + ("Requires at least {} characters", "Potrebuje vsaj {} znakov."), + ("Wrong PIN", "Napačen PIN"), + ("Set PIN", "Nastavi PIN"), + ("Enable trusted devices", "Omogoči zaupanja vredne naprave"), + ("Manage trusted devices", "Upravljaj zaupanja vredne naprave"), + ("Platform", "Platforma"), + ("Days remaining", "Preostane dni"), + ("enable-trusted-devices-tip", "Na zaupanja vrednih napravah ni potrebno dvostopenjsko preverjanje"), + ("Parent directory", "Nadrejena mapa"), + ("Resume", "Nadaljuj"), + ("Invalid file name", "Neveljavno ime datoteke"), + ("one-way-file-transfer-tip", "Enosmerni prenos datotek je omogočen na nadzorovani strani"), + ("Authentication Required", "Potrebno je preverjanje pristnosti"), + ("Authenticate", "Preverjanje pristnosti"), + ("web_id_input_tip", "Vnesete lahko ID iz istega strežnika, neposredni dostop preko IP naslova v spletnem odjemalcu ni podprt.\nČe želite dostopati do naprave na drugem strežniku, pripnite naslov strežnika (@?key=), npr. 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nČe želite dostopati do naprave na javnem strežniku, vnesite »@public«; ključ za javni strežnik ni potreben."), + ("Download", "Prenos"), + ("Upload folder", "Naloži mapo"), + ("Upload files", "Naloži datoteke"), + ("Clipboard is synchronized", "Odložišče je usklajeno"), + ("Update client clipboard", "Osveži odjemalčevo odložišče"), + ("Untagged", "Neoznačeno"), + ("new-version-of-{}-tip", "Na voljo je nova različica {}"), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Prosimo, nadgradite RustDesk odjemalec na različico {} ali novejšo na oddaljeni strani."), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Pogled kamere"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Nadaljuj z {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/sq.rs b/vendor/rustdesk/src/lang/sq.rs new file mode 100644 index 0000000..f7f6c16 --- /dev/null +++ b/vendor/rustdesk/src/lang/sq.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Statusi"), + ("Your Desktop", "Desktopi juaj"), + ("desk_tip", "Desktopi juaj mund të aksesohet me këtë ID dhe fjalëkalim."), + ("Password", "fjalëkalimi"), + ("Ready", "Gati"), + ("Established", "I themeluar"), + ("connecting_status", "statusi_i_lidhjes"), + ("Enable service", "Aktivizo Shërbimin"), + ("Start service", "Nis Shërbimin"), + ("Service is running", "Shërbimi është duke funksionuar"), + ("Service is not running", "Shërbimi nuk është duke funksionuar"), + ("not_ready_status", "Jo gati.Ju lutem kontolloni lidhjen tuaj."), + ("Control Remote Desktop", "Kontrolli i desktopit në distancë"), + ("Transfer file", "Transfero dosje"), + ("Connect", "Lidh"), + ("Recent sessions", "Sessioni i fundit"), + ("Address book", "Libër adresash"), + ("Confirmation", "Konfirmimi"), + ("TCP tunneling", "TCP tunel"), + ("Remove", "Hiqni"), + ("Refresh random password", "Rifreskoni fjalëkalimin e rastësishëm"), + ("Set your own password", "Vendosni fjalëkalimin tuaj"), + ("Enable keyboard/mouse", "Aktivizoni Tastierën/Mousin"), + ("Enable clipboard", "Aktivizo"), + ("Enable file transfer", "Aktivizoni transferimin e skedarëve"), + ("Enable TCP tunneling", "Aktivizoni TCP tunneling"), + ("IP Whitelisting", ""), + ("ID/Relay Server", "ID/server rele"), + ("Import server config", "Konfigurimi i severit të importit"), + ("Export Server Config", "Konfigurimi i severit të eksportit"), + ("Import server configuration successfully", "Konfigurimi i severit të importit i suksesshëm"), + ("Export server configuration successfully", "Konfigurimi i severit të eksprotit i suksesshëm"), + ("Invalid server configuration", "Konfigurim i pavlefshëm i serverit"), + ("Clipboard is empty", "Clipboard është bosh"), + ("Stop service", "Ndaloni shërbimin"), + ("Change ID", "Ndryshoni ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Lejohen Vetëm karkteret a-z,A-Z,0-9, - (dash) dhe _(nënvizimet).Shkronja e parë duhet të jetë a-z, A-Z. Gjatesia midis 6 dhe 16."), + ("Website", "Faqe ëebi"), + ("About", "Rreth"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Pa zë"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), + ("Audio Input", "Inputi zërit"), + ("Enhancements", "Përmirësimet"), + ("Hardware Codec", "Kodeku Harduerik"), + ("Adaptive bitrate", "Shpejtësia adaptive e biteve"), + ("ID Server", "ID e serverit"), + ("Relay Server", "Serveri rele"), + ("API Server", "Serveri API"), + ("invalid_http", "Duhet të fillojë me http:// ose https://"), + ("Invalid IP", "IP e pavlefshme"), + ("Invalid format", "Format i pavlefshëm"), + ("server_not_support", "Nuk suportohet akoma nga severi"), + ("Not available", "I padisponueshëm"), + ("Too frequent", "Shumë i përdorur"), + ("Cancel", "Anullo"), + ("Skip", "Kalo"), + ("Close", "Mbyll"), + ("Retry", "Riprovo"), + ("OK", "OK"), + ("Password Required", "Fjalëkalimi i detyrueshëm"), + ("Please enter your password", "Ju lutem vendosni fjalëkalimin tuaj"), + ("Remember password", "Mbani mend fjalëkalimin"), + ("Wrong Password", "Fjalëkalim i gabuar"), + ("Do you want to enter again?", "Dëshironi të vendosni përsëri"), + ("Connection Error", "Gabim në lidhje"), + ("Error", "Gabim"), + ("Reset by the peer", "Riseto nga peer"), + ("Connecting...", "Duke u lidhur"), + ("Connection in progress. Please wait.", "Lidhja në progres. Ju lutem prisni"), + ("Please try 1 minute later", "Ju lutemi provoni 1 minut më vonë"), + ("Login Error", "Gabim në login"), + ("Successful", "E suksesshme"), + ("Connected, waiting for image...", "E lidhur , prisni për imazhin..."), + ("Name", "Emri"), + ("Type", "Shkruaj"), + ("Modified", "E modifikuar"), + ("Size", "Madhesia"), + ("Show Hidden Files", "Shfaq skedarët e fshehur"), + ("Receive", "Merr"), + ("Send", "Dërgo"), + ("Refresh File", "Rifreskoni skedarët"), + ("Local", "Lokal"), + ("Remote", "Në distancë"), + ("Remote Computer", "Kompjuter në distancë"), + ("Local Computer", "Kompjuter Lokal"), + ("Confirm Delete", "Konfirmoni fshirjen"), + ("Delete", "Fshij"), + ("Properties", "Karakteristikat"), + ("Multi Select", "Shumë përzgjedhje"), + ("Select All", "Selektoni të gjitha"), + ("Unselect All", "Ç'selektoni të gjitha"), + ("Empty Directory", "Direktori boshe"), + ("Not an empty directory", "Jo një direktori boshe"), + ("Are you sure you want to delete this file?", "Jeni të sigurtë që doni të fshini këtë skedarë"), + ("Are you sure you want to delete this empty directory?", "Jeni të sigurtë që dëshironi të fshini këtë direktori boshe"), + ("Are you sure you want to delete the file of this directory?", "Jeni të sigurtë që dëshironi te fshini skedarin e kësaj direktorie"), + ("Do this for all conflicts", "Bëjeni këtë për të gjitha konfliktet"), + ("This is irreversible!", "Kjo është e pakthyeshme"), + ("Deleting", "Duke i fshirë"), + ("files", "Skedarë"), + ("Waiting", "Në pritje"), + ("Finished", "Përfunduar"), + ("Speed", "Shpejtësia"), + ("Custom Image Quality", "Cilësi e personalizuar imazhi"), + ("Privacy mode", "Modaliteti i Privatësisë"), + ("Block user input", "Blloko inputin e përdorusesit"), + ("Unblock user input", "Zhblloko inputin e përdorusesit"), + ("Adjust Window", "Rregulloni dritaren"), + ("Original", "Origjinal"), + ("Shrink", "Shkurtim"), + ("Stretch", "Shtrirje"), + ("Scrollbar", "Shiriti i lëvizjes"), + ("ScrollAuto", "Levizje automatikisht"), + ("Good image quality", "Cilësi e mirë imazhi"), + ("Balanced", "E balancuar"), + ("Optimize reaction time", "Optimizo kohën e reagimit"), + ("Custom", "Personalizuar"), + ("Show remote cursor", "Shfaq kursorin në distancë"), + ("Show quality monitor", "Shaq cilësinë e monitorit"), + ("Disable clipboard", "Ç'aktivizo clipboard"), + ("Lock after session end", "Kyç pasi sesioni të përfundoj"), + ("Insert Ctrl + Alt + Del", "Fut Ctrl + Alt + Del"), + ("Insert Lock", "Fut bllokimin"), + ("Refresh", "Rifresko"), + ("ID does not exist", "ID nuk ekziston"), + ("Failed to connect to rendezvous server", "Dështoj të lidhet me serverin e takimit"), + ("Please try later", "Ju lutemi provoni më vonë"), + ("Remote desktop is offline", "Desktopi në distancë nuk është në linjë"), + ("Key mismatch", "Mospërputhje kryesore"), + ("Timeout", "Koha mbaroi"), + ("Failed to connect to relay server", "Lidhja me serverin transmetues dështoi"), + ("Failed to connect via rendezvous server", "Lidhja nëpërmjet serverit të takimit dështoi"), + ("Failed to connect via relay server", "Lidhja nëpërmjet serverit të transmetimit dështoi"), + ("Failed to make direct connection to remote desktop", "Lidhja direkte me desktopin në distancë dështoi"), + ("Set Password", "Vendosni fjalëkalimin"), + ("OS Password", "OS fjalëkalim"), + ("install_tip", "Për shkak të UAC, RustDesk nuk mund të punoj sic duhet si nje remote në distancë në disa raste. Për të shamngur UAC, ju lutem klikoni butonin më poshtë për të instaluar RustDesk në sistem."), + ("Click to upgrade", "Klikoni për përmirësim"), + ("Configure", "Koniguro"), + ("config_acc", "Për të kontrolluar Desktopin tuaj nga distanca, duhet të jepni leje RustDesk \"Aksesueshmëri\"."), + ("config_screen", "Për të aksesuar Desktopin tuaj nga distanca, duhet ti jepni lejet RustDesk \"Regjistrimin e ekranit\"."), + ("Installing ...", "Duke u instaluar"), + ("Install", "Instalo"), + ("Installation", "Instalimi"), + ("Installation Path", "Rruga instalimit"), + ("Create start menu shortcuts", "Krijoni shortcuts për menunë e fillimit"), + ("Create desktop icon", "Krijoni ikonën e desktopit"), + ("agreement_tip", "Duke filluar instalimin, ju pranoni marrëveshjen e licencës"), + ("Accept and Install", "Pranoni dhe instaloni"), + ("End-user license agreement", "Marrëeveshja e licencës së perdoruesit fundor"), + ("Generating ...", "Duke gjeneruar"), + ("Your installation is lower version.", "Instalimi juaj është version i ulët"), + ("not_close_tcp_tip", "Mos e mbyll këtë dritare ndërsa jeni duke përdorur tunelin"), + ("Listening ...", "Duke dëgjuar"), + ("Remote Host", "Host në distancë"), + ("Remote Port", "Port në distancë"), + ("Action", "Veprim"), + ("Add", "Shto"), + ("Local Port", "Portë Lokale"), + ("Local Address", "Adresë Lokale"), + ("Change Local Port", "Ndryshoni portën lokale"), + ("setup_server_tip", "Për lidhje më të shpejtë, ju lutemi konfiguroni serverin tuaj"), + ("Too short, at least 6 characters.", "Shumë e shkurtër , nevojiten të paktën 6 karaktere"), + ("The confirmation is not identical.", "Konfirmimi nuk është identik"), + ("Permissions", "Leje"), + ("Accept", "Prano"), + ("Dismiss", "Hiq"), + ("Disconnect", "Shkëput"), + ("Enable file copy and paste", "Lejoni kopjimin dhe pastimin e skedarëve"), + ("Connected", "I lidhur"), + ("Direct and encrypted connection", "Lidhje direkte dhe enkriptuar"), + ("Relayed and encrypted connection", "Lidhje transmetuese dhe e enkriptuar"), + ("Direct and unencrypted connection", "Lidhje direkte dhe jo e enkriptuar"), + ("Relayed and unencrypted connection", "Lidhje transmetuese dhe jo e enkriptuar"), + ("Enter Remote ID", "Vendosni ID në distancë"), + ("Enter your password", "Vendosni fjalëkalimin tuaj"), + ("Logging in...", "Duke u loguar"), + ("Enable RDP session sharing", "Aktivizoni shpërndarjen e sesionit RDP"), + ("Auto Login", "Hyrje automatike"), + ("Enable direct IP access", "Aktivizoni aksesimin e IP direkte"), + ("Rename", "Riemërto"), + ("Space", "Hapërsirë"), + ("Create desktop shortcut", "Krijoni shortcut desktop"), + ("Change Path", "Ndrysho rrugëzimin"), + ("Create Folder", "Krijoni një folder"), + ("Please enter the folder name", "Ju lutem vendosni emrin e folderit"), + ("Fix it", "Rregulloni ate"), + ("Warning", "Dicka po shkon keq"), + ("Login screen using Wayland is not supported", "Hyrja në ekran duke përdorur Wayland muk suportohet"), + ("Reboot required", "Kërkohet rinisja"), + ("Unsupported display server", "Nuk supurtohet severi ekranit"), + ("x11 expected", "Pritet x11"), + ("Port", "Port"), + ("Settings", "Cilësimet"), + ("Username", "Emri i përdoruesit"), + ("Invalid port", "Port e pavlefshme"), + ("Closed manually by the peer", "E mbyllur manualisht nga peer"), + ("Enable remote configuration modification", "Aktivizoni modifikimin e konfigurimit në distancë"), + ("Run without install", "Ekzekuto pa instaluar"), + ("Connect via relay", ""), + ("Always connect via relay", "Gjithmonë lidheni me transmetues"), + ("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."), + ("Login", "Hyrje"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), + ("Logout", "Dalje"), + ("Tags", "Tage"), + ("Search ID", "Kerko ID"), + ("whitelist_sep", "Të ndara me presje, pikëpresje, hapësira ose rresht të ri"), + ("Add ID", "Shto ID"), + ("Add Tag", "Shto Tag"), + ("Unselect all tags", "Hiq selektimin e te gjithë tageve"), + ("Network error", "Gabim në rrjet"), + ("Username missed", "Mungon përdorusesi"), + ("Password missed", "Mungon fjalëkalimi"), + ("Wrong credentials", "Kredinciale të gabuara"), + ("The verification code is incorrect or has expired", ""), + ("Edit Tag", "Edito tagun"), + ("Forget Password", "Fjalëkalim jo i kujtueshëm"), + ("Favorites", "Te preferuarat"), + ("Add to Favorites", "Shto te të preferuarat"), + ("Remove from Favorites", "Hiq nga të preferuarat"), + ("Empty", "Bosh"), + ("Invalid folder name", "Emri i dosjes i pavlefshëm"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "I pambuluar"), + ("install_daemon_tip", "Për të nisur në boot, duhet të instaloni shërbimin e sistemit"), + ("Remote ID", "ID në distancë"), + ("Paste", "Ngjit"), + ("Paste here?", "Ngjit këtu"), + ("Are you sure to close the connection?", "Jeni të sigurtë të mbyllni lidhjen"), + ("Download new version", "Shkarko versionin e ri"), + ("Touch mode", "Metoda me prekje"), + ("Mouse mode", "Modaliteti mausit"), + ("One-Finger Tap", "Prekja Një gisht"), + ("Left Mouse", "Mausi majt"), + ("One-Long Tap", "Prekja nje-gjate"), + ("Two-Finger Tap", "Prekja dy-gishta"), + ("Right Mouse", "Mausi i djathtë"), + ("One-Finger Move", "Lëvizja një-gisht"), + ("Double Tap & Move", "Prekja dhe lëvizja e dyfishtë"), + ("Mouse Drag", "Zhvendosja e mausit"), + ("Three-Finger vertically", "Tre-Gishta vertikalisht"), + ("Mouse Wheel", "Rrota mausit"), + ("Two-Finger Move", "Lëvizja Dy-Gishta"), + ("Canvas Move", "Lëvizja Canvas"), + ("Pinch to Zoom", "Prekni për të zmadhuar"), + ("Canvas Zoom", "Zmadhimi Canavas"), + ("Reset canvas", "Riseto canvas"), + ("No permission of file transfer", "Nuk ka leje për transferimin e dosjesve"), + ("Note", "Shënime"), + ("Connection", "Lidhja"), + ("Share screen", "Ndaj ekranin"), + ("Chat", "Biseda"), + ("Total", "Total"), + ("items", "artikuj"), + ("Selected", "E zgjedhur"), + ("Screen Capture", "Kapja e ekranit"), + ("Input Control", "Kontrollo inputin"), + ("Audio Capture", "Kapja e zërit"), + ("Do you accept?", "E pranoni"), + ("Open System Setting", "Hapni cilësimet e sistemit"), + ("How to get Android input permission?", "Si të merrni leje e inputit të Android"), + ("android_input_permission_tip1", "Në mënyrë që një pajisje në distancë të kontrollojë pajisjen tuaj Android nëpërmjet mausit ose prekjes, duhet të lejoni RustDesk të përdorë shërbimin."), + ("android_input_permission_tip2", "Ju lutemi shkoni në faqen tjetër të cilësimeve të sistemit, gjeni dhe shtypni [Shërbimet e Instaluara], aktivizoni shërbimin [RustDesk Input]"), + ("android_new_connection_tip", "Është marrë një kërkesë e re kontrolli, e cila dëshiron të kontrollojë pajisjen tuaj aktuale."), + ("android_service_will_start_tip", "Aktivizimi i \"Regjistrimi i ekranit\" do të nisë automatikisht shërbimin, duke lejuar pajisjet e tjera të kërkojnë një lidhje me pajisjen tuaj."), + ("android_stop_service_tip", "Mbyllja e shërbimit do të mbyllë automatikisht të gjitha lidhjet e vendosura."), + ("android_version_audio_tip", "Versioni aktual i Android nuk mbështet regjistrimin e audios, ju lutemi përmirësoni në Android 10 ose më të lartë."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), + ("Account", "Llogaria"), + ("Overwrite", "Përshkruaj"), + ("This file exists, skip or overwrite this file?", "Ky skedar ekziston , tejkalo ose përshkruaj këtë skedarë"), + ("Quit", "Hiq"), + ("Help", "Ndihmë"), + ("Failed", "Deshtoi"), + ("Succeeded", "Sukses"), + ("Someone turns on privacy mode, exit", "Dikush ka ndezur menyrën e privatësisë , largohu"), + ("Unsupported", "Nuk mbështetet"), + ("Peer denied", "Peer mohohet"), + ("Please install plugins", "Ju lutemi instaloni shtojcat"), + ("Peer exit", "Dalje peer"), + ("Failed to turn off", "Dështoi të fiket"), + ("Turned off", "I fikur"), + ("Language", "Gjuha"), + ("Keep RustDesk background service", "Mbaje shërbimin e sfondit të RustDesk"), + ("Ignore Battery Optimizations", "Injoro optimizimet e baterisë"), + ("android_open_battery_optimizations_tip", "Nëse dëshironi ta çaktivizoni këtë veçori, ju lutemi shkoni te faqja tjetër e cilësimeve të aplikacionit RustDesk, gjeni dhe shtypni [Batteri], hiqni zgjedhjen [Te pakufizuara]"), + ("Start on boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), + ("Connection not allowed", "Lidhja nuk lejohet"), + ("Legacy mode", "Modaliteti i trashëgimisë"), + ("Map mode", "Modaliteti i hartës"), + ("Translate mode", "Modaliteti i përkthimit"), + ("Use permanent password", "Përdor fjalëkalim të përhershëm"), + ("Use both passwords", "Përdor të dy fjalëkalimet"), + ("Set permanent password", "Vendos fjalëkalimin e përhershëm"), + ("Enable remote restart", "Aktivizo rinisjen në distancë"), + ("Restart remote device", "Rinisni pajisjen në distancë"), + ("Are you sure you want to restart", "A jeni i sigurt që dëshironi të rinisni"), + ("Restarting remote device", "Rinisja e pajisjes në distancë"), + ("remote_restarting_tip", "Pajisja në distancë po riniset, ju lutemi mbyllni këtë kuti mesazhi dhe lidheni përsëri me fjalëkalim të përhershëm pas një kohe"), + ("Copied", "Kopjuar"), + ("Exit Fullscreen", "Dil nga ekrani i plotë"), + ("Fullscreen", "Ekran i plotë"), + ("Mobile Actions", "Veprimet celulare"), + ("Select Monitor", "Zgjidh Monitor"), + ("Control Actions", "Veprimet e kontrollit"), + ("Display Settings", "Cilësimet e ekranit"), + ("Ratio", "Raport"), + ("Image Quality", "Cilësia e imazhit"), + ("Scroll Style", "Stili i lëvizjes"), + ("Show Toolbar", ""), + ("Hide Toolbar", ""), + ("Direct Connection", "Lidhja e drejtpërdrejtë"), + ("Relay Connection", "Lidhja rele"), + ("Secure Connection", "Lidhje e sigurt"), + ("Insecure Connection", "Lidhje e pasigurt"), + ("Scale original", "Shkalla origjinale"), + ("Scale adaptive", " E përsjhtatshme në shkallë"), + ("General", "Gjeneral"), + ("Security", "Siguria"), + ("Theme", "Theme"), + ("Dark Theme", "Theme e errët"), + ("Light Theme", ""), + ("Dark", "E errët"), + ("Light", "Drita"), + ("Follow System", "Ndiq sistemin"), + ("Enable hardware codec", "Aktivizo kodekun e harduerit"), + ("Unlock Security Settings", "Zhbllokoni cilësimet e sigurisë"), + ("Enable audio", "Aktivizo audio"), + ("Unlock Network Settings", "Zhbllokoni cilësimet e rrjetit"), + ("Server", "Server"), + ("Direct IP Access", "Qasje e drejtpërdrejtë IP"), + ("Proxy", "Proxy"), + ("Apply", "Apliko"), + ("Disconnect all devices?", "Shkyç të gjitha pajisjet?"), + ("Clear", "Pastro"), + ("Audio Input Device", "Pajisja e hyrjes audio"), + ("Use IP Whitelisting", "Përdor listën e bardhë IP"), + ("Network", "Rrjeti"), + ("Pin Toolbar", ""), + ("Unpin Toolbar", ""), + ("Recording", "Regjistrimi"), + ("Directory", "Direktoria"), + ("Automatically record incoming sessions", "Regjistro automatikisht seancat hyrëse"), + ("Automatically record outgoing sessions", ""), + ("Change", "Ndrysho"), + ("Start session recording", "Fillo regjistrimin e sesionit"), + ("Stop session recording", "Ndalo regjistrimin e sesionit"), + ("Enable recording session", "Aktivizo seancën e regjistrimit"), + ("Enable LAN discovery", "Aktivizo zbulimin e LAN"), + ("Deny LAN discovery", "Mohoni zbulimin e LAN"), + ("Write a message", "Shkruani një mesazh"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Ju lutemi prisni për konfirmimin e UAC..."), + ("elevated_foreground_window_tip", "Përkohësisht është e pamundur për të përdorur mausin dhe tastierën, për shkak se dritarja aktuale e desktopit në distancë kërkon privilegj më të lartë për të vepruar,ju mund t'i kërkoni përdoruesit në distancë të minimizojë dritaren aktuale. Për të shmangur këtë problem, rekomandohet të instaloni softuerin në pajisjen në distancë ose ekzekutoni atë me privilegje administratori."), + ("Disconnected", "Shkyçur"), + ("Other", "Tjetër"), + ("Confirm before closing multiple tabs", "Konfirmo përpara se të mbyllësh shumë skeda"), + ("Keyboard Settings", "Cilësimet e tastierës"), + ("Full Access", "Qasje e plotë"), + ("Screen Share", "Ndarja e ekranit"), + ("ubuntu-21-04-required", "Wayland kërkon Ubuntu 21.04 ose version më të lartë"), + ("wayland-requires-higher-linux-version", "Wayland kërkon një version më të lartë të shpërndarjes linux. Ju lutemi provoni desktopin X11 ose ndryshoni OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Ju lutemi zgjidhni ekranin që do të ndahet (Vepro në anën e kolegëve"), + ("Show RustDesk", "Shfaq RustDesk"), + ("This PC", "Ky PC"), + ("or", "ose"), + ("Elevate", "Ngritja"), + ("Zoom cursor", "Zmadho kursorin"), + ("Accept sessions via password", "Prano sesionin nëpërmjet fjalëkalimit"), + ("Accept sessions via click", "Prano sesionet nëpërmjet klikimit"), + ("Accept sessions via both", "Prano sesionet nëpërmjet të dyjave"), + ("Please wait for the remote side to accept your session request...", "Ju lutem prisni që ana në distancë të pranoj kërkësen tuaj"), + ("One-time Password", "Fjalëkalim Një-herë"), + ("Use one-time password", "Përdorni fjalëkalim Një-herë"), + ("One-time password length", "Gjatësia e fjalëkalimit një herë"), + ("Request access to your device", "Kërko akses në pajisjejn tuaj"), + ("Hide connection management window", "Fshih dritaren e menaxhimit të lidhjes"), + ("hide_cm_tip", "Kjo është e mundur vetëm nëse aksesi bëhet nëpërmjet një fjalëkalimi të përhershëm"), + ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), + ("Skipped", ""), + ("Add to address book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), + ("Select a peer", ""), + ("Select peers", ""), + ("Plugins", ""), + ("Uninstall", ""), + ("Update", ""), + ("Enable", ""), + ("Disable", ""), + ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), + ("Collapse toolbar", ""), + ("Accept and Elevate", ""), + ("accept_and_elevate_btn_tooltip", ""), + ("clipboard_wait_response_timeout_tip", ""), + ("Incoming connection", ""), + ("Outgoing connection", ""), + ("Exit", ""), + ("Open", ""), + ("logout_tip", ""), + ("Service", ""), + ("Start", ""), + ("Stop", ""), + ("exceed_max_devices", ""), + ("Sync with recent sessions", ""), + ("Sort tags", ""), + ("Open connection in new tab", ""), + ("Move tab to new window", ""), + ("Can not be empty", ""), + ("Already exists", ""), + ("Change Password", ""), + ("Refresh Password", ""), + ("ID", ""), + ("Grid View", ""), + ("List View", ""), + ("Select", ""), + ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), + ("synced_peer_readded_tip", ""), + ("Change Color", ""), + ("Primary Color", ""), + ("HSV Color", ""), + ("Installation Successful!", ""), + ("Installation failed!", ""), + ("Reverse mouse wheel", ""), + ("{} sessions", ""), + ("scam_title", ""), + ("scam_text1", ""), + ("scam_text2", ""), + ("Don't show again", ""), + ("I Agree", ""), + ("Decline", ""), + ("Timeout in minutes", ""), + ("auto_disconnect_option_tip", ""), + ("Connection failed due to inactivity", ""), + ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), + ("pull_group_failed_tip", ""), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), + ("idd_not_support_under_win10_2004_tip", ""), + ("input_source_1_tip", ""), + ("input_source_2_tip", ""), + ("Swap control-command key", ""), + ("swap-left-right-mouse", ""), + ("2FA code", ""), + ("More", ""), + ("enable-2fa-title", ""), + ("enable-2fa-desc", ""), + ("wrong-2fa-code", ""), + ("enter-2fa-title", ""), + ("Email verification code must be 6 characters.", ""), + ("2FA code must be 6 digits.", ""), + ("Multiple Windows sessions found", ""), + ("Please select the session you want to connect to", ""), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("share_warning_tip", ""), + ("Everyone", ""), + ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", ""), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", ""), + ("Floating window", ""), + ("floating_window_tip", ""), + ("Keep screen on", ""), + ("Never", ""), + ("During controlled", ""), + ("During service is on", ""), + ("Capture screen using DirectX", ""), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", ""), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Vazhdo me {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/sr.rs b/vendor/rustdesk/src/lang/sr.rs new file mode 100644 index 0000000..bedbe48 --- /dev/null +++ b/vendor/rustdesk/src/lang/sr.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Vaša radna površina"), + ("desk_tip", "Vašoj radnoj površini se može pristupiti ovim ID i lozinkom."), + ("Password", "Lozinka"), + ("Ready", "Spremno"), + ("Established", "Uspostavljeno"), + ("connecting_status", "Spajanje na RustDesk mrežu..."), + ("Enable service", "Dozvoli servis"), + ("Start service", "Pokreni servis"), + ("Service is running", "Servis je pokrenut"), + ("Service is not running", "Servis nije pokrenut"), + ("not_ready_status", "Nije spremno. Proverite konekciju."), + ("Control Remote Desktop", "Upravljanje udaljenom radnom površinom"), + ("Transfer file", "Prenos fajla"), + ("Connect", "Spajanje"), + ("Recent sessions", "Poslednje sesije"), + ("Address book", "Adresar"), + ("Confirmation", "Potvrda"), + ("TCP tunneling", "TCP tunel"), + ("Remove", "Ukloni"), + ("Refresh random password", "Osveži slučajnu lozinku"), + ("Set your own password", "Postavi lozinku"), + ("Enable keyboard/mouse", "Dozvoli tastaturu/miša"), + ("Enable clipboard", "Dozvoli clipboard"), + ("Enable file transfer", "Dozvoli prenos fajlova"), + ("Enable TCP tunneling", "Dozvoli TCP tunel"), + ("IP Whitelisting", "IP pouzdana lista"), + ("ID/Relay Server", "ID/Posredni server"), + ("Import server config", "Import server konfiguracije"), + ("Export Server Config", "Eksport server konfiguracije"), + ("Import server configuration successfully", "Import server konfiguracije uspešan"), + ("Export server configuration successfully", "Eksport server konfiguracije uspešan"), + ("Invalid server configuration", "Pogrešna konfiguracija servera"), + ("Clipboard is empty", "Clipboard je prazan"), + ("Stop service", "Stopiraj servis"), + ("Change ID", "Promeni ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Dozvoljeni su samo a-z, A-Z, 0-9, - (dash) i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Dužina je od 6 do 16."), + ("Website", "Web sajt"), + ("About", "O programu"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Utišaj"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), + ("Audio Input", "Audio ulaz"), + ("Enhancements", "Proširenja"), + ("Hardware Codec", "Hardverski kodek"), + ("Adaptive bitrate", "Prilagodljiva gustina podataka"), + ("ID Server", "ID server"), + ("Relay Server", "Posredni server"), + ("API Server", "API server"), + ("invalid_http", "mora početi sa http:// ili https://"), + ("Invalid IP", "Nevažeća IP"), + ("Invalid format", "Pogrešan format"), + ("server_not_support", "Server još uvek ne podržava"), + ("Not available", "Nije dostupno"), + ("Too frequent", "Previše često"), + ("Cancel", "Otkaži"), + ("Skip", "Preskoči"), + ("Close", "Zatvori"), + ("Retry", "Ponovi"), + ("OK", "Ok"), + ("Password Required", "Potrebna lozinka"), + ("Please enter your password", "Molimo unesite svoju lozinku"), + ("Remember password", "Zapamti lozinku"), + ("Wrong Password", "Pogrešna lozinka"), + ("Do you want to enter again?", "Želite li da unesete ponovo?"), + ("Connection Error", "Greška u konekciji"), + ("Error", "Greška"), + ("Reset by the peer", "Prekinuto sa druge strane"), + ("Connecting...", "Povezivanje..."), + ("Connection in progress. Please wait.", "Povezivanje u toku. Molimo sačekajte."), + ("Please try 1 minute later", "Pokušajte minut kasnije"), + ("Login Error", "Greška u prijavljivanju"), + ("Successful", "Uspešno"), + ("Connected, waiting for image...", "Spojeno, sačekajte sliku..."), + ("Name", "Ime"), + ("Type", "Tip"), + ("Modified", "Izmenjeno"), + ("Size", "Veličina"), + ("Show Hidden Files", "Prikaži skrivene datoteke"), + ("Receive", "Prijem"), + ("Send", "Slanje"), + ("Refresh File", "Osveži datoteku"), + ("Local", "Lokalno"), + ("Remote", "Udaljeno"), + ("Remote Computer", "Udaljeni računar"), + ("Local Computer", "Lokalni računar"), + ("Confirm Delete", "Potvrdite brisanje"), + ("Delete", "Brisanje"), + ("Properties", "Osobine"), + ("Multi Select", "Višestruko selektovanje"), + ("Select All", "Selektuj sve"), + ("Unselect All", "Deselektuj sve"), + ("Empty Directory", "Prazan direktorijum"), + ("Not an empty directory", "Nije prazan direktorijum"), + ("Are you sure you want to delete this file?", "Da li ste sigurni da želite da obrišete ovu datoteku?"), + ("Are you sure you want to delete this empty directory?", "Da li ste sigurni da želite da obrišete ovaj prazan direktorijum?"), + ("Are you sure you want to delete the file of this directory?", "Da li ste sigurni da želite da obrišete datoteku ovog direktorijuma?"), + ("Do this for all conflicts", "Uradi ovo za sve konflikte"), + ("This is irreversible!", "Ovo je nepovratno"), + ("Deleting", "Brisanje"), + ("files", "datoteke"), + ("Waiting", "Čekanje"), + ("Finished", "Završeno"), + ("Speed", "Brzina"), + ("Custom Image Quality", "Korisnički kvalitet slike"), + ("Privacy mode", "Mod privatnosti"), + ("Block user input", "Blokiraj korisnikov unos"), + ("Unblock user input", "Odblokiraj korisnikov unos"), + ("Adjust Window", "Podesi prozor"), + ("Original", "Original"), + ("Shrink", "Skupi"), + ("Stretch", "Raširi"), + ("Scrollbar", "Skrol linija"), + ("ScrollAuto", "Auto skrol"), + ("Good image quality", "Dobar kvalitet slike"), + ("Balanced", "Balansirano"), + ("Optimize reaction time", "Optimizuj vreme reakcije"), + ("Custom", "Korisnički"), + ("Show remote cursor", "Prikaži udaljeni kursor"), + ("Show quality monitor", "Prikaži monitor kvaliteta"), + ("Disable clipboard", "Zabrani clipboard"), + ("Lock after session end", "Zaključaj po završetku sesije"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del umetanje"), + ("Insert Lock", "Zaključaj umetanje"), + ("Refresh", "Osveži"), + ("ID does not exist", "ID ne postoji"), + ("Failed to connect to rendezvous server", "Greška u spajanju na server za povezivanje"), + ("Please try later", "Molimo pokušajte kasnije"), + ("Remote desktop is offline", "Udaljeni ekran je isključen"), + ("Key mismatch", "Pogrešan ključ"), + ("Timeout", "Isteklo vreme"), + ("Failed to connect to relay server", "Greška u spajanju na posredni server"), + ("Failed to connect via rendezvous server", "Greška u spajanju preko servera za povezivanje"), + ("Failed to connect via relay server", "Greška u spajanju preko posrednog servera"), + ("Failed to make direct connection to remote desktop", "Greška u direktnom spajanju na udaljenu radnu površinu"), + ("Set Password", "Postavi lozinku"), + ("OS Password", "OS lozinka"), + ("install_tip", "Zbog UAC RustDesk ne može raditi pravilno u nekim slučajevima. Da biste prevazišli UAC, kliknite taster ispod da instalirate RustDesk na sistem."), + ("Click to upgrade", "Klik za nadogradnju"), + ("Configure", "Konfigurisanje"), + ("config_acc", "Da biste daljinski kontrolisali radnu površinu, RustDesk-u treba da dodelite \"Accessibility\" prava."), + ("config_screen", "Da biste daljinski pristupili radnoj površini, RustDesk-u treba da dodelite \"Screen Recording\" prava."), + ("Installing ...", "Instaliranje..."), + ("Install", "Instaliraj"), + ("Installation", "Instalacija"), + ("Installation Path", "Putanja za instalaciju"), + ("Create start menu shortcuts", "Kreiraj prečice u meniju"), + ("Create desktop icon", "Kreiraj ikonicu na radnoj površini"), + ("agreement_tip", "Pokretanjem instalacije prihvatate ugovor o licenciranju."), + ("Accept and Install", "Prihvati i instaliraj"), + ("End-user license agreement", "Ugovor sa krajnjim korisnikom"), + ("Generating ...", "Generisanje..."), + ("Your installation is lower version.", "Vaša instalacija je niže verzije"), + ("not_close_tcp_tip", "Ne zatvarajte ovaj prozor dok koristite tunel"), + ("Listening ...", "Na slušanju..."), + ("Remote Host", "Adresa udaljenog uređaja"), + ("Remote Port", "Udaljeni port"), + ("Action", "Akcija"), + ("Add", "Dodaj"), + ("Local Port", "Lokalni port"), + ("Local Address", "Lokalna adresa"), + ("Change Local Port", "Promeni lokalni port"), + ("setup_server_tip", "Za brže spajanje, molimo da koristite svoj server"), + ("Too short, at least 6 characters.", "Prekratko, najmanje 6 znakova."), + ("The confirmation is not identical.", "Potvrda nije identična"), + ("Permissions", "Dozvole"), + ("Accept", "Prihvati"), + ("Dismiss", "Odbaci"), + ("Disconnect", "Raskini konekciju"), + ("Enable file copy and paste", "Dozvoli kopiranje i lepljenje fajlova"), + ("Connected", "Spojeno"), + ("Direct and encrypted connection", "Direktna i kriptovana konekcija"), + ("Relayed and encrypted connection", "Posredna i kriptovana konekcija"), + ("Direct and unencrypted connection", "Direktna i nekriptovana konekcija"), + ("Relayed and unencrypted connection", "Posredna i nekriptovana konekcija"), + ("Enter Remote ID", "Unesite ID udaljenog uređaja"), + ("Enter your password", "Unesite svoju lozinku"), + ("Logging in...", "Prijava..."), + ("Enable RDP session sharing", "Dozvoli deljenje RDP sesije"), + ("Auto Login", "Auto prijavljivanje (Važeće samo ako ste postavili \"Lock after session end\")"), + ("Enable direct IP access", "Dozvoli direktan pristup preko IP"), + ("Rename", "Preimenuj"), + ("Space", "Prazno"), + ("Create desktop shortcut", "Kreiraj prečicu na radnoj površini"), + ("Change Path", "Promeni putanju"), + ("Create Folder", "Kreiraj direktorijum"), + ("Please enter the folder name", "Unesite ime direktorijuma"), + ("Fix it", "Popravi ga"), + ("Warning", "Upozorenje"), + ("Login screen using Wayland is not supported", "Ekran za prijavu koji koristi Wayland nije podržan"), + ("Reboot required", "Potreban je restart"), + ("Unsupported display server", "Nepodržan server za prikaz"), + ("x11 expected", "x11 očekivan"), + ("Port", "Port"), + ("Settings", "Postavke"), + ("Username", "Korisničko ime"), + ("Invalid port", "Pogrešan port"), + ("Closed manually by the peer", "Klijent ručno raskinuo konekciju"), + ("Enable remote configuration modification", "Dozvoli modifikaciju udaljene konfiguracije"), + ("Run without install", "Pokreni bez instalacije"), + ("Connect via relay", ""), + ("Always connect via relay", "Uvek se spoj preko posrednika"), + ("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"), + ("Login", "Prijava"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), + ("Logout", "Odjava"), + ("Tags", "Oznake"), + ("Search ID", "Traži ID"), + ("whitelist_sep", "Odvojeno zarezima, tačka zarezima, praznim mestima ili novim redovima"), + ("Add ID", "Dodaj ID"), + ("Add Tag", "Dodaj oznaku"), + ("Unselect all tags", "Odselektuj sve oznake"), + ("Network error", "Greška na mreži"), + ("Username missed", "Korisničko ime promašeno"), + ("Password missed", "Lozinka promašena"), + ("Wrong credentials", "Pogrešno korisničko ime ili lozinka"), + ("The verification code is incorrect or has expired", ""), + ("Edit Tag", "Izmeni oznaku"), + ("Forget Password", "Zaboravi lozinku"), + ("Favorites", "Favoriti"), + ("Add to Favorites", "Dodaj u favorite"), + ("Remove from Favorites", "Izbaci iz favorita"), + ("Empty", "Prazno"), + ("Invalid folder name", "Pogrešno ime direktorijuma"), + ("Socks5 Proxy", "Socks5 proksi"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) proksi"), + ("Discovered", "Otkriveno"), + ("install_daemon_tip", "Za pokretanje pri startu sistema, treba da instalirate sistemski servis."), + ("Remote ID", "Udaljeni ID"), + ("Paste", "Nalepi"), + ("Paste here?", "Nalepi ovde?"), + ("Are you sure to close the connection?", "Da li ste sigurni da želite da zatvorite konekciju?"), + ("Download new version", "Preuzmi novu verziju"), + ("Touch mode", "Mod na dodir"), + ("Mouse mode", "Miš mod"), + ("One-Finger Tap", "Pritisak jednim prstom"), + ("Left Mouse", "Levi miš"), + ("One-Long Tap", "Dugi pritisak"), + ("Two-Finger Tap", "Pritisak sa dva prsta"), + ("Right Mouse", "Desni miš"), + ("One-Finger Move", "Pomeranje jednim prstom"), + ("Double Tap & Move", "Dupli pritisak i pomeranje"), + ("Mouse Drag", "Prevlačenje mišem"), + ("Three-Finger vertically", "Sa tri prsta vertikalno"), + ("Mouse Wheel", "Točkić miša"), + ("Two-Finger Move", "Pomeranje sa dva prsta"), + ("Canvas Move", "Pomeranje pozadine"), + ("Pinch to Zoom", "Stisnite za zumiranje"), + ("Canvas Zoom", "Zumiranje pozadine"), + ("Reset canvas", "Resetuj pozadinu"), + ("No permission of file transfer", "Nemate pravo prenosa datoteka"), + ("Note", "Primedba"), + ("Connection", "Konekcija"), + ("Share screen", "Podeli ekran"), + ("Chat", "Dopisivanje"), + ("Total", "Ukupno"), + ("items", "stavki"), + ("Selected", "Izabrano"), + ("Screen Capture", "Snimanje ekrana"), + ("Input Control", "Kontrola unosa"), + ("Audio Capture", "Snimanje zvuka"), + ("Do you accept?", "Prihvatate?"), + ("Open System Setting", "Postavke otvorenog sistema"), + ("How to get Android input permission?", "Kako dobiti pristup za Android unos?"), + ("android_input_permission_tip1", "Da bi daljinski uređaj kontrolisao vaš Android uređaj preko miša ili na dodir, treba da dozvolite RustDesk-u da koristi \"Accessibility\" servis."), + ("android_input_permission_tip2", "Molimo pređite na sledeću stranicu sistemskih podešavanja, pronađite i unesite [Installed Services], uključite [RustDesk Input] servis."), + ("android_new_connection_tip", "Primljen je novi zahtev za upravljanje, koji želi da upravlja ovim vašim uređajem."), + ("android_service_will_start_tip", "Uključenje \"Screen Capture\" automatski će pokrenuti servis, dozvoljavajući drugim uređajima da zahtevaju spajanje na vaš uređaj."), + ("android_stop_service_tip", "Zatvaranje servisa automatski će zatvoriti sve uspostavljene konekcije."), + ("android_version_audio_tip", "Tekuća Android verzija ne podržava audio snimanje, molimo nadogradite na Android 10 ili veći."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), + ("Account", "Nalog"), + ("Overwrite", "Prepiši preko"), + ("This file exists, skip or overwrite this file?", "Ova datoteka postoji, preskoči ili prepiši preko?"), + ("Quit", "Izlaz"), + ("Help", "Pomoć"), + ("Failed", "Greška"), + ("Succeeded", "Uspešno"), + ("Someone turns on privacy mode, exit", "Neko je uključio mod privatnosti, izlaz."), + ("Unsupported", "Nepodržano"), + ("Peer denied", "Klijent zabranjen"), + ("Please install plugins", "Molimo instalirajte dodatke"), + ("Peer exit", "Klijent izašao"), + ("Failed to turn off", "Greška kod isključenja"), + ("Turned off", "Isključeno"), + ("Language", "Jezik"), + ("Keep RustDesk background service", "Zadrži RustDesk kao pozadinski servis"), + ("Ignore Battery Optimizations", "Zanemari optimizacije baterije"), + ("android_open_battery_optimizations_tip", "Ako želite da onemogućite ovu funkciju, molimo idite na sledeću stranicu za podešavanje RustDesk aplikacije, pronađite i uđite u [Battery], isključite [Unrestricted]"), + ("Start on boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), + ("Connection not allowed", "Konekcija nije dozvoljena"), + ("Legacy mode", "Zastareli mod"), + ("Map mode", "Mod mapiranja"), + ("Translate mode", "Mod prevođenja"), + ("Use permanent password", "Koristi trajnu lozinku"), + ("Use both passwords", "Koristi obe lozinke"), + ("Set permanent password", "Postavi trajnu lozinku"), + ("Enable remote restart", "Omogući daljinsko restartovanje"), + ("Restart remote device", "Restartuj daljinski uređaj"), + ("Are you sure you want to restart", "Da li ste sigurni da želite restart"), + ("Restarting remote device", "Restartovanje daljinskog uređaja"), + ("remote_restarting_tip", "Udaljeni uređaj se restartuje, molimo zatvorite ovu poruku i ponovo se kasnije povežite trajnom šifrom"), + ("Copied", "Kopirano"), + ("Exit Fullscreen", "Napusti mod celog ekrana"), + ("Fullscreen", "Mod celog ekrana"), + ("Mobile Actions", "Mobilne akcije"), + ("Select Monitor", "Izbor monitora"), + ("Control Actions", "Upravljačke akcije"), + ("Display Settings", "Postavke prikaza"), + ("Ratio", "Odnos"), + ("Image Quality", "Kvalitet slike"), + ("Scroll Style", "Stil skrolovanja"), + ("Show Toolbar", ""), + ("Hide Toolbar", ""), + ("Direct Connection", "Direktna konekcija"), + ("Relay Connection", "Posredna konekcija"), + ("Secure Connection", "Bezbedna konekcija"), + ("Insecure Connection", "Nebezbedna konekcija"), + ("Scale original", "Skaliraj original"), + ("Scale adaptive", "Adaptivno skaliranje"), + ("General", "Uopšteno"), + ("Security", "Bezbednost"), + ("Theme", "Tema"), + ("Dark Theme", "Tamna tema"), + ("Light Theme", ""), + ("Dark", "Tamno"), + ("Light", "Svetlo"), + ("Follow System", "Prema sistemu"), + ("Enable hardware codec", "Omogući hardverski kodek"), + ("Unlock Security Settings", "Otključaj postavke bezbednosti"), + ("Enable audio", "Dozvoli zvuk"), + ("Unlock Network Settings", "Otključaj postavke mreže"), + ("Server", "Server"), + ("Direct IP Access", "Direktan IP pristup"), + ("Proxy", "Proksi"), + ("Apply", "Primeni"), + ("Disconnect all devices?", "Otkači sve uređaju?"), + ("Clear", "Obriši"), + ("Audio Input Device", "Uređaj za ulaz zvuka"), + ("Use IP Whitelisting", "Koristi listu pouzdanih IP"), + ("Network", "Mreža"), + ("Pin Toolbar", ""), + ("Unpin Toolbar", ""), + ("Recording", "Snimanje"), + ("Directory", "Direktorijum"), + ("Automatically record incoming sessions", "Automatski snimaj dolazne sesije"), + ("Automatically record outgoing sessions", ""), + ("Change", "Promeni"), + ("Start session recording", "Započni snimanje sesije"), + ("Stop session recording", "Zaustavi snimanje sesije"), + ("Enable recording session", "Omogući snimanje sesije"), + ("Enable LAN discovery", "Omogući LAN otkrivanje"), + ("Deny LAN discovery", "Zabrani LAN otkrivanje"), + ("Write a message", "Napiši poruku"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Molimo sačekajte UAC potvrdu..."), + ("elevated_foreground_window_tip", "Tekući prozor udaljene radne površine zahteva veću privilegiju za rad, tako da trenutno nije moguće koristiti miša i tastaturu. Možete zahtevati od udaljenog korisnika da minimizira aktivni prozor, ili kliknuti na taster za podizanje privilegija u prozoru za rad sa konekcijom. Da biste prevazišli ovaj problem, preporučljivo je da instalirate softver na udaljeni uređaj."), + ("Disconnected", "Odspojeno"), + ("Other", "Ostalo"), + ("Confirm before closing multiple tabs", "Potvrda pre zatvaranja više kartica"), + ("Keyboard Settings", "Postavke tastature"), + ("Full Access", "Pun pristup"), + ("Screen Share", "Deljenje ekrana"), + ("ubuntu-21-04-required", "Wayland zahteva Ubuntu 21.04 ili veću verziju"), + ("wayland-requires-higher-linux-version", "Wayland zahteva veću verziju Linux distribucije. Molimo pokušajte X11 ili promenite OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Vidi"), + ("Please Select the screen to be shared(Operate on the peer side).", "Molimo izaberite ekran koji će biti podeljen (Za rad na klijent strani)"), + ("Show RustDesk", "Prikazi RustDesk"), + ("This PC", "Ovaj PC"), + ("or", "ili"), + ("Elevate", "Izdigni"), + ("Zoom cursor", "Zumiraj kursor"), + ("Accept sessions via password", "Prihvati sesije preko lozinke"), + ("Accept sessions via click", "Prihvati sesije preko klika"), + ("Accept sessions via both", "Prihvati sesije preko oboje"), + ("Please wait for the remote side to accept your session request...", "Molimo sačekajte da udaljena strana prihvati vaš zahtev za sesijom..."), + ("One-time Password", "Jednokratna lozinka"), + ("Use one-time password", "Koristi jednokratnu lozinku"), + ("One-time password length", "Dužina jednokratne lozinke"), + ("Request access to your device", "Zahtev za pristup vašem uređaju"), + ("Hide connection management window", "Sakrij prozor za uređivanje konekcije"), + ("hide_cm_tip", "Skrivanje dozvoljeno samo prihvatanjem sesije preko lozinke i korišćenjem trajne lozinke"), + ("wayland_experiment_tip", "Wayland eksperiment savet"), + ("Right click to select tabs", "Desni klik za izbor kartica"), + ("Skipped", ""), + ("Add to address book", "Dodaj u adresar"), + ("Group", "Grupa"), + ("Search", "Pretraga"), + ("Closed manually by web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), + ("Select a peer", ""), + ("Select peers", ""), + ("Plugins", ""), + ("Uninstall", ""), + ("Update", ""), + ("Enable", ""), + ("Disable", ""), + ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), + ("Collapse toolbar", ""), + ("Accept and Elevate", ""), + ("accept_and_elevate_btn_tooltip", ""), + ("clipboard_wait_response_timeout_tip", ""), + ("Incoming connection", ""), + ("Outgoing connection", ""), + ("Exit", ""), + ("Open", ""), + ("logout_tip", ""), + ("Service", ""), + ("Start", ""), + ("Stop", ""), + ("exceed_max_devices", ""), + ("Sync with recent sessions", ""), + ("Sort tags", ""), + ("Open connection in new tab", ""), + ("Move tab to new window", ""), + ("Can not be empty", ""), + ("Already exists", ""), + ("Change Password", ""), + ("Refresh Password", ""), + ("ID", ""), + ("Grid View", ""), + ("List View", ""), + ("Select", ""), + ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), + ("synced_peer_readded_tip", ""), + ("Change Color", ""), + ("Primary Color", ""), + ("HSV Color", ""), + ("Installation Successful!", ""), + ("Installation failed!", ""), + ("Reverse mouse wheel", ""), + ("{} sessions", ""), + ("scam_title", ""), + ("scam_text1", ""), + ("scam_text2", ""), + ("Don't show again", ""), + ("I Agree", ""), + ("Decline", ""), + ("Timeout in minutes", ""), + ("auto_disconnect_option_tip", ""), + ("Connection failed due to inactivity", ""), + ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), + ("pull_group_failed_tip", ""), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), + ("idd_not_support_under_win10_2004_tip", ""), + ("input_source_1_tip", ""), + ("input_source_2_tip", ""), + ("Swap control-command key", ""), + ("swap-left-right-mouse", ""), + ("2FA code", ""), + ("More", ""), + ("enable-2fa-title", ""), + ("enable-2fa-desc", ""), + ("wrong-2fa-code", ""), + ("enter-2fa-title", ""), + ("Email verification code must be 6 characters.", ""), + ("2FA code must be 6 digits.", ""), + ("Multiple Windows sessions found", ""), + ("Please select the session you want to connect to", ""), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("share_warning_tip", ""), + ("Everyone", ""), + ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", ""), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", ""), + ("Floating window", ""), + ("floating_window_tip", ""), + ("Keep screen on", ""), + ("Never", ""), + ("During controlled", ""), + ("During service is on", ""), + ("Capture screen using DirectX", ""), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Pregled kamere"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Nastavi sa {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/sv.rs b/vendor/rustdesk/src/lang/sv.rs new file mode 100644 index 0000000..eda7851 --- /dev/null +++ b/vendor/rustdesk/src/lang/sv.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Ditt skrivbord"), + ("desk_tip", "Ditt skrivbord kan delas med hjälp av detta ID och lösenord"), + ("Password", "Lösenord"), + ("Ready", "Redo"), + ("Established", "Uppkopplad"), + ("connecting_status", "Ansluter till RustDesk..."), + ("Enable service", "Sätt på tjänsten"), + ("Start service", "Starta tjänsten"), + ("Service is running", "Tjänsten är startad"), + ("Service is not running", "Tjänsten är ej startad"), + ("not_ready_status", "Ej redo. Kontrollera din nätverksanslutning"), + ("Control Remote Desktop", "Kontrollera fjärrskrivbord"), + ("Transfer file", "Överför fil"), + ("Connect", "Anslut"), + ("Recent sessions", "Dina senaste sessioner"), + ("Address book", "Addressbok"), + ("Confirmation", "Bekräftelse"), + ("TCP tunneling", "TCP Tunnel"), + ("Remove", "Ta bort"), + ("Refresh random password", "Skapa nytt slumpmässigt lösenord"), + ("Set your own password", "Skapa ditt eget lösenord"), + ("Enable keyboard/mouse", "Tillåt tangentbord/mus"), + ("Enable clipboard", "Tillåt urklipp"), + ("Enable file transfer", "Tillåt filöverföring"), + ("Enable TCP tunneling", "Tillåt TCP tunnel"), + ("IP Whitelisting", "IP Vitlisting"), + ("ID/Relay Server", "ID/Relay Server"), + ("Import server config", "Importera Server config"), + ("Export Server Config", "Exportera Server config"), + ("Import server configuration successfully", "Importering lyckades"), + ("Export server configuration successfully", "Exportering lyckades"), + ("Invalid server configuration", "Ogiltig server config"), + ("Clipboard is empty", "Urklippet är tomt"), + ("Stop service", "Avsluta tjänsten"), + ("Change ID", "Byt ID"), + ("Your new ID", "Ditt nya ID"), + ("length %min% to %max%", "längd %min% till %max%"), + ("starts with a letter", "börjar med en bokstav"), + ("allowed characters", "tillåtna tecken"), + ("id_change_tip", "Bara a-z, A-Z, 0-9, - (dash) och _ (understräck) tecken är tillåtna. Den första bokstaven måste vara a-z, A-Z. Längd mellan 6 och 16."), + ("Website", "Hemsida"), + ("About", "Om"), + ("Slogan_tip", ""), + ("Privacy Statement", "Integritetspolicy"), + ("Mute", "Tyst"), + ("Build Date", ""), + ("Version", "Version"), + ("Home", "Hem"), + ("Audio Input", "Ljud input"), + ("Enhancements", "Förbättringar"), + ("Hardware Codec", "Hårdvarucodec"), + ("Adaptive bitrate", "Adaptiv Bitrate"), + ("ID Server", "ID server"), + ("Relay Server", "Relay Server"), + ("API Server", "API Server"), + ("invalid_http", "måste börja med http:// eller https://"), + ("Invalid IP", "Ogiltig IP"), + ("Invalid format", "Ogiltigt format"), + ("server_not_support", "Stöds ännu inte av servern"), + ("Not available", "Ej tillgänglig"), + ("Too frequent", "För ofta"), + ("Cancel", "Avbryt"), + ("Skip", "Hoppa över"), + ("Close", "Stäng"), + ("Retry", "Försök igen"), + ("OK", "OK"), + ("Password Required", "Lösenord krävs"), + ("Please enter your password", "Skriv in ditt lösenord"), + ("Remember password", "Kom ihåg lösenord"), + ("Wrong Password", "Fel lösenord"), + ("Do you want to enter again?", "Vill du skriva in igen?"), + ("Connection Error", "Anslutningsfel"), + ("Error", "Ett fel uppstod"), + ("Reset by the peer", "Återställt av klienten"), + ("Connecting...", "Ansluter..."), + ("Connection in progress. Please wait.", "Anslutning pågår. Var god vänta."), + ("Please try 1 minute later", "Försök igen om en minut"), + ("Login Error", "Inloggningsfel"), + ("Successful", "Lyckat"), + ("Connected, waiting for image...", "Ansluten, väntar på bild..."), + ("Name", "Namn"), + ("Type", "Typ"), + ("Modified", "Modifierad"), + ("Size", "Storlek"), + ("Show Hidden Files", "Visa gömda filer"), + ("Receive", "Ta emot"), + ("Send", "Skicka"), + ("Refresh File", "Uppdatera fil"), + ("Local", "Lokalt"), + ("Remote", "Fjärr"), + ("Remote Computer", "Fjärrdator"), + ("Local Computer", "Lokal dator"), + ("Confirm Delete", "Bekräfta borttagning"), + ("Delete", "Ta bort"), + ("Properties", "Egenskaper"), + ("Multi Select", "Välj flera"), + ("Select All", "Markera alla "), + ("Unselect All", "Avmärkera alla"), + ("Empty Directory", "Tom mapp"), + ("Not an empty directory", "Inte en tom mapp"), + ("Are you sure you want to delete this file?", "Är du säker att du vill ta bort filen?"), + ("Are you sure you want to delete this empty directory?", "Är du säker att du vill ta bort den tomma mappen?"), + ("Are you sure you want to delete the file of this directory?", "Är du säker att du vill ta bort filen ur mappen?"), + ("Do this for all conflicts", "Gör för alla konflikter"), + ("This is irreversible!", "Detta går ej att ångra!"), + ("Deleting", "Tar bort"), + ("files", "filer"), + ("Waiting", "Väntnar"), + ("Finished", "Klar"), + ("Speed", "Hastighet"), + ("Custom Image Quality", "Anpassad bildkvalitet"), + ("Privacy mode", "Säkerhetsläge"), + ("Block user input", "Blokera användarinput"), + ("Unblock user input", "Tillåt användarinput"), + ("Adjust Window", "Ändra fönster"), + ("Original", "Orginal"), + ("Shrink", "Krymp"), + ("Stretch", "Sträck ut"), + ("Scrollbar", "Scrollbar"), + ("ScrollAuto", "ScrollAuto"), + ("Good image quality", "Bra bildkvalitet"), + ("Balanced", "Balanserad"), + ("Optimize reaction time", "Optimera reaktionstid"), + ("Custom", "Anpassat"), + ("Show remote cursor", "Visa fjärrmus"), + ("Show quality monitor", "Visa bildkvalitet"), + ("Disable clipboard", "Stäng av urklipp"), + ("Lock after session end", "Lås efter sessionens slut"), + ("Insert Ctrl + Alt + Del", "Insert Ctrl + Alt + Del"), + ("Insert Lock", "Insert lås"), + ("Refresh", "Uppdatera"), + ("ID does not exist", "Detta ID existerar inte"), + ("Failed to connect to rendezvous server", "Lyckades inte ansluta till randezvous servern"), + ("Please try later", "Försök igen senare"), + ("Remote desktop is offline", "Fjärrskrivbordet är offline"), + ("Key mismatch", "Nyckeln stämmer inte"), + ("Timeout", "Timeout"), + ("Failed to connect to relay server", "Lyckades inte ansluta till relay servern"), + ("Failed to connect via rendezvous server", "Lyckades inte ansluta via randezvous servern"), + ("Failed to connect via relay server", "Lyckades inte ansluta via relay servern"), + ("Failed to make direct connection to remote desktop", "Lyckades inte ansluta direkt till fjärrskrivbordet"), + ("Set Password", "Välj lösenord"), + ("OS Password", "OS lösenord"), + ("install_tip", "På grund av UAC, kan inte RustDesk fungera ordentligt på klientsidan. För att undvika problem med UAC, tryck på knappen nedan för att installera RustDesk på systemet."), + ("Click to upgrade", "Klicka för att nedgradera"), + ("Configure", "Konfigurera"), + ("config_acc", "För att kontrollera din dator på distans måste du ge RustDesk \"Tillgänglighets\" rättigheter."), + ("config_screen", "För att kontrollera din dator på distans måste du ge RustDesk \"Skärminspelnings\" rättigheter."), + ("Installing ...", "Installerar..."), + ("Install", "Installera"), + ("Installation", "Installation"), + ("Installation Path", "Installationsplats"), + ("Create start menu shortcuts", "Skapa startmeny genväg"), + ("Create desktop icon", "Skapa ikon på skrivbordet"), + ("agreement_tip", "Genom att starta installationen accepterar du licensavtalet."), + ("Accept and Install", "Acceptera och installera"), + ("End-user license agreement", "End-user license agreement"), + ("Generating ...", "Genererar..."), + ("Your installation is lower version.", "Ditt skrivbord har en lägre version"), + ("not_close_tcp_tip", "Stäng inde detta fönster när du använder tunneln"), + ("Listening ...", "Lyssnar..."), + ("Remote Host", "Fjärrhost"), + ("Remote Port", "Fjärrport"), + ("Action", "Handling"), + ("Add", "Lägg till"), + ("Local Port", "Lokal port"), + ("Local Address", "Lokal adress"), + ("Change Local Port", "Ändra lokal port"), + ("setup_server_tip", "Sätt upp din egen server för en snabbare anslutning"), + ("Too short, at least 6 characters.", "För kort, minst 6 tecken."), + ("The confirmation is not identical.", "Bekräftelsen stämmer inte."), + ("Permissions", "Rättigheter"), + ("Accept", "Acceptera"), + ("Dismiss", "Tillåt inte"), + ("Disconnect", "Koppla ifrån"), + ("Enable file copy and paste", "Tillåt kopiering av filer"), + ("Connected", "Ansluten"), + ("Direct and encrypted connection", "Direkt och krypterad anslutning"), + ("Relayed and encrypted connection", "Vidarebefodrad och krypterad anslutning"), + ("Direct and unencrypted connection", "Direkt och okrypterad anslutning"), + ("Relayed and unencrypted connection", "Vidarebefodrad och okrypterad anslutning"), + ("Enter Remote ID", "Skriv in fjärr-ID"), + ("Enter your password", "Skriv in ditt lösenord"), + ("Logging in...", "Loggar in..."), + ("Enable RDP session sharing", "Tillåt RDP sessionsdelning"), + ("Auto Login", "Auto Login (Bara giltigt om du sätter \"Lås efter sessionens slut\")"), + ("Enable direct IP access", "Tillåt direkt IP anslutningar"), + ("Rename", "Byt namn"), + ("Space", "Mellanslag"), + ("Create desktop shortcut", "Skapa skrivbordsgenväg"), + ("Change Path", "Ändra plats"), + ("Create Folder", "Skapa mapp"), + ("Please enter the folder name", "Skriv in namnet på mappen"), + ("Fix it", "Fixa det"), + ("Warning", "Varning"), + ("Login screen using Wayland is not supported", "Login med Wayland stöds inte"), + ("Reboot required", "Omstart krävs"), + ("Unsupported display server", "Displayserver stöds inte "), + ("x11 expected", "x11 förväntades"), + ("Port", "Port"), + ("Settings", "Inställningar"), + ("Username", "Användarnamn"), + ("Invalid port", "Ogiltig port"), + ("Closed manually by the peer", "Stängd manuellt av klienten"), + ("Enable remote configuration modification", "Tillåt fjärrkonfigurering"), + ("Run without install", "Kör utan installation"), + ("Connect via relay", "Anslut via relay"), + ("Always connect via relay", "Anslut alltid via relay"), + ("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"), + ("Login", "Logga in"), + ("Verify", "Verifiera"), + ("Remember me", "Kom ihåg mig"), + ("Trust this device", "Lita på denna enhet"), + ("Verification code", "Verifikationskod"), + ("verification_tip", "verifikation_tips"), + ("Logout", "Logga ut"), + ("Tags", "Taggar"), + ("Search ID", "Sök ID"), + ("whitelist_sep", "Separerat av ett comma, semikolon, mellanslag eller ny linje"), + ("Add ID", "Lägg till ID"), + ("Add Tag", "Lägg till Tagg"), + ("Unselect all tags", "Avmarkera alla taggar"), + ("Network error", "Nätverksfel"), + ("Username missed", "Användarnamn saknas"), + ("Password missed", "Lösenord saknas"), + ("Wrong credentials", "Fel användarnamn eller lösenord"), + ("The verification code is incorrect or has expired", "Verifikationskoden är felaktig eller har löpt ut"), + ("Edit Tag", "Ändra Tagg"), + ("Forget Password", "Glöm lösenord"), + ("Favorites", "Favoriter"), + ("Add to Favorites", "Lägg till favorit"), + ("Remove from Favorites", "Ta bort från favoriter"), + ("Empty", "Tom"), + ("Invalid folder name", "Ogiltigt mappnamn"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "Upptäckt"), + ("install_daemon_tip", "För att starta efter boot måste du installera systemtjänsten."), + ("Remote ID", "Fjärr ID"), + ("Paste", "Klistra in"), + ("Paste here?", "Klistra in här?"), + ("Are you sure to close the connection?", "Är du säker att du vill avsluta anslutningen?"), + ("Download new version", "Ladda ner ny version"), + ("Touch mode", "Touchläge"), + ("Mouse mode", "Musläge"), + ("One-Finger Tap", "En fingers tryck"), + ("Left Mouse", "Vänster mus"), + ("One-Long Tap", "Långt tryck"), + ("Two-Finger Tap", "Långt tryck med två fingrar"), + ("Right Mouse", "Höger mus"), + ("One-Finger Move", "En fingers drag"), + ("Double Tap & Move", "Dubbeltryck och flytta"), + ("Mouse Drag", "Dra med musen"), + ("Three-Finger vertically", "Tre fingrar vertikalt"), + ("Mouse Wheel", "Scrollhjul"), + ("Two-Finger Move", "Två fingers flytt"), + ("Canvas Move", "Flytta canvas"), + ("Pinch to Zoom", "Nyp för zoom"), + ("Canvas Zoom", "Canvas zoom"), + ("Reset canvas", "Återställ canvas"), + ("No permission of file transfer", "Rättigheter saknas"), + ("Note", "Notering"), + ("Connection", "Anslutning"), + ("Share screen", "Dela skärm"), + ("Chat", "Chatt"), + ("Total", "Totalt"), + ("items", "föremål"), + ("Selected", "Valda"), + ("Screen Capture", "Skärminspelning"), + ("Input Control", "Inputkontroll"), + ("Audio Capture", "Ljudinspelning"), + ("Do you accept?", "Accepterar du?"), + ("Open System Setting", "Öppna systeminställnig"), + ("How to get Android input permission?", "Hur får man Android rättigheter?"), + ("android_input_permission_tip1", "Android rättigheter saknas"), + ("android_input_permission_tip2", "Gå till systeminställningarna, hitta [Installed Services], sätt på [RustDesk Input] tjänsten."), + ("android_new_connection_tip", "Ny kontrollförfrågan mottagen, denna vill kontrollera din enhet."), + ("android_service_will_start_tip", "Sätter du på \"skärminspelning\" kommer tjänsten automatiskt att starta. Detta tillåter andra enheter att kontrollera din enhet."), + ("android_stop_service_tip", "Genom att stänga av tjänsten kommer alla enheter att kopplas ifrån."), + ("android_version_audio_tip", "Din version av Android stödjer inte ljudinspelning, Android 10 eller nyare krävs"), + ("android_start_service_tip", "android_start_service_tips"), + ("android_permission_may_not_change_tip", ""), + ("Account", "Konto"), + ("Overwrite", "Skriv över"), + ("This file exists, skip or overwrite this file?", "Filen finns redan, hoppa över eller skriv över filen?"), + ("Quit", "Avsluta"), + ("Help", "Hjälp"), + ("Failed", "Misslyckades"), + ("Succeeded", "Lyckades"), + ("Someone turns on privacy mode, exit", "Någon sätter på säkerhetesläge, avsluta"), + ("Unsupported", "Stöds inte"), + ("Peer denied", "Klienten nekade"), + ("Please install plugins", "Var god installera plugins"), + ("Peer exit", "Avsluta klient"), + ("Failed to turn off", "Misslyckades med avstängning"), + ("Turned off", "Avstängd"), + ("Language", "Språk"), + ("Keep RustDesk background service", "Behåll RustDesk i bakgrunden"), + ("Ignore Battery Optimizations", "Ignorera batterioptimering"), + ("android_open_battery_optimizations_tip", "Om du vill stänga av denna funktion, gå till nästa RustDesk programs inställningar, hitta [Batteri], Checka ur [Obegränsad]"), + ("Start on boot", "Starta vid uppstart"), + ("Start the screen sharing service on boot, requires special permissions", "Starta skärmdelningstjänsten vid uppstart, kräver särskilda rättigheter"), + ("Connection not allowed", "Anslutning ej tillåten"), + ("Legacy mode", "Legacy mode"), + ("Map mode", "Kartläge"), + ("Translate mode", "Översättningsläge"), + ("Use permanent password", "Använd permanent lösenord"), + ("Use both passwords", "Använd båda lösenorden"), + ("Set permanent password", "Ställ in permanent lösenord"), + ("Enable remote restart", "Sätt på fjärromstart"), + ("Restart remote device", "Starta om fjärrenheten"), + ("Are you sure you want to restart", "Är du säker att du vill starta om?"), + ("Restarting remote device", "Startar om fjärrenheten"), + ("remote_restarting_tip", "Enheten startar om, stäng detta meddelande och anslut igen om en liten stund"), + ("Copied", "Kopierad"), + ("Exit Fullscreen", "Gå ur fullskärmsläge"), + ("Fullscreen", "Fullskärm"), + ("Mobile Actions", "Mobila återgärder"), + ("Select Monitor", "Välj skärm"), + ("Control Actions", "Kontroller"), + ("Display Settings", "Skärminställningar"), + ("Ratio", "Ratio"), + ("Image Quality", "Bildkvalitet"), + ("Scroll Style", "Scrollstil"), + ("Show Toolbar", "Visa verktygsfältet"), + ("Hide Toolbar", "Dölj verktygsfältet"), + ("Direct Connection", "Direktanslutning"), + ("Relay Connection", "Relayanslutning"), + ("Secure Connection", "Säker anslutning"), + ("Insecure Connection", "Osäker anslutning"), + ("Scale original", "Skala orginal"), + ("Scale adaptive", "Skala adaptivt"), + ("General", "Generellt"), + ("Security", "Säkerhet"), + ("Theme", "Tema"), + ("Dark Theme", "Mörkt tema"), + ("Light Theme", "Ljust tema"), + ("Dark", "Mörk"), + ("Light", "Ljus"), + ("Follow System", "Följ system"), + ("Enable hardware codec", "Aktivera hårdvarucodec"), + ("Unlock Security Settings", "Lås upp säkerhetsinställningar"), + ("Enable audio", "Sätt på ljud"), + ("Unlock Network Settings", "Lås upp nätverksinställningar"), + ("Server", "Server"), + ("Direct IP Access", "Direkt IP åtkomst"), + ("Proxy", "Proxy"), + ("Apply", "Tillämpa"), + ("Disconnect all devices?", "Koppla ifrån alla enheter?"), + ("Clear", "Töm"), + ("Audio Input Device", "Inmatningsenhet för ljud"), + ("Use IP Whitelisting", "Använd IP-Vitlistning"), + ("Network", "Nätverk"), + ("Pin Toolbar", "Fäst verktygsfältet"), + ("Unpin Toolbar", "Ta bort verktygsfältet"), + ("Recording", "Spelar in"), + ("Directory", "Katalog"), + ("Automatically record incoming sessions", "Spela in inkommande sessioner automatiskt"), + ("Automatically record outgoing sessions", "Spela in utgående sessioner automatiskt"), + ("Change", "Byt"), + ("Start session recording", "Starta inspelning"), + ("Stop session recording", "Avsluta inspelning"), + ("Enable recording session", "Sätt på sessionsinspelning"), + ("Enable LAN discovery", "Sätt på LAN upptäckt"), + ("Deny LAN discovery", "Neka LAN upptäckt"), + ("Write a message", "Skriv ett meddelande"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Var god vänta för UAC bekräftelse..."), + ("elevated_foreground_window_tip", "Detta fönster hos klienten kräver en högre behörighet. Du kan be användaren att minimera fönstret, eller att ge högre behörigheter i fönstret för anslutningsinställningar. För att undvika detta problem i framtiden, installera programmet på klientens sida."), + ("Disconnected", "Frånkopplad"), + ("Other", "Övrigt"), + ("Confirm before closing multiple tabs", "Bekräfta innan du stänger flera flikar"), + ("Keyboard Settings", "Tangentbordsinställningar"), + ("Full Access", "Full tillgång"), + ("Screen Share", "Skärmdelning"), + ("ubuntu-21-04-required", "Wayland kräver Ubuntu 21.04 eller högre."), + ("wayland-requires-higher-linux-version", "Wayland kräver en högre version av linux. Försök igen eller byt OS."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Välj skärm att dela"), + ("Show RustDesk", "Visa RustDesk"), + ("This PC", "Denna dator"), + ("or", "eller"), + ("Elevate", "Höj upp"), + ("Zoom cursor", "Zoom"), + ("Accept sessions via password", "Acceptera sessioner via lösenord"), + ("Accept sessions via click", "Acceptera sessioner via klick"), + ("Accept sessions via both", "Acceptera sessioner via båda"), + ("Please wait for the remote side to accept your session request...", "Var god vänta på att klienten accepterar din förfrågan..."), + ("One-time Password", "En-gångs lösenord"), + ("Use one-time password", "Använd en-gångs lösenord"), + ("One-time password length", "Längd på en-gångs lösenord"), + ("Request access to your device", "Begär åtkomst till din enhet"), + ("Hide connection management window", "Göm hanteringsfönster"), + ("hide_cm_tip", "Tillåt att gömma endast om accepterande sessioner med lösenord och permanenta lösenord"), + ("wayland_experiment_tip", ""), + ("Right click to select tabs", "Högerklicka för att välja flikar"), + ("Skipped", "Hoppade över"), + ("Add to address book", "Lägg till i adressboken"), + ("Group", "Grupp"), + ("Search", "Sök"), + ("Closed manually by web console", "Stängt manuellt av webkonsolen"), + ("Local keyboard type", "Lokal tangentbordstyp"), + ("Select local keyboard type", "Välj lokal tangentbordstyp"), + ("software_render_tip", ""), + ("Always use software rendering", "Använd alltid mjukvarurendering"), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", "Vänta"), + ("Elevation Error", ""), + ("Ask the remote user for authentication", "Fråga fjärranvändaren för autentisering"), + ("Choose this if the remote account is administrator", "Välj detta om fjärrkontot är administratör"), + ("Transmit the username and password of administrator", "Skicka administratörens användarnamn och lösenord"), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", "versal"), + ("lowercase", "gemen"), + ("digit", "siffra"), + ("special character", "specialtecken"), + ("length>=8", "längd>=8"), + ("Weak", "Svag"), + ("Medium", "Medium"), + ("Strong", "Stark"), + ("Switch Sides", "Byt sidor"), + ("Please confirm if you want to share your desktop?", "Vänligen bekräfta att du vill dela ditt skrivbord?"), + ("Display", "Display"), + ("Default View Style", "Standardvisningsstil"), + ("Default Scroll Style", "Standardscrollstil"), + ("Default Image Quality", "Standardbildkvalitet"), + ("Default Codec", "Standard Kodek"), + ("Bitrate", "Bithastighet"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Andra Standardinställningar"), + ("Voice call", "Röstsamtal"), + ("Text chat", "Meddelandechatt"), + ("Stop voice call", "Stoppa röstsamtal"), + ("relay_hint_tip", ""), + ("Reconnect", "Återanslut"), + ("Codec", "Kodek"), + ("Resolution", "Upplösning"), + ("No transfers in progress", "Inga överförningar pågår"), + ("Set one-time password length", "Ställ in engångslösenordets längd"), + ("RDP Settings", "RDP inställningar"), + ("Sort by", "Sortera efter"), + ("New Connection", "Ny Anslutning"), + ("Restore", "Återställ"), + ("Minimize", "Minimera"), + ("Maximize", "Maximera"), + ("Your Device", "Din Enhet"), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("Empty Username", "Tomt användarnamn"), + ("Empty Password", "Tomt lösenord"), + ("Me", "Jag"), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", "Visningsläge"), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", "OS-konto"), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", "Systemljud"), + ("Default", "Standard"), + ("New RDP", "Ny RDP"), + ("Fingerprint", "Fingeravtryck"), + ("Copy Fingerprint", "Kopiera fingeravtryck"), + ("no fingerprints", "inga fingeravtryck"), + ("Select a peer", ""), + ("Select peers", ""), + ("Plugins", "Plugin"), + ("Uninstall", "Avinstallera"), + ("Update", "Uppdatera"), + ("Enable", "Aktivera"), + ("Disable", "Inaktivera"), + ("Options", "Inställningar"), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), + ("Collapse toolbar", "Komprimera verktygsfältet"), + ("Accept and Elevate", ""), + ("accept_and_elevate_btn_tooltip", ""), + ("clipboard_wait_response_timeout_tip", ""), + ("Incoming connection", "Inkommande anslutning"), + ("Outgoing connection", "Utgående anslutning"), + ("Exit", "Stäng"), + ("Open", "Öppna"), + ("logout_tip", ""), + ("Service", "Tjänst"), + ("Start", "Start"), + ("Stop", "Stopp"), + ("exceed_max_devices", ""), + ("Sync with recent sessions", "Synkronisera med senaste sessioner"), + ("Sort tags", "Sortera taggar"), + ("Open connection in new tab", "Öppna anslutning i ny flik"), + ("Move tab to new window", "Flytta flik till nytt fönster"), + ("Can not be empty", "Kan ej vara tom"), + ("Already exists", "Existerar redan"), + ("Change Password", "Byt lösenord"), + ("Refresh Password", "Uppdatera lösenord"), + ("ID", "ID"), + ("Grid View", "Rutnätsvy"), + ("List View", "Listvy"), + ("Select", "Välj"), + ("Toggle Tags", "Växla flikar"), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), + ("synced_peer_readded_tip", ""), + ("Change Color", "Byt färg"), + ("Primary Color", "Primärfärg"), + ("HSV Color", "HSV färg"), + ("Installation Successful!", "Installationen lyckades!"), + ("Installation failed!", "Installationen misslyckades!"), + ("Reverse mouse wheel", "Ändra riktning för scrollhjulet"), + ("{} sessions", "{} sessioner"), + ("scam_title", ""), + ("scam_text1", ""), + ("scam_text2", ""), + ("Don't show again", "Visa inte igen"), + ("I Agree", "Jag godkänner"), + ("Decline", "Avböj"), + ("Timeout in minutes", "Timeout i minuter"), + ("auto_disconnect_option_tip", ""), + ("Connection failed due to inactivity", "Anslutningen misslyckades på grund av inaktivitet"), + ("Check for software update on startup", "Kolla efter mjukvaruuppdateringar vid start"), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), + ("pull_group_failed_tip", ""), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", "Dölj bakgrunden vid inkommande sessioner"), + ("Test", "Test"), + ("display_is_plugged_out_msg", ""), + ("No displays", "Inga skärmar"), + ("Open in new window", "Öppna i nytt fönster"), + ("Show displays as individual windows", "Visa skärmar som enskilda fönster"), + ("Use all my displays for the remote session", "Använd alla mina skärmar för fjärrsessionen"), + ("selinux_tip", ""), + ("Change view", "Byt vy"), + ("Big tiles", "Stora rutor"), + ("Small tiles", "Små rutor"), + ("List", "Lista"), + ("Virtual display", "Virtuell skärm"), + ("Plug out all", "Koppla ur alla"), + ("True color (4:4:4)", "Sann färg (4:4:4)"), + ("Enable blocking user input", "Aktivera blockering av användarinmatning"), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", "Aktivera privatläge"), + ("Exit privacy mode", "Inaktivera privatläge"), + ("idd_not_support_under_win10_2004_tip", ""), + ("input_source_1_tip", ""), + ("input_source_2_tip", ""), + ("Swap control-command key", "Byt control-command knapp"), + ("swap-left-right-mouse", ""), + ("2FA code", "Tvåstegsverifieringskod"), + ("More", "Mer"), + ("enable-2fa-title", ""), + ("enable-2fa-desc", ""), + ("wrong-2fa-code", ""), + ("enter-2fa-title", ""), + ("Email verification code must be 6 characters.", "Mailverifikationskoden måste vara 6 tecken."), + ("2FA code must be 6 digits.", "Tvåstegsverifikationskoden måste vara 6 siffor."), + ("Multiple Windows sessions found", "Flera Windows sessioner hittades"), + ("Please select the session you want to connect to", "Välj den session du vill ansluta till"), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", "Säkerhetsvarning"), + ("My address book", "Min adressbok"), + ("Personal", "Personlig"), + ("Owner", "Ägare"), + ("Set shared password", "Välj delat lösenord"), + ("Exist in", "Existerar i"), + ("Read-only", "Skrivskyddad"), + ("Read/Write", "Läs/Skriv"), + ("Full Control", "Full kontroll"), + ("share_warning_tip", ""), + ("Everyone", "Alla"), + ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", "Följ fjärrpekaren"), + ("Follow remote window focus", "Följ fjärrfönstrets fokus"), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", "Inkommande"), + ("Outgoing", "Utgående"), + ("Clear Wayland screen selection", "Rensa wayland-skärmens val"), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", "Använd texturrendering"), + ("Floating window", "Flytande fönster"), + ("floating_window_tip", ""), + ("Keep screen on", "Behåll skärmen på"), + ("Never", "Aldrig"), + ("During controlled", ""), + ("During service is on", "Medan tjänsten är på"), + ("Capture screen using DirectX", "Spela in skärmen med DirectX"), + ("Back", "Bak"), + ("Apps", "Appar"), + ("Volume up", "Volym upp"), + ("Volume down", "Volym ner"), + ("Power", "Strömbrytare"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", "Om RustDesk"), + ("Send clipboard keystrokes", "Skicka knappkombination för urklipp"), + ("network_error_tip", ""), + ("Unlock with PIN", "Lås upp med PIN"), + ("Requires at least {} characters", "Kräver minst {} tecken}"), + ("Wrong PIN", "Fel PIN"), + ("Set PIN", "Välj PIN"), + ("Enable trusted devices", "Tillåt betrodda enheter"), + ("Manage trusted devices", "Hantera betrodda enheter"), + ("Platform", "Plattform"), + ("Days remaining", "Dagar kvar"), + ("enable-trusted-devices-tip", ""), + ("Parent directory", "Föräldrakatalog"), + ("Resume", "Återuppta"), + ("Invalid file name", "Felaktigt filnamn"), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", "Autentisering krävs"), + ("Authenticate", "Autentisera"), + ("web_id_input_tip", ""), + ("Download", "Ladda ner"), + ("Upload folder", "Ladda upp mapp"), + ("Upload files", "Ladda upp filer"), + ("Clipboard is synchronized", "Urklippet är synkroniserat"), + ("Update client clipboard", "Uppdatera klientens urklipp"), + ("Untagged", "Otaggad"), + ("new-version-of-{}-tip", ""), + ("Accessible devices", "Tillgängliga enheter"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", "Använd D3D rendering"), + ("Printer", "Skrivarer"), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", "Installera {} skrivare"), + ("Outgoing Print Jobs", "Utgående skrivarjobb"), + ("Incoming Print Jobs", "Inkommande skrivarjobb"), + ("Incoming Print Job", "Inkommande skrivarjobb"), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", "Ta skärmbild"), + ("Taking screenshot", "Tar skärmbild"), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", "Spara som"), + ("Copy to clipboard", "Kppiera till urklipp"), + ("Enable remote printer", "Aktivera fjärrskrivare"), + ("Downloading {}", "Laddar ner {}"), + ("{} Update", "{} Uppdatera"), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", "Automatisk uppdatering"), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", "Använd WebSocket"), + ("Trackpad speed", "Styrplattans hastighet"), + ("Default trackpad speed", "Standardhastighet för styrplattan"), + ("Numeric one-time password", "Numeriskt engångslösenord"), + ("Enable IPv6 P2P connection", "Aktivera IPv6 P2P anslutning"), + ("Enable UDP hole punching", "Aktivera UDP hålslagning"), + ("View camera", "Visa kamera"), + ("Enable camera", "Aktivera kamera"), + ("No cameras", "Inga kameror"), + ("view_camera_unsupported_tip", ""), + ("Terminal", "Terminal"), + ("Enable terminal", "Aktivera terminal"), + ("New tab", "Ny flik"), + ("Keep terminal sessions on disconnect", "Behåll terminalsessioner vid frånkpppling"), + ("Terminal (Run as administrator)", "Terminal (Kör som administratör)"), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", "Misslyckades med att hämta användartoken."), + ("Incorrect username or password.", "Felaktigt användarnamn eller lösenord."), + ("The user is not an administrator.", "Användaren är inte en administratör."), + ("Failed to check if the user is an administrator.", "Misslyckades med att kontrollera om användaren är administratör."), + ("Supported only in the installed version.", "Stöds endast i den installerade versionen."), + ("elevation_username_tip", ""), + ("Preparing for installation ...", "Förbereder för installation ..."), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Fortsätt med {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/ta.rs b/vendor/rustdesk/src/lang/ta.rs new file mode 100644 index 0000000..6e56525 --- /dev/null +++ b/vendor/rustdesk/src/lang/ta.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "நிலை"), + ("Your Desktop", "உங்கள் டெஸ்க்டாப்"), + ("desk_tip", "டெஸ்க்_குறிப்பு"), + ("Password", "கடவுச்சொல்"), + ("Ready", "தயார்"), + ("Established", "நிறைவேற்றம்"), + ("connecting_status", "இணைப்பு நிலை"), + ("Enable service", "சேவையை இயக்கு"), + ("Start service", "சேவையை தொடங்கு"), + ("Service is running", "சேவை இயங்குகிறது"), + ("Service is not running", "சேவை இயங்கவில்லை."), + ("not_ready_status", "இயக்கம் இல்லை"), + ("Control Remote Desktop", "ரிமோட் டெஸ்க்டாப் கட்டுப்பாடு"), + ("Transfer file", "கோப்பு பரிமாற்றம்"), + ("Connect", "இணைக்க"), + ("Recent sessions", "கடந்த அமர்வுகள்"), + ("Address book", "முகவரி புத்தகம்"), + ("Confirmation", "உறுதிப்படுத்தல்"), + ("TCP tunneling", "TCP டன்னலிங்"), + ("Remove", "அகற்று"), + ("Refresh random password", "சீரற்ற கடவுச்சொல் புதுப்பி"), + ("Set your own password", "கடவுச்சொல் அமைக்கவும்"), + ("Enable keyboard/mouse", "விசைப்பலகை/சுட்டி இயக்கு"), + ("Enable clipboard", "கிளிப்போர்டு இயக்கு"), + ("Enable file transfer", "கோப்பு பரிமாற்றம் இயக்கு"), + ("Enable TCP tunneling", "TCP டன்னலிங் இயக்கு"), + ("IP Whitelisting", "IP அனுமதிப்பட்டியல்"), + ("ID/Relay Server", "ஐடி/ரிலே சர்வர்"), + ("Import server config", "சர்வர் உள்ளமைவு இறக்குமதி"), + ("Export Server Config", "சர்வர் உள்ளமைவு ஏற்றுமதி"), + ("Import server configuration successfully", "சர்வர் உள்ளமைவு இறக்குமதி வெற்றி"), + ("Export server configuration successfully", "சர்வர் உள்ளமைவு ஏற்றுமதி வெற்றி"), + ("Invalid server configuration", "தவறான சர்வர் உள்ளமைவு"), + ("Clipboard is empty", "கிளிப்போர்டு காலி"), + ("Stop service", "சேவையை நிறுத்து"), + ("Change ID", "ஐடி மாற்று"), + ("Your new ID", "உங்கள் புதிய ஐடி"), + ("length %min% to %max%", "நீளம் %min% முதல் %max%"), + ("starts with a letter", "ஒரு எழுத்தால் தொடங்கு"), + ("allowed characters", "அனுமதிக்கப்பட்ட எழுத்துக்கள்"), + ("id_change_tip", "ஐடி_மாற்ற_குறிப்பு"), + ("Website", "இணையதளம்"), + ("About", "பற்றி"), + ("Slogan_tip", "சுலோகம்_குறிப்பு"), + ("Privacy Statement", "தனியுரிமை அறிக்கை"), + ("Mute", "ஒலியடக்கவும்"), + ("Build Date", "கட்டப்பட்ட தேதி"), + ("Version", "பதிப்பு"), + ("Home", "வீடு"), + ("Audio Input", "ஒலி உள்ளீடு"), + ("Enhancements", "மேம்பாடுகள்"), + ("Hardware Codec", "வன்பொருள் கோடெக்"), + ("Adaptive bitrate", "தகவமைப்பு பிட்ரேட்"), + ("ID Server", "ஐடி சர்வர்"), + ("Relay Server", "ரிலே சர்வர்"), + ("API Server", "API சர்வர்"), + ("invalid_http", "தவறான_http"), + ("Invalid IP", "தவறான IP"), + ("Invalid format", "தவறான வடிவம்"), + ("server_not_support", "சர்வர்_ஆதரவு_இல்லை"), + ("Not available", "இல்லை"), + ("Too frequent", "அடிக்கடி"), + ("Cancel", "ரத்துசெய்"), + ("Skip", "தவிர்"), + ("Close", "மூடு"), + ("Retry", "மீண்டும் முயலவும்"), + ("OK", "சரி"), + ("Password Required", "கடவுச்சொல்_தேவை"), + ("Please enter your password", "உங்கள் கடவுச்சொல்லை உள்ளிடுக"), + ("Remember password", "கடவுச்சொல்லை நினைவு கொள்"), + ("Wrong Password", "தவறான கடவுச்சொல்"), + ("Do you want to enter again?", "மீண்டும் முயலவுமா?"), + ("Connection Error", "இணைப்பு பிழை"), + ("Error", "பிழை"), + ("Reset by the peer", "பியர் மூலம் மீட்டமை"), + ("Connecting...", "இணைப்பு ..."), + ("Connection in progress. Please wait.", "இணைப்பு முயற்சியில். காத்திருக்கவும்..."), + ("Please try 1 minute later", "1 நிமிடம் கழித்து முயலவும்"), + ("Login Error", "பதிவு பிழை"), + ("Successful", "வெற்றிகரம்"), + ("Connected, waiting for image...", "இணைப்பு தயார், படத்துக்காக காத்திருக்கிறது..."), + ("Name", "பெயர்"), + ("Type", "வகை"), + ("Modified", "மாற்றப்பட்டது"), + ("Size", "அளவு"), + ("Show Hidden Files", "மறைந்த கோப்புகளை காட்டு"), + ("Receive", "பெறு"), + ("Send", "அனுப்பு"), + ("Refresh File", "கோப்பு புதுப்பி"), + ("Local", "உள்ளூர்"), + ("Remote", "ரிமோட்"), + ("Remote Computer", "ரிமோட் கணினி"), + ("Local Computer", "உள்ளூர் கணினி"), + ("Confirm Delete", "நீக்குவதை உறுதிசெய்"), + ("Delete", "நீக்கு"), + ("Properties", "பண்புகள்"), + ("Multi Select", "பலவற்றை தேர்வு"), + ("Select All", "அனைத்தும் தேர்வு"), + ("Unselect All", "அனைத்தும் தேர்வு நீக்கு"), + ("Empty Directory", "காலியான கோப்புக்குழு"), + ("Not an empty directory", "காலியான கோப்புக்குழு அல்ல"), + ("Are you sure you want to delete this file?", "கோப்பை நீக்க உறுதியா?"), + ("Are you sure you want to delete this empty directory?", "காலி கோப்புறையை நீக்க உறுதியா?"), + ("Are you sure you want to delete the file of this directory?", "கோப்புறையின் கோப்புகளை நீக்க உறுதியா?"), + ("Do this for all conflicts", "அனைத்து முரண்பாடுகளுக்கும் இதை செய்"), + ("This is irreversible!", "இது மீளாது!"), + ("Deleting", "நீக்குதல்"), + ("files", "கோப்புகள்"), + ("Waiting", "காத்திருக்கும்"), + ("Finished", "முடிந்தது"), + ("Speed", "வேகம்"), + ("Custom Image Quality", "தனிப்பட்ட புகைப்பட தரம்"), + ("Privacy mode", "தனியுரிமை முறை"), + ("Block user input", "பயனர் உள்ளீட்டைத் தடு"), + ("Unblock user input", "பயனர் உள்ளீடு தடை நீக்கு"), + ("Adjust Window", "சாளரம் சரிசெய்"), + ("Original", "அசல்"), + ("Shrink", "குறுக்கு"), + ("Stretch", "நீட்டு"), + ("Scrollbar", "ஸ்க்ரோல் பட்டி"), + ("ScrollAuto", "ஸ்க்ரோல்ஆட்டோ"), + ("Good image quality", "நல்ல புகைப்பட தரம்"), + ("Balanced", "சமநிலை"), + ("Optimize reaction time", "எதிர்வினை நேரத்தை மேம்பாடு"), + ("Custom", "தனிப்பட்ட"), + ("Show remote cursor", "ரிமோட் கர்சர் காட்டு"), + ("Show quality monitor", "தரம் காட்டு"), + ("Disable clipboard", "கிளிப்போர்டை மறை"), + ("Lock after session end", "அமர்வு முடிவுக்குப் பின் மறை"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del செய்"), + ("Insert Lock", "மறை செய்"), + ("Refresh", "புதுப்பி"), + ("ID does not exist", "ஐடி இல்லை"), + ("Failed to connect to rendezvous server", "சந்திப்பு சர்வர் இணைப்பு பிழை"), + ("Please try later", "பிறகு முயலவும்"), + ("Remote desktop is offline", "ரிமோட் டெஸ்க்டாப் ஆஃப்லைன்"), + ("Key mismatch", "விசை பொருந்தவில்லை"), + ("Timeout", "நேரம் முடிந்தது"), + ("Failed to connect to relay server", "ரிலே சர்வர் இணைப்பு தோல்வி"), + ("Failed to connect via rendezvous server", "சந்திப்பு சர்வர் வழி இணைப்பு தோல்வி"), + ("Failed to connect via relay server", "ரிலே சர்வர் வழி இணைப்பு தோல்வி"), + ("Failed to make direct connection to remote desktop", "ரிமோட் டெஸ்க்டாப் நேரடி இணைப்பு தோல்வி"), + ("Set Password", "கடவுச்சொல் அமை"), + ("OS Password", "OS கடவுச்சொல்"), + ("install_tip", "நிறுவு_குறிப்பு"), + ("Click to upgrade", "மேம்படுத்த கிளிக் செய்"), + ("Configure", "உள்ளமை"), + ("config_acc", "உள்ளமைவு_அக்கெஸ்ஸ்"), + ("config_screen", "config_screen"), + ("Installing ...", "நிறுவுதல் ..."), + ("Install", "நிறுவு"), + ("Installation", "நிறுவல்"), + ("Installation Path", "நிறுவல் பாதை"), + ("Create start menu shortcuts", "தொடக்க மெனு ஷார்ட்கட் உருவாக்கு"), + ("Create desktop icon", "டெஸ்க்டாப் ஐகான் உருவாக்கு"), + ("agreement_tip", "ஒப்பந்தம்_குறிப்பு"), + ("Accept and Install", "ஏற்றுக்கொண்டு நிறுவு"), + ("End-user license agreement", "இறுதி-பயனர் உரிம ஒப்பந்தம்"), + ("Generating ...", "உருவாக்குதல் ..."), + ("Your installation is lower version.", "குறைந்த பதிப்பு நிறுவப்பட்டுள்ளது"), + ("not_close_tcp_tip", "tcp_மூடாதே_குறிப்பு"), + ("Listening ...", "கேட்கிறது..."), + ("Remote Host", "தொலை ஹோஸ்ட்"), + ("Remote Port", "தொலை போர்ட்"), + ("Action", "செயல்"), + ("Add", "சேர்"), + ("Local Port", "உள்ளூர் போர்ட்"), + ("Local Address", "உள்ளூர் முகவரி"), + ("Change Local Port", "உள்ளூர் போர்ட் மாற்று"), + ("setup_server_tip", "சர்வர்_அமைவு_குறிப்பு"), + ("Too short, at least 6 characters.", "மிகக் குறுகியது, குறைந்தது 6 எழுத்து"), + ("The confirmation is not identical.", "உறுதிப்படுத்தல் பொருந்தவில்லை"), + ("Permissions", "அனுமதிகள்"), + ("Accept", "ஏற்று"), + ("Dismiss", "ரத்து"), + ("Disconnect", "துண்டி"), + ("Enable file copy and paste", "கோப்பு நகல் மற்றும் பேஸ்ட் இயக்கு"), + ("Connected", "இணைக்கப்பட்டது"), + ("Direct and encrypted connection", "நேரடி மற்றும் மறையான இணைப்பு"), + ("Relayed and encrypted connection", "ரிலே மற்றும் மறையான இணைப்பு"), + ("Direct and unencrypted connection", "நேரடி மற்றும் மறையான இணைப்பு"), + ("Relayed and unencrypted connection", "ரிலே மற்றும் மறையான இணைப்பு"), + ("Enter Remote ID", "தொலை ஐடியை உள்ளிடு"), + ("Enter your password", "உங்கள் கடவுச்சொல்லை உள்ளிடு"), + ("Logging in...", "பதிவு முயற்சிக்கிறது..."), + ("Enable RDP session sharing", "RDP அமர்வு பகிர்வு இயக்கு"), + ("Auto Login", "தானியங்கு உள்நுழைவு"), + ("Enable direct IP access", "நேரடி IP அனுமதிப்பு இயக்கு"), + ("Rename", "பெயர் மாற்று"), + ("Space", "இடம்"), + ("Create desktop shortcut", "டெஸ்க்டாப் ஐகானை உருவாக்கு"), + ("Change Path", "பாதை மாற்று"), + ("Create Folder", "கோப்புக்குழு உருவாக்கு"), + ("Please enter the folder name", "கோப்புக்குழுவின் பெயரை உள்ளிடு"), + ("Fix it", "சரி செய்"), + ("Warning", "எச்சரிக்கை"), + ("Login screen using Wayland is not supported", "Wayland உள்நுழைவுத் திரை ஆதரவில்லை"), + ("Reboot required", "மறுதொடக்கம் தேவை"), + ("Unsupported display server", "திரை சர்வர் ஆதரவு இல்லை"), + ("x11 expected", "x11 எதிர்பார்க்கப்படுகிறது"), + ("Port", "போர்ட்"), + ("Settings", "அமைப்புகள்"), + ("Username", "பயனர்பெயர்"), + ("Invalid port", "தவறான போர்ட்"), + ("Closed manually by the peer", "பியர் மூலம் மூடப்பட்டது"), + ("Enable remote configuration modification", "தொலை அமைப்பு மாற்று இயக்கு"), + ("Run without install", "நிறுவல் இல்லாமல் இயக்கு"), + ("Connect via relay", "ரிலே மூலம் இணைக்கவும்"), + ("Always connect via relay", "எப்போதும் ரிலே மூலம் இணைக்கவும்"), + ("whitelist_tip", "வெள்ளைப்பட்டியல்_குறிப்பு"), + ("Login", "உள்நுழை"), + ("Verify", "உறுதிப்படுத்து"), + ("Remember me", "நினைவு கொள்"), + ("Trust this device", "இந்த சாதனத்தை நம்பு"), + ("Verification code", "சரிபார்ப்பு குறியீடு"), + ("verification_tip", "சரிபார்ப்பு_குறிப்பு"), + ("Logout", "வெளியேறு"), + ("Tags", "குறிச்சொற்கள்"), + ("Search ID", "ஐடி தேடு"), + ("whitelist_sep", "அனுமதிப்பட்டியல்_sep"), + ("Add ID", "ஐடி சேர்"), + ("Add Tag", "குறிச்சொற்கள் சேர்"), + ("Unselect all tags", "அனைத்து குறிச்சொற்களைத் தேர்வு நீக்கு"), + ("Network error", "நெட்வொர்க் பிழை"), + ("Username missed", "பயனர்பெயர் தவறவிட்டது"), + ("Password missed", "கடவுச்சொல் தவறவிட்டது"), + ("Wrong credentials", "தவறான சான்றுகள்"), + ("The verification code is incorrect or has expired", "சரிபார்ப்புக் குறியீடு தவறானது அல்லது காலாவதி"), + ("Edit Tag", "குறிச்சொற்கள் மாற்று"), + ("Forget Password", "கடவுச்சொல்லை மறந்துவிடு"), + ("Favorites", "விருப்பங்கள்"), + ("Add to Favorites", "விருப்பங்களுக்கு சேர்"), + ("Remove from Favorites", "விருப்பங்களுக்கு நீக்கு"), + ("Empty", "காலி"), + ("Invalid folder name", "தவறான கோப்புக்குழு பெயர்"), + ("Socks5 Proxy", "Socks5 ப்ராக்ஸி"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) ப்ராக்ஸி"), + ("Discovered", "கண்டுபிடிக்கப்பட்டது"), + ("install_daemon_tip", "டீமான்_நிறுவு_குறிப்பு"), + ("Remote ID", "தொலை ஐடி"), + ("Paste", "பேஸ்ட்"), + ("Paste here?", "இங்கே பேஸ்ட் செய்?"), + ("Are you sure to close the connection?", "இணைப்பை மூட உறுதியா?"), + ("Download new version", "புதிய பதிப்பை பதிவிறக்கு"), + ("Touch mode", "தொடுதல் முறை"), + ("Mouse mode", "சுட்டி முறை"), + ("One-Finger Tap", "ஒரு விரல் தட்டு"), + ("Left Mouse", "இடது சுட்டி"), + ("One-Long Tap", "ஒரு நீண்ட தட்டு"), + ("Two-Finger Tap", "இரு விரல் தட்டு"), + ("Right Mouse", "வலது சுட்டி"), + ("One-Finger Move", "ஒரு விரல் நகர்த்தல்"), + ("Double Tap & Move", "இரட்டை தட்டு மற்றும் நகர்த்தல்"), + ("Mouse Drag", "சுட்டி இழுத்தல்"), + ("Three-Finger vertically", "மூன்று விரல் செங்குத்தாக"), + ("Mouse Wheel", "சுட்டி சக்கரம்"), + ("Two-Finger Move", "இரு விரல் நகர்த்தல்"), + ("Canvas Move", "கேன்வாஸ் நகர்த்தல்"), + ("Pinch to Zoom", "சிமுட்டி பெரிதாக்கல்"), + ("Canvas Zoom", "கேன்வாஸ் பெரிதாக்கல்"), + ("Reset canvas", "கேன்வாஸ் மீட்டமை"), + ("No permission of file transfer", "கோப்பு பரிமாற்ற அனுமதி இல்லை"), + ("Note", "குறிப்பு"), + ("Connection", "இணைப்பு"), + ("Share screen", ""), + ("Chat", "அரட்டை"), + ("Total", "மொத்தம்"), + ("items", "பொருட்கள்"), + ("Selected", "தேர்ந்தெடுக்கப்பட்டது"), + ("Screen Capture", "திரை பிடிப்பு"), + ("Input Control", "உள்ளீடு கட்டுப்பாடு"), + ("Audio Capture", "ஒலி பிடிப்பு"), + ("Do you accept?", "நீங்கள் ஏற்றுக்கொள்கிறீர்களா?"), + ("Open System Setting", "சிஸ்டம் அமைப்புகளைத் திற"), + ("How to get Android input permission?", "Android உள்ளீடு அனுமதி எப்படி பெறுவது?"), + ("android_input_permission_tip1", "RustDesk இந்த Android சாதனத்தை கட்டுப்படுத்த \"அணுகல் சேவைகள்\" அனுமதி தேவை."), + ("android_input_permission_tip2", "சிஸ்டம் அமைப்புகளில் [நிறுவப்பட்ட சேவைகள்] கண்டுபிடித்து, RustDesk சேவை இயக்கவும்."), + ("android_new_connection_tip", "புதிய கட்டுப்பாட்டு கோரிக்கை வந்துள்ளது"), + ("android_service_will_start_tip", "திரை பிடிப்பு இயக்கினால் சேவை தானாக தொடங்கும்"), + ("android_stop_service_tip", "சேவை நிறுத்தினால் எல்லா இணைப்புகளும் மூடிவிடும்"), + ("android_version_audio_tip", "Android 10+ தேவை ஒலி பிடிப்புக்கு"), + ("android_start_service_tip", "[சேவை தொடங்கு] தட்டவும் அல்லது [திரை பிடிப்பு] இயக்கவும்"), + ("android_permission_may_not_change_tip", "அனுமதிகள் உடனே மாறாமல் இருக்கலாம், மீண்டும் இணைக்கவும்"), + ("Account", "கணக்கு"), + ("Overwrite", "மேலெழுது"), + ("This file exists, skip or overwrite this file?", "கோப்பு உள்ளது, தவிர்க்கவா அல்லது மேலெழுதவா?"), + ("Quit", "வெளியேறு"), + ("Help", "உதவி"), + ("Failed", "தோல்வி"), + ("Succeeded", "வெற்றி"), + ("Someone turns on privacy mode, exit", "தனியுரிமை முறை இயக்கப்பட்டது, வெளியேறு"), + ("Unsupported", "ஆதரவு இல்லை"), + ("Peer denied", "இணையாளர் மறுத்தார்"), + ("Please install plugins", "இணைப்புகளை நிறுவுங்கள்"), + ("Peer exit", "இணையாளர் வெளியேறினார்"), + ("Failed to turn off", "அணைக்க முடியவில்லை"), + ("Turned off", "அணைக்கப்பட்டது"), + ("Language", "மொழி"), + ("Keep RustDesk background service", "RustDesk பின்புல சேவையை வைத்திரு"), + ("Ignore Battery Optimizations", "பேட்டரி மேம்படுத்தல்களை புறக்கணி"), + ("android_open_battery_optimizations_tip", "RustDesk க்கு பேட்டரி மேம்படுத்தல் அணைக்க அமைப்புகளுக்கு செல்லவும்"), + ("Start on boot", "துவக்கத்தில் தொடங்கு"), + ("Start the screen sharing service on boot, requires special permissions", "துவக்கத்தில் திரை பகிர்வு தொடங்கு, சிறப்பு அனுமதி தேவை"), + ("Connection not allowed", "இணைப்பு அனுமதிக்கப்படவில்லை"), + ("Legacy mode", "பழைய முறை"), + ("Map mode", "வரைபட முறை"), + ("Translate mode", "மொழிபெயர்ப்பு முறை"), + ("Use permanent password", "நிரந்தர கடவுச்சொல் பயன்படுத்து"), + ("Use both passwords", "இரண்டு கடவுச்சொல்களும் பயன்படுத்து"), + ("Set permanent password", "நிரந்தர கடவுச்சொல் அமை"), + ("Enable remote restart", "தொலைநிலை மறுதொடக்கத்தை இயக்கு"), + ("Restart remote device", "தொலைநிலை சாதனத்தை மறுதொடக்கு"), + ("Are you sure you want to restart", "மறுதொடக்கம் செய்ய உறுதியா"), + ("Restarting remote device", "ரிமோட் சாதனம் மறுதொடக்கம் ஆகிறது"), + ("remote_restarting_tip", "ரிமோட்_மறுதொடக்கம்_குறிப்பு"), + ("Copied", "நகலெடுக்கப்பட்டது"), + ("Exit Fullscreen", "முழுத்திரையிலிருந்து வெளியேறு"), + ("Fullscreen", "முழுத்திரை"), + ("Mobile Actions", "மொபைல் செயல்கள்"), + ("Select Monitor", "மானிட்டரைத் தேர்ந்தெடு"), + ("Control Actions", "கட்டுப்பாட்டு செயல்கள்"), + ("Display Settings", "திரை அமைப்புகள்"), + ("Ratio", "விகிதம்"), + ("Image Quality", "புகைப்பட தரம்"), + ("Scroll Style", "ஸ்க்ரோல் பாணி"), + ("Show Toolbar", "கருவிப்பட்டியைக் காட்டு"), + ("Hide Toolbar", "கருவிப்பட்டியை மறை"), + ("Direct Connection", "நேரடி இணைப்பு"), + ("Relay Connection", "ரிலே இணைப்பு"), + ("Secure Connection", "பாதுகாப்பான இணைப்பு"), + ("Insecure Connection", "பாதுகாப்பற்ற இணைப்பு"), + ("Scale original", "அசல் அளவு"), + ("Scale adaptive", "தகவமைப்பு அளவு"), + ("General", "பொது"), + ("Security", "பாதுகாப்பு"), + ("Theme", "தீம்"), + ("Dark Theme", "இருண்ட தீம்"), + ("Light Theme", "வெளிச்ச தீம்"), + ("Dark", "இருண்ட"), + ("Light", "வெளிச்சம்"), + ("Follow System", "சிஸ்டத்தைப் பின்பற்று"), + ("Enable hardware codec", "வன்பொருள் கோடெக்கை இயக்கு"), + ("Unlock Security Settings", "பாதுகாப்பு அமைப்புகளை திற"), + ("Enable audio", "ஒலியை இயக்கு"), + ("Unlock Network Settings", "நெட்வொர்க் அமைப்புகளை திற"), + ("Server", "சர்வர்"), + ("Direct IP Access", "நேரடி IP அணுகல்"), + ("Proxy", "ப்ராக்ஸி"), + ("Apply", "பயன்படுத்து"), + ("Disconnect all devices?", "அனைத்து சாதனங்களையும் துண்டிக்கவா?"), + ("Clear", "தெளிவுப்படுத்து"), + ("Audio Input Device", "ஒலி உள்ளீடு சாதனம்"), + ("Use IP Whitelisting", "IP அனுமதிப்பட்டியலைப் பயன்படுத்து"), + ("Network", "நெட்வொர்க்"), + ("Pin Toolbar", "கருவிப்பட்டியை பின் செய்"), + ("Unpin Toolbar", "கருவிப்பட்டியை அன்பின் செய்"), + ("Recording", "பதிவு"), + ("Directory", "கோப்பகம்"), + ("Automatically record incoming sessions", "உள்வரும் அமர்வுகளை தானாக பதிவு செய்"), + ("Automatically record outgoing sessions", "வெளியேறும் அமர்வுகளை தானாக பதிவு செய்"), + ("Change", "மாற்று"), + ("Start session recording", "அமர்வு பதிவைத் தொடங்கு"), + ("Stop session recording", "அமர்வு பதிவை நிறுத்து"), + ("Enable recording session", "பதிவு அமர்வை இயக்கு"), + ("Enable LAN discovery", "LAN கண்டுபிடிப்பை இயக்கு"), + ("Deny LAN discovery", "LAN கண்டுபிடிப்பை மறு"), + ("Write a message", "ஒரு செய்தி எழுது"), + ("Prompt", "தூண்டுதல்"), + ("Please wait for confirmation of UAC...", "UAC உறுதிப்படுத்தலுக்காக காத்திருக்கவும்..."), + ("elevated_foreground_window_tip", "முன்னணி_சாளர_உயர்வு_குறிப்பு"), + ("Disconnected", "துண்டிக்கப்பட்டது"), + ("Other", "மற்றவை"), + ("Confirm before closing multiple tabs", "பல தாவல்களை மூடுவதற்கு முன் உறுதிப்படுத்து"), + ("Keyboard Settings", "விசைப்பலகை அமைப்புகள்"), + ("Full Access", "முழு அணுகல்"), + ("Screen Share", "திரை பகிர்வு"), + ("ubuntu-21-04-required", "Wayland க்கு Ubuntu 21.04+ தேவை"), + ("wayland-requires-higher-linux-version", "Wayland க்கு உயர் Linux பதிப்பு தேவை. X11 முயற்சிக்கவும் அல்லது OS மாற்றவும்."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "ஜம்ப் லிங்க்"), + ("Please Select the screen to be shared(Operate on the peer side).", "பகிரப்பட வேண்டிய திரை தேர்ந்தெடுக்கவும்"), + ("Show RustDesk", "RustDesk ஐ காட்டு"), + ("This PC", "இந்த PC"), + ("or", "அல்லது"), + ("Elevate", "உயர்த்து"), + ("Zoom cursor", "கர்சரை பெரிதாக்கு"), + ("Accept sessions via password", "கடவுச்சொல் வழியாக அமர்வுகளை ஏற்று"), + ("Accept sessions via click", "கிளிக் வழியாக அமர்வுகளை ஏற்று"), + ("Accept sessions via both", "இரண்டு வழியிலும் அமர்வுகளை ஏற்று"), + ("Please wait for the remote side to accept your session request...", "அமர்வு கோரிக்கை ஏற்பதற்காக காத்திருக்கவும்..."), + ("One-time Password", "ஒருமுறை கடவுச்சொல்"), + ("Use one-time password", "ஒருமுறை கடவுச்சொல் பயன்படுத்து"), + ("One-time password length", "ஒருமுறை கடவுச்சொல் நீளம்"), + ("Request access to your device", "உங்கள் சாதனத்திற்கு அணுகல் கோரவும்"), + ("Hide connection management window", "இணைப்பு மேலாண்மை சாளரத்தை மறை"), + ("hide_cm_tip", "இணைப்பு_மேலாளர்_மறை_குறிப்பு"), + ("wayland_experiment_tip", "வேலேண்ட்_சோதனை_குறிப்பு"), + ("Right click to select tabs", "தாவல்களைத் தேர்ந்தெடுக்க வலது கிளிக் செய்யவும்"), + ("Skipped", "தவிர்க்கப்பட்டது"), + ("Add to address book", "முகவரி புத்தகத்தில் சேர்"), + ("Group", "குழு"), + ("Search", "தேடு"), + ("Closed manually by web console", "வெப் கன்சோலால் மூடப்பட்டது"), + ("Local keyboard type", "உள்ளூர் விசைபலகை வகை"), + ("Select local keyboard type", "உள்ளூர் விசைபலகை வகை தேர்வு"), + ("software_render_tip", "மென்பொருள்_ரெண்டர்_குறிப்பு"), + ("Always use software rendering", "எப்போதும் மென்பொருள் ரெண்டரிங்"), + ("config_input", "உள்ளீடு கட்டுப்பாட்டு அனுமதி தேவை"), + ("config_microphone", "மைக்ரோஃபோன் அனுமதி தேவை"), + ("request_elevation_tip", "உயர்வு_கோரிக்கை_குறிப்பு"), + ("Wait", "காத்திரு"), + ("Elevation Error", "உயர்வு பிழை"), + ("Ask the remote user for authentication", "தொலை பயனர் அங்கீகாரம் கோரு"), + ("Choose this if the remote account is administrator", "தொலை கணக்கு நிர்வாகி எனில் தேர்வு"), + ("Transmit the username and password of administrator", "நிர்வாகி பயனர்பெயர் கடவுச்சொல் அனுப்பு"), + ("still_click_uac_tip", "uac_ஐ_இன்னும்_சொடுக்கவும்_குறிப்பு"), + ("Request Elevation", "உயர்வு கோரிக்கை"), + ("wait_accept_uac_tip", "uac_ஏற்புக்காக_காத்திரு_குறிப்பு"), + ("Elevate successfully", "வெற்றிகரமாக உயர்த்தப்பட்டது"), + ("uppercase", "பெரிய எழுத்து"), + ("lowercase", "சிறிய எழுத்து"), + ("digit", "எண்"), + ("special character", "சிறப்பு எழுத்து"), + ("length>=8", "நீளம்>=8"), + ("Weak", "பலவீனம்"), + ("Medium", "நடுத்தரம்"), + ("Strong", "வலுவான"), + ("Switch Sides", "பக்கம் மாற்று"), + ("Please confirm if you want to share your desktop?", "டெஸ்க்டாப் பகிர உறுதிப்படுத்தவும்?"), + ("Display", "காட்சி"), + ("Default View Style", "இயல்புநிலை காட்சி பாணி"), + ("Default Scroll Style", "இயல்புநிலை ஸ்க்ரோல் பாணி"), + ("Default Image Quality", "இயல்புநிலை படத்தரம்"), + ("Default Codec", "இயல்புநிலை கோடெக்"), + ("Bitrate", "பிட்ரேட்"), + ("FPS", "FPS"), + ("Auto", "தானியங்கு"), + ("Other Default Options", "பிற இயல்புநிலை விருப்பங்கள்"), + ("Voice call", "குரல் அழைப்பு"), + ("Text chat", "உரை அரட்டை"), + ("Stop voice call", "குரல் அழைப்பு நிறுத்து"), + ("relay_hint_tip", "ரிலே_குறிப்பு_குறிப்பு"), + ("Reconnect", "மீண்டும் இணை"), + ("Codec", "கோடெக்"), + ("Resolution", "தெளிவுத்திறன்"), + ("No transfers in progress", "பரிமாற்றம் எதுவும் நடைபெறவில்லை"), + ("Set one-time password length", "ஒருமுறை கடவுச்சொல் நீளம் அமை"), + ("RDP Settings", "RDP அமைப்புகள்"), + ("Sort by", "வரிசைப்படுத்து"), + ("New Connection", "புதிய இணைப்பு"), + ("Restore", "மீட்டமை"), + ("Minimize", "குறைக்கவும்"), + ("Maximize", "பெரிதாக்கு"), + ("Your Device", "உங்கள் சாதனம்"), + ("empty_recent_tip", "காலி_சமீபத்திய_குறிப்பு"), + ("empty_favorite_tip", "காலி_விருப்பமான_குறிப்பு"), + ("empty_lan_tip", "காலி_லேன்_குறிப்பு"), + ("empty_address_book_tip", "காலி_முகவரி_புத்தக_குறிப்பு"), + ("Empty Username", "காலி பயனர்பெயர்"), + ("Empty Password", "காலி கடவுச்சொல்"), + ("Me", "நான்"), + ("identical_file_tip", "ஒரே_மாதிரியான_கோப்பு_குறிப்பு"), + ("show_monitors_tip", "மானிட்டர்களை_காட்டு_குறிப்பு"), + ("View Mode", "காட்சி முறை"), + ("login_linux_tip", "லினக்ஸ்_உள்நுழைவு_குறிப்பு"), + ("verify_rustdesk_password_tip", "rustdesk_கடவுச்சொல்_சரிபார்ப்பு_குறிப்பு"), + ("remember_account_tip", "கணக்கை_நினைவில்_கொள்_குறிப்பு"), + ("os_account_desk_tip", "os_கணக்கு_டெஸ்க்_குறிப்பு"), + ("OS Account", "OS கணக்கு"), + ("another_user_login_title_tip", "மற்றொரு_பயனர்_உள்நுழைவு_தலைப்பு_குறிப்பு"), + ("another_user_login_text_tip", "மற்றொரு_பயனர்_உள்நுழைவு_உரை_குறிப்பு"), + ("xorg_not_found_title_tip", "xorg_காணப்படவில்லை_தலைப்பு_குறிப்பு"), + ("xorg_not_found_text_tip", "xorg_காணப்படவில்லை_உரை_குறிப்பு"), + ("no_desktop_title_tip", "டெஸ்க்டாப்_இல்லை_தலைப்பு_குறிப்பு"), + ("no_desktop_text_tip", "டெஸ்க்டாப்_இல்லை_உரை_குறிப்பு"), + ("No need to elevate", "உயர்த்த தேவையில்லை"), + ("System Sound", "சிஸ்டம் ஒலி"), + ("Default", "இயல்புநிலை"), + ("New RDP", "புதிய RDP"), + ("Fingerprint", "கைரேகை"), + ("Copy Fingerprint", "கைரேகை நகல்"), + ("no fingerprints", "கைரேகைகள் இல்லை"), + ("Select a peer", "பியர் தேர்வு"), + ("Select peers", "பியர்கள் தேர்வு"), + ("Plugins", "இணைப்புகள்"), + ("Uninstall", "நிறுவல் நீக்கு"), + ("Update", "புதுப்பி"), + ("Enable", "இயக்கு"), + ("Disable", "அணை"), + ("Options", "விருப்பங்கள்"), + ("resolution_original_tip", "அசல் தெளிவுத்திறன்"), + ("resolution_fit_local_tip", "உள்ளூர் பொருத்தம்"), + ("resolution_custom_tip", "தனிப்பயன் தெளிவுத்திறன்"), + ("Collapse toolbar", "கருவிப்பட்டி மூடு"), + ("Accept and Elevate", "ஏற்று உயர்த்து"), + ("accept_and_elevate_btn_tooltip", "ஏற்று_உயர்த்து_பொத்தான்_குறிப்பு"), + ("clipboard_wait_response_timeout_tip", "கிளிப்போர்டு_பதில்_நேரமுடிவு_குறிப்பு"), + ("Incoming connection", "உள்வரும் இணைப்பு"), + ("Outgoing connection", "வெளியேறும் இணைப்பு"), + ("Exit", "வெளியேறு"), + ("Open", "திற"), + ("logout_tip", "வெளியேறு_குறிப்பு"), + ("Service", "சேவை"), + ("Start", "தொடங்கு"), + ("Stop", "நிறுத்து"), + ("exceed_max_devices", "அதிகபட்ச சாதனங்களை மீறியது"), + ("Sync with recent sessions", "சமீபத்திய அமர்வுகளுடன் ஒத்திசை"), + ("Sort tags", "குறிச்சொற்கள் வரிசை"), + ("Open connection in new tab", "புதிய தாவலில் இணைப்பு திற"), + ("Move tab to new window", "தாவல் புதிய சாளரத்துக்கு நகர்த்து"), + ("Can not be empty", "காலியாக முடியாது"), + ("Already exists", "ஏற்கனவே உள்ளது"), + ("Change Password", "கடவுச்சொல் மாற்று"), + ("Refresh Password", "கடவுச்சொல் புதுப்பி"), + ("ID", "ஐடி"), + ("Grid View", "கிரிட் காட்சி"), + ("List View", "பட்டியல் காட்சி"), + ("Select", "தேர்வு"), + ("Toggle Tags", "குறிச்சொற்கள் மாற்று"), + ("pull_ab_failed_tip", "முகவரி புத்தகம் புதுப்பிப்பு தோல்வி"), + ("push_ab_failed_tip", "முகவரி புத்தகம் சிங்க் தோல்வி"), + ("synced_peer_readded_tip", "சிங்க் பியர் மீண்டும் சேர்க்கப்பட்டது"), + ("Change Color", "நிறம் மாற்று"), + ("Primary Color", "முதன்மை நிறம்"), + ("HSV Color", "HSV நிறம்"), + ("Installation Successful!", "நிறுவல் வெற்றி!"), + ("Installation failed!", "நிறுவல் தோல்வி!"), + ("Reverse mouse wheel", "சுட்டி சக்கரம் தலைகீழ்"), + ("{} sessions", "{} அமர்வுகள்"), + ("scam_title", "மோசடி எச்சரிக்கை"), + ("scam_text1", "தொலைபேசி மோசடியின் பலியாகலாம்!"), + ("scam_text2", "RustDesk ஊழியர் இவ்வாறு தொடர்பு கொள்ள மாட்டார்கள்"), + ("Don't show again", "மீண்டும் காட்ட வேண்டாம்"), + ("I Agree", "ஏற்கிறேன்"), + ("Decline", "மறு"), + ("Timeout in minutes", "நிமிடங்களில் நேரமுடிவு"), + ("auto_disconnect_option_tip", "தானியங்கு துண்டிப்பு விருப்பம்"), + ("Connection failed due to inactivity", "செயலின்மையால் இணைப்பு தோல்வி"), + ("Check for software update on startup", "தொடக்கத்தில் மென்பொருள் புதுப்பிப்பு சரிபார்"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "RustDesk Server Pro {} க்கு மேம்படுத்து"), + ("pull_group_failed_tip", "குழு இழுக்க தோல்வி"), + ("Filter by intersection", "குறுக்குவெட்டால் வடிகட்டு"), + ("Remove wallpaper during incoming sessions", "உள்வரும் அமர்வுகளில் வால்பேப்பர் நீக்கு"), + ("Test", "சோதனை"), + ("display_is_plugged_out_msg", "காட்சி அடாப்டர் துண்டிக்கப்பட்டது"), + ("No displays", "காட்சிகள் இல்லை"), + ("Open in new window", "புதிய சாளரத்தில் திற"), + ("Show displays as individual windows", "காட்சிகளை தனி சாளரங்களாக காட்டு"), + ("Use all my displays for the remote session", "அனைத்து காட்சிகளையும் தொலை அமர்வுக்கு பயன்படுத்து"), + ("selinux_tip", "SELinux இயக்கப்பட்டது, RustDesk அனுமதி வேண்டும்"), + ("Change view", "காட்சி மாற்று"), + ("Big tiles", "பெரிய ஓடுகள்"), + ("Small tiles", "சிறிய ஓடுகள்"), + ("List", "பட்டியல்"), + ("Virtual display", "மெய்நிகர் காட்சி"), + ("Plug out all", "அனைத்தையும் துண்டி"), + ("True color (4:4:4)", "உண்மை நிறம் (4:4:4)"), + ("Enable blocking user input", "பயனர் உள்ளீடு தடுப்பு இயக்கு"), + ("id_input_tip", "ஐடி உள்ளீடு எழுத்துகள் எண்கள் மட்டும்"), + ("privacy_mode_impl_mag_tip", "Windows Magnifier API"), + ("privacy_mode_impl_virtual_display_tip", "Virtual Display Driver"), + ("Enter privacy mode", "தனியுரிமை முறையில் நுழை"), + ("Exit privacy mode", "தனியுரிமை முறையிலிருந்து வெளியேறு"), + ("idd_not_support_under_win10_2004_tip", "Virtual Display Driver Windows 10 2004 க்கு கீழ் ஆதரவில்லை"), + ("input_source_1_tip", "உள்ளீடு மூலம் = விசைப்பலகை"), + ("input_source_2_tip", "உள்ளீடு மூலம் = சுட்டி"), + ("Swap control-command key", "control-command விசை மாற்று"), + ("swap-left-right-mouse", "இடது-வலது சுட்டி மாற்று"), + ("2FA code", "2FA குறியீடு"), + ("More", "மேலும்"), + ("enable-2fa-title", "இரு காரணி அங்கீகாரம் இயக்கு"), + ("enable-2fa-desc", "RustDesk இரு காரணி அங்கீகாரம் ஆதரிக்கிறது"), + ("wrong-2fa-code", "தவறான 2FA குறியீடு"), + ("enter-2fa-title", "2FA குறியீடு உள்ளிடு"), + ("Email verification code must be 6 characters.", "மின்னஞ்சல் சரிபார்ப்பு 6 எழுத்துகள்"), + ("2FA code must be 6 digits.", "2FA குறியீடு 6 எண்கள்"), + ("Multiple Windows sessions found", "பல Windows அமர்வுகள் கண்டறியப்பட்டன"), + ("Please select the session you want to connect to", "இணைக்க விரும்பும் அமர்வு தேர்வு"), + ("powered_by_me", "என்னால் இயக்கப்படுகிறது"), + ("outgoing_only_desk_tip", "வெளியேறும் அமர்வுகள் மட்டும் ஆதரவு"), + ("preset_password_warning", "முன்னமைவு கடவுச்சொல் எச்சரிக்கை"), + ("Security Alert", "பாதுகாப்பு எச்சரிக்கை"), + ("My address book", "எனது முகவரி புத்தகம்"), + ("Personal", "தனிப்பட்ட"), + ("Owner", "உரிமையாளர்"), + ("Set shared password", "பகிரப்பட்ட கடவுச்சொல் அமை"), + ("Exist in", "இல் உள்ளது"), + ("Read-only", "படிக்க மட்டும்"), + ("Read/Write", "படி/எழுது"), + ("Full Control", "முழு கட்டுப்பாடு"), + ("share_warning_tip", "பியர் பகிர்வு அனுமதி தேவை"), + ("Everyone", "அனைவரும்"), + ("ab_web_console_tip", "வெப் கன்சோலில் முகவரி புத்தகம் நிர்வகி"), + ("allow-only-conn-window-open-tip", "இணைப்பு_சாளரம்_திறக்க_மட்டும்_அனுமதி_குறிப்பு"), + ("no_need_privacy_mode_no_physical_displays_tip", "தனியுரிமை_முறை_தேவையில்லை_பருப்பொருள்_காட்சிகள்_இல்லை_குறிப்பு"), + ("Follow remote cursor", "தொலை கர்சர் பின்பற்று"), + ("Follow remote window focus", "தொலை சாளர கவனம் பின்பற்று"), + ("default_proxy_tip", "இயல்புநிலை_ப்ராக்ஸி_குறிப்பு"), + ("no_audio_input_device_tip", "ஒலி_உள்ளீட்டு_சாதனம்_இல்லை_குறிப்பு"), + ("Incoming", "உள்வரும்"), + ("Outgoing", "வெளியேறும்"), + ("Clear Wayland screen selection", "Wayland திரை தேர்வு அழி"), + ("clear_Wayland_screen_selection_tip", "wayland_திரை_தேர்வு_அழி_குறிப்பு"), + ("confirm_clear_Wayland_screen_selection_tip", "wayland_திரை_தேர்வு_அழிக்க_உறுதிப்படுத்து_குறிப்பு"), + ("android_new_voice_call_tip", "android_புதிய_குரல்_அழைப்பு_குறிப்பு"), + ("texture_render_tip", "டெக்ஸ்ச்சர்_ரெண்டர்_குறிப்பு"), + ("Use texture rendering", "texture ரெண்டரிங் பயன்படுத்து"), + ("Floating window", "மிதக்கும் சாளரம்"), + ("floating_window_tip", "மிதக்கும்_சாளரம்_குறிப்பு"), + ("Keep screen on", "திரை இயக்கத்தில் வை"), + ("Never", "ஒருபோதும் இல்லை"), + ("During controlled", "கட்டுப்படுத்தும்போது"), + ("During service is on", "சேவை இயக்கத்தில் இருக்கும்போது"), + ("Capture screen using DirectX", "DirectX பயன்படுத்தி திரை பிடிப்பு"), + ("Back", "பின்"), + ("Apps", "ஆப்ஸ்"), + ("Volume up", "ஒலி அதிகரி"), + ("Volume down", "ஒலி குறை"), + ("Power", "மின் பட்டன்"), + ("Telegram bot", "Telegram போட்"), + ("enable-bot-tip", "போட்_இயக்க_குறிப்பு"), + ("enable-bot-desc", "RustDesk Telegram போட் ஆதரிக்கிறது"), + ("cancel-2fa-confirm-tip", "2fa_ரத்து_உறுதி_குறிப்பு"), + ("cancel-bot-confirm-tip", "போட்_ரத்து_உறுதி_குறிப்பு"), + ("About RustDesk", "RustDesk பற்றி"), + ("Send clipboard keystrokes", "கிளிப்போர்டு விசைத்தள உள்ளீடு அனுப்பு"), + ("network_error_tip", "நெட்வொர்க்_பிழை_குறிப்பு"), + ("Unlock with PIN", "PIN உடன் திற"), + ("Requires at least {} characters", "குறைந்தது {} எழுத்துகள் தேவை"), + ("Wrong PIN", "தவறான PIN"), + ("Set PIN", "PIN அமை"), + ("Enable trusted devices", "நம்பகமான சாதனங்கள் இயக்கு"), + ("Manage trusted devices", "நம்பகமான சாதனங்கள் நிர்வகி"), + ("Platform", "இயங்குதளம்"), + ("Days remaining", "மீதமுள்ள நாட்கள்"), + ("enable-trusted-devices-tip", "நம்பகமான_சாதனங்கள்_இயக்க_குறிப்பு"), + ("Parent directory", "மேல் கோப்பகம்"), + ("Resume", "தொடர்"), + ("Invalid file name", "தவறான கோப்பு பெயர்"), + ("one-way-file-transfer-tip", "ஒருவழி_கோப்பு_பரிமாற்ற_குறிப்பு"), + ("Authentication Required", "அங்கீகாரம் தேவை"), + ("Authenticate", "அங்கீகரி"), + ("web_id_input_tip", "வலை_ஐடி_உள்ளீடு_குறிப்பு"), + ("Download", "பதிவிறக்கு"), + ("Upload folder", "கோப்பகம் ஏற்று"), + ("Upload files", "கோப்புகள் ஏற்று"), + ("Clipboard is synchronized", "கிளிப்போர்டு ஒத்திசைக்கப்பட்டது"), + ("Update client clipboard", "கிளையன் கிளிப்போர்டு புதுப்பி"), + ("Untagged", "குறிச்சொல் இல்லாத"), + ("new-version-of-{}-tip", "{}_புதிய_பதிப்பு_குறிப்பு"), + ("Accessible devices", "அணுகக்கூடிய சாதனங்கள்"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "ரிமோட்_rustdesk_கிளையன்டை_{}_மேம்படுத்து_குறிப்பு"), + ("d3d_render_tip", "d3d_ரெண்டர்_குறிப்பு"), + ("Use D3D rendering", "D3D ரெண்டரிங் பயன்படுத்து"), + ("Printer", "அச்சுப்பொறி"), + ("printer-os-requirement-tip", "பிரிண்டர்_os_தேவை_குறிப்பு"), + ("printer-requires-installed-{}-client-tip", "பிரிண்டர்_தேவை_நிறுவப்பட்ட_{}_கிளையண்ட்_குறிப்பு"), + ("printer-{}-not-installed-tip", "பிரிண்டர்_{}_நிறுவப்படவில்லை_குறிப்பு"), + ("printer-{}-ready-tip", "பிரிண்டர்_{}_தயார்_குறிப்பு"), + ("Install {} Printer", "{} அச்சுப்பொறி நிறுவு"), + ("Outgoing Print Jobs", "வெளியேறும் அச்சு வேலைகள்"), + ("Incoming Print Jobs", "உள்வரும் அச்சு வேலைகள்"), + ("Incoming Print Job", "உள்வரும் அச்சு வேலை"), + ("use-the-default-printer-tip", "இயல்புநிலை_அச்சுப்பொறியை_பயன்படுத்து_குறிப்பு"), + ("use-the-selected-printer-tip", "தேர்ந்தெடுக்கப்பட்ட_அச்சுப்பொறியை_பயன்படுத்து_குறிப்பு"), + ("auto-print-tip", "தானியங்கு_அச்சு_குறிப்பு"), + ("print-incoming-job-confirm-tip", "உள்வரும்_அச்சு_வேலையை_உறுதிப்படுத்து_குறிப்பு"), + ("remote-printing-disallowed-tile-tip", "ரிமோட்_அச்சிடுதல்_அனுமதிக்கப்படாத_டைல்_குறிப்பு"), + ("remote-printing-disallowed-text-tip", "ரிமோட்_அச்சிடுதல்_அனுமதிக்கப்படாத_உரை_குறிப்பு"), + ("save-settings-tip", "அமைப்புகளை_சேமி_குறிப்பு"), + ("dont-show-again-tip", "மீண்டும்_காட்டாதே_குறிப்பு"), + ("Take screenshot", "திரைப்பிடிப்பு எடு"), + ("Taking screenshot", "திரைப்பிடிப்பு எடுத்துக்கொண்டிருக்கிறது"), + ("screenshot-merged-screen-not-supported-tip", "ஸ்கிரீன்ஷாட்_இணைக்கப்பட்ட_திரை_ஆதரவற்ற_குறிப்பு"), + ("screenshot-action-tip", "ஸ்கிரீன்ஷாட்_செயல்_குறிப்பு"), + ("Save as", "இப்படி சேமி"), + ("Copy to clipboard", "கிளிப்போர்டில் நகல்"), + ("Enable remote printer", "தொலை அச்சுப்பொறி இயக்கு"), + ("Downloading {}", "{} பதிவிறக்குகிறது"), + ("{} Update", "{} புதுப்பிப்பு"), + ("{}-to-update-tip", "{}_புதுப்பிக்க_குறிப்பு"), + ("download-new-version-failed-tip", "புதிய_பதிப்பு_பதிவிறக்கம்_தோல்வி_குறிப்பு"), + ("Auto update", "தானியங்கு புதுப்பிப்பு"), + ("update-failed-check-msi-tip", "புதுப்பிப்பு_தோல்வி_எம்எஸ்ஐ_சரிபார்_குறிப்பு"), + ("websocket_tip", "வெப்சாக்கெட்_குறிப்பு"), + ("Use WebSocket", "WebSocket பயன்படுத்து"), + ("Trackpad speed", "டிராக்பேட் வேகம்"), + ("Default trackpad speed", "இயல்புநிலை டிராக்பேட் வேகம்"), + ("Numeric one-time password", "எண் ஒருமுறை கடவுச்சொல்"), + ("Enable IPv6 P2P connection", "IPv6 P2P இணைப்பு இயக்கு"), + ("Enable UDP hole punching", "UDP hole punching இயக்கு"), + ("View camera", "கேமரா பார்"), + ("Enable camera", "கேமரா இயக்கு"), + ("No cameras", "கேமராக்கள் இல்லை"), + ("view_camera_unsupported_tip", "கேமரா_காட்சி_ஆதரவற்ற_குறிப்பு"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "{} உடன் தொடர்"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/template.rs b/vendor/rustdesk/src/lang/template.rs new file mode 100644 index 0000000..5e25801 --- /dev/null +++ b/vendor/rustdesk/src/lang/template.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", ""), + ("Your Desktop", ""), + ("desk_tip", ""), + ("Password", ""), + ("Ready", ""), + ("Established", ""), + ("connecting_status", ""), + ("Enable service", ""), + ("Start service", ""), + ("Service is running", ""), + ("Service is not running", ""), + ("not_ready_status", ""), + ("Control Remote Desktop", ""), + ("Transfer file", ""), + ("Connect", ""), + ("Recent sessions", ""), + ("Address book", ""), + ("Confirmation", ""), + ("TCP tunneling", ""), + ("Remove", ""), + ("Refresh random password", ""), + ("Set your own password", ""), + ("Enable keyboard/mouse", ""), + ("Enable clipboard", ""), + ("Enable file transfer", ""), + ("Enable TCP tunneling", ""), + ("IP Whitelisting", ""), + ("ID/Relay Server", ""), + ("Import server config", ""), + ("Export Server Config", ""), + ("Import server configuration successfully", ""), + ("Export server configuration successfully", ""), + ("Invalid server configuration", ""), + ("Clipboard is empty", ""), + ("Stop service", ""), + ("Change ID", ""), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", ""), + ("Website", ""), + ("About", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), + ("Audio Input", ""), + ("Enhancements", ""), + ("Hardware Codec", ""), + ("Adaptive bitrate", ""), + ("ID Server", ""), + ("Relay Server", ""), + ("API Server", ""), + ("invalid_http", ""), + ("Invalid IP", ""), + ("Invalid format", ""), + ("server_not_support", ""), + ("Not available", ""), + ("Too frequent", ""), + ("Cancel", ""), + ("Skip", ""), + ("Close", ""), + ("Retry", ""), + ("OK", ""), + ("Password Required", ""), + ("Please enter your password", ""), + ("Remember password", ""), + ("Wrong Password", ""), + ("Do you want to enter again?", ""), + ("Connection Error", ""), + ("Error", ""), + ("Reset by the peer", ""), + ("Connecting...", ""), + ("Connection in progress. Please wait.", ""), + ("Please try 1 minute later", ""), + ("Login Error", ""), + ("Successful", ""), + ("Connected, waiting for image...", ""), + ("Name", ""), + ("Type", ""), + ("Modified", ""), + ("Size", ""), + ("Show Hidden Files", ""), + ("Receive", ""), + ("Send", ""), + ("Refresh File", ""), + ("Local", ""), + ("Remote", ""), + ("Remote Computer", ""), + ("Local Computer", ""), + ("Confirm Delete", ""), + ("Delete", ""), + ("Properties", ""), + ("Multi Select", ""), + ("Select All", ""), + ("Unselect All", ""), + ("Empty Directory", ""), + ("Not an empty directory", ""), + ("Are you sure you want to delete this file?", ""), + ("Are you sure you want to delete this empty directory?", ""), + ("Are you sure you want to delete the file of this directory?", ""), + ("Do this for all conflicts", ""), + ("This is irreversible!", ""), + ("Deleting", ""), + ("files", ""), + ("Waiting", ""), + ("Finished", ""), + ("Speed", ""), + ("Custom Image Quality", ""), + ("Privacy mode", ""), + ("Block user input", ""), + ("Unblock user input", ""), + ("Adjust Window", ""), + ("Original", ""), + ("Shrink", ""), + ("Stretch", ""), + ("Scrollbar", ""), + ("ScrollAuto", ""), + ("Good image quality", ""), + ("Balanced", ""), + ("Optimize reaction time", ""), + ("Custom", ""), + ("Show remote cursor", ""), + ("Show quality monitor", ""), + ("Disable clipboard", ""), + ("Lock after session end", ""), + ("Insert Ctrl + Alt + Del", ""), + ("Insert Lock", ""), + ("Refresh", ""), + ("ID does not exist", ""), + ("Failed to connect to rendezvous server", ""), + ("Please try later", ""), + ("Remote desktop is offline", ""), + ("Key mismatch", ""), + ("Timeout", ""), + ("Failed to connect to relay server", ""), + ("Failed to connect via rendezvous server", ""), + ("Failed to connect via relay server", ""), + ("Failed to make direct connection to remote desktop", ""), + ("Set Password", ""), + ("OS Password", ""), + ("install_tip", ""), + ("Click to upgrade", ""), + ("Configure", ""), + ("config_acc", ""), + ("config_screen", ""), + ("Installing ...", ""), + ("Install", ""), + ("Installation", ""), + ("Installation Path", ""), + ("Create start menu shortcuts", ""), + ("Create desktop icon", ""), + ("agreement_tip", ""), + ("Accept and Install", ""), + ("End-user license agreement", ""), + ("Generating ...", ""), + ("Your installation is lower version.", ""), + ("not_close_tcp_tip", ""), + ("Listening ...", ""), + ("Remote Host", ""), + ("Remote Port", ""), + ("Action", ""), + ("Add", ""), + ("Local Port", ""), + ("Local Address", ""), + ("Change Local Port", ""), + ("setup_server_tip", ""), + ("Too short, at least 6 characters.", ""), + ("The confirmation is not identical.", ""), + ("Permissions", ""), + ("Accept", ""), + ("Dismiss", ""), + ("Disconnect", ""), + ("Enable file copy and paste", ""), + ("Connected", ""), + ("Direct and encrypted connection", ""), + ("Relayed and encrypted connection", ""), + ("Direct and unencrypted connection", ""), + ("Relayed and unencrypted connection", ""), + ("Enter Remote ID", ""), + ("Enter your password", ""), + ("Logging in...", ""), + ("Enable RDP session sharing", ""), + ("Auto Login", ""), + ("Enable direct IP access", ""), + ("Rename", ""), + ("Space", ""), + ("Create desktop shortcut", ""), + ("Change Path", ""), + ("Create Folder", ""), + ("Please enter the folder name", ""), + ("Fix it", ""), + ("Warning", ""), + ("Login screen using Wayland is not supported", ""), + ("Reboot required", ""), + ("Unsupported display server", ""), + ("x11 expected", ""), + ("Port", ""), + ("Settings", ""), + ("Username", ""), + ("Invalid port", ""), + ("Closed manually by the peer", ""), + ("Enable remote configuration modification", ""), + ("Run without install", ""), + ("Connect via relay", ""), + ("Always connect via relay", ""), + ("whitelist_tip", ""), + ("Login", ""), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), + ("Logout", ""), + ("Tags", ""), + ("Search ID", ""), + ("whitelist_sep", ""), + ("Add ID", ""), + ("Add Tag", ""), + ("Unselect all tags", ""), + ("Network error", ""), + ("Username missed", ""), + ("Password missed", ""), + ("Wrong credentials", ""), + ("The verification code is incorrect or has expired", ""), + ("Edit Tag", ""), + ("Forget Password", ""), + ("Favorites", ""), + ("Add to Favorites", ""), + ("Remove from Favorites", ""), + ("Empty", ""), + ("Invalid folder name", ""), + ("Socks5 Proxy", ""), + ("Socks5/Http(s) Proxy", ""), + ("Discovered", ""), + ("install_daemon_tip", ""), + ("Remote ID", ""), + ("Paste", ""), + ("Paste here?", ""), + ("Are you sure to close the connection?", ""), + ("Download new version", ""), + ("Touch mode", ""), + ("Mouse mode", ""), + ("One-Finger Tap", ""), + ("Left Mouse", ""), + ("One-Long Tap", ""), + ("Two-Finger Tap", ""), + ("Right Mouse", ""), + ("One-Finger Move", ""), + ("Double Tap & Move", ""), + ("Mouse Drag", ""), + ("Three-Finger vertically", ""), + ("Mouse Wheel", ""), + ("Two-Finger Move", ""), + ("Canvas Move", ""), + ("Pinch to Zoom", ""), + ("Canvas Zoom", ""), + ("Reset canvas", ""), + ("No permission of file transfer", ""), + ("Note", ""), + ("Connection", ""), + ("Share screen", ""), + ("Chat", ""), + ("Total", ""), + ("items", ""), + ("Selected", ""), + ("Screen Capture", ""), + ("Input Control", ""), + ("Audio Capture", ""), + ("Do you accept?", ""), + ("Open System Setting", ""), + ("How to get Android input permission?", ""), + ("android_input_permission_tip1", ""), + ("android_input_permission_tip2", ""), + ("android_new_connection_tip", ""), + ("android_service_will_start_tip", ""), + ("android_stop_service_tip", ""), + ("android_version_audio_tip", ""), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), + ("Account", ""), + ("Overwrite", ""), + ("This file exists, skip or overwrite this file?", ""), + ("Quit", ""), + ("Help", ""), + ("Failed", ""), + ("Succeeded", ""), + ("Someone turns on privacy mode, exit", ""), + ("Unsupported", ""), + ("Peer denied", ""), + ("Please install plugins", ""), + ("Peer exit", ""), + ("Failed to turn off", ""), + ("Turned off", ""), + ("Language", ""), + ("Keep RustDesk background service", ""), + ("Ignore Battery Optimizations", ""), + ("android_open_battery_optimizations_tip", ""), + ("Start on boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), + ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), + ("Translate mode", ""), + ("Use permanent password", ""), + ("Use both passwords", ""), + ("Set permanent password", ""), + ("Enable remote restart", ""), + ("Restart remote device", ""), + ("Are you sure you want to restart", ""), + ("Restarting remote device", ""), + ("remote_restarting_tip", ""), + ("Copied", ""), + ("Exit Fullscreen", ""), + ("Fullscreen", ""), + ("Mobile Actions", ""), + ("Select Monitor", ""), + ("Control Actions", ""), + ("Display Settings", ""), + ("Ratio", ""), + ("Image Quality", ""), + ("Scroll Style", ""), + ("Show Toolbar", ""), + ("Hide Toolbar", ""), + ("Direct Connection", ""), + ("Relay Connection", ""), + ("Secure Connection", ""), + ("Insecure Connection", ""), + ("Scale original", ""), + ("Scale adaptive", ""), + ("General", ""), + ("Security", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Light Theme", ""), + ("Dark", ""), + ("Light", ""), + ("Follow System", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable audio", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Pin Toolbar", ""), + ("Unpin Toolbar", ""), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Automatically record outgoing sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), + ("Enable recording session", ""), + ("Enable LAN discovery", ""), + ("Deny LAN discovery", ""), + ("Write a message", ""), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), + ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), + ("Full Access", ""), + ("Screen Share", ""), + ("ubuntu-21-04-required", ""), + ("wayland-requires-higher-linux-version", ""), + ("xdp-portal-unavailable", ""), + ("JumpLink", ""), + ("Please Select the screen to be shared(Operate on the peer side).", ""), + ("Show RustDesk", ""), + ("This PC", ""), + ("or", ""), + ("Elevate", ""), + ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), + ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), + ("Skipped", ""), + ("Add to address book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), + ("Select a peer", ""), + ("Select peers", ""), + ("Plugins", ""), + ("Uninstall", ""), + ("Update", ""), + ("Enable", ""), + ("Disable", ""), + ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), + ("Collapse toolbar", ""), + ("Accept and Elevate", ""), + ("accept_and_elevate_btn_tooltip", ""), + ("clipboard_wait_response_timeout_tip", ""), + ("Incoming connection", ""), + ("Outgoing connection", ""), + ("Exit", ""), + ("Open", ""), + ("logout_tip", ""), + ("Service", ""), + ("Start", ""), + ("Stop", ""), + ("exceed_max_devices", ""), + ("Sync with recent sessions", ""), + ("Sort tags", ""), + ("Open connection in new tab", ""), + ("Move tab to new window", ""), + ("Can not be empty", ""), + ("Already exists", ""), + ("Change Password", ""), + ("Refresh Password", ""), + ("ID", ""), + ("Grid View", ""), + ("List View", ""), + ("Select", ""), + ("Toggle Tags", ""), + ("pull_ab_failed_tip", ""), + ("push_ab_failed_tip", ""), + ("synced_peer_readded_tip", ""), + ("Change Color", ""), + ("Primary Color", ""), + ("HSV Color", ""), + ("Installation Successful!", ""), + ("Installation failed!", ""), + ("Reverse mouse wheel", ""), + ("{} sessions", ""), + ("scam_title", ""), + ("scam_text1", ""), + ("scam_text2", ""), + ("Don't show again", ""), + ("I Agree", ""), + ("Decline", ""), + ("Timeout in minutes", ""), + ("auto_disconnect_option_tip", ""), + ("Connection failed due to inactivity", ""), + ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), + ("pull_group_failed_tip", ""), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), + ("idd_not_support_under_win10_2004_tip", ""), + ("input_source_1_tip", ""), + ("input_source_2_tip", ""), + ("Swap control-command key", ""), + ("swap-left-right-mouse", ""), + ("2FA code", ""), + ("More", ""), + ("enable-2fa-title", ""), + ("enable-2fa-desc", ""), + ("wrong-2fa-code", ""), + ("enter-2fa-title", ""), + ("Email verification code must be 6 characters.", ""), + ("2FA code must be 6 digits.", ""), + ("Multiple Windows sessions found", ""), + ("Please select the session you want to connect to", ""), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("share_warning_tip", ""), + ("Everyone", ""), + ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", ""), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", ""), + ("Floating window", ""), + ("floating_window_tip", ""), + ("Keep screen on", ""), + ("Never", ""), + ("During controlled", ""), + ("During service is on", ""), + ("Capture screen using DirectX", ""), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", ""), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", ""), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/th.rs b/vendor/rustdesk/src/lang/th.rs new file mode 100644 index 0000000..c2d058c --- /dev/null +++ b/vendor/rustdesk/src/lang/th.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "สถานะ"), + ("Your Desktop", "หน้าจอของคุณ"), + ("desk_tip", "คุณสามารถเข้าถึงเดสก์ท็อปของคุณได้ด้วย ID และรหัสผ่านต่อไปนี้"), + ("Password", "รหัสผ่าน"), + ("Ready", "พร้อม"), + ("Established", "เชื่อมต่อแล้ว"), + ("connecting_status", "กำลังเชื่อมต่อไปยังเครือข่าย RustDesk..."), + ("Enable service", "เปิดใช้การงานเซอร์วิส"), + ("Start service", "เริ่มต้นใช้งานเซอร์วิส"), + ("Service is running", "เซอร์วิสกำลังทำงาน"), + ("Service is not running", "เซอร์วิสไม่ทำงาน"), + ("not_ready_status", "ไม่พร้อมใช้งาน กรุณาตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ"), + ("Control Remote Desktop", "การควบคุมเดสก์ท็อปปลายทาง"), + ("Transfer file", "การถ่ายโอนไฟล์"), + ("Connect", "เชื่อมต่อ"), + ("Recent sessions", "เซสชันล่าสุด"), + ("Address book", "สมุดรายชื่อ"), + ("Confirmation", "การยืนยัน"), + ("TCP tunneling", "อุโมงค์การเชื่อมต่อ TCP"), + ("Remove", "ลบ"), + ("Refresh random password", "รีเฟรชรหัสผ่านใหม่แบบสุ่ม"), + ("Set your own password", "ตั้งรหัสผ่านของคุณเอง"), + ("Enable keyboard/mouse", "เปิดการใช้งาน คีย์บอร์ด/เมาส์"), + ("Enable clipboard", "เปิดการใช้งาน คลิปบอร์ด"), + ("Enable file transfer", "เปิดการใช้งาน การถ่ายโอนไฟล์"), + ("Enable TCP tunneling", "เปิดการใช้งาน อุโมงค์การเชื่อมต่อ TCP"), + ("IP Whitelisting", "IP ไวท์ลิสต์"), + ("ID/Relay Server", "เซิร์ฟเวอร์ ID/Relay"), + ("Import server config", "นำเข้าการตั้งค่าเซิร์ฟเวอร์"), + ("Export Server Config", "ส่งออกการตั้งค่าเซิร์ฟเวอร์"), + ("Import server configuration successfully", "นำเข้าการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), + ("Export server configuration successfully", "ส่งออกการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), + ("Invalid server configuration", "การตั้งค่าของเซิร์ฟเวอร์ไม่ถูกต้อง"), + ("Clipboard is empty", "คลิปบอร์ดว่างเปล่า"), + ("Stop service", "หยุดการใช้งานเซอร์วิส"), + ("Change ID", "เปลี่ยน ID"), + ("Your new ID", "ID ใหม่ของคุณ"), + ("length %min% to %max%", "ความยาวตั้งแต่ %min% ถึง %max%"), + ("starts with a letter", "เริ่มต้นด้วยตัวอักษร"), + ("allowed characters", "ตัวอักขระที่อนุญาต"), + ("id_change_tip", "อนุญาตเฉพาะตัวอักษร a-z A-Z 0-9, - (dash) และ _ (ขีดล่าง) เท่านั้น โดยตัวอักษรขึ้นต้นจะต้องเป็น a-z หรือไม่ก็ A-Z และมีความยาวระหว่าง 6 ถึง 16 ตัวอักษร"), + ("Website", "เว็บไซต์"), + ("About", "เกี่ยวกับ"), + ("Slogan_tip", "ทำด้วยใจ ในโลกที่วุ่นวาย!"), + ("Privacy Statement", "คำแถลงเกี่ยวกับความเป็นส่วนตัว"), + ("Mute", "ปิดเสียง"), + ("Build Date", "วันที่ Build"), + ("Version", "เวอร์ชัน"), + ("Home", "หน้าหลัก"), + ("Audio Input", "ออดิโออินพุท"), + ("Enhancements", "การปรับปรุง"), + ("Hardware Codec", "ฮาร์ดแวร์ Codec"), + ("Adaptive bitrate", "Bitrate ผันแปร"), + ("ID Server", "เซิร์ฟเวอร์ ID"), + ("Relay Server", "เซิร์ฟเวอร์ Relay"), + ("API Server", "เซิร์ฟเวอร์ API"), + ("invalid_http", "ต้องขึ้นต้นด้วย http:// หรือ https:// เท่านั้น"), + ("Invalid IP", "IP ไม่ถูกต้อง"), + ("Invalid format", "รูปแบบไม่ถูกต้อง"), + ("server_not_support", "ยังไม่รองรับโดยเซิร์ฟเวอร์"), + ("Not available", "ไม่พร้อมใช้งาน"), + ("Too frequent", "ดำเนินการถี่เกินไป"), + ("Cancel", "ยกเลิก"), + ("Skip", "ข้าม"), + ("Close", "ปิด"), + ("Retry", "ลองใหม่อีกครั้ง"), + ("OK", "ตกลง"), + ("Password Required", "ต้องใช้รหัสผ่าน"), + ("Please enter your password", "กรุณาใส่รหัสผ่านของคุณ"), + ("Remember password", "จดจำรหัสผ่าน"), + ("Wrong Password", "รหัสผ่านไม่ถูกต้อง"), + ("Do you want to enter again?", "ต้องการใส่ข้อมูลอีกครั้งหรือไม่?"), + ("Connection Error", "การเชื่อมต่อผิดพลาด"), + ("Error", "ข้อผิดพลาด"), + ("Reset by the peer", "รีเซ็ตโดยอีกฝั่ง"), + ("Connecting...", "กำลังเชื่อมต่อ..."), + ("Connection in progress. Please wait.", "กำลังดำเนินการเชื่อมต่อ กรุณารอซักครู่"), + ("Please try 1 minute later", "กรุณาลองใหม่อีกครั้งใน 1 นาที"), + ("Login Error", "การเข้าสู่ระบบผิดพลาด"), + ("Successful", "สำเร็จ"), + ("Connected, waiting for image...", "เชื่อมต่อสำเร็จ กำลังรับข้อมูลภาพ..."), + ("Name", "ชื่อ"), + ("Type", "ประเภท"), + ("Modified", "แก้ไขล่าสุด"), + ("Size", "ขนาด"), + ("Show Hidden Files", "แสดงไฟล์ที่ถูกซ่อน"), + ("Receive", "รับ"), + ("Send", "ส่ง"), + ("Refresh File", "รีเฟรชไฟล์"), + ("Local", "ต้นทาง"), + ("Remote", "ปลายทาง"), + ("Remote Computer", "คอมพิวเตอร์ปลายทาง"), + ("Local Computer", "คอมพิวเตอร์ต้นทาง"), + ("Confirm Delete", "ยืนยันการลบ"), + ("Delete", "ลบ"), + ("Properties", "ข้อมูล"), + ("Multi Select", "เลือกหลายรายการ"), + ("Select All", "เลือกทั้งหมด"), + ("Unselect All", "ยกเลิกการเลือกทั้งหมด"), + ("Empty Directory", "ไดเรกทอรีว่างเปล่า"), + ("Not an empty directory", "ไม่ใช่ไดเรกทอรีว่างเปล่า"), + ("Are you sure you want to delete this file?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์นี้?"), + ("Are you sure you want to delete this empty directory?", "คุณแน่ใจหรือไม่ที่จะลบไดเรอทอรีว่างเปล่านี้?"), + ("Are you sure you want to delete the file of this directory?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์ของไดเรกทอรีนี้?"), + ("Do this for all conflicts", "ดำเนินการแบบเดียวกันสำหรับรายการทั้งหมด"), + ("This is irreversible!", "การดำเนินการนี้ไม่สามารถย้อนกลับได้!"), + ("Deleting", "กำลังลบ"), + ("files", "ไฟล์"), + ("Waiting", "กำลังรอ"), + ("Finished", "เสร็จแล้ว"), + ("Speed", "ความเร็ว"), + ("Custom Image Quality", "คุณภาพของภาพแบบกำหนดเอง"), + ("Privacy mode", "โหมดความเป็นส่วนตัว"), + ("Block user input", "บล็อคอินพุทจากผู้ใช้งาน"), + ("Unblock user input", "ยกเลิกการบล็อคอินพุทจากผู้ใช้งาน"), + ("Adjust Window", "ปรับขนาดหน้าต่าง"), + ("Original", "ต้นฉบับ"), + ("Shrink", "ย่อ"), + ("Stretch", "ยืด"), + ("Scrollbar", "แถบเลื่อน"), + ("ScrollAuto", "เลื่อนอัตโนมัติ"), + ("Good image quality", "ภาพคุณภาพดี"), + ("Balanced", "สมดุล"), + ("Optimize reaction time", "เน้นการตอบสนอง"), + ("Custom", "กำหนดเอง"), + ("Show remote cursor", "แสดงเคอร์เซอร์ปลายทาง"), + ("Show quality monitor", "แสดงคุณภาพหน้าจอ"), + ("Disable clipboard", "ปิดการใช้งานคลิปบอร์ด"), + ("Lock after session end", "ล็อคหลังจากจบเซสชัน"), + ("Insert Ctrl + Alt + Del", "แทรก Ctrl + Alt + Del"), + ("Insert Lock", "แทรกล็อค"), + ("Refresh", "รีเฟรช"), + ("ID does not exist", "ไม่พอข้อมูล ID"), + ("Failed to connect to rendezvous server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์นัดพบล้มเหลว"), + ("Please try later", "กรุณาลองใหม่ในภายหลัง"), + ("Remote desktop is offline", "เดสก์ท็อปปลายทางออฟไลน์"), + ("Key mismatch", "คีย์ไม่ถูกต้อง"), + ("Timeout", "หมดเวลา"), + ("Failed to connect to relay server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์ Relay ล้มเหลว"), + ("Failed to connect via rendezvous server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์นัดพบล้มเหลว"), + ("Failed to connect via relay server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์ Relay ล้มเหลว"), + ("Failed to make direct connection to remote desktop", "การเชื่อมต่อตรงไปยังเดสก์ท็อปปลายทางล้มเหลว"), + ("Set Password", "ตั้งรหัสผ่าน"), + ("OS Password", "รหัสผ่านระบบปฏิบัติการ"), + ("install_tip", "เนื่องด้วยข้อจำกัดของการใช้งาน UAC ทำให้ RustDesk ไม่สามารถทำงานได้ปกติในฝั่งปลายทางในบางครั้ง เพื่อหลีกเลี่ยงข้อจำกัดของ UAC กรุณากดปุ่มด้านล่างเพื่อติดตั้ง RustDesk ไปยังระบบของคุณ"), + ("Click to upgrade", "คลิกเพื่ออัปเกรด"), + ("Configure", "ปรับแต่งค่า"), + ("config_acc", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่ RustDesk"), + ("config_screen", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การบันทึกภาพหน้าจอ\" ให้แก่ RustDesk"), + ("Installing ...", "กำลังติดตั้ง ..."), + ("Install", "ติดตั้ง"), + ("Installation", "การติดตั้ง"), + ("Installation Path", "ตำแหน่งที่ติดตั้ง"), + ("Create start menu shortcuts", "สร้างทางลัดไปยัง Start Menu"), + ("Create desktop icon", "สร้างไอคอนบนเดสก์ท็อป"), + ("agreement_tip", "ในการเริ่มต้นการติดตั้ง ถือว่าคุณได้ยอมรับข้อตกลงใบอนุญาตแล้ว"), + ("Accept and Install", "ยอมรับและติดตั้ง"), + ("End-user license agreement", "ข้อตกลงใบอนุญาตผู้ใช้งาน"), + ("Generating ...", "กำลังสร้าง ..."), + ("Your installation is lower version.", "การติดตั้งของคุณเป็นเวอร์ชันที่ต่ำกว่า"), + ("not_close_tcp_tip", "อย่าปิดหน้าต่างนี้ในขณะที่คุณกำลังใช้งานอุโมงค์การเชื่อมต่อ"), + ("Listening ...", "กำลังรอรับข้อมูล ..."), + ("Remote Host", "โฮสต์ปลายทาง"), + ("Remote Port", "พอร์ทปลายทาง"), + ("Action", "การดำเนินการ"), + ("Add", "เพิ่ม"), + ("Local Port", "พอร์ทต้นทาง"), + ("Local Address", "ที่อยู่ต้นทาง"), + ("Change Local Port", "เปลี่ยนพอร์ทต้นทาง"), + ("setup_server_tip", "เพื่อการเชื่อมต่อที่เร็วขึ้น กรุณาเซ็ตอัปเซิร์ฟเวอร์ของคุณเอง"), + ("Too short, at least 6 characters.", "สั้นเกินไป ต้องไม่ต่ำกว่า 6 ตัวอักษร"), + ("The confirmation is not identical.", "การยืนยันข้อมูลไม่ถูกต้อง"), + ("Permissions", "สิทธิ์การใช้งาน"), + ("Accept", "ยอมรับ"), + ("Dismiss", "ปิด"), + ("Disconnect", "ยกเลิกการเชื่อมต่อ"), + ("Enable file copy and paste", "อนุญาตให้มีการคัดลอกและวางไฟล์"), + ("Connected", "เชื่อมต่อแล้ว"), + ("Direct and encrypted connection", "การเชื่อมต่อตรงที่มีการเข้ารหัส"), + ("Relayed and encrypted connection", "การเชื่อมต่อแบบ Relay ที่มีการเข้ารหัส"), + ("Direct and unencrypted connection", "การเชื่อมต่อตรงที่ไม่มีการเข้ารหัส"), + ("Relayed and unencrypted connection", "การเชื่อมต่อแบบ Relay ที่ไม่มีการเข้ารหัส"), + ("Enter Remote ID", "กรอก ID ปลายทาง"), + ("Enter your password", "กรอกรหัสผ่าน"), + ("Logging in...", "กำลังเข้าสู่ระบบ..."), + ("Enable RDP session sharing", "เปิดการใช้งานการแชร์เซสชัน RDP"), + ("Auto Login", "เข้าสู่ระบอัตโนมัติ"), + ("Enable direct IP access", "เปิดการใช้งาน IP ตรง"), + ("Rename", "ปลายทาง"), + ("Space", "พื้นที่ว่าง"), + ("Create desktop shortcut", "สร้างทางลัดบนเดสก์ท็อป"), + ("Change Path", "เปลี่ยนตำแหน่ง"), + ("Create Folder", "สร้างโฟลเดอร์"), + ("Please enter the folder name", "กรุณาใส่ชื่อโฟลเดอร์"), + ("Fix it", "แก้ไข"), + ("Warning", "คำเตือน"), + ("Login screen using Wayland is not supported", "หน้าจอการเข้าสู่ระบบโดยใช้ Wayland ยังไม่ถูกรองรับ"), + ("Reboot required", "จำเป็นต้องเริ่มต้นระบบใหม่"), + ("Unsupported display server", "เซิร์ฟเวอร์การแสดงผลที่ไม่รองรับ"), + ("x11 expected", "ต้องใช้งาน x11"), + ("Port", "พอร์ท"), + ("Settings", "ตั้งค่า"), + ("Username", "ชื่อผู้ใช้งาน"), + ("Invalid port", "พอร์ทไม่ถูกต้อง"), + ("Closed manually by the peer", "ถูกปิดโดยอีกฝั่งของการเชื่อมต่อ"), + ("Enable remote configuration modification", "เปิดการใช้งานการแก้ไขการตั้งค่าปลายทาง"), + ("Run without install", "ใช้งานโดยไม่ต้องติดตั้ง"), + ("Connect via relay", "เชื่อมต่อผ่าน Relay"), + ("Always connect via relay", "เชื่อมต่อผ่าน Relay เสมอ"), + ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), + ("Login", "เข้าสู่ระบบ"), + ("Verify", "ยืนยันความถูกต้อง"), + ("Remember me", "จดจำฉัน"), + ("Trust this device", "เชื่อถืออุปกรณ์นี้"), + ("Verification code", "รหัสยืนยันความถูกต้อง"), + ("verification_tip", "รหัสยืนยันความถูกต้องได้ถูกส่งไปยังอีเมล์ที่ลงทะเบียนแล้ว กรุณากรอกรหัสยืนยันความถูกต้องเพื่อดำเนินการเข้าสู่ระบบต่อ"), + ("Logout", "ออกจากระบบ"), + ("Tags", "แท็ก"), + ("Search ID", "ค้นหา ID"), + ("whitelist_sep", "คั่นโดยเครื่องหมาย comma semicolon เว้นวรรค หรือ ขึ้นบรรทัดใหม่"), + ("Add ID", "เพิ่ม ID"), + ("Add Tag", "เพิ่มแท็ก"), + ("Unselect all tags", "ยกเลิกการเลือกแท็กทั้งหมด"), + ("Network error", "ข้อผิดพลาดของเครือข่าย"), + ("Username missed", "ไม่พบข้อมูลผู้ใช้งาน"), + ("Password missed", "ไม่พบรหัสผ่าน"), + ("Wrong credentials", "ข้อมูลสำหรับเข้าสู่ระบบไม่ถูกต้อง"), + ("The verification code is incorrect or has expired", "รหัสยืนยันไม่ถูกต้องหรือหมดอายุแล้ว"), + ("Edit Tag", "แก้ไขแท็ก"), + ("Forget Password", "ยกเลิกการจดจำรหัสผ่าน"), + ("Favorites", "รายการโปรด"), + ("Add to Favorites", "เพิ่มไปยังรายการโปรด"), + ("Remove from Favorites", "ลบออกจากรายการโปรด"), + ("Empty", "ว่างเปล่า"), + ("Invalid folder name", "ชื่อโฟลเดอร์ไม่ถูกต้อง"), + ("Socks5 Proxy", "พรอกซี Socks5"), + ("Socks5/Http(s) Proxy", "พรอกซี Socks5/Http(s)"), + ("Discovered", "ค้นพบ"), + ("install_daemon_tip", "หากต้องการใช้งานขณะระบบเริ่มต้น คุณจำเป็นจะต้องติดตั้งเซอร์วิส"), + ("Remote ID", "ID ปลายทาง"), + ("Paste", "วาง"), + ("Paste here?", "วางที่นี่หรือไม่?"), + ("Are you sure to close the connection?", "คุณแน่ใจหรือไม่ที่จะปิดการเชื่อมต่อ?"), + ("Download new version", "ดาวน์โหลดเวอร์ชันใหม่"), + ("Touch mode", "โหมดการสัมผัส"), + ("Mouse mode", "โหมดการใช้เมาส์"), + ("One-Finger Tap", "แตะนิ้วเดียว"), + ("Left Mouse", "เมาส์ซ้าย"), + ("One-Long Tap", "แตะยาวหนึ่งครั้ง"), + ("Two-Finger Tap", "แตะสองนิ้ว"), + ("Right Mouse", "เมาส์ขวา"), + ("One-Finger Move", "ลากนิ้วเดียว"), + ("Double Tap & Move", "แตะเบิ้ลและลาก"), + ("Mouse Drag", "ลากเมาส์"), + ("Three-Finger vertically", "สามนิ้วแนวตั้ง"), + ("Mouse Wheel", "ลูกลิ้งเมาส์"), + ("Two-Finger Move", "ลากสองนิ้ว"), + ("Canvas Move", "ลากแคนวาส"), + ("Pinch to Zoom", "ถ่างเพื่อขยาย"), + ("Canvas Zoom", "ขยายแคนวาส"), + ("Reset canvas", "รีเซ็ตแคนวาส"), + ("No permission of file transfer", "ไม่มีสิทธิ์ในการถ่ายโอนไฟล์"), + ("Note", "บันทึกข้อความ"), + ("Connection", "การเชื่อมต่อ"), + ("Share screen", "แชร์หน้าจอ"), + ("Chat", "แชท"), + ("Total", "รวม"), + ("items", "รายการ"), + ("Selected", "ถูกเลือก"), + ("Screen Capture", "บันทึกหน้าจอ"), + ("Input Control", "ควบคุมอินพุท"), + ("Audio Capture", "บันทึกเสียง"), + ("Do you accept?", "ยอมรับหรือไม่?"), + ("Open System Setting", "เปิดการตั้งค่าระบบ"), + ("How to get Android input permission?", "เปิดสิทธิ์การใช้งานอินพุทของแอนดรอยด์ได้อย่างไร?"), + ("android_input_permission_tip1", "ในการที่จะอนุญาตให้เครื่องปลายทางควบคุมอุปกรณ์แอนดรอยด์ของคุณโดยใช้เมาส์หรือการสัมผัส คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่เซอร์วิสของ RustDesk"), + ("android_input_permission_tip2", "กรุณาไปยังหน้าตั้งค่าถัดไป ค้นหาและเข้าไปยัง [เซอร์วิสที่ถูกติดตั้ง] และเปิดการใช้งานเซอร์วิส [อินพุท RustDesk]"), + ("android_new_connection_tip", "ได้รับคำขอควบคุมใหม่ที่ต้องการควบคุมอุปกรณ์ของคุณ"), + ("android_service_will_start_tip", "การเปิดการใช้งาน \"การบันทึกหน้าจอ\" จะเป็นการเริ่มต้นการทำงานของเซอร์วิสโดยอัตโนมัติ ที่จะอนุญาตให้อุปกรณ์อื่นๆ ส่งคำขอเข้าถึงมายังอุปกรณ์ของคุณได้"), + ("android_stop_service_tip", "การปิดการใช้งานเซอร์วิสจะปิดการเชื่อมต่อทั้งหมดโดยอัตโนมัติ"), + ("android_version_audio_tip", "เวอร์ชันแอนดรอยด์ปัจจุบันของคุณไม่รองรับการบันทึกข้อมูลเสียง กรุณาอัปเกรดเป็นแอนดรอยด์เวอร์ชัน 10 หรือสูงกว่า"), + ("android_start_service_tip", "แตะ [เริ่มต้นใช้งานเซอร์วิส] หรือเปิดสิทธิ์การใช้งาน [บันทึกหน้าจอ] เพื่อเริ่มต้นใช้งานเซอร์วิสสำหรับการแบ่งปันหน้าจอ"), + ("android_permission_may_not_change_tip", "สิทธิ์การใช้งานสำหรับการเชื่อมต่อที่กำลังเปิดใช้งานอยู่อาจจะไม่ได้เปลี่ยนแปลงในทันทีจนกว่าจะเริ่มต้นการเชื่อมต่อใหม่อีกครั้ง"), + ("Account", "บัญชี"), + ("Overwrite", "เขียนทับ"), + ("This file exists, skip or overwrite this file?", "พบไฟล์ที่มีอยู่แล้ว ต้องการเขียนทับหรือไม่?"), + ("Quit", "ออก"), + ("Help", "ช่วยเหลือ"), + ("Failed", "ล้มเหลว"), + ("Succeeded", "สำเร็จ"), + ("Someone turns on privacy mode, exit", "มีใครบางคนเปิดใช้งานโหมดความเป็นส่วนตัว กำลังออก"), + ("Unsupported", "ไม่รองรับ"), + ("Peer denied", "ถูกปฏิเสธโดยอีกฝั่ง"), + ("Please install plugins", "กรุณาติดตั้งปลั๊กอิน"), + ("Peer exit", "อีกฝั่งออก"), + ("Failed to turn off", "การปิดล้มเหลว"), + ("Turned off", "ปิด"), + ("Language", "ภาษา"), + ("Keep RustDesk background service", "คงสถานะการทำงานเบื้องหลังของเซอร์วิส RustDesk"), + ("Ignore Battery Optimizations", "เพิกเฉยการตั้งค่าการใช้งาน Battery Optimization"), + ("android_open_battery_optimizations_tip", "หากคุณต้องการปิดการใช้งานฟีเจอร์นี้ กรุณาไปยังหน้าตั้งค่าในแอปพลิเคชัน RustDesk ค้นหาหัวข้อ [Battery] และยกเลิกการเลือกรายการ [Unrestricted]"), + ("Start on boot", "เริ่มต้นเมื่อเปิดเครื่อง"), + ("Start the screen sharing service on boot, requires special permissions", "เริ่มต้นใช้งานเซอร์วิสสำหรับการแบ่งปันหน้าจอเมื่อเปิดเครื่อง (ต้องมีการให้สิทธิ์การใช้งานพิเศษเพิ่มเติม)"), + ("Connection not allowed", "การเชื่อมต่อไม่อนุญาต"), + ("Legacy mode", "โหมดดั้งเดิม"), + ("Map mode", "โหมดการจับคู่"), + ("Translate mode", "โหมดแปลงค่า"), + ("Use permanent password", "ใช้รหัสผ่านถาวร"), + ("Use both passwords", "ใช้รหัสผ่านทั้งสองแบบ"), + ("Set permanent password", "ตั้งค่ารหัสผ่านถาวร"), + ("Enable remote restart", "เปิดการใช้งานการรีสตาร์ทระบบทางไกล"), + ("Restart remote device", "รีสตาร์ทอุปกรณ์ปลายทาง"), + ("Are you sure you want to restart", "คุณแน่ใจหรือไม่ที่จะรีสตาร์ท"), + ("Restarting remote device", "กำลังรีสตาร์ทระบบปลายทาง"), + ("remote_restarting_tip", "ระบบปลายทางกำลังรีสตาร์ท กรุณาปิดกล่องข้อความนี้และดำเนินการเขื่อมต่อใหม่อีกครั้งด้วยรหัสผ่านถาวรหลังจากผ่านไปซักครู่"), + ("Copied", "คัดลอกแล้ว"), + ("Exit Fullscreen", "ออกจากเต็มหน้าจอ"), + ("Fullscreen", "เต็มหน้าจอ"), + ("Mobile Actions", "การดำเนินการบนมือถือ"), + ("Select Monitor", "เลือกหน้าจอ"), + ("Control Actions", "การดำเนินการควบคุม"), + ("Display Settings", "การตั้งค่าแสดงผล"), + ("Ratio", "อัตราส่วน"), + ("Image Quality", "คุณภาพภาพ"), + ("Scroll Style", "ลักษณะการเลื่อน"), + ("Show Toolbar", "แสดงแถบเครื่องมือ"), + ("Hide Toolbar", "ซ่อนแถบเครื่องมือ"), + ("Direct Connection", "การเชื่อมต่อตรง"), + ("Relay Connection", "การเชื่อมต่อแบบ Relay "), + ("Secure Connection", "การเชื่อมต่อที่ปลอดภัย"), + ("Insecure Connection", "การเชื่อมต่อที่ไม่ปลอดภัย"), + ("Scale original", "ขนาดเดิม"), + ("Scale adaptive", "ขนาดยืดหยุ่น"), + ("General", "ทั่วไป"), + ("Security", "ความปลอดภัย"), + ("Theme", "ธีม"), + ("Dark Theme", "ธีมมืด"), + ("Light Theme", "ธีมสว่าง"), + ("Dark", "มืด"), + ("Light", "สว่าง"), + ("Follow System", "ตามระบบ"), + ("Enable hardware codec", "เปิดการใช้งานฮาร์ดแวร์ codec"), + ("Unlock Security Settings", "ปลดล็อคการตั้งค่าความปลอดภัย"), + ("Enable audio", "เปิดการใช้งานเสียง"), + ("Unlock Network Settings", "ปลดล็อคการตั้งค่าเครือข่าย"), + ("Server", "เซิร์ฟเวอร์"), + ("Direct IP Access", "การเข้าถึง IP ตรง"), + ("Proxy", "พรอกซี"), + ("Apply", "นำไปใช้"), + ("Disconnect all devices?", "ยกเลิกการเชื่อมต่ออุปกรณ์ทั้งหมด?"), + ("Clear", "ล้างข้อมูล"), + ("Audio Input Device", "อุปกรณ์รับอินพุทข้อมูลเสียง"), + ("Use IP Whitelisting", "ใช้งาน IP ไวท์ลิสต์"), + ("Network", "เครือข่าย"), + ("Pin Toolbar", "ปักหมุดแถบเครื่องมือ"), + ("Unpin Toolbar", "ยกเลิกการปักหมุดแถบเครื่องมือ"), + ("Recording", "การบันทึก"), + ("Directory", "ไดเรกทอรี่"), + ("Automatically record incoming sessions", "บันทึกเซสชันขาเข้าโดยอัตโนมัติ"), + ("Automatically record outgoing sessions", ""), + ("Change", "เปลี่ยน"), + ("Start session recording", "เริ่มต้นการบันทึกเซสชัน"), + ("Stop session recording", "หยุดการบันทึกเซสซัน"), + ("Enable recording session", "เปิดใช้งานการบันทึกเซสชัน"), + ("Enable LAN discovery", "เปิดการใช้งานการค้นหาในวง LAN"), + ("Deny LAN discovery", "ปฏิเสธการใช้งานการค้นหาในวง LAN"), + ("Write a message", "เขียนข้อความ"), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", "กรุณารอการยืนยันจาก UAC..."), + ("elevated_foreground_window_tip", "หน้าต่างปัจจุบันของเครื่องปลายทางต้องการสิทธิ์การใช้งานที่สูงขึ้นสำหรับการทำงาน ดังนั้นเมาส์และคีย์บอร์ดจะไม่สามารถใช้งานได้ชั่วคราว คุณสามารถขอผู้ใช้งานปลายทางให้ย่อหน้าต่าง หรือคลิกปุ่มให้สิทธิ์การใช้งานในหน้าต่างการจัดการการเชื่อมต่อ เพื่อหลีกเลี่ยงปัญหานี้เราแนะนำให้ดำเนินการติดตั้งซอฟท์แวร์ในเครื่องปลายทาง"), + ("Disconnected", "ยกเลิกการเชื่อมต่อ"), + ("Other", "อื่นๆ"), + ("Confirm before closing multiple tabs", "ยืนยันการปิดหลายแท็บ"), + ("Keyboard Settings", "การตั้งค่าคีย์บอร์ด"), + ("Full Access", "การเข้าถึงทั้งหมด"), + ("Screen Share", "การแชร์จอ"), + ("ubuntu-21-04-required", "Wayland ต้องการ Ubuntu เวอร์ชัน 21.04 หรือสูงกว่า"), + ("wayland-requires-higher-linux-version", "Wayland ต้องการลินุกซ์เวอร์ชันที่สูงกว่านี้ กรุณาเปลี่ยนไปใช้เดสก์ท็อป X11 หรือเปลี่ยนระบบปฏิบัติการของคุณ"), + ("xdp-portal-unavailable", ""), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "กรุณาเลือกหน้าจอที่ต้องการแชร์ (ใช้งานในอีกฝั่งของการเชื่อมต่อ)"), + ("Show RustDesk", "แสดง RustDesk"), + ("This PC", "พีซีเครื่องนี้"), + ("or", "หรือ"), + ("Elevate", "ยกระดับ"), + ("Zoom cursor", "ขยายเคอร์เซอร์"), + ("Accept sessions via password", "ยอมรับการเชื่อมต่อด้วยรหัสผ่าน"), + ("Accept sessions via click", "ยอมรับการเชื่อมต่อด้วยการคลิก"), + ("Accept sessions via both", "ยอมรับการเชื่อมต่อด้วยทั้งสองวิธิ"), + ("Please wait for the remote side to accept your session request...", "กรุณารอให้อีกฝั่งยอมรับการเชื่อมต่อของคุณ..."), + ("One-time Password", "รหัสผ่านครั้งเดียว"), + ("Use one-time password", "ใช้รหัสผ่านครั้งเดียว"), + ("One-time password length", "ความยาวรหัสผ่านครั้งเดียว"), + ("Request access to your device", "คำขอการเข้าถึงอุปกรณ์ของคุณ"), + ("Hide connection management window", "ซ่อนหน้าต่างการจัดการการเชื่อมต่อ"), + ("hide_cm_tip", "อนุญาตการซ่อนก็ต่อเมื่อยอมรับการเชื่อมต่อด้วยรหัสผ่าน และต้องเป็นรหัสผ่านถาวรเท่านั้น"), + ("wayland_experiment_tip", "การสนับสนุน Wayland ยังอยู่ในขั้นตอนการทดลอง กรุณาใช้ X11 หากคุณต้องการใช้งานการเข้าถึงแบบไม่มีผู้ดูแล"), + ("Right click to select tabs", "คลิกขวาเพื่อเลือกแท็บ"), + ("Skipped", "ข้าม"), + ("Add to address book", "เพิ่มไปยังสมุดรายชื่อ"), + ("Group", "กลุ่ม"), + ("Search", "ค้นหา"), + ("Closed manually by web console", "ถูกปิดโดยเว็บคอนโซล"), + ("Local keyboard type", "ประเภทคีย์บอร์ด"), + ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), + ("software_render_tip", "ถ้าคุณใช้กราฟิกการ์ดกับระบบ Linux และหน้าต่างของเครื่องปลายทางปิดในทันทีหลังจากการเชื่อมต่อ การเปลี่ยนไปใช้ไดรเวอร์ Nouveau และเลือกใช้โหมดการเรนเดอร์แบบซอฟท์แวร์อาจช่วยได้ (ต้องรีสตาร์ทโปรแกรม)"), + ("Always use software rendering", "ใช้การเรนเดอร์แบบซอฟท์แวร์เสมอ"), + ("config_input", "เพื่อที่จะควบคุมเครื่องเดสก์ท็อปปลายทางด้วยคีย์บอร์ด คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การตรวจสอบ Input\" ให้แก่ RustDesk"), + ("config_microphone", "เพื่อที่จะส่งเสียงพูดไปยังปลายทาง คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การบันทึกเสียง\" ให้แก่ RustDesk"), + ("request_elevation_tip", "คุณสามารถขอยกระดับสิทธิ์การใช้งานได้ หากมีผู้ใช้งานอยู่ในฝั่งเครื่องปลายทาง"), + ("Wait", "รอ"), + ("Elevation Error", "การยกระดับสิทธิ์การใช้งานผิดพลาด"), + ("Ask the remote user for authentication", "ขอความช่วยเหลือผู้ใช้งานปลายทางเพื่อพิสูจน์ตัวตน"), + ("Choose this if the remote account is administrator", "เลือกข้อนี้ถ้าบัญชีผู้ใช้งานปลายทางเป็นผู้ดูแลระบบ"), + ("Transmit the username and password of administrator", "ส่งข้อมูลผู้ใช้งานและรหัสผ่านของผู้ดูแลระบบ"), + ("still_click_uac_tip", "ผู้ใช้งานปลายทางยังจำเป็นที่จะต้องกดปุ่ม ตกลง บนหน้าต่าง UAC ของ RustDesk"), + ("Request Elevation", "ขอยกระดับสิทธิ์การใช้งาน"), + ("wait_accept_uac_tip", "กรุณารอผู้ใช้งานปลายทางกดยินยอมหน้าต่าง UAC"), + ("Elevate successfully", "การยกระดับสิทธิ์การใช้งานสำเร็จ"), + ("uppercase", "พิมพ์ใหญ่"), + ("lowercase", "พิมพ์เล็ก"), + ("digit", "หลัก"), + ("special character", "อักขระพิเศษ"), + ("length>=8", "ความยาวมากกว่า 8"), + ("Weak", "ไม่ปลอดภัย"), + ("Medium", "กลาง"), + ("Strong", "ปลอดภัย"), + ("Switch Sides", "สลับฝั่ง"), + ("Please confirm if you want to share your desktop?", "กรุณายืนยันว่าคุณต้องการแบ่งปันหน้าเดสก์ท็อปของคุณ"), + ("Display", "จอแสดงผล"), + ("Default View Style", "แสดงผลแบบเริ่มต้น"), + ("Default Scroll Style", "การเลื่อนแบบเริ่มต้น"), + ("Default Image Quality", "คุณภาพของภาพแบบเริ่มต้น"), + ("Default Codec", "Codec เริ่มต้น"), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", "อัตโนมัติ"), + ("Other Default Options", "ตัวเลือกเริ่มต้นอื่นๆ"), + ("Voice call", "การโทรด้วยเสียง"), + ("Text chat", "การสนทนาด้วยข้อความ"), + ("Stop voice call", "หยุดการโทรด้วยเสียง"), + ("relay_hint_tip", "การเชื่อมต่อโดยตรงอาจเป็นไปไม่ได้ ดังนั้นคุณสามารถลองเชื่อมต่อผ่าน Relay หรือตั้งค่าให้เชื่อมต่อผ่าน Relay เป็นค่าเริ่มต้น คุณสามารถเพิ่ม \"/r\" ต่อท้ายไปยัง ID หรือเลือกตัวเลือก \"เชื่อมต่อผ่าน Relay เสมอ\" ในการ์ดของการเชื่อมต่อล่าสุด (ถ้ามี)"), + ("Reconnect", "เชื่อมต่ออีกครั้ง"), + ("Codec", ""), + ("Resolution", "ความละเอียด"), + ("No transfers in progress", "ไม่มีการถ่ายโอนในขณะนี้"), + ("Set one-time password length", "ตั้งค่าความยาวรหัสผ่านครั้งเดียว"), + ("RDP Settings", "การตั้งค่า RDP"), + ("Sort by", "เรียงลำดับโดย"), + ("New Connection", "การเชื่อมต่อใหม่"), + ("Restore", "คืนค่า"), + ("Minimize", "ย่อ"), + ("Maximize", "ขยาย"), + ("Your Device", "อุปกรณ์ของคุณ"), + ("empty_recent_tip", "คุณยังไม่มีการเชื่อมต่อล่าสุด ได้เวลาวางแผนเพื่อเริ่มต้นแล้ว"), + ("empty_favorite_tip", "ยังไม่มีการเชื่อมต่อรายการโปรดเหรอ? มาเริ่มต้นหาใครซักคนเพื่อเชื่อมต่อด้วย และเพิ่มเข้าไปยังรายการโปรดของคุณกัน"), + ("empty_lan_tip", "ไม่นะ ดูเหมือนว่าเราจะยังไม่พบใครตรงนี้"), + ("empty_address_book_tip", "ดูเหมือนว่าคุณยังไม่มีใครถูกบันทึกในสมุดรายชื่อของคุณ"), + ("Empty Username", "ชื่อผู้ใช้งานว่างเปล่า"), + ("Empty Password", "รหัสผ่านว่างเปล่า"), + ("Me", "ฉัน"), + ("identical_file_tip", "ไฟล์นี้เหมือนกับไฟล์ของอีกฝั่ง"), + ("show_monitors_tip", "แสดงหน้าจอในแถบเครื่องมือ"), + ("View Mode", "โหมดการดู"), + ("login_linux_tip", "คุณจำเป็นจะต้องเข้าสู่ระบบไปยังบัญชีลินุกซ์ปลายทางเพื่อใช้งานเดสก์ท็อปเซสชัน X"), + ("verify_rustdesk_password_tip", "ยืนยันความถูกต้องรหัสผ่านของ RustDesk"), + ("remember_account_tip", "จดจำบัญชีนี้"), + ("os_account_desk_tip", "บัญชีนี้จะถูกใช้ในการเข้าสู่ระบบเครื่องปลายทางและเริ่มใช้งานเดสก์ท็อปเซสชันแบบ headless"), + ("OS Account", "บัญชีระบบปฏิบัติการ"), + ("another_user_login_title_tip", "ผู้ใช้งานอื่นเข้าสู่ระบบอยู่แล้ว"), + ("another_user_login_text_tip", "ยกเลิกการเชื่อมต่อ"), + ("xorg_not_found_title_tip", "ไม่พบ Xorg"), + ("xorg_not_found_text_tip", "กรุณาติดตั้ง Xorg"), + ("no_desktop_title_tip", "ไม่มีหน้าเดสก์ท็อปที่ใช้งานได้"), + ("no_desktop_text_tip", "กรุณาติดตั้ง GNOME เดสกท็อป"), + ("No need to elevate", "ไม่จำเป็นต้องยกระดับสิทธิ์การใช้งาน"), + ("System Sound", "เสียงของระบบ"), + ("Default", "ค่าเริ่มต้น"), + ("New RDP", "RDP ใหม่"), + ("Fingerprint", "ลายนิ้วมือ"), + ("Copy Fingerprint", "คัดลอกลายนิ้วมือ"), + ("no fingerprints", "ไม่มีลายนิ้วมือ"), + ("Select a peer", "เลือกผู้ใช้งาน"), + ("Select peers", "เลือกผู้ใช้งาน"), + ("Plugins", "ปลั๊กอิน"), + ("Uninstall", "ถอนการติดตั้ง"), + ("Update", "อัปเดต"), + ("Enable", "เปิดใช้งาน"), + ("Disable", "ปิดใช้งาน"), + ("Options", "ตัวเลือก"), + ("resolution_original_tip", "ความละเอียดดั้งเดิม"), + ("resolution_fit_local_tip", "ความละเอียดตามต้นทาง"), + ("resolution_custom_tip", "ความละเอียดแบบกำหนดเอง"), + ("Collapse toolbar", "พับแถบเครื่องมือ"), + ("Accept and Elevate", "ยอมรับ และยกระดับสิทธิ์การใช้งาน"), + ("accept_and_elevate_btn_tooltip", "ยอมรับการเชื่อมต่อ และยกระดับสิทธิ์ UAC"), + ("clipboard_wait_response_timeout_tip", "หมดเวลารอการตอบสนองของการคัดลอก"), + ("Incoming connection", "การเชื่อมต่อขาเข้า"), + ("Outgoing connection", "การเชื่อมต่อขาออก"), + ("Exit", "ออก"), + ("Open", "เปิด"), + ("logout_tip", "คุณแน่ใจที่จะออกจากระบบหรือไม่?"), + ("Service", "เซอร์วิส"), + ("Start", "เริ่ม"), + ("Stop", "หยุด"), + ("exceed_max_devices", "จำนวนอุปกรณ์ที่จัดการของคุณเต็มจำนวนแล้ว"), + ("Sync with recent sessions", "Sync กับเซสชันล่าสุด"), + ("Sort tags", "เรียงแท็ก"), + ("Open connection in new tab", "เริ่มการเชื่อมต่อในแท็บใหม่"), + ("Move tab to new window", "ย้ายแท็บไปหน้าต่างใหม่"), + ("Can not be empty", "ไม่สามารถเว้นว่างได้"), + ("Already exists", "มีอยู่แล้ว"), + ("Change Password", "เปลี่ยนรหัสผ่าน"), + ("Refresh Password", "รีเฟรชรหัสผ่าน"), + ("ID", ""), + ("Grid View", "มุมมองแบบช่อง"), + ("List View", "มุมมองแบบรายการ"), + ("Select", "เลือก"), + ("Toggle Tags", "สลับแท็ก"), + ("pull_ab_failed_tip", "การรีเฟรชสมุดรายชื่อล้มเหลว"), + ("push_ab_failed_tip", "การ Sync สมุดรายชื่อไปยังเซิร์ฟเวอร์ล้มเหลว"), + ("synced_peer_readded_tip", "อุปกรณ์ที่อยู่ในรายการล่าสุดจะถูก sync กลับไปยังสมุดรายชื่อ"), + ("Change Color", "เปลี่ยนสี"), + ("Primary Color", "สีหลัก"), + ("HSV Color", "สี HSV"), + ("Installation Successful!", "การติดตั้งเสร็จสมบูรณ์"), + ("Installation failed!", "การติดตั้งล้มเหลว"), + ("Reverse mouse wheel", "เลื่อมลูกกลิ้งเมาส์แบบกลับด้าน"), + ("{} sessions", "{} เซสชัน"), + ("scam_title", "คุณอาจกำลังถูกหลอกลวง!"), + ("scam_text1", "ถ้าคุณกำลังคุยโทรศัพท์กับคนที่คุณไม่รู้จักและไม่ไว้ใจ และคนๆนั้นกำลังขอให้คุณเปิดใช้งาน RustDesk อย่าทำตามและรีบวางสายในทันที"), + ("scam_text2", "เขาเหล่านั้นอาจเป็นมิจฉาชีพที่กำลังพยายามจะขโมยเงินและข้อมูลส่วนตัวของคุณ"), + ("Don't show again", "อย่าแสดงอีก"), + ("I Agree", "ยอมรับ"), + ("Decline", "ปฏิเสธ"), + ("Timeout in minutes", "หมดเวลาในอีกซักครู่"), + ("auto_disconnect_option_tip", "ยกเลิกการเชื่อมต่ออัตโนมัติในกรณีที่ผู้ใช้งานไม่มีการเคลื่อนไหว"), + ("Connection failed due to inactivity", "การเชื่อมต่อล้มเหลวเนื่องจากไม่มีการเคลื่อนไหว"), + ("Check for software update on startup", "ตรวจสอบการอัปเดตโปรแกรมเมื่อเริ่มต้นใช้งาน"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "กรุณาอัปเดต RustDesk Server Pro ไปยังเวอร์ชัน {} หรือใหม่กว่า!"), + ("pull_group_failed_tip", "การเรียกใช้งานกลุ่มล้มเหลว"), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), + ("idd_not_support_under_win10_2004_tip", ""), + ("input_source_1_tip", ""), + ("input_source_2_tip", ""), + ("Swap control-command key", ""), + ("swap-left-right-mouse", ""), + ("2FA code", ""), + ("More", ""), + ("enable-2fa-title", ""), + ("enable-2fa-desc", ""), + ("wrong-2fa-code", ""), + ("enter-2fa-title", ""), + ("Email verification code must be 6 characters.", ""), + ("2FA code must be 6 digits.", ""), + ("Multiple Windows sessions found", ""), + ("Please select the session you want to connect to", ""), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("share_warning_tip", ""), + ("Everyone", ""), + ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), + ("default_proxy_tip", ""), + ("no_audio_input_device_tip", ""), + ("Incoming", ""), + ("Outgoing", ""), + ("Clear Wayland screen selection", ""), + ("clear_Wayland_screen_selection_tip", ""), + ("confirm_clear_Wayland_screen_selection_tip", ""), + ("android_new_voice_call_tip", ""), + ("texture_render_tip", ""), + ("Use texture rendering", ""), + ("Floating window", ""), + ("floating_window_tip", ""), + ("Keep screen on", ""), + ("Never", ""), + ("During controlled", ""), + ("During service is on", ""), + ("Capture screen using DirectX", ""), + ("Back", ""), + ("Apps", ""), + ("Volume up", ""), + ("Volume down", ""), + ("Power", ""), + ("Telegram bot", ""), + ("enable-bot-tip", ""), + ("enable-bot-desc", ""), + ("cancel-2fa-confirm-tip", ""), + ("cancel-bot-confirm-tip", ""), + ("About RustDesk", ""), + ("Send clipboard keystrokes", ""), + ("network_error_tip", ""), + ("Unlock with PIN", ""), + ("Requires at least {} characters", ""), + ("Wrong PIN", ""), + ("Set PIN", ""), + ("Enable trusted devices", ""), + ("Manage trusted devices", ""), + ("Platform", ""), + ("Days remaining", ""), + ("enable-trusted-devices-tip", ""), + ("Parent directory", ""), + ("Resume", ""), + ("Invalid file name", ""), + ("one-way-file-transfer-tip", ""), + ("Authentication Required", ""), + ("Authenticate", ""), + ("web_id_input_tip", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "กรุณาอัปเดต RustDesk ไคลเอนต์ไปยังเวอร์ชัน {} หรือใหม่กว่าที่ฝั่งปลายทาง!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "ดูกล้อง"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", ""), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "ทำต่อด้วย {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/tr.rs b/vendor/rustdesk/src/lang/tr.rs new file mode 100644 index 0000000..d93ad4f --- /dev/null +++ b/vendor/rustdesk/src/lang/tr.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Durum"), + ("Your Desktop", "Sizin Masaüstünüz"), + ("desk_tip", "Masaüstünüze bu ID ve parola ile erişilebilir"), + ("Password", "Parola"), + ("Ready", "Hazır"), + ("Established", "Bağlantı sağlandı"), + ("connecting_status", "Bağlanılıyor "), + ("Enable service", "Servisi aktif et"), + ("Start service", "Servisi başlat"), + ("Service is running", "Servis çalışıyor"), + ("Service is not running", "Servis çalışmıyor"), + ("not_ready_status", "Hazır değil. Bağlantınızı kontrol edin"), + ("Control Remote Desktop", "Uzak Masaüstünü Denetle"), + ("Transfer file", "Dosya transferi"), + ("Connect", "Bağlan"), + ("Recent sessions", "Son oturumlar"), + ("Address book", "Adres Defteri"), + ("Confirmation", "Onayla"), + ("TCP tunneling", "TCP tünelleri"), + ("Remove", "Kaldır"), + ("Refresh random password", "Yeni rastgele parola oluştur"), + ("Set your own password", "Kendi parolanı oluştur"), + ("Enable keyboard/mouse", "Klavye ve Fareye izin ver"), + ("Enable clipboard", "Kopyalanan geçici veriye izin ver"), + ("Enable file transfer", "Dosya Transferine izin ver"), + ("Enable TCP tunneling", "TCP Tüneline izin ver"), + ("IP Whitelisting", "İzinli IP listesi"), + ("ID/Relay Server", "ID/Relay Sunucusu"), + ("Import server config", "Sunucu ayarlarını içe aktar"), + ("Export Server Config", "Sunucu Yapılandırmasını Dışa Aktar"), + ("Import server configuration successfully", "Sunucu ayarları başarıyla içe aktarıldı"), + ("Export server configuration successfully", "Sunucu yapılandırmasını başarıyla dışa aktar"), + ("Invalid server configuration", "Geçersiz sunucu ayarı"), + ("Clipboard is empty", "Kopyalanan geçici veri boş"), + ("Stop service", "Servisi Durdur"), + ("Change ID", "ID Değiştir"), + ("Your new ID", "Yeni ID'niz"), + ("length %min% to %max%", "uzunluk %min% ila %max%"), + ("starts with a letter", "bir harfle başlar"), + ("allowed characters", "izin verilen karakterler"), + ("id_change_tip", "Yalnızca a-z, A-Z, 0-9, - (dash) ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), + ("Website", "Website"), + ("About", "Hakkında"), + ("Slogan_tip", "Bu kaotik dünyada gönülden yapıldı!"), + ("Privacy Statement", "Gizlilik Beyanı"), + ("Mute", "Sustur"), + ("Build Date", "Derleme Tarihi"), + ("Version", "Sürüm"), + ("Home", "Ana Sayfa"), + ("Audio Input", "Ses Girişi"), + ("Enhancements", "Geliştirmeler"), + ("Hardware Codec", "Donanımsal Codec"), + ("Adaptive bitrate", "Uyarlanabilir Bit Hızı"), + ("ID Server", "ID Sunucu"), + ("Relay Server", "Relay Sunucu"), + ("API Server", "API Sunucu"), + ("invalid_http", "http:// veya https:// ile başlamalıdır"), + ("Invalid IP", "Geçersiz IP adresi"), + ("Invalid format", "Hatalı Format"), + ("server_not_support", "Henüz sunucu tarafından desteklenmiyor"), + ("Not available", "Erişilebilir değil"), + ("Too frequent", "Çok sık"), + ("Cancel", "İptal"), + ("Skip", "Atla"), + ("Close", "Kapat"), + ("Retry", "Tekrar Dene"), + ("OK", "Tamam"), + ("Password Required", "Parola Gerekli"), + ("Please enter your password", "Lütfen parolanızı giriniz"), + ("Remember password", "Parolayı hatırla"), + ("Wrong Password", "Hatalı parola"), + ("Do you want to enter again?", "Tekrar giriş yapmak ister misiniz?"), + ("Connection Error", "Bağlantı Hatası"), + ("Error", "Hata"), + ("Reset by the peer", "Eş tarafından sıfırlandı"), + ("Connecting...", "Bağlanılıyor..."), + ("Connection in progress. Please wait.", "Bağlantı sağlanıyor. Lütfen bekleyiniz."), + ("Please try 1 minute later", "Lütfen 1 dakika sonra tekrar deneyiniz"), + ("Login Error", "Giriş Hatalı"), + ("Successful", "Başarılı"), + ("Connected, waiting for image...", "Bağlandı. Görüntü bekleniyor..."), + ("Name", "Ad"), + ("Type", "Tip"), + ("Modified", "Değiştirildi"), + ("Size", "Boyut"), + ("Show Hidden Files", "Gizli Dosyaları Göster"), + ("Receive", "Al"), + ("Send", "Gönder"), + ("Refresh File", "Dosyayı yenile"), + ("Local", "Yerel"), + ("Remote", "Uzak"), + ("Remote Computer", "Uzak Bilgisayar"), + ("Local Computer", "Yerel Bilgisayar"), + ("Confirm Delete", "Silmeyi Onayla"), + ("Delete", "Sil"), + ("Properties", "Özellikler"), + ("Multi Select", "Çoklu Seçim"), + ("Select All", "Tümünü Seç"), + ("Unselect All", "Tüm Seçimi Kaldır"), + ("Empty Directory", "Boş Klasör"), + ("Not an empty directory", "Klasör boş değil"), + ("Are you sure you want to delete this file?", "Bu dosyayı silmek istediğinize emin misiniz?"), + ("Are you sure you want to delete this empty directory?", "Bu boş klasörü silmek istediğinize emin misiniz?"), + ("Are you sure you want to delete the file of this directory?", "Bu klasördeki dosyayı silmek istediğinize emin misiniz?"), + ("Do this for all conflicts", "Bunu tüm çakışmalar için yap"), + ("This is irreversible!", "Bu işlem geri döndürülemez!"), + ("Deleting", "Siliniyor"), + ("files", "dosyalar"), + ("Waiting", "Bekleniyor"), + ("Finished", "Tamamlandı"), + ("Speed", "Hız"), + ("Custom Image Quality", "Özel Görüntü Kalitesi"), + ("Privacy mode", "Gizlilik modu"), + ("Block user input", "Kullanıcı girişini engelle"), + ("Unblock user input", "Kullanı girişine izin ver"), + ("Adjust Window", "Pencereyi Ayarla"), + ("Original", "Orjinal"), + ("Shrink", "Küçült"), + ("Stretch", "Uzat"), + ("Scrollbar", "Kaydırma çubuğu"), + ("ScrollAuto", "Otomatik Kaydır"), + ("Good image quality", "İyi görüntü kalitesi"), + ("Balanced", "Dengelenmiş"), + ("Optimize reaction time", "Tepki süresini optimize et"), + ("Custom", "Özel"), + ("Show remote cursor", "Uzaktaki fare imlecini göster"), + ("Show quality monitor", "Kalite monitörünü göster"), + ("Disable clipboard", "Hafızadaki kopyalanmışları engelle"), + ("Lock after session end", "Bağlantıdan sonra kilitle"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Ekle"), + ("Insert Lock", "Kilit Ekle"), + ("Refresh", "Yenile"), + ("ID does not exist", "ID bulunamadı"), + ("Failed to connect to rendezvous server", "ID oluşturma sunucusuna bağlanılamadı"), + ("Please try later", "Daha sonra tekrar deneyiniz"), + ("Remote desktop is offline", "Uzak masaüstü kapalı"), + ("Key mismatch", "Anahtar uyumlu değil"), + ("Timeout", "Zaman aşımı"), + ("Failed to connect to relay server", "Relay sunucusuna bağlanılamadı"), + ("Failed to connect via rendezvous server", "ID oluşturma sunucusuna bağlanılamadı"), + ("Failed to connect via relay server", "Aktarma sunucusuna bağlanılamadı"), + ("Failed to make direct connection to remote desktop", "Uzak masaüstüne doğrudan bağlantı kurulamadı"), + ("Set Password", "Parola ayarla"), + ("OS Password", "İşletim Sistemi Parolası"), + ("install_tip", "Kullanıcı Hesabı Denetimi nedeniyle, RustDesk bir uzak masaüstü olarak düzgün çalışmayabilir. Bu sorunu önlemek için, RustDesk'i sistem seviyesinde kurmak için aşağıdaki butona tıklayın."), + ("Click to upgrade", "Yükseltmek için tıklayınız"), + ("Configure", "Ayarla"), + ("config_acc", "Masaüstünüzü dışarıdan kontrol etmek için RustDesk'e \"Erişilebilirlik\""), + ("config_screen", "Masaüstünüzü dışarıdan kontrol etmek için RustDesk'e \"Ekran Kaydı\" iznini vermeniz gerekir."), + ("Installing ...", "Yükleniyor ..."), + ("Install", "Yükle"), + ("Installation", "Kurulum"), + ("Installation Path", "Kurulacak olan konum"), + ("Create start menu shortcuts", "Başlangıca kısayol oluştur"), + ("Create desktop icon", "Masaüstüne kısayol oluştur"), + ("agreement_tip", "Kurulumu başlatarak, lisans sözleşmesinin şartlarını kabul etmiş olursunuz."), + ("Accept and Install", "Kabul Et ve Yükle"), + ("End-user license agreement", "Son kullanıcı lisans anlaşması"), + ("Generating ...", "Oluşturuluyor..."), + ("Your installation is lower version.", "Kurulumunuz alt sürümdür."), + ("not_close_tcp_tip", "Tüneli kullanırken bu pencereyi kapatmayın"), + ("Listening ...", "Dinleniyor..."), + ("Remote Host", "Uzak Sunucu"), + ("Remote Port", "Uzak Port"), + ("Action", "Eylem"), + ("Add", "Ekle"), + ("Local Port", "Yerel Port"), + ("Local Address", "Yerel Adres"), + ("Change Local Port", "Yerel Port'u Değiştir"), + ("setup_server_tip", "Daha hızlı bağlantı için kendi sunucunuzu kurun"), + ("Too short, at least 6 characters.", "Çok kısa en az 6 karakter gerekli."), + ("The confirmation is not identical.", "Doğrulama yapılamadı."), + ("Permissions", "İzinler"), + ("Accept", "Kabul Et"), + ("Dismiss", "Reddet"), + ("Disconnect", "Bağlanıyı kes"), + ("Enable file copy and paste", "Dosya kopyalamaya ve yapıştırmaya izin ver"), + ("Connected", "Bağlandı"), + ("Direct and encrypted connection", "Doğrudan ve şifreli bağlantı"), + ("Relayed and encrypted connection", "Aktarmalı ve şifreli bağlantı"), + ("Direct and unencrypted connection", "Doğrudan ve şifrelenmemiş bağlantı"), + ("Relayed and unencrypted connection", "Aktarmalı ve şifrelenmemiş bağlantı"), + ("Enter Remote ID", "Uzak ID'yi Girin"), + ("Enter your password", "Parolanızı girin"), + ("Logging in...", "Giriş yapılıyor..."), + ("Enable RDP session sharing", "RDP oturum paylaşımını etkinleştir"), + ("Auto Login", "Otomatik giriş"), + ("Enable direct IP access", "Doğrudan IP Erişimini Etkinleştir"), + ("Rename", "Yeniden adlandır"), + ("Space", "Boşluk"), + ("Create desktop shortcut", "Masaüstü kısayolu oluşturun"), + ("Change Path", "Yolu değiştir"), + ("Create Folder", "Klasör oluşturun"), + ("Please enter the folder name", "Lütfen klasör adını girin"), + ("Fix it", "Düzenle"), + ("Warning", "Uyarı"), + ("Login screen using Wayland is not supported", "Wayland kullanan giriş ekranı desteklenmiyor"), + ("Reboot required", "Yeniden başlatma gerekli"), + ("Unsupported display server", "Desteklenmeyen görüntü sunucusu"), + ("x11 expected", "x11 bekleniyor"), + ("Port", "Port"), + ("Settings", "Ayarlar"), + ("Username", "Kullanıcı Adı"), + ("Invalid port", "Geçersiz port"), + ("Closed manually by the peer", "Eş tarafından manuel olarak kapatıldı"), + ("Enable remote configuration modification", "Uzaktan yapılandırma değişikliğini etkinleştir"), + ("Run without install", "Yüklemeden çalıştır"), + ("Connect via relay", "Aktarmalı üzerinden bağlan"), + ("Always connect via relay", "Her zaman aktarmalı üzerinden bağlan"), + ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), + ("Login", "Giriş yap"), + ("Verify", "Doğrula"), + ("Remember me", "Beni hatırla"), + ("Trust this device", "Bu cihaza güvenin"), + ("Verification code", "Doğrulama kodu"), + ("verification_tip", "doğrulama tipi"), + ("Logout", "Çıkış yap"), + ("Tags", "Etiketler"), + ("Search ID", "ID Arama"), + ("whitelist_sep", "Virgül, noktalı virgül, boşluk veya yeni satır ile ayrılmış"), + ("Add ID", "ID Ekle"), + ("Add Tag", "Etiket Ekle"), + ("Unselect all tags", "Tüm etiketlerin seçimini kaldır"), + ("Network error", "Bağlantı hatası"), + ("Username missed", "Kullanıcı adı boş"), + ("Password missed", "Parola boş"), + ("Wrong credentials", "Yanlış kimlik bilgileri"), + ("The verification code is incorrect or has expired", "Doğrulama kodu hatalı veya süresi dolmuş"), + ("Edit Tag", "Etiketi düzenle"), + ("Forget Password", "Parolayı Unut"), + ("Favorites", "Favoriler"), + ("Add to Favorites", "Favorilere ekle"), + ("Remove from Favorites", "Favorilerden çıkar"), + ("Empty", "Boş"), + ("Invalid folder name", "Geçersiz klasör adı"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "Keşfedilenler"), + ("install_daemon_tip", "Başlangıçta başlamak için sistem hizmetini yüklemeniz gerekir."), + ("Remote ID", "Uzak ID"), + ("Paste", "Yapıştır"), + ("Paste here?", "Buraya yapıştır?"), + ("Are you sure to close the connection?", "Bağlantıyı kapatmak istediğinize emin misiniz?"), + ("Download new version", "Yeni sürümü indir"), + ("Touch mode", "Dokunmatik mod"), + ("Mouse mode", "Fare modu"), + ("One-Finger Tap", "Tek Parmakla Dokunma"), + ("Left Mouse", "Sol Fare"), + ("One-Long Tap", "Tek-Uzun Dokunma"), + ("Two-Finger Tap", "İki-Parmak Dokunma"), + ("Right Mouse", "Sağ Fare"), + ("One-Finger Move", "Tek Parmakla Hareket"), + ("Double Tap & Move", "Çift Dokun ve Taşı"), + ("Mouse Drag", "Fare Sürükleme"), + ("Three-Finger vertically", "Dikey olarak üç parmak"), + ("Mouse Wheel", "Fare Tekerliği"), + ("Two-Finger Move", "İki Parmakla Hareket"), + ("Canvas Move", "Tuval Hareketi"), + ("Pinch to Zoom", "İki parmakla yakınlaştır"), + ("Canvas Zoom", "Tuval Yakınlaştırma"), + ("Reset canvas", "Tuvali sıfırla"), + ("No permission of file transfer", "Dosya aktarımı izni yok"), + ("Note", "Not"), + ("Connection", "Bağlantı"), + ("Share screen", "Ekranı Paylaş"), + ("Chat", "Mesajlaş"), + ("Total", "Toplam"), + ("items", "ögeler"), + ("Selected", "Seçildi"), + ("Screen Capture", "Ekran Görüntüsü"), + ("Input Control", "Giriş Kontrolü"), + ("Audio Capture", "Ses Yakalama"), + ("Do you accept?", "Kabul ediyor musun?"), + ("Open System Setting", "Sistem Ayarını Aç"), + ("How to get Android input permission?", "Android giriş izni nasıl alınır?"), + ("android_input_permission_tip1", "Uzak bir cihazın Android cihazınızı fare veya dokunma yoluyla kontrol edebilmesi için, RustDesk'in \"Erişilebilirlik\" özelliğini kullanmasına izin vermelisiniz."), + ("android_input_permission_tip2", "Sonraki sistem ayarları sayfasına gidin, [Yüklü Hizmetler]'i bulun ve erişin, [RustDesk Girişi] hizmetini etkinleştirin."), + ("android_new_connection_tip", "Yeni bir kontrol talebi alındı, cihazınızı kontrol etmesine izin verilsin mi."), + ("android_service_will_start_tip", "Ekran Yakalamanın etkinleştirilmesi, hizmeti otomatik olarak başlatacak ve diğer cihazların bu cihazdan bağlantı talep etmesine izin verecektir."), + ("android_stop_service_tip", "Hizmetin kapatılması, kurulan tüm bağlantıları otomatik olarak kapatacaktır."), + ("android_version_audio_tip", "Mevcut Android sürümü ses yakalamayı desteklemiyor, lütfen Android 10 veya sonraki bir sürüme yükseltin."), + ("android_start_service_tip", "Ekran paylaşım hizmetini başlatmak için [Hizmeti başlat] ögesine dokunun veya [Ekran Görüntüsü] iznini etkinleştirin."), + ("android_permission_may_not_change_tip", "Kurulan bağlantılara ait izinler, yeniden bağlantı kurulana kadar anında değiştirilemez."), + ("Account", "Hesap"), + ("Overwrite", "Üzerine yaz"), + ("This file exists, skip or overwrite this file?", "Bu dosya var, bu dosya atlansın veya üzerine yazılsın mı?"), + ("Quit", "Çıkış"), + ("Help", "Yardım"), + ("Failed", "Arızalı"), + ("Succeeded", "başarılı"), + ("Someone turns on privacy mode, exit", "Birisi gizlilik modunu açarsa, çık"), + ("Unsupported", "desteklenmiyor"), + ("Peer denied", "eş reddedildi"), + ("Please install plugins", "Lütfen eklentileri yükleyin"), + ("Peer exit", "Eş çıkışı"), + ("Failed to turn off", "Kapatılamadı"), + ("Turned off", "Kapatıldı"), + ("Language", "Dil"), + ("Keep RustDesk background service", "RustDesk arka plan hizmetini sürdürün"), + ("Ignore Battery Optimizations", "Pil Optimizasyonlarını Yoksay"), + ("android_open_battery_optimizations_tip", "Bu özelliği devre dışı bırakmak istiyorsanız lütfen bir sonraki RustDesk uygulama ayarları sayfasına gidin, [Pil] ögesini bulun ve girin, [Sınırsız] ögesinin işaretini kaldırın"), + ("Start on boot", "Önyüklemede başla"), + ("Start the screen sharing service on boot, requires special permissions", "Ekran paylaşım hizmetini önyüklemede başlatmak için özel izinler gerekir"), + ("Connection not allowed", "Bağlantıya izin verilmedi"), + ("Legacy mode", "Eski mod"), + ("Map mode", "Haritalama modu"), + ("Translate mode", "Çeviri modu"), + ("Use permanent password", "Kalıcı parola kullan"), + ("Use both passwords", "İki parolayı da kullan"), + ("Set permanent password", "Kalıcı parola oluştur"), + ("Enable remote restart", "Uzaktan yeniden başlatmayı aktif et"), + ("Restart remote device", "Uzaktaki cihazı yeniden başlat"), + ("Are you sure you want to restart", "Yeniden başlatmak istediğine emin misin?"), + ("Restarting remote device", "Uzaktan yeniden başlatılıyor"), + ("remote_restarting_tip", "Uzak cihaz yeniden başlatılıyor, lütfen bu mesaj kutusunu kapatın ve bir süre sonra kalıcı parola ile yeniden bağlanın"), + ("Copied", "Kopyalandı"), + ("Exit Fullscreen", "Tam Ekrandan Çık"), + ("Fullscreen", "Tam Ekran"), + ("Mobile Actions", "Mobil İşlemler"), + ("Select Monitor", "Monitörü Seç"), + ("Control Actions", "Kontrol Eylemleri"), + ("Display Settings", "Görüntü Ayarları"), + ("Ratio", "Oran"), + ("Image Quality", "Görüntü Kalitesi"), + ("Scroll Style", "Kaydırma Stili"), + ("Show Toolbar", "Araç Çubuğunu Göster"), + ("Hide Toolbar", "Araç Çubuğunu Gizle"), + ("Direct Connection", "Doğrudan Bağlantı"), + ("Relay Connection", "Aktarmalı Bağlantı"), + ("Secure Connection", "Güvenli Bağlantı"), + ("Insecure Connection", "Güvenli Olmayan Bağlantı"), + ("Scale original", "Orijinal ölçekte"), + ("Scale adaptive", "Uyarlanabilir ölçekte"), + ("General", "Genel"), + ("Security", "Güvenlik"), + ("Theme", "Tema"), + ("Dark Theme", "Koyu Tema"), + ("Light Theme", "Açık Tema"), + ("Dark", "Koyu"), + ("Light", "Açık"), + ("Follow System", "Sisteme Uy"), + ("Enable hardware codec", "Donanımsal codec aktif et"), + ("Unlock Security Settings", "Güvenlik Ayarlarını Aç"), + ("Enable audio", "Sesi Aktif Et"), + ("Unlock Network Settings", "Ağ Ayarlarını Aç"), + ("Server", "Sunucu"), + ("Direct IP Access", "Doğrudan IP Erişimi"), + ("Proxy", "Vekil"), + ("Apply", "Uygula"), + ("Disconnect all devices?", "Tüm cihazların bağlantısı kesilsin mi?"), + ("Clear", "Temizle"), + ("Audio Input Device", "Ses Giriş Aygıtı"), + ("Use IP Whitelisting", "IP Beyaz Listeyi Kullan"), + ("Network", "Ağ"), + ("Pin Toolbar", "Araç Çubuğunu Sabitle"), + ("Unpin Toolbar", "Araç Çubuğunu Sabitlemeyi Kaldır"), + ("Recording", "Kaydediliyor"), + ("Directory", "Dizin"), + ("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kaydet"), + ("Automatically record outgoing sessions", "Giden oturumları otomatik olarak kaydet"), + ("Change", "Değiştir"), + ("Start session recording", "Oturum kaydını başlat"), + ("Stop session recording", "Oturum kaydını sonlandır"), + ("Enable recording session", "Kayıt Oturumunu Aktif Et"), + ("Enable LAN discovery", "Yerel Ağ Keşfine İzin Ver"), + ("Deny LAN discovery", "Yerel Ağ Keşfine İzin Verme"), + ("Write a message", "Bir mesaj yazın"), + ("Prompt", "İstem"), + ("Please wait for confirmation of UAC...", "UAC onayı için lütfen bekleyiniz..."), + ("elevated_foreground_window_tip", "elevated_foreground_window_tip"), + ("Disconnected", "Bağlantı Kesildi"), + ("Other", "Diğer"), + ("Confirm before closing multiple tabs", "Çoklu sekmeleri kapatmadan önce onayla"), + ("Keyboard Settings", "Klavye Ayarları"), + ("Full Access", "Tam Erişim"), + ("Screen Share", "Ekran Paylaşımı"), + ("ubuntu-21-04-required", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), + ("wayland-requires-higher-linux-version", "Wayland, linux dağıtımının daha yüksek bir sürümünü gerektirir. Lütfen X11 masaüstünü deneyin veya işletim sisteminizi değiştirin."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Lütfen paylaşılacak ekranı seçiniz (Ekran tarafında çalıştırın)."), + ("Show RustDesk", "RustDesk'i Göster"), + ("This PC", "Bu PC"), + ("or", "veya"), + ("Elevate", "Yükseltme"), + ("Zoom cursor", "Yakınlaştırma imleci"), + ("Accept sessions via password", "Oturumları parola ile kabul etme"), + ("Accept sessions via click", "Tıklama yoluyla oturumları kabul edin"), + ("Accept sessions via both", "Her ikisi aracılığıyla oturumları kabul edin"), + ("Please wait for the remote side to accept your session request...", "Lütfen uzak tarafın oturum isteğinizi kabul etmesini bekleyin..."), + ("One-time Password", "Tek Kullanımlık Parola"), + ("Use one-time password", "Tek seferlik parola kullanın"), + ("One-time password length", "Tek seferlik parola uzunluğu"), + ("Request access to your device", "Cihazınıza erişim talep edin"), + ("Hide connection management window", "Bağlantı yönetimi penceresini gizle"), + ("hide_cm_tip", "Oturumları yalnızca parola ile kabul edebilir ve kalıcı parola kullanıyorsanız gizlemeye izin verin"), + ("wayland_experiment_tip", "Wayland desteği deneysel aşamada olduğundan, gerektiğinde X11'i kullanmanız önerilir"), + ("Right click to select tabs", "Sekmeleri seçmek için sağ tıklayın"), + ("Skipped", "Atlandı"), + ("Add to address book", "Adres Defterine Ekle"), + ("Group", "Grup"), + ("Search", "Ara"), + ("Closed manually by web console", "Web konsoluyla manuel olarak kapatıldı"), + ("Local keyboard type", "Yerel klavye türü"), + ("Select local keyboard type", "Yerel klavye türünü seçin"), + ("software_render_tip", "Linux altında Nvidia grafik kartı kullanıyorsanız ve uzak pencere bağlandıktan hemen sonra kapanıyorsa, açık kaynaklı Nouveau sürücüsüne geçmeyi ve yazılım renderleme seçeneğini seçmeyi deneyin. Yazılımı yeniden başlatmanız gerekebilir."), + ("Always use software rendering", "Her zaman yazılım renderleme kullan"), + ("config_input", "Uzaktaki masaüstünü klavye ile kontrol etmek için RustDesk'e \"Giriş İzleme\" izinleri vermelisiniz."), + ("config_microphone", "Uzaktan konuşmak için RustDesk'e \"Ses Kaydı\" izinleri vermelisiniz."), + ("request_elevation_tip", "Ayrıca, uzak tarafta biri varsa yükseltme isteğinde bulunabilirsiniz."), + ("Wait", "Bekle"), + ("Elevation Error", "Yükseltme Hatası"), + ("Ask the remote user for authentication", "Uzaktaki kullanıcıdan kimlik doğrulamasını isteyin"), + ("Choose this if the remote account is administrator", "Uzak hesap yönetici ise bunu seçin"), + ("Transmit the username and password of administrator", "Yönetici kullanıcı adı ve parolasını iletim yapın"), + ("still_click_uac_tip", "Uzaktaki kullanıcının çalışan RustDesk'in UAC penceresinde hala Tamam'ı tıklaması gerekmektedir."), + ("Request Elevation", "Yükseltme İsteği"), + ("wait_accept_uac_tip", "Lütfen uzaktaki kullanıcının UAC iletişim kutusunu kabul etmesini bekleyin."), + ("Elevate successfully", "Başarıyla yükseltildi"), + ("uppercase", "büyük harf"), + ("lowercase", "küçük harf"), + ("digit", "rakam"), + ("special character", "özel karakter"), + ("length>=8", "uzunluk>=8"), + ("Weak", "Zayıf"), + ("Medium", "Orta"), + ("Strong", "Güçlü"), + ("Switch Sides", "Tarafları Değiştir"), + ("Please confirm if you want to share your desktop?", "Masaüstünüzü paylaşmak isteyip istemediğinizi onaylayın?"), + ("Display", "Görüntüle"), + ("Default View Style", "Varsayılan Görünüm Stili"), + ("Default Scroll Style", "Varsayılan Kaydırma Stili"), + ("Default Image Quality", "Varsayılan Görüntü Kalitesi"), + ("Default Codec", "Varsayılan Kodlayıcı"), + ("Bitrate", "Bit Hızı"), + ("FPS", "FPS"), + ("Auto", "Otomatik"), + ("Other Default Options", "Diğer Varsayılan Seçenekler"), + ("Voice call", "Sesli görüşme"), + ("Text chat", "Metin sohbeti"), + ("Stop voice call", "Sesli görüşmeyi durdur"), + ("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; aktarmalı bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde aktarma sunucusu kullanmak istiyorsanız ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her Zaman Aktarmalı Üzerinden Bağlan\" seçeneğini seçebilirsiniz."), + ("Reconnect", "Yeniden Bağlan"), + ("Codec", "Kodlayıcı"), + ("Resolution", "Çözünürlük"), + ("No transfers in progress", "Devam eden aktarımlar yok"), + ("Set one-time password length", "Bir seferlik parola uzunluğunu ayarla"), + ("RDP Settings", "RDP Ayarları"), + ("Sort by", "Sırala"), + ("New Connection", "Yeni Bağlantı"), + ("Restore", "Geri Yükle"), + ("Minimize", "Simge Durumuna Küçült"), + ("Maximize", "Büyüt"), + ("Your Device", "Cihazınız"), + ("empty_recent_tip", "Üzgünüz, henüz son oturum yok!\nYeni bir plan yapma zamanı."), + ("empty_favorite_tip", "Henüz favori cihazınız yok mu?\nBağlanacak ve favorilere eklemek için birini bulalım!"), + ("empty_lan_tip", "Hayır, henüz hiçbir cihaz bulamadık gibi görünüyor."), + ("empty_address_book_tip", "Üzgünüm, şu anda adres defterinizde kayıtlı cihaz yok gibi görünüyor."), + ("Empty Username", "Boş Kullanıcı Adı"), + ("Empty Password", "Boş Parola"), + ("Me", "Ben"), + ("identical_file_tip", "Bu dosya, cihazın dosyası ile aynıdır."), + ("show_monitors_tip", "Monitörleri araç çubuğunda göster"), + ("View Mode", "Görünüm Modu"), + ("login_linux_tip", "X masaüstü oturumu başlatmak için uzaktaki Linux hesabına giriş yapmanız gerekiyor"), + ("verify_rustdesk_password_tip", "RustDesk parolasını doğrulayın"), + ("remember_account_tip", "Bu hesabı hatırla"), + ("os_account_desk_tip", "Bu hesap, uzaktaki işletim sistemine giriş yapmak ve başsız masaüstü oturumunu etkinleştirmek için kullanılır."), + ("OS Account", "İşletim Sistemi Hesabı"), + ("another_user_login_title_tip", "Başka bir kullanıcı zaten oturum açtı"), + ("another_user_login_text_tip", "Bağlantıyı Kapat"), + ("xorg_not_found_title_tip", "Xorg bulunamadı"), + ("xorg_not_found_text_tip", "Lütfen Xorg'u yükleyin"), + ("no_desktop_title_tip", "Masaüstü mevcut değil"), + ("no_desktop_text_tip", "Lütfen GNOME masaüstünü yükleyin"), + ("No need to elevate", "Yükseltmeye gerek yok"), + ("System Sound", "Sistem Sesi"), + ("Default", "Varsayılan"), + ("New RDP", "Yeni RDP"), + ("Fingerprint", "Parmak İzi"), + ("Copy Fingerprint", "Parmak İzini Kopyala"), + ("no fingerprints", "parmak izi yok"), + ("Select a peer", "Bir cihaz seçin"), + ("Select peers", "Cihazları seçin"), + ("Plugins", "Eklentiler"), + ("Uninstall", "Kaldır"), + ("Update", "Güncelle"), + ("Enable", "Etkinleştir"), + ("Disable", "Devre Dışı Bırak"), + ("Options", "Seçenekler"), + ("resolution_original_tip", "Orijinal çözünürlük"), + ("resolution_fit_local_tip", "Yerel çözünürlüğe sığdır"), + ("resolution_custom_tip", "Özel çözünürlük"), + ("Collapse toolbar", "Araç çubuğunu daralt"), + ("Accept and Elevate", "Kabul Et ve Yükselt"), + ("accept_and_elevate_btn_tooltip", "Bağlantıyı kabul et ve UAC izinlerini yükselt."), + ("clipboard_wait_response_timeout_tip", "Kopyalama yanıtı için zaman aşımına uğradı."), + ("Incoming connection", "Gelen bağlantı"), + ("Outgoing connection", "Giden bağlantı"), + ("Exit", "Çıkış"), + ("Open", "Aç"), + ("logout_tip", "Çıkış yapmak istediğinizden emin misiniz?"), + ("Service", "Hizmet"), + ("Start", "Başlat"), + ("Stop", "Durdur"), + ("exceed_max_devices", "Yönetilen cihazların maksimum sayısına ulaştınız."), + ("Sync with recent sessions", "Son oturumlarla senkronize et"), + ("Sort tags", "Etiketleri sırala"), + ("Open connection in new tab", "Bağlantıyı yeni sekmede aç"), + ("Move tab to new window", "Sekmeyi yeni pencereye taşı"), + ("Can not be empty", "Boş olamaz"), + ("Already exists", "Zaten var"), + ("Change Password", "Parolayı Değiştir"), + ("Refresh Password", "Parolayı Yenile"), + ("ID", "Kimlik"), + ("Grid View", "Izgara Görünümü"), + ("List View", "Liste Görünümü"), + ("Select", "Seç"), + ("Toggle Tags", "Etiketleri Değiştir"), + ("pull_ab_failed_tip", "Adres defterini yenileyemedi"), + ("push_ab_failed_tip", "Adres defterini sunucuya senkronize edemedi"), + ("synced_peer_readded_tip", "Son oturumlar listesinde bulunan cihazlar adres defterine geri senkronize edilecektir."), + ("Change Color", "Rengi Değiştir"), + ("Primary Color", "Birincil Renk"), + ("HSV Color", "HSV Rengi"), + ("Installation Successful!", "Kurulum Başarılı!"), + ("Installation failed!", "Kurulum başarısız!"), + ("Reverse mouse wheel", "Ters fare tekerleği"), + ("{} sessions", "{} oturum"), + ("scam_title", "Dolandırılıyor Olabilirsiniz!"), + ("scam_text1", "Eğer tanımadığınız ve güvenmediğiniz birisiyle telefonda konuşuyorsanız ve sizden RustDesk'i kullanmanızı ve hizmeti başlatmanızı istiyorsa devam etmeyin ve hemen telefonu kapatın."), + ("scam_text2", "Muhtemelen paranızı veya diğer özel bilgilerinizi çalmaya çalışan dolandırıcılardır."), + ("Don't show again", "Bir daha gösterme"), + ("I Agree", "Kabul Ediyorum"), + ("Decline", "Reddet"), + ("Timeout in minutes", "Zaman aşımı (dakika)"), + ("auto_disconnect_option_tip", "Kullanıcı etkin olmadığında gelen oturumları otomatik olarak kapat"), + ("Connection failed due to inactivity", "Etkin olmama nedeniyle otomatik olarak bağlantı kesildi"), + ("Check for software update on startup", "Başlangıçta yazılım güncellemesini kontrol et"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Lütfen RustDesk Server Pro'yu {} veya daha yeni bir sürüme yükseltin!"), + ("pull_group_failed_tip", "Grup yenilenemedi"), + ("Filter by intersection", "Kesişim noktasına göre filtrele"), + ("Remove wallpaper during incoming sessions", "Gelen oturumlar sırasında duvar kağıdını kaldır"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Ekran fişi çekilmiş, ilk ekrana geç."), + ("No displays", "Görüntü yok"), + ("Open in new window", "Yeni pencerede aç"), + ("Show displays as individual windows", "Ekranları ayrı pencereler olarak göster"), + ("Use all my displays for the remote session", "Uzak oturum için tüm ekranlarımı kullan"), + ("selinux_tip", "Cihazınızda SELinux etkin olduğundan, RustDesk'in kontrollü tarafta düzgün çalışmasını engelleyebilir."), + ("Change view", "Görünümü değiştir"), + ("Big tiles", "Büyük döşemeler"), + ("Small tiles", "Küçük döşemeler"), + ("List", "Liste"), + ("Virtual display", "Sanal ekran"), + ("Plug out all", "Tümünü çıkar"), + ("True color (4:4:4)", "Gerçek renk (4:4:4)"), + ("Enable blocking user input", "Kullanıcı girişini engellemeyi etkinleştir"), + ("id_input_tip", "Bir ID, doğrudan IP veya portlu bir etki alanı (:) girebilirsiniz.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (@?key=) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız lütfen \"@public\" girin, genel sunucu için anahtara gerek yoktur.\n\nİlk bağlantıda bir aktarma bağlantısının kullanılmasını zorlamak istiyorsanız ID'nin sonuna \"/r\" ekleyin, örneğin, \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Mod 1"), + ("privacy_mode_impl_virtual_display_tip", "Mod 2"), + ("Enter privacy mode", "Gizlilik moduna gir"), + ("Exit privacy mode", "Gizlilik modundan çık"), + ("idd_not_support_under_win10_2004_tip", "Dolaylı ekran sürücüsü desteklenmiyor. Windows 10, sürüm 2004 veya daha yenisi gereklidir."), + ("input_source_1_tip", "Giriş kaynağı 1"), + ("input_source_2_tip", "Giriş kaynağı 2"), + ("Swap control-command key", "Kontrol-komut tuşunu değiştir"), + ("swap-left-right-mouse", "Sol-sağ fare tuşlarını değiştir"), + ("2FA code", "2FA kodu"), + ("More", "Daha"), + ("enable-2fa-title", "İki faktörlü kimlik doğrulamayı etkinleştir"), + ("enable-2fa-desc", "Lütfen kimlik doğrulayıcınızı şimdi kurun. Telefonunuzda veya masaüstünüzde Authy, Microsoft veya Google Authenticator gibi bir kimlik doğrulayıcı uygulaması kullanabilirsiniz. İki faktörlü kimlik doğrulamayı etkinleştirmek için QR kodunu uygulamanızla tarayın ve uygulamanızın gösterdiği kodu girin."), + ("wrong-2fa-code", "Kod doğrulanamıyor. Kod ve yerel saat ayarlarının doğru olduğundan emin olun."), + ("enter-2fa-title", "İki faktörlü kimlik doğrulama"), + ("Email verification code must be 6 characters.", "E-posta doğrulama kodu 6 karakterden oluşmalıdır."), + ("2FA code must be 6 digits.", "2FA kodu 6 haneli olmalıdır."), + ("Multiple Windows sessions found", "Birden fazla Windows oturumu bulundu"), + ("Please select the session you want to connect to", "Lütfen bağlanmak istediğiniz oturumu seçin"), + ("powered_by_me", "RustDesk tarafından desteklenmektedir"), + ("outgoing_only_desk_tip", "Bu özelleştirilmiş bir sürümdür.\nDiğer cihazlara bağlanabilirsiniz, ancak diğer cihazlar cihazınıza bağlanamaz."), + ("preset_password_warning", "Bu özelleştirilmiş sürüm, önceden ayarlanmış bir parola ile birlikte gelir. Bu parolayı bilen herkes cihazınızın tam kontrolünü ele geçirebilir. Bunu beklemiyorsanız yazılımı hemen kaldırın."), + ("Security Alert", "Güvenlik Uyarısı"), + ("My address book", "Adres defterim"), + ("Personal", "Kişisel"), + ("Owner", "Sahip"), + ("Set shared password", "Paylaşılan parolayı ayarla"), + ("Exist in", "İçinde varolan"), + ("Read-only", "Salt okunur"), + ("Read/Write", "Okuma/Yazma"), + ("Full Control", "Tam Kontrol"), + ("share_warning_tip", "Yukarıdaki alanlar paylaşılır ve başkaları tarafından görülebilir"), + ("Everyone", "Herkes"), + ("ab_web_console_tip", "Web konsolu hakkında daha fazla bilgi"), + ("allow-only-conn-window-open-tip", "Yalnızca RustDesk penceresi açıksa bağlantıya izin ver"), + ("no_need_privacy_mode_no_physical_displays_tip", "Fiziksel ekran yok, gizlilik modunu kullanmaya gerek yok."), + ("Follow remote cursor", "Uzak imleci takip et"), + ("Follow remote window focus", "Uzak pencere odağını takip et"), + ("default_proxy_tip", "Varsayılan protokol ve port Socks5 ve 1080'dir."), + ("no_audio_input_device_tip", "Ses girişi aygıtı bulunamadı."), + ("Incoming", "Gelen"), + ("Outgoing", "Giden"), + ("Clear Wayland screen selection", "Wayland ekran seçimini temizle"), + ("clear_Wayland_screen_selection_tip", "Ekran seçimini temizledikten sonra paylaşılacak ekranı tekrar seçebilirsiniz."), + ("confirm_clear_Wayland_screen_selection_tip", "Wayland ekran seçimini temizlemek istediğinizden emin misiniz?"), + ("android_new_voice_call_tip", "Yeni bir sesli arama isteği alındı. Kabul ederseniz sesli iletişime geçilecektir."), + ("texture_render_tip", "Resimleri daha pürüzsüz hale getirmek için doku oluşturmayı kullanın. Oluşturma sorunlarıyla karşılaşırsanız bu seçeneği devre dışı bırakmayı deneyebilirsiniz."), + ("Use texture rendering", "Doku oluşturmayı kullan"), + ("Floating window", "Yüzen pencere"), + ("floating_window_tip", "RustDesk arka plan hizmetini açık tutmaya yardımcı olur"), + ("Keep screen on", "Ekranı açık tut"), + ("Never", "Asla"), + ("During controlled", "Kontrol sırasında"), + ("During service is on", "Servis açıkken"), + ("Capture screen using DirectX", "DirectX kullanarak ekran görüntüsü al"), + ("Back", "Geri"), + ("Apps", "Uygulamalar"), + ("Volume up", "Sesi yükselt"), + ("Volume down", "Sesi azalt"), + ("Power", "Güç"), + ("Telegram bot", "Telegram botu"), + ("enable-bot-tip", "Bu özelliği etkinleştirirseniz botunuzdan 2FA kodunu alabilirsiniz. Aynı zamanda bağlantı bildirimi işlevi de görebilir."), + ("enable-bot-desc", "1. @BotFather ile bir sohbet açın.\n2. \"/newbot\" komutunu gönderin. Bu adımı tamamladıktan sonra bir jeton alacaksınız.\n3. Yeni oluşturduğunuz botla bir sohbet başlatın. Etkinleştirmek için eğik çizgiyle (\"/\") başlayan \"/merhaba\" gibi bir mesaj gönderin.\n"), + ("cancel-2fa-confirm-tip", "2FA'yı iptal etmek istediğinizden emin misiniz?"), + ("cancel-bot-confirm-tip", "Telegram botunu iptal etmek istediğinizden emin misiniz?"), + ("About RustDesk", "RustDesk Hakkında"), + ("Send clipboard keystrokes", "Panoya tuş vuruşlarını gönder"), + ("network_error_tip", "Lütfen ağ bağlantınızı kontrol edin ve ardından yeniden dene'ye tıklayın."), + ("Unlock with PIN", "PIN ile kilidi açın"), + ("Requires at least {} characters", "En az {} karakter gerektirir"), + ("Wrong PIN", "Yanlış PIN"), + ("Set PIN", "PIN'i ayarla"), + ("Enable trusted devices", "Güvenilir cihazları etkinleştir"), + ("Manage trusted devices", "Güvenilir cihazları yönet"), + ("Platform", "Platform"), + ("Days remaining", "Kalan gün sayısı"), + ("enable-trusted-devices-tip", "Güvenilir cihazlarda 2FA doğrulamasını atla"), + ("Parent directory", "Üst dizin"), + ("Resume", "Devam ettir"), + ("Invalid file name", "Geçersiz dosya adı"), + ("one-way-file-transfer-tip", "Kontrol edilen tarafta tek yönlü dosya transferi aktiftir."), + ("Authentication Required", "Kimlik Doğrulama Gerekli"), + ("Authenticate", "Kimlik Doğrula"), + ("web_id_input_tip", "Aynı sunucuda bir kimlik girebilirsiniz, web istemcisinde doğrudan IP erişimi desteklenmez.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (@?key=) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız, lütfen \"@public\" girin, genel sunucu için anahtara gerek yoktur."), + ("Download", "İndir"), + ("Upload folder", "Klasör yükle"), + ("Upload files", "Dosya yükle"), + ("Clipboard is synchronized", "Pano senkronize edildi"), + ("Update client clipboard", "İstemci panosunu güncelle"), + ("Untagged", "Etiketsiz"), + ("new-version-of-{}-tip", "{}'nin yeni bir sürümü mevcut"), + ("Accessible devices", "Erişilebilir cihazlar"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Lütfen uzak tarafta RustDesk istemcisini {} sürümüne veya daha yenisine güncelleyin!"), + ("d3d_render_tip", "D3D oluşturma etkinleştirildiğinde, bazı bilgisayarlarda uzak kontrol ekranı siyah görünebilir."), + ("Use D3D rendering", "D3D oluşturmayı kullan"), + ("Printer", "Yazıcı"), + ("printer-os-requirement-tip", "Yazıcı çıkış fonksiyonu için Windows 10 veya üzeri gereklidir."), + ("printer-requires-installed-{}-client-tip", "Uzaktan yazdırmayı kullanabilmek için bu cihaza {} yüklenmesi gerekir."), + ("printer-{}-not-installed-tip", "{} Yazıcısı yüklü değil."), + ("printer-{}-ready-tip", "{} Yazıcısı kuruldu ve kullanıma hazır."), + ("Install {} Printer", "{} Yazıcısını Yükle"), + ("Outgoing Print Jobs", "Giden Yazdırma İşleri"), + ("Incoming Print Jobs", "Gelen Yazdırma İşleri"), + ("Incoming Print Job", "Gelen Yazdırma İşi"), + ("use-the-default-printer-tip", "Varsayılan yazıcıyı kullan"), + ("use-the-selected-printer-tip", "Seçili yazıcıyı kullan"), + ("auto-print-tip", "Seçili yazıcıyı kullanarak otomatik olarak yazdır."), + ("print-incoming-job-confirm-tip", "Uzak bir kaynaktan yazdırma işi aldınız. Bunu kendi tarafınızda çalıştırmak ister misiniz?"), + ("remote-printing-disallowed-tile-tip", "Uzak Yazdırma engellendi"), + ("remote-printing-disallowed-text-tip", "Kontrol edilen tarafın izin ayarları Uzak Yazdırmaya izin vermiyor."), + ("save-settings-tip", "Ayarları kaydet"), + ("dont-show-again-tip", "Bunu bir daha gösterme"), + ("Take screenshot", "Ekran görüntüsü al"), + ("Taking screenshot", "Ekran görüntüsü alınıyor"), + ("screenshot-merged-screen-not-supported-tip", "Birden fazla ekranın ekran görüntülerinin birleştirilmesi şu anda desteklenmiyor. Lütfen tek bir ekrana geçin ve tekrar deneyin."), + ("screenshot-action-tip", "Lütfen ekran görüntüsüyle nasıl devam edeceğinizi seçin."), + ("Save as", "Farklı kaydet"), + ("Copy to clipboard", "Panoya kopyala"), + ("Enable remote printer", "Uzak yazıcıyı etkinleştir"), + ("Downloading {}", "{} indiriliyor"), + ("{} Update", "{} Güncellemesi"), + ("{}-to-update-tip", "{} şimdi kapanacak ve yeni sürüm kurulacak."), + ("download-new-version-failed-tip", "İndirme başarısız oldu. Tekrar deneyebilir veya 'İndir' düğmesine tıklayarak sürüm sayfasından manuel olarak indirip güncelleyebilirsiniz."), + ("Auto update", "Otomatik güncelleme"), + ("update-failed-check-msi-tip", "Kurulum yöntemi denetimi başarısız oldu. Sürüm sayfasından indirmek ve manuel olarak yükseltmek için lütfen \"İndir\" düğmesine tıklayın."), + ("websocket_tip", "WebSocket kullanıldığında yalnızca aktarma bağlantıları desteklenir."), + ("Use WebSocket", "WebSocket'ı kullan"), + ("Trackpad speed", "İzleme paneli hızı"), + ("Default trackpad speed", "Varsayılan izleme paneli hızı"), + ("Numeric one-time password", "Sayısal tek seferlik parola"), + ("Enable IPv6 P2P connection", "IPv6 P2P bağlantısını etkinleştir"), + ("Enable UDP hole punching", "UDP delik açmayı etkinleştir"), + ("View camera", "Kamerayı görüntüle"), + ("Enable camera", "Kamerayı etkinleştir"), + ("No cameras", "Kamera yok"), + ("view_camera_unsupported_tip", "Uzak cihaz, kameranın görüntülenmesini desteklemiyor."), + ("Terminal", "Terminal"), + ("Enable terminal", "Terminali etkinleştir"), + ("New tab", "Yeni sekme"), + ("Keep terminal sessions on disconnect", "Bağlantı kesildiğinde terminal oturumlarını açık tut"), + ("Terminal (Run as administrator)", "Terminal (Yönetici olarak çalıştır)"), + ("terminal-admin-login-tip", "Lütfen kontrol edilen tarafın yönetici kullanıcı adı ve parolasını giriniz."), + ("Failed to get user token.", "Kullanıcı belirteci alınamadı."), + ("Incorrect username or password.", "Hatalı kullanıcı adı veya parola."), + ("The user is not an administrator.", "Kullanıcı bir yönetici değil."), + ("Failed to check if the user is an administrator.", "Kullanıcının yönetici olup olmadığı kontrol edilemedi."), + ("Supported only in the installed version.", "Sadece yüklü sürümde desteklenir."), + ("elevation_username_tip", "Kullanıcı adı veya etki alanı\\kullanıcı adı girin"), + ("Preparing for installation ...", "Kuruluma hazırlanıyor..."), + ("Show my cursor", "İmlecimi göster"), + ("Scale custom", "Özel ölçekte"), + ("Custom scale slider", "Özel ölçek kaydırıcısı"), + ("Decrease", "Azalt"), + ("Increase", "Arttır"), + ("Show virtual mouse", "Sanal fareyi göster"), + ("Virtual mouse size", "Sanal fare boyutu"), + ("Small", "Küçük"), + ("Large", "Büyük"), + ("Show virtual joystick", "Sanal joystiği göster"), + ("Edit note", "Notu düzenle"), + ("Alias", "Takma ad"), + ("ScrollEdge", "Kaydırma kenarı"), + ("Allow insecure TLS fallback", "Güvensiz TLS geri dönüşüne izin ver"), + ("allow-insecure-tls-fallback-tip", "Varsayılan olarak, RustDesk sunucu sertifikasını TLS kullanarak protokoller için doğrular.\nBu seçenek etkinleştirildiğinde, doğrulama başarısızlığı durumunda RustDesk doğrulama adımını atlayarak işleme devam eder."), + ("Disable UDP", "UDP'yi devre dışı bırak"), + ("disable-udp-tip", "Yalnızca TCP kullanılıp kullanılmayacağını kontrol eder.\nBu seçenek etkinleştirildiğinde, RustDesk artık UDP 21116'yı kullanmayacak, bunun yerine TCP 21116 kullanılacaktır."), + ("server-oss-not-support-tip", "NOT: RustDesk sunucu OSS'si bu özelliği içermemektedir."), + ("input note here", "Notu buraya girin"), + ("note-at-conn-end-tip", "Bağlantı bittiğinde not sorulsun"), + ("Show terminal extra keys", "Terminal ek tuşlarını göster"), + ("Relative mouse mode", "Fareyi göreli modda kullan"), + ("rel-mouse-not-supported-peer-tip", "Karşı taraf göreli fare modunu desteklemiyor"), + ("rel-mouse-not-ready-tip", "Göreli fare modu henüz hazır değil"), + ("rel-mouse-lock-failed-tip", "Göreli fare kilitlenemedi"), + ("rel-mouse-exit-{}-tip", "Göreli fare modundan çıkmak için {}"), + ("rel-mouse-permission-lost-tip", "Göreli fare izinleri geçerli değil"), + ("Changelog", "Değişiklik Günlüğü"), + ("keep-awake-during-outgoing-sessions-label", "Giden oturumlar süresince ekranı açık tutun"), + ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), + ("Continue with {}", "{} ile devam et"), + ("Display Name", "Görünen Ad"), + ("password-hidden-tip", "Parola gizli"), + ("preset-password-in-use-tip", "Önceden ayarlanmış parola kullanılıyor"), + ("Enable privacy mode", "Gizlilik modunu etkinleştir"), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/tw.rs b/vendor/rustdesk/src/lang/tw.rs new file mode 100644 index 0000000..b23b849 --- /dev/null +++ b/vendor/rustdesk/src/lang/tw.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "狀態"), + ("Your Desktop", "您的桌面"), + ("desk_tip", "您可以透過此 ID 及密碼存取您的桌面"), + ("Password", "密碼"), + ("Ready", "就緒"), + ("Established", "已建立"), + ("connecting_status", "正在連線到 RustDesk 網路..."), + ("Enable service", "啟用服務"), + ("Start service", "啟動服務"), + ("Service is running", "服務正在執行"), + ("Service is not running", "服務尚未執行"), + ("not_ready_status", "尚未就緒,請檢查您的網路連線。"), + ("Control Remote Desktop", "控制遠端桌面"), + ("Transfer file", "傳輸檔案"), + ("Connect", "連線"), + ("Recent sessions", "近期的工作階段"), + ("Address book", "通訊錄"), + ("Confirmation", "確認"), + ("TCP tunneling", "TCP 通道"), + ("Remove", "移除"), + ("Refresh random password", "重新產生隨機密碼"), + ("Set your own password", "自行設定密碼"), + ("Enable keyboard/mouse", "啟用鍵盤和滑鼠"), + ("Enable clipboard", "啟用剪貼簿"), + ("Enable file transfer", "啟用檔案傳輸"), + ("Enable TCP tunneling", "啟用 TCP 通道"), + ("IP Whitelisting", "IP 白名單"), + ("ID/Relay Server", "ID / 中繼伺服器"), + ("Import server config", "匯入伺服器設定"), + ("Export Server Config", "匯出伺服器設定"), + ("Import server configuration successfully", "匯入伺服器設定成功"), + ("Export server configuration successfully", "匯出伺服器設定成功"), + ("Invalid server configuration", "無效的伺服器設定"), + ("Clipboard is empty", "剪貼簿是空的"), + ("Stop service", "停止服務"), + ("Change ID", "更改 ID"), + ("Your new ID", "您的新 ID"), + ("length %min% to %max%", "長度在 %min% 與 %max% 之間"), + ("starts with a letter", "以字母開頭"), + ("allowed characters", "允許的字元"), + ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、 - (dash)、_ (底線)。第一個字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), + ("Website", "網站"), + ("About", "關於"), + ("Slogan_tip", "在這個混沌的世界中用心製作!"), + ("Privacy Statement", "隱私權宣告"), + ("Mute", "靜音"), + ("Build Date", "建構日期"), + ("Version", "版本"), + ("Home", "首頁"), + ("Audio Input", "音訊輸入"), + ("Enhancements", "增強功能"), + ("Hardware Codec", "硬體編解碼器"), + ("Adaptive bitrate", "自適應位元速率"), + ("ID Server", "ID 伺服器"), + ("Relay Server", "中繼伺服器"), + ("API Server", "API 伺服器"), + ("invalid_http", "開頭必須為 http:// 或 https://"), + ("Invalid IP", "IP 無效"), + ("Invalid format", "格式無效"), + ("server_not_support", "伺服器尚未支援"), + ("Not available", "無法使用"), + ("Too frequent", "修改過於頻繁,請稍後再試。"), + ("Cancel", "取消"), + ("Skip", "跳過"), + ("Close", "關閉"), + ("Retry", "重試"), + ("OK", "確定"), + ("Password Required", "需要密碼"), + ("Please enter your password", "請輸入您的密碼"), + ("Remember password", "記住密碼"), + ("Wrong Password", "密碼錯誤"), + ("Do you want to enter again?", "您要重新輸入嗎?"), + ("Connection Error", "連線錯誤"), + ("Error", "錯誤"), + ("Reset by the peer", "對方重設了連線"), + ("Connecting...", "正在連線..."), + ("Connection in progress. Please wait.", "正在連線,請稍候。"), + ("Please try 1 minute later", "請於 1 分鐘後再試"), + ("Login Error", "登入錯誤"), + ("Successful", "成功"), + ("Connected, waiting for image...", "已連線,等待畫面傳輸..."), + ("Name", "名稱"), + ("Type", "類型"), + ("Modified", "修改時間"), + ("Size", "大小"), + ("Show Hidden Files", "顯示隱藏檔案"), + ("Receive", "接收"), + ("Send", "傳送"), + ("Refresh File", "重新整理檔案"), + ("Local", "本機"), + ("Remote", "遠端"), + ("Remote Computer", "遠端電腦"), + ("Local Computer", "本機電腦"), + ("Confirm Delete", "確認刪除"), + ("Delete", "刪除"), + ("Properties", "屬性"), + ("Multi Select", "多選"), + ("Select All", "全選"), + ("Unselect All", "取消全選"), + ("Empty Directory", "空資料夾"), + ("Not an empty directory", "不是一個空資料夾"), + ("Are you sure you want to delete this file?", "您確定要刪除此檔案嗎?"), + ("Are you sure you want to delete this empty directory?", "您確定要刪除此空資料夾嗎?"), + ("Are you sure you want to delete the file of this directory?", "您確定要刪除此資料夾中的檔案嗎?"), + ("Do this for all conflicts", "套用到其他衝突"), + ("This is irreversible!", "此操作不可逆!"), + ("Deleting", "正在刪除..."), + ("files", "檔案"), + ("Waiting", "正在等候..."), + ("Finished", "已完成"), + ("Speed", "速度"), + ("Custom Image Quality", "自訂畫面品質"), + ("Privacy mode", "隱私模式"), + ("Block user input", "封鎖使用者輸入"), + ("Unblock user input", "取消封鎖使用者輸入"), + ("Adjust Window", "調整視窗"), + ("Original", "原始"), + ("Shrink", "縮減"), + ("Stretch", "延展"), + ("Scrollbar", "捲動條"), + ("ScrollAuto", "自動捲動"), + ("Good image quality", "最佳化畫面品質"), + ("Balanced", "平衡"), + ("Optimize reaction time", "最佳化反應時間"), + ("Custom", "自訂"), + ("Show remote cursor", "顯示遠端游標"), + ("Show quality monitor", "顯示品質監測"), + ("Disable clipboard", "停用剪貼簿"), + ("Lock after session end", "工作階段結束後鎖定電腦"), + ("Insert Ctrl + Alt + Del", "插入 Ctrl + Alt + Del"), + ("Insert Lock", "鎖定遠端電腦"), + ("Refresh", "重新載入"), + ("ID does not exist", "ID 不存在"), + ("Failed to connect to rendezvous server", "無法連線到 ID 伺服器"), + ("Please try later", "請稍候再試"), + ("Remote desktop is offline", "遠端桌面已離線"), + ("Key mismatch", "金鑰不符"), + ("Timeout", "逾時"), + ("Failed to connect to relay server", "無法連線到中繼伺服器"), + ("Failed to connect via rendezvous server", "無法透過 ID 伺服器連線"), + ("Failed to connect via relay server", "無法透過中繼伺服器連線"), + ("Failed to make direct connection to remote desktop", "無法直接連線到遠端桌面"), + ("Set Password", "設定密碼"), + ("OS Password", "作業系統密碼"), + ("install_tip", "UAC 會導致 RustDesk 在某些情況下無法正常作為遠端端點運作。若要避開 UAC,請點選下方按鈕將 RustDesk 安裝到系統中。"), + ("Click to upgrade", "點選以升級"), + ("Configure", "設定"), + ("config_acc", "為了遠端控制您的桌面,您需要授予 RustDesk「無障礙功能」權限。"), + ("config_screen", "為了遠端存取您的桌面,您需要授予 RustDesk「螢幕錄製」權限。"), + ("Installing ...", "正在安裝..."), + ("Install", "安裝"), + ("Installation", "安裝"), + ("Installation Path", "安裝路徑"), + ("Create start menu shortcuts", "新增開始功能表捷徑"), + ("Create desktop icon", "新增桌面捷徑"), + ("agreement_tip", "開始安裝即表示您接受授權條款。"), + ("Accept and Install", "接受並安裝"), + ("End-user license agreement", "終端使用者授權合約"), + ("Generating ...", "正在產生..."), + ("Your installation is lower version.", "您安裝的版本過舊。"), + ("not_close_tcp_tip", "在使用通道時請不要關閉此視窗"), + ("Listening ...", "正在等待通道連線..."), + ("Remote Host", "遠端主機"), + ("Remote Port", "遠端連接埠"), + ("Action", "操作"), + ("Add", "新增"), + ("Local Port", "本機連接埠"), + ("Local Address", "本機位址"), + ("Change Local Port", "修改本機連接埠"), + ("setup_server_tip", "若您需要更快的連線速度,您可以選擇自行建立伺服器"), + ("Too short, at least 6 characters.", "過短,至少需要 6 個字元。"), + ("The confirmation is not identical.", "兩次輸入不相符"), + ("Permissions", "權限"), + ("Accept", "接受"), + ("Dismiss", "關閉"), + ("Disconnect", "中斷連線"), + ("Enable file copy and paste", "允許檔案複製和貼上"), + ("Connected", "已連線"), + ("Direct and encrypted connection", "加密直接連線"), + ("Relayed and encrypted connection", "加密中繼連線"), + ("Direct and unencrypted connection", "直接且未加密的連線"), + ("Relayed and unencrypted connection", "中繼且未加密的連線"), + ("Enter Remote ID", "輸入遠端 ID"), + ("Enter your password", "輸入您的密碼"), + ("Logging in...", "正在登入..."), + ("Enable RDP session sharing", "啟用 RDP 工作階段分享"), + ("Auto Login", "自動登入 (只在您設定「工作階段結束後鎖定」時有效)"), + ("Enable direct IP access", "啟用 IP 直接存取"), + ("Rename", "重新命名"), + ("Space", "空白"), + ("Create desktop shortcut", "新增桌面捷徑"), + ("Change Path", "更改路徑"), + ("Create Folder", "新增資料夾"), + ("Please enter the folder name", "請輸入資料夾名稱"), + ("Fix it", "修復"), + ("Warning", "警告"), + ("Login screen using Wayland is not supported", "不支援使用 Wayland 的登入畫面"), + ("Reboot required", "需要重新啟動"), + ("Unsupported display server", "不支援的顯示伺服器"), + ("x11 expected", "預期為 x11"), + ("Port", "連接埠"), + ("Settings", "設定"), + ("Username", "使用者名稱"), + ("Invalid port", "連接埠無效"), + ("Closed manually by the peer", "對方關閉了工作階段"), + ("Enable remote configuration modification", "允許遠端使用者更改設定"), + ("Run without install", "跳過安裝直接執行"), + ("Connect via relay", "中繼連線"), + ("Always connect via relay", "一律透過中繼連線"), + ("whitelist_tip", "只有白名單上的 IP 可以存取"), + ("Login", "登入"), + ("Verify", "驗證"), + ("Remember me", "記住我"), + ("Trust this device", "信任這部裝置"), + ("Verification code", "驗證碼"), + ("verification_tip", "驗證碼已傳送到註冊的電子郵件地址,請輸入驗證碼以繼續登入。"), + ("Logout", "登出"), + ("Tags", "標籤"), + ("Search ID", "搜尋 ID"), + ("whitelist_sep", "使用逗號、分號、空格,或是換行來分隔"), + ("Add ID", "新增 ID"), + ("Add Tag", "新增標籤"), + ("Unselect all tags", "取消選取所有標籤"), + ("Network error", "網路錯誤"), + ("Username missed", "缺少使用者名稱"), + ("Password missed", "缺少密碼"), + ("Wrong credentials", "登入資訊錯誤"), + ("The verification code is incorrect or has expired", "驗證碼錯誤或已過期"), + ("Edit Tag", "編輯標籤"), + ("Forget Password", "忘記密碼"), + ("Favorites", "我的最愛"), + ("Add to Favorites", "加入我的最愛"), + ("Remove from Favorites", "從我的最愛中移除"), + ("Empty", "空空如也"), + ("Invalid folder name", "資料夾名稱無效"), + ("Socks5 Proxy", "Socks5 代理伺服器"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) 代理伺服器"), + ("Discovered", "已探索"), + ("install_daemon_tip", "若要在開機時啟動,您需要安裝系統服務。"), + ("Remote ID", "遠端 ID"), + ("Paste", "貼上"), + ("Paste here?", "在此貼上?"), + ("Are you sure to close the connection?", "您確定要關閉連線嗎?"), + ("Download new version", "下載新版本"), + ("Touch mode", "觸控模式"), + ("Mouse mode", "滑鼠模式"), + ("One-Finger Tap", "單指輕觸"), + ("Left Mouse", "滑鼠左鍵"), + ("One-Long Tap", "單指長按"), + ("Two-Finger Tap", "雙指輕觸"), + ("Right Mouse", "滑鼠右鍵"), + ("One-Finger Move", "單指移動"), + ("Double Tap & Move", "點兩下並移動"), + ("Mouse Drag", "滑鼠拖曳"), + ("Three-Finger vertically", "三指垂直滑動"), + ("Mouse Wheel", "滑鼠滾輪"), + ("Two-Finger Move", "雙指移動"), + ("Canvas Move", "移動畫布"), + ("Pinch to Zoom", "雙指縮放"), + ("Canvas Zoom", "縮放畫布"), + ("Reset canvas", "重設畫布"), + ("No permission of file transfer", "沒有檔案傳輸權限"), + ("Note", "備註"), + ("Connection", "連線"), + ("Share screen", "螢幕分享"), + ("Chat", "聊天"), + ("Total", "總計"), + ("items", "個項目"), + ("Selected", "已選擇"), + ("Screen Capture", "畫面錄製"), + ("Input Control", "輸入控制"), + ("Audio Capture", "音訊錄製"), + ("Do you accept?", "是否接受?"), + ("Open System Setting", "開啟系統設定"), + ("How to get Android input permission?", "如何取得 Android 的輸入權限?"), + ("android_input_permission_tip1", "為了讓遠端裝置能夠透過滑鼠或觸控控制您的 Android 裝置,您需要允許 RustDesk 使用「輔助功能」服務。"), + ("android_input_permission_tip2", "請前往下一個系統設定頁面,找到並進入「已安裝的服務」,開啟「RustDesk Input」服務。"), + ("android_new_connection_tip", "收到新的控制請求,對方想要控制您目前的裝置。"), + ("android_service_will_start_tip", "開啟「畫面錄製」將自動啟動服務,允許其他裝置向您的裝置請求連線。"), + ("android_stop_service_tip", "關閉服務將自動關閉所有已建立的連線。"), + ("android_version_audio_tip", "目前的 Android 版本不支援音訊錄製,請升級至 Android 10 或更新的版本。"), + ("android_start_service_tip", "點選「啟動服務」或啟用「畫面錄製」權限以啟動螢幕分享服務。"), + ("android_permission_may_not_change_tip", "已建立連線的權限可能不會立即改變,除非重新連線。"), + ("Account", "帳號"), + ("Overwrite", "取代"), + ("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要略過或是取代此檔案嗎?"), + ("Quit", "退出"), + ("Help", "說明"), + ("Failed", "失敗"), + ("Succeeded", "成功"), + ("Someone turns on privacy mode, exit", "有人開啟了隱私模式,退出"), + ("Unsupported", "不支援"), + ("Peer denied", "對方拒絕"), + ("Please install plugins", "請安裝外掛程式"), + ("Peer exit", "對方退出"), + ("Failed to turn off", "關閉失敗"), + ("Turned off", "已關閉"), + ("Language", "語言"), + ("Keep RustDesk background service", "保持 RustDesk 後台服務"), + ("Ignore Battery Optimizations", "忽略電池最佳化"), + ("android_open_battery_optimizations_tip", "如果您想要停用此功能,請前往下一個 RustDesk 應用程式設定頁面,找到並進入「電池」,取消勾選「不受限制」"), + ("Start on boot", "開機時啟動"), + ("Start the screen sharing service on boot, requires special permissions", "開機時啟動螢幕分享服務,需要特殊權限。"), + ("Connection not allowed", "不允許連線"), + ("Legacy mode", "傳統模式"), + ("Map mode", "1:1 傳輸模式"), + ("Translate mode", "翻譯模式"), + ("Use permanent password", "使用固定密碼"), + ("Use both passwords", "同時使用兩種密碼"), + ("Set permanent password", "設定固定密碼"), + ("Enable remote restart", "啟用遠端重新啟動"), + ("Restart remote device", "重新啟動遠端裝置"), + ("Are you sure you want to restart", "您確定要重新啟動嗎?"), + ("Restarting remote device", "正在重新啟動遠端裝置"), + ("remote_restarting_tip", "遠端裝置正在重新啟動,請關閉此對話框,並在一段時間後使用永久密碼重新連線"), + ("Copied", "已複製"), + ("Exit Fullscreen", "退出全螢幕"), + ("Fullscreen", "全螢幕"), + ("Mobile Actions", "手機操作"), + ("Select Monitor", "選擇顯示器"), + ("Control Actions", "控制操作"), + ("Display Settings", "顯示設定"), + ("Ratio", "比例"), + ("Image Quality", "畫質"), + ("Scroll Style", "捲動樣式"), + ("Show Toolbar", "顯示工具列"), + ("Hide Toolbar", "隱藏工具列"), + ("Direct Connection", "直接連線"), + ("Relay Connection", "中繼連線"), + ("Secure Connection", "安全連線"), + ("Insecure Connection", "非安全連線"), + ("Scale original", "原始尺寸"), + ("Scale adaptive", "適應視窗"), + ("General", "一般"), + ("Security", "安全"), + ("Theme", "主題"), + ("Dark Theme", "黑暗主題"), + ("Light Theme", "明亮主題"), + ("Dark", "黑暗"), + ("Light", "明亮"), + ("Follow System", "跟隨系統"), + ("Enable hardware codec", "啟用硬體編解碼器"), + ("Unlock Security Settings", "解鎖安全設定"), + ("Enable audio", "啟用音訊"), + ("Unlock Network Settings", "解鎖網路設定"), + ("Server", "伺服器"), + ("Direct IP Access", "IP 直接連線"), + ("Proxy", "代理伺服器"), + ("Apply", "套用"), + ("Disconnect all devices?", "是否中斷所有遠端連線?"), + ("Clear", "清空"), + ("Audio Input Device", "音訊輸入裝置"), + ("Use IP Whitelisting", "只允許白名單上的 IP 進行連線"), + ("Network", "網路"), + ("Pin Toolbar", "釘選工具列"), + ("Unpin Toolbar", "取消釘選工具列"), + ("Recording", "錄製"), + ("Directory", "路徑"), + ("Automatically record incoming sessions", "自動錄製連入的工作階段"), + ("Automatically record outgoing sessions", "自動錄製連出的工作階段"), + ("Change", "變更"), + ("Start session recording", "開始錄影"), + ("Stop session recording", "停止錄影"), + ("Enable recording session", "啟用錄製工作階段"), + ("Enable LAN discovery", "允許區域網路探索"), + ("Deny LAN discovery", "拒絕區域網路探索"), + ("Write a message", "輸入聊天訊息"), + ("Prompt", "提示"), + ("Please wait for confirmation of UAC...", "請等待對方確認 UAC..."), + ("elevated_foreground_window_tip", "目前遠端桌面的視窗需要更高的權限才能繼續操作,您暫時無法使用滑鼠和鍵盤,您可以請求對方最小化目前視窗,或者在連線管理視窗點選提升權限。為了避免這個問題,建議在遠端裝置上安裝本軟體。"), + ("Disconnected", "斷開連線"), + ("Other", "其他"), + ("Confirm before closing multiple tabs", "關閉多個分頁前詢問我"), + ("Keyboard Settings", "鍵盤設定"), + ("Full Access", "完全存取"), + ("Screen Share", "僅分享螢幕畫面"), + ("ubuntu-21-04-required", "Wayland 需要 Ubuntu 21.04 或更新的版本。"), + ("wayland-requires-higher-linux-version", "Wayland 需要更新版的 Linux 發行版。請嘗試使用 X11 桌面或更改您的作業系統。"), + ("xdp-portal-unavailable", ""), + ("JumpLink", "查看"), + ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的螢幕畫面(在對方的裝置上操作)。"), + ("Show RustDesk", "顯示 RustDesk"), + ("This PC", "此電腦"), + ("or", "或"), + ("Elevate", "提升權限"), + ("Zoom cursor", "縮放游標"), + ("Accept sessions via password", "只允許透過輸入密碼進行連線"), + ("Accept sessions via click", "只允許透過點選接受進行連線"), + ("Accept sessions via both", "允許輸入密碼或點選接受進行連線"), + ("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求..."), + ("One-time Password", "一次性密碼"), + ("Use one-time password", "使用一次性密碼"), + ("One-time password length", "一次性密碼長度"), + ("Request access to your device", "請求存取您的裝置"), + ("Hide connection management window", "隱藏連線管理視窗"), + ("hide_cm_tip", "只在允許密碼連線且使用固定密碼的情況下才隱藏"), + ("wayland_experiment_tip", "目前對於 Wayland 的支援處於實驗階段,如果您需要使用無人值守存取,請使用 X11。"), + ("Right click to select tabs", "右鍵選擇分頁"), + ("Skipped", "已跳過"), + ("Add to address book", "新增到通訊錄"), + ("Group", "群組"), + ("Search", "搜尋"), + ("Closed manually by web console", "被 Web 控制台手動關閉"), + ("Local keyboard type", "本機鍵盤類型"), + ("Select local keyboard type", "請選擇本機鍵盤類型"), + ("software_render_tip", "如果您使用 Nvidia 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體繪製可能會有幫助。重新啟動軟體後生效。"), + ("Always use software rendering", "使用軟體繪製"), + ("config_input", "為了能夠透過鍵盤控制遠端桌面,請給予 RustDesk「輸入監控」權限。"), + ("config_microphone", "為了支援透過麥克風進行音訊傳輸,請給予 RustDesk「錄音」權限。"), + ("request_elevation_tip", "如果遠端使用者可以操作電腦,您可以請求提升權限。"), + ("Wait", "等待"), + ("Elevation Error", "權限提升失敗"), + ("Ask the remote user for authentication", "請求遠端使用者進行驗證"), + ("Choose this if the remote account is administrator", "當遠端使用者帳戶是管理員時,請選擇此選項"), + ("Transmit the username and password of administrator", "傳送管理員的使用者名稱和密碼"), + ("still_click_uac_tip", "依然需要遠端使用者在執行 RustDesk 時於 UAC 視窗點選「是」。"), + ("Request Elevation", "請求權限提升"), + ("wait_accept_uac_tip", "請等待遠端使用者確認 UAC 對話框。"), + ("Elevate successfully", "權限提升成功"), + ("uppercase", "大寫字母"), + ("lowercase", "小寫字母"), + ("digit", "數字"), + ("special character", "特殊字元"), + ("length>=8", "長度大於或等於 8"), + ("Weak", "弱"), + ("Medium", "中"), + ("Strong", "強"), + ("Switch Sides", "反轉存取方向"), + ("Please confirm if you want to share your desktop?", "請確認是否要讓對方存取您的桌面?"), + ("Display", "顯示"), + ("Default View Style", "預設顯示方式"), + ("Default Scroll Style", "預設捲動方式"), + ("Default Image Quality", "預設影像品質"), + ("Default Codec", "預設編解碼器"), + ("Bitrate", "位元速率"), + ("FPS", "FPS"), + ("Auto", "自動"), + ("Other Default Options", "其他預設選項"), + ("Voice call", "語音通話"), + ("Text chat", "文字聊天"), + ("Stop voice call", "停止語音通話"), + ("relay_hint_tip", "可能無法使用直接連線,您可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,您可以在 ID 後面新增「/r」,或是如果近期的工作階段裡存在該裝置,您也可以在裝置選項裡選擇「一律透過中繼連線」。"), + ("Reconnect", "重新連線"), + ("Codec", "編解碼器"), + ("Resolution", "解析度"), + ("No transfers in progress", "沒有正在進行的傳輸"), + ("Set one-time password length", "設定一次性密碼的長度"), + ("RDP Settings", "RDP 設定"), + ("Sort by", "排序方式"), + ("New Connection", "新連線"), + ("Restore", "還原"), + ("Minimize", "最小化"), + ("Maximize", "最大化"), + ("Your Device", "您的裝置"), + ("empty_recent_tip", "哎呀,沒有近期的工作階段!\n是時候安排點新工作了。"), + ("empty_favorite_tip", "空空如也"), + ("empty_lan_tip", "喔不,看來我們目前找不到任何夥伴。"), + ("empty_address_book_tip", "老天,看來您的通訊錄中沒有任何夥伴。"), + ("Empty Username", "空使用者帳號"), + ("Empty Password", "空密碼"), + ("Me", "我"), + ("identical_file_tip", "此檔案與對方的檔案一致。"), + ("show_monitors_tip", "在工具列中顯示顯示器"), + ("View Mode", "瀏覽模式"), + ("login_linux_tip", "需要登入到遠端 Linux 使用者帳戶才能啟用 X 桌面環境"), + ("verify_rustdesk_password_tip", "驗證 RustDesk 密碼"), + ("remember_account_tip", "記住此使用者帳戶"), + ("os_account_desk_tip", "此使用者帳戶將用於登入遠端作業系統並啟用無頭模式 (headless mode) 的桌面連線"), + ("OS Account", "作業系統使用者帳戶"), + ("another_user_login_title_tip", "另一個使用者已經登入"), + ("another_user_login_text_tip", "斷開連線"), + ("xorg_not_found_title_tip", "找不到 Xorg"), + ("xorg_not_found_text_tip", "請安裝 Xorg"), + ("no_desktop_title_tip", "沒有可用的桌面環境"), + ("no_desktop_text_tip", "請安裝 GNOME 桌面"), + ("No need to elevate", "不需要提升權限"), + ("System Sound", "系統音效"), + ("Default", "預設"), + ("New RDP", "新的 RDP"), + ("Fingerprint", "指紋"), + ("Copy Fingerprint", "複製指紋"), + ("no fingerprints", "沒有指紋"), + ("Select a peer", "選擇夥伴"), + ("Select peers", "選擇夥伴"), + ("Plugins", "外掛程式"), + ("Uninstall", "解除安裝"), + ("Update", "更新"), + ("Enable", "啟用"), + ("Disable", "停用"), + ("Options", "選項"), + ("resolution_original_tip", "原始解析度"), + ("resolution_fit_local_tip", "調整成本機解析度"), + ("resolution_custom_tip", "自訂解析度"), + ("Collapse toolbar", "收回工具列"), + ("Accept and Elevate", "接受並提升權限"), + ("accept_and_elevate_btn_tooltip", "接受連線並提升 UAC 權限。"), + ("clipboard_wait_response_timeout_tip", "等待複製回應逾時。"), + ("Incoming connection", "傳入的連線"), + ("Outgoing connection", "發起的連線"), + ("Exit", "退出"), + ("Open", "開啟"), + ("logout_tip", "您確定要登出嗎?"), + ("Service", "服務"), + ("Start", "啟動"), + ("Stop", "停止"), + ("exceed_max_devices", "您的已管理裝置已超過最大數量。"), + ("Sync with recent sessions", "與近期工作階段同步"), + ("Sort tags", "排序標籤"), + ("Open connection in new tab", "在新分頁開啟連線"), + ("Move tab to new window", "移動標籤到新視窗"), + ("Can not be empty", "不能為空"), + ("Already exists", "已經存在"), + ("Change Password", "更改密碼"), + ("Refresh Password", "重新整理密碼"), + ("ID", "ID"), + ("Grid View", "網格檢視"), + ("List View", "清單檢視"), + ("Select", "選擇"), + ("Toggle Tags", "切換標籤"), + ("pull_ab_failed_tip", "通訊錄更新失敗"), + ("push_ab_failed_tip", "同步通訊錄至伺服器失敗"), + ("synced_peer_readded_tip", "最近工作階段中存在的裝置將會被重新同步到通訊錄。"), + ("Change Color", "更改顏色"), + ("Primary Color", "基本色"), + ("HSV Color", "HSV 色"), + ("Installation Successful!", "安裝成功!"), + ("Installation failed!", "安裝失敗!"), + ("Reverse mouse wheel", "滑鼠滾輪反向"), + ("{} sessions", "{} 個工作階段"), + ("scam_title", "您可能遭遇詐騙!"), + ("scam_text1", "如果您正在和素不相識的人通話,而對方要求您使用 RustDesk 啟用服務,請勿繼續操作並立即掛斷電話。"), + ("scam_text2", "他們很可能是詐騙,試圖竊取您的金錢或其他個人資料。"), + ("Don't show again", "下次不再顯示"), + ("I Agree", "同意"), + ("Decline", "拒絕"), + ("Timeout in minutes", "超時(分鐘)"), + ("auto_disconnect_option_tip", "自動在連入的使用者不活躍時關閉工作階段"), + ("Connection failed due to inactivity", "由於長時間沒有操作,已自動關閉工作階段"), + ("Check for software update on startup", "啟動時檢查更新"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "請升級專業版伺服器到{}或更高版本!"), + ("pull_group_failed_tip", "獲取群組訊息失敗"), + ("Filter by intersection", "按照交集篩選"), + ("Remove wallpaper during incoming sessions", "在接受連入連線時移除桌布"), + ("Test", "測試"), + ("display_is_plugged_out_msg", "螢幕已被拔除,切換到第一個螢幕。"), + ("No displays", "沒有已連結的螢幕"), + ("Open in new window", "在新視窗中開啟"), + ("Show displays as individual windows", "在各別的視窗開啟螢幕畫面"), + ("Use all my displays for the remote session", "使用所有的螢幕用於遠端連線"), + ("selinux_tip", "SELinux 處於啟用狀態,RustDesk 可能無法作為被控端正常運作。"), + ("Change view", "更改檢視方式"), + ("Big tiles", "大磁磚"), + ("Small tiles", "小磁磚"), + ("List", "清單"), + ("Virtual display", "虛擬螢幕"), + ("Plug out all", "拔出所有"), + ("True color (4:4:4)", "全彩模式(4:4:4)"), + ("Enable blocking user input", "允許封鎖使用者輸入"), + ("id_input_tip", "您可以輸入 ID、IP、或網域名稱+連接埠(<網域名稱>:<連接埠>)。\n如果您要存取位於其他伺服器上的裝置,請在 ID 之後新增伺服器位址(@<伺服器位址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的裝置,請輸入「@public」,不需輸入金鑰。\n\n如果您想要在第一次連線時,強制使用中繼連線,請在 ID 的結尾新增「/r」,例如,「9123456234/r」。"), + ("privacy_mode_impl_mag_tip", "模式 1"), + ("privacy_mode_impl_virtual_display_tip", "模式 2"), + ("Enter privacy mode", "進入隱私模式"), + ("Exit privacy mode", "退出隱私模式"), + ("idd_not_support_under_win10_2004_tip", "不支援 Indirect display driver。需要 Windows 10 版本 2004 以上版本。"), + ("input_source_1_tip", "輸入源 1"), + ("input_source_2_tip", "輸入源 2"), + ("Swap control-command key", "交換 Control 和 Command 按鍵"), + ("swap-left-right-mouse", "交換滑鼠左右鍵"), + ("2FA code", "二步驟驗證碼"), + ("More", "更多"), + ("enable-2fa-title", "啟用二步驟驗證"), + ("enable-2fa-desc", "現在請您設定您的二步驟驗證程式。您可以在手機或電腦使用 Authy、Microsoft 或 Google Authenticator 等驗證器程式。\n\n用它掃描QR Code或輸入下方金鑰至您的驗證器,然後輸入顯示的驗證碼以啟用二步驟驗證。"), + ("wrong-2fa-code", "無法驗證此驗證碼。請確認您的驗證碼和您的本機時間設定是正確的"), + ("enter-2fa-title", "二步驟驗證"), + ("Email verification code must be 6 characters.", "電子郵件驗證碼必須是 6 個字元。"), + ("2FA code must be 6 digits.", "二步驟驗證碼必須是 6 位數字。"), + ("Multiple Windows sessions found", "發現多個 Windows 工作階段"), + ("Please select the session you want to connect to", "請選擇您想要連結的工作階段"), + ("powered_by_me", "由 RustDesk 提供支援"), + ("outgoing_only_desk_tip", "目前版本的軟體是自訂版本。\n您可以連線至其他裝置,但是其他裝置無法連線至您的裝置。"), + ("preset_password_warning", "此客製化版本附有預設密碼。任何知曉此密碼的人都能完全控制您的裝置。如果這不是您所預期的,請立即移除此軟體。"), + ("Security Alert", "安全警告"), + ("My address book", "我的通訊錄"), + ("Personal", "個人的"), + ("Owner", "擁有者"), + ("Set shared password", "設定共用密碼"), + ("Exist in", "存在於"), + ("Read-only", "唯讀"), + ("Read/Write", "讀寫"), + ("Full Control", "完全控制"), + ("share_warning_tip", "上述的欄位為共用且對其他人可見。"), + ("Everyone", "所有人"), + ("ab_web_console_tip", "開啟 Web 控制台以進行更多操作"), + ("allow-only-conn-window-open-tip", "只在 RustDesk 視窗開啟時允許連接"), + ("no_need_privacy_mode_no_physical_displays_tip", "沒有物理螢幕,沒必要使用隱私模式。"), + ("Follow remote cursor", "跟隨遠端游標"), + ("Follow remote window focus", "跟隨遠端視窗焦點"), + ("default_proxy_tip", "預設代理協定及通訊埠為 Socks5 和 1080"), + ("no_audio_input_device_tip", "未找到音訊輸入裝置"), + ("Incoming", "連入"), + ("Outgoing", "連出"), + ("Clear Wayland screen selection", "清除 Wayland 的螢幕選擇"), + ("clear_Wayland_screen_selection_tip", "清除 Wayland 的螢幕選擇後,您可以重新選擇分享的螢幕。"), + ("confirm_clear_Wayland_screen_selection_tip", "是否確認清除 Wayland 的分享螢幕選擇?"), + ("android_new_voice_call_tip", "收到新的語音通話請求。如果您接受,音訊將切換為語音通訊。"), + ("texture_render_tip", "使用紋理繪製,讓圖片更加順暢。如果您遭遇繪製問題,可嘗試關閉此選項。"), + ("Use texture rendering", "使用紋理繪製"), + ("Floating window", "懸浮視窗"), + ("floating_window_tip", "有助於保持 RustDesk 後台服務"), + ("Keep screen on", "保持螢幕開啟"), + ("Never", "從不"), + ("During controlled", "被控期間"), + ("During service is on", "服務開啟期間"), + ("Capture screen using DirectX", "使用 DirectX 擷取螢幕"), + ("Back", "返回"), + ("Apps", "應用程式"), + ("Volume up", "提高音量"), + ("Volume down", "降低音量"), + ("Power", "電源"), + ("Telegram bot", "Telegram 機器人"), + ("enable-bot-tip", "如果您啟用此功能,您可以從您的機器人接收二步驟驗證碼,亦可作為連線通知之用。"), + ("enable-bot-desc", "1. 開啟與 @BotFather 的對話。\n2. 傳送指令「/newbot」。您將會在完成此步驟後收到權杖 (Token)。\n3. 開始與您剛創立的機器人的對話。傳送一則以正斜線 (「/」) 開頭的訊息來啟用它,例如「/hello」。"), + ("cancel-2fa-confirm-tip", "確定要取消二步驟驗證嗎?"), + ("cancel-bot-confirm-tip", "確定要取消 Telegram 機器人嗎?"), + ("About RustDesk", "關於 RustDesk"), + ("Send clipboard keystrokes", "傳送剪貼簿按鍵"), + ("network_error_tip", "請檢查網路連結,然後點選重試"), + ("Unlock with PIN", "使用 PIN 碼解鎖設定"), + ("Requires at least {} characters", "不少於 {} 個字元"), + ("Wrong PIN", "PIN 碼錯誤"), + ("Set PIN", "設定 PIN 碼"), + ("Enable trusted devices", "啟用信任裝置"), + ("Manage trusted devices", "管理信任裝置"), + ("Platform", "平台"), + ("Days remaining", "剩餘天數"), + ("enable-trusted-devices-tip", "允許受信任的裝置跳過 2FA 驗證"), + ("Parent directory", "父目錄"), + ("Resume", "繼續"), + ("Invalid file name", "無效檔名"), + ("one-way-file-transfer-tip", "被控端啟用了單向檔案傳輸"), + ("Authentication Required", "需要驗證"), + ("Authenticate", "認證"), + ("web_id_input_tip", "您可以輸入同一個伺服器內的 ID,Web 客戶端不支援直接 IP 存取。\n如果您要存取位於其他伺服器上的裝置,請在 ID 之後新增伺服器位址(@<伺服器位址>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\n要存取公共伺服器上的裝置,請輸入「@public」,不需輸入金鑰。"), + ("Download", "下載"), + ("Upload folder", "上傳資料夾"), + ("Upload files", "上傳檔案"), + ("Clipboard is synchronized", "剪貼簿已同步"), + ("Update client clipboard", "更新客戶端的剪貼簿"), + ("Untagged", "無標籤"), + ("new-version-of-{}-tip", "有新版本的 {} 可用"), + ("Accessible devices", "可存取的裝置"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "請將遠端 RustDesk 客戶端升級到 {} 或更新版本!"), + ("d3d_render_tip", "當啟用 D3D 渲染時,某些機器可能會無法顯示遠端畫面。"), + ("Use D3D rendering", "使用 D3D 渲染"), + ("Printer", "印表機"), + ("printer-os-requirement-tip", "印表機的傳出功能需要 Windows 10 或更高版本。"), + ("printer-requires-installed-{}-client-tip", "為了使用遠端列印功能,請安裝 {} 到此設備。"), + ("printer-{}-not-installed-tip", "{} 印表機未安裝。"), + ("printer-{}-ready-tip", "{} 印表機已安裝,您可以使用列印功能了。"), + ("Install {} Printer", "安裝 {} 印表機"), + ("Outgoing Print Jobs", "傳出的列印任務"), + ("Incoming Print Jobs", "傳入的列印任務"), + ("Incoming Print Job", "傳入的列印任務"), + ("use-the-default-printer-tip", "使用預設的印表機"), + ("use-the-selected-printer-tip", "使用選取的印表機"), + ("auto-print-tip", "使用選取的印表機自動執行"), + ("print-incoming-job-confirm-tip", "您收到一個遠端列印任務,您想在本地執行它嗎?"), + ("remote-printing-disallowed-tile-tip", "不允許遠端列印"), + ("remote-printing-disallowed-text-tip", "被控端的權限設置拒絕了遠端列印。"), + ("save-settings-tip", "儲存設定"), + ("dont-show-again-tip", "不再顯示此訊息"), + ("Take screenshot", "擷取畫面"), + ("Taking screenshot", "正在擷取畫面"), + ("screenshot-merged-screen-not-supported-tip", "目前不支援合併多個螢幕的截圖。請切換至單一螢幕後再試。"), + ("screenshot-action-tip", "請選擇要如何處理這張截圖。"), + ("Save as", "另存為"), + ("Copy to clipboard", "複製到剪貼簿"), + ("Enable remote printer", "啟用遠端列印"), + ("Downloading {}", "正在下載 {} 並安裝新版本。"), + ("{} Update", "{} 更新"), + ("{}-to-update-tip", "即將關閉 {} 並安裝新版本。"), + ("download-new-version-failed-tip", "下載失敗,您可以重試或點擊\"下載\"按鈕以從發布網址下載,並手動升級。"), + ("Auto update", "自動更新"), + ("update-failed-check-msi-tip", "安裝方式偵測失敗,請點擊\"下載\"按鈕以從發布網址下載,並手動升級。"), + ("websocket_tip", "使用 WebSocket 時,只支援使用中繼連接。"), + ("Use WebSocket", "使用 WebSocket"), + ("Trackpad speed", "觸控板速度"), + ("Default trackpad speed", "預設觸控板速度"), + ("Numeric one-time password", "數字一次性密碼"), + ("Enable IPv6 P2P connection", "啟用 IPv6 P2P 連線"), + ("Enable UDP hole punching", "啟用 UDP 打洞"), + ("View camera", "檢視相機"), + ("Enable camera", "允許查看鏡頭"), + ("No cameras", "沒有鏡頭"), + ("view_camera_unsupported_tip", "您的遠端設備不支援查看鏡頭"), + ("Terminal", "終端機"), + ("Enable terminal", "啟用終端機"), + ("New tab", "新分頁"), + ("Keep terminal sessions on disconnect", "在斷線時保持終端機的工作階段"), + ("Terminal (Run as administrator)", "終端機(使用系統管理員執行)"), + ("terminal-admin-login-tip", "請輸入被控端系統管理員的使用者名稱與密碼"), + ("Failed to get user token.", "取得使用者權杖失敗"), + ("Incorrect username or password.", "使用者名稱或密碼不正確"), + ("The user is not an administrator.", "使用者並不是系統管理員"), + ("Failed to check if the user is an administrator.", "檢查使用者是否是系統管理員時失敗了"), + ("Supported only in the installed version.", "僅支援於已安裝的版本"), + ("elevation_username_tip", "輸入使用者名稱或網域\\使用者名稱"), + ("Preparing for installation ...", "正在準備安裝..."), + ("Show my cursor", "顯示我的游標"), + ("Scale custom", "自訂縮放"), + ("Custom scale slider", "自訂縮放滑桿"), + ("Decrease", "縮小"), + ("Increase", "放大"), + ("Show virtual mouse", "顯示虛擬滑鼠"), + ("Virtual mouse size", "虛擬滑鼠大小"), + ("Small", "小"), + ("Large", "大"), + ("Show virtual joystick", "顯示虛擬搖桿"), + ("Edit note", "編輯備註"), + ("Alias", "別名"), + ("ScrollEdge", "邊緣滾動"), + ("Allow insecure TLS fallback", "允許降級到不安全的 TLS 連接"), + ("allow-insecure-tls-fallback-tip", "預設情況下,對於使用 TLS 的協定,RustDesk 會驗證伺服器的憑證。\n啟用此選項後,在驗證失敗時,RustDesk 將轉為跳過驗證步驟並繼續連接。"), + ("Disable UDP", "停用 UDP"), + ("disable-udp-tip", "控制是否僅使用 TCP。\n啟用此選項後,RustDesk 將不再使用 UDP 21116,而是使用 TCP 21116。"), + ("server-oss-not-support-tip", "注意:RustDesk 開源伺服器 (OSS server) 不包含此功能。"), + ("input note here", "輸入備註"), + ("note-at-conn-end-tip", "在連接結束時請求備註"), + ("Show terminal extra keys", "顯示終端機額外按鍵"), + ("Relative mouse mode", "相對滑鼠模式"), + ("rel-mouse-not-supported-peer-tip", "被控端不支援相對滑鼠模式"), + ("rel-mouse-not-ready-tip", "相對滑鼠模式尚未就緒,請稍候再試"), + ("rel-mouse-lock-failed-tip", "無法鎖定游標,相對滑鼠模式已停用"), + ("rel-mouse-exit-{}-tip", "按下 {} 退出"), + ("rel-mouse-permission-lost-tip", "鍵盤權限被撤銷,相對滑鼠模式已被停用"), + ("Changelog", "更新日誌"), + ("keep-awake-during-outgoing-sessions-label", "在連出工作階段期間保持螢幕喚醒"), + ("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"), + ("Continue with {}", "使用 {} 登入"), + ("Display Name", "顯示名稱"), + ("password-hidden-tip", "固定密碼已設定(已隱藏)"), + ("preset-password-in-use-tip", "目前正在使用預設密碼"), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/uk.rs b/vendor/rustdesk/src/lang/uk.rs new file mode 100644 index 0000000..3e1c4f2 --- /dev/null +++ b/vendor/rustdesk/src/lang/uk.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Статус"), + ("Your Desktop", "Ваша стільниця"), + ("desk_tip", "Доступ до вашої стільниці можливий з цим ID та паролем."), + ("Password", "Пароль"), + ("Ready", "Готово"), + ("Established", "Встановлено"), + ("connecting_status", "Підключення до мережі RustDesk..."), + ("Enable service", "Увімкнути службу"), + ("Start service", "Запустити службу"), + ("Service is running", "Служба працює"), + ("Service is not running", "Служба не запущена"), + ("not_ready_status", "Не готово. Будь ласка, перевірте ваше підключення"), + ("Control Remote Desktop", "Керування віддаленою стільницею"), + ("Transfer file", "Надіслати файл"), + ("Connect", "Підключитися"), + ("Recent sessions", "Нещодавні сеанси"), + ("Address book", "Адресна книга"), + ("Confirmation", "Підтвердження"), + ("TCP tunneling", "TCP-тунелювання"), + ("Remove", "Видалити"), + ("Refresh random password", "Оновити випадковий пароль"), + ("Set your own password", "Встановити свій пароль"), + ("Enable keyboard/mouse", "Увімкнути клавіатуру/мишу"), + ("Enable clipboard", "Увімкнути буфер обміну"), + ("Enable file transfer", "Увімкнути передачу файлів"), + ("Enable TCP tunneling", "Увімкнути тунелювання TCP"), + ("IP Whitelisting", "Список дозволених IP-адрес"), + ("ID/Relay Server", "ID/Сервер ретрансляції"), + ("Import server config", "Імпортувати конфігурацію сервера"), + ("Export Server Config", "Експортувати конфігурацію сервера"), + ("Import server configuration successfully", "Конфігурацію сервера успішно імпортовано"), + ("Export server configuration successfully", "Конфігурацію сервера успішно експортовано"), + ("Invalid server configuration", "Неправильна конфігурація сервера"), + ("Clipboard is empty", "Буфер обміну порожній"), + ("Stop service", "Зупинити службу"), + ("Change ID", "Змінити ID"), + ("Your new ID", "Ваш новий ID"), + ("length %min% to %max%", "від %min% до %max% символів"), + ("starts with a letter", "починається з літери"), + ("allowed characters", "дозволені символи"), + ("id_change_tip", "Допускаються лише символи a-z, A-Z, 0-9, - (dash) і _ (підкреслення). Першою повинна бути літера a-z, A-Z. Довжина — від 6 до 16 символів"), + ("Website", "Веб-сайт"), + ("About", "Про застосунок"), + ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), + ("Privacy Statement", "Декларація про конфіденційність"), + ("Mute", "Вимкнути звук"), + ("Build Date", "Дата збірки"), + ("Version", "Версія"), + ("Home", "Домівка"), + ("Audio Input", "Аудіовхід"), + ("Enhancements", "Покращення"), + ("Hardware Codec", "Апаратний кодек"), + ("Adaptive bitrate", "Адаптивна швидкість потоку"), + ("ID Server", "ID-сервер"), + ("Relay Server", "Сервер ретрансляції"), + ("API Server", "API-сервер"), + ("invalid_http", "Повинна починатися з http:// або https://"), + ("Invalid IP", "Неправильна IP-адреса"), + ("Invalid format", "Неправильний формат"), + ("server_not_support", "Наразі не підтримується сервером"), + ("Not available", "Недоступно"), + ("Too frequent", "Занадто часто"), + ("Cancel", "Скасувати"), + ("Skip", "Пропустити"), + ("Close", "Закрити"), + ("Retry", "Повторити"), + ("OK", "OK"), + ("Password Required", "Потрібен пароль"), + ("Please enter your password", "Будь ласка, введіть ваш пароль"), + ("Remember password", "Запамʼятати пароль"), + ("Wrong Password", "Неправильний пароль"), + ("Do you want to enter again?", "Бажаєте увійти знову?"), + ("Connection Error", "Помилка підключення"), + ("Error", "Помилка"), + ("Reset by the peer", "Віддалений пристрій скинув підключення"), + ("Connecting...", "Підключення..."), + ("Connection in progress. Please wait.", "Виконується підключення. Будь ласка, зачекайте."), + ("Please try 1 minute later", "Спробуйте через 1 хвилину"), + ("Login Error", "Помилка входу"), + ("Successful", "Операція успішна"), + ("Connected, waiting for image...", "Підключено, очікування зображення..."), + ("Name", "Імʼя"), + ("Type", "Тип"), + ("Modified", "Змінено"), + ("Size", "Розмір"), + ("Show Hidden Files", "Показати приховані файли"), + ("Receive", "Отримати"), + ("Send", "Надіслати"), + ("Refresh File", "Оновити файл"), + ("Local", "Локальний"), + ("Remote", "Віддалений"), + ("Remote Computer", "Віддалений компʼютер"), + ("Local Computer", "Локальний компʼютер"), + ("Confirm Delete", "Підтвердити видалення"), + ("Delete", "Видалити"), + ("Properties", "Властивості"), + ("Multi Select", "Багатоелементний вибір"), + ("Select All", "Вибрати все"), + ("Unselect All", "Скасувати вибір"), + ("Empty Directory", "Порожня тека"), + ("Not an empty directory", "Тека не порожня"), + ("Are you sure you want to delete this file?", "Ви впевнені, що хочете видалити цей файл?"), + ("Are you sure you want to delete this empty directory?", "Ви впевнені, що хочете видалити порожню теку?"), + ("Are you sure you want to delete the file of this directory?", "Ви впевнені, що хочете видалити файл із цієї теки?"), + ("Do this for all conflicts", "Це стосується всіх конфліктів"), + ("This is irreversible!", "Це незворотна дія!"), + ("Deleting", "Видалення"), + ("files", "файли"), + ("Waiting", "Очікування"), + ("Finished", "Завершено"), + ("Speed", "Швидкість"), + ("Custom Image Quality", "Користувацька якість зображення"), + ("Privacy mode", "Режим конфіденційності"), + ("Block user input", "Блокувати користувацьке введення"), + ("Unblock user input", "Розблокувати користувацьке введення"), + ("Adjust Window", "Налаштувати вікно"), + ("Original", "Оригінал"), + ("Shrink", "Зменшити"), + ("Stretch", "Розтягнути"), + ("Scrollbar", "Смужка гортання"), + ("ScrollAuto", "Автоматичне гортання"), + ("Good image quality", "Гарна якість зображення"), + ("Balanced", "Збалансована"), + ("Optimize reaction time", "Оптимізувати час реакції"), + ("Custom", "Користувацька"), + ("Show remote cursor", "Показати віддалений вказівник"), + ("Show quality monitor", "Показати якість"), + ("Disable clipboard", "Вимкнути буфер обміну"), + ("Lock after session end", "Блокування після завершення сеансу"), + ("Insert Ctrl + Alt + Del", "Вставити Ctrl + Alt + Del"), + ("Insert Lock", "Встановити замок"), + ("Refresh", "Оновити"), + ("ID does not exist", "ID не існує"), + ("Failed to connect to rendezvous server", "Не вдалося підключитися до сервера рандеву"), + ("Please try later", "Будь ласка, спробуйте пізніше"), + ("Remote desktop is offline", "Віддалена стільниця не в мережі"), + ("Key mismatch", "Невідповідність ключів"), + ("Timeout", "Час очікування"), + ("Failed to connect to relay server", "Не вдалося підключитися до сервера ретрансляції"), + ("Failed to connect via rendezvous server", "Не вдалося підключитися через сервер рандеву"), + ("Failed to connect via relay server", "Не вдалося підключитися через сервер ретрансляції"), + ("Failed to make direct connection to remote desktop", "Не вдалося встановити пряме підключення до віддаленої стільниці"), + ("Set Password", "Встановити пароль"), + ("OS Password", "Пароль ОС"), + ("install_tip", "Через UAC, в деяких випадках RustDesk може працювати некоректно на віддаленому вузлі. Щоб уникнути UAC, натисніть кнопку нижче для встановлення RustDesk в системі"), + ("Click to upgrade", "Натисніть, щоб перевірити наявність оновлень"), + ("Configure", "Налаштувати"), + ("config_acc", "Для віддаленого керування вашою стільницею, вам необхідно надати RustDesk дозволи \"Спеціальні можливості\""), + ("config_screen", "Для віддаленого доступу до вашої стільниці, вам необхідно надати RustDesk дозволи на \"Запис екрана\""), + ("Installing ...", "Встановлюється..."), + ("Install", "Встановити"), + ("Installation", "Встановлення"), + ("Installation Path", "Шлях встановлення"), + ("Create start menu shortcuts", "Створити ярлики меню \"Пуск\""), + ("Create desktop icon", "Створити піктограму на стільниці"), + ("agreement_tip", "Починаючи встановлення, ви приймаєте умови ліцензійної угоди"), + ("Accept and Install", "Прийняти та встановити"), + ("End-user license agreement", "Ліцензійна угода з кінцевим користувачем"), + ("Generating ...", "Генерування..."), + ("Your installation is lower version.", "У вас встановлена більш рання версія"), + ("not_close_tcp_tip", "Не закривайте це вікно під час використання тунелю"), + ("Listening ...", "Очікуємо ..."), + ("Remote Host", "Віддалена машина"), + ("Remote Port", "Віддалений порт"), + ("Action", "Дія"), + ("Add", "Додати"), + ("Local Port", "Локальний порт"), + ("Local Address", "Локальна адреса"), + ("Change Local Port", "Змінити локальний порт"), + ("setup_server_tip", "Для пришвидшення зʼєднання, будь ласка, налаштуйте власний сервер"), + ("Too short, at least 6 characters.", "Має бути щонайменше 6 символів"), + ("The confirmation is not identical.", "Підтвердження не збігається"), + ("Permissions", "Дозволи"), + ("Accept", "Прийняти"), + ("Dismiss", "Відхилити"), + ("Disconnect", "Відʼєднати"), + ("Enable file copy and paste", "Дозволити копіювання та вставку файлів"), + ("Connected", "Підключено"), + ("Direct and encrypted connection", "Пряме та зашифроване підключення"), + ("Relayed and encrypted connection", "Ретрансльоване та зашифроване підключення"), + ("Direct and unencrypted connection", "Пряме та незашифроване підключення"), + ("Relayed and unencrypted connection", "Ретрансльоване та незашифроване підключення"), + ("Enter Remote ID", "Введіть віддалений ID"), + ("Enter your password", "Введіть пароль"), + ("Logging in...", "Вхід..."), + ("Enable RDP session sharing", "Увімкнути загальний доступ до сеансу RDP"), + ("Auto Login", "Автоматичний вхід (дійсний лише якщо ви встановили \"Блокування після завершення сеансу\")"), + ("Enable direct IP access", "Увімкнути прямий IP-доступ"), + ("Rename", "Перейменувати"), + ("Space", "Місце"), + ("Create desktop shortcut", "Створити ярлик на стільниці"), + ("Change Path", "Змінити шлях"), + ("Create Folder", "Створити теку"), + ("Please enter the folder name", "Будь ласка, введіть назву для теки"), + ("Fix it", "Виправити"), + ("Warning", "Попередження"), + ("Login screen using Wayland is not supported", "Екран входу, який використовує Wayland, не підтримується"), + ("Reboot required", "Потрібне перезавантаження"), + ("Unsupported display server", "Графічний сервер не підтримується"), + ("x11 expected", "Потрібен X11"), + ("Port", "Порт"), + ("Settings", "Налаштування"), + ("Username", "Імʼя користувача"), + ("Invalid port", "Неправильний порт"), + ("Closed manually by the peer", "Завершено вручну з боку віддаленого пристрою"), + ("Enable remote configuration modification", "Дозволити віддалену зміну конфігурації"), + ("Run without install", "Запустити без встановлення"), + ("Connect via relay", "Підключитися через ретранслятор"), + ("Always connect via relay", "Завжди підключатися через ретранслятор"), + ("whitelist_tip", "Лише IP-адреси з білого списку можуть отримати доступ до мене"), + ("Login", "Увійти"), + ("Verify", "Підтвердити"), + ("Remember me", "Запамʼятати мене"), + ("Trust this device", "Довірений пристрій"), + ("Verification code", "Код підтвердження"), + ("verification_tip", "Код підтвердження надіслано на зареєстровану email-адресу, введіть код підтвердження для продовження авторизації."), + ("Logout", "Вийти"), + ("Tags", "Мітки"), + ("Search ID", "Пошук за ID"), + ("whitelist_sep", "Відокремлення комою, крапкою з комою, пропуском або новим рядком"), + ("Add ID", "Додати ID"), + ("Add Tag", "Додати мітку"), + ("Unselect all tags", "Скасувати вибір усіх міток"), + ("Network error", "Помилка мережі"), + ("Username missed", "Імʼя користувача відсутнє"), + ("Password missed", "Пароль відсутній"), + ("Wrong credentials", "Неправильні дані"), + ("The verification code is incorrect or has expired", "Код підтвердження некоректний або протермінований"), + ("Edit Tag", "Редагувати мітку"), + ("Forget Password", "Не зберігати пароль"), + ("Favorites", "Вибране"), + ("Add to Favorites", "Додати до обраного"), + ("Remove from Favorites", "Видалити з обраного"), + ("Empty", "Пусто"), + ("Invalid folder name", "Неприпустима назва теки"), + ("Socks5 Proxy", "Проксі-сервер Socks5"), + ("Socks5/Http(s) Proxy", "Проксі-сервер Socks5/Http(s)"), + ("Discovered", "Знайдено"), + ("install_daemon_tip", "Для запуску під час завантаження, вам необхідно встановити системну службу"), + ("Remote ID", "Віддалений ідентифікатор"), + ("Paste", "Вставити"), + ("Paste here?", "Вставити сюди?"), + ("Are you sure to close the connection?", "Ви впевнені, що хочете завершити підключення?"), + ("Download new version", "Завантажити нову версію"), + ("Touch mode", "Сенсорний режим"), + ("Mouse mode", "Режим миші"), + ("One-Finger Tap", "Дотик одним пальцем"), + ("Left Mouse", "Ліва кнопка миші"), + ("One-Long Tap", "Одне довге натискання пальцем"), + ("Two-Finger Tap", "Дотик двома пальцями"), + ("Right Mouse", "Права кнопка миші"), + ("One-Finger Move", "Рух одним пальцем"), + ("Double Tap & Move", "Подвійне натискання та переміщення"), + ("Mouse Drag", "Перетягування мишею"), + ("Three-Finger vertically", "Трьома пальцями по вертикалі"), + ("Mouse Wheel", "Коліщатко миші"), + ("Two-Finger Move", "Рух двома пальцями"), + ("Canvas Move", "Переміщення полотна"), + ("Pinch to Zoom", "Стисніть, щоб збільшити"), + ("Canvas Zoom", "Масштаб полотна"), + ("Reset canvas", "Відновлення полотна"), + ("No permission of file transfer", "Немає дозволу на передачу файлів"), + ("Note", "Примітка"), + ("Connection", "Підключення"), + ("Share screen", "Поділитися екраном"), + ("Chat", "Чат"), + ("Total", "Всього"), + ("items", "елементи"), + ("Selected", "Обрано"), + ("Screen Capture", "Захоплення екрана"), + ("Input Control", "Керування введенням"), + ("Audio Capture", "Захоплення аудіо"), + ("Do you accept?", "Ви згодні?"), + ("Open System Setting", "Відкрити налаштування системи"), + ("How to get Android input permission?", "Як отримати дозвіл на введення в Android?"), + ("android_input_permission_tip1", "Для того, щоб віддалений пристрій міг керувати вашим Android-пристроєм за допомогою миші або дотику, вам необхідно дозволити RustDesk використовувати службу \"Спеціальні можливості\"."), + ("android_input_permission_tip2", "Будь ласка, перейдіть на наступну сторінку системних налаштувань, знайдіть та увійдіть у [Встановлені служби], увімкніть службу [RustDesk Input]."), + ("android_new_connection_tip", "Отримано новий запит на керування вашим поточним пристроєм."), + ("android_service_will_start_tip", "Увімкнення \"Захоплення екрана\" автоматично запускає службу, дозволяючи іншим пристроям запитувати підключення до вашого пристрою."), + ("android_stop_service_tip", "Зупинка служби автоматично завершить всі встановлені зʼєднання."), + ("android_version_audio_tip", "Поточна версія Android не підтримує захоплення звуку, будь ласка, оновіться до Android 10 або вище."), + ("android_start_service_tip", "Натисніть [Запустити службу] або увімкніть дозвіл на [Захоплення екрана], щоб запустити службу спільного доступу до екрана."), + ("android_permission_may_not_change_tip", "Дозволи для встановлених зʼєднань можуть не застосуватися аж до перепідключення."), + ("Account", "Обліковий запис"), + ("Overwrite", "Перезаписати"), + ("This file exists, skip or overwrite this file?", "Цей файл існує, пропустити чи перезаписати файл?"), + ("Quit", "Вийти"), + ("Help", "Допомога"), + ("Failed", "Не вдалося"), + ("Succeeded", "Успішно"), + ("Someone turns on privacy mode, exit", "Хтось вмикає режим конфіденційності, вихід"), + ("Unsupported", "Не підтримується"), + ("Peer denied", "Відхилено віддаленим пристроєм"), + ("Please install plugins", "Будь ласка, встановіть плагіни"), + ("Peer exit", "Вийти з віддаленого пристрою"), + ("Failed to turn off", "Не вдалося вимкнути"), + ("Turned off", "Вимкнений"), + ("Language", "Мова"), + ("Keep RustDesk background service", "Зберегти фонову службу RustDesk"), + ("Ignore Battery Optimizations", "Ігнорувати оптимізації батареї"), + ("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"), + ("Start on boot", "Автозапуск"), + ("Start the screen sharing service on boot, requires special permissions", "Запускати службу спільного доступу до екрана під час завантаження, потребує спеціальних дозволів"), + ("Connection not allowed", "Підключення не дозволено"), + ("Legacy mode", "Застарілий режим"), + ("Map mode", "Режим карти"), + ("Translate mode", "Режим перекладу"), + ("Use permanent password", "Використовувати постійний пароль"), + ("Use both passwords", "Використовувати обидва паролі"), + ("Set permanent password", "Встановити постійний пароль"), + ("Enable remote restart", "Увімкнути віддалений перезапуск"), + ("Restart remote device", "Перезапустити віддалений пристрій"), + ("Are you sure you want to restart", "Ви впевнені, що хочете виконати перезапуск?"), + ("Restarting remote device", "Перезапуск віддаленого пристрою"), + ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення та через деякий час перепідключіться, використовуючи постійний пароль."), + ("Copied", "Скопійовано"), + ("Exit Fullscreen", "Вийти з повноекранного режиму"), + ("Fullscreen", "Повноекранний"), + ("Mobile Actions", "Мобільні дії"), + ("Select Monitor", "Виберіть монітор"), + ("Control Actions", "Дії для керування"), + ("Display Settings", "Налаштування дисплею"), + ("Ratio", "Співвідношення"), + ("Image Quality", "Якість зображення"), + ("Scroll Style", "Стиль гортання"), + ("Show Toolbar", "Показати панель інструментів"), + ("Hide Toolbar", "Приховати панель інструментів"), + ("Direct Connection", "Пряме підключення"), + ("Relay Connection", "Ретрансльоване підключення"), + ("Secure Connection", "Безпечне підключення"), + ("Insecure Connection", "Небезпечне підключення"), + ("Scale original", "Оригінальний масштаб"), + ("Scale adaptive", "Адаптивний масштаб"), + ("General", "Загальні"), + ("Security", "Безпека"), + ("Theme", "Тема"), + ("Dark Theme", "Темна тема"), + ("Light Theme", "Світла тема"), + ("Dark", "Темна"), + ("Light", "Світла"), + ("Follow System", "Як в системі"), + ("Enable hardware codec", "Увімкнути апаратний кодек"), + ("Unlock Security Settings", "Розблокувати налаштування безпеки"), + ("Enable audio", "Увімкнути аудіо"), + ("Unlock Network Settings", "Розблокувати мережеві налаштування"), + ("Server", "Сервер"), + ("Direct IP Access", "Прямий IP-доступ"), + ("Proxy", "Проксі"), + ("Apply", "Застосувати"), + ("Disconnect all devices?", "Відʼєднати всі прилади?"), + ("Clear", "Очистити"), + ("Audio Input Device", "Пристрій введення звуку"), + ("Use IP Whitelisting", "Використовувати білий список IP"), + ("Network", "Мережа"), + ("Pin Toolbar", "Закріпити панель інструментів"), + ("Unpin Toolbar", "Відкріпити панель інструментів"), + ("Recording", "Запис"), + ("Directory", "Директорія"), + ("Automatically record incoming sessions", "Автоматично записувати вхідні сеанси"), + ("Automatically record outgoing sessions", "Автоматично записувати вихідні сеанси"), + ("Change", "Змінити"), + ("Start session recording", "Розпочати запис сеансу"), + ("Stop session recording", "Закінчити запис сеансу"), + ("Enable recording session", "Увімкнути запис сеансу"), + ("Enable LAN discovery", "Увімкнути пошук локальної мережі"), + ("Deny LAN discovery", "Заборонити виявлення локальної мережі"), + ("Write a message", "Написати повідомлення"), + ("Prompt", "Підказка"), + ("Please wait for confirmation of UAC...", "Будь ласка, зачекайте підтвердження UAC..."), + ("elevated_foreground_window_tip", "Поточне вікно віддаленої стільниці потребує розширених прав для роботи, тому наразі неможливо використовувати мишу та клавіатуру. Ви можете запропонувати віддаленому користувачеві згорнути поточне вікно чи натиснути кнопку розширення прав у вікні керування підключеннями. Для уникнення цієї проблеми, рекомендується встановити програму на віддаленому пристрої."), + ("Disconnected", "Відʼєднано"), + ("Other", "Інше"), + ("Confirm before closing multiple tabs", "Підтверджувати перед закриттям кількох вкладок"), + ("Keyboard Settings", "Налаштування клавіатури"), + ("Full Access", "Повний доступ"), + ("Screen Share", "Демонстрація екрана"), + ("ubuntu-21-04-required", "Wayland потребує Ubuntu 21.04 або новішої версії."), + ("wayland-requires-higher-linux-version", "Для Wayland потрібна новіша версія дистрибутива Linux. Будь ласка, спробуйте стільницю на X11 або змініть свою ОС."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Перегляд"), + ("Please Select the screen to be shared(Operate on the peer side).", "Будь ласка, виберіть екран, до якого потрібно надати доступ (на віддаленому пристрої)."), + ("Show RustDesk", "Показати RustDesk"), + ("This PC", "Цей ПК"), + ("or", "чи"), + ("Elevate", "Розширення прав"), + ("Zoom cursor", "Збільшити вказівник"), + ("Accept sessions via password", "Підтверджувати сеанси паролем"), + ("Accept sessions via click", "Підтверджувати сеанси натисканням"), + ("Accept sessions via both", "Підтверджувати сеанси обома способами"), + ("Please wait for the remote side to accept your session request...", "Буль ласка, зачекайте, поки віддалена сторона підтвердить запит на сеанс..."), + ("One-time Password", "Одноразовий пароль"), + ("Use one-time password", "Використати одноразовий пароль"), + ("One-time password length", "Довжина одноразового пароля"), + ("Request access to your device", "Дати запит щодо доступ до свого пристрою"), + ("Hide connection management window", "Приховати вікно керування підключеннями"), + ("hide_cm_tip", "Дозволено приховати лише якщо сеанс підтверджується постійним паролем"), + ("wayland_experiment_tip", "Підтримка Wayland на експериментальній стадії, будь ласка, використовуйте X11, якщо необхідний автоматичний доступ."), + ("Right click to select tabs", "Вибір вкладок клацанням правою"), + ("Skipped", "Пропущено"), + ("Add to address book", "Додати IP до Адресної книги"), + ("Group", "Група"), + ("Search", "Пошук"), + ("Closed manually by web console", "Закрито вручну з веб-консолі"), + ("Local keyboard type", "Тип локальної клавіатури"), + ("Select local keyboard type", "Оберіть тип локальної клавіатури"), + ("software_render_tip", "Якщо ви використовуєте відеокарту Nvidia на Linux, і віддалене вікно закривається відразу після підключення, то перехід на вільний драйвер Nouveau та увімкнення програмної візуалізації може допомогти. Для застосування змін необхідно перезапустити програму."), + ("Always use software rendering", "Завжди використовувати програмну візуалізацію"), + ("config_input", "Для віддаленого керування віддаленою стільницею з клавіатури, вам необхідно надати RustDesk дозволи на \"Відстеження введення\""), + ("config_microphone", "Для можливості віддаленої розмови, вам необхідно надати RustDesk дозвіл на \"Запис аудіо\""), + ("request_elevation_tip", "Ви можете також надіслати запит на розширення прав, в разі присутності особи з віддаленого боку."), + ("Wait", "Зачекайте"), + ("Elevation Error", "Невдала спроба розширення прав"), + ("Ask the remote user for authentication", "Попросіть віддаленого користувача пройти автентифікацію"), + ("Choose this if the remote account is administrator", "Виберіть це, якщо віддалений обліковий запис є адміністративним"), + ("Transmit the username and password of administrator", "Передайте імʼя користувача та пароль адміністратора"), + ("still_click_uac_tip", "Досі необхідне підтвердження UAC з боку віддаленого користувача"), + ("Request Elevation", "Запит на розширення прав"), + ("wait_accept_uac_tip", "Будь ласка, очікуйте підтвердження діалогу UAC з боку віддаленого користувача."), + ("Elevate successfully", "Успішне розширення прав"), + ("uppercase", "верхній регістр"), + ("lowercase", "нижній регістр"), + ("digit", "цифра"), + ("special character", "спецсимвол"), + ("length>=8", "довжина>=8"), + ("Weak", "Слабкий"), + ("Medium", "Середній"), + ("Strong", "Сильний"), + ("Switch Sides", "Поміняти місцями"), + ("Please confirm if you want to share your desktop?", "Будь ласка, підтвердіть дозвіл на спільне використання стільниці"), + ("Display", "Екран"), + ("Default View Style", "Типовий стиль перегляду"), + ("Default Scroll Style", "Типовий стиль гортання"), + ("Default Image Quality", "Типова якість зображення"), + ("Default Codec", "Типовий кодек"), + ("Bitrate", "Бітрейт"), + ("FPS", "FPS"), + ("Auto", "Авто"), + ("Other Default Options", "Інші типові параметри"), + ("Voice call", "Голосовий виклик"), + ("Text chat", "Текстовий чат"), + ("Stop voice call", "Завершити голосовий виклик"), + ("relay_hint_tip", "Якщо відсутня можливості підключитись напряму, ви можете спробувати підключення через ретранслятор. \nТакож, якщо ви хочете відразу використовувати ретранслятор, можна додати суфікс \"/r\" до ID, або ж вибрати опцію \"Завжди підключатися через ретранслятор\" в картці нещодавніх сеансів."), + ("Reconnect", "Перепідключитися"), + ("Codec", "Кодек"), + ("Resolution", "Роздільна здатність"), + ("No transfers in progress", "Наразі нічого не пересилається"), + ("Set one-time password length", "Вказати довжину одноразового пароля"), + ("RDP Settings", "Налаштування RDP"), + ("Sort by", "Сортувати за"), + ("New Connection", "Нове підключення"), + ("Restore", "Відновити"), + ("Minimize", "Згорнути"), + ("Maximize", "Розгорнути"), + ("Your Device", "Вам пристрій"), + ("empty_recent_tip", "Овва, відсутні нещодавні сеанси!\nСаме час запланувати нове підключення."), + ("empty_favorite_tip", "Досі немає улюблених вузлів?\nДавайте організуємо нове підключення та додамо його до улюблених!"), + ("empty_lan_tip", "О ні, схоже ми ще не виявили жодного віддаленого пристрою."), + ("empty_address_book_tip", "Ой лишенько, схоже у вашій адресній книзі немає жодного віддаленого пристрою."), + ("Empty Username", "Незаповнене імʼя"), + ("Empty Password", "Незаповнений пароль"), + ("Me", "Я"), + ("identical_file_tip", "Цей файл ідентичний з тим, що на вузлі"), + ("show_monitors_tip", "Показувати монітори на панелі інструментів"), + ("View Mode", "Режим перегляду"), + ("login_linux_tip", "Вам необхідно увійти у віддалений обліковий запис Linux, щоб увімкнути стільничний сеанс X"), + ("verify_rustdesk_password_tip", "Перевірте пароль RustDesk"), + ("remember_account_tip", "Запамʼятати цей обліковий запис"), + ("os_account_desk_tip", "Цей обліковий запис використовується для входу до віддаленої ОС та вмикання сеансу стільниці в режимі без графічного інтерфейсу"), + ("OS Account", "Користувач ОС"), + ("another_user_login_title_tip", "Інший користувач вже в системі"), + ("another_user_login_text_tip", "Відʼєднатися"), + ("xorg_not_found_title_tip", "Xorg не знайдено"), + ("xorg_not_found_text_tip", "Будь ласка, встановіть Xorg"), + ("no_desktop_title_tip", "Жодне стільничне середовище не доступне"), + ("no_desktop_text_tip", "Будь ласка, встановіть стільничне середовище GNOME"), + ("No need to elevate", "Немає потреби в розширенні прав"), + ("System Sound", "Системний звук"), + ("Default", "Типово"), + ("New RDP", "Нове RDP"), + ("Fingerprint", "Відбитки пальців"), + ("Copy Fingerprint", "Копіювати відбитки пальців"), + ("no fingerprints", "немає відбитків пальців"), + ("Select a peer", "Оберіть віддалений пристрій"), + ("Select peers", "Оберіть віддалені пристрої"), + ("Plugins", "Плагіни"), + ("Uninstall", "Видалити"), + ("Update", "Оновити"), + ("Enable", "Увімкнути"), + ("Disable", "Вимкнути"), + ("Options", "Опції"), + ("resolution_original_tip", "Початкова роздільна здатність"), + ("resolution_fit_local_tip", "Припасувати поточну роздільну здатність"), + ("resolution_custom_tip", "Користувацька роздільна здатність"), + ("Collapse toolbar", "Згорнути панель інструментів"), + ("Accept and Elevate", "Погодитись та розширити права"), + ("accept_and_elevate_btn_tooltip", "Погодити підключення та розширити дозволи UAC."), + ("clipboard_wait_response_timeout_tip", "Вийшов час очікування копіювання."), + ("Incoming connection", "Вхідне підключення"), + ("Outgoing connection", "Вихідне підключення"), + ("Exit", "Вийти"), + ("Open", "Відкрити"), + ("logout_tip", "Ви впевнені, що хочете вийти з системи?"), + ("Service", "Служба"), + ("Start", "Запустити"), + ("Stop", "Зупинити"), + ("exceed_max_devices", "У вас максимальна кількість керованих пристроїв."), + ("Sync with recent sessions", "Синхронізація з нещодавніми сеансами"), + ("Sort tags", "Сортувати мітки"), + ("Open connection in new tab", "Відкрити підключення в новій вкладці"), + ("Move tab to new window", "Перемістити вкладку до нового вікна"), + ("Can not be empty", "Не може бути порожнім"), + ("Already exists", "Вже існує"), + ("Change Password", "Змінити пароль"), + ("Refresh Password", "Оновити пароль"), + ("ID", "ID"), + ("Grid View", "Перегляд ґраткою"), + ("List View", "Перегляд списком"), + ("Select", "Вибрати"), + ("Toggle Tags", "Видимість міток"), + ("pull_ab_failed_tip", "Не вдалося оновити адресну книгу"), + ("push_ab_failed_tip", "Не вдалося синхронізувати адресну книгу"), + ("synced_peer_readded_tip", "Пристрої з нещодавніх сеансів будуть синхронізовані з адресною книгою."), + ("Change Color", "Змінити колір"), + ("Primary Color", "Основний колір"), + ("HSV Color", "Колір HSV"), + ("Installation Successful!", "Успішне встановлення!"), + ("Installation failed!", "Невдале встановлення!"), + ("Reverse mouse wheel", "Зворотній напрям гортання"), + ("{} sessions", "{} сеансів"), + ("scam_title", "Вас можуть ОБМАНУТИ!"), + ("scam_text1", "Якщо ви розмовляєте по телефону з кимось, кого НЕ ЗНАЄТЕ чи кому НЕ ДОВІРЯЄТЕ, і ця особа хоче, щоб ви використали RustDesk та запустили службу, не робіть цього та негайно завершіть дзвінок."), + ("scam_text2", "Ймовірно, ви маєте справу з шахраєм, що намагається викрасти ваші гроші чи особисті дані."), + ("Don't show again", "Не показувати знову"), + ("I Agree", "Я погоджуюсь"), + ("Decline", "Я не погоджуюсь"), + ("Timeout in minutes", "Час очікування в хвилинах"), + ("auto_disconnect_option_tip", "Автоматично завершувати вхідні сеанси в разі неактивності користувача"), + ("Connection failed due to inactivity", "Зʼєднання розірвано через неактивність"), + ("Check for software update on startup", "Перевіряти оновлення під час запуску"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Будь ласка, оновіть RustDesk Server Pro до версії {} чи новіше!"), + ("pull_group_failed_tip", "Не вдалося оновити групу"), + ("Filter by intersection", "Фільтр за збігом"), + ("Remove wallpaper during incoming sessions", "Прибирати шпалеру під час вхідних сеансів"), + ("Test", "Тест"), + ("display_is_plugged_out_msg", "Дисплей відключено, перемкніться на перший дисплей"), + ("No displays", "Відсутні дисплеї"), + ("Open in new window", "Відкрити в новому вікні"), + ("Show displays as individual windows", "Показувати дисплеї в окремих вікнах"), + ("Use all my displays for the remote session", "Використовувати всі мої дисплеї для віддаленого сеансу"), + ("selinux_tip", "SELinux увімкнено на вашому пристрої, що може ускладнити для іншої сторони віддалене керування за допомогою RustDesk."), + ("Change view", "Режим перегляду"), + ("Big tiles", "Великі плитки"), + ("Small tiles", "Маленькі плитки"), + ("List", "Список"), + ("Virtual display", "Віртуальний дисплей"), + ("Plug out all", "Відключити все"), + ("True color (4:4:4)", "Справжній колір (4:4:4)"), + ("Enable blocking user input", "Блокувати введення для користувача"), + ("id_input_tip", "Ви можете ввести ID, безпосередню IP, або ж домен з портом (<домен>:<порт>).\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>), наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\", для публічного сервера ключ не потрібен."), + ("privacy_mode_impl_mag_tip", "Режим 1"), + ("privacy_mode_impl_virtual_display_tip", "Режим 2"), + ("Enter privacy mode", "Увійти в режим конфіденційності"), + ("Exit privacy mode", "Вийти з режиму конфіденційності"), + ("idd_not_support_under_win10_2004_tip", "Драйвер непрямого відображення не підтримується. Потрібна Windows 10 версії 2004 або новіше."), + ("input_source_1_tip", "Джерело введення 1"), + ("input_source_2_tip", "Джерело введення 2"), + ("Swap control-command key", "Поміняти місцями клавіші Control та Command"), + ("swap-left-right-mouse", "Поміняти місцями ліву та праву кнопки миші"), + ("2FA code", "Код двофакторної автентифікації"), + ("More", "Більше"), + ("enable-2fa-title", "Увімкнути двофакторну автентифікацію"), + ("enable-2fa-desc", "Будь ласка, налаштуйте ваш автентифікатор зараз. Ви можете використати програму-автентифікатор, таку як Authy, Microsoft Authenticator або Google Authenticator на телефоні чи компʼютері.\n\nПроскануйте QR-код за допомогою програми та введіть код, який показує програма, щоб увімкнути двофакторну автентифікацію."), + ("wrong-2fa-code", "Не вдається підтвердити код. Перевірте код та налаштування місцевого часу"), + ("enter-2fa-title", "Двофакторна автентифікація"), + ("Email verification code must be 6 characters.", "Код підтвердження з email повинен складатися з 6 символів."), + ("2FA code must be 6 digits.", "Код двофакторної автентифікації повинен складатися з 6 символів."), + ("Multiple Windows sessions found", "Виявлено декілька сеансів Windows"), + ("Please select the session you want to connect to", "Будь ласка, оберіть сеанс, до якого ви хочете підключитися"), + ("powered_by_me", "На основі Rustdesk"), + ("outgoing_only_desk_tip", "Це персоналізована версія.\nВи можете підключатися до інших пристроїв, але інші пристрої не можуть підключатися до вашого."), + ("preset_password_warning", "Ця персоналізована версія містить попередньо встановлений пароль. Будь-хто з цим паролем може отримати повний доступ до вашого пристрою. Якщо це неочікувано для вас, негайно видаліть цю програму."), + ("Security Alert", "Попередження щодо безпеки"), + ("My address book", "Моя адресна книга"), + ("Personal", "Особиста"), + ("Owner", "Власник"), + ("Set shared password", "Встановити спільний пароль"), + ("Exist in", "Існує у"), + ("Read-only", "Лише читання"), + ("Read/Write", "Читання/запис"), + ("Full Control", "Повний доступ"), + ("share_warning_tip", "Поля вище є спільними та видимі для інших."), + ("Everyone", "Всі"), + ("ab_web_console_tip", "Детальніше про веб-консоль"), + ("allow-only-conn-window-open-tip", "Дозволяти підключення лише коли відкрите вікно RustDesk"), + ("no_need_privacy_mode_no_physical_displays_tip", "Відсутні фізичні дисплеї, немає потреби використовувати режим приватності."), + ("Follow remote cursor", "Прямувати за віддаленим курсором"), + ("Follow remote window focus", "Прямувати за фокусом у віддаленому вікні"), + ("default_proxy_tip", "Типовий протокол та порт — Socks5 та 1080"), + ("no_audio_input_device_tip", "Не знайдено жодного пристрою введення."), + ("Incoming", "Вхідні"), + ("Outgoing", "Вихідні"), + ("Clear Wayland screen selection", "Очистити вибір екрана Wayland"), + ("clear_Wayland_screen_selection_tip", "Після очищення вибору екрана ви можете повторно вибрати екран для поширення."), + ("confirm_clear_Wayland_screen_selection_tip", "Ви впевнені, що хочете очистити вибір екрана Wayland?"), + ("android_new_voice_call_tip", "Отримано новий запит на голосовий дзвінок. Якщо ви приймете його, аудіо перемкнеться на голосовий звʼязок."), + ("texture_render_tip", "Використовувати візуалізацію текстур для покращення плавності зображень."), + ("Use texture rendering", "Використовувати візуалізацію текстур"), + ("Floating window", "Рухоме вікно"), + ("floating_window_tip", "Допомагає зберегти фонову службу Rustdesk"), + ("Keep screen on", "Тримати екран увімкненим"), + ("Never", "Ніколи"), + ("During controlled", "Коли керується"), + ("During service is on", "Коли запущена служба"), + ("Capture screen using DirectX", "Захоплення екрана з використанням DirectX"), + ("Back", "Назад"), + ("Apps", "Застосунки"), + ("Volume up", "Збільшити гучність"), + ("Volume down", "Зменшити гучність"), + ("Power", "Живлення"), + ("Telegram bot", "Бот Telegram"), + ("enable-bot-tip", "Надає можливість отримувати код двофакторної автентифікації від вашого бота. Також може сповіщати про підключення"), + ("enable-bot-desc", "1. Відкрийте чат з @BotFather.\n2. Надішліть команду \"/newbot\". Ви отримаєте токен.\n3. Почніть чат з вашим щойно створеним ботом. Щоб активувати його, надішліть повідомлення, що починається зі скісної риски (\"/\"), наприклад \"/hello\".\n"), + ("cancel-2fa-confirm-tip", "Ви впевнені, що хочете скасувати код двофакторної автентифікації?"), + ("cancel-bot-confirm-tip", "Ви впевнені, що хочете скасувати Telegram бота?"), + ("About RustDesk", "Про Rustdesk"), + ("Send clipboard keystrokes", "Надіслати вміст буфера обміну"), + ("network_error_tip", "Будь ласка, перевірте ваше підключення до мережі та натисніть \"Повторити\""), + ("Unlock with PIN", "Розблокування PIN-кодом"), + ("Requires at least {} characters", "Потрібно щонайменше {} символів"), + ("Wrong PIN", "Неправильний PIN-код"), + ("Set PIN", "Встановити PIN-код"), + ("Enable trusted devices", "Увімкнути довірені пристрої"), + ("Manage trusted devices", "Керувати довіреними пристроями"), + ("Platform", "Платформа"), + ("Days remaining", "Залишилося днів"), + ("enable-trusted-devices-tip", "Пропускати двофакторну автентифікацію на довірених пристроях"), + ("Parent directory", "Батьківський каталог"), + ("Resume", "Продовжити"), + ("Invalid file name", "Неправильна назва файлу"), + ("one-way-file-transfer-tip", "На стороні, що керується, увімкнено односторонню передачу файлів."), + ("Authentication Required", "Потрібна автентифікація"), + ("Authenticate", "Автентифікувати"), + ("web_id_input_tip", "Ви можете ввести ID на тому самому серверу, прямий IP-доступ у веб-клієнті не підтримується.\nЯкщо ви хочете отримати доступ до пристрою на іншому сервері, будь ласка, додайте адресу сервера (@<адреса_сервера>?key=<значення_ключа>). Наприклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЯкщо ви хочете отримати доступ до пристрою на публічному сервері, будь ласка, введіть \"@public\". Для публічного сервера ключ не потрібен."), + ("Download", "Отримати"), + ("Upload folder", "Надіслати теку"), + ("Upload files", "Надіслати файли"), + ("Clipboard is synchronized", "Буфер обміну синхронізовано"), + ("Update client clipboard", "Оновити буфер обміну клієнта"), + ("Untagged", "Без міток"), + ("new-version-of-{}-tip", "Доступна нова версія {}"), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Будь ласка, оновіть RustDesk клієнт на віддаленому пристрої до версії {} чи новіше!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Перегляд камери"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ("Show my cursor", ""), + ("Scale custom", "Користувацький масштаб"), + ("Custom scale slider", ""), + ("Decrease", ""), + ("Increase", ""), + ("Show virtual mouse", ""), + ("Virtual mouse size", ""), + ("Small", ""), + ("Large", ""), + ("Show virtual joystick", ""), + ("Edit note", ""), + ("Alias", ""), + ("ScrollEdge", ""), + ("Allow insecure TLS fallback", ""), + ("allow-insecure-tls-fallback-tip", ""), + ("Disable UDP", ""), + ("disable-udp-tip", ""), + ("server-oss-not-support-tip", ""), + ("input note here", ""), + ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Продовжити з {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lang/vi.rs b/vendor/rustdesk/src/lang/vi.rs new file mode 100644 index 0000000..3fadb0e --- /dev/null +++ b/vendor/rustdesk/src/lang/vi.rs @@ -0,0 +1,748 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Trạng thái hiện tại"), + ("Your Desktop", "Desktop của bạn"), + ("desk_tip", "Desktop của bạn có thể được truy cập bằng ID và mật khẩu này."), + ("Password", "Mật khẩu"), + ("Ready", "Sẵn sàng"), + ("Established", "Đã được thiết lập"), + ("connecting_status", "Đang kết nối đến mạng lưới RustDesk..."), + ("Enable service", "Bật dịch vụ"), + ("Start service", "Bắt đầu dịch vụ"), + ("Service is running", "Dịch vụ hiện đang chạy"), + ("Service is not running", "Dịch vụ hiện đang dừng"), + ("not_ready_status", "Hiện chưa sẵn sàng. Hãy kiểm tra kết nối của bạn"), + ("Control Remote Desktop", "Điều khiển Desktop Từ Xa"), + ("Transfer file", "Truyền Tệp Tin"), + ("Connect", "Kết nối"), + ("Recent sessions", "Các phiên gần đây"), + ("Address book", "Sổ địa chỉ"), + ("Confirmation", "Xác nhận"), + ("TCP tunneling", "TCP tunneling"), + ("Remove", "Loại bỏ"), + ("Refresh random password", "Làm mới mật khẩu ngẫu nhiên"), + ("Set your own password", "Đặt mật khẩu riêng"), + ("Enable keyboard/mouse", "Cho phép sử dụng bàn phím/chuột"), + ("Enable clipboard", "Cho phép sử dụng Clipboard"), + ("Enable file transfer", "Cho phép truyền tệp tin"), + ("Enable TCP tunneling", "Cho phép TCP tunneling"), + ("IP Whitelisting", "Danh sách trắng IP"), + ("ID/Relay Server", "Máy chủ ID/Chuyển tiếp"), + ("Import server config", "Nhập cấu hình máy chủ"), + ("Export Server Config", "Xuất cấu hình máy chủ"), + ("Import server configuration successfully", "Nhập cấu hình máy chủ thành công"), + ("Export server configuration successfully", "Xuất cấu hình máy chủ thành công"), + ("Invalid server configuration", "Cấu hình máy chủ không hợp lệ"), + ("Clipboard is empty", "Khay nhớ tạm trống"), + ("Stop service", "Dừng dịch vụ"), + ("Change ID", "Thay đổi ID"), + ("Your new ID", "ID mới của bạn"), + ("length %min% to %max%", "độ dài từ %min% đến %max%"), + ("starts with a letter", "bắt đầu bằng một chữ cái"), + ("allowed characters", "các ký tự được phép"), + ("id_change_tip", "Các ký tự được phép: a-z, A-Z, 0-9, - (gạch ngang) và _ (gạch dưới). Ký tự đầu tiên phải là chữ cái. Độ dài từ 6 đến 16."), + ("Website", "Trang web"), + ("About", "Giới thiệu"), + ("Slogan_tip", "Được tạo ra với sự tận tâm trong thế giới đầy hỗn loạn này!"), + ("Privacy Statement", "Chính sách bảo mật"), + ("Mute", "Tắt tiếng"), + ("Build Date", "Ngày đóng gói"), + ("Version", "Phiên bản"), + ("Home", "Trang chủ"), + ("Audio Input", "Đầu vào âm thanh"), + ("Enhancements", "Tiện ích mở rộng"), + ("Hardware Codec", "Codec phần cứng"), + ("Adaptive bitrate", "Bitrate thích ứng"), + ("ID Server", "Máy chủ ID"), + ("Relay Server", "Máy chủ Chuyển tiếp"), + ("API Server", "Máy chủ API"), + ("invalid_http", "phải bắt đầu bằng http:// hoặc https://"), + ("Invalid IP", "IP không hợp lệ"), + ("Invalid format", "Định dạng không hợp lệ"), + ("server_not_support", "Máy chủ chưa hỗ trợ"), + ("Not available", "Không khả dụng"), + ("Too frequent", "Thao tác quá thường xuyên"), + ("Cancel", "Hủy"), + ("Skip", "Bỏ qua"), + ("Close", "Đóng"), + ("Retry", "Thử lại"), + ("OK", "OK"), + ("Password Required", "Yêu cầu mật khẩu"), + ("Please enter your password", "Vui lòng nhập mật khẩu"), + ("Remember password", "Nhớ mật khẩu"), + ("Wrong Password", "Sai mật khẩu"), + ("Do you want to enter again?", "Bạn có muốn nhập lại không?"), + ("Connection Error", "Lỗi kết nối"), + ("Error", "Lỗi"), + ("Reset by the peer", "Phía đối tác đã đặt lại kết nối"), + ("Connecting...", "Đang kết nối..."), + ("Connection in progress. Please wait.", "Đang thiết lập kết nối. Vui lòng chờ."), + ("Please try 1 minute later", "Vui lòng thử lại sau 1 phút"), + ("Login Error", "Lỗi đăng nhập"), + ("Successful", "Thành công"), + ("Connected, waiting for image...", "Đã kết nối, đang đợi hình ảnh..."), + ("Name", "Tên"), + ("Type", "Loại"), + ("Modified", "Ngày chỉnh sửa"), + ("Size", "Kích cỡ"), + ("Show Hidden Files", "Hiện tệp ẩn"), + ("Receive", "Nhận"), + ("Send", "Gửi"), + ("Refresh File", "Làm mới tệp"), + ("Local", "Cục bộ"), + ("Remote", "Từ xa"), + ("Remote Computer", "Máy tính từ xa"), + ("Local Computer", "Máy tính cục bộ"), + ("Confirm Delete", "Xác nhận xóa"), + ("Delete", "Xóa"), + ("Properties", "Thuộc tính"), + ("Multi Select", "Chọn nhiều"), + ("Select All", "Chọn tất cả"), + ("Unselect All", "Bỏ chọn tất cả"), + ("Empty Directory", "Thư mục trống"), + ("Not an empty directory", "Thư mục không trống"), + ("Are you sure you want to delete this file?", "Bạn có chắc chắn muốn xóa tệp này không?"), + ("Are you sure you want to delete this empty directory?", "Bạn có chắc chắn muốn xóa thư mục trống này không?"), + ("Are you sure you want to delete the file of this directory?", "Bạn có chắc chắn muốn xóa các tệp trong thư mục này không?"), + ("Do this for all conflicts", "Áp dụng cho mọi xung đột"), + ("This is irreversible!", "Hành động này không thể hoàn tác!"), + ("Deleting", "Đang xóa"), + ("files", "tệp"), + ("Waiting", "Đang chờ"), + ("Finished", "Hoàn thành"), + ("Speed", "Tốc độ"), + ("Custom Image Quality", "Tùy chỉnh chất lượng hình ảnh"), + ("Privacy mode", "Chế độ riêng tư"), + ("Block user input", "Chặn tương tác người dùng"), + ("Unblock user input", "Hủy chặn tương tác người dùng"), + ("Adjust Window", "Điều chỉnh cửa sổ"), + ("Original", "Gốc"), + ("Shrink", "Thu nhỏ"), + ("Stretch", "Kéo giãn"), + ("Scrollbar", "Thanh cuộn"), + ("ScrollAuto", "Tự động cuộn"), + ("Good image quality", "Chất lượng hình ảnh tốt"), + ("Balanced", "Cân bằng"), + ("Optimize reaction time", "Tối ưu thời gian phản hồi"), + ("Custom", "Tùy chỉnh"), + ("Show remote cursor", "Hiện con trỏ từ xa"), + ("Show quality monitor", "Hiện thông tin chất lượng"), + ("Disable clipboard", "Tắt Clipboard"), + ("Lock after session end", "Khóa máy sau khi kết thúc"), + ("Insert Ctrl + Alt + Del", "Gửi Ctrl + Alt + Del"), + ("Insert Lock", "Khóa máy"), + ("Refresh", "Làm mới"), + ("ID does not exist", "ID không tồn tại"), + ("Failed to connect to rendezvous server", "Không thể kết nối đến máy chủ Rendezvous"), + ("Please try later", "Vui lòng thử lại sau"), + ("Remote desktop is offline", "Máy tính từ xa đang ngoại tuyến"), + ("Key mismatch", "Khóa không khớp"), + ("Timeout", "Quá thời gian"), + ("Failed to connect to relay server", "Không thể kết nối tới máy chủ Chuyển tiếp"), + ("Failed to connect via rendezvous server", "Không thể kết nối qua máy chủ Rendezvous"), + ("Failed to connect via relay server", "Không thể kết nối qua máy chủ Chuyển tiếp"), + ("Failed to make direct connection to remote desktop", "Không thể kết nối trực tiếp"), + ("Set Password", "Đặt mật khẩu"), + ("OS Password", "Mật khẩu hệ điều hành"), + ("install_tip", "Do cơ chế UAC, RustDesk có thể không hoạt động ổn định ở phía người dùng từ xa trong một số trường hợp. Để tránh vấn đề này, vui lòng nhấn nút bên dưới để cài đặt RustDesk vào hệ thống."), + ("Click to upgrade", "Nhấn để nâng cấp"), + ("Configure", "Cấu hình"), + ("config_acc", "Để điều khiển từ xa, bạn cần cấp quyền \"Trợ năng\" cho RustDesk."), + ("config_screen", "Để truy cập từ xa, bạn cần cấp quyền \"Ghi màn hình\" cho RustDesk."), + ("Installing ...", "Đang cài đặt..."), + ("Install", "Cài đặt"), + ("Installation", "Cài đặt"), + ("Installation Path", "Đường dẫn cài đặt"), + ("Create start menu shortcuts", "Tạo shortcut ở Start Menu"), + ("Create desktop icon", "Tạo biểu tượng ngoài màn hình"), + ("agreement_tip", "Bằng việc bắt đầu cài đặt, bạn đồng ý với các điều khoản cấp phép."), + ("Accept and Install", "Chấp nhận và Cài đặt"), + ("End-user license agreement", "Thỏa thuận người dùng cuối"), + ("Generating ...", "Đang khởi tạo..."), + ("Your installation is lower version.", "Phiên bản cài đặt của bạn cũ hơn."), + ("not_close_tcp_tip", "Đừng đóng cửa sổ này khi đang sử dụng Tunnel"), + ("Listening ...", "Đang lắng nghe..."), + ("Remote Host", "Máy chủ từ xa"), + ("Remote Port", "Cổng từ xa"), + ("Action", "Hành động"), + ("Add", "Thêm"), + ("Local Port", "Cổng nội bộ"), + ("Local Address", "Địa chỉ nội bộ"), + ("Change Local Port", "Đổi cổng nội bộ"), + ("setup_server_tip", "Để kết nối nhanh hơn, hãy tự thiết lập máy chủ riêng"), + ("Too short, at least 6 characters.", "Quá ngắn, cần ít nhất 6 ký tự."), + ("The confirmation is not identical.", "Mật khẩu xác nhận không khớp"), + ("Permissions", "Quyền"), + ("Accept", "Chấp nhận"), + ("Dismiss", "Bỏ qua"), + ("Disconnect", "Ngắt kết nối"), + ("Enable file copy and paste", "Cho phép sao chép và dán tệp"), + ("Connected", "Đã kết nối"), + ("Direct and encrypted connection", "Kết nối trực tiếp và mã hóa"), + ("Relayed and encrypted connection", "Kết nối chuyển tiếp và mã hóa"), + ("Direct and unencrypted connection", "Kết nối trực tiếp và không mã hóa"), + ("Relayed and unencrypted connection", "Kết nối chuyển tiếp và không mã hóa"), + ("Enter Remote ID", "Nhập ID từ xa"), + ("Enter your password", "Nhập mật khẩu của bạn"), + ("Logging in...", "Đang đăng nhập..."), + ("Enable RDP session sharing", "Cho phép chia sẻ phiên RDP"), + ("Auto Login", "Tự động đăng nhập"), + ("Enable direct IP access", "Cho phép truy cập IP trực tiếp"), + ("Rename", "Đổi tên"), + ("Space", "Khoảng cách"), + ("Create desktop shortcut", "Tạo shortcut màn hình"), + ("Change Path", "Đổi đường dẫn"), + ("Create Folder", "Tạo thư mục"), + ("Please enter the folder name", "Vui lòng nhập tên thư mục"), + ("Fix it", "Sửa lỗi"), + ("Warning", "Cảnh báo"), + ("Login screen using Wayland is not supported", "Màn hình đăng nhập Wayland không được hỗ trợ"), + ("Reboot required", "Yêu cầu khởi động lại"), + ("Unsupported display server", "Máy chủ hiển thị không được hỗ trợ"), + ("x11 expected", "Yêu cầu X11"), + ("Port", "Cổng"), + ("Settings", "Cài đặt"), + ("Username", "Tên người dùng"), + ("Invalid port", "Cổng không hợp lệ"), + ("Closed manually by the peer", "Bị đóng thủ công bởi đối tác"), + ("Enable remote configuration modification", "Cho phép sửa cấu hình từ xa"), + ("Run without install", "Chạy không cần cài đặt"), + ("Connect via relay", "Kết nối qua chuyển tiếp"), + ("Always connect via relay", "Luôn kết nối qua chuyển tiếp"), + ("whitelist_tip", "Chỉ IP trong danh sách trắng mới có thể truy cập"), + ("Login", "Đăng nhập"), + ("Verify", "Xác thực"), + ("Remember me", "Ghi nhớ"), + ("Trust this device", "Tin tưởng thiết bị này"), + ("Verification code", "Mã xác thực"), + ("verification_tip", "Bạn đang đăng nhập trên thiết bị mới. Một mã xác thực đã được gửi đến email của bạn, vui lòng nhập mã để tiếp tục."), + ("Logout", "Đăng xuất"), + ("Tags", "Thẻ"), + ("Search ID", "Tìm ID"), + ("whitelist_sep", "Phân cách bởi dấu phẩy, dấu chấm phẩy, khoảng trắng hoặc dòng mới"), + ("Add ID", "Thêm ID"), + ("Add Tag", "Thêm thẻ"), + ("Unselect all tags", "Bỏ chọn tất cả thẻ"), + ("Network error", "Lỗi mạng"), + ("Username missed", "Thiếu tên người dùng"), + ("Password missed", "Thiếu mật khẩu"), + ("Wrong credentials", "Thông tin đăng nhập sai"), + ("The verification code is incorrect or has expired", "Mã xác thực không đúng hoặc đã hết hạn"), + ("Edit Tag", "Sửa thẻ"), + ("Forget Password", "Quên mật khẩu"), + ("Favorites", "Yêu thích"), + ("Add to Favorites", "Thêm vào yêu thích"), + ("Remove from Favorites", "Xóa khỏi yêu thích"), + ("Empty", "Trống"), + ("Invalid folder name", "Tên thư mục không hợp lệ"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "Đã phát hiện"), + ("install_daemon_tip", "Để khởi động cùng hệ thống, bạn cần cài đặt dịch vụ daemon."), + ("Remote ID", "ID từ xa"), + ("Paste", "Dán"), + ("Paste here?", "Dán vào đây?"), + ("Are you sure to close the connection?", "Bạn có chắc chắn muốn đóng kết nối?"), + ("Download new version", "Tải phiên bản mới"), + ("Touch mode", "Chế độ chạm"), + ("Mouse mode", "Chế độ chuột"), + ("One-Finger Tap", "Chạm một ngón"), + ("Left Mouse", "Chuột trái"), + ("One-Long Tap", "Chạm giữ một ngón"), + ("Two-Finger Tap", "Chạm hai ngón"), + ("Right Mouse", "Chuột phải"), + ("One-Finger Move", "Di chuyển một ngón"), + ("Double Tap & Move", "Chạm đúp và di chuyển"), + ("Mouse Drag", "Kéo chuột"), + ("Three-Finger vertically", "Ba ngón theo chiều dọc"), + ("Mouse Wheel", "Con lăn chuột"), + ("Two-Finger Move", "Di chuyển hai ngón"), + ("Canvas Move", "Di chuyển khung hình"), + ("Pinch to Zoom", "Véo để thu phóng"), + ("Canvas Zoom", "Thu phóng khung hình"), + ("Reset canvas", "Đặt lại khung hình"), + ("No permission of file transfer", "Không có quyền truyền tệp"), + ("Note", "Ghi chú"), + ("Connection", "Kết nối"), + ("Share screen", "Chia sẻ màn hình"), + ("Chat", "Trò chuyện"), + ("Total", "Tổng cộng"), + ("items", "mục"), + ("Selected", "Đã chọn"), + ("Screen Capture", "Chụp màn hình"), + ("Input Control", "Kiểm soát đầu vào"), + ("Audio Capture", "Ghi âm thanh"), + ("Do you accept?", "Bạn có đồng ý không?"), + ("Open System Setting", "Mở cài đặt hệ thống"), + ("How to get Android input permission?", "Làm sao để lấy quyền nhập liệu trên Android?"), + ("android_input_permission_tip1", "Để điều khiển Android bằng chuột hoặc chạm, bạn cần cấp quyền [Trợ năng]."), + ("android_input_permission_tip2", "Vui lòng tìm [Dịch vụ đã cài đặt] trong cài đặt và bật [RustDesk Input]."), + ("android_new_connection_tip", "Yêu cầu điều khiển mới đã được nhận."), + ("android_service_will_start_tip", "Bật [Ghi màn hình] sẽ tự động khởi động dịch vụ."), + ("android_stop_service_tip", "Dừng dịch vụ sẽ đóng tất cả các kết nối."), + ("android_version_audio_tip", "Phiên bản Android này không hỗ trợ ghi âm, vui lòng nâng cấp lên Android 10+."), + ("android_start_service_tip", "Nhấn [Bắt đầu dịch vụ] để chia sẻ màn hình."), + ("android_permission_may_not_change_tip", "Quyền có thể không thay đổi ngay lập tức cho đến khi kết nối lại."), + ("Account", "Tài khoản"), + ("Overwrite", "Ghi đè"), + ("This file exists, skip or overwrite this file?", "Tệp đã tồn tại, bỏ qua hay ghi đè?"), + ("Quit", "Thoát"), + ("Help", "Trợ giúp"), + ("Failed", "Thất bại"), + ("Succeeded", "Thành công"), + ("Someone turns on privacy mode, exit", "Chế độ riêng tư đã được bật, thoát"), + ("Unsupported", "Không hỗ trợ"), + ("Peer denied", "Đối tác từ chối"), + ("Please install plugins", "Vui lòng cài đặt plugin"), + ("Peer exit", "Đối tác đã thoát"), + ("Failed to turn off", "Không thể tắt"), + ("Turned off", "Đã tắt"), + ("Language", "Ngôn ngữ"), + ("Keep RustDesk background service", "Giữ dịch vụ RustDesk chạy nền"), + ("Ignore Battery Optimizations", "Bỏ qua tối ưu hóa pin"), + ("android_open_battery_optimizations_tip", "Vui lòng chọn [Không hạn chế] trong cài đặt Pin."), + ("Start on boot", "Khởi động cùng hệ thống"), + ("Start the screen sharing service on boot, requires special permissions", "Khởi động dịch vụ chia sẻ màn hình khi bật máy (cần quyền đặc biệt)"), + ("Connection not allowed", "Kết nối không được phép"), + ("Legacy mode", "Chế độ cũ"), + ("Map mode", "Chế độ bản đồ"), + ("Translate mode", "Chế độ dịch"), + ("Use permanent password", "Dùng mật khẩu vĩnh viễn"), + ("Use both passwords", "Dùng cả hai mật khẩu"), + ("Set permanent password", "Đặt mật khẩu vĩnh viễn"), + ("Enable remote restart", "Cho phép khởi động lại từ xa"), + ("Restart remote device", "Khởi động lại máy từ xa"), + ("Are you sure you want to restart", "Bạn có chắc chắn muốn khởi động lại?"), + ("Restarting remote device", "Đang khởi động lại máy từ xa..."), + ("remote_restarting_tip", "Máy từ xa đang khởi động lại, vui lòng kết nối lại sau ít phút."), + ("Copied", "Đã sao chép"), + ("Exit Fullscreen", "Thoát toàn màn hình"), + ("Fullscreen", "Toàn màn hình"), + ("Mobile Actions", "Thao tác di động"), + ("Select Monitor", "Chọn màn hình"), + ("Control Actions", "Thao tác điều khiển"), + ("Display Settings", "Cài đặt hiển thị"), + ("Ratio", "Tỷ lệ"), + ("Image Quality", "Chất lượng hình ảnh"), + ("Scroll Style", "Kiểu cuộn"), + ("Show Toolbar", "Hiện thanh công cụ"), + ("Hide Toolbar", "Ẩn thanh công cụ"), + ("Direct Connection", "Kết nối trực tiếp"), + ("Relay Connection", "Kết nối chuyển tiếp"), + ("Secure Connection", "Kết nối bảo mật"), + ("Insecure Connection", "Kết nối không bảo mật"), + ("Scale original", "Tỷ lệ gốc"), + ("Scale adaptive", "Tỷ lệ thích ứng"), + ("General", "Chung"), + ("Security", "Bảo mật"), + ("Theme", "Chủ đề"), + ("Dark Theme", "Chủ đề Tối"), + ("Light Theme", "Chủ đề Sáng"), + ("Dark", "Tối"), + ("Light", "Sáng"), + ("Follow System", "Theo hệ thống"), + ("Enable hardware codec", "Bật Codec phần cứng"), + ("Unlock Security Settings", "Mở khóa cài đặt bảo mật"), + ("Enable audio", "Bật âm thanh"), + ("Unlock Network Settings", "Mở khóa cài đặt mạng"), + ("Server", "Máy chủ"), + ("Direct IP Access", "Truy cập IP trực tiếp"), + ("Proxy", "Proxy"), + ("Apply", "Áp dụng"), + ("Disconnect all devices?", "Ngắt tất cả thiết bị?"), + ("Clear", "Xóa sạch"), + ("Audio Input Device", "Thiết bị đầu vào âm thanh"), + ("Use IP Whitelisting", "Sử dụng danh sách trắng IP"), + ("Network", "Mạng"), + ("Pin Toolbar", "Ghim thanh công cụ"), + ("Unpin Toolbar", "Bỏ ghim thanh công cụ"), + ("Recording", "Đang ghi hình"), + ("Directory", "Thư mục"), + ("Automatically record incoming sessions", "Tự động ghi lại các kết nối đến"), + ("Automatically record outgoing sessions", "Tự động ghi lại các kết nối đi"), + ("Change", "Thay đổi"), + ("Start session recording", "Bắt đầu ghi hình phiên"), + ("Stop session recording", "Dừng ghi hình phiên"), + ("Enable recording session", "Cho phép ghi hình phiên"), + ("Enable LAN discovery", "Bật phát hiện trong mạng LAN"), + ("Deny LAN discovery", "Từ chối phát hiện trong mạng LAN"), + ("Write a message", "Viết tin nhắn..."), + ("Prompt", "Gợi ý"), + ("Please wait for confirmation of UAC...", "Vui lòng chờ xác nhận UAC..."), + ("elevated_foreground_window_tip", "Cửa sổ phía trước yêu cầu quyền cao hơn, tạm thời không thể sử dụng chuột/phím. Yêu cầu phía đối tác thu nhỏ cửa sổ hoặc cấp quyền."), + ("Disconnected", "Đã ngắt kết nối"), + ("Other", "Khác"), + ("Confirm before closing multiple tabs", "Xác nhận trước khi đóng nhiều tab"), + ("Keyboard Settings", "Cài đặt bàn phím"), + ("Full Access", "Toàn quyền truy cập"), + ("Screen Share", "Chia sẻ màn hình"), + ("ubuntu-21-04-required", "Wayland yêu cầu Ubuntu 21.04 trở lên."), + ("wayland-requires-higher-linux-version", "Wayland yêu cầu phiên bản Linux mới hơn. Hãy thử X11 hoặc đổi hệ điều hành."), + ("xdp-portal-unavailable", ""), + ("JumpLink", "Xem"), + ("Please Select the screen to be shared(Operate on the peer side).", "Vui lòng chọn màn hình chia sẻ (Thao tác ở phía đối tác)."), + ("Show RustDesk", "Hiện RustDesk"), + ("This PC", "Máy tính này"), + ("or", "hoặc"), + ("Elevate", "Nâng quyền"), + ("Zoom cursor", "Phóng to con trỏ"), + ("Accept sessions via password", "Chấp nhận phiên qua mật khẩu"), + ("Accept sessions via click", "Chấp nhận phiên qua xác nhận"), + ("Accept sessions via both", "Chấp nhận phiên qua cả hai"), + ("Please wait for the remote side to accept your session request...", "Vui lòng chờ phía đối tác chấp nhận yêu cầu kết nối..."), + ("One-time Password", "Mật khẩu dùng một lần"), + ("Use one-time password", "Sử dụng mật khẩu một lần"), + ("One-time password length", "Độ dài mật khẩu một lần"), + ("Request access to your device", "Yêu cầu truy cập thiết bị của bạn"), + ("Hide connection management window", "Ẩn cửa sổ quản lý kết nối"), + ("hide_cm_tip", "Chỉ ẩn khi sử dụng mật khẩu vĩnh viễn"), + ("wayland_experiment_tip", "Wayland đang thử nghiệm, hãy dùng X11 nếu muốn ổn định."), + ("Right click to select tabs", "Chuột phải để chọn tab"), + ("Skipped", "Đã bỏ qua"), + ("Add to address book", "Thêm vào sổ địa chỉ"), + ("Group", "Nhóm"), + ("Search", "Tìm kiếm"), + ("Closed manually by web console", "Đã đóng bởi Web Console"), + ("Local keyboard type", "Loại bàn phím cục bộ"), + ("Select local keyboard type", "Chọn loại bàn phím cục bộ"), + ("software_render_tip", "Nếu gặp lỗi hiển thị trên Linux với Nvidia, hãy thử phần mềm render."), + ("Always use software rendering", "Luôn sử dụng render bằng phần mềm"), + ("config_input", "Cấp quyền [Theo dõi đầu vào] để dùng bàn phím."), + ("config_microphone", "Cấp quyền [Ghi âm] để trò chuyện."), + ("request_elevation_tip", "Bạn cũng có thể yêu cầu nâng quyền từ người ở phía xa."), + ("Wait", "Chờ"), + ("Elevation Error", "Lỗi nâng quyền"), + ("Ask the remote user for authentication", "Yêu cầu người dùng từ xa xác thực"), + ("Choose this if the remote account is administrator", "Chọn nếu tài khoản từ xa là Quản trị viên"), + ("Transmit the username and password of administrator", "Gửi tên đăng nhập và mật khẩu Quản trị viên"), + ("still_click_uac_tip", "Người dùng từ xa vẫn cần nhấn OK trên hộp thoại UAC."), + ("Request Elevation", "Yêu cầu nâng quyền"), + ("wait_accept_uac_tip", "Vui lòng chờ đối tác chấp nhận UAC."), + ("Elevate successfully", "Nâng quyền thành công"), + ("uppercase", "chữ hoa"), + ("lowercase", "chữ thường"), + ("digit", "số"), + ("special character", "ký tự đặc biệt"), + ("length>=8", "độ dài >= 8"), + ("Weak", "Yếu"), + ("Medium", "Trung bình"), + ("Strong", "Mạnh"), + ("Switch Sides", "Đổi bên"), + ("Please confirm if you want to share your desktop?", "Xác nhận chia sẻ màn hình?"), + ("Display", "Hiển thị"), + ("Default View Style", "Kiểu xem mặc định"), + ("Default Scroll Style", "Kiểu cuộn mặc định"), + ("Default Image Quality", "Chất lượng hình ảnh mặc định"), + ("Default Codec", "Codec mặc định"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Tự động"), + ("Other Default Options", "Các tùy chọn mặc định khác"), + ("Voice call", "Gọi thoại"), + ("Text chat", "Chat văn bản"), + ("Stop voice call", "Dừng gọi thoại"), + ("relay_hint_tip", "Nếu không kết nối trực tiếp được, hãy thử qua máy chủ chuyển tiếp (ID/r)."), + ("Reconnect", "Kết nối lại"), + ("Codec", "Codec"), + ("Resolution", "Độ phân giải"), + ("No transfers in progress", "Không có tệp nào đang truyền"), + ("Set one-time password length", "Đặt độ dài mật khẩu một lần"), + ("RDP Settings", "Cài đặt RDP"), + ("Sort by", "Sắp xếp theo"), + ("New Connection", "Kết nối mới"), + ("Restore", "Khôi phục"), + ("Minimize", "Thu nhỏ"), + ("Maximize", "Phóng to"), + ("Your Device", "Thiết bị của bạn"), + ("empty_recent_tip", "Chưa có kết nối gần đây."), + ("empty_favorite_tip", "Chưa có mục yêu thích."), + ("empty_lan_tip", "Không tìm thấy thiết bị nào trong LAN."), + ("empty_address_book_tip", "Sổ địa chỉ đang trống."), + ("Empty Username", "Tên người dùng trống"), + ("Empty Password", "Mật khẩu trống"), + ("Me", "Tôi"), + ("identical_file_tip", "Tệp này giống hệt ở phía đối tác."), + ("show_monitors_tip", "Hiện màn hình trên thanh công cụ"), + ("View Mode", "Chế độ xem"), + ("login_linux_tip", "Cần đăng nhập tài khoản Linux để kích hoạt X session."), + ("verify_rustdesk_password_tip", "Xác thực mật khẩu RustDesk"), + ("remember_account_tip", "Nhớ tài khoản này"), + ("os_account_desk_tip", "Tài khoản OS được dùng để đăng nhập và chạy session không màn hình (headless)."), + ("OS Account", "Tài khoản OS"), + ("another_user_login_title_tip", "Người dùng khác đã đăng nhập"), + ("another_user_login_text_tip", "Ngắt kết nối hiện tại"), + ("xorg_not_found_title_tip", "Không tìm thấy Xorg"), + ("xorg_not_found_text_tip", "Vui lòng cài đặt Xorg"), + ("no_desktop_title_tip", "Không có desktop"), + ("no_desktop_text_tip", "Vui lòng cài đặt GNOME hoặc desktop khác."), + ("No need to elevate", "Không cần nâng quyền"), + ("System Sound", "Âm thanh hệ thống"), + ("Default", "Mặc định"), + ("New RDP", "RDP mới"), + ("Fingerprint", "Dấu vân tay"), + ("Copy Fingerprint", "Sao chép fingerprint"), + ("no fingerprints", "không có fingerprint"), + ("Select a peer", "Chọn một đối tác"), + ("Select peers", "Chọn các đối tác"), + ("Plugins", "Plugin"), + ("Uninstall", "Gỡ cài đặt"), + ("Update", "Cập nhật"), + ("Enable", "Bật"), + ("Disable", "Tắt"), + ("Options", "Tùy chọn"), + ("resolution_original_tip", "Độ phân giải gốc"), + ("resolution_fit_local_tip", "Vừa với máy cục bộ"), + ("resolution_custom_tip", "Độ phân giải tùy chỉnh"), + ("Collapse toolbar", "Thu gọn thanh công cụ"), + ("Accept and Elevate", "Chấp nhận và Nâng quyền"), + ("accept_and_elevate_btn_tooltip", "Chấp nhận kết nối và nâng quyền UAC."), + ("clipboard_wait_response_timeout_tip", "Hết thời gian chờ Clipboard phản hồi."), + ("Incoming connection", "Kết nối đến"), + ("Outgoing connection", "Kết nối đi"), + ("Exit", "Thoát"), + ("Open", "Mở"), + ("logout_tip", "Bạn có chắc muốn đăng xuất?"), + ("Service", "Dịch vụ"), + ("Start", "Bắt đầu"), + ("Stop", "Dừng"), + ("exceed_max_devices", "Vượt quá số lượng thiết bị tối đa."), + ("Sync with recent sessions", "Đồng bộ với các phiên gần đây"), + ("Sort tags", "Sắp xếp thẻ"), + ("Open connection in new tab", "Mở kết nối trong tab mới"), + ("Move tab to new window", "Di chuyển tab sang cửa sổ mới"), + ("Can not be empty", "Không được để trống"), + ("Already exists", "Đã tồn tại"), + ("Change Password", "Đổi mật khẩu"), + ("Refresh Password", "Làm mới mật khẩu"), + ("ID", "ID"), + ("Grid View", "Dạng lưới"), + ("List View", "Dạng danh sách"), + ("Select", "Chọn"), + ("Toggle Tags", "Bật/Tắt thẻ"), + ("pull_ab_failed_tip", "Lấy sổ địa chỉ thất bại."), + ("push_ab_failed_tip", "Đồng bộ sổ địa chỉ thất bại."), + ("synced_peer_readded_tip", "Thiết bị đã đồng bộ được thêm lại."), + ("Change Color", "Đổi màu"), + ("Primary Color", "Màu chính"), + ("HSV Color", "Màu HSV"), + ("Installation Successful!", "Cài đặt thành công!"), + ("Installation failed!", "Cài đặt thất bại!"), + ("Reverse mouse wheel", "Đảo ngược con lăn chuột"), + ("{} sessions", "{} phiên"), + ("scam_title", "CẢNH BÁO LỪA ĐẢO"), + ("scam_text1", "KHÔNG chia sẻ ID/Mật khẩu với người lạ qua điện thoại. Nếu họ yêu cầu, họ có thể là kẻ lừa đảo."), + ("scam_text2", "Chỉ sử dụng RustDesk với những người bạn thực sự tin tưởng."), + ("Don't show again", "Không hiển thị lại"), + ("I Agree", "Tôi đồng ý"), + ("Decline", "Từ chối"), + ("Timeout in minutes", "Thời gian chờ (phút)"), + ("auto_disconnect_option_tip", "Tự động ngắt kết nối khi không hoạt động"), + ("Connection failed due to inactivity", "Ngắt kết nối do không hoạt động"), + ("Check for software update on startup", "Kiểm tra cập nhật khi khởi động"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Nâng cấp lên Pro để có thêm tính năng"), + ("pull_group_failed_tip", "Lấy thông tin nhóm thất bại"), + ("Filter by intersection", "Lọc theo giao điểm"), + ("Remove wallpaper during incoming sessions", "Xóa hình nền khi có kết nối đến"), + ("Test", "Kiểm tra"), + ("display_is_plugged_out_msg", "Màn hình đã bị rút."), + ("No displays", "Không có màn hình"), + ("Open in new window", "Mở trong cửa sổ mới"), + ("Show displays as individual windows", "Hiển thị mỗi màn hình một cửa sổ"), + ("Use all my displays for the remote session", "Sử dụng tất cả màn hình của tôi"), + ("selinux_tip", "SELinux đang bật, có thể gây lỗi."), + ("Change view", "Đổi kiểu xem"), + ("Big tiles", "Ô lớn"), + ("Small tiles", "Ô nhỏ"), + ("List", "Danh sách"), + ("Virtual display", "Màn hình ảo"), + ("Plug out all", "Rút tất cả"), + ("True color (4:4:4)", "Màu thực (4:4:4)"), + ("Enable blocking user input", "Cho phép chặn đầu vào người dùng"), + ("id_input_tip", "Nhập ID hoặc IP."), + ("privacy_mode_impl_mag_tip", "Chế độ riêng tư (Magnifier)"), + ("privacy_mode_impl_virtual_display_tip", "Chế độ riêng tư (Virtual Display)"), + ("Enter privacy mode", "Vào chế độ riêng tư"), + ("Exit privacy mode", "Thoát chế độ riêng tư"), + ("idd_not_support_under_win10_2004_tip", "Yêu cầu Windows 10 2004 trở lên."), + ("input_source_1_tip", "Nguồn đầu vào 1"), + ("input_source_2_tip", "Nguồn đầu vào 2"), + ("Swap control-command key", "Hoán đổi phím Ctrl-Cmd"), + ("swap-left-right-mouse", "Hoán đổi chuột trái-phải"), + ("2FA code", "Mã 2FA"), + ("More", "Thêm"), + ("enable-2fa-title", "Bật xác thực 2 bước"), + ("enable-2fa-desc", "Vui lòng quét mã QR để bật 2FA."), + ("wrong-2fa-code", "Mã 2FA sai"), + ("enter-2fa-title", "Nhập mã 2FA"), + ("Email verification code must be 6 characters.", "Mã xác thực email phải có 6 ký tự."), + ("2FA code must be 6 digits.", "Mã 2FA phải có 6 chữ số."), + ("Multiple Windows sessions found", "Tìm thấy nhiều phiên Windows"), + ("Please select the session you want to connect to", "Chọn phiên bạn muốn kết nối"), + ("powered_by_me", "Cung cấp bởi tôi"), + ("outgoing_only_desk_tip", "Chỉ cho phép kết nối đi."), + ("preset_password_warning", "Cảnh báo mật khẩu thiết lập sẵn"), + ("Security Alert", "Cảnh báo bảo mật"), + ("My address book", "Sổ địa chỉ của tôi"), + ("Personal", "Cá nhân"), + ("Owner", "Chủ sở hữu"), + ("Set shared password", "Đặt mật khẩu chia sẻ"), + ("Exist in", "Tồn tại trong"), + ("Read-only", "Chỉ đọc"), + ("Read/Write", "Đọc/Ghi"), + ("Full Control", "Toàn quyền"), + ("share_warning_tip", "Cẩn thận khi chia sẻ quyền điều khiển!"), + ("Everyone", "Mọi người"), + ("ab_web_console_tip", "Quản lý qua Web Console"), + ("allow-only-conn-window-open-tip", "Chỉ cho phép khi cửa sổ RustDesk mở"), + ("no_need_privacy_mode_no_physical_displays_tip", "Không cần chế độ riêng tư vì không có màn hình vật lý."), + ("Follow remote cursor", "Theo con trỏ từ xa"), + ("Follow remote window focus", "Theo tiêu điểm cửa sổ từ xa"), + ("default_proxy_tip", "Proxy mặc định"), + ("no_audio_input_device_tip", "Không tìm thấy thiết bị thu âm."), + ("Incoming", "Đang đến"), + ("Outgoing", "Đang đi"), + ("Clear Wayland screen selection", "Xóa lựa chọn màn hình Wayland"), + ("clear_Wayland_screen_selection_tip", "Đặt lại các quyền chọn màn hình."), + ("confirm_clear_Wayland_screen_selection_tip", "Bạn có chắc muốn đặt lại?"), + ("android_new_voice_call_tip", "Yêu cầu gọi thoại mới."), + ("texture_render_tip", "Sử dụng Texture Rendering"), + ("Use texture rendering", "Sử dụng Texture Rendering"), + ("Floating window", "Cửa sổ nổi"), + ("floating_window_tip", "Giữ RustDesk trên cùng"), + ("Keep screen on", "Giữ màn hình luôn bật"), + ("Never", "Không bao giờ"), + ("During controlled", "Trong khi bị điều khiển"), + ("During service is on", "Trong khi dịch vụ đang bật"), + ("Capture screen using DirectX", "Chụp màn hình bằng DirectX"), + ("Back", "Trở về"), + ("Apps", "Ứng dụng"), + ("Volume up", "Tăng âm lượng"), + ("Volume down", "Giảm âm lượng"), + ("Power", "Nguồn"), + ("Telegram bot", "Telegram Bot"), + ("enable-bot-tip", "Bật thông báo qua Telegram"), + ("enable-bot-desc", "Liên kết với Telegram Bot của bạn."), + ("cancel-2fa-confirm-tip", "Xác nhận tắt 2FA?"), + ("cancel-bot-confirm-tip", "Xác nhận tắt Bot?"), + ("About RustDesk", "Về RustDesk"), + ("Send clipboard keystrokes", "Gửi phím từ Clipboard"), + ("network_error_tip", "Lỗi mạng, vui lòng kiểm tra lại."), + ("Unlock with PIN", "Mở khóa bằng mã PIN"), + ("Requires at least {} characters", "Yêu cầu ít nhất {} ký tự"), + ("Wrong PIN", "Mã PIN sai"), + ("Set PIN", "Đặt mã PIN"), + ("Enable trusted devices", "Bật thiết bị tin cậy"), + ("Manage trusted devices", "Quản lý thiết bị tin cậy"), + ("Platform", "Nền tảng"), + ("Days remaining", "Số ngày còn lại"), + ("enable-trusted-devices-tip", "Chỉ thiết bị tin cậy mới có thể kết nối không cần mật khẩu."), + ("Parent directory", "Thư mục cha"), + ("Resume", "Tiếp tục"), + ("Invalid file name", "Tên tệp không hợp lệ"), + ("one-way-file-transfer-tip", "Chỉ cho phép truyền tệp một chiều."), + ("Authentication Required", "Yêu cầu xác thực"), + ("Authenticate", "Xác thực"), + ("web_id_input_tip", "Nhập ID để bắt đầu kết nối Web."), + ("Download", "Tải xuống"), + ("Upload folder", "Tải lên thư mục"), + ("Upload files", "Tải lên tệp"), + ("Clipboard is synchronized", "Clipboard đã được đồng bộ"), + ("Update client clipboard", "Cập nhật Clipboard của khách"), + ("Untagged", "Chưa gắn thẻ"), + ("new-version-of-{}-tip", "Đã có phiên bản mới của {}"), + ("Accessible devices", "Thiết bị có thể truy cập"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Vui lòng nâng cấp đối tác lên {}"), + ("d3d_render_tip", "Sử dụng D3D Rendering"), + ("Use D3D rendering", "Sử dụng D3D Rendering"), + ("Printer", "Máy in"), + ("printer-os-requirement-tip", "Yêu cầu hệ điều hành hỗ trợ máy in."), + ("printer-requires-installed-{}-client-tip", "Cần cài đặt driver {}"), + ("printer-{}-not-installed-tip", "Máy in {} chưa được cài đặt."), + ("printer-{}-ready-tip", "Máy in {} đã sẵn sàng."), + ("Install {} Printer", "Cài đặt máy in {}"), + ("Outgoing Print Jobs", "Yêu cầu in đi"), + ("Incoming Print Jobs", "Yêu cầu in đến"), + ("Incoming Print Job", "Yêu cầu in đến"), + ("use-the-default-printer-tip", "Sử dụng máy in mặc định"), + ("use-the-selected-printer-tip", "Sử dụng máy in đã chọn"), + ("auto-print-tip", "Tự động in"), + ("print-incoming-job-confirm-tip", "Xác nhận in tệp này?"), + ("remote-printing-disallowed-tile-tip", "In từ xa bị cấm"), + ("remote-printing-disallowed-text-tip", "Vui lòng bật quyền in trong cài đặt."), + ("save-settings-tip", "Lưu cài đặt"), + ("dont-show-again-tip", "Đừng hiện lại"), + ("Take screenshot", "Chụp màn hình"), + ("Taking screenshot", "Đang chụp màn hình..."), + ("screenshot-merged-screen-not-supported-tip", "Không hỗ trợ chụp gộp nhiều màn hình."), + ("screenshot-action-tip", "Hành động chụp màn hình"), + ("Save as", "Lưu thành"), + ("Copy to clipboard", "Sao chép vào Clipboard"), + ("Enable remote printer", "Bật máy in từ xa"), + ("Downloading {}", "Đang tải xuống {}"), + ("{} Update", "Cập nhật {}"), + ("{}-to-update-tip", "Cần nâng cấp để sử dụng tính năng này."), + ("download-new-version-failed-tip", "Tải phiên bản mới thất bại."), + ("Auto update", "Tự động cập nhật"), + ("update-failed-check-msi-tip", "Cập nhật lỗi, vui lòng kiểm tra file MSI."), + ("websocket_tip", "Sử dụng giao thức WebSocket"), + ("Use WebSocket", "Sử dụng WebSocket"), + ("Trackpad speed", "Tốc độ Trackpad"), + ("Default trackpad speed", "Tốc độ Trackpad mặc định"), + ("Numeric one-time password", "Mật khẩu số dùng một lần"), + ("Enable IPv6 P2P connection", "Cho phép kết nối IPv6 P2P"), + ("Enable UDP hole punching", "Bật UDP Hole Punching"), + ("View camera", "Xem Camera"), + ("Enable camera", "Bật Camera"), + ("No cameras", "Không có camera"), + ("view_camera_unsupported_tip", "Đối tác chưa hỗ trợ xem camera."), + ("Terminal", "Terminal"), + ("Enable terminal", "Bật Terminal"), + ("New tab", "Tab mới"), + ("Keep terminal sessions on disconnect", "Giữ phiên terminal khi ngắt kết nối"), + ("Terminal (Run as administrator)", "Terminal (Quyền Quản trị viên)"), + ("terminal-admin-login-tip", "Đang đăng nhập quyền quản trị..."), + ("Failed to get user token.", "Lấy mã token người dùng thất bại."), + ("Incorrect username or password.", "Tên người dùng hoặc mật khẩu sai."), + ("The user is not an administrator.", "Người dùng không phải Quản trị viên."), + ("Failed to check if the user is an administrator.", "Kiểm tra quyền Quản trị viên thất bại."), + ("Supported only in the installed version.", "Chỉ hỗ trợ trên bản đã cài đặt."), + ("elevation_username_tip", "Tên đăng nhập để nâng quyền"), + ("Preparing for installation ...", "Đang chuẩn bị cài đặt..."), + ("Show my cursor", "Hiện con trỏ của tôi"), + ("Scale custom", "Tùy chỉnh tỷ lệ"), + ("Custom scale slider", "Thanh trượt tỷ lệ"), + ("Decrease", "Giảm"), + ("Increase", "Tăng"), + ("Show virtual mouse", "Hiện chuột ảo"), + ("Virtual mouse size", "Kích thước chuột ảo"), + ("Small", "Nhỏ"), + ("Large", "Lớn"), + ("Show virtual joystick", "Hiện Joystick ảo"), + ("Edit note", "Sửa ghi chú"), + ("Alias", "Bí danh"), + ("ScrollEdge", "Cuộn ở cạnh"), + ("Allow insecure TLS fallback", "Cho phép hạ cấp TLS không an toàn"), + ("allow-insecure-tls-fallback-tip", "Cho phép kết nối nếu máy chủ dùng TLS cũ."), + ("Disable UDP", "Tắt UDP"), + ("disable-udp-tip", "Chỉ sử dụng TCP để kết nối."), + ("server-oss-not-support-tip", "Máy chủ mã nguồn mở không hỗ trợ tính năng này."), + ("input note here", "nhập ghi chú tại đây"), + ("note-at-conn-end-tip", "Hiện ghi chú khi kết thúc phiên"), + ("Show terminal extra keys", "Hiện các phím phụ Terminal"), + ("Relative mouse mode", "Chế độ chuột tương đối"), + ("rel-mouse-not-supported-peer-tip", "Đối tác không hỗ trợ chuột tương đối."), + ("rel-mouse-not-ready-tip", "Chuột tương đối chưa sẵn sàng."), + ("rel-mouse-lock-failed-tip", "Khóa chuột thất bại."), + ("rel-mouse-exit-{}-tip", "Thoát chế độ chuột tương đối: {}"), + ("rel-mouse-permission-lost-tip", "Mất quyền điều khiển chuột tương đối."), + ("Changelog", "Nhật ký thay đổi"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Tiếp tục với {}"), + ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), + ].iter().cloned().collect(); +} diff --git a/vendor/rustdesk/src/lib.rs b/vendor/rustdesk/src/lib.rs new file mode 100644 index 0000000..c28da09 --- /dev/null +++ b/vendor/rustdesk/src/lib.rs @@ -0,0 +1,79 @@ +mod keyboard; +/// cbindgen:ignore +pub mod platform; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use platform::{ + clip_cursor, get_cursor, get_cursor_data, get_cursor_pos, get_focused_display, + set_cursor_pos, start_os_service, +}; +#[cfg(not(any(target_os = "ios")))] +/// cbindgen:ignore +mod server; +#[cfg(not(any(target_os = "ios")))] +pub use self::server::*; +mod client; +mod lan; +#[cfg(not(any(target_os = "ios")))] +mod rendezvous_mediator; +#[cfg(not(any(target_os = "ios")))] +pub use self::rendezvous_mediator::*; +/// cbindgen:ignore +pub mod common; +#[cfg(not(any(target_os = "ios")))] +pub mod ipc; +#[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" +)))] +pub mod ui; +mod version; +pub use version::*; +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +mod bridge_generated; +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +pub mod flutter; +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +pub mod flutter_ffi; +use common::*; +mod auth_2fa; +#[cfg(feature = "cli")] +pub mod cli; +#[cfg(not(target_os = "ios"))] +mod clipboard; +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +pub mod core_main; +pub mod custom_server; +mod lang; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod port_forward; + +#[cfg(all(feature = "flutter", feature = "plugin_framework"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod plugin; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod tray; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod whiteboard; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod updater; + +mod ui_cm_interface; +mod ui_interface; +mod ui_session_interface; + +mod hbbs_http; + +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +pub mod clipboard_file; + +pub mod privacy_mode; + +#[cfg(windows)] +pub mod virtual_display_manager; + +mod kcp_stream; diff --git a/vendor/rustdesk/src/main.rs b/vendor/rustdesk/src/main.rs new file mode 100644 index 0000000..c46ba98 --- /dev/null +++ b/vendor/rustdesk/src/main.rs @@ -0,0 +1,104 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +use librustdesk::*; + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +fn main() { + if !common::global_init() { + eprintln!("Global initialization failed."); + return; + } + common::test_rendezvous_server(); + common::test_nat_type(); + common::global_clean(); +} + +#[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" +)))] +fn main() { + #[cfg(all(windows, not(feature = "inline")))] + unsafe { + winapi::um::shellscalingapi::SetProcessDpiAwareness(2); + } + if let Some(args) = crate::core_main::core_main().as_mut() { + ui::start(args); + } + common::global_clean(); +} + +#[cfg(feature = "cli")] +fn main() { + if !common::global_init() { + return; + } + use clap::App; + use hbb_common::log; + let args = format!( + "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' + -c, --connect=[REMOTE_ID] 'test only' + -k, --key=[KEY] '' + -s, --server=[] 'Start server'", + ); + let matches = App::new("rustdesk") + .version(crate::VERSION) + .author("cStudio GmbH") + .about("RustDesk command line tool") + .args_from_usage(&args) + .get_matches(); + use hbb_common::{config::LocalConfig, env_logger::*}; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); + if let Some(p) = matches.value_of("port-forward") { + let options: Vec = p.split(":").map(|x| x.to_owned()).collect(); + if options.len() < 3 { + log::error!("Wrong port-forward options"); + return; + } + let mut port = 0; + if let Ok(v) = options[1].parse::() { + port = v; + } else { + log::error!("Wrong local-port"); + return; + } + let mut remote_port = 0; + if let Ok(v) = options[2].parse::() { + remote_port = v; + } else { + log::error!("Wrong remote-port"); + return; + } + let mut remote_host = "localhost".to_owned(); + if options.len() > 3 { + remote_host = options[3].clone(); + } + common::test_rendezvous_server(); + common::test_nat_type(); + let key = matches.value_of("key").unwrap_or("").to_owned(); + let token = LocalConfig::get_option("access_token"); + cli::start_one_port_forward( + options[0].clone(), + port, + remote_host, + remote_port, + key, + token, + ); + } else if let Some(p) = matches.value_of("connect") { + common::test_rendezvous_server(); + common::test_nat_type(); + let key = matches.value_of("key").unwrap_or("").to_owned(); + let token = LocalConfig::get_option("access_token"); + cli::connect_test(p, key, token); + } else if let Some(p) = matches.value_of("server") { + log::info!("id={}", hbb_common::config::Config::get_id()); + crate::start_server(true, false); + } + common::global_clean(); +} diff --git a/vendor/rustdesk/src/naming.rs b/vendor/rustdesk/src/naming.rs new file mode 100644 index 0000000..0436a23 --- /dev/null +++ b/vendor/rustdesk/src/naming.rs @@ -0,0 +1,28 @@ +mod custom_server; +use hbb_common::{ResultType, base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}}; +use custom_server::*; + +fn gen_name(lic: &CustomServer) -> ResultType { + let tmp = URL_SAFE_NO_PAD.encode(&serde_json::to_vec(lic)?); + Ok(tmp.chars().rev().collect()) +} + +fn main() { + let args: Vec<_> = std::env::args().skip(1).collect(); + let api = args.get(2).cloned().unwrap_or_default(); + let relay = args.get(3).cloned().unwrap_or_default(); + if args.len() >= 2 { + match gen_name(&CustomServer { + key: args[0].clone(), + host: args[1].clone(), + api, + relay, + }) { + Ok(name) => println!("rustdesk-custom_serverd-{}.exe", name), + Err(e) => println!("{:?}", e), + } + } + if args.len() == 1 { + println!("{:?}", get_custom_server_from_string(&args[0])); + } +} diff --git a/vendor/rustdesk/src/platform/delegate.rs b/vendor/rustdesk/src/platform/delegate.rs new file mode 100644 index 0000000..60a9ee5 --- /dev/null +++ b/vendor/rustdesk/src/platform/delegate.rs @@ -0,0 +1,277 @@ +use std::{ffi::c_void, rc::Rc}; + +#[cfg(target_os = "macos")] +use cocoa::{ + appkit::{NSApp, NSApplication, NSApplicationActivationPolicy::*, NSMenu, NSMenuItem}, + base::{id, nil, YES}, + foundation::{NSAutoreleasePool, NSString}, +}; +use objc::runtime::{Class, NO}; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Object, Sel, BOOL}, + sel, sel_impl, +}; +use sciter::{make_args, Host}; + +use hbb_common::log; + +static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; + +const TERMINATE_TAG: u32 = 0; +const SHOW_ABOUT_TAG: u32 = 1; +const SHOW_SETTINGS_TAG: u32 = 2; +const RUN_ME_TAG: u32 = 3; +const AWAKE: u32 = 4; + +pub trait AppHandler { + fn command(&mut self, cmd: u32); +} + +struct DelegateState { + handler: Option>, +} + +impl DelegateState { + fn command(&mut self, command: u32) { + if command == TERMINATE_TAG { + unsafe { + let () = msg_send!(NSApp(), terminate: nil); + } + } else if let Some(inner) = self.handler.as_mut() { + inner.command(command) + } + } +} + +static mut LAUNCHED: bool = false; + +impl AppHandler for Rc { + fn command(&mut self, cmd: u32) { + if cmd == SHOW_ABOUT_TAG { + let _ = self.call_function("awake", &make_args![]); + let _ = self.call_function("showAbout", &make_args![]); + } else if cmd == SHOW_SETTINGS_TAG { + let _ = self.call_function("awake", &make_args![]); + let _ = self.call_function("showSettings", &make_args![]); + } else if cmd == AWAKE { + let _ = self.call_function("awake", &make_args![]); + } + } +} + +// https://github.com/xi-editor/druid/blob/master/druid-shell/src/platform/mac/application.rs +unsafe fn set_delegate(handler: Option>) { + let Some(mut decl) = ClassDecl::new("AppDelegate", class!(NSObject)) else { + log::error!("Failed to new AppDelegate"); + return; + }; + decl.add_ivar::<*mut c_void>(APP_HANDLER_IVAR); + + decl.add_method( + sel!(applicationDidFinishLaunching:), + application_did_finish_launching as extern "C" fn(&mut Object, Sel, id), + ); + + decl.add_method( + sel!(applicationShouldOpenUntitledFile:), + application_should_handle_open_untitled_file as extern "C" fn(&mut Object, Sel, id) -> BOOL, + ); + + decl.add_method( + sel!(applicationDidBecomeActive:), + application_did_become_active as extern "C" fn(&mut Object, Sel, id) -> BOOL, + ); + + decl.add_method( + sel!(applicationDidUnhide:), + application_did_become_unhide as extern "C" fn(&mut Object, Sel, id) -> BOOL, + ); + + decl.add_method( + sel!(applicationShouldHandleReopen:), + application_should_handle_reopen as extern "C" fn(&mut Object, Sel, id) -> BOOL, + ); + + decl.add_method( + sel!(applicationWillTerminate:), + application_will_terminate as extern "C" fn(&mut Object, Sel, id) -> BOOL, + ); + + decl.add_method( + sel!(handleMenuItem:), + handle_menu_item as extern "C" fn(&mut Object, Sel, id), + ); + decl.add_method( + sel!(application:openURLs:), + handle_open_urls as extern "C" fn(&Object, Sel, id, id) -> (), + ); + let decl = decl.register(); + let delegate: id = msg_send![decl, alloc]; + let () = msg_send![delegate, init]; + let state = DelegateState { handler }; + let handler_ptr = Box::into_raw(Box::new(state)); + (*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void); + // Set the url scheme handler + let Some(cls) = Class::get("NSAppleEventManager") else { + log::error!("Failed to get NSAppleEventManager"); + return; + }; + let manager: *mut Object = msg_send![cls, sharedAppleEventManager]; + let _: () = msg_send![manager, + setEventHandler: delegate + andSelector: sel!(handleEvent:withReplyEvent:) + forEventClass: fruitbasket::kInternetEventClass + andEventID: fruitbasket::kAEGetURL]; + let () = msg_send![NSApp(), setDelegate: delegate]; +} + +extern "C" fn application_did_finish_launching(_this: &mut Object, _: Sel, _notification: id) { + unsafe { + LAUNCHED = true; + } + unsafe { + let () = msg_send![NSApp(), activateIgnoringOtherApps: YES]; + } +} + +extern "C" fn application_should_handle_open_untitled_file( + this: &mut Object, + _: Sel, + _sender: id, +) -> BOOL { + unsafe { + if !LAUNCHED { + return YES; + } + crate::platform::macos::handle_application_should_open_untitled_file(); + let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); + let inner = &mut *(inner as *mut DelegateState); + (*inner).command(AWAKE); + } + YES +} + +extern "C" fn application_should_handle_reopen(_this: &mut Object, _: Sel, _sender: id) -> BOOL { + YES +} + +extern "C" fn application_did_become_active(_this: &mut Object, _: Sel, _sender: id) -> BOOL { + YES +} + +extern "C" fn application_did_become_unhide(_this: &mut Object, _: Sel, _sender: id) -> BOOL { + YES +} + +extern "C" fn application_will_terminate(_this: &mut Object, _: Sel, _sender: id) -> BOOL { + YES +} + +/// This handles menu items in the case that all windows are closed. +extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { + unsafe { + let tag: isize = msg_send![item, tag]; + let tag = tag as u32; + if tag == RUN_ME_TAG { + crate::run_me(Vec::::new()).ok(); + } else { + let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); + let inner = &mut *(inner as *mut DelegateState); + (*inner).command(tag as u32); + } + } +} + +#[no_mangle] +extern "C" fn handle_open_urls(_self: &Object, _cmd: Sel, _: id, urls: id) -> () { + use cocoa::foundation::NSArray; + use cocoa::foundation::NSURL; + use std::ffi::CStr; + unsafe { + for i in 0..urls.count() { + let theurl = CStr::from_ptr(urls.objectAtIndex(i).absoluteString().UTF8String()) + .to_string_lossy() + .into_owned(); + log::debug!("URL received: {}", theurl); + std::thread::spawn(move || crate::handle_url_scheme(theurl)); + } + } +} + +// Customize the service opening logic. +#[no_mangle] +fn service_should_handle_reopen( + _obj: &Object, + _sel: Sel, + _sender: id, + _has_visible_windows: BOOL, +) -> BOOL { + log::debug!("Invoking the main rustdesk process"); + std::thread::spawn(move || crate::handle_url_scheme("".to_string())); + // Prevent default logic. + NO +} + +unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { + let title = NSString::alloc(nil).init_str(title); + let action = sel!(handleMenuItem:); + let key = NSString::alloc(nil).init_str(key); + let object = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_(title, action, key) + .autorelease(); + let () = msg_send![object, setTag: tag]; + object +} + +pub fn make_menubar(host: Rc, is_index: bool) { + unsafe { + let _pool = NSAutoreleasePool::new(nil); + set_delegate(Some(Box::new(host))); + let menubar = NSMenu::new(nil).autorelease(); + let app_menu_item = NSMenuItem::new(nil).autorelease(); + menubar.addItem_(app_menu_item); + let app_menu = NSMenu::new(nil).autorelease(); + + if !is_index { + let new_item = make_menu_item("New Window", "n", RUN_ME_TAG); + app_menu.addItem_(new_item); + } else { + // When app launched without argument, is the main panel. + let about_item = make_menu_item("About", "", SHOW_ABOUT_TAG); + app_menu.addItem_(about_item); + let separator = NSMenuItem::separatorItem(nil).autorelease(); + app_menu.addItem_(separator); + let settings_item = make_menu_item("Settings", "s", SHOW_SETTINGS_TAG); + app_menu.addItem_(settings_item); + } + let separator = NSMenuItem::separatorItem(nil).autorelease(); + app_menu.addItem_(separator); + let quit_item = make_menu_item( + &format!("Quit {}", crate::get_app_name()), + "q", + TERMINATE_TAG, + ); + app_menu_item.setSubmenu_(app_menu); + /* + if !enabled { + let () = msg_send![quit_item, setEnabled: NO]; + } + + if selected { + let () = msg_send![quit_item, setState: 1_isize]; + } + let () = msg_send![item, setTag: id as isize]; + */ + app_menu.addItem_(quit_item); + NSApp().setMainMenu_(menubar); + } +} + +pub fn show_dock() { + unsafe { + NSApp().setActivationPolicy_(NSApplicationActivationPolicyRegular); + } +} diff --git a/vendor/rustdesk/src/platform/gtk_sudo.rs b/vendor/rustdesk/src/platform/gtk_sudo.rs new file mode 100644 index 0000000..37b541c --- /dev/null +++ b/vendor/rustdesk/src/platform/gtk_sudo.rs @@ -0,0 +1,773 @@ +// https://github.com/aarnt/qt-sudo +// Sometimes reboot is needed to refresh sudoers. + +use crate::lang::translate; +use gtk::{glib, prelude::*}; +use hbb_common::{ + anyhow::{bail, Error}, + log, + platform::linux::CMD_SH, + ResultType, +}; +use nix::{ + libc::{fcntl, kill}, + pty::{forkpty, ForkptyResult}, + sys::{ + signal::Signal, + wait::{waitpid, WaitPidFlag}, + }, + unistd::{execvp, setsid, Pid}, +}; +use std::{ + ffi::CString, + fs::File, + io::{Read, Write}, + os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}, + sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, + }, +}; + +const EXIT_CODE: i32 = -1; + +enum Message { + PasswordPrompt((String, bool)), + Password((String, String)), + ErrorDialog(String), + Cancel, + Exit(i32), +} + +pub fn run(cmds: Vec<&str>) -> ResultType<()> { + // rustdesk service kill `rustdesk --` processes + let second_arg = std::env::args().nth(1).unwrap_or_default(); + let cmd_mode = + second_arg.starts_with("--") && second_arg != "--tray" && second_arg != "--no-server"; + let mod_arg = if cmd_mode { "cmd" } else { "gui" }; + let mut args = vec!["-gtk-sudo", mod_arg]; + args.append(&mut cmds.clone()); + let mut child = crate::run_me(args)?; + let exit_status = child.wait()?; + if exit_status.success() { + Ok(()) + } else { + bail!("child exited with status: {:?}", exit_status); + } +} + +pub fn exec() { + let mut args = vec![]; + for arg in std::env::args().skip(3) { + args.push(arg); + } + let cmd_mode = std::env::args().nth(2) == Some("cmd".to_string()); + if cmd_mode { + cmd(args); + } else { + ui(args); + } +} + +fn cmd(args: Vec) { + match unsafe { forkpty(None, None) } { + Ok(forkpty_result) => match forkpty_result { + ForkptyResult::Parent { child, master } => { + if let Err(e) = cmd_parent(child, master) { + log::error!("Parent error: {:?}", e); + kill_child(child); + std::process::exit(EXIT_CODE); + } + } + ForkptyResult::Child => { + if let Err(e) = child(None, args) { + log::error!("Child error: {:?}", e); + std::process::exit(EXIT_CODE); + } + } + }, + Err(err) => { + log::error!("forkpty error: {:?}", err); + std::process::exit(EXIT_CODE); + } + } +} + +fn ui(args: Vec) { + // https://docs.gtk.org/gtk4/ctor.Application.new.html + // https://docs.gtk.org/gio/type_func.Application.id_is_valid.html + let application = gtk::Application::new(None, Default::default()); + + let (tx_to_ui, rx_to_ui) = channel::(); + let (tx_from_ui, rx_from_ui) = channel::(); + + let rx_to_ui = Arc::new(Mutex::new(rx_to_ui)); + let tx_from_ui = Arc::new(Mutex::new(tx_from_ui)); + + let rx_to_ui_clone = rx_to_ui.clone(); + let tx_from_ui_clone = tx_from_ui.clone(); + + let username = Arc::new(Mutex::new(crate::platform::get_active_username())); + let username_clone = username.clone(); + + application.connect_activate(glib::clone!(@weak application =>move |_| { + let rx_to_ui = rx_to_ui_clone.clone(); + let tx_from_ui = tx_from_ui_clone.clone(); + let last_password = Arc::new(Mutex::new(String::new())); + let username = username_clone.clone(); + + glib::timeout_add_local(std::time::Duration::from_millis(50), move || { + if let Ok(msg) = rx_to_ui.lock().unwrap().try_recv() { + match msg { + Message::PasswordPrompt((err_msg, show_edit)) => { + let last_pwd = last_password.lock().unwrap().clone(); + let username = username.lock().unwrap().clone(); + if let Some((username, password)) = password_prompt(&username, &last_pwd, &err_msg, show_edit) { + *last_password.lock().unwrap() = password.clone(); + if let Err(e) = tx_from_ui + .lock() + .unwrap() + .send(Message::Password((username, password))) { + error_dialog_and_exit(&format!("Channel error: {e:?}"), EXIT_CODE); + } + } else { + if let Err(e) = tx_from_ui.lock().unwrap().send(Message::Cancel) { + error_dialog_and_exit(&format!("Channel error: {e:?}"), EXIT_CODE); + } + } + } + Message::ErrorDialog(err_msg) => { + error_dialog_and_exit(&err_msg, EXIT_CODE); + } + Message::Exit(code) => { + log::info!("Exit code: {}", code); + std::process::exit(code); + } + _ => {} + } + } + glib::ControlFlow::Continue + }); + })); + + let tx_to_ui_clone = tx_to_ui.clone(); + std::thread::spawn(move || { + let acitve_user = crate::platform::get_active_username(); + let mut initial_password = None; + if acitve_user != "root" { + if let Err(e) = tx_to_ui_clone.send(Message::PasswordPrompt(("".to_string(), true))) { + log::error!("Channel error: {e:?}"); + std::process::exit(EXIT_CODE); + } + match rx_from_ui.recv() { + Ok(Message::Password((user, password))) => { + *username.lock().unwrap() = user; + initial_password = Some(password); + } + Ok(Message::Cancel) => { + log::info!("User canceled"); + std::process::exit(EXIT_CODE); + } + _ => { + log::error!("Unexpected message"); + std::process::exit(EXIT_CODE); + } + } + } + let username = username.lock().unwrap().clone(); + let su_user = if username == acitve_user { + None + } else { + Some(username) + }; + match unsafe { forkpty(None, None) } { + Ok(forkpty_result) => match forkpty_result { + ForkptyResult::Parent { child, master } => { + if let Err(e) = ui_parent( + child, + master, + tx_to_ui_clone, + rx_from_ui, + su_user.is_some(), + initial_password, + ) { + log::error!("Parent error: {:?}", e); + kill_child(child); + std::process::exit(EXIT_CODE); + } + } + ForkptyResult::Child => { + if let Err(e) = child(su_user, args) { + log::error!("Child error: {:?}", e); + std::process::exit(EXIT_CODE); + } + } + }, + Err(err) => { + log::error!("forkpty error: {:?}", err); + if let Err(e) = + tx_to_ui.send(Message::ErrorDialog(format!("Forkpty error: {:?}", err))) + { + log::error!("Channel error: {e:?}"); + std::process::exit(EXIT_CODE); + } + } + } + }); + + let _holder = application.hold(); + let args: Vec<&str> = vec![]; + application.run_with_args(&args); + log::debug!("exit from gtk::Application::run_with_args"); + std::process::exit(EXIT_CODE); +} + +fn cmd_parent(child: Pid, master: OwnedFd) -> ResultType<()> { + let raw_fd = master.as_raw_fd(); + if unsafe { fcntl(raw_fd, nix::libc::F_SETFL, nix::libc::O_NONBLOCK) } != 0 { + let errno = std::io::Error::last_os_error(); + bail!("fcntl error: {errno:?}"); + } + let mut file = unsafe { File::from_raw_fd(raw_fd) }; + let mut stdout = std::io::stdout(); + let stdin = std::io::stdin(); + let stdin_fd = stdin.as_raw_fd(); + let old_termios = termios::Termios::from_fd(stdin_fd)?; + turn_off_echo(stdin_fd).ok(); + shutdown_hooks::add_shutdown_hook(turn_on_echo_shutdown_hook); + let (tx, rx) = channel::>(); + std::thread::spawn(move || loop { + let mut line = String::default(); + match stdin.read_line(&mut line) { + Ok(0) => { + kill_child(child); + break; + } + Ok(_) => { + if let Err(e) = tx.send(line.as_bytes().to_vec()) { + log::error!("Channel error: {e:?}"); + kill_child(child); + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {} + Err(e) => { + log::info!("Failed to read stdin: {e:?}"); + kill_child(child); + break; + } + }; + }); + loop { + let mut buf = [0; 1024]; + match file.read(&mut buf) { + Ok(0) => { + log::info!("read from child: EOF"); + break; + } + Ok(n) => { + let buf = String::from_utf8_lossy(&buf[..n]).to_string(); + print!("{}", buf); + if let Err(e) = stdout.flush() { + log::error!("flush failed: {e:?}"); + kill_child(child); + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Err(e) => { + // Child process is dead + log::info!("Read child error: {:?}", e); + break; + } + } + match rx.try_recv() { + Ok(v) => { + if let Err(e) = file.write_all(&v) { + log::error!("write error: {e:?}"); + kill_child(child); + break; + } + } + Err(e) => match e { + std::sync::mpsc::TryRecvError::Empty => {} + std::sync::mpsc::TryRecvError::Disconnected => { + log::error!("receive error: {e:?}"); + kill_child(child); + break; + } + }, + } + } + + // Wait for child process + let status = waitpid(child, None); + log::info!("waitpid status: {:?}", status); + let mut code = EXIT_CODE; + match status { + Ok(s) => match s { + nix::sys::wait::WaitStatus::Exited(_pid, status) => { + code = status; + } + _ => {} + }, + Err(_) => {} + } + termios::tcsetattr(stdin_fd, termios::TCSANOW, &old_termios).ok(); + std::process::exit(code); +} + +fn ui_parent( + child: Pid, + master: OwnedFd, + tx_to_ui: Sender, + rx_from_ui: Receiver, + is_su: bool, + initial_password: Option, +) -> ResultType<()> { + let mut initial_password = initial_password; + let raw_fd = master.as_raw_fd(); + if unsafe { fcntl(raw_fd, nix::libc::F_SETFL, nix::libc::O_NONBLOCK) } != 0 { + let errno = std::io::Error::last_os_error(); + tx_to_ui.send(Message::ErrorDialog(format!("fcntl error: {errno:?}")))?; + bail!("fcntl error: {errno:?}"); + } + let mut file = unsafe { File::from_raw_fd(raw_fd) }; + + let mut first = initial_password.is_none(); + let mut su_password_sent = false; + let mut saved_output = String::default(); + loop { + let mut buf = [0; 1024]; + match file.read(&mut buf) { + Ok(0) => { + log::info!("read from child: EOF"); + break; + } + Ok(n) => { + saved_output = String::default(); + let buf = String::from_utf8_lossy(&buf[..n]).trim().to_string(); + let last_line = buf.lines().last().unwrap_or(&buf).trim().to_string(); + log::info!("read from child: {}", buf); + + if last_line.starts_with("sudo:") || last_line.starts_with("su:") { + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(last_line)) { + log::error!("Channel error: {e:?}"); + kill_child(child); + } + break; + } else if last_line.ends_with(":") { + match get_echo_turn_off(raw_fd) { + Ok(true) => { + log::debug!("get_echo_turn_off ok"); + if let Some(password) = initial_password.clone() { + let v = format!("{}\n", password); + if let Err(e) = file.write_all(v.as_bytes()) { + let e = format!("Failed to send password: {e:?}"); + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(e)) { + log::error!("Channel error: {e:?}"); + } + kill_child(child); + break; + } + if is_su && !su_password_sent { + su_password_sent = true; + continue; + } + initial_password = None; + continue; + } + // In fact, su mode can only input password once + let err_msg = if first { "" } else { "Sorry, try again." }; + first = false; + if let Err(e) = + tx_to_ui.send(Message::PasswordPrompt((err_msg.to_string(), false))) + { + log::error!("Channel error: {e:?}"); + kill_child(child); + break; + } + match rx_from_ui.recv() { + Ok(Message::Password((_, password))) => { + let v = format!("{}\n", password); + if let Err(e) = file.write_all(v.as_bytes()) { + let e = format!("Failed to send password: {e:?}"); + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(e)) { + log::error!("Channel error: {e:?}"); + } + kill_child(child); + break; + } + } + Ok(Message::Cancel) => { + log::info!("User canceled"); + kill_child(child); + break; + } + _ => { + log::error!("Unexpected message"); + break; + } + } + } + Ok(false) => log::warn!("get_echo_turn_off timeout"), + Err(e) => log::error!("get_echo_turn_off error: {:?}", e), + } + } else { + saved_output = buf.clone(); + if !last_line.is_empty() && initial_password.is_some() { + log::error!("received not empty line: {last_line}, clear initial password"); + initial_password = None; + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(std::time::Duration::from_millis(50)); + } + Err(e) => { + // Child process is dead + log::debug!("Read error: {:?}", e); + break; + } + } + } + + // Wait for child process + let status = waitpid(child, None); + log::info!("waitpid status: {:?}", status); + let mut code = EXIT_CODE; + match status { + Ok(s) => match s { + nix::sys::wait::WaitStatus::Exited(_pid, status) => { + code = status; + } + _ => {} + }, + Err(_) => {} + } + + if code != 0 && !saved_output.is_empty() { + if let Err(e) = tx_to_ui.send(Message::ErrorDialog(saved_output.clone())) { + log::error!("Channel error: {e:?}"); + std::process::exit(code); + } + return Ok(()); + } + if let Err(e) = tx_to_ui.send(Message::Exit(code)) { + log::error!("Channel error: {e:?}"); + std::process::exit(code); + } + Ok(()) +} + +fn child(su_user: Option, args: Vec) -> ResultType<()> { + // https://doc.rust-lang.org/std/env/consts/constant.OS.html + let os = std::env::consts::OS; + let bsd = os == "freebsd" || os == "dragonfly" || os == "netbsd" || os == "openbsd"; + let mut params = vec!["sudo".to_string()]; + if su_user.is_some() { + params.push("-S".to_string()); + } + params.push(CMD_SH.to_string()); + params.push("-c".to_string()); + + let command = args + .iter() + .map(|s| { + if su_user.is_some() { + s.to_string() + } else { + quote_shell_arg(s, true) + } + }) + .collect::>() + .join(" "); + let mut command = if bsd { + let lc = match std::env::var("LC_ALL") { + Ok(lc_all) => { + if lc_all.contains('\'') { + eprintln!( + "sudo: Detected attempt to inject privileged command via LC_ALL env({lc_all}). Exiting!\n", + ); + std::process::exit(EXIT_CODE); + } + format!("LC_ALL='{lc_all}' ") + } + Err(_) => { + format!("unset LC_ALL;") + } + }; + format!("{}exec {}", lc, command) + } else { + command.to_string() + }; + if su_user.is_some() { + command = format!("'{}'", quote_shell_arg(&command, false)); + } + params.push(command); + std::env::set_var("LC_ALL", "C"); + + if let Some(user) = &su_user { + let su_subcommand = params + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(" "); + params = vec![ + "su".to_string(), + "-".to_string(), + user.to_string(), + "-c".to_string(), + su_subcommand, + ]; + } + + // allow failure here + let _ = setsid(); + let mut cparams = vec![]; + for param in ¶ms { + cparams.push(CString::new(param.as_str())?); + } + let su_or_sudo = if su_user.is_some() { "su" } else { "sudo" }; + let res = execvp(CString::new(su_or_sudo)?.as_c_str(), &cparams); + eprintln!("sudo: execvp error: {:?}", res); + std::process::exit(EXIT_CODE); +} + +fn get_echo_turn_off(fd: RawFd) -> Result { + let tios = termios::Termios::from_fd(fd)?; + for _ in 0..10 { + if tios.c_lflag & termios::ECHO == 0 { + return Ok(true); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + Ok(false) +} + +fn turn_off_echo(fd: RawFd) -> Result<(), Error> { + use termios::*; + let mut termios = Termios::from_fd(fd)?; + // termios.c_lflag &= !(ECHO | ECHONL | ICANON | IEXTEN); + termios.c_lflag &= !ECHO; + tcsetattr(fd, TCSANOW, &termios)?; + Ok(()) +} + +pub extern "C" fn turn_on_echo_shutdown_hook() { + let fd = std::io::stdin().as_raw_fd(); + if let Ok(mut termios) = termios::Termios::from_fd(fd) { + termios.c_lflag |= termios::ECHO; + termios::tcsetattr(fd, termios::TCSANOW, &termios).ok(); + } +} + +fn kill_child(child: Pid) { + unsafe { kill(child.as_raw(), Signal::SIGINT as _) }; + let mut res = 0; + + for _ in 0..10 { + match waitpid(child, Some(WaitPidFlag::WNOHANG)) { + Ok(_) => { + res = 1; + break; + } + Err(_) => (), + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + if res == 0 { + log::info!("Force killing child process"); + unsafe { kill(child.as_raw(), Signal::SIGKILL as _) }; + } +} + +fn password_prompt( + username: &str, + last_password: &str, + err: &str, + show_edit: bool, +) -> Option<(String, String)> { + let dialog = gtk::Dialog::builder() + .title(crate::get_app_name()) + .modal(true) + .build(); + // https://docs.gtk.org/gtk4/method.Dialog.set_default_response.html + dialog.set_default_response(gtk::ResponseType::Ok); + let content_area = dialog.content_area(); + + let label = gtk::Label::builder() + .label(translate("Authentication Required".to_string())) + .margin_top(10) + .build(); + content_area.add(&label); + + let image = gtk::Image::from_icon_name(Some("avatar-default-symbolic"), gtk::IconSize::Dialog); + image.set_margin_top(10); + content_area.add(&image); + + let user_label = gtk::Label::new(Some(username)); + let edit_button = gtk::Button::new(); + edit_button.set_relief(gtk::ReliefStyle::None); + let edit_icon = + gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button.into()); + edit_button.set_image(Some(&edit_icon)); + edit_button.set_can_focus(false); + let user_entry = gtk::Entry::new(); + user_entry.set_alignment(0.5); + user_entry.set_width_request(100); + let user_box = gtk::Box::new(gtk::Orientation::Horizontal, 5); + user_box.add(&user_label); + user_box.add(&edit_button); + user_box.add(&user_entry); + user_box.set_halign(gtk::Align::Center); + user_box.set_valign(gtk::Align::Center); + user_box.set_vexpand(true); + content_area.add(&user_box); + + edit_button.connect_clicked( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry=> move |_| { + let username = user_label.text().to_string(); + user_entry.set_text(&username); + user_label.hide(); + edit_button.hide(); + user_entry.show(); + user_entry.grab_focus(); + }), + ); + + let password_input = gtk::Entry::builder() + .visibility(false) + .input_purpose(gtk::InputPurpose::Password) + .placeholder_text(translate("Password".to_string())) + .margin_top(20) + .margin_start(30) + .margin_end(30) + .activates_default(true) + .text(last_password) + .build(); + password_input.set_alignment(0.5); + // https://docs.gtk.org/gtk3/signal.Entry.activate.html + password_input.connect_activate(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Ok); + })); + content_area.add(&password_input); + + user_entry.connect_focus_out_event( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry, @weak password_input => @default-return glib::Propagation::Proceed, move |_, _| { + let username = user_entry.text().to_string(); + user_label.set_text(&username); + user_entry.hide(); + user_label.show(); + edit_button.show(); + glib::Propagation::Proceed + }), + ); + user_entry.connect_activate( + glib::clone!(@weak user_label, @weak edit_button, @weak user_entry, @weak password_input => move |_| { + let username = user_entry.text().to_string(); + user_label.set_text(&username); + user_entry.hide(); + user_label.show(); + edit_button.show(); + password_input.grab_focus(); + }), + ); + + if !err.is_empty() { + let err_label = gtk::Label::new(None); + err_label.set_markup(&format!( + "{}", + err + )); + err_label.set_selectable(true); + content_area.add(&err_label); + } + + let cancel_button = gtk::Button::builder() + .label(translate("Cancel".to_string())) + .hexpand(true) + .build(); + cancel_button.connect_clicked(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Cancel); + })); + let authenticate_button = gtk::Button::builder() + .label(translate("Authenticate".to_string())) + .hexpand(true) + .build(); + authenticate_button.connect_clicked(glib::clone!(@weak dialog => move |_| { + dialog.response(gtk::ResponseType::Ok); + })); + let button_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .hexpand(true) + .homogeneous(true) + .spacing(10) + .margin_top(10) + .build(); + button_box.add(&cancel_button); + button_box.add(&authenticate_button); + content_area.add(&button_box); + + content_area.set_spacing(10); + content_area.set_border_width(10); + + dialog.set_width_request(400); + dialog.show_all(); + dialog.set_position(gtk::WindowPosition::Center); + dialog.set_keep_above(true); + password_input.grab_focus(); + user_entry.hide(); + if !show_edit { + edit_button.hide(); + } + dialog.check_resize(); + let response = dialog.run(); + dialog.hide(); + + if response == gtk::ResponseType::Ok { + let username = if user_entry.get_visible() { + user_entry.text().to_string() + } else { + user_label.text().to_string() + }; + Some((username, password_input.text().to_string())) + } else { + None + } +} + +fn error_dialog_and_exit(err_msg: &str, exit_code: i32) { + log::error!("Error dialog: {err_msg}, exit code: {exit_code}"); + let dialog = gtk::MessageDialog::builder() + .message_type(gtk::MessageType::Error) + .title(crate::get_app_name()) + .text("Error") + .secondary_text(err_msg) + .modal(true) + .buttons(gtk::ButtonsType::Ok) + .build(); + dialog.set_position(gtk::WindowPosition::Center); + dialog.set_keep_above(true); + dialog.run(); + dialog.close(); + std::process::exit(exit_code); +} + +fn quote_shell_arg(arg: &str, add_splash_if_match: bool) -> String { + let mut rv = arg.to_string(); + let re = hbb_common::regex::Regex::new("(\\s|[][!\"#$&'()*,;<=>?\\^`{}|~])"); + let Ok(re) = re else { + return rv; + }; + if re.is_match(arg) { + rv = rv.replace("'", "'\\''"); + if add_splash_if_match { + rv = format!("'{}'", rv); + } + } + rv +} diff --git a/vendor/rustdesk/src/platform/linux.rs b/vendor/rustdesk/src/platform/linux.rs new file mode 100644 index 0000000..7157da7 --- /dev/null +++ b/vendor/rustdesk/src/platform/linux.rs @@ -0,0 +1,2279 @@ +use super::{gtk_sudo, CursorData, ResultType}; +use desktop::Desktop; +pub use hbb_common::platform::linux::*; +use hbb_common::{ + allow_err, + anyhow::anyhow, + bail, + config::{keys::OPTION_ALLOW_LINUX_HEADLESS, 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 libxdo_sys::{self, xdo_t, Window}; +use std::{ + cell::RefCell, + ffi::{OsStr, OsString}, + path::{Path, PathBuf}, + process::{Child, Command}, + string::String, + sync::atomic::{AtomicBool, Ordering}, + sync::Arc, + time::{Duration, Instant}, +}; +use terminfo::{capability as cap, Database}; +use wallpaper; + +pub const PA_SAMPLE_RATE: u32 = 48000; +static mut UNMODIFIED: bool = true; + +const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"]; +const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"]; + +// Terminal type constants +const TERM_XTERM_256COLOR: &str = "xterm-256color"; +const TERM_SCREEN_256COLOR: &str = "screen-256color"; +const TERM_XTERM: &str = "xterm"; + +lazy_static::lazy_static! { + pub static ref IS_X11: bool = hbb_common::platform::linux::is_x11_or_headless(); + // Cache for TERM value - once TERM_XTERM_256COLOR is found, reuse it directly + static ref CACHED_TERM: std::sync::Mutex> = std::sync::Mutex::new(None); + static ref DATABASE_XTERM_256COLOR: Option = { + match Database::from_name(TERM_XTERM_256COLOR) { + Ok(database) => Some(database), + Err(err) => { + log::error!("Failed to initialize {} database: {}", TERM_XTERM_256COLOR, err); + None + } + } + }; + // https://github.com/rustdesk/rustdesk/issues/13705 + // Check if `sudo -E` actually preserves environment. + // + // This flag is only used by `run_as_user()` (root service -> user session). If the current process is not + // running as `root`, this check is meaningless (and `sudo -n` may fail), so we return `false` directly. + // + // On Ubuntu 25.10, `sudo -E` may still succeed but effectively ignores `-E`. Some versions print a warning + // to stderr (wording may vary by locale), so we verify behavior instead: + // - Inject a sentinel environment variable into the `sudo` process + // - Run `sudo -n -E env` and check whether the sentinel is present in stdout + static ref SUDO_E_PRESERVES_ENV: bool = { + if !is_root() { + log::warn!("Not running as root, SUDO_E_PRESERVES_ENV check skipped"); + false + } else { + let key = format!("__RUSTDESK_SUDO_E_TEST_{}", std::process::id()); + let val = "1"; + let expected = format!("{key}={val}"); + Command::new("sudo") + // -n for non-interactive to avoid password prompt + .env(&key, val) + .args(["-n", "-E", "env"]) + .output() + .map(|o| { + o.status.success() + && String::from_utf8_lossy(&o.stdout).contains(expected.as_str()) + }) + .unwrap_or(false) + } + }; +} + +thread_local! { + // XDO context - created via libxdo-sys (which uses dynamic loading stub). + // If libxdo is not available, xdo will be null and xdo-based functions become no-ops. + static XDO: RefCell<*mut xdo_t> = RefCell::new({ + let xdo = unsafe { libxdo_sys::xdo_new(std::ptr::null()) }; + if xdo.is_null() { + log::warn!("Failed to create xdo context, xdo functions will be disabled"); + } else { + log::info!("xdo context created successfully"); + } + xdo + }); + static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())}); +} + +// X11 error event structure for the custom error handler. +// See: https://www.x.org/releases/current/doc/libX11/libX11/libX11.html#Using-the-Default-Error-Handlers +#[repr(C)] +struct XErrorEvent { + type_: c_int, + display: *mut c_void, // Display* + resourceid: c_ulong, // XID + serial: c_ulong, + error_code: u8, + request_code: u8, + minor_code: u8, +} + +type XErrorHandler = unsafe extern "C" fn(*mut c_void, *mut XErrorEvent) -> c_int; + +const X11_BAD_WINDOW: u8 = 3; +const XDO_SUCCESS: c_int = 0; +const XDO_ERROR: c_int = 1; + +/// Atomic flag set by the custom X error handler when a BadWindow error occurs. +static X_BAD_WINDOW_DETECTED: AtomicBool = AtomicBool::new(false); +static X_UNEXPECTED_ERROR_DETECTED: AtomicBool = AtomicBool::new(false); + +/// Custom X error handler that catches BadWindow errors (error_code == 3) instead of +/// letting the default handler terminate the process. +/// See issue: https://github.com/rustdesk/rustdesk/issues/9003 +unsafe extern "C" fn handle_x_error(_display: *mut c_void, event: *mut XErrorEvent) -> c_int { + if !event.is_null() && (*event).error_code == X11_BAD_WINDOW { + X_BAD_WINDOW_DETECTED.store(true, Ordering::SeqCst); + log::debug!("Caught X11 BadWindow error (suppressed), window was likely destroyed"); + return 0; + } + X_UNEXPECTED_ERROR_DETECTED.store(true, Ordering::SeqCst); + if !event.is_null() { + log::warn!( + "X11 error: error_code={}, request_code={}, minor_code={}", + (*event).error_code, + (*event).request_code, + (*event).minor_code, + ); + } + 0 +} + +#[link(name = "X11")] +extern "C" { + fn XOpenDisplay(display_name: *const c_char) -> *mut c_void; + // fn XCloseDisplay(d: *mut c_void) -> c_int; + fn XSetErrorHandler(handler: Option) -> Option; +} + +#[link(name = "Xfixes")] +extern "C" { + // fn XFixesQueryExtension(dpy: *mut c_void, event: *mut c_int, error: *mut c_int) -> c_int; + fn XFixesGetCursorImage(dpy: *mut c_void) -> *const xcb_xfixes_get_cursor_image; + fn XFree(data: *mut c_void); +} + +// /usr/include/X11/extensions/Xfixes.h +#[repr(C)] +pub struct xcb_xfixes_get_cursor_image { + pub x: i16, + pub y: i16, + pub width: u16, + pub height: u16, + pub xhot: u16, + pub yhot: u16, + pub cursor_serial: c_long, + pub pixels: *const c_long, +} + +#[inline] +pub fn is_headless_allowed() -> bool { + Config::get_option(OPTION_ALLOW_LINUX_HEADLESS) == "Y" +} + +#[inline] +pub fn is_login_screen_wayland() -> bool { + let values = get_values_of_seat0_with_gdm_wayland(&[0, 2]); + is_gdm_user(&values[1]) && get_display_server_of_session(&values[0]) == DISPLAY_SERVER_WAYLAND +} + +#[inline] +fn sleep_millis(millis: u64) { + std::thread::sleep(Duration::from_millis(millis)); +} + +pub fn get_cursor_pos() -> Option<(i32, i32)> { + let mut res = None; + XDO.with(|xdo| { + if let Ok(xdo) = xdo.try_borrow() { + if xdo.is_null() { + return; + } + let mut x: c_int = 0; + let mut y: c_int = 0; + unsafe { + libxdo_sys::xdo_get_mouse_location( + *xdo as *const _, + &mut x as _, + &mut y as _, + std::ptr::null_mut(), + ); + } + res = Some((x, y)); + } + }); + res +} + +pub fn set_cursor_pos(x: i32, y: i32) -> bool { + let mut res = false; + XDO.with(|xdo| { + match xdo.try_borrow() { + Ok(xdo) => { + if xdo.is_null() { + log::debug!("set_cursor_pos: xdo is null"); + return; + } + unsafe { + let ret = libxdo_sys::xdo_move_mouse(*xdo as *const _, x, y, 0); + if ret != 0 { + log::debug!( + "set_cursor_pos: xdo_move_mouse failed with code {} for coordinates ({}, {})", + ret, x, y + ); + } + res = ret == 0; + } + } + Err(_) => { + log::debug!("set_cursor_pos: failed to borrow xdo"); + } + } + }); + res +} + +/// Clip cursor - Linux implementation is a no-op. +/// +/// On X11, there's no direct equivalent to Windows ClipCursor. XGrabPointer +/// can confine the pointer but requires a window handle and has side effects. +/// +/// On Wayland, pointer constraints require the zwp_pointer_constraints_v1 +/// protocol which is compositor-dependent. +/// +/// For relative mouse mode on Linux, the Flutter side uses pointer warping +/// (set_cursor_pos) to re-center the cursor after each movement, which achieves +/// a similar effect without requiring cursor clipping. +/// +/// Returns true (always succeeds as no-op). +pub fn clip_cursor(_rect: Option<(i32, i32, i32, i32)>) -> bool { + // Log only once per process to avoid flooding logs when called frequently. + static LOGGED: AtomicBool = AtomicBool::new(false); + if !LOGGED.swap(true, Ordering::Relaxed) { + log::debug!("clip_cursor called (no-op on Linux, this message is logged only once)"); + } + true +} + +pub fn reset_input_cache() {} + +pub fn get_focused_display(displays: Vec) -> Option { + let mut res = None; + XDO.with(|xdo| { + if let Ok(xdo) = xdo.try_borrow() { + if xdo.is_null() { + return; + } + let mut x: c_int = 0; + let mut y: c_int = 0; + let mut width: c_uint = 0; + let mut height: c_uint = 0; + let mut window: Window = 0; + + unsafe { + if libxdo_sys::xdo_get_active_window(*xdo as *const _, &mut window) != 0 { + return; + } + + // XSetErrorHandler is process-global, not scoped to this Display/thread. + // This path is currently called by the single window_focus service thread. + // While installed, this handler can still observe unrelated X11 errors from + // other threads; unexpected errors make this geometry query fail. + X_BAD_WINDOW_DETECTED.store(false, Ordering::SeqCst); + X_UNEXPECTED_ERROR_DETECTED.store(false, Ordering::SeqCst); + let prev_handler = XSetErrorHandler(Some(handle_x_error)); + + let loc_ret = libxdo_sys::xdo_get_window_location( + *xdo as *const _, + window, + &mut x as _, + &mut y as _, + std::ptr::null_mut(), + ); + let size_ret = if loc_ret == XDO_SUCCESS { + libxdo_sys::xdo_get_window_size( + *xdo as *const _, + window, + &mut width, + &mut height, + ) + } else { + XDO_ERROR + }; + + // Do not call XSync(DISPLAY) here: DISPLAY is a separate + // XOpenDisplay() connection, while libxdo owns the Display* + // used by these geometry queries. These libxdo calls are + // synchronous XGetWindowAttributes-based queries, so the target + // BadWindow is expected to be delivered before the calls return. + XSetErrorHandler(prev_handler); + if X_BAD_WINDOW_DETECTED.load(Ordering::SeqCst) + || X_UNEXPECTED_ERROR_DETECTED.load(Ordering::SeqCst) + || loc_ret != XDO_SUCCESS + || size_ret != XDO_SUCCESS + { + return; + } + + let center_x = x + (width / 2) as c_int; + let center_y = y + (height / 2) as c_int; + res = displays.iter().position(|d| { + center_x >= d.x + && center_x < d.x + d.width + && center_y >= d.y + && center_y < d.y + d.height + }); + } + } + }); + res +} + +pub fn get_cursor() -> ResultType> { + let mut res = None; + DISPLAY.with(|conn| { + if let Ok(d) = conn.try_borrow_mut() { + if !d.is_null() { + unsafe { + let img = XFixesGetCursorImage(*d); + if !img.is_null() { + res = Some((*img).cursor_serial as u64); + XFree(img as _); + } + } + } + } + }); + Ok(res) +} + +pub fn get_cursor_data(hcursor: u64) -> ResultType { + let mut res = None; + DISPLAY.with(|conn| { + if let Ok(ref mut d) = conn.try_borrow_mut() { + if !d.is_null() { + unsafe { + let img = XFixesGetCursorImage(**d); + if !img.is_null() && hcursor == (*img).cursor_serial as u64 { + let mut cd: CursorData = Default::default(); + cd.hotx = (*img).xhot as _; + cd.hoty = (*img).yhot as _; + cd.width = (*img).width as _; + cd.height = (*img).height as _; + // to-do: how about if it is 0 + cd.id = (*img).cursor_serial as _; + let pixels = + std::slice::from_raw_parts((*img).pixels, (cd.width * cd.height) as _); + // cd.colors.resize(pixels.len() * 4, 0); + let mut cd_colors = vec![0_u8; pixels.len() * 4]; + for y in 0..cd.height { + for x in 0..cd.width { + let pos = (y * cd.width + x) as usize; + let p = pixels[pos]; + let a = (p >> 24) & 0xff; + let r = (p >> 16) & 0xff; + let g = (p >> 8) & 0xff; + let b = (p >> 0) & 0xff; + if a == 0 { + continue; + } + let pos = pos * 4; + cd_colors[pos] = r as _; + cd_colors[pos + 1] = g as _; + cd_colors[pos + 2] = b as _; + cd_colors[pos + 3] = a as _; + } + } + cd.colors = cd_colors.into(); + res = Some(cd); + } + if !img.is_null() { + XFree(img as _); + } + } + } + } + }); + match res { + Some(x) => Ok(x), + _ => bail!("Failed to get cursor image of {}", hcursor), + } +} + +fn start_uinput_service() { + use crate::server::uinput::service; + std::thread::spawn(|| { + service::start_service_control(); + }); + std::thread::spawn(|| { + service::start_service_keyboard(); + }); + std::thread::spawn(|| { + service::start_service_mouse(); + }); +} + +/// Suggests the best terminal type based on the environment. +/// +/// The function prioritizes terminal types in the following order: +/// 1. `screen-256color`: Preferred when running inside `tmux` or `screen` sessions, +/// as these multiplexers often support advanced terminal features. +/// 2. `xterm-256color`: Selected if the terminal supports 256 colors, which is +/// suitable for modern terminal applications. +/// 3. `xterm`: Used as a fallback for basic terminal compatibility. +/// +/// Terminals like `linux` and `vt100` are excluded because they lack support for +/// modern features required by many applications. +fn suggest_best_term() -> String { + if is_running_in_tmux() || is_running_in_screen() { + return TERM_SCREEN_256COLOR.to_string(); + } + if term_supports_256_colors(TERM_XTERM_256COLOR) { + return TERM_XTERM_256COLOR.to_string(); + } + TERM_XTERM.to_string() +} + +fn is_running_in_tmux() -> bool { + std::env::var("TMUX").is_ok() +} + +fn is_running_in_screen() -> bool { + std::env::var("STY").is_ok() +} + +fn supports_256_colors(db: &Database) -> bool { + db.get::().map_or(false, |n| n.0 >= 256) +} + +fn term_supports_256_colors(term: &str) -> bool { + match term { + TERM_XTERM_256COLOR => DATABASE_XTERM_256COLOR + .as_ref() + .map_or(false, |db| supports_256_colors(db)), + _ => Database::from_name(term).map_or(false, |db| supports_256_colors(&db)), + } +} + +fn get_cur_term(uid: &str) -> Option { + // Check cache first - if TERM_XTERM_256COLOR was found before, reuse it + if let Ok(cache) = CACHED_TERM.lock() { + if let Some(ref cached) = *cache { + if cached == TERM_XTERM_256COLOR { + return Some(cached.clone()); + } + } + } + + if uid.is_empty() { + return None; + } + + // Check current process environment + if let Ok(term) = std::env::var("TERM") { + if term == TERM_XTERM_256COLOR { + if let Ok(mut cache) = CACHED_TERM.lock() { + *cache = Some(term.clone()); + } + return Some(term); + } + } + + // Collect all TERM values from shell processes, looking for TERM_XTERM_256COLOR + let terms = get_all_term_values(uid); + + // Prefer TERM_XTERM_256COLOR + if terms.iter().any(|t| t == TERM_XTERM_256COLOR) { + if let Ok(mut cache) = CACHED_TERM.lock() { + *cache = Some(TERM_XTERM_256COLOR.to_string()); + } + return Some(TERM_XTERM_256COLOR.to_string()); + } + + // Return first valid TERM if no TERM_XTERM_256COLOR found + let fallback = terms.into_iter().next(); + if let Some(ref term) = fallback { + log::debug!( + "TERM_XTERM_256COLOR not found, using fallback TERM: {}", + term + ); + } + fallback +} + +/// Get all TERM values from shell processes (bash, zsh, fish, sh). +/// Returns a Vec of unique, valid TERM values. +fn get_all_term_values(uid: &str) -> Vec { + let Ok(uid_num) = uid.parse::() else { + return Vec::new(); + }; + + // Build regex pattern to match shell processes using only argv[0] (the executable path) + // Pattern: match process name at start or after '/', followed by space or end + // e.g., "bash", "/bin/bash", "/usr/bin/zsh" + let shell_pattern = SHELL_PROCESSES + .iter() + .map(|p| format!(r"(^|/){p}(\s|$)")) + .collect::>() + .join("|"); + let Ok(re) = Regex::new(&shell_pattern) else { + return Vec::new(); + }; + + let Ok(entries) = std::fs::read_dir("/proc") else { + return Vec::new(); + }; + + let mut terms = Vec::new(); + + for entry in entries.flatten() { + let file_name = entry.file_name(); + let Some(pid_str) = file_name.to_str() else { + continue; + }; + if !pid_str.chars().all(|c| c.is_ascii_digit()) { + continue; + } + + let proc_path = entry.path(); + + // Check if process belongs to the specified uid + if let Ok(meta) = std::fs::metadata(&proc_path) { + use std::os::unix::fs::MetadataExt; + if meta.uid() != uid_num { + continue; + } + } else { + continue; + } + + // Check cmdline matches process pattern + // /proc//cmdline is a sequence of null-terminated strings; the first + // one (argv[0]) is the executable path. Match the regex only against that + // to avoid false positives from arguments (e.g., "python /path/to/bash-script.py"). + let cmdline_path = proc_path.join("cmdline"); + let Ok(cmdline) = std::fs::read(&cmdline_path) else { + continue; + }; + let exe_end = cmdline + .iter() + .position(|&b| b == 0) + .unwrap_or(cmdline.len()); + let exe_str = String::from_utf8_lossy(&cmdline[..exe_end]); + if !re.is_match(&exe_str) { + continue; + } + + // Read environ and extract TERM + let environ_path = proc_path.join("environ"); + let Ok(environ) = std::fs::read(&environ_path) else { + continue; + }; + + for part in environ.split(|&b| b == 0) { + if part.is_empty() { + continue; + } + if let Some(eq) = part.iter().position(|&b| b == b'=') { + let key_bytes = &part[..eq]; + if key_bytes == b"TERM" { + let val_bytes = &part[eq + 1..]; + let term = String::from_utf8_lossy(val_bytes).into_owned(); + if !INVALID_TERM_VALUES.contains(&term.as_str()) && !terms.contains(&term) { + // Early return if we found the preferred term + if term == TERM_XTERM_256COLOR { + return vec![term]; + } + terms.push(term); + } + break; + } + } + } + } + + terms +} + +#[inline] +fn try_start_server_(desktop: Option<&Desktop>) -> ResultType> { + match desktop { + Some(desktop) => { + let mut envs = vec![]; + if !desktop.display.is_empty() { + envs.push(("DISPLAY", desktop.display.clone())); + } + if !desktop.xauth.is_empty() { + envs.push(("XAUTHORITY", desktop.xauth.clone())); + } + if !desktop.wl_display.is_empty() { + envs.push(("WAYLAND_DISPLAY", desktop.wl_display.clone())); + } + if !desktop.home.is_empty() { + envs.push(("HOME", desktop.home.clone())); + } + if !desktop.dbus.is_empty() { + envs.push(("DBUS_SESSION_BUS_ADDRESS", desktop.dbus.clone())); + } + envs.push(( + "TERM", + get_cur_term(&desktop.uid).unwrap_or_else(|| suggest_best_term()), + )); + run_as_user( + vec!["--server"], + Some((desktop.uid.clone(), desktop.username.clone())), + envs, + ) + } + None => Ok(Some(crate::run_me(vec!["--server"])?)), + } +} + +#[inline] +fn start_server(desktop: Option<&Desktop>, server: &mut Option) { + match try_start_server_(desktop) { + Ok(ps) => *server = ps, + Err(err) => { + log::error!("Failed to start server: {}", err); + } + } +} + +fn stop_server(server: &mut Option) { + if let Some(mut ps) = server.take() { + allow_err!(ps.kill()); + sleep_millis(30); + match ps.try_wait() { + Ok(Some(_status)) => {} + Ok(None) => { + let _res = ps.wait(); + } + Err(e) => log::error!("error attempting to wait: {e}"), + } + } +} + +fn set_x11_env(desktop: &Desktop) { + log::info!("DISPLAY: {}", desktop.display); + log::info!("XAUTHORITY: {}", desktop.xauth); + if !desktop.display.is_empty() { + std::env::set_var("DISPLAY", &desktop.display); + } + if !desktop.xauth.is_empty() { + std::env::set_var("XAUTHORITY", &desktop.xauth); + } +} + +#[inline] +fn stop_rustdesk_servers() { + let _ = run_cmds(&format!( + r##"ps -ef | grep -E '{} +--server' | awk '{{print $2}}' | xargs -r kill -9"##, + crate::get_app_name().to_lowercase(), + )); +} + +#[inline] +fn stop_subprocess() { + let _ = run_cmds(&format!( + r##"ps -ef | grep '/etc/{}/xorg.conf' | grep -v grep | awk '{{print $2}}' | xargs -r kill -9"##, + crate::get_app_name().to_lowercase(), + )); + let _ = run_cmds(&format!( + r##"ps -ef | grep -E '{} +--cm-no-ui' | grep -v grep | awk '{{print $2}}' | xargs -r kill -9"##, + crate::get_app_name().to_lowercase(), + )); +} + +fn should_start_server( + try_x11: bool, + is_display_changed: bool, + uid: &mut String, + desktop: &Desktop, + cm0: &mut bool, + last_restart: &mut Instant, + server: &mut Option, +) -> bool { + let cm = get_cm(); + let mut start_new = false; + let mut should_kill = false; + + if desktop.is_headless() { + if !uid.is_empty() { + // From having a monitor to not having a monitor. + *uid = "".to_owned(); + should_kill = true; + } + } else if is_display_changed || desktop.uid != *uid && !desktop.uid.is_empty() { + *uid = desktop.uid.clone(); + if try_x11 { + set_x11_env(&desktop); + } + should_kill = true; + } + + if !should_kill + && !cm + && ((*cm0 && last_restart.elapsed().as_secs() > 60) + || last_restart.elapsed().as_secs() > 3600) + { + let terminal_session_count = crate::ipc::get_terminal_session_count().unwrap_or(0); + if terminal_session_count > 0 { + // There are terminal sessions, so we don't restart the server. + // We also need to keep `cm0` unchanged, so that we can reach this branch the next time. + return false; + } + // restart server if new connections all closed, or every one hour, + // as a workaround to resolve "SpotUdp" (dns resolve) + // and x server get displays failure issue + should_kill = true; + log::info!("restart server"); + } + + if should_kill { + if let Some(ps) = server.as_mut() { + allow_err!(ps.kill()); + sleep_millis(30); + *last_restart = Instant::now(); + } + } + + if let Some(ps) = server.as_mut() { + match ps.try_wait() { + Ok(Some(_)) => { + *server = None; + start_new = true; + } + _ => {} + } + } else { + start_new = true; + } + *cm0 = cm; + start_new +} + +// to-do: stop_server(&mut user_server); may not stop child correctly +// stop_rustdesk_servers() is just a temp solution here. +fn force_stop_server() { + stop_rustdesk_servers(); + sleep_millis(super::SERVICE_INTERVAL); +} + +pub fn start_os_service() { + check_if_stop_service(); + stop_rustdesk_servers(); + stop_subprocess(); + start_uinput_service(); + + std::thread::spawn(|| { + allow_err!(crate::ipc::start(crate::POSTFIX_SERVICE)); + }); + + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + let (mut display, mut xauth): (String, String) = ("".to_owned(), "".to_owned()); + let mut desktop = Desktop::default(); + let mut sid = "".to_owned(); + let mut uid = "".to_owned(); + let mut server: Option = None; + let mut user_server: Option = None; + if let Err(err) = ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) { + println!("Failed to set Ctrl-C handler: {}", err); + } + + let mut cm0 = false; + let mut last_restart = Instant::now(); + while running.load(Ordering::SeqCst) { + desktop.refresh(); + + // Duplicate logic here with should_start_server + // Login wayland will try to start a headless --server. + if desktop.username == "root" || desktop.is_login_wayland() { + // try kill subprocess "--server" + stop_server(&mut user_server); + // try start subprocess "--server" + // No need to check is_display_changed here. + if should_start_server( + true, + false, + &mut uid, + &desktop, + &mut cm0, + &mut last_restart, + &mut server, + ) { + stop_subprocess(); + force_stop_server(); + start_server(None, &mut server); + } + } else if desktop.username != "" { + // try kill subprocess "--server" + stop_server(&mut server); + + let is_display_changed = desktop.display != display || desktop.xauth != xauth; + display = desktop.display.clone(); + xauth = desktop.xauth.clone(); + + // try start subprocess "--server" + if should_start_server( + !desktop.is_wayland(), + is_display_changed, + &mut uid, + &desktop, + &mut cm0, + &mut last_restart, + &mut user_server, + ) { + stop_subprocess(); + force_stop_server(); + start_server(Some(&desktop), &mut user_server); + } + } else { + force_stop_server(); + stop_server(&mut user_server); + stop_server(&mut server); + } + + let keeps_headless = sid.is_empty() && desktop.is_headless(); + let keeps_session = sid == desktop.sid; + if keeps_headless || keeps_session { + // for fixing https://github.com/rustdesk/rustdesk/issues/3129 to avoid too much dbus calling, + sleep_millis(500); + } else { + sleep_millis(super::SERVICE_INTERVAL); + } + if !desktop.is_headless() { + sid = desktop.sid.clone(); + } + } + + if let Some(ps) = user_server.take().as_mut() { + allow_err!(ps.kill()); + } + if let Some(ps) = server.take().as_mut() { + allow_err!(ps.kill()); + } + log::info!("Exit"); +} + +#[inline] +pub fn get_active_user_id_name() -> (String, String) { + let vec_id_name = get_values_of_seat0(&[1, 2]); + (vec_id_name[0].clone(), vec_id_name[1].clone()) +} + +#[inline] +pub fn get_active_userid() -> String { + get_values_of_seat0(&[1])[0].clone() +} + +fn get_cm() -> bool { + // We use `CMD_PS` instead of `ps` to suppress some audit messages on some systems. + if let Ok(output) = Command::new(CMD_PS.as_str()).args(vec!["aux"]).output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if line.contains(&format!( + "{} --cm", + std::env::current_exe() + .unwrap_or("".into()) + .to_string_lossy() + )) { + return true; + } + } + } + false +} + +pub fn is_login_wayland() -> bool { + let files = ["/etc/gdm3/custom.conf", "/etc/gdm/custom.conf"]; + match ( + Regex::new(r"# *WaylandEnable *= *false"), + Regex::new(r"WaylandEnable *= *true"), + ) { + (Ok(pat1), Ok(pat2)) => { + for file in files { + if let Ok(contents) = std::fs::read_to_string(file) { + return pat1.is_match(&contents) || pat2.is_match(&contents); + } + } + } + _ => {} + } + false +} + +#[inline] +pub fn current_is_wayland() -> bool { + return is_desktop_wayland() && unsafe { UNMODIFIED }; +} + +// to-do: test the other display manager +fn _get_display_manager() -> String { + if let Ok(x) = std::fs::read_to_string("/etc/X11/default-display-manager") { + if let Some(x) = x.split("/").last() { + return x.to_owned(); + } + } + "gdm3".to_owned() +} + +#[inline] +pub fn get_active_username() -> String { + get_values_of_seat0(&[2])[0].clone() +} + +pub fn get_user_home_by_name(username: &str) -> Option { + return match get_user_by_name(username) { + None => None, + Some(user) => { + let home = user.home_dir(); + if Path::is_dir(home) { + Some(PathBuf::from(home)) + } else { + None + } + } + }; +} + +pub fn get_active_user_home() -> Option { + let username = get_active_username(); + if !username.is_empty() { + match get_user_home_by_name(&username) { + None => { + // fallback to most common default pattern + let home = PathBuf::from(format!("/home/{}", username)); + if home.exists() { + return Some(home); + } + } + Some(home) => { + return Some(home); + } + } + } + None +} + +pub fn get_env_var(k: &str) -> String { + match std::env::var(k) { + Ok(v) => v, + Err(_e) => "".to_owned(), + } +} + +fn is_flatpak() -> bool { + std::path::PathBuf::from("/.flatpak-info").exists() +} + +// Headless is enabled, always return true. +pub fn is_prelogin() -> bool { + if is_flatpak() { + return false; + } + let name = get_active_username(); + if let Ok(res) = run_cmds(&format!("getent passwd {}", name)) { + return res.contains("/bin/false") || res.contains("/usr/sbin/nologin"); + } + false +} + +// Check "Lock". +// "Switch user" can't be checked, because `get_values_of_seat0(&[0])` does not return the session. +// The logged in session is "online" not "active". +// And the "Switch user" screen is usually Wayland login session, which we do not support. +pub fn is_locked() -> bool { + if is_prelogin() { + return false; + } + + let values = get_values_of_seat0(&[0]); + // Though the values can't be empty, we still add check here for safety. + // Because we cannot guarantee whether the internal implementation will change in the future. + // https://github.com/rustdesk/hbb_common/blob/ebb4d4a48cf7ed6ca62e93f8ed124065c6408536/src/platform/linux.rs#L119 + if values.is_empty() { + log::debug!("Failed to check is locked, values vector is empty."); + return false; + } + let session = &values[0]; + if session.is_empty() { + log::debug!("Failed to check is locked, session is empty."); + return false; + } + is_session_locked(session) +} + +pub fn is_root() -> bool { + crate::username() == "root" +} + +fn is_opensuse() -> bool { + if let Ok(res) = run_cmds("cat /etc/os-release | grep opensuse") { + if !res.is_empty() { + return true; + } + } + false +} + +pub fn run_as_user( + arg: Vec<&str>, + user: Option<(String, String)>, + envs: I, +) -> ResultType> +where + I: IntoIterator, + K: AsRef, + V: AsRef, +{ + let (uid, username) = match user { + Some(id_name) => id_name, + None => get_active_user_id_name(), + }; + let cmd = std::env::current_exe()?; + if uid.is_empty() { + bail!("No valid uid"); + } + + let xdg = &format!("XDG_RUNTIME_DIR=/run/user/{uid}"); + if *SUDO_E_PRESERVES_ENV { + // Original logic: use sudo -E to preserve environment + let mut args = vec![xdg, "-u", &username, cmd.to_str().unwrap_or("")]; + args.append(&mut arg.clone()); + // -E is required to preserve env + args.insert(0, "-E"); + let task = Command::new("sudo").envs(envs).args(args).spawn()?; + Ok(Some(task)) + } else { + // Fallback: sudo -u username env VAR=VALUE ... cmd args + // For systems where sudo -E is not supported (e.g., Ubuntu 25.10+) + // + // SECURITY: No shell is involved here (we use execve-style argv). + // Environment is passed via `env` arguments, + // so there is no shell injection vector. + // + // Only accept portable env var names (POSIX portable character set for shells). + // Most legitimate env vars follow [A-Za-z_][A-Za-z0-9_]* convention. + // Variables with dots (e.g., "java.home") are Java system properties, not env vars. + // Being restrictive here is intentional for security in this sudo context. + fn is_valid_env_key(key: &str) -> bool { + let mut it = key.chars(); + match it.next() { + Some(c) if c.is_ascii_alphabetic() || c == '_' => {} + _ => return false, + } + it.all(|c| c.is_ascii_alphanumeric() || c == '_') + } + + let mut sudo = Command::new("sudo"); + sudo.arg("-u").arg(&username).arg("--").arg("env").arg(xdg); + + for (k, v) in envs { + let key = k.as_ref().to_string_lossy(); + if !is_valid_env_key(&key) { + log::warn!("Skipping environment variable with invalid key: '{}'. Only [A-Za-z_][A-Za-z0-9_]* are allowed in sudo context.", key); + continue; + } + // IMPORTANT: do NOT add shell quotes here; `Command` does not invoke a shell. + // Passing KEY=VALUE as a single argv element is safe and preserves spaces. + let mut arg = OsString::from(&*key); + arg.push("="); + arg.push(v.as_ref()); + sudo.arg(arg); + } + + sudo.arg(cmd).args(arg); + let task = sudo.spawn()?; + Ok(Some(task)) + } +} + +pub fn get_pa_monitor() -> String { + get_pa_sources() + .drain(..) + .map(|x| x.0) + .filter(|x| x.contains("monitor")) + .next() + .unwrap_or("".to_owned()) +} + +pub fn get_pa_source_name(desc: &str) -> String { + get_pa_sources() + .drain(..) + .filter(|x| x.1 == desc) + .map(|x| x.0) + .next() + .unwrap_or("".to_owned()) +} + +pub fn get_pa_sources() -> Vec<(String, String)> { + use pulsectl::controllers::*; + let mut out = Vec::new(); + match SourceController::create() { + Ok(mut handler) => { + if let Ok(devices) = handler.list_devices() { + for dev in devices.clone() { + out.push(( + dev.name.unwrap_or("".to_owned()), + dev.description.unwrap_or("".to_owned()), + )); + } + } + } + Err(err) => { + log::error!("Failed to get_pa_sources: {:?}", err); + } + } + out +} + +pub fn get_default_pa_source() -> Option<(String, String)> { + use pulsectl::controllers::*; + match SourceController::create() { + Ok(mut handler) => { + if let Ok(dev) = handler.get_default_device() { + return Some(( + dev.name.unwrap_or("".to_owned()), + dev.description.unwrap_or("".to_owned()), + )); + } + } + Err(err) => { + log::error!("Failed to get_pa_source: {:?}", err); + } + } + None +} + +pub fn lock_screen() { + Command::new("xdg-screensaver").arg("lock").spawn().ok(); +} + +pub fn toggle_blank_screen(_v: bool) { + // https://unix.stackexchange.com/questions/17170/disable-keyboard-mouse-input-on-unix-under-x +} + +pub fn block_input(_v: bool) -> (bool, String) { + (true, "".to_owned()) +} + +pub fn is_installed() -> bool { + if let Ok(p) = std::env::current_exe() { + p.to_str().unwrap_or_default().starts_with("/usr") + || p.to_str().unwrap_or_default().starts_with("/nix/store") + } else { + false + } +} + +/// Get multiple environment variables from a process matching the given criteria. +/// This version reads /proc directly instead of spawning shell commands. +/// +/// # Arguments +/// * `uid` - User ID to filter processes +/// * `process_pat` - Regex pattern to match process cmdline +/// * `names` - Environment variable names to retrieve. **Must be <= 64 elements** due to +/// the internal bitmask used for tie-breaking. +/// +/// # Panics (debug builds) +/// Panics if `names.len() > 64`. +/// +/// # Implementation notes +/// - Returns values from a *single* best-matching process_pat (for consistency). +/// - Avoids repeated scanning by parsing `environ` once per process. +fn get_envs<'a>( + uid: &str, + process_pat: &str, + names: &[&'a str], +) -> std::collections::HashMap<&'a str, String> { + // The tie-breaking logic uses a u64 bitmask, limiting us to 64 variables. + debug_assert!( + names.len() <= 64, + "get_envs: names.len() must be <= 64, got {}", + names.len() + ); + + let empty: std::collections::HashMap<&'a str, String> = + names.iter().map(|&n| (n, String::new())).collect(); + + let Ok(uid_num) = uid.parse::() else { + return empty; + }; + let Ok(re) = Regex::new(process_pat) else { + return empty; + }; + + // Used for stable tie-breaking when multiple processes match. + // Higher bits correspond to earlier entries in `names`. + let name_indices: std::collections::HashMap<&'a str, usize> = + names.iter().enumerate().map(|(i, &n)| (n, i)).collect(); + + let mut best = empty.clone(); + let mut best_count = 0usize; + let mut best_mask: u64 = 0; + + // Iterate /proc to find matching processes + let Ok(entries) = std::fs::read_dir("/proc") else { + return best; + }; + + for entry in entries.flatten() { + let file_name = entry.file_name(); + let Some(pid_str) = file_name.to_str() else { + continue; + }; + if !pid_str.chars().all(|c| c.is_ascii_digit()) { + continue; + } + + let proc_path = entry.path(); + + // Check if process belongs to the specified uid + if let Ok(meta) = std::fs::metadata(&proc_path) { + use std::os::unix::fs::MetadataExt; + if meta.uid() != uid_num { + continue; + } + } else { + continue; + } + + // Check cmdline matches process pattern + let cmdline_path = proc_path.join("cmdline"); + let Ok(cmdline) = std::fs::read(&cmdline_path) else { + continue; + }; + let cmdline_str = String::from_utf8_lossy(&cmdline).replace('\0', " "); + if !re.is_match(&cmdline_str) { + continue; + } + + // Read environ and extract matching variables + let environ_path = proc_path.join("environ"); + let Ok(environ) = std::fs::read(&environ_path) else { + continue; + }; + + let mut found = empty.clone(); + let mut found_count = 0usize; + let mut found_mask: u64 = 0; + + for part in environ.split(|&b| b == 0) { + if part.is_empty() { + continue; + } + let Some(eq) = part.iter().position(|&b| b == b'=') else { + continue; + }; + let key_bytes = &part[..eq]; + let val_bytes = &part[eq + 1..]; + + let Ok(key) = std::str::from_utf8(key_bytes) else { + continue; + }; + if let Some(slot) = found.get_mut(key) { + if slot.is_empty() { + *slot = String::from_utf8_lossy(val_bytes).into_owned(); + found_count += 1; + + if let Some(&idx) = name_indices.get(key) { + let total = names.len(); + if total <= 64 { + let bit = 1u64 << (total - 1 - idx); + found_mask |= bit; + } + } + + if found_count == names.len() { + return found; + } + } + } + } + + if found_count > best_count || (found_count == best_count && found_mask > best_mask) { + best = found; + best_count = found_count; + best_mask = found_mask; + } + } + + best +} + +/// Deprecated: Use `get_envs` instead. +/// +/// https://github.com/rustdesk/rustdesk/discussions/11959 +/// +/// **Note**: This function is retained for conservative migration. The plan is to gradually +/// transition all callers to `get_envs` after it proves stable and reliable. Once `get_envs` +/// is confirmed to work correctly across all use cases, this function will be removed entirely. +/// +/// # Arguments +/// * `name` - Environment variable name to retrieve +/// * `uid` - User ID to filter processes +/// * `process` - Process name pattern to match +/// +/// # Returns +/// The environment variable value, or empty string if not found +#[inline] +fn get_env(name: &str, uid: &str, process: &str) -> String { + let cmd = format!("ps -u {} -f | grep -E '{}' | grep -v 'grep' | tail -1 | awk '{{print $2}}' | xargs -I__ cat /proc/__/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", uid, process, name, name); + if let Ok(x) = run_cmds(&cmd) { + x.trim_end().to_string() + } else { + "".to_owned() + } +} + +#[inline] +fn get_env_from_pid(name: &str, pid: &str) -> String { + let cmd = format!("cat /proc/{}/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", pid, name, name); + if let Ok(x) = run_cmds(&cmd) { + x.trim_end().to_string() + } else { + "".to_owned() + } +} + +#[link(name = "gtk-3")] +extern "C" { + fn gtk_main_quit(); +} + +pub fn quit_gui() { + unsafe { gtk_main_quit() }; +} + +/* +pub fn exec_privileged(args: &[&str]) -> ResultType { + Ok(Command::new("pkexec").args(args).spawn()?) +} +*/ + +pub fn check_super_user_permission() -> ResultType { + gtk_sudo::run(vec!["echo"])?; + Ok(true) +} + +/* +pub fn elevate(args: Vec<&str>) -> ResultType { + let cmd = std::env::current_exe()?; + match cmd.to_str() { + Some(cmd) => { + let mut args_with_exe = vec![cmd]; + args_with_exe.append(&mut args.clone()); + // -E required for opensuse + if is_opensuse() { + args_with_exe.insert(0, "-E"); + } + let res = match exec_privileged(&args_with_exe)?.wait() { + Ok(status) => { + if status.success() { + true + } else { + log::error!( + "Failed to wait install process, process status: {:?}", + status + ); + false + } + } + Err(e) => { + log::error!("Failed to wait install process, error: {}", e); + false + } + }; + Ok(res) + } + None => { + hbb_common::bail!("Failed to get current exe as str"); + } + } +} +*/ + +type GtkSettingsPtr = *mut c_void; +type GObjectPtr = *mut c_void; +#[link(name = "gtk-3")] +extern "C" { + // fn gtk_init(argc: *mut c_int, argv: *mut *mut c_char); + fn gtk_settings_get_default() -> GtkSettingsPtr; +} + +#[link(name = "gobject-2.0")] +extern "C" { + fn g_object_get(object: GObjectPtr, first_property_name: *const c_char, ...); +} + +pub fn get_double_click_time() -> u32 { + // GtkSettings *settings = gtk_settings_get_default (); + // g_object_get (settings, "gtk-double-click-time", &double_click_time, NULL); + unsafe { + let mut double_click_time = 0u32; + let Ok(property) = std::ffi::CString::new("gtk-double-click-time") else { + return 0; + }; + let settings = gtk_settings_get_default(); + g_object_get( + settings, + property.as_ptr(), + &mut double_click_time as *mut u32, + 0 as *const c_void, + ); + double_click_time + } +} + +#[inline] +fn get_width_height_from_captures<'t>(caps: &Captures<'t>) -> Option<(i32, i32)> { + match (caps.name("width"), caps.name("height")) { + (Some(width), Some(height)) => { + match ( + width.as_str().parse::(), + height.as_str().parse::(), + ) { + (Ok(width), Ok(height)) => { + return Some((width, height)); + } + _ => {} + } + } + _ => {} + } + None +} + +#[inline] +fn get_xrandr_conn_pat(name: &str) -> String { + format!( + r"{}\s+connected.+?(?P\d+)x(?P\d+)\+(?P\d+)\+(?P\d+).*?\n", + name + ) +} + +pub fn resolutions(name: &str) -> Vec { + let resolutions_pat = r"(?P(\s*\d+x\d+\s+\d+.*\n)+)"; + let connected_pat = get_xrandr_conn_pat(name); + let mut v = vec![]; + if let Ok(re) = Regex::new(&format!("{}{}", connected_pat, resolutions_pat)) { + match run_cmds("xrandr --query | tr -s ' '") { + Ok(xrandr_output) => { + // There'are different kinds of xrandr output. + /* + 1. + Screen 0: minimum 320 x 175, current 1920 x 1080, maximum 1920 x 1080 + default connected 1920x1080+0+0 0mm x 0mm + 1920x1080 10.00* + 1280x720 25.00 + 1680x1050 60.00 + Virtual2 disconnected (normal left inverted right x axis y axis) + Virtual3 disconnected (normal left inverted right x axis y axis) + + Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384 + eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 344mm x 193mm + 1920x1080 60.01*+ 60.01 59.97 59.96 59.93 + 1680x1050 59.95 59.88 + 1600x1024 60.17 + + XWAYLAND0 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm + Virtual1 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm + HDMI-0 connected (normal left inverted right x axis y axis) + + rdp0 connected primary 1920x1080+0+0 0mm x 0mm + */ + if let Some(caps) = re.captures(&xrandr_output) { + if let Some(resolutions) = caps.name("resolutions") { + let resolution_pat = + r"\s*(?P\d+)x(?P\d+)\s+(?P(\d+\.\d+\D*)+)\s*\n"; + let Ok(resolution_re) = Regex::new(&format!(r"{}", resolution_pat)) else { + log::error!("Regex new failed"); + return vec![]; + }; + for resolution_caps in resolution_re.captures_iter(resolutions.as_str()) { + if let Some((width, height)) = + get_width_height_from_captures(&resolution_caps) + { + let resolution = Resolution { + width, + height, + ..Default::default() + }; + if !v.contains(&resolution) { + v.push(resolution); + } + } + } + } + } + } + Err(e) => log::error!("Failed to run xrandr query, {}", e), + } + } + + v +} + +pub fn current_resolution(name: &str) -> ResultType { + let xrandr_output = run_cmds("xrandr --query | tr -s ' '")?; + let re = Regex::new(&get_xrandr_conn_pat(name))?; + if let Some(caps) = re.captures(&xrandr_output) { + if let Some((width, height)) = get_width_height_from_captures(&caps) { + return Ok(Resolution { + width, + height, + ..Default::default() + }); + } + } + bail!("Failed to find current resolution for {}", name); +} + +pub fn change_resolution_directly(name: &str, width: usize, height: usize) -> ResultType<()> { + Command::new("xrandr") + .args(vec![ + "--output", + name, + "--mode", + &format!("{}x{}", width, height), + ]) + .spawn()?; + Ok(()) +} + +#[inline] +pub fn is_xwayland_running() -> bool { + if let Ok(output) = run_cmds("pgrep -a Xwayland") { + return output.contains("Xwayland"); + } + false +} + +mod desktop { + use super::*; + + pub const XFCE4_PANEL: &str = "xfce4-panel"; + pub const SDDM_GREETER: &str = "sddm-greeter"; + + // xdg-desktop-portal runs on all Wayland desktops (GNOME, KDE, wlroots, etc.) + const XDG_DESKTOP_PORTAL: &str = "xdg-desktop-portal"; + const XWAYLAND: &str = "Xwayland"; + const IBUS_DAEMON: &str = "ibus-daemon"; + const PLASMA_KDED: &str = "kded[0-9]+"; + const GNOME_GOA_DAEMON: &str = "goa-daemon"; + + const ENV_KEY_DISPLAY: &str = "DISPLAY"; + const ENV_KEY_XAUTHORITY: &str = "XAUTHORITY"; + const ENV_KEY_WAYLAND_DISPLAY: &str = "WAYLAND_DISPLAY"; + const ENV_KEY_DBUS_SESSION_BUS_ADDRESS: &str = "DBUS_SESSION_BUS_ADDRESS"; + + #[derive(Debug, Clone, Default)] + pub struct Desktop { + pub sid: String, + pub username: String, + pub uid: String, + pub protocol: String, + pub display: String, + pub xauth: String, + pub home: String, + pub dbus: String, + pub is_rustdesk_subprocess: bool, + pub wl_display: String, + } + + impl Desktop { + #[inline] + pub fn is_wayland(&self) -> bool { + self.protocol == DISPLAY_SERVER_WAYLAND + } + + #[inline] + pub fn is_login_wayland(&self) -> bool { + super::is_gdm_user(&self.username) && self.protocol == DISPLAY_SERVER_WAYLAND + } + + #[inline] + pub fn is_headless(&self) -> bool { + self.sid.is_empty() || self.is_rustdesk_subprocess + } + + fn get_display_xauth_wayland(&mut self) { + for _ in 1..=10 { + // Prefer Wayland-related variables first when multiple portal processes match. + let mut envs = get_envs( + &self.uid, + XDG_DESKTOP_PORTAL, + &[ + ENV_KEY_WAYLAND_DISPLAY, + ENV_KEY_DBUS_SESSION_BUS_ADDRESS, + ENV_KEY_DISPLAY, + ENV_KEY_XAUTHORITY, + ], + ); + self.display = envs.remove(ENV_KEY_DISPLAY).unwrap_or_default(); + self.xauth = envs.remove(ENV_KEY_XAUTHORITY).unwrap_or_default(); + self.wl_display = envs.remove(ENV_KEY_WAYLAND_DISPLAY).unwrap_or_default(); + self.dbus = envs + .remove(ENV_KEY_DBUS_SESSION_BUS_ADDRESS) + .unwrap_or_default(); + // For pure Wayland sessions, prefer `WAYLAND_DISPLAY`. + // NOTE: On some systems (e.g. Ubuntu 25.10), `DISPLAY`/`XAUTHORITY` may exist even when XWayland + // is not running, so do NOT treat them as a success condition here. + let has_wayland = !self.wl_display.is_empty(); + let has_dbus = !self.dbus.is_empty(); + if has_wayland && has_dbus { + return; + } + sleep_millis(300); + } + } + + fn get_display_xauth_xwayland(&mut self) { + let tray = format!("{} +--tray", crate::get_app_name().to_lowercase()); + for _ in 1..=10 { + let display_proc = vec![ + XDG_DESKTOP_PORTAL, + XWAYLAND, + IBUS_DAEMON, + GNOME_GOA_DAEMON, + PLASMA_KDED, + tray.as_str(), + ]; + for proc in display_proc { + self.display = get_env(ENV_KEY_DISPLAY, &self.uid, proc); + self.xauth = get_env(ENV_KEY_XAUTHORITY, &self.uid, proc); + self.wl_display = get_env(ENV_KEY_WAYLAND_DISPLAY, &self.uid, proc); + self.dbus = get_env(ENV_KEY_DBUS_SESSION_BUS_ADDRESS, &self.uid, proc); + if !self.display.is_empty() && !self.xauth.is_empty() { + return; + } + } + sleep_millis(300); + } + } + + fn get_display_x11(&mut self) { + for _ in 1..=10 { + let display_proc = vec![ + XWAYLAND, + IBUS_DAEMON, + GNOME_GOA_DAEMON, + PLASMA_KDED, + XFCE4_PANEL, + SDDM_GREETER, + ]; + for proc in display_proc { + self.display = get_env(ENV_KEY_DISPLAY, &self.uid, proc); + if !self.display.is_empty() { + break; + } + } + if !self.display.is_empty() { + break; + } + sleep_millis(300); + } + + if self.display.is_empty() { + self.display = Self::get_display_by_user(&self.username); + } + if self.display.is_empty() { + self.display = ":0".to_owned(); + } + self.display = self + .display + .replace(&hbb_common::whoami::hostname(), "") + .replace("localhost", ""); + } + + fn get_home(&mut self) { + self.home = "".to_string(); + + let cmd = format!( + "getent passwd '{}' | awk -F':' '{{print $6}}'", + &self.username + ); + self.home = run_cmds_trim_newline(&cmd).unwrap_or(format!("/home/{}", &self.username)); + } + + fn get_xauth_from_xorg(&mut self) { + if let Ok(output) = run_cmds(&format!( + "ps -u {} -f | grep 'Xorg' | grep -v 'grep'", + &self.uid + )) { + for line in output.lines() { + let mut auth_found = false; + + for v in line.split_whitespace() { + if v == "-auth" { + auth_found = true; + } else if auth_found { + if std::path::Path::new(v).is_absolute() + && std::path::Path::new(v).exists() + { + self.xauth = v.to_string(); + } else { + if let Some(pid) = line.split_whitespace().nth(1) { + let mut base_dir: String = String::from("/home"); // default pattern + let home_dir = get_env_from_pid("HOME", pid); + if home_dir.is_empty() { + if let Some(home) = get_user_home_by_name(&self.username) { + base_dir = home.as_path().to_string_lossy().to_string(); + }; + } else { + base_dir = home_dir; + } + if Path::new(&base_dir).exists() { + self.xauth = format!("{}/{}", base_dir, v); + }; + } else { + // unreachable! + } + } + return; + } + } + } + } + } + + fn get_xauth_x11(&mut self) { + // try by direct access to window manager process by name + let tray = format!("{} +--tray", crate::get_app_name().to_lowercase()); + for _ in 1..=10 { + let display_proc = vec![ + XWAYLAND, + IBUS_DAEMON, + GNOME_GOA_DAEMON, + PLASMA_KDED, + XFCE4_PANEL, + SDDM_GREETER, + tray.as_str(), + ]; + for proc in display_proc { + self.xauth = get_env("XAUTHORITY", &self.uid, proc); + if !self.xauth.is_empty() { + break; + } + } + if !self.xauth.is_empty() { + break; + } + sleep_millis(300); + } + + // get from Xorg process, parameter and environment + if self.xauth.is_empty() { + self.get_xauth_from_xorg(); + } + + // fallback to default file name + if self.xauth.is_empty() { + let gdm = format!("/run/user/{}/gdm/Xauthority", self.uid); + self.xauth = if std::path::Path::new(&gdm).exists() { + gdm + } else { + let username = &self.username; + match get_user_home_by_name(username) { + None => { + if username == "root" { + format!("/{}/.Xauthority", username) + } else { + let tmp = format!("/home/{}/.Xauthority", username); + if std::path::Path::new(&tmp).exists() { + tmp + } else { + format!("/var/lib/{}/.Xauthority", username) + } + } + } + Some(home) => { + format!( + "{}/.Xauthority", + home.as_path().to_string_lossy().to_string() + ) + } + } + }; + } + } + + fn get_display_by_user(user: &str) -> String { + // log::debug!("w {}", &user); + if let Ok(output) = std::process::Command::new("w").arg(&user).output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + let mut iter = line.split_whitespace(); + let b = iter.nth(2); + if let Some(b) = b { + if b.starts_with(":") { + return b.to_owned(); + } + } + } + } + // above not work for gdm user + //log::debug!("ls -l /tmp/.X11-unix/"); + let mut last = "".to_owned(); + if let Ok(output) = std::process::Command::new("ls") + .args(vec!["-l", "/tmp/.X11-unix/"]) + .output() + { + for line in String::from_utf8_lossy(&output.stdout).lines() { + let mut iter = line.split_whitespace(); + let user_field = iter.nth(2); + if let Some(x) = iter.last() { + if x.starts_with("X") { + last = x.replace("X", ":").to_owned(); + if user_field == Some(&user) { + return last; + } + } + } + } + } + last + } + + fn set_is_subprocess(&mut self) { + self.is_rustdesk_subprocess = false; + let cmd = format!( + "ps -ef | grep '{}/xorg.conf' | grep -v grep | wc -l", + crate::get_app_name().to_lowercase() + ); + if let Ok(res) = run_cmds(&cmd) { + if res.trim() != "0" { + self.is_rustdesk_subprocess = true; + } + } + } + + pub fn refresh(&mut self) { + if !self.sid.is_empty() && is_active_and_seat0(&self.sid) { + // Xwayland display and xauth may not be available in a short time after login. + if is_xwayland_running() && !self.is_login_wayland() { + self.get_display_xauth_xwayland(); + self.is_rustdesk_subprocess = false; + } else if self.is_wayland() { + self.get_display_xauth_wayland(); + } + return; + } + + let seat0_values = get_values_of_seat0_with_gdm_wayland(&[0, 1, 2]); + if seat0_values[0].is_empty() { + *self = Self::default(); + self.is_rustdesk_subprocess = false; + return; + } + + self.sid = seat0_values[0].clone(); + self.uid = seat0_values[1].clone(); + self.username = seat0_values[2].clone(); + self.protocol = get_display_server_of_session(&self.sid).into(); + if self.is_login_wayland() { + self.display = "".to_owned(); + self.xauth = "".to_owned(); + self.is_rustdesk_subprocess = false; + return; + } + + self.get_home(); + if self.is_wayland() { + if is_xwayland_running() { + self.get_display_xauth_xwayland(); + } else { + self.get_display_xauth_wayland(); + } + self.is_rustdesk_subprocess = false; + } else { + self.get_display_x11(); + self.get_xauth_x11(); + self.set_is_subprocess(); + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_desktop_env() { + let mut d = Desktop::default(); + d.refresh(); + if d.username == "root" { + assert_eq!(d.home, "/root"); + } else { + if !d.username.is_empty() { + let home = super::super::get_env_var("HOME"); + if !home.is_empty() { + assert_eq!(d.home, home); + } else { + // + } + } + } + } + } +} + +pub struct WakeLock(Option); + +impl WakeLock { + pub fn new(display: bool, idle: bool, sleep: bool) -> Self { + WakeLock( + keepawake::Builder::new() + .display(display) + .idle(idle) + .sleep(sleep) + .create() + .ok(), + ) + } +} + +fn has_cmd(cmd: &str) -> bool { + std::process::Command::new("which") + .arg(cmd) + .status() + .map(|x| x.success()) + .unwrap_or_default() +} + +pub fn run_cmds_privileged(cmds: &str) -> bool { + crate::platform::gtk_sudo::run(vec![cmds]).is_ok() +} + +/// Spawn the current executable after a delay. +/// +/// # Security +/// The executable path is safely quoted using `shell_quote()` to prevent +/// command injection vulnerabilities. The `secs` parameter is a u32, so it +/// cannot contain malicious input. +/// +/// # Arguments +/// * `secs` - Number of seconds to wait before spawning +pub fn run_me_with(secs: u32) { + let exe = match std::env::current_exe() { + Ok(path) => path, + Err(e) => { + log::error!("Failed to get current exe: {}", e); + return; + } + }; + + // SECURITY: Use shell_quote to safely escape the executable path, + // preventing command injection even if the path contains special characters. + let exe_quoted = shell_quote(&exe.to_string_lossy()); + + // Spawn a background process that sleeps and then executes. + // The child process is automatically orphaned when parent exits, + // and will be adopted by init (PID 1). + Command::new(CMD_SH.as_str()) + .arg("-c") + .arg(&format!("sleep {secs}; exec {exe_quoted}")) + .spawn() + .ok(); +} + +fn switch_service(stop: bool) -> String { + // SECURITY: Use trusted home directory lookup via getpwuid instead of $HOME env var + // to prevent confused-deputy attacks where an attacker manipulates environment variables. + let home = get_home_dir_trusted() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_default(); + Config::set_option("stop-service".into(), if stop { "Y" } else { "" }.into()); + if !home.is_empty() && home != "/root" && !Config::get().is_empty() { + let app_name_lower = crate::get_app_name().to_lowercase(); + let app_name0 = crate::get_app_name(); + let config_subdir = format!(".config/{}", app_name_lower); + + // SECURITY: Quote all paths to prevent shell injection from paths containing + // spaces, semicolons, or other special characters. + let src1 = shell_quote(&format!("{}/{}/{}.toml", home, config_subdir, app_name0)); + let src2 = shell_quote(&format!("{}/{}/{}2.toml", home, config_subdir, app_name0)); + let dst = shell_quote(&format!("/root/{}/", config_subdir)); + + format!("cp -f {} {}; cp -f {} {};", src1, dst, src2, dst) + } else { + "".to_owned() + } +} + +pub fn uninstall_service(show_new_window: bool, _: bool) -> bool { + if !has_cmd("systemctl") { + // Failed when installed + flutter run + started by `show_new_window`. + return false; + } + log::info!("Uninstalling service..."); + let cp = switch_service(true); + let app_name = crate::get_app_name().to_lowercase(); + // systemctl kill rustdesk --tray, execute cp first + if !run_cmds_privileged(&format!( + "{cp} systemctl disable {app_name}; systemctl stop {app_name};" + )) { + Config::set_option("stop-service".into(), "".into()); + return true; + } + // systemctl stop will kill child processes, below may not be executed. + if show_new_window { + run_me_with(2); + } + std::process::exit(0); +} + +pub fn install_service() -> bool { + let _installing = crate::platform::InstallingService::new(); + if !has_cmd("systemctl") { + return false; + } + log::info!("Installing service..."); + let cp = switch_service(false); + let app_name = crate::get_app_name().to_lowercase(); + if !run_cmds_privileged(&format!( + "{cp} systemctl enable {app_name}; systemctl start {app_name};" + )) { + Config::set_option("stop-service".into(), "Y".into()); + } + true +} + +fn check_if_stop_service() { + if Config::get_option("stop-service".into()) == "Y" { + let app_name = crate::get_app_name().to_lowercase(); + allow_err!(run_cmds(&format!( + "systemctl disable {app_name}; systemctl stop {app_name}" + ))); + } +} + +pub fn check_autostart_config() -> ResultType<()> { + // SECURITY: Use trusted home directory lookup via getpwuid instead of $HOME env var + // to prevent confused-deputy attacks where an attacker manipulates environment variables. + let home = match get_home_dir_trusted() { + Some(p) => p.to_string_lossy().to_string(), + None => { + log::warn!("Failed to get trusted home directory for autostart config check"); + return Ok(()); + } + }; + let app_name = crate::get_app_name().to_lowercase(); + let path = format!("{home}/.config/autostart"); + let file = format!("{path}/{app_name}.desktop"); + // https://github.com/rustdesk/rustdesk/issues/4863 + std::fs::remove_file(&file).ok(); + /* + std::fs::create_dir_all(&path).ok(); + if !Path::new(&file).exists() { + // write text to the desktop file + let mut file = std::fs::File::create(&file)?; + file.write_all( + format!( + " + [Desktop Entry] + Type=Application + Exec={app_name} --tray + NoDisplay=false + " + ) + .as_bytes(), + )?; + } + */ + Ok(()) +} + +pub struct WallPaperRemover { + old_path: String, + old_path_dark: Option, // ubuntu 22.04 light/dark theme have different uri +} + +impl WallPaperRemover { + pub fn new() -> ResultType { + let start = std::time::Instant::now(); + let old_path = wallpaper::get().map_err(|e| anyhow!(e.to_string()))?; + let old_path_dark = wallpaper::get_dark().ok(); + if old_path.is_empty() && old_path_dark.clone().unwrap_or_default().is_empty() { + bail!("already solid color"); + } + wallpaper::set_from_path("").map_err(|e| anyhow!(e.to_string()))?; + wallpaper::set_dark_from_path("").ok(); + log::info!( + "created wallpaper remover, old_path: {:?}, old_path_dark: {:?}, elapsed: {:?}", + old_path, + old_path_dark, + start.elapsed(), + ); + Ok(Self { + old_path, + old_path_dark, + }) + } + + pub fn support() -> bool { + let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); + if wallpaper::gnome::is_compliant(&desktop) || desktop.as_str() == "XFCE" { + return wallpaper::get().is_ok(); + } + false + } +} + +impl Drop for WallPaperRemover { + fn drop(&mut self) { + allow_err!(wallpaper::set_from_path(&self.old_path).map_err(|e| anyhow!(e.to_string()))); + if let Some(old_path_dark) = &self.old_path_dark { + allow_err!(wallpaper::set_dark_from_path(old_path_dark.as_str()) + .map_err(|e| anyhow!(e.to_string()))); + } + } +} + +#[inline] +pub fn is_x11() -> bool { + *IS_X11 +} + +#[inline] +pub fn is_selinux_enforcing() -> bool { + match run_cmds("getenforce") { + Ok(output) => output.trim() == "Enforcing", + Err(_) => match run_cmds("sestatus") { + Ok(output) => { + for line in output.lines() { + if line.contains("Current mode:") { + return line.contains("enforcing"); + } + } + false + } + Err(_) => false, + }, + } +} + +/// Get the app ID for shortcuts inhibitor permission. +/// Returns different ID based on whether running in Flatpak or native. +/// The ID must match the installed .desktop filename, as GNOME Shell's +/// inhibitShortcutsDialog uses `Shell.WindowTracker.get_window_app(window).get_id()`. +fn get_shortcuts_inhibitor_app_id() -> String { + if is_flatpak() { + // In Flatpak, FLATPAK_ID is set automatically by the runtime to the app ID + // (e.g., "com.rustdesk.RustDesk"). This is the most reliable source. + // Fall back to constructing from app name if not available. + match std::env::var("FLATPAK_ID") { + Ok(id) if !id.is_empty() => format!("{}.desktop", id), + _ => { + let app_name = crate::get_app_name(); + format!("com.{}.{}.desktop", app_name.to_lowercase(), app_name) + } + } + } else { + format!("{}.desktop", crate::get_app_name().to_lowercase()) + } +} + +const PERMISSION_STORE_DEST: &str = "org.freedesktop.impl.portal.PermissionStore"; +const PERMISSION_STORE_PATH: &str = "/org/freedesktop/impl/portal/PermissionStore"; +const PERMISSION_STORE_IFACE: &str = "org.freedesktop.impl.portal.PermissionStore"; + +/// Clear GNOME shortcuts inhibitor permission via D-Bus. +/// This allows the permission dialog to be shown again. +pub fn clear_gnome_shortcuts_inhibitor_permission() -> ResultType<()> { + let app_id = get_shortcuts_inhibitor_app_id(); + log::info!( + "Clearing shortcuts inhibitor permission for app_id: {}, is_flatpak: {}", + app_id, + is_flatpak() + ); + + let conn = dbus::blocking::Connection::new_session()?; + let proxy = conn.with_proxy( + PERMISSION_STORE_DEST, + PERMISSION_STORE_PATH, + std::time::Duration::from_secs(3), + ); + + // DeletePermission(s table, s id, s app) -> () + let result: Result<(), dbus::Error> = proxy.method_call( + PERMISSION_STORE_IFACE, + "DeletePermission", + ("gnome", "shortcuts-inhibitor", app_id.as_str()), + ); + + match result { + Ok(()) => { + log::info!("Successfully cleared GNOME shortcuts inhibitor permission"); + Ok(()) + } + Err(e) => { + let err_name = e.name().unwrap_or(""); + // If the permission doesn't exist, that's also fine + if err_name == "org.freedesktop.portal.Error.NotFound" + || err_name == "org.freedesktop.DBus.Error.UnknownObject" + || err_name == "org.freedesktop.DBus.Error.ServiceUnknown" + { + log::info!( + "GNOME shortcuts inhibitor permission was not set ({})", + err_name + ); + Ok(()) + } else { + bail!("Failed to clear permission: {}", e) + } + } + } +} + +/// Check if GNOME shortcuts inhibitor permission exists. +pub fn has_gnome_shortcuts_inhibitor_permission() -> bool { + let app_id = get_shortcuts_inhibitor_app_id(); + + let conn = match dbus::blocking::Connection::new_session() { + Ok(c) => c, + Err(e) => { + log::debug!("Failed to connect to session bus: {}", e); + return false; + } + }; + let proxy = conn.with_proxy( + PERMISSION_STORE_DEST, + PERMISSION_STORE_PATH, + std::time::Duration::from_secs(3), + ); + + // Lookup(s table, s id) -> (a{sas} permissions, v data) + // We only need the permissions dict; check if app_id is a key. + let result: Result< + ( + std::collections::HashMap>, + dbus::arg::Variant>, + ), + dbus::Error, + > = proxy.method_call( + PERMISSION_STORE_IFACE, + "Lookup", + ("gnome", "shortcuts-inhibitor"), + ); + + match result { + Ok((permissions, _)) => { + let found = permissions.contains_key(&app_id); + log::debug!( + "Shortcuts inhibitor permission lookup: app_id={}, found={}, keys={:?}", + app_id, + found, + permissions.keys().collect::>() + ); + found + } + Err(e) => { + log::debug!("Failed to query shortcuts inhibitor permission: {}", e); + false + } + } +} diff --git a/vendor/rustdesk/src/platform/linux_desktop_manager.rs b/vendor/rustdesk/src/platform/linux_desktop_manager.rs new file mode 100644 index 0000000..03f1f62 --- /dev/null +++ b/vendor/rustdesk/src/platform/linux_desktop_manager.rs @@ -0,0 +1,744 @@ +use super::{linux::*, ResultType}; +use crate::client::{ + LOGIN_MSG_DESKTOP_NO_DESKTOP, LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER, + LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, + LOGIN_MSG_DESKTOP_XSESSION_FAILED, +}; +use hbb_common::{ + allow_err, bail, log, + rand::prelude::*, + tokio::time, + users::{get_user_by_name, os::unix::UserExt, User}, +}; +use pam; +use std::{ + collections::HashMap, + os::unix::process::CommandExt, + path::Path, + process::{Child, Command}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{sync_channel, SyncSender}, + Arc, Mutex, + }, + time::{Duration, Instant}, +}; + +lazy_static::lazy_static! { + static ref DESKTOP_RUNNING: Arc = Arc::new(AtomicBool::new(false)); + static ref DESKTOP_MANAGER: Arc>> = Arc::new(Mutex::new(None)); +} + +#[derive(Debug)] +struct DesktopManager { + seat0_username: String, + seat0_display_server: String, + child_username: String, + child_exit: Arc, + is_child_running: Arc, +} + +fn check_desktop_manager() { + let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); + if let Some(desktop_manager) = &mut (*desktop_manager) { + if desktop_manager.is_child_running.load(Ordering::SeqCst) { + return; + } + desktop_manager.child_exit.store(true, Ordering::SeqCst); + } +} + +pub fn start_xdesktop() { + debug_assert!(crate::is_server()); + std::thread::spawn(|| { + *DESKTOP_MANAGER.lock().unwrap() = Some(DesktopManager::new()); + + let interval = time::Duration::from_millis(super::SERVICE_INTERVAL); + DESKTOP_RUNNING.store(true, Ordering::SeqCst); + while DESKTOP_RUNNING.load(Ordering::SeqCst) { + check_desktop_manager(); + std::thread::sleep(interval); + } + log::info!("xdesktop child thread exit"); + }); +} + +pub fn stop_xdesktop() { + DESKTOP_RUNNING.store(false, Ordering::SeqCst); + *DESKTOP_MANAGER.lock().unwrap() = None; +} + +fn detect_headless() -> Option<&'static str> { + match run_cmds(&format!("which {}", DesktopManager::get_xorg())) { + Ok(output) => { + if output.trim().is_empty() { + return Some(LOGIN_MSG_DESKTOP_XORG_NOT_FOUND); + } + } + _ => { + return Some(LOGIN_MSG_DESKTOP_XORG_NOT_FOUND); + } + } + + match run_cmds("ls /usr/share/xsessions/") { + Ok(output) => { + if output.trim().is_empty() { + return Some(LOGIN_MSG_DESKTOP_NO_DESKTOP); + } + } + _ => { + return Some(LOGIN_MSG_DESKTOP_NO_DESKTOP); + } + } + + None +} + +pub fn try_start_desktop(_username: &str, _passsword: &str) -> String { + debug_assert!(crate::is_server()); + if _username.is_empty() { + let username = get_username(); + if username.is_empty() { + if let Some(msg) = detect_headless() { + msg + } else { + LOGIN_MSG_DESKTOP_SESSION_NOT_READY + } + } else { + "" + } + .to_owned() + } else { + let username = get_username(); + if username == _username { + // No need to verify password here. + return "".to_owned(); + } + if !username.is_empty() { + // Another user is logged in. No need to start a new xsession. + return "".to_owned(); + } + + if let Some(msg) = detect_headless() { + return msg.to_owned(); + } + + match try_start_x_session(_username, _passsword) { + Ok((username, x11_ready)) => { + if x11_ready { + if _username != username { + LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER.to_owned() + } else { + "".to_owned() + } + } else { + LOGIN_MSG_DESKTOP_SESSION_NOT_READY.to_owned() + } + } + Err(e) => { + log::error!("Failed to start xsession {}", e); + LOGIN_MSG_DESKTOP_XSESSION_FAILED.to_owned() + } + } + } +} + +fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bool)> { + let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); + if let Some(desktop_manager) = &mut (*desktop_manager) { + if let Some(seat0_username) = desktop_manager.get_supported_display_seat0_username() { + return Ok((seat0_username, true)); + } + + let _ = desktop_manager.try_start_x_session(username, password)?; + log::debug!( + "try_start_x_session, username: {}, {:?}", + &username, + &desktop_manager + ); + Ok(( + desktop_manager.child_username.clone(), + desktop_manager.is_running(), + )) + } else { + bail!(crate::client::LOGIN_MSG_DESKTOP_NOT_INITED); + } +} + +#[inline] +pub fn is_headless() -> bool { + DESKTOP_MANAGER + .lock() + .unwrap() + .as_ref() + .map_or(false, |manager| { + manager.get_supported_display_seat0_username().is_none() + }) +} + +pub fn get_username() -> String { + match &*DESKTOP_MANAGER.lock().unwrap() { + Some(manager) => { + if let Some(seat0_username) = manager.get_supported_display_seat0_username() { + seat0_username + } else { + if manager.is_running() && !manager.child_username.is_empty() { + manager.child_username.clone() + } else { + "".to_owned() + } + } + } + None => "".to_owned(), + } +} + +impl Drop for DesktopManager { + fn drop(&mut self) { + self.stop_children(); + } +} + +impl DesktopManager { + fn fatal_exit() { + std::process::exit(0); + } + + pub fn new() -> Self { + let mut seat0_username = "".to_owned(); + let mut seat0_display_server = "".to_owned(); + let seat0_values = get_values_of_seat0(&[0, 2]); + if !seat0_values[0].is_empty() { + seat0_username = seat0_values[1].clone(); + seat0_display_server = get_display_server_of_session(&seat0_values[0]); + } + Self { + seat0_username, + seat0_display_server, + child_username: "".to_owned(), + child_exit: Arc::new(AtomicBool::new(true)), + is_child_running: Arc::new(AtomicBool::new(false)), + } + } + + fn get_supported_display_seat0_username(&self) -> Option { + if is_gdm_user(&self.seat0_username) && self.seat0_display_server == DISPLAY_SERVER_WAYLAND + { + None + } else if self.seat0_username.is_empty() { + None + } else { + Some(self.seat0_username.clone()) + } + } + + #[inline] + fn get_xauth() -> String { + let xauth = get_env_var("XAUTHORITY"); + if xauth.is_empty() { + "/tmp/.Xauthority".to_owned() + } else { + xauth + } + } + + #[inline] + fn is_running(&self) -> bool { + self.is_child_running.load(Ordering::SeqCst) + } + + fn try_start_x_session(&mut self, username: &str, password: &str) -> ResultType<()> { + match get_user_by_name(username) { + Some(userinfo) => { + let mut client = pam::Client::with_password(&pam_get_service_name())?; + client + .conversation_mut() + .set_credentials(username, password); + match client.authenticate() { + Ok(_) => { + if self.is_running() { + return Ok(()); + } + + match self.start_x_session(&userinfo, username, password) { + Ok(_) => { + log::info!("Succeeded to start x11"); + self.child_username = username.to_string(); + Ok(()) + } + Err(e) => { + bail!("failed to start x session, {}", e); + } + } + } + Err(e) => { + bail!("failed to check user pass for {}, {}", username, e); + } + } + } + None => { + bail!("failed to get userinfo of {}", username); + } + } + } + + // The logic mainly from https://github.com/neutrinolabs/xrdp/blob/34fe9b60ebaea59e8814bbc3ca5383cabaa1b869/sesman/session.c#L334. + fn get_avail_display() -> ResultType { + let display_range = 0..51; + for i in display_range.clone() { + if Self::is_x_server_running(i) { + continue; + } + return Ok(i); + } + bail!("No available display found in range {:?}", display_range) + } + + #[inline] + fn is_x_server_running(display: u32) -> bool { + Path::new(&format!("/tmp/.X11-unix/X{}", display)).exists() + || Path::new(&format!("/tmp/.X{}-lock", display)).exists() + } + + fn start_x_session( + &mut self, + userinfo: &User, + username: &str, + password: &str, + ) -> ResultType<()> { + self.stop_children(); + + let display_num = Self::get_avail_display()?; + // "xServer_ip:display_num.screen_num" + + let uid = userinfo.uid(); + let gid = userinfo.primary_group_id(); + let envs = HashMap::from([ + ("SHELL", userinfo.shell().to_string_lossy().to_string()), + ("PATH", "/sbin:/bin:/usr/bin:/usr/local/bin".to_owned()), + ("USER", username.to_string()), + ("UID", userinfo.uid().to_string()), + ("HOME", userinfo.home_dir().to_string_lossy().to_string()), + ( + "XDG_RUNTIME_DIR", + format!("/run/user/{}", userinfo.uid().to_string()), + ), + // ("DISPLAY", self.display.clone()), + // ("XAUTHORITY", self.xauth.clone()), + // (ENV_DESKTOP_PROTOCOL, XProtocol::X11.to_string()), + ]); + self.child_exit.store(false, Ordering::SeqCst); + let is_child_running = self.is_child_running.clone(); + + let (tx_res, rx_res) = sync_channel(1); + let password = password.to_string(); + let username = username.to_string(); + // start x11 + std::thread::spawn(move || { + match Self::start_x_session_thread( + tx_res.clone(), + is_child_running, + uid, + gid, + display_num, + username, + password, + envs, + ) { + Ok(_) => {} + Err(e) => { + log::error!("Failed to start x session thread"); + allow_err!(tx_res.send(format!("Failed to start x session thread, {}", e))); + } + } + }); + + // wait x11 + match rx_res.recv_timeout(Duration::from_millis(10_000)) { + Ok(res) => { + if res == "" { + Ok(()) + } else { + bail!(res) + } + } + Err(e) => { + bail!("Failed to recv x11 result {}", e) + } + } + } + + #[inline] + fn display_from_num(num: u32) -> String { + format!(":{num}") + } + + fn start_x_session_thread( + tx_res: SyncSender, + is_child_running: Arc, + uid: u32, + gid: u32, + display_num: u32, + username: String, + password: String, + envs: HashMap<&str, String>, + ) -> ResultType<()> { + let mut client = pam::Client::with_password(&pam_get_service_name())?; + client + .conversation_mut() + .set_credentials(&username, &password); + client.authenticate()?; + + client.set_item(pam::PamItemType::TTY, &Self::display_from_num(display_num))?; + client.open_session()?; + + // fixme: FreeBSD kernel needs to login here. + // see: https://github.com/neutrinolabs/xrdp/blob/a64573b596b5fb07ca3a51590c5308d621f7214e/sesman/session.c#L556 + + let (child_xorg, child_wm) = Self::start_x11(uid, gid, username, display_num, &envs)?; + is_child_running.store(true, Ordering::SeqCst); + + log::info!("Start xorg and wm done, notify and wait xtop x11"); + allow_err!(tx_res.send("".to_owned())); + + Self::wait_stop_x11(child_xorg, child_wm); + log::info!("Wait x11 stop done"); + Ok(()) + } + + fn wait_xorg_exit(child_xorg: &mut Child) -> ResultType { + if let Ok(_) = child_xorg.kill() { + for _ in 0..3 { + match child_xorg.try_wait() { + Ok(Some(status)) => return Ok(format!("Xorg exit with {}", status)), + Ok(None) => {} + Err(e) => { + // fatal error + log::error!("Failed to wait xorg process, {}", e); + bail!("Failed to wait xorg process, {}", e) + } + } + std::thread::sleep(std::time::Duration::from_millis(1_000)); + } + log::error!("Failed to wait xorg process, not exit"); + bail!("Failed to wait xorg process, not exit") + } else { + Ok("Xorg is already exited".to_owned()) + } + } + + fn add_xauth_cookie( + file: &str, + display: &str, + uid: u32, + gid: u32, + envs: &HashMap<&str, String>, + ) -> ResultType<()> { + let randstr = (0..16) + .map(|_| format!("{:02x}", random::())) + .collect::(); + let output = Command::new("xauth") + .uid(uid) + .gid(gid) + .envs(envs) + .args(vec!["-q", "-f", file, "add", display, ".", &randstr]) + .output()?; + // xauth run success, even the following error occurs. + // Ok(Output { status: ExitStatus(unix_wait_status(0)), stdout: "", stderr: "xauth: file .Xauthority does not exist\n" }) + let errmsg = String::from_utf8_lossy(&output.stderr).to_string(); + if !errmsg.is_empty() { + if !errmsg.contains("does not exist") { + bail!("Failed to launch xauth, {}", errmsg) + } + } + Ok(()) + } + + fn wait_x_server_running(pid: u32, display_num: u32, max_wait_secs: u64) -> ResultType<()> { + let wait_begin = Instant::now(); + loop { + if run_cmds(&format!("ls /proc/{}", pid))?.is_empty() { + bail!("X server exit"); + } + + if Self::is_x_server_running(display_num) { + return Ok(()); + } + if wait_begin.elapsed().as_secs() > max_wait_secs { + bail!("Failed to wait xserver after {} seconds", max_wait_secs); + } + std::thread::sleep(Duration::from_millis(300)); + } + } + + fn start_x11( + uid: u32, + gid: u32, + username: String, + display_num: u32, + envs: &HashMap<&str, String>, + ) -> ResultType<(Child, Child)> { + log::debug!("envs of user {}: {:?}", &username, &envs); + + let xauth = Self::get_xauth(); + let display = Self::display_from_num(display_num); + + Self::add_xauth_cookie(&xauth, &display, uid, gid, &envs)?; + + // Start Xorg + let mut child_xorg = Self::start_x_server(&xauth, &display, uid, gid, &envs)?; + + log::info!("xorg started, wait 10 secs to ensuer x server is running"); + + let max_wait_secs = 10; + // wait x server running + if let Err(e) = Self::wait_x_server_running(child_xorg.id(), display_num, max_wait_secs) { + match Self::wait_xorg_exit(&mut child_xorg) { + Ok(msg) => log::info!("{}", msg), + Err(e) => { + log::error!("{}", e); + Self::fatal_exit(); + } + } + bail!(e) + } + + log::info!( + "xorg is running, start x window manager with DISPLAY: {}, XAUTHORITY: {}", + &display, + &xauth + ); + + std::env::set_var("DISPLAY", &display); + std::env::set_var("XAUTHORITY", &xauth); + // start window manager (startwm.sh) + let child_wm = match Self::start_x_window_manager(uid, gid, &envs) { + Ok(c) => c, + Err(e) => { + match Self::wait_xorg_exit(&mut child_xorg) { + Ok(msg) => log::info!("{}", msg), + Err(e) => { + log::error!("{}", e); + Self::fatal_exit(); + } + } + bail!(e) + } + }; + log::info!("x window manager is started"); + + Ok((child_xorg, child_wm)) + } + + fn try_wait_x11_child_exit(child_xorg: &mut Child, child_wm: &mut Child) -> bool { + match child_xorg.try_wait() { + Ok(Some(status)) => { + log::info!("Xorg exit with {}", status); + return true; + } + Ok(None) => {} + Err(e) => log::error!("Failed to wait xorg process, {}", e), + } + + match child_wm.try_wait() { + Ok(Some(status)) => { + // Logout may result "wm exit with signal: 11 (SIGSEGV) (core dumped)" + log::info!("wm exit with {}", status); + return true; + } + Ok(None) => {} + Err(e) => log::error!("Failed to wait xorg process, {}", e), + } + false + } + + fn wait_x11_children_exit(child_xorg: &mut Child, child_wm: &mut Child) { + log::debug!("Try kill child process xorg"); + if let Ok(_) = child_xorg.kill() { + let mut exited = false; + for _ in 0..2 { + match child_xorg.try_wait() { + Ok(Some(status)) => { + log::info!("Xorg exit with {}", status); + exited = true; + break; + } + Ok(None) => {} + Err(e) => { + log::error!("Failed to wait xorg process, {}", e); + Self::fatal_exit(); + } + } + std::thread::sleep(std::time::Duration::from_millis(1_000)); + } + if !exited { + log::error!("Failed to wait child xorg, after kill()"); + // try kill -9? + } + } + log::debug!("Try kill child process wm"); + if let Ok(_) = child_wm.kill() { + let mut exited = false; + for _ in 0..2 { + match child_wm.try_wait() { + Ok(Some(status)) => { + // Logout may result "wm exit with signal: 11 (SIGSEGV) (core dumped)" + log::info!("wm exit with {}", status); + exited = true; + } + Ok(None) => {} + Err(e) => { + log::error!("Failed to wait wm process, {}", e); + Self::fatal_exit(); + } + } + std::thread::sleep(std::time::Duration::from_millis(1_000)); + } + if !exited { + log::error!("Failed to wait child xorg, after kill()"); + // try kill -9? + } + } + } + + fn try_wait_stop_x11(child_xorg: &mut Child, child_wm: &mut Child) -> bool { + let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); + let mut exited = true; + if let Some(desktop_manager) = &mut (*desktop_manager) { + if desktop_manager.child_exit.load(Ordering::SeqCst) { + exited = true; + } else { + exited = Self::try_wait_x11_child_exit(child_xorg, child_wm); + } + if exited { + log::debug!("Wait x11 children exiting"); + Self::wait_x11_children_exit(child_xorg, child_wm); + desktop_manager + .is_child_running + .store(false, Ordering::SeqCst); + desktop_manager.child_exit.store(true, Ordering::SeqCst); + } + } + exited + } + + fn wait_stop_x11(mut child_xorg: Child, mut child_wm: Child) { + loop { + if Self::try_wait_stop_x11(&mut child_xorg, &mut child_wm) { + break; + } + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); + } + } + + fn get_xorg() -> &'static str { + // Fedora 26 or later + let xorg = "/usr/libexec/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Debian 9 or later + let xorg = "/usr/lib/xorg/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Ubuntu 16.04 or later + let xorg = "/usr/lib/xorg/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Arch Linux + let xorg = "/usr/lib/xorg-server/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Arch Linux + let xorg = "/usr/lib/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // CentOS 7 /usr/bin/Xorg or param=Xorg + + log::warn!("Failed to find xorg, use default Xorg.\n Please add \"allowed_users=anybody\" to \"/etc/X11/Xwrapper.config\"."); + "Xorg" + } + + fn start_x_server( + xauth: &str, + display: &str, + uid: u32, + gid: u32, + envs: &HashMap<&str, String>, + ) -> ResultType { + let xorg = Self::get_xorg(); + log::info!("Use xorg: {}", &xorg); + let app_name = crate::get_app_name().to_lowercase(); + let conf = format!("/etc/{app_name}/xorg.conf"); + match Command::new(xorg) + .envs(envs) + .uid(uid) + .gid(gid) + .args(vec![ + "-noreset", + "+extension", + "GLX", + "+extension", + "RANDR", + "+extension", + "RENDER", + "-config", + conf.as_ref(), + "-auth", + xauth, + display, + ]) + .spawn() + { + Ok(c) => Ok(c), + Err(e) => { + bail!("Failed to start Xorg with display {}, {}", display, e); + } + } + } + + fn start_x_window_manager( + uid: u32, + gid: u32, + envs: &HashMap<&str, String>, + ) -> ResultType { + let app_name = crate::get_app_name().to_lowercase(); + match Command::new(&format!("/etc/{app_name}/startwm.sh")) + .envs(envs) + .uid(uid) + .gid(gid) + .spawn() + { + Ok(c) => Ok(c), + Err(e) => { + bail!("Failed to start window manager, {}", e); + } + } + } + + fn stop_children(&mut self) { + self.child_exit.store(true, Ordering::SeqCst); + for _i in 1..10 { + if !self.is_child_running.load(Ordering::SeqCst) { + break; + } + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); + } + if self.is_child_running.load(Ordering::SeqCst) { + log::warn!("xdesktop child is still running!"); + } + } +} + +fn pam_get_service_name() -> String { + let app_name = crate::get_app_name().to_lowercase(); + if Path::new(&format!("/etc/pam.d/{app_name}")).is_file() { + app_name + } else { + "gdm".to_owned() + } +} diff --git a/vendor/rustdesk/src/platform/macos.mm b/vendor/rustdesk/src/platform/macos.mm new file mode 100644 index 0000000..3303855 --- /dev/null +++ b/vendor/rustdesk/src/platform/macos.mm @@ -0,0 +1,909 @@ +#import +#import +#import +#include +#include + +#include +#include +#include +#include +#include +#include + +extern "C" bool CanUseNewApiForScreenCaptureCheck() { + #ifdef NO_InputMonitoringAuthStatus + return false; + #else + NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion]; + return version.majorVersion >= 11; + #endif +} + +extern "C" uint32_t majorVersion() { + NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion]; + return version.majorVersion; +} + +extern "C" bool IsCanScreenRecording(bool prompt) { + #ifdef NO_InputMonitoringAuthStatus + return false; + #else + bool res = CGPreflightScreenCaptureAccess(); + if (!res && prompt) { + CGRequestScreenCaptureAccess(); + } + return res; + #endif +} + + +// https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm + +extern "C" bool InputMonitoringAuthStatus(bool prompt) { + #ifdef NO_InputMonitoringAuthStatus + return true; + #else + if (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_15) { + IOHIDAccessType theType = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent); + NSLog(@"IOHIDCheckAccess = %d, kIOHIDAccessTypeGranted = %d", theType, kIOHIDAccessTypeGranted); + switch (theType) { + case kIOHIDAccessTypeGranted: + return true; + break; + case kIOHIDAccessTypeDenied: { + if (prompt) { + NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"; + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]]; + } + break; + } + case kIOHIDAccessTypeUnknown: { + if (prompt) { + bool result = IOHIDRequestAccess(kIOHIDRequestTypeListenEvent); + NSLog(@"IOHIDRequestAccess result = %d", result); + } + break; + } + default: + break; + } + } else { + return true; + } + return false; + #endif +} + +extern "C" bool Elevate(char* process, char** args) { + AuthorizationRef authRef; + OSStatus status; + + status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, + kAuthorizationFlagDefaults, &authRef); + if (status != errAuthorizationSuccess) { + printf("Failed to create AuthorizationRef\n"); + return false; + } + + AuthorizationItem authItem = {kAuthorizationRightExecute, 0, NULL, 0}; + AuthorizationRights authRights = {1, &authItem}; + AuthorizationFlags flags = kAuthorizationFlagDefaults | + kAuthorizationFlagInteractionAllowed | + kAuthorizationFlagPreAuthorize | + kAuthorizationFlagExtendRights; + status = AuthorizationCopyRights(authRef, &authRights, kAuthorizationEmptyEnvironment, flags, NULL); + if (status != errAuthorizationSuccess) { + printf("Failed to authorize\n"); + return false; + } + + if (process != NULL) { + FILE *pipe = NULL; + status = AuthorizationExecuteWithPrivileges(authRef, process, kAuthorizationFlagDefaults, args, &pipe); + if (status != errAuthorizationSuccess) { + printf("Failed to run as root\n"); + AuthorizationFree(authRef, kAuthorizationFlagDefaults); + return false; + } + } + + AuthorizationFree(authRef, kAuthorizationFlagDefaults); + return true; +} + +extern "C" bool MacCheckAdminAuthorization() { + return Elevate(NULL, NULL); +} + +// https://gist.github.com/briankc/025415e25900750f402235dbf1b74e42 +extern "C" float BackingScaleFactor(uint32_t display) { + NSArray *screens = [NSScreen screens]; + for (NSScreen *screen in screens) { + NSDictionary *deviceDescription = [screen deviceDescription]; + NSNumber *screenNumber = [deviceDescription objectForKey:@"NSScreenNumber"]; + CGDirectDisplayID screenDisplayID = [screenNumber unsignedIntValue]; + if (screenDisplayID == display) { + return [screen backingScaleFactor]; + } + } + return 1; +} + +// https://github.com/jhford/screenresolution/blob/master/cg_utils.c +// https://github.com/jdoupe/screenres/blob/master/setgetscreen.m + +size_t bitDepth(CGDisplayModeRef mode) { + size_t depth = 0; + // Deprecated, same display same bpp? + // https://stackoverflow.com/questions/8210824/how-to-avoid-cgdisplaymodecopypixelencoding-to-get-bpp + // https://github.com/libsdl-org/SDL/pull/6628 + CFStringRef pixelEncoding = CGDisplayModeCopyPixelEncoding(mode); + // my numerical representation for kIO16BitFloatPixels and kIO32bitFloatPixels + // are made up and possibly non-sensical + if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO32BitFloatPixels), kCFCompareCaseInsensitive)) { + depth = 96; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO64BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 64; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO16BitFloatPixels), kCFCompareCaseInsensitive)) { + depth = 48; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 32; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO30BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 30; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 16; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive)) { + depth = 8; + } + CFRelease(pixelEncoding); + return depth; +} + +static bool isHiDPIMode(CGDisplayModeRef mode) { + // Check if the mode is HiDPI by comparing pixel width to width + // If pixel width is greater than width, it's a HiDPI mode + return CGDisplayModeGetPixelWidth(mode) > CGDisplayModeGetWidth(mode); +} + +CFArrayRef getAllModes(CGDirectDisplayID display) { + // Create options dictionary to include HiDPI modes + CFMutableDictionaryRef options = CFDictionaryCreateMutable( + kCFAllocatorDefault, + 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + // Include HiDPI modes + CFDictionarySetValue(options, kCGDisplayShowDuplicateLowResolutionModes, kCFBooleanTrue); + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, options); + CFRelease(options); + return allModes; +} + +extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) { + CFArrayRef allModes = getAllModes(display); + if (allModes == NULL) { + return false; + } + *numModes = CFArrayGetCount(allModes); + CFRelease(allModes); + return true; +} + +extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_t *heights, bool *hidpis, uint32_t max, uint32_t *numModes) { + CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display); + if (currentMode == NULL) { + return false; + } + CFArrayRef allModes = getAllModes(display); + if (allModes == NULL) { + CGDisplayModeRelease(currentMode); + return false; + } + uint32_t allModeCount = CFArrayGetCount(allModes); + uint32_t realNum = 0; + for (uint32_t i = 0; i < allModeCount && realNum < max; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + if (CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) && + bitDepth(currentMode) == bitDepth(mode)) { + widths[realNum] = (uint32_t)CGDisplayModeGetWidth(mode); + heights[realNum] = (uint32_t)CGDisplayModeGetHeight(mode); + hidpis[realNum] = isHiDPIMode(mode); + realNum++; + } + } + *numModes = realNum; + CGDisplayModeRelease(currentMode); + CFRelease(allModes); + return true; +} + +extern "C" bool MacGetMode(CGDirectDisplayID display, uint32_t *width, uint32_t *height) { + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display); + if (mode == NULL) { + return false; + } + *width = (uint32_t)CGDisplayModeGetWidth(mode); + *height = (uint32_t)CGDisplayModeGetHeight(mode); + CGDisplayModeRelease(mode); + return true; +} + +static bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { + CGError rc; + CGDisplayConfigRef config; + rc = CGBeginDisplayConfiguration(&config); + if (rc != kCGErrorSuccess) { + return false; + } + rc = CGConfigureDisplayWithDisplayMode(config, display, mode, NULL); + if (rc != kCGErrorSuccess) { + return false; + } + rc = CGCompleteDisplayConfiguration(config, kCGConfigureForSession); + if (rc != kCGErrorSuccess) { + return false; + } + return true; +} + +// Set the display to a specific mode based on width and height. +// Returns true if the display mode was successfully changed, false otherwise. +// If no such mode is available, it will not change the display mode. +// +// If `tryHiDPI` is true, it will try to set the display to a HiDPI mode if available. +// If no HiDPI mode is available, it will fall back to a non-HiDPI mode with the same resolution. +// If `tryHiDPI` is false, it sets the display to the first mode with the same resolution, no matter if it's HiDPI or not. +extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height, bool tryHiDPI) +{ + bool ret = false; + CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display); + if (currentMode == NULL) { + return ret; + } + CFArrayRef allModes = getAllModes(display); + + if (allModes == NULL) { + CGDisplayModeRelease(currentMode); + return ret; + } + int numModes = CFArrayGetCount(allModes); + CGDisplayModeRef preferredHiDPIMode = NULL; + CGDisplayModeRef fallbackMode = NULL; + for (int i = 0; i < numModes; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + if (width == CGDisplayModeGetWidth(mode) && + height == CGDisplayModeGetHeight(mode) && + CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) && + bitDepth(currentMode) == bitDepth(mode)) { + + if (isHiDPIMode(mode)) { + preferredHiDPIMode = mode; + break; + } else { + fallbackMode = mode; + if (!tryHiDPI) { + break; + } + } + } + } + + if (preferredHiDPIMode) { + ret = setDisplayToMode(display, preferredHiDPIMode); + } else if (fallbackMode) { + ret = setDisplayToMode(display, fallbackMode); + } + + CGDisplayModeRelease(currentMode); + CFRelease(allModes); + return ret; +} + +static CFMachPortRef g_eventTap = NULL; +static CFRunLoopSourceRef g_runLoopSource = NULL; +static std::mutex g_privacyModeMutex; +static bool g_privacyModeActive = false; + +// Flag to request asynchronous shutdown of privacy mode. +// This is set by DisplayReconfigurationCallback when an error occurs, instead of calling +// TurnOffPrivacyModeInternal() directly from within the callback. This avoids potential +// issues with unregistering a callback from within itself, which is not explicitly +// guaranteed to be safe by Apple documentation. +static bool g_privacyModeShutdownRequested = false; + +// Timestamp of the last display reconfiguration event (in milliseconds). +// Used for debouncing rapid successive changes (e.g., multiple resolution changes). +static uint64_t g_lastReconfigTimestamp = 0; + +// Flag indicating whether a delayed blackout reapplication is already scheduled. +// Prevents multiple concurrent delayed tasks from being created. +static bool g_blackoutReapplicationScheduled = false; + +// Use CFStringRef (UUID) as key instead of CGDirectDisplayID for stability across reconnections +// CGDirectDisplayID can change when displays are reconnected, but UUID remains stable +static std::map> g_originalGammas; + +// The event source user data value used by enigo library for injected events. +// This allows us to distinguish remote input (which should be allowed) from local physical input. +// See: libs/enigo/src/macos/macos_impl.rs - ENIGO_INPUT_EXTRA_VALUE +static const int64_t ENIGO_INPUT_EXTRA_VALUE = 100; + +// Duration in milliseconds to monitor and enforce blackout after display reconfiguration. +// macOS may restore default gamma (via ColorSync) at unpredictable times after display changes, +// so we need to actively monitor and reapply blackout during this period. +static const int64_t DISPLAY_RECONFIG_MONITOR_DURATION_MS = 5000; + +// Interval in milliseconds between gamma checks during the monitoring period. +static const int64_t GAMMA_CHECK_INTERVAL_MS = 200; + +// Helper function to get UUID string from DisplayID +static std::string GetDisplayUUID(CGDirectDisplayID displayId) { + CFUUIDRef uuid = CGDisplayCreateUUIDFromDisplayID(displayId); + if (uuid == NULL) { + return ""; + } + CFStringRef uuidStr = CFUUIDCreateString(kCFAllocatorDefault, uuid); + CFRelease(uuid); + if (uuidStr == NULL) { + return ""; + } + char buffer[128]; + if (CFStringGetCString(uuidStr, buffer, sizeof(buffer), kCFStringEncodingUTF8)) { + CFRelease(uuidStr); + return std::string(buffer); + } + CFRelease(uuidStr); + return ""; +} + +// Helper function to find DisplayID by UUID from current online displays +static CGDirectDisplayID FindDisplayIdByUUID(const std::string& targetUuid) { + uint32_t count = 0; + CGGetOnlineDisplayList(0, NULL, &count); + if (count == 0) return kCGNullDirectDisplay; + + std::vector displays(count); + CGGetOnlineDisplayList(count, displays.data(), &count); + + for (uint32_t i = 0; i < count; i++) { + std::string uuid = GetDisplayUUID(displays[i]); + if (uuid == targetUuid) { + return displays[i]; + } + } + return kCGNullDirectDisplay; +} + +// Helper function to restore gamma values for all displays in g_originalGammas. +// Returns true if all displays were restored successfully, false if any failed. +// Note: This function does NOT clear g_originalGammas - caller should do that if needed. +static bool RestoreAllGammas() { + bool allSuccess = true; + for (auto const& [uuid, gamma] : g_originalGammas) { + CGDirectDisplayID d = FindDisplayIdByUUID(uuid); + if (d == kCGNullDirectDisplay) { + NSLog(@"Display with UUID %s no longer online, skipping gamma restore", uuid.c_str()); + continue; + } + + uint32_t sampleCount = gamma.size() / 3; + if (sampleCount > 0) { + const CGGammaValue* red = gamma.data(); + const CGGammaValue* green = red + sampleCount; + const CGGammaValue* blue = green + sampleCount; + CGError error = CGSetDisplayTransferByTable(d, sampleCount, red, green, blue); + if (error != kCGErrorSuccess) { + NSLog(@"Failed to restore gamma for display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error); + allSuccess = false; + } + } + } + return allSuccess; +} + +// Helper function to apply blackout to a single display +static bool ApplyBlackoutToDisplay(CGDirectDisplayID display) { + uint32_t capacity = CGDisplayGammaTableCapacity(display); + if (capacity > 0) { + std::vector zeros(capacity, 0.0f); + CGError error = CGSetDisplayTransferByTable(display, capacity, zeros.data(), zeros.data(), zeros.data()); + if (error != kCGErrorSuccess) { + NSLog(@"ApplyBlackoutToDisplay: Failed to set gamma for display %u (error %d)", (unsigned)display, error); + return false; + } + return true; + } + NSLog(@"ApplyBlackoutToDisplay: Display %u has zero gamma table capacity, blackout not supported", (unsigned)display); + return false; +} + +// Forward declaration - defined later in the file +// Must be called while holding g_privacyModeMutex +static bool TurnOffPrivacyModeInternal(); + +// Helper function to schedule asynchronous shutdown of privacy mode. +// This is called from DisplayReconfigurationCallback when an error occurs, +// instead of calling TurnOffPrivacyModeInternal() directly. This avoids +// potential issues with unregistering a callback from within itself. +// Note: This function should be called while holding g_privacyModeMutex. +static void ScheduleAsyncPrivacyModeShutdown(const char* reason) { + if (g_privacyModeShutdownRequested) { + // Already requested, no need to schedule again + return; + } + g_privacyModeShutdownRequested = true; + NSLog(@"Privacy mode shutdown requested: %s", reason); + + // Schedule the actual shutdown on the main queue asynchronously + // This ensures we're outside the callback when we unregister it + dispatch_async(dispatch_get_main_queue(), ^{ + std::lock_guard lock(g_privacyModeMutex); + if (g_privacyModeShutdownRequested && g_privacyModeActive) { + NSLog(@"Executing deferred privacy mode shutdown"); + TurnOffPrivacyModeInternal(); + } + g_privacyModeShutdownRequested = false; + }); +} + +// Helper function to apply blackout to all online displays. +// Must be called while holding g_privacyModeMutex. +static void ApplyBlackoutToAllDisplays() { + uint32_t onlineCount = 0; + CGGetOnlineDisplayList(0, NULL, &onlineCount); + std::vector onlineDisplays(onlineCount); + CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount); + + for (uint32_t i = 0; i < onlineCount; i++) { + ApplyBlackoutToDisplay(onlineDisplays[i]); + } +} + +// Helper function to get current timestamp in milliseconds +static uint64_t GetCurrentTimestampMs() { + return (uint64_t)(CFAbsoluteTimeGetCurrent() * 1000.0); +} + +// Helper function to check if a display's gamma is currently blacked out (all zeros). +// Returns true if gamma appears to be blacked out, false otherwise. +static bool IsDisplayBlackedOut(CGDirectDisplayID display) { + uint32_t capacity = CGDisplayGammaTableCapacity(display); + if (capacity == 0) { + return true; // Can't check, assume it's fine + } + + std::vector red(capacity), green(capacity), blue(capacity); + uint32_t sampleCount = 0; + if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) != kCGErrorSuccess) { + return true; // Can't read, assume it's fine + } + + // Check if all values are zero (or very close to zero) + for (uint32_t i = 0; i < sampleCount; i++) { + if (red[i] > 0.01f || green[i] > 0.01f || blue[i] > 0.01f) { + return false; // Not blacked out + } + } + return true; +} + +// Internal function that monitors and enforces blackout for a period after display reconfiguration. +// This function checks gamma values periodically and reapplies blackout if needed. +// Must NOT be called while holding g_privacyModeMutex (it acquires the lock internally). +static void RunBlackoutMonitor() { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(GAMMA_CHECK_INTERVAL_MS * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ + std::lock_guard lock(g_privacyModeMutex); + + if (!g_privacyModeActive) { + g_blackoutReapplicationScheduled = false; + return; + } + + uint64_t now = GetCurrentTimestampMs(); + + // Calculate effective end time based on the last reconfig event + uint64_t effectiveEndTime = g_lastReconfigTimestamp + DISPLAY_RECONFIG_MONITOR_DURATION_MS; + + // Check all displays and reapply blackout if any has been restored + uint32_t onlineCount = 0; + CGGetOnlineDisplayList(0, NULL, &onlineCount); + std::vector onlineDisplays(onlineCount); + CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount); + + bool needsReapply = false; + for (uint32_t i = 0; i < onlineCount; i++) { + if (!IsDisplayBlackedOut(onlineDisplays[i])) { + needsReapply = true; + break; + } + } + + if (needsReapply) { + NSLog(@"Gamma was restored by system, reapplying blackout"); + ApplyBlackoutToAllDisplays(); + } + + // Continue monitoring if we haven't reached the end time + if (now < effectiveEndTime) { + RunBlackoutMonitor(); + } else { + NSLog(@"Blackout monitoring period ended"); + g_blackoutReapplicationScheduled = false; + } + }); +} + +// Helper function to start monitoring and enforcing blackout after display reconfiguration. +// This is used after display reconfiguration events because macOS may restore +// default gamma (via ColorSync) at unpredictable times after display changes. +// Note: This function should be called while holding g_privacyModeMutex. +static void ScheduleDelayedBlackoutReapplication(const char* reason) { + // Update timestamp to current time + g_lastReconfigTimestamp = GetCurrentTimestampMs(); + + NSLog(@"Starting blackout monitor: %s", reason); + + // Only schedule if not already scheduled + if (!g_blackoutReapplicationScheduled) { + g_blackoutReapplicationScheduled = true; + RunBlackoutMonitor(); + } + // If already scheduled, the running monitor will see the updated timestamp + // and extend its monitoring period +} + +// Display reconfiguration callback to handle display connect/disconnect events +// +// IMPORTANT: When errors occur in this callback, we use ScheduleAsyncPrivacyModeShutdown() +// instead of calling TurnOffPrivacyModeInternal() directly. This is because: +// 1. TurnOffPrivacyModeInternal() calls CGDisplayRemoveReconfigurationCallback to unregister +// this callback, and unregistering a callback from within itself is not explicitly +// guaranteed to be safe by Apple documentation. +// 2. Using async dispatch ensures we're completely outside the callback context when +// performing the cleanup, avoiding any potential undefined behavior. +static void DisplayReconfigurationCallback(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void *userInfo) { + (void)userInfo; + + // Note: We need to handle the callback carefully because: + // 1. macOS may call this callback multiple times during display reconfiguration + // 2. The system may restore ColorSync settings after our gamma change + // 3. We should not hold the lock for too long in the callback + + // Skip begin configuration flag - wait for the actual change + if (flags & kCGDisplayBeginConfigurationFlag) { + return; + } + + std::lock_guard lock(g_privacyModeMutex); + + if (!g_privacyModeActive) { + return; + } + + if (flags & kCGDisplayAddFlag) { + // A display was added - apply blackout to it + NSLog(@"Display %u added during privacy mode, applying blackout", (unsigned)display); + std::string uuid = GetDisplayUUID(display); + if (uuid.empty()) { + NSLog(@"Failed to get UUID for newly added display %u, exiting privacy mode", (unsigned)display); + ScheduleAsyncPrivacyModeShutdown("Failed to get UUID for newly added display"); + return; + } + + // Save original gamma if not already saved for this UUID + if (g_originalGammas.find(uuid) == g_originalGammas.end()) { + uint32_t capacity = CGDisplayGammaTableCapacity(display); + if (capacity > 0) { + std::vector red(capacity), green(capacity), blue(capacity); + uint32_t sampleCount = 0; + if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) { + std::vector all; + all.insert(all.end(), red.begin(), red.begin() + sampleCount); + all.insert(all.end(), green.begin(), green.begin() + sampleCount); + all.insert(all.end(), blue.begin(), blue.begin() + sampleCount); + g_originalGammas[uuid] = all; + } else { + NSLog(@"DisplayReconfigurationCallback: Failed to get gamma table for display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str()); + ScheduleAsyncPrivacyModeShutdown("Failed to get gamma table for newly added display"); + return; + } + } else { + NSLog(@"DisplayReconfigurationCallback: Display %u (UUID: %s) has zero gamma table capacity, exiting privacy mode", (unsigned)display, uuid.c_str()); + ScheduleAsyncPrivacyModeShutdown("Newly added display has zero gamma table capacity"); + return; + } + } + + // Apply blackout to the new display immediately + if (!ApplyBlackoutToDisplay(display)) { + NSLog(@"DisplayReconfigurationCallback: Failed to blackout display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str()); + ScheduleAsyncPrivacyModeShutdown("Failed to blackout newly added display"); + return; + } + + // Schedule a delayed re-application to handle ColorSync restoration + // macOS may restore default gamma for ALL displays after a new display is added, + // so we need to reapply blackout to all online displays, not just the new one + ScheduleDelayedBlackoutReapplication("after new display added"); + } else if (flags & kCGDisplayRemoveFlag) { + // A display was removed - update our mapping and reapply blackout to remaining displays + NSLog(@"Display %u removed during privacy mode", (unsigned)display); + std::string uuid = GetDisplayUUID(display); + (void)uuid; // UUID retrieved for potential future use or logging + + // When a display is removed, macOS may reconfigure other displays and restore their gamma. + // Schedule a delayed re-application of blackout to all remaining online displays. + ScheduleDelayedBlackoutReapplication("after display removal"); + } else if (flags & kCGDisplaySetModeFlag) { + // Display mode changed (resolution change, ColorSync/Night Shift interference, etc.) + // macOS resets gamma to default when display mode changes, so we need to reapply blackout. + // Schedule a delayed re-application because ColorSync restoration happens asynchronously. + NSLog(@"Display %u mode changed during privacy mode, reapplying blackout", (unsigned)display); + ScheduleDelayedBlackoutReapplication("after display mode change"); + } +} + +CGEventRef MyEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { + (void)proxy; + (void)refcon; + + // Handle EventTap being disabled by system timeout + if (type == kCGEventTapDisabledByTimeout) { + NSLog(@"EventTap was disabled by timeout, re-enabling"); + if (g_eventTap) { + CGEventTapEnable(g_eventTap, true); + } + return event; + } + + // Handle EventTap being disabled by user input + if (type == kCGEventTapDisabledByUserInput) { + NSLog(@"EventTap was disabled by user input, re-enabling"); + if (g_eventTap) { + CGEventTapEnable(g_eventTap, true); + } + return event; + } + + // Allow events explicitly injected by enigo (remote input), identified via custom user data. + int64_t userData = CGEventGetIntegerValueField(event, kCGEventSourceUserData); + if (userData == ENIGO_INPUT_EXTRA_VALUE) { + return event; + } + // Block local physical HID input. + if (CGEventGetIntegerValueField(event, kCGEventSourceStateID) == kCGEventSourceStateHIDSystemState) { + return NULL; + } + return event; +} + +// Helper function to set up EventTap on the main thread +// Returns true if EventTap was successfully created and enabled +static bool SetupEventTapOnMainThread() { + __block bool success = false; + + void (^setupBlock)(void) = ^{ + if (g_eventTap) { + // Already set up + success = true; + return; + } + + // Note: kCGEventTapDisabledByTimeout and kCGEventTapDisabledByUserInput are special + // notification types (0xFFFFFFFE and 0xFFFFFFFF) that are delivered via the callback's + // type parameter, not through the event mask. They should NOT be included in eventMask + // as bit-shifting by these values causes undefined behavior. + CGEventMask eventMask = (1 << kCGEventKeyDown) | (1 << kCGEventKeyUp) | + (1 << kCGEventLeftMouseDown) | (1 << kCGEventLeftMouseUp) | + (1 << kCGEventRightMouseDown) | (1 << kCGEventRightMouseUp) | + (1 << kCGEventOtherMouseDown) | (1 << kCGEventOtherMouseUp) | + (1 << kCGEventLeftMouseDragged) | (1 << kCGEventRightMouseDragged) | + (1 << kCGEventOtherMouseDragged) | + (1 << kCGEventMouseMoved) | (1 << kCGEventScrollWheel); + + g_eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, + eventMask, MyEventTapCallback, NULL); + if (g_eventTap) { + g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0); + CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes); + CGEventTapEnable(g_eventTap, true); + success = true; + } else { + NSLog(@"MacSetPrivacyMode: Failed to create CGEventTap; input blocking not enabled."); + success = false; + } + }; + + // Execute on main thread to ensure CFRunLoop operations are safe. + // Use dispatch_sync if not on main thread, otherwise execute directly to avoid deadlock. + // + // IMPORTANT: Potential deadlock consideration: + // Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread + // tries to acquire g_privacyModeMutex. Currently this is safe because: + // 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads + // 2. The main thread never directly calls MacSetPrivacyMode + // If this assumption changes in the future, consider releasing the mutex before dispatch_sync + // or restructuring the locking strategy. + if ([NSThread isMainThread]) { + setupBlock(); + } else { + dispatch_sync(dispatch_get_main_queue(), setupBlock); + } + + return success; +} + +// Helper function to tear down EventTap on the main thread +static void TeardownEventTapOnMainThread() { + void (^teardownBlock)(void) = ^{ + if (g_eventTap) { + CGEventTapEnable(g_eventTap, false); + CFRunLoopRemoveSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes); + CFRelease(g_runLoopSource); + CFRelease(g_eventTap); + g_eventTap = NULL; + g_runLoopSource = NULL; + } + }; + + // Execute on main thread to ensure CFRunLoop operations are safe. + // + // NOTE: We use dispatch_sync here instead of dispatch_async because: + // 1. TurnOffPrivacyModeInternal() expects EventTap to be fully torn down before + // proceeding with gamma restoration - using async would cause race conditions. + // 2. The caller (MacSetPrivacyMode) needs deterministic cleanup order. + // + // IMPORTANT: Potential deadlock consideration (same as SetupEventTapOnMainThread): + // Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread + // tries to acquire g_privacyModeMutex. Currently this is safe because: + // 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads + // 2. The main thread never directly calls MacSetPrivacyMode + // If this assumption changes in the future, consider releasing the mutex before dispatch_sync + // or restructuring the locking strategy. + if ([NSThread isMainThread]) { + teardownBlock(); + } else { + dispatch_sync(dispatch_get_main_queue(), teardownBlock); + } +} + +// Internal function to turn off privacy mode without acquiring the mutex +// Must be called while holding g_privacyModeMutex +static bool TurnOffPrivacyModeInternal() { + if (!g_privacyModeActive) { + return true; + } + + // 1. Unregister display reconfiguration callback + CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL); + + // 2. Input - restore (tear down EventTap on main thread) + TeardownEventTapOnMainThread(); + + // 3. Gamma - restore using UUID to find current DisplayID + bool restoreSuccess = RestoreAllGammas(); + + // 4. Fallback: Always call CGDisplayRestoreColorSyncSettings as a safety net + // This ensures displays return to normal even if our restoration failed or + // if the system (ColorSync/Night Shift) modified gamma during privacy mode + CGDisplayRestoreColorSyncSettings(); + + // Clean up + g_originalGammas.clear(); + g_privacyModeActive = false; + g_privacyModeShutdownRequested = false; + g_lastReconfigTimestamp = 0; + g_blackoutReapplicationScheduled = false; + + return restoreSuccess; +} + +extern "C" bool MacSetPrivacyMode(bool on) { + std::lock_guard lock(g_privacyModeMutex); + if (on) { + // Already in privacy mode + if (g_privacyModeActive) { + return true; + } + + // 1. Input Blocking - set up EventTap on main thread + if (!SetupEventTapOnMainThread()) { + return false; + } + + // 2. Register display reconfiguration callback to handle hot-plug events + CGDisplayRegisterReconfigurationCallback(DisplayReconfigurationCallback, NULL); + + // 3. Gamma Blackout + uint32_t count = 0; + CGGetOnlineDisplayList(0, NULL, &count); + std::vector displays(count); + CGGetOnlineDisplayList(count, displays.data(), &count); + + uint32_t blackoutSuccessCount = 0; + uint32_t blackoutAttemptCount = 0; + + for (uint32_t i = 0; i < count; i++) { + CGDirectDisplayID d = displays[i]; + std::string uuid = GetDisplayUUID(d); + + if (uuid.empty()) { + NSLog(@"MacSetPrivacyMode: Failed to get UUID for display %u, privacy mode requires all displays", (unsigned)d); + // Privacy mode requires ALL connected displays to be successfully blacked out + // to ensure user privacy. If we can't identify a display (no UUID), + // we can't safely manage its state or restore it later. + // Therefore, we must abort the entire operation and clean up any resources + // already allocated (like event taps and reconfiguration callbacks). + CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL); + TeardownEventTapOnMainThread(); + // Restore gamma for displays that were already blacked out before this failure + if (!RestoreAllGammas()) { + // If any display failed to restore, use system reset as fallback + CGDisplayRestoreColorSyncSettings(); + } + g_originalGammas.clear(); + return false; + } + + // Save original gamma using UUID as key (stable across reconnections) + if (g_originalGammas.find(uuid) == g_originalGammas.end()) { + uint32_t capacity = CGDisplayGammaTableCapacity(d); + if (capacity > 0) { + std::vector red(capacity), green(capacity), blue(capacity); + uint32_t sampleCount = 0; + if (CGGetDisplayTransferByTable(d, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) { + std::vector all; + all.insert(all.end(), red.begin(), red.begin() + sampleCount); + all.insert(all.end(), green.begin(), green.begin() + sampleCount); + all.insert(all.end(), blue.begin(), blue.begin() + sampleCount); + g_originalGammas[uuid] = all; + } else { + NSLog(@"MacSetPrivacyMode: Failed to get gamma table for display %u (UUID: %s)", (unsigned)d, uuid.c_str()); + } + } else { + NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity, not supported", (unsigned)d, uuid.c_str()); + } + } + + // Set to black only if we have saved original gamma for this display + if (g_originalGammas.find(uuid) != g_originalGammas.end()) { + uint32_t capacity = CGDisplayGammaTableCapacity(d); + if (capacity > 0) { + std::vector zeros(capacity, 0.0f); + blackoutAttemptCount++; + CGError error = CGSetDisplayTransferByTable(d, capacity, zeros.data(), zeros.data(), zeros.data()); + if (error != kCGErrorSuccess) { + NSLog(@"MacSetPrivacyMode: Failed to blackout display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error); + } else { + blackoutSuccessCount++; + } + } else { + NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity for blackout", (unsigned)d, uuid.c_str()); + } + } + } + + // Return false if any display failed to blackout - privacy mode requires ALL displays to be blacked out + if (blackoutAttemptCount > 0 && blackoutSuccessCount < blackoutAttemptCount) { + NSLog(@"MacSetPrivacyMode: Failed to blackout all displays (%u/%u succeeded)", blackoutSuccessCount, blackoutAttemptCount); + // Clean up: unregister callback and disable event tap since we're failing + CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL); + TeardownEventTapOnMainThread(); + // Restore gamma for displays that were successfully blacked out + if (!RestoreAllGammas()) { + // If any display failed to restore, use system reset as fallback + NSLog(@"Some displays failed to restore gamma during cleanup, using CGDisplayRestoreColorSyncSettings as fallback"); + CGDisplayRestoreColorSyncSettings(); + } + g_originalGammas.clear(); + return false; + } + + g_privacyModeActive = true; + return true; + + } else { + return TurnOffPrivacyModeInternal(); + } +} diff --git a/vendor/rustdesk/src/platform/macos.rs b/vendor/rustdesk/src/platform/macos.rs new file mode 100644 index 0000000..2e68cf5 --- /dev/null +++ b/vendor/rustdesk/src/platform/macos.rs @@ -0,0 +1,1230 @@ +// https://developer.apple.com/documentation/appkit/nscursor +// https://github.com/servo/core-foundation-rs +// https://github.com/rust-windowing/winit + +use super::{CursorData, ResultType}; +use cocoa::{ + appkit::{NSApp, NSApplication, NSApplicationActivationPolicy::*}, + base::{id, nil, BOOL, NO, YES}, + foundation::{NSDictionary, NSPoint, NSSize, NSString}, +}; +use core_foundation::{ + array::{CFArrayGetCount, CFArrayGetValueAtIndex}, + dictionary::CFDictionaryRef, + string::CFStringRef, +}; +use core_graphics::{ + display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo}, + window::{kCGWindowName, kCGWindowOwnerPID}, +}; +use hbb_common::{ + anyhow::anyhow, + bail, log, + message_proto::{DisplayInfo, Resolution}, + sysinfo::{Pid, Process, ProcessRefreshKind, System}, +}; +use include_dir::{include_dir, Dir}; +use objc::rc::autoreleasepool; +use objc::{class, msg_send, sel, sel_impl}; +use scrap::{libc::c_void, quartz::ffi::*}; +use std::{ + collections::HashMap, + os::unix::process::CommandExt, + path::{Path, PathBuf}, + process::{Command, Stdio}, + sync::Mutex, +}; + +// macOS boolean_t is defined as `int` in +type BooleanT = hbb_common::libc::c_int; + +static PRIVILEGES_SCRIPTS_DIR: Dir = + include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); +static mut LATEST_SEED: i32 = 0; + +#[inline] +fn get_update_temp_dir() -> PathBuf { + let euid = unsafe { hbb_common::libc::geteuid() }; + Path::new("/tmp").join(format!(".rustdeskupdate-{}", euid)) +} + +#[inline] +fn get_update_temp_dir_string() -> String { + get_update_temp_dir().to_string_lossy().into_owned() +} + +/// Global mutex to serialize CoreGraphics cursor operations. +/// This prevents race conditions between cursor visibility (hide depth tracking) +/// and cursor positioning/clipping operations. +static CG_CURSOR_MUTEX: Mutex<()> = Mutex::new(()); + +extern "C" { + fn CGSCurrentCursorSeed() -> i32; + fn CGEventCreate(r: *const c_void) -> *const c_void; + fn CGEventGetLocation(e: *const c_void) -> CGPoint; + static kAXTrustedCheckOptionPrompt: CFStringRef; + fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; + fn InputMonitoringAuthStatus(_: BOOL) -> BOOL; + fn IsCanScreenRecording(_: BOOL) -> BOOL; + fn CanUseNewApiForScreenCaptureCheck() -> BOOL; + fn MacCheckAdminAuthorization() -> BOOL; + fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL; + fn MacGetModes( + display: u32, + widths: *mut u32, + heights: *mut u32, + hidpis: *mut BOOL, + max: u32, + numModes: *mut u32, + ) -> BOOL; + fn majorVersion() -> u32; + fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL; + fn MacSetMode(display: u32, width: u32, height: u32, tryHiDPI: bool) -> BOOL; + fn CGWarpMouseCursorPosition(newCursorPosition: CGPoint) -> CGError; + fn CGAssociateMouseAndMouseCursorPosition(connected: BooleanT) -> CGError; +} + +pub fn major_version() -> u32 { + unsafe { majorVersion() } +} + +pub fn is_process_trusted(prompt: bool) -> bool { + autoreleasepool(|| unsafe_is_process_trusted(prompt)) +} + +fn unsafe_is_process_trusted(prompt: bool) -> bool { + unsafe { + let value = if prompt { YES } else { NO }; + let value: id = msg_send![class!(NSNumber), numberWithBool: value]; + let options = NSDictionary::dictionaryWithObject_forKey_( + nil, + value, + kAXTrustedCheckOptionPrompt as _, + ); + AXIsProcessTrustedWithOptions(options as _) == YES + } +} + +pub fn is_can_input_monitoring(prompt: bool) -> bool { + unsafe { + let value = if prompt { YES } else { NO }; + InputMonitoringAuthStatus(value) == YES + } +} + +pub fn is_can_screen_recording(prompt: bool) -> bool { + autoreleasepool(|| unsafe_is_can_screen_recording(prompt)) +} + +// macOS >= 10.15 +// https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/ +// remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk +fn unsafe_is_can_screen_recording(prompt: bool) -> bool { + // we got some report that we show no permission even after set it, so we try to use new api for screen recording check + // the new api is only available on macOS >= 10.15, but on stackoverflow, some people said it works on >= 10.16 (crash on 10.15), + // but also some said it has bug on 10.16, so we just use it on 11.0. + unsafe { + if CanUseNewApiForScreenCaptureCheck() == YES { + return IsCanScreenRecording(if prompt { YES } else { NO }) == YES; + } + } + let mut can_record_screen: bool = false; + unsafe { + let our_pid: i32 = std::process::id() as _; + let our_pid: id = msg_send![class!(NSNumber), numberWithInteger: our_pid]; + let window_list = + CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID); + let n = CFArrayGetCount(window_list); + let dock = NSString::alloc(nil).init_str("Dock"); + for i in 0..n { + let w: id = CFArrayGetValueAtIndex(window_list, i) as _; + let name: id = msg_send![w, valueForKey: kCGWindowName as id]; + if name.is_null() { + continue; + } + let pid: id = msg_send![w, valueForKey: kCGWindowOwnerPID as id]; + let is_me: BOOL = msg_send![pid, isEqual: our_pid]; + if is_me == YES { + continue; + } + let pid: i32 = msg_send![pid, intValue]; + let p: id = msg_send![ + class!(NSRunningApplication), + runningApplicationWithProcessIdentifier: pid + ]; + if p.is_null() { + // ignore processes we don't have access to, such as WindowServer, which manages the windows named "Menubar" and "Backstop Menubar" + continue; + } + let url: id = msg_send![p, executableURL]; + let exe_name: id = msg_send![url, lastPathComponent]; + if exe_name.is_null() { + continue; + } + let is_dock: BOOL = msg_send![exe_name, isEqual: dock]; + if is_dock == YES { + // ignore the Dock, which provides the desktop picture + continue; + } + can_record_screen = true; + break; + } + } + if !can_record_screen && prompt { + use scrap::{Capturer, Display}; + if let Ok(d) = Display::primary() { + Capturer::new(d).ok(); + } + } + can_record_screen +} + +pub fn install_service() -> bool { + is_installed_daemon(false) +} + +// Remember to check if `update_daemon_agent()` need to be changed if changing `is_installed_daemon()`. +// No need to merge the existing dup code, because the code in these two functions are too critical. +// New code should be written in a common function. +pub fn is_installed_daemon(prompt: bool) -> bool { + let daemon = format!("{}_service.plist", crate::get_full_name()); + let agent = format!("{}_server.plist", crate::get_full_name()); + let agent_plist_file = format!("/Library/LaunchAgents/{}", agent); + if !prompt { + // in macos 13, there is new way to check if they are running or enabled, https://developer.apple.com/documentation/servicemanagement/updating-helper-executables-from-earlier-versions-of-macos#Respond-to-changes-in-System-Settings + if !std::path::Path::new(&format!("/Library/LaunchDaemons/{}", daemon)).exists() { + return false; + } + if !std::path::Path::new(&agent_plist_file).exists() { + return false; + } + return true; + } + + let Some(install_script) = PRIVILEGES_SCRIPTS_DIR.get_file("install.scpt") else { + return false; + }; + let Some(install_script_body) = install_script.contents_utf8().map(correct_app_name) else { + return false; + }; + + let Some(daemon_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("daemon.plist") else { + return false; + }; + let Some(daemon_plist_body) = daemon_plist.contents_utf8().map(correct_app_name) else { + return false; + }; + + let Some(agent_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("agent.plist") else { + return false; + }; + let Some(agent_plist_body) = agent_plist.contents_utf8().map(correct_app_name) else { + return false; + }; + + std::thread::spawn(move || { + match std::process::Command::new("osascript") + .arg("-e") + .arg(install_script_body) + .arg(daemon_plist_body) + .arg(agent_plist_body) + .arg(&get_active_username()) + .status() + { + Err(e) => { + log::error!("run osascript failed: {}", e); + } + _ => { + let installed = std::path::Path::new(&agent_plist_file).exists(); + log::info!("Agent file {} installed: {}", agent_plist_file, installed); + if installed { + log::info!("launch server"); + std::process::Command::new("launchctl") + .args(&["load", "-w", &agent_plist_file]) + .status() + .ok(); + } + } + } + }); + false +} + +fn update_daemon_agent(agent_plist_file: String, update_source_dir: String, sync: bool) { + let update_script_file = "update.scpt"; + let Some(update_script) = PRIVILEGES_SCRIPTS_DIR.get_file(update_script_file) else { + return; + }; + let Some(update_script_body) = update_script.contents_utf8().map(correct_app_name) else { + return; + }; + + let Some(daemon_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("daemon.plist") else { + return; + }; + let Some(daemon_plist_body) = daemon_plist.contents_utf8().map(correct_app_name) else { + return; + }; + let Some(agent_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("agent.plist") else { + return; + }; + let Some(agent_plist_body) = agent_plist.contents_utf8().map(correct_app_name) else { + return; + }; + + let func = move || { + let mut binding = std::process::Command::new("osascript"); + let cmd = binding + .arg("-e") + .arg(update_script_body) + .arg(daemon_plist_body) + .arg(agent_plist_body) + .arg(&get_active_username()) + .arg(std::process::id().to_string()) + .arg(update_source_dir); + match cmd.status() { + Err(e) => { + log::error!("run osascript failed: {}", e); + } + Ok(status) if !status.success() => { + log::warn!("run osascript failed with status: {}", status); + } + _ => { + let installed = std::path::Path::new(&agent_plist_file).exists(); + log::info!("Agent file {} installed: {}", &agent_plist_file, installed); + } + } + }; + if sync { + func(); + } else { + std::thread::spawn(func); + } +} + +fn correct_app_name(s: &str) -> String { + let mut s = s.to_owned(); + if let Some(bundleid) = get_bundle_id() { + s = s.replace("com.carriez.rustdesk", &bundleid); + } + s = s.replace("rustdesk", &crate::get_app_name().to_lowercase()); + s = s.replace("RustDesk", &crate::get_app_name()); + s +} + +pub fn uninstall_service(show_new_window: bool, sync: bool) -> bool { + // to-do: do together with win/linux about refactory start/stop service + if !is_installed_daemon(false) { + return false; + } + + let Some(script_file) = PRIVILEGES_SCRIPTS_DIR.get_file("uninstall.scpt") else { + return false; + }; + let Some(script_body) = script_file.contents_utf8().map(correct_app_name) else { + return false; + }; + + let func = move || { + match std::process::Command::new("osascript") + .arg("-e") + .arg(script_body) + .status() + { + Err(e) => { + log::error!("run osascript failed: {}", e); + } + _ => { + let agent = format!("{}_server.plist", crate::get_full_name()); + let agent_plist_file = format!("/Library/LaunchAgents/{}", agent); + let uninstalled = !std::path::Path::new(&agent_plist_file).exists(); + log::info!( + "Agent file {} uninstalled: {}", + agent_plist_file, + uninstalled + ); + if uninstalled { + if !show_new_window { + let _ = crate::ipc::close_all_instances(); + // leave ipc a little time + std::thread::sleep(std::time::Duration::from_millis(300)); + } + crate::ipc::set_option("stop-service", "Y"); + std::process::Command::new("launchctl") + .args(&["remove", &format!("{}_server", crate::get_full_name())]) + .status() + .ok(); + if show_new_window { + std::process::Command::new("open") + .arg("-n") + .arg(&format!("/Applications/{}.app", crate::get_app_name())) + .spawn() + .ok(); + // leave open a little time + std::thread::sleep(std::time::Duration::from_millis(300)); + } + quit_gui(); + } + } + } + }; + if sync { + func(); + } else { + std::thread::spawn(func); + } + true +} + +pub fn get_cursor_pos() -> Option<(i32, i32)> { + unsafe { + let e = CGEventCreate(0 as _); + let point = CGEventGetLocation(e); + CFRelease(e); + Some((point.x as _, point.y as _)) + } + /* + let mut pt: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] }; + let screen: id = unsafe { msg_send![class!(NSScreen), currentScreenForMouseLocation] }; + let frame: NSRect = unsafe { msg_send![screen, frame] }; + pt.x -= frame.origin.x; + pt.y -= frame.origin.y; + Some((pt.x as _, pt.y as _)) + */ +} + +/// Warp the mouse cursor to the specified screen position. +/// +/// # Thread Safety +/// This function affects global cursor state and acquires `CG_CURSOR_MUTEX`. +/// Callers must ensure no nested calls occur while the mutex is held. +/// +/// # Arguments +/// * `x` - X coordinate in screen points (macOS uses points, not pixels) +/// * `y` - Y coordinate in screen points +pub fn set_cursor_pos(x: i32, y: i32) -> bool { + // Acquire lock with deadlock detection in debug builds. + // In debug builds, try_lock detects re-entrant calls early; on failure we return immediately. + // In release builds, we use blocking lock() which will wait if contended. + #[cfg(debug_assertions)] + let _guard = match CG_CURSOR_MUTEX.try_lock() { + Ok(guard) => guard, + Err(std::sync::TryLockError::WouldBlock) => { + log::error!( + "[BUG] set_cursor_pos: CG_CURSOR_MUTEX is already held - potential deadlock!" + ); + debug_assert!(false, "Re-entrant call to set_cursor_pos detected"); + return false; + } + Err(std::sync::TryLockError::Poisoned(e)) => e.into_inner(), + }; + #[cfg(not(debug_assertions))] + let _guard = CG_CURSOR_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + unsafe { + let result = CGWarpMouseCursorPosition(CGPoint { + x: x as f64, + y: y as f64, + }); + if result != CGError::Success { + log::error!( + "CGWarpMouseCursorPosition({}, {}) returned error: {:?}", + x, + y, + result + ); + } + result == CGError::Success + } +} + +/// Toggle pointer lock (dissociate/associate mouse from cursor position). +/// +/// On macOS, cursor clipping is not supported directly like Windows ClipCursor. +/// Instead, we use CGAssociateMouseAndMouseCursorPosition to dissociate mouse +/// movement from cursor position, achieving a "pointer lock" effect. +/// +/// # Thread Safety +/// This function affects global cursor state and acquires `CG_CURSOR_MUTEX`. +/// Callers must ensure only one owner toggles pointer lock at a time; +/// nested Some/None transitions from different call sites may cause unexpected behavior. +/// +/// # Arguments +/// * `rect` - When `Some(_)`, dissociates mouse from cursor (enables pointer lock). +/// When `None`, re-associates mouse with cursor (disables pointer lock). +/// The rect coordinate values are ignored on macOS; only `Some`/`None` matters. +/// The parameter signature matches Windows for API consistency. +pub fn clip_cursor(rect: Option<(i32, i32, i32, i32)>) -> bool { + // Acquire lock with deadlock detection in debug builds. + // In debug builds, try_lock detects re-entrant calls early; on failure we return immediately. + // In release builds, we use blocking lock() which will wait if contended. + #[cfg(debug_assertions)] + let _guard = match CG_CURSOR_MUTEX.try_lock() { + Ok(guard) => guard, + Err(std::sync::TryLockError::WouldBlock) => { + log::error!("[BUG] clip_cursor: CG_CURSOR_MUTEX is already held - potential deadlock!"); + debug_assert!(false, "Re-entrant call to clip_cursor detected"); + return false; + } + Err(std::sync::TryLockError::Poisoned(e)) => e.into_inner(), + }; + #[cfg(not(debug_assertions))] + let _guard = CG_CURSOR_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + // CGAssociateMouseAndMouseCursorPosition takes a boolean_t: + // 1 (true) = associate mouse with cursor position (normal mode) + // 0 (false) = dissociate mouse from cursor position (pointer lock mode) + // When rect is Some, we want pointer lock (dissociate), so associate = false (0). + // When rect is None, we want normal mode (associate), so associate = true (1). + let associate: BooleanT = if rect.is_some() { 0 } else { 1 }; + unsafe { + let result = CGAssociateMouseAndMouseCursorPosition(associate); + if result != CGError::Success { + log::warn!( + "CGAssociateMouseAndMouseCursorPosition({}) returned error: {:?}", + associate, + result + ); + } + result == CGError::Success + } +} + +pub fn get_focused_display(displays: Vec) -> Option { + autoreleasepool(|| unsafe_get_focused_display(displays)) +} + +fn unsafe_get_focused_display(displays: Vec) -> Option { + unsafe { + let main_screen: id = msg_send![class!(NSScreen), mainScreen]; + let screen: id = msg_send![main_screen, deviceDescription]; + let id: id = + msg_send![screen, objectForKey: NSString::alloc(nil).init_str("NSScreenNumber")]; + let display_name: u32 = msg_send![id, unsignedIntValue]; + + displays + .iter() + .position(|d| d.name == display_name.to_string()) + } +} + +pub fn get_cursor() -> ResultType> { + autoreleasepool(|| unsafe_get_cursor()) +} + +fn unsafe_get_cursor() -> ResultType> { + unsafe { + let seed = CGSCurrentCursorSeed(); + if seed == LATEST_SEED { + return Ok(None); + } + LATEST_SEED = seed; + } + let c = get_cursor_id()?; + Ok(Some(c.1)) +} + +pub fn reset_input_cache() { + unsafe { + LATEST_SEED = 0; + } +} + +fn get_cursor_id() -> ResultType<(id, u64)> { + unsafe { + let c: id = msg_send![class!(NSCursor), currentSystemCursor]; + if c == nil { + bail!("Failed to call [NSCursor currentSystemCursor]"); + } + let hotspot: NSPoint = msg_send![c, hotSpot]; + let img: id = msg_send![c, image]; + if img == nil { + bail!("Failed to call [NSCursor image]"); + } + let size: NSSize = msg_send![img, size]; + let tif: id = msg_send![img, TIFFRepresentation]; + if tif == nil { + bail!("Failed to call [NSImage TIFFRepresentation]"); + } + let rep: id = msg_send![class!(NSBitmapImageRep), imageRepWithData: tif]; + if rep == nil { + bail!("Failed to call [NSBitmapImageRep imageRepWithData]"); + } + let rep_size: NSSize = msg_send![rep, size]; + let mut hcursor = + size.width + size.height + hotspot.x + hotspot.y + rep_size.width + rep_size.height; + let x = (rep_size.width * hotspot.x / size.width) as usize; + let y = (rep_size.height * hotspot.y / size.height) as usize; + for i in 0..2 { + let mut x2 = x + i; + if x2 >= rep_size.width as usize { + x2 = rep_size.width as usize - 1; + } + let mut y2 = y + i; + if y2 >= rep_size.height as usize { + y2 = rep_size.height as usize - 1; + } + let color: id = msg_send![rep, colorAtX:x2 y:y2]; + if color != nil { + let r: f64 = msg_send![color, redComponent]; + let g: f64 = msg_send![color, greenComponent]; + let b: f64 = msg_send![color, blueComponent]; + let a: f64 = msg_send![color, alphaComponent]; + hcursor += (r + g + b + a) * (255 << i) as f64; + } + } + Ok((c, hcursor as _)) + } +} + +pub fn get_cursor_data(hcursor: u64) -> ResultType { + autoreleasepool(|| unsafe_get_cursor_data(hcursor)) +} + +// https://github.com/stweil/OSXvnc/blob/master/OSXvnc-server/mousecursor.c +fn unsafe_get_cursor_data(hcursor: u64) -> ResultType { + unsafe { + let (c, hcursor2) = get_cursor_id()?; + if hcursor != hcursor2 { + bail!("cursor changed"); + } + let hotspot: NSPoint = msg_send![c, hotSpot]; + let img: id = msg_send![c, image]; + let size: NSSize = msg_send![img, size]; + let reps: id = msg_send![img, representations]; + if reps == nil { + bail!("Failed to call [NSImage representations]"); + } + let nreps: usize = msg_send![reps, count]; + if nreps == 0 { + bail!("Get empty [NSImage representations]"); + } + let rep: id = msg_send![reps, objectAtIndex: 0]; + /* + let n: id = msg_send![class!(NSNumber), numberWithFloat:1.0]; + let props: id = msg_send![class!(NSDictionary), dictionaryWithObject:n forKey:NSString::alloc(nil).init_str("NSImageCompressionFactor")]; + let image_data: id = msg_send![rep, representationUsingType:2 properties:props]; + let () = msg_send![image_data, writeToFile:NSString::alloc(nil).init_str("cursor.jpg") atomically:0]; + */ + let mut colors: Vec = Vec::new(); + colors.reserve((size.height * size.width) as usize * 4); + // TIFF is rgb colorspace, no need to convert + // let cs: id = msg_send![class!(NSColorSpace), sRGBColorSpace]; + for y in 0..(size.height as _) { + for x in 0..(size.width as _) { + let color: id = msg_send![rep, colorAtX:x as cocoa::foundation::NSInteger y:y as cocoa::foundation::NSInteger]; + // let color: id = msg_send![color, colorUsingColorSpace: cs]; + if color == nil { + continue; + } + let r: f64 = msg_send![color, redComponent]; + let g: f64 = msg_send![color, greenComponent]; + let b: f64 = msg_send![color, blueComponent]; + let a: f64 = msg_send![color, alphaComponent]; + colors.push((r * 255.) as _); + colors.push((g * 255.) as _); + colors.push((b * 255.) as _); + colors.push((a * 255.) as _); + } + } + Ok(CursorData { + id: hcursor, + colors: colors.into(), + hotx: hotspot.x as _, + hoty: hotspot.y as _, + width: size.width as _, + height: size.height as _, + ..Default::default() + }) + } +} + +fn get_active_user(t: &str) -> String { + if let Ok(output) = std::process::Command::new("ls") + .args(vec![t, "/dev/console"]) + .output() + { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if let Some(n) = line.split_whitespace().nth(2) { + return n.to_owned(); + } + } + } + "".to_owned() +} + +pub fn get_active_username() -> String { + get_active_user("-l") +} + +pub fn get_active_userid() -> String { + get_active_user("-n") +} + +pub fn get_active_user_home() -> Option { + let username = get_active_username(); + if !username.is_empty() { + let home = PathBuf::from(format!("/Users/{}", username)); + if home.exists() { + return Some(home); + } + } + None +} + +pub fn is_prelogin() -> bool { + get_active_userid() == "0" +} + +// https://stackoverflow.com/questions/11505255/osx-check-if-the-screen-is-locked +// No "CGSSessionScreenIsLocked" can be found when macOS is not locked. +// +// `ioreg -n Root -d1` returns `"CGSSessionScreenIsLocked"=Yes` +// `ioreg -n Root -d1 -a` returns +// ``` +// ... +// CGSSessionScreenIsLocked +// +// ... +// ``` +pub fn is_locked() -> bool { + match std::process::Command::new("ioreg") + .arg("-n") + .arg("Root") + .arg("-d1") + .output() + { + Ok(output) => { + let output_str = String::from_utf8_lossy(&output.stdout); + // Although `"CGSSessionScreenIsLocked"=Yes` was printed on my macOS, + // I also check `"CGSSessionScreenIsLocked"=true` for better compability. + output_str.contains("\"CGSSessionScreenIsLocked\"=Yes") + || output_str.contains("\"CGSSessionScreenIsLocked\"=true") + } + Err(e) => { + log::error!("Failed to query ioreg for the lock state: {}", e); + false + } + } +} + +pub fn is_root() -> bool { + crate::username() == "root" +} + +pub fn run_as_user(arg: Vec<&str>) -> ResultType> { + let uid = get_active_userid(); + let cmd = std::env::current_exe()?; + let mut args = vec!["asuser", &uid, cmd.to_str().unwrap_or("")]; + args.append(&mut arg.clone()); + let task = std::process::Command::new("launchctl").args(args).spawn()?; + Ok(Some(task)) +} + +pub fn lock_screen() { + std::process::Command::new( + "/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession", + ) + .arg("-suspend") + .output() + .ok(); +} + +pub fn start_os_service() { + log::info!("Username: {}", crate::username()); + if let Err(err) = crate::ipc::start("_service") { + log::error!("Failed to start ipc_service: {}", err); + } + + /* // mouse/keyboard works in prelogin now with launchctl asuser. + // below can avoid multi-users logged in problem, but having its own below problem. + // Not find a good way to start --cm without root privilege (affect file transfer). + // one way is to start with `launchctl asuser open -n -a /Applications/RustDesk.app/ --args --cm`, + // this way --cm is started with the user privilege, but we will have problem to start another RustDesk.app + // with open in explorer. + use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }; + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + let mut uid = "".to_owned(); + let mut server: Option = None; + if let Err(err) = ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) { + println!("Failed to set Ctrl-C handler: {}", err); + } + while running.load(Ordering::SeqCst) { + let tmp = get_active_userid(); + let mut start_new = false; + if tmp != uid && !tmp.is_empty() { + uid = tmp; + log::info!("active uid: {}", uid); + if let Some(ps) = server.as_mut() { + hbb_common::allow_err!(ps.kill()); + } + } + if let Some(ps) = server.as_mut() { + match ps.try_wait() { + Ok(Some(_)) => { + server = None; + start_new = true; + } + _ => {} + } + } else { + start_new = true; + } + if start_new { + match run_as_user("--server") { + Ok(Some(ps)) => server = Some(ps), + Err(err) => { + log::error!("Failed to start server: {}", err); + } + _ => { /*no happen*/ } + } + } + std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); + } + + if let Some(ps) = server.take().as_mut() { + hbb_common::allow_err!(ps.kill()); + } + log::info!("Exit"); + */ +} + +pub fn toggle_blank_screen(_v: bool) { + // https://unix.stackexchange.com/questions/17115/disable-keyboard-mouse-temporarily +} + +pub fn block_input(_v: bool) -> (bool, String) { + (true, "".to_owned()) +} + +pub fn is_installed() -> bool { + if let Ok(p) = std::env::current_exe() { + return p + .to_str() + .unwrap_or_default() + .starts_with(&format!("/Applications/{}.app", crate::get_app_name())); + } + false +} + +pub fn quit_gui() { + unsafe { + let () = msg_send!(NSApp(), terminate: nil); + }; +} + +#[inline] +pub fn try_remove_temp_update_dir(dir: Option<&str>) { + let target_path_buf = dir.map(PathBuf::from).unwrap_or_else(get_update_temp_dir); + let target_path = target_path_buf.as_path(); + if target_path.exists() { + std::fs::remove_dir_all(target_path).ok(); + } +} + +pub fn update_me() -> ResultType<()> { + let is_installed_daemon = is_installed_daemon(false); + let option_stop_service = "stop-service"; + let is_service_stopped = hbb_common::config::option2bool( + option_stop_service, + &crate::ui_interface::get_option(option_stop_service), + ); + + let cmd = std::env::current_exe()?; + // RustDesk.app/Contents/MacOS/RustDesk + let app_dir = cmd + .parent() + .and_then(|p| p.parent()) + .and_then(|p| p.parent()) + .map(|d| d.to_string_lossy().to_string()); + let Some(app_dir) = app_dir else { + bail!("Unknown app directory of current exe file: {:?}", cmd); + }; + + let app_name = crate::get_app_name(); + if is_installed_daemon && !is_service_stopped { + let agent = format!("{}_server.plist", crate::get_full_name()); + let agent_plist_file = format!("/Library/LaunchAgents/{}", agent); + update_daemon_agent(agent_plist_file, app_dir, true); + } else { + // `kill -9` may not work without "administrator privileges" + let update_body = r#" +on run {app_name, cur_pid, app_dir, user_name} + set app_bundle to "/Applications/" & app_name & ".app" + set app_bundle_q to quoted form of app_bundle + set app_dir_q to quoted form of app_dir + set user_name_q to quoted form of user_name + + set check_source to "test -d " & app_dir_q & " || exit 1;" + set kill_others to "pids=$(pgrep -x '" & app_name & "' | grep -vx " & cur_pid & " || true); if [ -n \"$pids\" ]; then echo \"$pids\" | xargs kill -9 || true; fi;" + set copy_files to "rm -rf " & app_bundle_q & " && ditto " & app_dir_q & " " & app_bundle_q & " && chown -R " & user_name_q & ":staff " & app_bundle_q & " && (xattr -r -d com.apple.quarantine " & app_bundle_q & " || true);" + set sh to "set -e;" & check_source & kill_others & copy_files + + do shell script sh with prompt app_name & " wants to update itself" with administrator privileges +end run + "#; + let active_user = get_active_username(); + let status = Command::new("osascript") + .arg("-e") + .arg(update_body) + .arg(app_name.to_string()) + .arg(std::process::id().to_string()) + .arg(app_dir) + .arg(active_user) + .status(); + match status { + Ok(status) if !status.success() => { + log::error!("osascript execution failed with status: {}", status); + } + Err(e) => { + log::error!("run osascript failed: {}", e); + } + _ => {} + } + } + std::process::Command::new("open") + .arg("-n") + .arg(&format!("/Applications/{}.app", app_name)) + .spawn() + .ok(); + // leave open a little time + std::thread::sleep(std::time::Duration::from_millis(300)); + Ok(()) +} + +pub fn update_from_dmg(dmg_path: &str) -> ResultType<()> { + let update_temp_dir = get_update_temp_dir_string(); + println!("Starting update from DMG: {}", dmg_path); + extract_dmg(dmg_path, &update_temp_dir)?; + println!("DMG extracted"); + update_extracted(&update_temp_dir)?; + println!("Update process started"); + Ok(()) +} + +pub fn update_to(_file: &str) -> ResultType<()> { + let update_temp_dir = get_update_temp_dir_string(); + update_extracted(&update_temp_dir)?; + Ok(()) +} + +pub fn extract_update_dmg(file: &str) { + let update_temp_dir = get_update_temp_dir_string(); + let mut evt: HashMap<&str, String> = + HashMap::from([("name", "extract-update-dmg".to_string())]); + match extract_dmg(file, &update_temp_dir) { + Ok(_) => { + log::info!("Extracted dmg file to {}", update_temp_dir); + } + Err(e) => { + evt.insert("err", e.to_string()); + log::error!("Failed to extract dmg file {}: {}", file, e); + } + } + let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned()); + #[cfg(feature = "flutter")] + crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt); +} + +fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { + let mount_point = "/Volumes/RustDeskUpdate"; + let target_path = Path::new(target_dir); + + if target_path.exists() { + std::fs::remove_dir_all(target_path)?; + } + std::fs::create_dir_all(target_path)?; + + let status = Command::new("hdiutil") + .args(&["attach", "-nobrowse", "-mountpoint", mount_point, dmg_path]) + .status()?; + + if !status.success() { + bail!("Failed to attach DMG image at {}: {:?}", dmg_path, status); + } + + struct DmgGuard(&'static str); + impl Drop for DmgGuard { + fn drop(&mut self) { + let _ = Command::new("hdiutil") + .args(&["detach", self.0, "-force"]) + .status(); + } + } + let _guard = DmgGuard(mount_point); + + let app_name = format!("{}.app", crate::get_app_name()); + let src_path = format!("{}/{}", mount_point, app_name); + let dest_path = format!("{}/{}", target_dir, app_name); + + let copy_status = Command::new("ditto") + .args(&[&src_path, &dest_path]) + .status()?; + + if !copy_status.success() { + bail!( + "Failed to copy application from {} to {}: {:?}", + src_path, + dest_path, + copy_status + ); + } + + if !Path::new(&dest_path).exists() { + bail!( + "Copy operation failed - destination not found at {}", + dest_path + ); + } + + Ok(()) +} + +fn update_extracted(target_dir: &str) -> ResultType<()> { + let app_name = crate::get_app_name(); + let exe_path = format!( + "{}/{}.app/Contents/MacOS/{}", + target_dir, app_name, app_name + ); + let _child = unsafe { + if let Err(e) = Command::new(&exe_path) + .arg("--update") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .pre_exec(|| { + hbb_common::libc::setsid(); + Ok(()) + }) + .spawn() + { + try_remove_temp_update_dir(Some(target_dir)); + bail!(e); + } + }; + Ok(()) +} + +pub fn get_double_click_time() -> u32 { + // to-do: https://github.com/servo/core-foundation-rs/blob/786895643140fa0ee4f913d7b4aeb0c4626b2085/cocoa/src/appkit.rs#L2823 + 500 as _ +} + +pub fn hide_dock() { + unsafe { + NSApp().setActivationPolicy_(NSApplicationActivationPolicyAccessory); + } +} + +#[inline] +#[allow(dead_code)] +fn get_server_start_time_of(p: &Process, path: &Path) -> Option { + let cmd = p.cmd(); + if cmd.len() <= 1 { + return None; + } + if &cmd[1] != "--server" { + return None; + } + let Ok(cur) = std::fs::canonicalize(p.exe()) else { + return None; + }; + if &cur != path { + return None; + } + Some(p.start_time() as _) +} + +#[inline] +#[allow(dead_code)] +fn get_server_start_time(sys: &mut System, path: &Path) -> Option<(i64, Pid)> { + sys.refresh_processes_specifics(ProcessRefreshKind::new()); + for (_, p) in sys.processes() { + if let Some(t) = get_server_start_time_of(p, path) { + return Some((t, p.pid() as _)); + } + } + None +} + +pub fn handle_application_should_open_untitled_file() { + hbb_common::log::debug!("icon clicked on finder"); + let x = std::env::args().nth(1).unwrap_or_default(); + if x == "--server" || x == "--cm" || x == "--tray" { + std::thread::spawn(move || crate::handle_url_scheme("".to_lowercase())); + } +} + +/// Get all resolutions of the display. The resolutions are: +/// 1. Sorted by width and height in descending order, with duplicates removed. +/// 2. Filtered out if the width is less than 800 (800x600) if there are too many (e.g., >15). +/// 3. Contain HiDPI resolutions and the real resolutions. +/// +/// We don't need to distinguish between HiDPI and real resolutions. +/// When the controlling side changes the resolution, it will call `change_resolution_directly()`. +/// `change_resolution_directly()` will try to use the HiDPI resolution first. +/// This is how teamviewer does it for now. +/// +/// If we need to distinguish HiDPI and real resolutions, we can add a flag to the `Resolution` struct. +pub fn resolutions(name: &str) -> Vec { + let mut v = vec![]; + if let Ok(display) = name.parse::() { + let mut num = 0; + unsafe { + if YES == MacGetModeNum(display, &mut num) { + let (mut widths, mut heights, mut _hidpis) = + (vec![0; num as _], vec![0; num as _], vec![NO; num as _]); + let mut real_num = 0; + if YES + == MacGetModes( + display, + widths.as_mut_ptr(), + heights.as_mut_ptr(), + _hidpis.as_mut_ptr(), + num, + &mut real_num, + ) + { + if real_num <= num { + v = (0..real_num) + .map(|i| Resolution { + width: widths[i as usize] as _, + height: heights[i as usize] as _, + ..Default::default() + }) + .collect::>(); + // Sort by (w, h), desc + v.sort_by(|a, b| { + if a.width == b.width { + b.height.cmp(&a.height) + } else { + b.width.cmp(&a.width) + } + }); + // Remove duplicates + v.dedup_by(|a, b| a.width == b.width && a.height == b.height); + // Filter out the ones that are less than width 800 (800x600) if there are too many. + // We can also do this filtering on the client side, but it is better not to change the client side to reduce the impact. + if v.len() > 15 { + // Most width > 800, so it's ok to remove the small ones. + v.retain(|r| r.width >= 800); + } + if v.len() > 15 { + // Ignore if the length is still too long. + } + } + } + } + } + } + v +} + +pub fn current_resolution(name: &str) -> ResultType { + let display = name.parse::().map_err(|e| anyhow!(e))?; + unsafe { + let (mut width, mut height) = (0, 0); + if NO == MacGetMode(display, &mut width, &mut height) { + bail!("MacGetMode failed"); + } + Ok(Resolution { + width: width as _, + height: height as _, + ..Default::default() + }) + } +} + +pub fn change_resolution_directly(name: &str, width: usize, height: usize) -> ResultType<()> { + let display = name.parse::().map_err(|e| anyhow!(e))?; + unsafe { + if NO == MacSetMode(display, width as _, height as _, true) { + bail!("MacSetMode failed"); + } + } + Ok(()) +} + +pub fn check_super_user_permission() -> ResultType { + unsafe { Ok(MacCheckAdminAuthorization() == YES) } +} + +pub fn elevate(args: Vec<&str>, prompt: &str) -> ResultType { + let cmd = std::env::current_exe()?; + match cmd.to_str() { + Some(cmd) => { + let mut cmd_with_args = cmd.to_string(); + for arg in args { + cmd_with_args = format!("{} {}", cmd_with_args, arg); + } + let script = format!( + r#"do shell script "{}" with prompt "{}" with administrator privileges"#, + cmd_with_args, prompt + ); + match std::process::Command::new("osascript") + .arg("-e") + .arg(script) + .arg(&get_active_username()) + .status() + { + Err(e) => { + bail!("Failed to run osascript: {}", e); + } + Ok(status) => Ok(status.success() && status.code() == Some(0)), + } + } + None => { + bail!("Failed to get current exe str"); + } + } +} + +pub struct WakeLock(Option); + +impl WakeLock { + pub fn new(display: bool, idle: bool, sleep: bool) -> Self { + WakeLock( + keepawake::Builder::new() + .display(display) + .idle(idle) + .sleep(sleep) + .create() + .ok(), + ) + } + + pub fn set_display(&mut self, display: bool) -> ResultType<()> { + self.0 + .as_mut() + .map(|h| h.set_display(display)) + .ok_or(anyhow!("no AwakeHandle"))? + } +} + +fn get_bundle_id() -> Option { + unsafe { + let bundle: id = msg_send![class!(NSBundle), mainBundle]; + if bundle.is_null() { + return None; + } + + let bundle_id: id = msg_send![bundle, bundleIdentifier]; + if bundle_id.is_null() { + return None; + } + + let c_str: *const std::os::raw::c_char = msg_send![bundle_id, UTF8String]; + if c_str.is_null() { + return None; + } + + let bundle_id_str = std::ffi::CStr::from_ptr(c_str) + .to_string_lossy() + .to_string(); + Some(bundle_id_str) + } +} diff --git a/vendor/rustdesk/src/platform/mod.rs b/vendor/rustdesk/src/platform/mod.rs new file mode 100644 index 0000000..c1bc382 --- /dev/null +++ b/vendor/rustdesk/src/platform/mod.rs @@ -0,0 +1,248 @@ +#[cfg(target_os = "linux")] +pub use linux::*; +#[cfg(target_os = "macos")] +pub use macos::*; +#[cfg(windows)] +pub use windows::*; + +#[cfg(windows)] +pub mod windows; + +#[cfg(windows)] +pub mod win_device; + +#[cfg(target_os = "macos")] +pub mod macos; + +#[cfg(target_os = "macos")] +pub mod delegate; + +#[cfg(target_os = "linux")] +pub mod linux; + +#[cfg(target_os = "linux")] +pub mod linux_desktop_manager; + +#[cfg(target_os = "linux")] +pub mod gtk_sudo; + +#[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 std::sync::{Arc, Mutex}; +#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))] +pub const SERVICE_INTERVAL: u64 = 300; + +lazy_static::lazy_static! { + static ref INSTALLING_SERVICE: Arc>= Default::default(); +} + +pub fn installing_service() -> bool { + INSTALLING_SERVICE.lock().unwrap().clone() +} + +pub fn is_xfce() -> bool { + #[cfg(target_os = "linux")] + { + return std::env::var_os("XDG_CURRENT_DESKTOP") == Some(std::ffi::OsString::from("XFCE")); + } + #[cfg(not(target_os = "linux"))] + { + return false; + } +} + +pub fn breakdown_callback() { + #[cfg(target_os = "linux")] + crate::input_service::clear_remapped_keycode(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::input_service::release_device_modifiers(); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + let cur_resolution = current_resolution(name)?; + // For MacOS + // to-do: Make sure the following comparison works. + // For Linux + // Just run "xrandr", dpi may not be taken into consideration. + // For Windows + // dmPelsWidth and dmPelsHeight is the same to width and height + // Because this process is running in dpi awareness mode. + if cur_resolution.width as usize == width && cur_resolution.height as usize == height { + return Ok(()); + } + hbb_common::log::warn!("Change resolution of '{}' to ({},{})", name, width, height); + change_resolution_directly(name, width, height) +} + +// Android +#[cfg(target_os = "android")] +pub fn get_active_username() -> String { + // TODO + "android".into() +} + +#[cfg(target_os = "android")] +pub const PA_SAMPLE_RATE: u32 = 48000; + +#[cfg(target_os = "android")] +#[derive(Default)] +pub struct WakeLock(Option); + +#[cfg(target_os = "android")] +impl WakeLock { + pub fn new(tag: &str) -> Self { + let tag = format!("{}:{tag}", crate::get_app_name()); + match android_wakelock::partial(tag) { + Ok(lock) => Self(Some(lock)), + Err(e) => { + hbb_common::log::error!("Failed to get wakelock: {e:?}"); + Self::default() + } + } + } +} + +#[cfg(not(target_os = "ios"))] +pub fn get_wakelock(_display: bool) -> WakeLock { + hbb_common::log::info!("new wakelock, require display on: {_display}"); + #[cfg(target_os = "android")] + return crate::platform::WakeLock::new("server"); + // display: keep screen on + // idle: keep cpu on + // sleep: prevent system from sleeping, even manually + #[cfg(not(target_os = "android"))] + return crate::platform::WakeLock::new(_display, true, false); +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +pub(crate) struct InstallingService; // please use new + +#[cfg(any(target_os = "windows", target_os = "linux"))] +impl InstallingService { + pub fn new() -> Self { + *INSTALLING_SERVICE.lock().unwrap() = true; + Self + } +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +impl Drop for InstallingService { + fn drop(&mut self) { + *INSTALLING_SERVICE.lock().unwrap() = false; + } +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +#[inline] +pub fn is_prelogin() -> bool { + false +} + +// Note: This method is inefficient on Windows. It will get all the processes. +// It should only be called when performance is not critical. +// If we wanted to get the command line ourselves, there would be a lot of new code. +#[allow(dead_code)] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn get_pids_of_process_with_args, S2: AsRef>( + name: S1, + args: &[S2], +) -> Vec { + // This function does not work when the process is 32-bit and the OS is 64-bit Windows, + // `process.cmd()` always returns [] in this case. + // So we use `windows::get_pids_with_args_by_wmic()` instead. + #[cfg(all(target_os = "windows", not(target_pointer_width = "64")))] + { + return windows::get_pids_with_args_by_wmic(name, args); + } + #[cfg(not(all(target_os = "windows", not(target_pointer_width = "64"))))] + { + let name = name.as_ref().to_lowercase(); + let system = System::new_all(); + system + .processes() + .iter() + .filter(|(_, process)| { + process.name().to_lowercase() == name + && process.cmd().len() == args.len() + 1 + && args.iter().enumerate().all(|(i, arg)| { + process.cmd()[i + 1].to_lowercase() == arg.as_ref().to_lowercase() + }) + }) + .map(|(&pid, _)| pid) + .collect() + } +} + +// Note: This method is inefficient on Windows. It will get all the processes. +// It should only be called when performance is not critical. +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_pids_of_process_with_first_arg, S2: AsRef>( + name: S1, + arg: S2, +) -> Vec { + // This function does not work when the process is 32-bit and the OS is 64-bit Windows, + // `process.cmd()` always returns [] in this case. + // So we use `windows::get_pids_with_first_arg_by_wmic()` instead. + #[cfg(all(target_os = "windows", not(target_pointer_width = "64")))] + { + return windows::get_pids_with_first_arg_by_wmic(name, arg); + } + #[cfg(not(all(target_os = "windows", not(target_pointer_width = "64"))))] + { + let name = name.as_ref().to_lowercase(); + let system = System::new_all(); + system + .processes() + .iter() + .filter(|(_, process)| { + process.name().to_lowercase() == name + && process.cmd().len() >= 2 + && process.cmd()[1].to_lowercase() == arg.as_ref().to_lowercase() + }) + .map(|(&pid, _)| pid) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_cursor_data() { + for _ in 0..30 { + if let Some(hc) = get_cursor().unwrap() { + let cd = get_cursor_data(hc).unwrap(); + repng::encode( + std::fs::File::create("cursor.png").unwrap(), + cd.width as _, + cd.height as _, + &cd.colors[..], + ) + .unwrap(); + } + #[cfg(target_os = "macos")] + macos::is_process_trusted(false); + } + } + #[test] + fn test_get_cursor_pos() { + for _ in 0..30 { + assert!(!get_cursor_pos().is_none()); + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[test] + fn test_resolution() { + let name = r"\\.\DISPLAY1"; + println!("current:{:?}", current_resolution(name)); + println!("change:{:?}", change_resolution(name, 2880, 1800)); + println!("resolutions:{:?}", resolutions(name)); + } +} diff --git a/vendor/rustdesk/src/platform/privileges_scripts/agent.plist b/vendor/rustdesk/src/platform/privileges_scripts/agent.plist new file mode 100644 index 0000000..28f9c02 --- /dev/null +++ b/vendor/rustdesk/src/platform/privileges_scripts/agent.plist @@ -0,0 +1,37 @@ + + + + + Label + com.carriez.RustDesk_server + + AssociatedBundleIdentifiers + com.carriez.rustdesk + + LimitLoadToSessionType + + LoginWindow + Aqua + + KeepAlive + + SuccessfulExit + + AfterInitialDemand + + + ThrottleInterval + 1 + RunAtLoad + + ProgramArguments + + /Applications/RustDesk.app/Contents/MacOS/RustDesk + --server + + WorkingDirectory + /Applications/RustDesk.app/Contents/MacOS/ + ProcessType + Interactive + + diff --git a/vendor/rustdesk/src/platform/privileges_scripts/daemon.plist b/vendor/rustdesk/src/platform/privileges_scripts/daemon.plist new file mode 100644 index 0000000..c003ea2 --- /dev/null +++ b/vendor/rustdesk/src/platform/privileges_scripts/daemon.plist @@ -0,0 +1,30 @@ + + + + + Label + com.carriez.RustDesk_service + + AssociatedBundleIdentifiers + com.carriez.rustdesk + + KeepAlive + + ThrottleInterval + 1 + ProgramArguments + + /bin/sh + -c + /Applications/RustDesk.app/Contents/MacOS/service + + RunAtLoad + + WorkingDirectory + /Applications/RustDesk.app/Contents/MacOS/ + StandardErrorPath + /tmp/rustdesk_service.err + StandardOutPath + /tmp/rustdesk_service.out + + diff --git a/vendor/rustdesk/src/platform/privileges_scripts/install.scpt b/vendor/rustdesk/src/platform/privileges_scripts/install.scpt new file mode 100644 index 0000000..797d02c --- /dev/null +++ b/vendor/rustdesk/src/platform/privileges_scripts/install.scpt @@ -0,0 +1,16 @@ +on run {daemon_file, agent_file, user} + + set sh1 to "echo " & quoted form of daemon_file & " > /Library/LaunchDaemons/com.carriez.RustDesk_service.plist && chown root:wheel /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" + + set sh2 to "echo " & quoted form of agent_file & " > /Library/LaunchAgents/com.carriez.RustDesk_server.plist && chown root:wheel /Library/LaunchAgents/com.carriez.RustDesk_server.plist;" + + set sh3 to "cp -rf /Users/" & user & "/Library/Preferences/com.carriez.RustDesk/RustDesk.toml /var/root/Library/Preferences/com.carriez.RustDesk/;" + + set sh4 to "cp -rf /Users/" & user & "/Library/Preferences/com.carriez.RustDesk/RustDesk2.toml /var/root/Library/Preferences/com.carriez.RustDesk/;" + + set sh5 to "launchctl load -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" + + set sh to sh1 & sh2 & sh3 & sh4 & sh5 + + do shell script sh with prompt "RustDesk wants to install daemon and agent" with administrator privileges +end run diff --git a/vendor/rustdesk/src/platform/privileges_scripts/uninstall.scpt b/vendor/rustdesk/src/platform/privileges_scripts/uninstall.scpt new file mode 100644 index 0000000..4a19fb3 --- /dev/null +++ b/vendor/rustdesk/src/platform/privileges_scripts/uninstall.scpt @@ -0,0 +1,6 @@ +set sh1 to "launchctl unload -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" +set sh2 to "/bin/rm /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" +set sh3 to "/bin/rm /Library/LaunchAgents/com.carriez.RustDesk_server.plist;" + +set sh to sh1 & sh2 & sh3 +do shell script sh with prompt "RustDesk wants to unload daemon" with administrator privileges \ No newline at end of file diff --git a/vendor/rustdesk/src/platform/privileges_scripts/update.scpt b/vendor/rustdesk/src/platform/privileges_scripts/update.scpt new file mode 100644 index 0000000..0484c25 --- /dev/null +++ b/vendor/rustdesk/src/platform/privileges_scripts/update.scpt @@ -0,0 +1,26 @@ +on run {daemon_file, agent_file, user, cur_pid, source_dir} + + set agent_plist to "/Library/LaunchAgents/com.carriez.RustDesk_server.plist" + set daemon_plist to "/Library/LaunchDaemons/com.carriez.RustDesk_service.plist" + set app_bundle to "/Applications/RustDesk.app" + + set check_source to "test -d " & quoted form of source_dir & " || exit 1;" + set resolve_uid to "uid=$(id -u " & quoted form of user & " 2>/dev/null || true);" + set unload_agent to "if [ -n \"$uid\" ]; then launchctl bootout gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootout user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl unload -w " & quoted form of agent_plist & " || true; else launchctl unload -w " & quoted form of agent_plist & " || true; fi;" + set unload_service to "launchctl unload -w " & daemon_plist & " || true;" + set kill_others to "pids=$(pgrep -x 'RustDesk' | grep -vx " & cur_pid & " || true); if [ -n \"$pids\" ]; then echo \"$pids\" | xargs kill -9 || true; fi;" + + set copy_files to "(rm -rf " & quoted form of app_bundle & " && ditto " & quoted form of source_dir & " " & quoted form of app_bundle & " && chown -R " & quoted form of user & ":staff " & quoted form of app_bundle & " && (xattr -r -d com.apple.quarantine " & quoted form of app_bundle & " || true)) || exit 1;" + + set write_daemon_plist to "echo " & quoted form of daemon_file & " > " & daemon_plist & " && chown root:wheel " & daemon_plist & ";" + set write_agent_plist to "echo " & quoted form of agent_file & " > " & agent_plist & " && chown root:wheel " & agent_plist & ";" + set load_service to "launchctl load -w " & daemon_plist & ";" + set agent_label_cmd to "agent_label=$(basename " & quoted form of agent_plist & " .plist);" + set bootstrap_agent to "if [ -n \"$uid\" ]; then launchctl bootstrap gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootstrap user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl load -w " & quoted form of agent_plist & " || true; else launchctl load -w " & quoted form of agent_plist & " || true; fi;" + set kickstart_agent to "if [ -n \"$uid\" ]; then launchctl kickstart -k gui/$uid/$agent_label 2>/dev/null || launchctl kickstart -k user/$uid/$agent_label 2>/dev/null || true; fi;" + set load_agent to agent_label_cmd & bootstrap_agent & kickstart_agent + + set sh to "set -e;" & check_source & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service & load_agent + + do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges +end run diff --git a/vendor/rustdesk/src/platform/win_device.rs b/vendor/rustdesk/src/platform/win_device.rs new file mode 100644 index 0000000..2650065 --- /dev/null +++ b/vendor/rustdesk/src/platform/win_device.rs @@ -0,0 +1,459 @@ +use hbb_common::{log, thiserror}; +use std::{ + ffi::OsStr, + io, + ops::{Deref, DerefMut}, + os::windows::ffi::OsStrExt, + ptr::null_mut, + result::Result, +}; +use winapi::{ + shared::{ + guiddef::GUID, + minwindef::{BOOL, DWORD, FALSE, MAX_PATH, PBOOL, TRUE}, + ntdef::{HANDLE, LPCWSTR, NULL}, + windef::HWND, + winerror::{ERROR_INSUFFICIENT_BUFFER, ERROR_NO_MORE_ITEMS}, + }, + um::{ + cfgmgr32::MAX_DEVICE_ID_LEN, + fileapi::{CreateFileW, OPEN_EXISTING}, + handleapi::{CloseHandle, INVALID_HANDLE_VALUE}, + ioapiset::DeviceIoControl, + setupapi::*, + winnt::{GENERIC_READ, GENERIC_WRITE}, + }, +}; + +#[link(name = "Newdev")] +extern "system" { + fn UpdateDriverForPlugAndPlayDevicesW( + hwnd_parent: HWND, + hardware_id: LPCWSTR, + full_inf_path: LPCWSTR, + install_flags: DWORD, + b_reboot_required: PBOOL, + ) -> BOOL; +} + +#[derive(thiserror::Error, Debug)] +pub enum DeviceError { + #[error("Failed to call {0}, {1:?}")] + WinApiLastErr(String, io::Error), + #[error("Failed to call {0}, returns {1}")] + WinApiErrCode(String, DWORD), + #[error("{0}")] + Raw(String), +} + +impl DeviceError { + #[inline] + fn new_api_last_err(api: &str) -> Self { + Self::WinApiLastErr(api.to_string(), io::Error::last_os_error()) + } +} + +struct DeviceInfo(HDEVINFO); + +impl DeviceInfo { + fn setup_di_create_device_info_list(class_guid: &mut GUID) -> Result { + let dev_info = unsafe { SetupDiCreateDeviceInfoList(class_guid, null_mut()) }; + if dev_info == null_mut() { + return Err(DeviceError::new_api_last_err("SetupDiCreateDeviceInfoList")); + } + + Ok(Self(dev_info)) + } + + fn setup_di_get_class_devs_ex_w( + class_guid: *const GUID, + flags: DWORD, + ) -> Result { + let dev_info = unsafe { + SetupDiGetClassDevsExW( + class_guid, + null_mut(), + null_mut(), + flags, + null_mut(), + null_mut(), + null_mut(), + ) + }; + if dev_info == null_mut() { + return Err(DeviceError::new_api_last_err("SetupDiGetClassDevsExW")); + } + Ok(Self(dev_info)) + } +} + +impl Drop for DeviceInfo { + fn drop(&mut self) { + unsafe { + SetupDiDestroyDeviceInfoList(self.0); + } + } +} + +impl Deref for DeviceInfo { + type Target = HDEVINFO; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for DeviceInfo { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub unsafe fn install_driver( + inf_path: &str, + hardware_id: &str, + reboot_required: &mut bool, +) -> Result<(), DeviceError> { + let driver_inf_path = OsStr::new(inf_path) + .encode_wide() + .chain(Some(0).into_iter()) + .collect::>(); + let hardware_id = OsStr::new(hardware_id) + .encode_wide() + .chain(Some(0).into_iter()) + .collect::>(); + + let mut class_guid: GUID = std::mem::zeroed(); + let mut class_name: [u16; 32] = [0; 32]; + + if SetupDiGetINFClassW( + driver_inf_path.as_ptr(), + &mut class_guid, + class_name.as_mut_ptr(), + class_name.len() as _, + null_mut(), + ) == FALSE + { + return Err(DeviceError::new_api_last_err("SetupDiGetINFClassW")); + } + + let dev_info = DeviceInfo::setup_di_create_device_info_list(&mut class_guid)?; + + let mut dev_info_data = SP_DEVINFO_DATA { + cbSize: std::mem::size_of::() as _, + ClassGuid: class_guid, + DevInst: 0, + Reserved: 0, + }; + if SetupDiCreateDeviceInfoW( + *dev_info, + class_name.as_ptr(), + &class_guid, + null_mut(), + null_mut(), + DICD_GENERATE_ID, + &mut dev_info_data, + ) == FALSE + { + return Err(DeviceError::new_api_last_err("SetupDiCreateDeviceInfoW")); + } + + if SetupDiSetDeviceRegistryPropertyW( + *dev_info, + &mut dev_info_data, + SPDRP_HARDWAREID, + hardware_id.as_ptr() as _, + (hardware_id.len() * 2) as _, + ) == FALSE + { + return Err(DeviceError::new_api_last_err( + "SetupDiSetDeviceRegistryPropertyW", + )); + } + + if SetupDiCallClassInstaller(DIF_REGISTERDEVICE, *dev_info, &mut dev_info_data) == FALSE { + return Err(DeviceError::new_api_last_err("SetupDiCallClassInstaller")); + } + + let mut reboot_required_ = FALSE; + if UpdateDriverForPlugAndPlayDevicesW( + null_mut(), + hardware_id.as_ptr(), + driver_inf_path.as_ptr(), + 1, + &mut reboot_required_, + ) == FALSE + { + return Err(DeviceError::new_api_last_err( + "UpdateDriverForPlugAndPlayDevicesW", + )); + } + *reboot_required = reboot_required_ == TRUE; + + Ok(()) +} + +unsafe fn is_same_hardware_id( + dev_info: &DeviceInfo, + devinfo_data: &mut SP_DEVINFO_DATA, + hardware_id: &str, +) -> Result { + let mut cur_hardware_id = [0u16; MAX_DEVICE_ID_LEN]; + if SetupDiGetDeviceRegistryPropertyW( + **dev_info, + devinfo_data, + SPDRP_HARDWAREID, + null_mut(), + cur_hardware_id.as_mut_ptr() as _, + cur_hardware_id.len() as _, + null_mut(), + ) == FALSE + { + return Err(DeviceError::new_api_last_err( + "SetupDiGetDeviceRegistryPropertyW", + )); + } + + let cur_hardware_id = String::from_utf16_lossy(&cur_hardware_id) + .trim_end_matches(char::from(0)) + .to_string(); + Ok(cur_hardware_id == hardware_id) +} + +pub unsafe fn uninstall_driver( + hardware_id: &str, + reboot_required: &mut bool, +) -> Result<(), DeviceError> { + let dev_info = + DeviceInfo::setup_di_get_class_devs_ex_w(null_mut(), DIGCF_ALLCLASSES | DIGCF_PRESENT)?; + + let mut device_info_list_detail = SP_DEVINFO_LIST_DETAIL_DATA_W { + cbSize: std::mem::size_of::() as _, + ClassGuid: std::mem::zeroed(), + RemoteMachineHandle: null_mut(), + RemoteMachineName: [0; SP_MAX_MACHINENAME_LENGTH], + }; + if SetupDiGetDeviceInfoListDetailW(*dev_info, &mut device_info_list_detail) == FALSE { + return Err(DeviceError::new_api_last_err( + "SetupDiGetDeviceInfoListDetailW", + )); + } + + let mut devinfo_data = SP_DEVINFO_DATA { + cbSize: std::mem::size_of::() as _, + ClassGuid: std::mem::zeroed(), + DevInst: 0, + Reserved: 0, + }; + + let mut device_index = 0; + loop { + if SetupDiEnumDeviceInfo(*dev_info, device_index, &mut devinfo_data) == FALSE { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(ERROR_NO_MORE_ITEMS as _) { + break; + } + return Err(DeviceError::WinApiLastErr( + "SetupDiEnumDeviceInfo".to_string(), + err, + )); + } + + match is_same_hardware_id(&dev_info, &mut devinfo_data, hardware_id) { + Ok(false) => { + device_index += 1; + continue; + } + Err(e) => { + log::error!("Failed to call is_same_hardware_id, {:?}", e); + device_index += 1; + continue; + } + _ => {} + } + + let mut remove_device_params = SP_REMOVEDEVICE_PARAMS { + ClassInstallHeader: SP_CLASSINSTALL_HEADER { + cbSize: std::mem::size_of::() as _, + InstallFunction: DIF_REMOVE, + }, + Scope: DI_REMOVEDEVICE_GLOBAL, + HwProfile: 0, + }; + + if SetupDiSetClassInstallParamsW( + *dev_info, + &mut devinfo_data, + &mut remove_device_params.ClassInstallHeader, + std::mem::size_of::() as _, + ) == FALSE + { + return Err(DeviceError::new_api_last_err( + "SetupDiSetClassInstallParams", + )); + } + + if SetupDiCallClassInstaller(DIF_REMOVE, *dev_info, &mut devinfo_data) == FALSE { + return Err(DeviceError::new_api_last_err("SetupDiCallClassInstaller")); + } + + let mut device_params = SP_DEVINSTALL_PARAMS_W { + cbSize: std::mem::size_of::() as _, + Flags: 0, + FlagsEx: 0, + hwndParent: null_mut(), + InstallMsgHandler: None, + InstallMsgHandlerContext: null_mut(), + FileQueue: null_mut(), + ClassInstallReserved: 0, + Reserved: 0, + DriverPath: [0; MAX_PATH], + }; + + if SetupDiGetDeviceInstallParamsW(*dev_info, &mut devinfo_data, &mut device_params) == FALSE + { + log::error!( + "Failed to call SetupDiGetDeviceInstallParamsW, {:?}", + io::Error::last_os_error() + ); + } else { + if device_params.Flags & (DI_NEEDRESTART | DI_NEEDREBOOT) != 0 { + *reboot_required = true; + } + } + + device_index += 1; + } + + Ok(()) +} + +pub unsafe fn device_io_control( + interface_guid: &GUID, + control_code: u32, + inbuf: &[u8], + outbuf_max_len: usize, +) -> Result, DeviceError> { + let h_device = open_device_handle(interface_guid)?; + let mut bytes_returned = 0; + let mut outbuf: Vec = vec![]; + let outbuf_ptr = if outbuf_max_len > 0 { + outbuf.reserve(outbuf_max_len); + outbuf.as_mut_ptr() + } else { + null_mut() + }; + let result = DeviceIoControl( + h_device, + control_code, + inbuf.as_ptr() as _, + inbuf.len() as _, + outbuf_ptr as _, + outbuf_max_len as _, + &mut bytes_returned, + null_mut(), + ); + CloseHandle(h_device); + if result == FALSE { + return Err(DeviceError::new_api_last_err("DeviceIoControl")); + } + if outbuf_max_len > 0 { + outbuf.set_len(bytes_returned as _); + Ok(outbuf) + } else { + Ok(Vec::new()) + } +} + +unsafe fn get_device_path(interface_guid: &GUID) -> Result, DeviceError> { + let dev_info = DeviceInfo::setup_di_get_class_devs_ex_w( + interface_guid, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE, + )?; + let mut device_interface_data = SP_DEVICE_INTERFACE_DATA { + cbSize: std::mem::size_of::() as _, + InterfaceClassGuid: *interface_guid, + Flags: 0, + Reserved: 0, + }; + if SetupDiEnumDeviceInterfaces( + *dev_info, + null_mut(), + interface_guid, + 0, + &mut device_interface_data, + ) == FALSE + { + return Err(DeviceError::new_api_last_err("SetupDiEnumDeviceInterfaces")); + } + + let mut required_length = 0; + if SetupDiGetDeviceInterfaceDetailW( + *dev_info, + &mut device_interface_data, + null_mut(), + 0, + &mut required_length, + null_mut(), + ) == FALSE + { + let err = io::Error::last_os_error(); + if err.raw_os_error() != Some(ERROR_INSUFFICIENT_BUFFER as _) { + return Err(DeviceError::WinApiLastErr( + "SetupDiGetDeviceInterfaceDetailW".to_string(), + err, + )); + } + } + + let predicted_length = required_length; + let mut vec_data: Vec = Vec::with_capacity(required_length as _); + let device_interface_detail_data = vec_data.as_mut_ptr(); + let device_interface_detail_data = + device_interface_detail_data as *mut SP_DEVICE_INTERFACE_DETAIL_DATA_W; + (*device_interface_detail_data).cbSize = + std::mem::size_of::() as _; + if SetupDiGetDeviceInterfaceDetailW( + *dev_info, + &mut device_interface_data, + device_interface_detail_data, + predicted_length, + &mut required_length, + null_mut(), + ) == FALSE + { + return Err(DeviceError::new_api_last_err( + "SetupDiGetDeviceInterfaceDetailW", + )); + } + + let mut path = Vec::new(); + let device_path_ptr = + std::ptr::addr_of!((*device_interface_detail_data).DevicePath) as *const u16; + let steps = device_path_ptr as usize - vec_data.as_ptr() as usize; + for i in 0..(predicted_length - steps as u32) / 2 { + if *device_path_ptr.offset(i as _) == 0 { + path.push(0); + break; + } + path.push(*device_path_ptr.offset(i as _)); + } + Ok(path) +} + +unsafe fn open_device_handle(interface_guid: &GUID) -> Result { + let device_path = get_device_path(interface_guid)?; + let h_device = CreateFileW( + device_path.as_ptr(), + GENERIC_READ | GENERIC_WRITE, + 0, + null_mut(), + OPEN_EXISTING, + 0, + null_mut(), + ); + if h_device == INVALID_HANDLE_VALUE || h_device == NULL { + return Err(DeviceError::new_api_last_err("CreateFileW")); + } + Ok(h_device) +} diff --git a/vendor/rustdesk/src/platform/windows.cc b/vendor/rustdesk/src/platform/windows.cc new file mode 100644 index 0000000..9027d9d --- /dev/null +++ b/vendor/rustdesk/src/platform/windows.cc @@ -0,0 +1,1058 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // NOLINT(build/include_order) +#include +#include +#include +#include +#include + +extern "C" uint32_t get_session_user_info(PWSTR bufin, uint32_t nin, uint32_t id); + +void flog(char const *fmt, ...) +{ + FILE *h = fopen("C:\\Windows\\temp\\test_rustdesk.log", "at"); + if (!h) + return; + va_list arg; + va_start(arg, fmt); + vfprintf(h, fmt, arg); + va_end(arg); + fclose(h); +} + +static BOOL GetProcessUserName(DWORD processID, LPWSTR outUserName, DWORD inUserNameSize) +{ + BOOL ret = FALSE; + HANDLE hProcess = NULL; + HANDLE hToken = NULL; + PTOKEN_USER tokenUser = NULL; + wchar_t *userName = NULL; + wchar_t *domainName = NULL; + + hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, processID); + if (hProcess == NULL) + { + goto cleanup; + } + if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) + { + goto cleanup; + } + DWORD tokenInfoLength = 0; + GetTokenInformation(hToken, TokenUser, NULL, 0, &tokenInfoLength); + if (tokenInfoLength == 0) + { + goto cleanup; + } + tokenUser = (PTOKEN_USER)malloc(tokenInfoLength); + if (tokenUser == NULL) + { + goto cleanup; + } + if (!GetTokenInformation(hToken, TokenUser, tokenUser, tokenInfoLength, &tokenInfoLength)) + { + goto cleanup; + } + DWORD userSize = 0; + DWORD domainSize = 0; + SID_NAME_USE snu; + LookupAccountSidW(NULL, tokenUser->User.Sid, NULL, &userSize, NULL, &domainSize, &snu); + if (userSize == 0 || domainSize == 0) + { + goto cleanup; + } + userName = (wchar_t *)malloc((userSize + 1) * sizeof(wchar_t)); + if (userName == NULL) + { + goto cleanup; + } + domainName = (wchar_t *)malloc((domainSize + 1) * sizeof(wchar_t)); + if (domainName == NULL) + { + goto cleanup; + } + if (!LookupAccountSidW(NULL, tokenUser->User.Sid, userName, &userSize, domainName, &domainSize, &snu)) + { + goto cleanup; + } + userName[userSize] = L'\0'; + domainName[domainSize] = L'\0'; + if (inUserNameSize <= userSize) + { + goto cleanup; + } + wcscpy(outUserName, userName); + + ret = TRUE; +cleanup: + if (userName) + { + free(userName); + } + if (domainName) + { + free(domainName); + } + if (tokenUser != NULL) + { + free(tokenUser); + } + if (hToken != NULL) + { + CloseHandle(hToken); + } + if (hProcess != NULL) + { + CloseHandle(hProcess); + } + + return ret; +} + +// ultravnc has rdp support +// https://github.com/veyon/ultravnc/blob/master/winvnc/winvnc/service.cpp +// https://github.com/TigerVNC/tigervnc/blob/master/win/winvnc/VNCServerService.cxx +// https://blog.csdn.net/MA540213/article/details/84638264 + +DWORD GetLogonPid(DWORD dwSessionId, BOOL as_user) +{ + DWORD dwLogonPid = 0; + HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnap != INVALID_HANDLE_VALUE) + { + PROCESSENTRY32W procEntry; + procEntry.dwSize = sizeof procEntry; + + if (Process32FirstW(hSnap, &procEntry)) + do + { + DWORD dwLogonSessionId = 0; + if (_wcsicmp(procEntry.szExeFile, as_user ? L"explorer.exe" : L"winlogon.exe") == 0 && + ProcessIdToSessionId(procEntry.th32ProcessID, &dwLogonSessionId) && + dwLogonSessionId == dwSessionId) + { + dwLogonPid = procEntry.th32ProcessID; + break; + } + } while (Process32NextW(hSnap, &procEntry)); + CloseHandle(hSnap); + } + return dwLogonPid; +} + +static DWORD GetFallbackUserPid(DWORD dwSessionId) +{ + DWORD dwFallbackPid = 0; + const wchar_t* fallbackUserProcs[] = {L"sihost.exe"}; + const int maxUsernameLen = 256; + wchar_t sessionUsername[maxUsernameLen + 1] = {0}; + wchar_t processUsername[maxUsernameLen + 1] = {0}; + + if (get_session_user_info(sessionUsername, maxUsernameLen, dwSessionId) == 0) + { + return 0; + } + HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnap != INVALID_HANDLE_VALUE) + { + PROCESSENTRY32W procEntry; + procEntry.dwSize = sizeof procEntry; + + if (Process32FirstW(hSnap, &procEntry)) + do + { + for (int i = 0; i < sizeof(fallbackUserProcs) / sizeof(fallbackUserProcs[0]); i++) + { + DWORD dwProcessSessionId = 0; + if (_wcsicmp(procEntry.szExeFile, fallbackUserProcs[i]) == 0 && + ProcessIdToSessionId(procEntry.th32ProcessID, &dwProcessSessionId) && + dwProcessSessionId == dwSessionId) + { + memset(processUsername, 0, sizeof(processUsername)); + if (GetProcessUserName(procEntry.th32ProcessID, processUsername, maxUsernameLen)) { + if (_wcsicmp(sessionUsername, processUsername) == 0) + { + dwFallbackPid = procEntry.th32ProcessID; + break; + } + } + } + } + if (dwFallbackPid != 0) + { + break; + } + } while (Process32NextW(hSnap, &procEntry)); + CloseHandle(hSnap); + } + return dwFallbackPid; +} + +// START the app as system +extern "C" +{ + // if should try WTSQueryUserToken? + // https://stackoverflow.com/questions/7285666/example-code-a-service-calls-createprocessasuser-i-want-the-process-to-run-in + BOOL GetSessionUserTokenWin(OUT LPHANDLE lphUserToken, DWORD dwSessionId, BOOL as_user, DWORD *pDwTokenPid) + { + BOOL bResult = FALSE; + DWORD Id = GetLogonPid(dwSessionId, as_user); + if (Id == 0) + { + Id = GetFallbackUserPid(dwSessionId); + } + if (pDwTokenPid) + *pDwTokenPid = Id; + if (HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Id)) + { + bResult = OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, lphUserToken); + CloseHandle(hProcess); + } + return bResult; + } + + bool is_windows_server() + { + return IsWindowsServer(); + } + + bool is_windows_10_or_greater() + { + return IsWindows10OrGreater(); + } + + HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user, BOOL show, DWORD *pDwTokenPid) + { + HANDLE hProcess = NULL; + HANDLE hToken = NULL; + if (GetSessionUserTokenWin(&hToken, dwSessionId, as_user, pDwTokenPid)) + { + STARTUPINFOW si; + ZeroMemory(&si, sizeof si); + si.cb = sizeof si; + si.dwFlags = STARTF_USESHOWWINDOW; + if (show) + { + si.lpDesktop = (LPWSTR)L"winsta0\\default"; + si.wShowWindow = SW_SHOW; + } + wchar_t buf[MAX_PATH]; + wcscpy_s(buf, MAX_PATH, cmd); + PROCESS_INFORMATION pi; + LPVOID lpEnvironment = NULL; + DWORD dwCreationFlags = DETACHED_PROCESS; + if (as_user) + { + + CreateEnvironmentBlock(&lpEnvironment, // Environment block + hToken, // New token + TRUE); // Inheritance + } + if (lpEnvironment) + { + dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT; + } + if (CreateProcessAsUserW(hToken, NULL, buf, NULL, NULL, FALSE, dwCreationFlags, lpEnvironment, NULL, &si, &pi)) + { + CloseHandle(pi.hThread); + hProcess = pi.hProcess; + } + CloseHandle(hToken); + if (lpEnvironment) + DestroyEnvironmentBlock(lpEnvironment); + } + return hProcess; + } + + // Switch the current thread to the specified desktop + static bool + switchToDesktop(HDESK desktop) + { + HDESK old_desktop = GetThreadDesktop(GetCurrentThreadId()); + if (!SetThreadDesktop(desktop)) + { + return false; + } + if (!CloseDesktop(old_desktop)) + { + // + } + return true; + } + + // https://github.com/TigerVNC/tigervnc/blob/8c6c584377feba0e3b99eecb3ef33b28cee318cb/win/rfb_win32/Service.cxx + + // Determine whether the thread's current desktop is the input one + BOOL + inputDesktopSelected() + { + HDESK current = GetThreadDesktop(GetCurrentThreadId()); + HDESK input = OpenInputDesktop(0, FALSE, + DESKTOP_CREATEMENU | DESKTOP_CREATEWINDOW | + DESKTOP_ENUMERATE | DESKTOP_HOOKCONTROL | + DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS | + DESKTOP_SWITCHDESKTOP | GENERIC_WRITE); + if (!input) + { + return FALSE; + } + + DWORD size; + char currentname[256]; + char inputname[256]; + + if (!GetUserObjectInformation(current, UOI_NAME, currentname, sizeof(currentname), &size)) + { + CloseDesktop(input); + return FALSE; + } + if (!GetUserObjectInformation(input, UOI_NAME, inputname, sizeof(inputname), &size)) + { + CloseDesktop(input); + return FALSE; + } + CloseDesktop(input); + // flog("%s %s\n", currentname, inputname); + return strcmp(currentname, inputname) == 0 ? TRUE : FALSE; + } + + // Switch the current thread into the input desktop + bool + selectInputDesktop() + { + // - Open the input desktop + HDESK desktop = OpenInputDesktop(0, FALSE, + DESKTOP_CREATEMENU | DESKTOP_CREATEWINDOW | + DESKTOP_ENUMERATE | DESKTOP_HOOKCONTROL | + DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS | + DESKTOP_SWITCHDESKTOP | GENERIC_WRITE); + if (!desktop) + { + return false; + } + + // - Switch into it + if (!switchToDesktop(desktop)) + { + CloseDesktop(desktop); + return false; + } + + // *** + DWORD size = 256; + char currentname[256]; + if (GetUserObjectInformation(desktop, UOI_NAME, currentname, 256, &size)) + { + // + } + + return true; + } + + int handleMask(uint8_t *rwbuffer, const uint8_t *mask, int width, int height, int bmWidthBytes, int bmHeight) + { + auto andMask = mask; + auto andMaskSize = bmWidthBytes * bmHeight; + auto offset = height * bmWidthBytes; + auto xorMask = mask + offset; + auto xorMaskSize = andMaskSize - offset; + int doOutline = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + int byte = y * bmWidthBytes + x / 8; + int bit = 7 - x % 8; + + if (byte < andMaskSize && !(andMask[byte] & (1 << bit))) + { + // Valid pixel, so make it opaque + rwbuffer[3] = 0xff; + + // Black or white? + if (xorMask[byte] & (1 << bit)) + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0xff; + else + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0; + } + else if (byte < xorMaskSize && xorMask[byte] & (1 << bit)) + { + // Replace any XORed pixels with black, because RFB doesn't support + // XORing of cursors. XORing is used for the I-beam cursor, which is most + // often used over a white background, but also sometimes over a black + // background. We set the XOR'd pixels to black, then draw a white outline + // around the whole cursor. + + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = 0; + rwbuffer[3] = 0xff; + + doOutline = 1; + } + else + { + // Transparent pixel + rwbuffer[0] = rwbuffer[1] = rwbuffer[2] = rwbuffer[3] = 0; + } + + rwbuffer += 4; + } + } + return doOutline; + } + + void drawOutline(uint8_t *out0, const uint8_t *in0, int width, int height, int out0_size) + { + auto in = in0; + auto out0_end = out0 + out0_size; + auto offset = width * 4 + 4; + auto out = out0 + offset; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // Visible pixel? + if (in[3] > 0) + { + auto n = 4 * 3; + auto p = out - (width + 2) * 4 - 4; + // Outline above... + if (p >= out0 && p + n <= out0_end) + memset(p, 0xff, n); + // ...besides... + p = out - 4; + if (p + n <= out0_end) + memset(p, 0xff, n); + // ...and above + p = out + (width + 2) * 4 - 4; + if (p + n <= out0_end) + memset(p, 0xff, n); + } + in += 4; + out += 4; + } + // outline is slightly larger + out += 2 * 4; + } + + // Pass 2, overwrite with actual cursor + in = in0; + out = out0 + offset; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (in[3] > 0 && out + 4 <= out0_end) + memcpy(out, in, 4); + in += 4; + out += 4; + } + out += 2 * 4; + } + } + + int ffi(unsigned v) + { + static const int MultiplyDeBruijnBitPosition[32] = + { + 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, + 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; + return MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27]; + } + + int get_di_bits(uint8_t *out, HDC dc, HBITMAP hbmColor, int width, int height) + { + BITMAPV5HEADER bi; + memset(&bi, 0, sizeof(BITMAPV5HEADER)); + + bi.bV5Size = sizeof(BITMAPV5HEADER); + bi.bV5Width = width; + bi.bV5Height = -height; // Negative for top-down + bi.bV5Planes = 1; + bi.bV5BitCount = 32; + bi.bV5Compression = BI_BITFIELDS; + bi.bV5RedMask = 0x000000FF; + bi.bV5GreenMask = 0x0000FF00; + bi.bV5BlueMask = 0x00FF0000; + bi.bV5AlphaMask = 0xFF000000; + + if (!GetDIBits(dc, hbmColor, 0, height, + out, (LPBITMAPINFO)&bi, DIB_RGB_COLORS)) + return 1; + + // We may not get the RGBA order we want, so shuffle things around + int ridx, gidx, bidx, aidx; + + ridx = ffi(bi.bV5RedMask) / 8; + gidx = ffi(bi.bV5GreenMask) / 8; + bidx = ffi(bi.bV5BlueMask) / 8; + // Usually not set properly + aidx = 6 - ridx - gidx - bidx; + + if ((bi.bV5RedMask != ((unsigned)0xff << ridx * 8)) || + (bi.bV5GreenMask != ((unsigned)0xff << gidx * 8)) || + (bi.bV5BlueMask != ((unsigned)0xff << bidx * 8))) + return 1; + + auto rwbuffer = out; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + uint8_t r, g, b, a; + + r = rwbuffer[ridx]; + g = rwbuffer[gidx]; + b = rwbuffer[bidx]; + a = rwbuffer[aidx]; + + rwbuffer[0] = r; + rwbuffer[1] = g; + rwbuffer[2] = b; + rwbuffer[3] = a; + + rwbuffer += 4; + } + } + return 0; + } + + void blank_screen(BOOL set) + { + if (set) + { + SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)2); + } + else + { + SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, (LPARAM)-1); + } + } + + void AddRecentDocument(PCWSTR path) + { + SHAddToRecentDocs(SHARD_PATHW, path); + } + + DWORD get_current_session(BOOL include_rdp) + { + auto rdp_or_console = WTSGetActiveConsoleSessionId(); + if (!include_rdp) + return rdp_or_console; + PWTS_SESSION_INFOA pInfos; + DWORD count; + auto rdp = "rdp"; + auto nrdp = strlen(rdp); + // https://github.com/rustdesk/rustdesk/discussions/937#discussioncomment-12373814 citrix session + auto ica = "ica"; + auto nica = strlen(ica); + if (WTSEnumerateSessionsA(WTS_CURRENT_SERVER_HANDLE, NULL, 1, &pInfos, &count)) + { + for (DWORD i = 0; i < count; i++) + { + auto info = pInfos[i]; + if (info.State == WTSActive) + { + if (info.pWinStationName == NULL) + continue; + if (!stricmp(info.pWinStationName, "console")) + { + auto id = info.SessionId; + WTSFreeMemory(pInfos); + return id; + } + if (!strnicmp(info.pWinStationName, rdp, nrdp) || !strnicmp(info.pWinStationName, ica, nica)) + { + rdp_or_console = info.SessionId; + } + } + } + WTSFreeMemory(pInfos); + } + return rdp_or_console; + } + + BOOL is_session_locked(DWORD session_id) + { + if (session_id == 0xFFFFFFFF) { + return FALSE; + } + PWTSINFOEXW pInfo = NULL; + DWORD bytes = 0; + BOOL locked = FALSE; + if (WTSQuerySessionInformationW( + WTS_CURRENT_SERVER_HANDLE, + session_id, + WTSSessionInfoEx, + (LPWSTR *)&pInfo, + &bytes)) { + if (pInfo && pInfo->Level == 1) { + locked = (pInfo->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_LOCK); + } + if (pInfo) { + WTSFreeMemory(pInfo); + } + } + return locked; + } + + uint32_t get_active_user(PWSTR bufin, uint32_t nin, BOOL rdp) + { + uint32_t nout = 0; + auto id = get_current_session(rdp); + PWSTR buf = NULL; + DWORD n = 0; + if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, id, WTSUserName, &buf, &n)) + { + if (buf) + { + nout = min(nin, n); + memcpy(bufin, buf, nout); + WTSFreeMemory(buf); + } + } + return nout; + } + + uint32_t get_session_user_info(PWSTR bufin, uint32_t nin, uint32_t id) + { + uint32_t nout = 0; + PWSTR buf = NULL; + DWORD n = 0; + if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, id, WTSUserName, &buf, &n)) + { + if (buf) + { + nout = min(nin, n); + memcpy(bufin, buf, nout); + WTSFreeMemory(buf); + } + } + return nout; + } + + void get_available_session_ids(PWSTR buf, uint32_t bufSize, BOOL include_rdp) { + std::vector sessionIds; + PWTS_SESSION_INFOA pInfos = NULL; + DWORD count; + + if (WTSEnumerateSessionsA(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pInfos, &count)) { + for (DWORD i = 0; i < count; i++) { + auto info = pInfos[i]; + auto rdp = "rdp"; + auto nrdp = strlen(rdp); + auto ica = "ica"; + auto nica = strlen(ica); + if (info.State == WTSActive) { + if (info.pWinStationName == NULL) + continue; + if (info.SessionId == 65536 || info.SessionId == 655) + continue; + + if (!stricmp(info.pWinStationName, "console")){ + sessionIds.push_back(std::wstring(L"Console:") + std::to_wstring(info.SessionId)); + } + else if (include_rdp && !strnicmp(info.pWinStationName, rdp, nrdp)) { + sessionIds.push_back(std::wstring(L"RDP:") + std::to_wstring(info.SessionId)); + } + else if (include_rdp && !strnicmp(info.pWinStationName, ica, nica)) { + sessionIds.push_back(std::wstring(L"ICA:") + std::to_wstring(info.SessionId)); + } + } + } + WTSFreeMemory(pInfos); + } + + std::wstring tmpStr; + for (size_t i = 0; i < sessionIds.size(); i++) { + if (i > 0) { + tmpStr += L","; + } + tmpStr += sessionIds[i]; + } + + if (buf && !tmpStr.empty() && tmpStr.size() < bufSize) { + wcsncpy_s(buf, bufSize, tmpStr.c_str(), tmpStr.size()); + } + } +} // end of extern "C" + +// below copied from https://github.com/TigerVNC/tigervnc/blob/master/vncviewer/win32.c +extern "C" +{ + static HANDLE thread; + static DWORD thread_id; + + static HHOOK hook = 0; + static HWND target_wnd = 0; + static HWND default_hook_wnd = 0; + static bool win_down = false; + static bool stop_system_key_propagate = false; + + bool is_win_down() + { + return win_down; + } + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof(*a)) + + static int is_system_hotkey(int vkCode, WPARAM wParam) + { + switch (vkCode) + { + case VK_LWIN: + case VK_RWIN: + win_down = wParam == WM_KEYDOWN; + case VK_SNAPSHOT: + return 1; + case VK_TAB: + if (GetAsyncKeyState(VK_MENU) & 0x8000) + return 1; + case VK_ESCAPE: + if (GetAsyncKeyState(VK_MENU) & 0x8000) + return 1; + if (GetAsyncKeyState(VK_CONTROL) & 0x8000) + return 1; + } + return 0; + } + + static LRESULT CALLBACK keyboard_hook(int nCode, WPARAM wParam, LPARAM lParam) + { + if (nCode >= 0) + { + KBDLLHOOKSTRUCT *msgInfo = (KBDLLHOOKSTRUCT *)lParam; + + // Grabbing everything seems to mess up some keyboard state that + // FLTK relies on, so just grab the keys that we normally cannot. + if (stop_system_key_propagate && is_system_hotkey(msgInfo->vkCode, wParam)) + { + PostMessage(target_wnd, wParam, msgInfo->vkCode, + (msgInfo->scanCode & 0xff) << 16 | + (msgInfo->flags & 0xff) << 24); + return 1; + } + } + + return CallNextHookEx(hook, nCode, wParam, lParam); + } + + static DWORD WINAPI keyboard_thread(LPVOID data) + { + MSG msg; + + target_wnd = (HWND)data; + + // Make sure a message queue is created + PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE | PM_NOYIELD); + + hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_hook, GetModuleHandle(0), 0); + // If something goes wrong then there is not much we can do. + // Just sit around and wait for WM_QUIT... + + while (GetMessage(&msg, NULL, 0, 0)) + ; + + if (hook) + UnhookWindowsHookEx(hook); + + target_wnd = 0; + + return 0; + } + + int win32_enable_lowlevel_keyboard(HWND hwnd) + { + if (!default_hook_wnd) + { + default_hook_wnd = hwnd; + } + if (!hwnd) + { + hwnd = default_hook_wnd; + } + // Only one target at a time for now + if (thread != NULL) + { + if (hwnd == target_wnd) + return 0; + + return 1; + } + + // We create a separate thread as it is crucial that hooks are processed + // in a timely manner. + thread = CreateThread(NULL, 0, keyboard_thread, hwnd, 0, &thread_id); + if (thread == NULL) + return 1; + + return 0; + } + + void win32_disable_lowlevel_keyboard(HWND hwnd) + { + if (!hwnd) + { + hwnd = default_hook_wnd; + } + if (hwnd != target_wnd) + return; + + PostThreadMessage(thread_id, WM_QUIT, 0, 0); + + CloseHandle(thread); + thread = NULL; + } + + void win_stop_system_key_propagate(bool v) + { + stop_system_key_propagate = v; + } + + // https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user + BOOL is_local_system() + { + HANDLE hToken; + UCHAR bTokenUser[sizeof(TOKEN_USER) + 8 + 4 * SID_MAX_SUB_AUTHORITIES]; + PTOKEN_USER pTokenUser = (PTOKEN_USER)bTokenUser; + ULONG cbTokenUser; + SID_IDENTIFIER_AUTHORITY siaNT = SECURITY_NT_AUTHORITY; + PSID pSystemSid; + BOOL bSystem; + + // open process token + if (!OpenProcessToken(GetCurrentProcess(), + TOKEN_QUERY, + &hToken)) + return FALSE; + + // retrieve user SID + if (!GetTokenInformation(hToken, TokenUser, pTokenUser, + sizeof(bTokenUser), &cbTokenUser)) + { + CloseHandle(hToken); + return FALSE; + } + + CloseHandle(hToken); + + // allocate LocalSystem well-known SID + if (!AllocateAndInitializeSid(&siaNT, 1, SECURITY_LOCAL_SYSTEM_RID, + 0, 0, 0, 0, 0, 0, 0, &pSystemSid)) + return FALSE; + + // compare the user SID from the token with the LocalSystem SID + bSystem = EqualSid(pTokenUser->User.Sid, pSystemSid); + + FreeSid(pSystemSid); + + return bSystem; + } + + void alloc_console_and_redirect() + { + AllocConsole(); + freopen("CONOUT$", "w", stdout); + } + + bool is_service_running_w(LPCWSTR serviceName) + { + SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT); + if (hSCManager == NULL) { + return false; + } + + SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_QUERY_STATUS); + if (hService == NULL) { + CloseServiceHandle(hSCManager); + return false; + } + + SERVICE_STATUS_PROCESS serviceStatus; + DWORD bytesNeeded; + if (!QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, reinterpret_cast(&serviceStatus), sizeof(serviceStatus), &bytesNeeded)) { + CloseServiceHandle(hService); + CloseServiceHandle(hSCManager); + return false; + } + + bool isRunning = (serviceStatus.dwCurrentState == SERVICE_RUNNING); + + CloseServiceHandle(hService); + CloseServiceHandle(hSCManager); + + return isRunning; + } +} // end of extern "C" + +// Remote printing +extern "C" +{ +// Dynamic loading of XPS Print functions +typedef HRESULT(WINAPI *StartXpsPrintJobFunc)( + LPCWSTR printerName, + LPCWSTR jobName, + LPCWSTR outputFileName, + HANDLE progressEvent, + HANDLE completionEvent, + UINT8* printablePagesOn, + UINT32 printablePagesOnCount, + IXpsPrintJob** xpsPrintJob, + IXpsPrintJobStream** documentStream, + IXpsPrintJobStream** printTicketStream); + +static HMODULE xpsPrintModule = nullptr; +static StartXpsPrintJobFunc StartXpsPrintJobPtr = nullptr; + +static bool InitXpsPrint() +{ + if (xpsPrintModule == nullptr) + { + xpsPrintModule = LoadLibraryA("XpsPrint.dll"); + if (xpsPrintModule == nullptr) + { + flog("Failed to load XpsPrint.dll. Error: %d\n", GetLastError()); + return false; + } + + StartXpsPrintJobPtr = (StartXpsPrintJobFunc)GetProcAddress(xpsPrintModule, "StartXpsPrintJob"); + if (StartXpsPrintJobPtr == nullptr) + { + flog("Failed to get StartXpsPrintJob function. Error: %d\n", GetLastError()); + FreeLibrary(xpsPrintModule); + xpsPrintModule = nullptr; + return false; + } + } + return true; +} +#pragma warning(push) +#pragma warning(disable : 4995) + +#define PRINT_XPS_CHECK_HR(hr, msg) \ + if (FAILED(hr)) \ + { \ + _com_error err(hr); \ + flog("%s Error: %s\n", msg, err.ErrorMessage()); \ + return -1; \ + } + + int PrintXPSRawData(LPWSTR printerName, BYTE *rawData, ULONG dataSize) + { + // Check if XPS Print DLL is available + if (!InitXpsPrint()) + { + flog("XPS Print functionality not available on this system\n"); + return -1; + } + + BOOL isCoInitializeOk = FALSE; + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (hr == RPC_E_CHANGED_MODE) + { + hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + } + if (hr == S_OK) + { + isCoInitializeOk = TRUE; + } + std::shared_ptr coInitGuard(nullptr, [isCoInitializeOk](int *) { + if (isCoInitializeOk) CoUninitialize(); + }); + + IXpsOMObjectFactory *xpsFactory = nullptr; + hr = CoCreateInstance( + __uuidof(XpsOMObjectFactory), + nullptr, + CLSCTX_INPROC_SERVER, + __uuidof(IXpsOMObjectFactory), + reinterpret_cast(&xpsFactory)); + PRINT_XPS_CHECK_HR(hr, "Failed to create XPS object factory."); + std::shared_ptr xpsFactoryGuard( + xpsFactory, + [](IXpsOMObjectFactory *xpsFactory) { + xpsFactory->Release(); + }); + + HANDLE completionEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + if (completionEvent == nullptr) + { + flog("Failed to create completion event. Last error: %d\n", GetLastError()); + return -1; + } + std::shared_ptr completionEventGuard( + &completionEvent, + [](HANDLE *completionEvent) { + CloseHandle(*completionEvent); + }); + + IXpsPrintJob *job = nullptr; + IXpsPrintJobStream *jobStream = nullptr; + // `StartXpsPrintJob()` is deprecated, but we still use it for compatibility. + // We may change to use the `Print Document Package API` in the future. + // https://learn.microsoft.com/en-us/windows/win32/printdocs/xpsprint-functions + hr = StartXpsPrintJobPtr( + printerName, + L"Print Job 1", + nullptr, + nullptr, + completionEvent, + nullptr, + 0, + &job, + &jobStream, + nullptr); + PRINT_XPS_CHECK_HR(hr, "Failed to start XPS print job."); + + std::shared_ptr jobStreamGuard(jobStream, [](IXpsPrintJobStream *jobStream) { + jobStream->Release(); + }); + BOOL jobOk = FALSE; + std::shared_ptr jobGuard(job, [&jobOk](IXpsPrintJob* job) { + if (jobOk == FALSE) + { + job->Cancel(); + } + job->Release(); + }); + + DWORD bytesWritten = 0; + hr = jobStream->Write(rawData, dataSize, &bytesWritten); + PRINT_XPS_CHECK_HR(hr, "Failed to write data to print job stream."); + + hr = jobStream->Close(); + PRINT_XPS_CHECK_HR(hr, "Failed to close print job stream."); + + // Wait about 5 minutes for the print job to complete. + DWORD waitMillis = 300 * 1000; + DWORD waitResult = WaitForSingleObject(completionEvent, waitMillis); + if (waitResult != WAIT_OBJECT_0) + { + flog("Wait for print job completion failed. Last error: %d\n", GetLastError()); + return -1; + } + jobOk = TRUE; + + return 0; + } + + void CleanupXpsPrint() + { + if (xpsPrintModule != nullptr) + { + FreeLibrary(xpsPrintModule); + xpsPrintModule = nullptr; + StartXpsPrintJobPtr = nullptr; + } + } + +#pragma warning(pop) +} diff --git a/vendor/rustdesk/src/platform/windows.rs b/vendor/rustdesk/src/platform/windows.rs new file mode 100644 index 0000000..4c09bbe --- /dev/null +++ b/vendor/rustdesk/src/platform/windows.rs @@ -0,0 +1,4384 @@ +use super::{CursorData, ResultType}; +use crate::{ + common::PORTABLE_APPNAME_RUNTIME_ENV_KEY, + custom_server::*, + ipc, + privacy_mode::win_topmost_window::{self, WIN_TOPMOST_INJECTED_PROCESS_EXE}, +}; +use hbb_common::{ + allow_err, + anyhow::anyhow, + bail, + config::{self, Config}, + libc::{c_int, wchar_t}, + log, + message_proto::{DisplayInfo, Resolution, WindowsSession}, + sleep, + sysinfo::{Pid, System}, + timeout, tokio, +}; +use std::{ + collections::HashMap, + ffi::{CString, OsString}, + fs, + io::{self, prelude::*}, + mem, + os::{ + raw::c_ulong, + windows::{ffi::OsStringExt, process::CommandExt}, + }, + path::*, + ptr::null_mut, + sync::{atomic::Ordering, Arc, Mutex}, + time::{Duration, Instant}, +}; +use wallpaper; +#[cfg(not(debug_assertions))] +use winapi::um::libloaderapi::{LoadLibraryExW, LOAD_LIBRARY_SEARCH_USER_DIRS}; +use winapi::{ + ctypes::c_void, + shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*}, + um::{ + errhandlingapi::GetLastError, + handleapi::{CloseHandle, INVALID_HANDLE_VALUE}, + libloaderapi::{ + GetProcAddress, LoadLibraryA, LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32, + }, + minwinbase::STILL_ACTIVE, + processthreadsapi::{ + GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, + OpenProcessToken, ProcessIdToSessionId, PROCESS_INFORMATION, STARTUPINFOW, + }, + securitybaseapi::{ + AllocateAndInitializeSid, DuplicateToken, EqualSid, FreeSid, GetTokenInformation, + }, + shellapi::ShellExecuteW, + sysinfoapi::{GetNativeSystemInfo, SYSTEM_INFO}, + winbase::*, + wingdi::*, + winnt::{ + SecurityImpersonation, TokenElevation, TokenGroups, TokenImpersonation, TokenType, + DOMAIN_ALIAS_RID_ADMINS, ES_AWAYMODE_REQUIRED, ES_CONTINUOUS, ES_DISPLAY_REQUIRED, + ES_SYSTEM_REQUIRED, HANDLE, PROCESS_ALL_ACCESS, PROCESS_QUERY_LIMITED_INFORMATION, + PSID, SECURITY_BUILTIN_DOMAIN_RID, SECURITY_NT_AUTHORITY, SID_IDENTIFIER_AUTHORITY, + TOKEN_ELEVATION, TOKEN_GROUPS, TOKEN_QUERY, TOKEN_TYPE, + }, + winreg::HKEY_CURRENT_USER, + winspool::{ + EnumPrintersW, GetDefaultPrinterW, PRINTER_ENUM_CONNECTIONS, PRINTER_ENUM_LOCAL, + PRINTER_INFO_1W, + }, + winuser::*, + }, +}; +use windows::Win32::{ + Foundation::{CloseHandle as WinCloseHandle, HANDLE as WinHANDLE}, + System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, + TH32CS_SNAPPROCESS, + }, +}; +use windows_service::{ + define_windows_service, + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, +}; +use winreg::{enums::*, RegKey}; + +pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window +pub const EXPLORER_EXE: &'static str = "explorer.exe"; +pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW"; + +const REG_NAME_INSTALL_DESKTOPSHORTCUTS: &str = "DESKTOPSHORTCUTS"; +const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS"; +pub const REG_NAME_INSTALL_PRINTER: &str = "PRINTER"; + +pub fn get_focused_display(displays: Vec) -> Option { + unsafe { + let hwnd = GetForegroundWindow(); + let mut rect: RECT = mem::zeroed(); + if GetWindowRect(hwnd, &mut rect as *mut RECT) == 0 { + return None; + } + displays.iter().position(|display| { + let center_x = rect.left + (rect.right - rect.left) / 2; + let center_y = rect.top + (rect.bottom - rect.top) / 2; + center_x >= display.x + && center_x < display.x + display.width + && center_y >= display.y + && center_y < display.y + display.height + }) + } +} + +pub fn get_cursor_pos() -> Option<(i32, i32)> { + unsafe { + let mut out = mem::MaybeUninit::::uninit(); + if GetCursorPos(out.as_mut_ptr()) == FALSE { + return None; + } + let out = out.assume_init(); + Some((out.x, out.y)) + } +} + +pub fn set_cursor_pos(x: i32, y: i32) -> bool { + unsafe { + if SetCursorPos(x, y) == FALSE { + let err = GetLastError(); + log::warn!("SetCursorPos failed: x={}, y={}, error_code={}", x, y, err); + return false; + } + true + } +} + +/// Clip cursor to a rectangle. Pass None to unclip. +pub fn clip_cursor(rect: Option<(i32, i32, i32, i32)>) -> bool { + unsafe { + let result = match rect { + Some((left, top, right, bottom)) => { + let r = RECT { + left, + top, + right, + bottom, + }; + ClipCursor(&r) + } + None => ClipCursor(std::ptr::null()), + }; + if result == FALSE { + let err = GetLastError(); + log::warn!("ClipCursor failed: rect={:?}, error_code={}", rect, err); + return false; + } + true + } +} + +pub fn reset_input_cache() {} + +pub fn get_cursor() -> ResultType> { + unsafe { + #[allow(invalid_value)] + let mut ci: CURSORINFO = mem::MaybeUninit::uninit().assume_init(); + ci.cbSize = std::mem::size_of::() as _; + if crate::portable_service::client::get_cursor_info(&mut ci) == FALSE { + return Err(io::Error::last_os_error().into()); + } + if ci.flags & CURSOR_SHOWING == 0 { + Ok(None) + } else { + Ok(Some(ci.hCursor as _)) + } + } +} + +struct IconInfo(ICONINFO); + +impl IconInfo { + fn new(icon: HICON) -> ResultType { + unsafe { + #[allow(invalid_value)] + let mut ii = mem::MaybeUninit::uninit().assume_init(); + if GetIconInfo(icon, &mut ii) == FALSE { + Err(io::Error::last_os_error().into()) + } else { + let ii = Self(ii); + if ii.0.hbmMask.is_null() { + bail!("Cursor bitmap handle is NULL"); + } + return Ok(ii); + } + } + } + + fn is_color(&self) -> bool { + !self.0.hbmColor.is_null() + } +} + +impl Drop for IconInfo { + fn drop(&mut self) { + unsafe { + if !self.0.hbmColor.is_null() { + DeleteObject(self.0.hbmColor as _); + } + if !self.0.hbmMask.is_null() { + DeleteObject(self.0.hbmMask as _); + } + } + } +} + +// https://github.com/TurboVNC/tightvnc/blob/a235bae328c12fd1c3aed6f3f034a37a6ffbbd22/vnc_winsrc/winvnc/vncEncoder.cpp +// https://github.com/TigerVNC/tigervnc/blob/master/win/rfb_win32/DeviceFrameBuffer.cxx +pub fn get_cursor_data(hcursor: u64) -> ResultType { + unsafe { + let mut ii = IconInfo::new(hcursor as _)?; + let bm_mask = get_bitmap(ii.0.hbmMask)?; + let mut width = bm_mask.bmWidth; + let mut height = if ii.is_color() { + bm_mask.bmHeight + } else { + bm_mask.bmHeight / 2 + }; + let cbits_size = width * height * 4; + if cbits_size < 16 { + bail!("Invalid icon: too small"); // solve some crash + } + let mut cbits: Vec = Vec::new(); + cbits.resize(cbits_size as _, 0); + let mut mbits: Vec = Vec::new(); + mbits.resize((bm_mask.bmWidthBytes * bm_mask.bmHeight) as _, 0); + let r = GetBitmapBits(ii.0.hbmMask, mbits.len() as _, mbits.as_mut_ptr() as _); + if r == 0 { + bail!("Failed to copy bitmap data"); + } + if r != (mbits.len() as i32) { + bail!( + "Invalid mask cursor buffer size, got {} bytes, expected {}", + r, + mbits.len() + ); + } + let do_outline; + if ii.is_color() { + get_rich_cursor_data(ii.0.hbmColor, width, height, &mut cbits)?; + do_outline = fix_cursor_mask( + &mut mbits, + &mut cbits, + width as _, + height as _, + bm_mask.bmWidthBytes as _, + ); + } else { + do_outline = handleMask( + cbits.as_mut_ptr(), + mbits.as_ptr(), + width, + height, + bm_mask.bmWidthBytes, + bm_mask.bmHeight, + ) > 0; + } + if do_outline { + let mut outline = Vec::new(); + outline.resize(((width + 2) * (height + 2) * 4) as _, 0); + drawOutline( + outline.as_mut_ptr(), + cbits.as_ptr(), + width, + height, + outline.len() as _, + ); + cbits = outline; + width += 2; + height += 2; + ii.0.xHotspot += 1; + ii.0.yHotspot += 1; + } + + Ok(CursorData { + id: hcursor, + colors: cbits.into(), + hotx: ii.0.xHotspot as _, + hoty: ii.0.yHotspot as _, + width: width as _, + height: height as _, + ..Default::default() + }) + } +} + +#[inline] +fn get_bitmap(handle: HBITMAP) -> ResultType { + unsafe { + let mut bm: BITMAP = mem::zeroed(); + if GetObjectA( + handle as _, + std::mem::size_of::() as _, + &mut bm as *mut BITMAP as *mut _, + ) == FALSE + { + return Err(io::Error::last_os_error().into()); + } + if bm.bmPlanes != 1 { + bail!("Unsupported multi-plane cursor"); + } + if bm.bmBitsPixel != 1 { + bail!("Unsupported cursor mask format"); + } + Ok(bm) + } +} + +struct DC(HDC); + +impl DC { + fn new() -> ResultType { + unsafe { + let dc = GetDC(0 as _); + if dc.is_null() { + bail!("Failed to get a drawing context"); + } + Ok(Self(dc)) + } + } +} + +impl Drop for DC { + fn drop(&mut self) { + unsafe { + if !self.0.is_null() { + ReleaseDC(0 as _, self.0); + } + } + } +} + +struct CompatibleDC(HDC); + +impl CompatibleDC { + fn new(existing: HDC) -> ResultType { + unsafe { + let dc = CreateCompatibleDC(existing); + if dc.is_null() { + bail!("Failed to get a compatible drawing context"); + } + Ok(Self(dc)) + } + } +} + +impl Drop for CompatibleDC { + fn drop(&mut self) { + unsafe { + if !self.0.is_null() { + DeleteDC(self.0); + } + } + } +} + +struct BitmapDC(CompatibleDC, HBITMAP); + +impl BitmapDC { + fn new(hdc: HDC, hbitmap: HBITMAP) -> ResultType { + unsafe { + let dc = CompatibleDC::new(hdc)?; + let oldbitmap = SelectObject(dc.0, hbitmap as _) as HBITMAP; + if oldbitmap.is_null() { + bail!("Failed to select CompatibleDC"); + } + Ok(Self(dc, oldbitmap)) + } + } + + fn dc(&self) -> HDC { + (self.0).0 + } +} + +impl Drop for BitmapDC { + fn drop(&mut self) { + unsafe { + if !self.1.is_null() { + SelectObject((self.0).0, self.1 as _); + } + } + } +} + +#[inline] +fn get_rich_cursor_data( + hbm_color: HBITMAP, + width: i32, + height: i32, + out: &mut Vec, +) -> ResultType<()> { + unsafe { + let dc = DC::new()?; + let bitmap_dc = BitmapDC::new(dc.0, hbm_color)?; + if get_di_bits(out.as_mut_ptr(), bitmap_dc.dc(), hbm_color, width, height) > 0 { + bail!("Failed to get di bits: {}", io::Error::last_os_error()); + } + } + Ok(()) +} + +fn fix_cursor_mask( + mbits: &mut Vec, + cbits: &mut Vec, + width: usize, + height: usize, + bm_width_bytes: usize, +) -> bool { + let mut pix_idx = 0; + for _ in 0..height { + for _ in 0..width { + if cbits[pix_idx + 3] != 0 { + return false; + } + pix_idx += 4; + } + } + + let packed_width_bytes = (width + 7) >> 3; + let bm_size = mbits.len(); + let c_size = cbits.len(); + + // Pack and invert bitmap data (mbits) + // borrow from tigervnc + for y in 0..height { + for x in 0..packed_width_bytes { + let a = y * packed_width_bytes + x; + let b = y * bm_width_bytes + x; + if a < bm_size && b < bm_size { + mbits[a] = !mbits[b]; + } + } + } + + // Replace "inverted background" bits with black color to ensure + // cross-platform interoperability. Not beautiful but necessary code. + // borrow from tigervnc + let bytes_row = width << 2; + for y in 0..height { + let mut bitmask: u8 = 0x80; + for x in 0..width { + let mask_idx = y * packed_width_bytes + (x >> 3); + if mask_idx < bm_size { + let pix_idx = y * bytes_row + (x << 2); + if (mbits[mask_idx] & bitmask) == 0 { + for b1 in 0..4 { + let a = pix_idx + b1; + if a < c_size { + if cbits[a] != 0 { + mbits[mask_idx] ^= bitmask; + for b2 in b1..4 { + let b = pix_idx + b2; + if b < c_size { + cbits[b] = 0x00; + } + } + break; + } + } + } + } + } + bitmask >>= 1; + if bitmask == 0 { + bitmask = 0x80; + } + } + } + + // borrow from noVNC + let mut pix_idx = 0; + for y in 0..height { + for x in 0..width { + let mask_idx = y * packed_width_bytes + (x >> 3); + let mut alpha = 255; + if mask_idx < bm_size { + if (mbits[mask_idx] << (x & 0x7)) & 0x80 == 0 { + alpha = 0; + } + } + let a = cbits[pix_idx + 2]; + let b = cbits[pix_idx + 1]; + let c = cbits[pix_idx]; + cbits[pix_idx] = a; + cbits[pix_idx + 1] = b; + cbits[pix_idx + 2] = c; + cbits[pix_idx + 3] = alpha; + pix_idx += 4; + } + } + return true; +} + +define_windows_service!(ffi_service_main, service_main); + +fn service_main(arguments: Vec) { + if let Err(e) = run_service(arguments) { + log::error!("run_service failed: {}", e); + } +} + +pub fn start_os_service() { + if let Err(e) = + windows_service::service_dispatcher::start(crate::get_app_name(), ffi_service_main) + { + log::error!("start_service failed: {}", e); + } +} + +const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + +extern "C" { + fn get_current_session(rdp: BOOL) -> DWORD; + fn is_session_locked(session_id: DWORD) -> BOOL; + fn LaunchProcessWin( + cmd: *const u16, + session_id: DWORD, + as_user: BOOL, + show: BOOL, + token_pid: &mut DWORD, + ) -> HANDLE; + fn GetSessionUserTokenWin( + lphUserToken: LPHANDLE, + dwSessionId: DWORD, + as_user: BOOL, + token_pid: &mut DWORD, + ) -> BOOL; + fn selectInputDesktop() -> BOOL; + fn inputDesktopSelected() -> BOOL; + fn is_windows_server() -> BOOL; + fn is_windows_10_or_greater() -> BOOL; + fn handleMask( + out: *mut u8, + mask: *const u8, + width: i32, + height: i32, + bmWidthBytes: i32, + bmHeight: i32, + ) -> i32; + fn drawOutline(out: *mut u8, in_: *const u8, width: i32, height: i32, out_size: i32); + fn get_di_bits(out: *mut u8, dc: HDC, hbmColor: HBITMAP, width: i32, height: i32) -> i32; + fn blank_screen(v: BOOL); + fn win32_enable_lowlevel_keyboard(hwnd: HWND) -> i32; + fn win32_disable_lowlevel_keyboard(hwnd: HWND); + fn win_stop_system_key_propagate(v: BOOL); + fn is_win_down() -> BOOL; + fn is_local_system() -> BOOL; + fn alloc_console_and_redirect(); + fn is_service_running_w(svc_name: *const u16) -> bool; +} + +pub fn get_current_session_id(share_rdp: bool) -> DWORD { + unsafe { get_current_session(if share_rdp { TRUE } else { FALSE }) } +} + +extern "system" { + fn BlockInput(v: BOOL) -> BOOL; +} + +#[tokio::main(flavor = "current_thread")] +async fn run_service(_arguments: Vec) -> ResultType<()> { + let event_handler = move |control_event| -> ServiceControlHandlerResult { + log::info!("Got service control event: {:?}", control_event); + match control_event { + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + ServiceControl::Stop | ServiceControl::Preshutdown | ServiceControl::Shutdown => { + send_close(crate::POSTFIX_SERVICE).ok(); + ServiceControlHandlerResult::NoError + } + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + // Register system service event handler + let status_handle = service_control_handler::register(crate::get_app_name(), event_handler)?; + + let next_status = ServiceStatus { + // Should match the one from system service registry + service_type: SERVICE_TYPE, + // The new state + current_state: ServiceState::Running, + // Accept stop events when running + controls_accepted: ServiceControlAccept::STOP, + // Used to report an error when starting or stopping only, otherwise must be zero + exit_code: ServiceExitCode::Win32(0), + // Only used for pending states, otherwise must be zero + checkpoint: 0, + // Only used for pending states, otherwise must be zero + wait_hint: Duration::default(), + process_id: None, + }; + + // Tell the system that the service is running now + status_handle.set_service_status(next_status)?; + + let mut session_id = unsafe { get_current_session(share_rdp()) }; + log::info!("session id {}", session_id); + let mut h_process = launch_server(session_id, true).await.unwrap_or(NULL); + let mut incoming = ipc::new_listener(crate::POSTFIX_SERVICE).await?; + let mut stored_usid = None; + loop { + let sids: Vec<_> = get_available_sessions(false) + .iter() + .map(|e| e.sid) + .collect(); + if !sids.contains(&session_id) || !is_share_rdp() { + let current_active_session = unsafe { get_current_session(share_rdp()) }; + if session_id != current_active_session { + session_id = current_active_session; + // https://github.com/rustdesk/rustdesk/discussions/10039 + let count = ipc::get_port_forward_session_count(1000).await.unwrap_or(0); + if count == 0 { + h_process = launch_server(session_id, true).await.unwrap_or(NULL); + } + } + } + let res = timeout(super::SERVICE_INTERVAL, incoming.next()).await; + match res { + Ok(res) => match res { + Some(Ok(stream)) => { + let mut stream = ipc::Connection::new(stream); + if let Ok(Some(data)) = stream.next_timeout(1000).await { + match data { + ipc::Data::Close => { + log::info!("close received"); + break; + } + ipc::Data::SAS => { + send_sas(); + } + ipc::Data::UserSid(usid) => { + if let Some(usid) = usid { + if session_id != usid { + log::info!( + "session changed from {} to {}", + session_id, + usid + ); + session_id = usid; + stored_usid = Some(session_id); + h_process = + launch_server(session_id, true).await.unwrap_or(NULL); + } + } + } + _ => {} + } + } + } + _ => {} + }, + Err(_) => { + // timeout + unsafe { + let tmp = get_current_session(share_rdp()); + if tmp == 0xFFFFFFFF { + continue; + } + let mut close_sent = false; + if tmp != session_id && stored_usid != Some(session_id) { + log::info!("session changed from {} to {}", session_id, tmp); + session_id = tmp; + let count = ipc::get_port_forward_session_count(1000).await.unwrap_or(0); + if count == 0 { + send_close_async("").await.ok(); + close_sent = true; + } + } + let mut exit_code: DWORD = 0; + if h_process.is_null() + || (GetExitCodeProcess(h_process, &mut exit_code) == TRUE + && exit_code != STILL_ACTIVE + && CloseHandle(h_process) == TRUE) + { + match launch_server(session_id, !close_sent).await { + Ok(ptr) => { + h_process = ptr; + } + Err(err) => { + log::error!("Failed to launch server: {}", err); + } + } + } + } + } + } + } + + if !h_process.is_null() { + send_close_async("").await.ok(); + unsafe { CloseHandle(h_process) }; + } + + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + Ok(()) +} + +async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType { + if close_first { + // in case started some elsewhere + send_close_async("").await.ok(); + } + let cmd = format!( + "\"{}\" --server", + std::env::current_exe()?.to_str().unwrap_or("") + ); + launch_privileged_process(session_id, &cmd) +} + +pub fn launch_privileged_process(session_id: DWORD, cmd: &str) -> ResultType { + use std::os::windows::ffi::OsStrExt; + let wstr: Vec = std::ffi::OsStr::new(&cmd) + .encode_wide() + .chain(Some(0).into_iter()) + .collect(); + let wstr = wstr.as_ptr(); + let mut token_pid = 0; + let h = unsafe { LaunchProcessWin(wstr, session_id, FALSE, FALSE, &mut token_pid) }; + if h.is_null() { + log::error!( + "Failed to launch privileged process: {}", + io::Error::last_os_error() + ); + if token_pid == 0 { + log::error!("No process winlogon.exe"); + } + } + Ok(h) +} + +pub fn run_as_user(arg: Vec<&str>) -> ResultType> { + run_exe_in_cur_session(std::env::current_exe()?.to_str().unwrap_or(""), arg, false) +} + +pub fn run_exe_direct( + exe: &str, + arg: Vec<&str>, + show: bool, +) -> ResultType> { + let mut cmd = std::process::Command::new(exe); + for a in arg { + cmd.arg(a); + } + if !show { + cmd.creation_flags(CREATE_NO_WINDOW); + } + match cmd.spawn() { + Ok(child) => Ok(Some(child)), + Err(e) => bail!("Failed to start process: {}", e), + } +} + +pub fn run_exe_in_cur_session( + exe: &str, + arg: Vec<&str>, + show: bool, +) -> ResultType> { + if is_root() { + let Some(session_id) = get_current_process_session_id() else { + bail!("Failed to get current process session id"); + }; + run_exe_in_session(exe, arg, session_id, show) + } else { + run_exe_direct(exe, arg, show) + } +} + +pub fn run_exe_in_session( + exe: &str, + arg: Vec<&str>, + session_id: DWORD, + show: bool, +) -> ResultType> { + use std::os::windows::ffi::OsStrExt; + let cmd = format!("\"{}\" {}", exe, arg.join(" "),); + let wstr: Vec = std::ffi::OsStr::new(&cmd) + .encode_wide() + .chain(Some(0).into_iter()) + .collect(); + let wstr = wstr.as_ptr(); + let mut token_pid = 0; + let h = unsafe { + LaunchProcessWin( + wstr, + session_id, + TRUE, + if show { TRUE } else { FALSE }, + &mut token_pid, + ) + }; + if h.is_null() { + if token_pid == 0 { + bail!( + "Failed to launch {:?} with session id {}: no process {}", + arg, + session_id, + EXPLORER_EXE + ); + } + bail!( + "Failed to launch {:?} with session id {}: {}", + arg, + session_id, + io::Error::last_os_error() + ); + } + Ok(None) +} + +#[tokio::main(flavor = "current_thread")] +async fn send_close(postfix: &str) -> ResultType<()> { + send_close_async(postfix).await +} + +async fn send_close_async(postfix: &str) -> ResultType<()> { + ipc::connect(1000, postfix) + .await? + .send(&ipc::Data::Close) + .await?; + // sleep a while to wait for closing and exit + sleep(0.1).await; + Ok(()) +} + +// https://docs.microsoft.com/en-us/windows/win32/api/sas/nf-sas-sendsas +// https://www.cnblogs.com/doutu/p/4892726.html +pub fn send_sas() { + #[link(name = "sas")] + extern "system" { + pub fn SendSAS(AsUser: BOOL); + } + unsafe { + log::info!("SAS received"); + + // Check and temporarily set SoftwareSASGeneration if needed + let mut original_value: Option = None; + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + if let Ok(policy_key) = hklm.open_subkey_with_flags( + "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", + KEY_READ | KEY_WRITE, + ) { + // Read current value + match policy_key.get_value::("SoftwareSASGeneration") { + Ok(value) => { + /* + - 0 = None (disabled) + - 1 = Services + - 2 = Ease of Access applications + - 3 = Services and Ease of Access applications (Both) + */ + if value != 1 && value != 3 { + original_value = Some(value); + log::info!("SoftwareSASGeneration is {}, setting to 1", value); + // Set to 1 for SendSAS to work + if let Err(e) = policy_key.set_value("SoftwareSASGeneration", &1u32) { + log::error!("Failed to set SoftwareSASGeneration: {}", e); + } + } + } + Err(e) => { + log::info!( + "SoftwareSASGeneration not found or error reading: {}, setting to 1", + e + ); + original_value = Some(0); // Mark that we need to restore (delete) it + // Create and set to 1 + if let Err(e) = policy_key.set_value("SoftwareSASGeneration", &1u32) { + log::error!("Failed to set SoftwareSASGeneration: {}", e); + } + } + } + } else { + log::error!("Failed to open registry key for SoftwareSASGeneration"); + } + + // Send SAS + SendSAS(FALSE); + + // Restore original value if we changed it + if let Some(original) = original_value { + if let Ok(policy_key) = hklm.open_subkey_with_flags( + "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", + KEY_WRITE, + ) { + if original == 0 { + // It didn't exist before, delete it + if let Err(e) = policy_key.delete_value("SoftwareSASGeneration") { + log::error!("Failed to delete SoftwareSASGeneration: {}", e); + } else { + log::info!("Deleted SoftwareSASGeneration (restored to original state)"); + } + } else { + // Restore the original value + if let Err(e) = policy_key.set_value("SoftwareSASGeneration", &original) { + log::error!( + "Failed to restore SoftwareSASGeneration to {}: {}", + original, + e + ); + } else { + log::info!("Restored SoftwareSASGeneration to {}", original); + } + } + } + } + } +} + +lazy_static::lazy_static! { + static ref SUPPRESS: Arc> = Arc::new(Mutex::new(Instant::now())); +} + +pub fn desktop_changed() -> bool { + unsafe { inputDesktopSelected() == FALSE } +} + +pub fn try_change_desktop() -> bool { + unsafe { + if inputDesktopSelected() == FALSE { + let res = selectInputDesktop() == TRUE; + if !res { + let mut s = SUPPRESS.lock().unwrap(); + if s.elapsed() > std::time::Duration::from_secs(3) { + log::error!("Failed to switch desktop: {}", io::Error::last_os_error()); + *s = Instant::now(); + } + } else { + log::info!("Desktop switched"); + } + return res; + } + } + return false; +} + +fn share_rdp() -> BOOL { + if get_reg("share_rdp") != "false" { + TRUE + } else { + FALSE + } +} + +pub fn is_share_rdp() -> bool { + share_rdp() == TRUE +} + +pub fn set_share_rdp(enable: bool) { + let (subkey, _, _, _) = get_install_info(); + let cmd = format!( + "reg add {} /f /v share_rdp /t REG_SZ /d \"{}\"", + subkey, + if enable { "true" } else { "false" } + ); + run_cmds(cmd, false, "share_rdp").ok(); +} + +pub fn get_current_process_session_id() -> Option { + get_session_id_of_process(unsafe { GetCurrentProcessId() }) +} + +pub fn get_session_id_of_process(pid: DWORD) -> Option { + let mut sid = 0; + if unsafe { ProcessIdToSessionId(pid, &mut sid) == TRUE } { + Some(sid) + } else { + None + } +} + +pub fn is_physical_console_session() -> Option { + if let Some(sid) = get_current_process_session_id() { + let physical_console_session_id = unsafe { get_current_session(FALSE) }; + if physical_console_session_id == u32::MAX { + return None; + } + return Some(physical_console_session_id == sid); + } + None +} + +pub fn get_active_username() -> String { + // get_active_user will give console username higher priority + if let Some(name) = get_current_session_username() { + return name; + } + if !is_root() { + return crate::username(); + } + + extern "C" { + fn get_active_user(path: *mut u16, n: u32, rdp: BOOL) -> u32; + } + let buff_size = 256; + let mut buff: Vec = Vec::with_capacity(buff_size); + buff.resize(buff_size, 0); + let n = unsafe { get_active_user(buff.as_mut_ptr(), buff_size as _, share_rdp()) }; + if n == 0 { + return "".to_owned(); + } + let sl = unsafe { std::slice::from_raw_parts(buff.as_ptr(), n as _) }; + String::from_utf16(sl) + .unwrap_or("??".to_owned()) + .trim_end_matches('\0') + .to_owned() +} + +fn get_current_session_username() -> Option { + let Some(sid) = get_current_process_session_id() else { + log::error!("get_current_process_session_id failed"); + return None; + }; + Some(get_session_username(sid)) +} + +fn get_session_username(session_id: u32) -> String { + extern "C" { + fn get_session_user_info(path: *mut u16, n: u32, session_id: u32) -> u32; + } + let buff_size = 256; + let mut buff: Vec = Vec::with_capacity(buff_size); + buff.resize(buff_size, 0); + let n = unsafe { get_session_user_info(buff.as_mut_ptr(), buff_size as _, session_id) }; + if n == 0 { + return "".to_owned(); + } + let sl = unsafe { std::slice::from_raw_parts(buff.as_ptr(), n as _) }; + String::from_utf16(sl) + .unwrap_or("".to_owned()) + .trim_end_matches('\0') + .to_owned() +} + +pub fn get_available_sessions(name: bool) -> Vec { + extern "C" { + fn get_available_session_ids(buf: *mut wchar_t, buf_size: c_int, include_rdp: bool); + } + const BUF_SIZE: c_int = 1024; + let mut buf: Vec = vec![0; BUF_SIZE as usize]; + + let station_session_id_array = unsafe { + get_available_session_ids(buf.as_mut_ptr(), BUF_SIZE, true); + let session_ids = String::from_utf16_lossy(&buf); + session_ids.trim_matches(char::from(0)).trim().to_string() + }; + let mut v: Vec = vec![]; + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-wtsgetactiveconsolesessionid + let physical_console_sid = unsafe { get_current_session(FALSE) }; + if physical_console_sid != u32::MAX { + let physical_console_name = if name { + let physical_console_username = get_session_username(physical_console_sid); + if physical_console_username.is_empty() { + "Console".to_owned() + } else { + format!("Console: {physical_console_username}") + } + } else { + "".to_owned() + }; + v.push(WindowsSession { + sid: physical_console_sid, + name: physical_console_name, + ..Default::default() + }); + } + // https://learn.microsoft.com/en-us/previous-versions//cc722458(v=technet.10)?redirectedfrom=MSDN + for type_session_id in station_session_id_array.split(",") { + let split: Vec<_> = type_session_id.split(":").collect(); + if split.len() == 2 { + if let Ok(sid) = split[1].parse::() { + if !v.iter().any(|e| (*e).sid == sid) { + let name = if name { + let name = get_session_username(sid); + if name.is_empty() { + split[0].to_string() + } else { + format!("{}: {}", split[0], name) + } + } else { + "".to_owned() + }; + v.push(WindowsSession { + sid, + name, + ..Default::default() + }); + } + } + } + } + if name { + let mut name_count: HashMap = HashMap::new(); + for session in &v { + *name_count.entry(session.name.clone()).or_insert(0) += 1; + } + let current_sid = get_current_process_session_id().unwrap_or_default(); + for e in v.iter_mut() { + let running = e.sid == current_sid && current_sid != 0; + if name_count.get(&e.name).map(|v| *v).unwrap_or_default() > 1 { + e.name = format!("{} (sid = {})", e.name, e.sid); + } + if running { + e.name = format!("{} (running)", e.name); + } + } + } + v +} + +pub fn get_active_user_home() -> Option { + let username = get_active_username(); + if !username.is_empty() { + let drive = std::env::var("SystemDrive").unwrap_or("C:".to_owned()); + let home = PathBuf::from(format!("{}\\Users\\{}", drive, username)); + if home.exists() { + return Some(home); + } + } + None +} + +pub fn is_prelogin() -> bool { + let Some(username) = get_current_session_username() else { + return false; + }; + username.is_empty() || username == "SYSTEM" +} + +pub fn is_locked() -> bool { + let Some(session_id) = get_current_process_session_id() else { + return false; + }; + unsafe { is_session_locked(session_id) == TRUE } +} + +#[inline] +pub fn is_logon_ui() -> ResultType { + let Some(current_sid) = get_current_process_session_id() else { + return Ok(false); + }; + let pids = get_pids("LogonUI.exe")?; + Ok(pids + .into_iter() + .any(|pid| get_session_id_of_process(pid) == Some(current_sid))) +} + +pub fn is_root() -> bool { + // https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user + unsafe { is_local_system() == TRUE } +} + +pub fn lock_screen() { + extern "system" { + pub fn LockWorkStation() -> BOOL; + } + unsafe { + LockWorkStation(); + } +} + +const IS1: &str = "{54E86BC2-6C85-41F3-A9EB-1A94AC9B1F93}_is1"; + +fn get_subkey(name: &str, wow: bool) -> String { + let tmp = format!( + "HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}", + name + ); + if wow { + tmp.replace("Microsoft", "Wow6432Node\\Microsoft") + } else { + tmp + } +} + +fn get_valid_subkey() -> String { + let subkey = get_subkey(IS1, false); + if !get_reg_of(&subkey, "InstallLocation").is_empty() { + return subkey; + } + let subkey = get_subkey(IS1, true); + if !get_reg_of(&subkey, "InstallLocation").is_empty() { + return subkey; + } + let app_name = crate::get_app_name(); + let subkey = get_subkey(&app_name, true); + if !get_reg_of(&subkey, "InstallLocation").is_empty() { + return subkey; + } + return get_subkey(&app_name, false); +} + +// Return install options other than InstallLocation. +pub fn get_install_options() -> String { + let app_name = crate::get_app_name(); + let subkey = format!(".{}", app_name.to_lowercase()); + let mut opts = HashMap::new(); + + let desktop_shortcuts = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_DESKTOPSHORTCUTS); + if let Some(desktop_shortcuts) = desktop_shortcuts { + opts.insert(REG_NAME_INSTALL_DESKTOPSHORTCUTS, desktop_shortcuts); + } + let start_menu_shortcuts = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_STARTMENUSHORTCUTS); + if let Some(start_menu_shortcuts) = start_menu_shortcuts { + opts.insert(REG_NAME_INSTALL_STARTMENUSHORTCUTS, start_menu_shortcuts); + } + let printer = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_PRINTER); + if let Some(printer) = printer { + opts.insert(REG_NAME_INSTALL_PRINTER, printer); + } + serde_json::to_string(&opts).unwrap_or("{}".to_owned()) +} + +// This function return Option, because some registry value may be empty. +fn get_reg_of_hkcr(subkey: &str, name: &str) -> Option { + let hkcr = RegKey::predef(HKEY_CLASSES_ROOT); + if let Ok(tmp) = hkcr.open_subkey(subkey.replace("HKEY_CLASSES_ROOT\\", "")) { + return tmp.get_value(name).ok(); + } + None +} + +pub fn get_install_info() -> (String, String, String, String) { + get_install_info_with_subkey(get_valid_subkey()) +} + +fn get_default_install_info() -> (String, String, String, String) { + get_install_info_with_subkey(get_subkey(&crate::get_app_name(), false)) +} + +fn get_default_install_path() -> String { + let mut pf = "C:\\Program Files".to_owned(); + if let Ok(x) = std::env::var("ProgramFiles") { + if std::path::Path::new(&x).exists() { + pf = x; + } + } + #[cfg(target_pointer_width = "32")] + { + let tmp = pf.replace("Program Files", "Program Files (x86)"); + if std::path::Path::new(&tmp).exists() { + pf = tmp; + } + } + format!("{}\\{}", pf, crate::get_app_name()) +} + +pub fn check_update_broker_process() -> ResultType<()> { + let process_exe = win_topmost_window::INJECTED_PROCESS_EXE; + let origin_process_exe = win_topmost_window::ORIGIN_PROCESS_EXE; + + let exe_file = std::env::current_exe()?; + let Some(cur_dir) = exe_file.parent() else { + bail!("Cannot get parent of current exe file"); + }; + let cur_exe = cur_dir.join(process_exe); + + // Force update broker exe if failed to check modified time. + let cmds = format!( + " + chcp 65001 + taskkill /F /IM {process_exe} + copy /Y \"{origin_process_exe}\" \"{cur_exe}\" + ", + cur_exe = cur_exe.to_string_lossy(), + ); + + if !std::path::Path::new(&cur_exe).exists() { + run_cmds(cmds, false, "update_broker")?; + return Ok(()); + } + + let ori_modified = fs::metadata(origin_process_exe)?.modified()?; + if let Ok(metadata) = fs::metadata(&cur_exe) { + if let Ok(cur_modified) = metadata.modified() { + if cur_modified == ori_modified { + return Ok(()); + } else { + log::info!( + "broker process updated, modify time from {:?} to {:?}", + cur_modified, + ori_modified + ); + } + } + } + + run_cmds(cmds, false, "update_broker")?; + + Ok(()) +} + +fn get_install_info_with_subkey(subkey: String) -> (String, String, String, String) { + let mut path = get_reg_of(&subkey, "InstallLocation"); + if path.is_empty() { + path = get_default_install_path(); + } + path = path.trim_end_matches('\\').to_owned(); + let start_menu = format!( + "%ProgramData%\\Microsoft\\Windows\\Start Menu\\Programs\\{}", + crate::get_app_name() + ); + let exe = format!("{}\\{}.exe", path, crate::get_app_name()); + (subkey, path, start_menu, exe) +} + +pub fn copy_raw_cmd(src_raw: &str, _raw: &str, _path: &str) -> ResultType { + let main_raw = format!( + "XCOPY \"{}\" \"{}\" /Y /E /H /C /I /K /R /Z", + PathBuf::from(src_raw) + .parent() + .ok_or(anyhow!("Can't get parent directory of {src_raw}"))? + .to_string_lossy() + .to_string(), + _path + ); + return Ok(main_raw); +} + +pub fn copy_exe_cmd(src_exe: &str, exe: &str, path: &str) -> ResultType { + let main_exe = copy_raw_cmd(src_exe, exe, path)?; + Ok(format!( + " + {main_exe} + copy /Y \"{ORIGIN_PROCESS_EXE}\" \"{path}\\{broker_exe}\" + ", + ORIGIN_PROCESS_EXE = win_topmost_window::ORIGIN_PROCESS_EXE, + broker_exe = win_topmost_window::INJECTED_PROCESS_EXE, + )) +} + +#[inline] +pub fn rename_exe_cmd(src_exe: &str, path: &str) -> ResultType { + let src_exe_filename = PathBuf::from(src_exe) + .file_name() + .ok_or(anyhow!("Can't get file name of {src_exe}"))? + .to_string_lossy() + .to_string(); + let app_name = crate::get_app_name().to_lowercase(); + if src_exe_filename.to_lowercase() == format!("{app_name}.exe") { + Ok("".to_owned()) + } else { + Ok(format!( + " + move /Y \"{path}\\{src_exe_filename}\" \"{path}\\{app_name}.exe\" + ", + )) + } +} + +#[inline] +pub fn remove_meta_toml_cmd(is_msi: bool, path: &str) -> String { + if is_msi && crate::is_custom_client() { + format!( + " + del /F /Q \"{path}\\meta.toml\" + ", + ) + } else { + "".to_owned() + } +} + +fn get_after_install( + exe: &str, + reg_value_start_menu_shortcuts: Option, + reg_value_desktop_shortcuts: Option, + reg_value_printer: Option, +) -> String { + let app_name = crate::get_app_name(); + let ext = app_name.to_lowercase(); + + // reg delete HKEY_CURRENT_USER\Software\Classes for + // https://github.com/rustdesk/rustdesk/commit/f4bdfb6936ae4804fc8ab1cf560db192622ad01a + // and https://github.com/leanflutter/uni_links_desktop/blob/1b72b0226cec9943ca8a84e244c149773f384e46/lib/src/protocol_registrar_impl_windows.dart#L30 + let hcu = RegKey::predef(HKEY_CURRENT_USER); + hcu.delete_subkey_all(format!("Software\\Classes\\{}", exe)) + .ok(); + + let desktop_shortcuts = reg_value_desktop_shortcuts + .map(|v| { + format!("reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {REG_NAME_INSTALL_DESKTOPSHORTCUTS} /t REG_SZ /d \"{v}\"") + }) + .unwrap_or_default(); + let start_menu_shortcuts = reg_value_start_menu_shortcuts + .map(|v| { + format!( + "reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {REG_NAME_INSTALL_STARTMENUSHORTCUTS} /t REG_SZ /d \"{v}\"" + ) + }) + .unwrap_or_default(); + let reg_printer = reg_value_printer + .map(|v| { + format!( + "reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {REG_NAME_INSTALL_PRINTER} /t REG_SZ /d \"{v}\"" + ) + }) + .unwrap_or_default(); + + format!(" + chcp 65001 + reg add HKEY_CLASSES_ROOT\\.{ext} /f + {desktop_shortcuts} + {start_menu_shortcuts} + {reg_printer} + reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f + reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{exe}\\\",0\" + reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f + reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open /f + reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f + reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{exe}\\\" --play \\\"%%1\\\"\" + reg add HKEY_CLASSES_ROOT\\{ext} /f + reg add HKEY_CLASSES_ROOT\\{ext} /f /v \"URL Protocol\" /t REG_SZ /d \"\" + reg add HKEY_CLASSES_ROOT\\{ext}\\shell /f + reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open /f + reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open\\command /f + reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{exe}\\\" \\\"%%1\\\"\" + netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=out action=allow program=\"{exe}\" enable=yes + netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=in action=allow program=\"{exe}\" enable=yes + {create_service} + reg add HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System /f /v SoftwareSASGeneration /t REG_DWORD /d 1 + ", create_service=get_create_service(&exe)) +} + +pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> ResultType<()> { + let uninstall_str = get_uninstall(false, false); + let mut path = path.trim_end_matches('\\').to_owned(); + let (subkey, _path, start_menu, exe) = get_default_install_info(); + let mut exe = exe; + if path.is_empty() { + path = _path; + } else { + exe = exe.replace(&_path, &path); + } + let mut version_major = "0"; + let mut version_minor = "0"; + let mut version_build = "0"; + let versions: Vec<&str> = crate::VERSION.split(".").collect(); + if versions.len() > 0 { + version_major = versions[0]; + } + if versions.len() > 1 { + version_minor = versions[1]; + } + if versions.len() > 2 { + version_build = versions[2]; + } + let app_name = crate::get_app_name(); + + let current_exe = std::env::current_exe()?; + + let tmp_path = std::env::temp_dir().to_string_lossy().to_string(); + let cur_exe = current_exe.to_str().unwrap_or("").to_owned(); + let shortcut_icon_location = get_shortcut_icon_location(&path, &cur_exe); + let mk_shortcut = write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +sLinkFile = \"{tmp_path}\\{app_name}.lnk\" + +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" + {shortcut_icon_location} +oLink.Save + " + ), + "vbs", + "mk_shortcut", + )? + .to_str() + .unwrap_or("") + .to_owned(); + // https://superuser.com/questions/392061/how-to-make-a-shortcut-from-cmd + let uninstall_shortcut = write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +sLinkFile = \"{tmp_path}\\Uninstall {app_name}.lnk\" +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" + oLink.Arguments = \"--uninstall\" + oLink.IconLocation = \"msiexec.exe\" +oLink.Save + " + ), + "vbs", + "uninstall_shortcut", + )? + .to_str() + .unwrap_or("") + .to_owned(); + let tray_shortcut = get_tray_shortcut(&path, &exe, &cur_exe, &tmp_path)?; + let mut reg_value_desktop_shortcuts = "0".to_owned(); + let mut reg_value_start_menu_shortcuts = "0".to_owned(); + let mut reg_value_printer = "0".to_owned(); + let mut shortcuts = Default::default(); + if options.contains("desktopicon") { + shortcuts = format!( + "copy /Y \"{}\\{}.lnk\" \"%PUBLIC%\\Desktop\\\"", + tmp_path, + crate::get_app_name() + ); + reg_value_desktop_shortcuts = "1".to_owned(); + } + if options.contains("startmenu") { + shortcuts = format!( + "{shortcuts} +md \"{start_menu}\" +copy /Y \"{tmp_path}\\{app_name}.lnk\" \"{start_menu}\\\" +copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" + " + ); + reg_value_start_menu_shortcuts = "1".to_owned(); + } + let install_printer = options.contains("printer") && is_win_10_or_greater(); + if install_printer { + reg_value_printer = "1".to_owned(); + } + + let meta = std::fs::symlink_metadata(¤t_exe)?; + let mut size = meta.len() / 1024; + if let Some(parent_dir) = current_exe.parent() { + if let Some(d) = parent_dir.to_str() { + size = get_directory_size_kb(d); + } + } + // https://docs.microsoft.com/zh-cn/windows/win32/msi/uninstall-registry-key?redirectedfrom=MSDNa + // https://www.windowscentral.com/how-edit-registry-using-command-prompt-windows-10 + // https://www.tenforums.com/tutorials/70903-add-remove-allowed-apps-through-windows-firewall-windows-10-a.html + // Note: without if exist, the bat may exit in advance on some Windows7 https://github.com/rustdesk/rustdesk/issues/895 + let dels = format!( + " +if exist \"{mk_shortcut}\" del /f /q \"{mk_shortcut}\" +if exist \"{uninstall_shortcut}\" del /f /q \"{uninstall_shortcut}\" +if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\" +if exist \"{tmp_path}\\{app_name}.lnk\" del /f /q \"{tmp_path}\\{app_name}.lnk\" +if exist \"{tmp_path}\\Uninstall {app_name}.lnk\" del /f /q \"{tmp_path}\\Uninstall {app_name}.lnk\" +if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} Tray.lnk\" + " + ); + let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_string(); + + // potential bug here: if run_cmd cancelled, but config file is changed. + if let Some(lic) = get_license() { + Config::set_option("key".into(), lic.key); + Config::set_option("custom-rendezvous-server".into(), lic.host); + Config::set_option("api-server".into(), lic.api); + } + + let tray_shortcuts = if config::is_outgoing_only() { + "".to_owned() + } else { + format!(" +cscript \"{tray_shortcut}\" +copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\" +") + }; + + let install_remote_printer = if install_printer { + // No need to use `|| true` here. + // The script will not exit even if `--install-remote-printer` panics. + format!("\"{}\" --install-remote-printer", &src_exe) + } else if is_win_10_or_greater() { + format!("\"{}\" --uninstall-remote-printer", &src_exe) + } else { + "".to_owned() + }; + + // Remember to check if `update_me` need to be changed if changing the `cmds`. + // No need to merge the existing dup code, because the code in these two functions are too critical. + // New code should be written in a common function. + let cmds = format!( + " +{uninstall_str} +chcp 65001 +md \"{path}\" +{copy_exe} +reg add {subkey} /f +reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{display_icon}\" +reg add {subkey} /f /v DisplayName /t REG_SZ /d \"{app_name}\" +reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\" +reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\" +reg add {subkey} /f /v BuildDate /t REG_SZ /d \"{build_date}\" +reg add {subkey} /f /v InstallLocation /t REG_SZ /d \"{path}\" +reg add {subkey} /f /v Publisher /t REG_SZ /d \"{app_name}\" +reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {version_major} +reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {version_minor} +reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build} +reg add {subkey} /f /v UninstallString /t REG_SZ /d \"\\\"{exe}\\\" --uninstall\" +reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size} +reg add {subkey} /f /v WindowsInstaller /t REG_DWORD /d 0 +cscript \"{mk_shortcut}\" +cscript \"{uninstall_shortcut}\" +{tray_shortcuts} +{shortcuts} +copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" +{dels} +{import_config} +{after_install} +{install_remote_printer} +{sleep} + ", + display_icon = get_custom_icon(&path, &cur_exe).unwrap_or(exe.to_string()), + version = crate::VERSION.replace("-", "."), + build_date = crate::BUILD_DATE, + after_install = get_after_install( + &exe, + Some(reg_value_start_menu_shortcuts), + Some(reg_value_desktop_shortcuts), + Some(reg_value_printer) + ), + sleep = if debug { "timeout 300" } else { "" }, + dels = if debug { "" } else { &dels }, + copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?, + import_config = get_import_config(&exe), + ); + run_cmds(cmds, debug, "install")?; + run_after_run_cmds(silent); + Ok(()) +} + +pub fn run_after_install() -> ResultType<()> { + let (_, _, _, exe) = get_install_info(); + run_cmds( + get_after_install(&exe, None, None, None), + true, + "after_install", + ) +} + +pub fn run_before_uninstall() -> ResultType<()> { + run_cmds(get_before_uninstall(true), true, "before_install") +} + +fn get_before_uninstall(kill_self: bool) -> String { + let app_name = crate::get_app_name(); + let ext = app_name.to_lowercase(); + let filter = if kill_self { + "".to_string() + } else { + format!(" /FI \"PID ne {}\"", get_current_pid()) + }; + format!( + " + chcp 65001 + sc stop {app_name} + sc delete {app_name} + taskkill /F /IM {broker_exe} + taskkill /F /IM {app_name}.exe{filter} + reg delete HKEY_CLASSES_ROOT\\.{ext} /f + reg delete HKEY_CLASSES_ROOT\\{ext} /f + netsh advfirewall firewall delete rule name=\"{app_name} Service\" + ", + broker_exe = WIN_TOPMOST_INJECTED_PROCESS_EXE, + ) +} + +/// Constructs the uninstall command string for the application. +/// +/// # Parameters +/// - `kill_self`: The command will kill the process of current app name. If `true`, it will kill +/// the current process as well. If `false`, it will exclude the current process from the kill +/// command. +/// - `uninstall_printer`: If `true`, includes commands to uninstall the remote printer. +/// +/// # Details +/// The `uninstall_printer` parameter determines whether the command to uninstall the remote printer +/// is included in the generated uninstall script. If `uninstall_printer` is `false`, the printer +/// related command is omitted from the script. +fn get_uninstall(kill_self: bool, uninstall_printer: bool) -> String { + let reg_uninstall_string = get_reg("UninstallString"); + if reg_uninstall_string.to_lowercase().contains("msiexec.exe") { + return reg_uninstall_string; + } + + let mut uninstall_cert_cmd = "".to_string(); + let mut uninstall_printer_cmd = "".to_string(); + if let Ok(exe) = std::env::current_exe() { + if let Some(exe_path) = exe.to_str() { + uninstall_cert_cmd = format!("\"{}\" --uninstall-cert", exe_path); + if uninstall_printer { + uninstall_printer_cmd = format!("\"{}\" --uninstall-remote-printer", &exe_path); + } + } + } + let (subkey, path, start_menu, _) = get_install_info(); + format!( + " + {before_uninstall} + {uninstall_printer_cmd} + {uninstall_cert_cmd} + reg delete {subkey} /f + {uninstall_amyuni_idd} + if exist \"{path}\" rd /s /q \"{path}\" + if exist \"{start_menu}\" rd /s /q \"{start_menu}\" + if exist \"%PUBLIC%\\Desktop\\{app_name}.lnk\" del /f /q \"%PUBLIC%\\Desktop\\{app_name}.lnk\" + if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" + ", + before_uninstall=get_before_uninstall(kill_self), + uninstall_amyuni_idd=get_uninstall_amyuni_idd(), + app_name = crate::get_app_name(), + ) +} + +pub fn uninstall_me(kill_self: bool) -> ResultType<()> { + run_cmds(get_uninstall(kill_self, true), true, "uninstall") +} + +fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType { + let mut cmds = cmds; + let mut tmp = std::env::temp_dir(); + // When dir contains these characters, the bat file will not execute in elevated mode. + if vec!["&", "@", "^"] + .drain(..) + .any(|s| tmp.to_string_lossy().to_string().contains(s)) + { + if let Ok(dir) = user_accessible_folder() { + tmp = dir; + } + } + tmp.push(format!("{}_{}.{}", crate::get_app_name(), tip, ext)); + let mut file = std::fs::File::create(&tmp)?; + if ext == "bat" { + let tmp2 = get_undone_file(&tmp)?; + std::fs::File::create(&tmp2).ok(); + cmds = format!( + " +{cmds} +if exist \"{path}\" del /f /q \"{path}\" +", + path = tmp2.to_string_lossy() + ); + } + // in case cmds mixed with \r\n and \n, make sure all ending with \r\n + // in some windows, \r\n required for cmd file to run + cmds = cmds.replace("\r\n", "\n").replace("\n", "\r\n"); + if ext == "vbs" { + let mut v: Vec = cmds.encode_utf16().collect(); + // utf8 -> utf16le which vbs support it only + file.write_all(to_le(&mut v))?; + } else { + file.write_all(cmds.as_bytes())?; + } + file.sync_all()?; + return Ok(tmp); +} + +fn to_le(v: &mut [u16]) -> &[u8] { + for b in v.iter_mut() { + *b = b.to_le() + } + unsafe { v.align_to().1 } +} + +fn get_undone_file(tmp: &Path) -> ResultType { + Ok(tmp.with_file_name(format!( + "{}.undone", + tmp.file_name() + .ok_or(anyhow!("Failed to get filename of {:?}", tmp))? + .to_string_lossy() + ))) +} + +fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> { + let tmp = write_cmds(cmds, "bat", tip)?; + let tmp2 = get_undone_file(&tmp)?; + let tmp_fn = tmp.to_str().unwrap_or(""); + // https://github.com/rustdesk/rustdesk/issues/6786#issuecomment-1879655410 + // Specify cmd.exe explicitly to avoid the replacement of cmd commands. + let res = runas::Command::new("cmd.exe") + .args(&["/C", &tmp_fn]) + .show(show) + .force_prompt(true) + .status(); + if !show { + allow_err!(std::fs::remove_file(tmp)); + } + let _ = res?; + if tmp2.exists() { + allow_err!(std::fs::remove_file(tmp2)); + bail!("{} failed", tip); + } + Ok(()) +} + +pub fn toggle_blank_screen(v: bool) { + let v = if v { TRUE } else { FALSE }; + unsafe { + blank_screen(v); + } +} + +pub fn block_input(v: bool) -> (bool, String) { + let v = if v { TRUE } else { FALSE }; + unsafe { + if BlockInput(v) == TRUE { + (true, "".to_owned()) + } else { + (false, format!("Error: {}", io::Error::last_os_error())) + } + } +} + +pub fn add_recent_document(path: &str) { + extern "C" { + fn AddRecentDocument(path: *const u16); + } + use std::os::windows::ffi::OsStrExt; + let wstr: Vec = std::ffi::OsStr::new(path) + .encode_wide() + .chain(Some(0).into_iter()) + .collect(); + let wstr = wstr.as_ptr(); + unsafe { + AddRecentDocument(wstr); + } +} + +pub fn is_installed() -> bool { + let (_, _, _, exe) = get_install_info(); + std::fs::metadata(exe).is_ok() +} + +pub fn get_reg(name: &str) -> String { + let (subkey, _, _, _) = get_install_info(); + get_reg_of(&subkey, name) +} + +fn get_reg_of(subkey: &str, name: &str) -> String { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + if let Ok(tmp) = hklm.open_subkey(subkey.replace("HKEY_LOCAL_MACHINE\\", "")) { + if let Ok(v) = tmp.get_value(name) { + return v; + } + } + "".to_owned() +} + +fn get_public_base_dir() -> PathBuf { + if let Ok(allusersprofile) = std::env::var("ALLUSERSPROFILE") { + let path = PathBuf::from(&allusersprofile); + if path.exists() { + return path; + } + } + if let Ok(public) = std::env::var("PUBLIC") { + let path = PathBuf::from(public).join("Documents"); + if path.exists() { + return path; + } + } + let program_data_dir = PathBuf::from("C:\\ProgramData"); + if program_data_dir.exists() { + return program_data_dir; + } + std::env::temp_dir() +} + +#[inline] +pub fn get_custom_client_staging_dir() -> PathBuf { + get_public_base_dir() + .join("RustDesk") + .join("RustDeskCustomClientStaging") +} + +/// Removes the custom client staging directory. +/// +/// Current behavior: intentionally a no-op (does not delete). +/// +/// Rationale +/// - The staging directory only contains a small `custom.txt`, leaving it is harmless. +/// - Deleting directories under a public location (e.g., C:\\ProgramData\\RustDesk) is +/// susceptible to TOCTOU attacks if an unprivileged user can replace the path with a +/// symlink/junction between checks and deletion. +/// +/// Future work: +/// - Use the files (if needed) in the installation directory instead of a public location. +/// This directory only contains a small `custom.txt` file. +/// - Pass the custom client name directly via command line +/// or environment variable during update installation. Then no staging directory is needed. +#[inline] +pub fn remove_custom_client_staging_dir(staging_dir: &Path) -> ResultType { + if !staging_dir.exists() { + return Ok(false); + } + + // First explicitly removes `custom.txt` to ensure stale config is never replayed, + // even if the subsequent directory removal fails. + // + // `std::fs::remove_file` on a symlink removes the symlink itself, not the target, + // so this is safe even in a TOCTOU race. + let custom_txt_path = staging_dir.join("custom.txt"); + if custom_txt_path.exists() { + allow_err!(std::fs::remove_file(&custom_txt_path)); + } + + // Intentionally not deleting. See the function docs for rationale. + log::debug!( + "Skip deleting staging directory {:?} (intentional to avoid TOCTOU)", + staging_dir + ); + Ok(false) +} + +// Prepare custom client update by copying staged custom.txt to current directory and loading it. +// Returns: +// 1. Ok(true) if preparation was successful or no staging directory exists. +// 2. Ok(false) if custom.txt file exists but has invalid contents or fails security checks +// (e.g., is a symlink or has invalid contents). +// 3. Err if any unexpected error occurs during file operations. +pub fn prepare_custom_client_update() -> ResultType { + let custom_client_staging_dir = get_custom_client_staging_dir(); + let current_exe = std::env::current_exe()?; + let current_exe_dir = current_exe + .parent() + .ok_or(anyhow!("Cannot get parent directory of current exe"))?; + + let staging_dir = custom_client_staging_dir.clone(); + let clear_staging_on_exit = crate::SimpleCallOnReturn { + b: true, + f: Box::new( + move || match remove_custom_client_staging_dir(&staging_dir) { + Ok(existed) => { + if existed { + log::info!("Custom client staging directory removed successfully."); + } + } + Err(e) => { + log::error!( + "Failed to remove custom client staging directory {:?}: {}", + staging_dir, + e + ); + } + }, + ), + }; + + if custom_client_staging_dir.exists() { + let custom_txt_path = custom_client_staging_dir.join("custom.txt"); + if !custom_txt_path.exists() { + return Ok(true); + } + + let metadata = std::fs::symlink_metadata(&custom_txt_path)?; + if metadata.is_symlink() { + log::error!( + "custom.txt is a symlink. Refusing to load custom client for security reasons." + ); + drop(clear_staging_on_exit); + return Ok(false); + } + if metadata.is_file() { + // Copy custom.txt to current directory + let local_custom_file_path = current_exe_dir.join("custom.txt"); + log::debug!( + "Copying staged custom file from {:?} to {:?}", + custom_txt_path, + local_custom_file_path + ); + + // No need to check symlink before copying. + // `load_custom_client()` will fail if the file is not valid. + fs::copy(&custom_txt_path, &local_custom_file_path)?; + log::info!("Staged custom client file copied to current directory."); + + // Load custom client + let is_custom_file_exists = + local_custom_file_path.exists() && local_custom_file_path.is_file(); + crate::load_custom_client(); + + // Remove the copied custom.txt file + allow_err!(fs::remove_file(&local_custom_file_path)); + + // Check if loaded successfully + if is_custom_file_exists && !crate::common::is_custom_client() { + // The custom.txt file existed, but its contents are invalid. + log::error!("Failed to load custom client from custom.txt."); + drop(clear_staging_on_exit); + // ERROR_INVALID_DATA + return Ok(false); + } + } else { + log::info!("No custom client files found in staging directory."); + } + } else { + log::info!( + "Custom client staging directory {:?} does not exist.", + custom_client_staging_dir + ); + } + + Ok(true) +} + +pub fn get_license_from_exe_name() -> ResultType { + let mut exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); + // if defined portable appname entry, replace original executable name with it. + if let Ok(portable_exe) = std::env::var(PORTABLE_APPNAME_RUNTIME_ENV_KEY) { + exe = portable_exe; + } + get_custom_server_from_string(&exe) +} + +// We can't directly use `RegKey::set_value` to update the registry value, because it will fail with `ERROR_ACCESS_DENIED` +// So we have to use `run_cmds` to update the registry value. +pub fn update_install_option(k: &str, v: &str) -> ResultType<()> { + // Don't update registry if not installed or not server process. + if !is_installed() || !crate::is_server() { + return Ok(()); + } + if ![REG_NAME_INSTALL_PRINTER].contains(&k) || !["0", "1"].contains(&v) { + return Ok(()); + } + let app_name = crate::get_app_name(); + let ext = app_name.to_lowercase(); + let cmds = + format!("chcp 65001 && reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {k} /t REG_SZ /d \"{v}\""); + run_cmds(cmds, false, "update_install_option")?; + Ok(()) +} + +#[inline] +pub fn is_win_server() -> bool { + unsafe { is_windows_server() > 0 } +} + +#[inline] +pub fn is_win_10_or_greater() -> bool { + unsafe { is_windows_10_or_greater() > 0 } +} + +pub fn bootstrap() -> bool { + if let Ok(lic) = get_license_from_exe_name() { + *config::EXE_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); + } + + #[cfg(debug_assertions)] + { + true + } + #[cfg(not(debug_assertions))] + { + // This function will cause `'sciter.dll' was not found neither in PATH nor near the current executable.` when debugging RustDesk. + // Only call set_safe_load_dll() on Windows 10 or greater + if is_win_10_or_greater() { + set_safe_load_dll() + } else { + true + } + } +} + +#[cfg(not(debug_assertions))] +fn set_safe_load_dll() -> bool { + if !unsafe { set_default_dll_directories() } { + return false; + } + + // `SetDllDirectoryW` should never fail. + // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw + if unsafe { SetDllDirectoryW(wide_string("").as_ptr()) == FALSE } { + eprintln!("SetDllDirectoryW failed: {}", io::Error::last_os_error()); + return false; + } + + true +} + +// https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-setdefaultdlldirectories +#[cfg(not(debug_assertions))] +unsafe fn set_default_dll_directories() -> bool { + let module = LoadLibraryExW( + wide_string("Kernel32.dll").as_ptr(), + 0 as _, + LOAD_LIBRARY_SEARCH_SYSTEM32, + ); + if module.is_null() { + return false; + } + + match CString::new("SetDefaultDllDirectories") { + Err(e) => { + eprintln!("CString::new failed: {}", e); + return false; + } + Ok(func_name) => { + let func = GetProcAddress(module, func_name.as_ptr()); + if func.is_null() { + eprintln!("GetProcAddress failed: {}", io::Error::last_os_error()); + return false; + } + type SetDefaultDllDirectories = unsafe extern "system" fn(DWORD) -> BOOL; + let func: SetDefaultDllDirectories = std::mem::transmute(func); + if func(LOAD_LIBRARY_SEARCH_SYSTEM32 | LOAD_LIBRARY_SEARCH_USER_DIRS) == FALSE { + eprintln!( + "SetDefaultDllDirectories failed: {}", + io::Error::last_os_error() + ); + return false; + } + } + } + true +} + +fn get_custom_icon(install_dir: &str, exe: &str) -> Option { + const RELATIVE_ICON_PATH: &str = "data\\flutter_assets\\assets\\icon.ico"; + if crate::is_custom_client() { + if let Some(p) = PathBuf::from(exe).parent() { + let alter_icon_path = p.join(RELATIVE_ICON_PATH); + if alter_icon_path.exists() { + // During installation, files under `install_dir` may not exist yet. + // So we validate the icon from the current executable directory first. + // But for shortcut/registry icon location, we should point to the final + // installed path so the icon works across different Windows users. + if let Ok(metadata) = std::fs::symlink_metadata(&alter_icon_path) { + if metadata.is_symlink() { + log::warn!( + "Custom icon at {:?} is a symlink, refusing to use it.", + alter_icon_path + ); + return None; + } + if metadata.is_file() { + return if install_dir.is_empty() { + Some(alter_icon_path.to_string_lossy().to_string()) + } else { + Some(format!("{}\\{}", install_dir, RELATIVE_ICON_PATH)) + }; + } + } + } + } + } + None +} + +#[inline] +fn get_shortcut_icon_location(install_dir: &str, exe: &str) -> String { + if exe.is_empty() { + return "".to_owned(); + } + + get_custom_icon(install_dir, exe) + .map(|p| format!("oLink.IconLocation = \"{}\"", p)) + .unwrap_or_default() +} + +pub fn create_shortcut(id: &str) -> ResultType<()> { + let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); + // https://github.com/rustdesk/rustdesk/issues/13735 + // Replace ':' with '_' for filename since ':' is not allowed in Windows filenames + // https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384 + let filename = id.replace(':', "_"); + let shortcut_icon_location = get_shortcut_icon_location("", &exe); + let shortcut = write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +strDesktop = oWS.SpecialFolders(\"Desktop\") +Set objFSO = CreateObject(\"Scripting.FileSystemObject\") +sLinkFile = objFSO.BuildPath(strDesktop, \"{filename}.lnk\") +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" + oLink.Arguments = \"--connect {id}\" + {shortcut_icon_location} +oLink.Save + " + ), + "vbs", + "connect_shortcut", + )? + .to_str() + .unwrap_or("") + .to_owned(); + std::process::Command::new("cscript") + .arg(&shortcut) + .creation_flags(CREATE_NO_WINDOW) + .output()?; + allow_err!(std::fs::remove_file(shortcut)); + Ok(()) +} + +pub fn enable_lowlevel_keyboard(hwnd: HWND) { + let ret = unsafe { win32_enable_lowlevel_keyboard(hwnd) }; + if ret != 0 { + log::error!("Failure grabbing keyboard"); + return; + } +} + +pub fn disable_lowlevel_keyboard(hwnd: HWND) { + unsafe { win32_disable_lowlevel_keyboard(hwnd) }; +} + +pub fn stop_system_key_propagate(v: bool) { + unsafe { win_stop_system_key_propagate(if v { TRUE } else { FALSE }) }; +} + +pub fn get_win_key_state() -> bool { + unsafe { is_win_down() == TRUE } +} + +pub fn quit_gui() { + std::process::exit(0); + // unsafe { PostQuitMessage(0) }; // some how not work +} + +pub fn get_user_token(session_id: u32, as_user: bool) -> HANDLE { + let mut token = NULL as HANDLE; + unsafe { + let mut _token_pid = 0; + if FALSE + == GetSessionUserTokenWin( + &mut token as _, + session_id, + if as_user { TRUE } else { FALSE }, + &mut _token_pid, + ) + { + NULL as _ + } else { + token + } + } +} + +pub fn run_background(exe: &str, arg: &str) -> ResultType { + let wexe = wide_string(exe); + let warg; + unsafe { + let ret = ShellExecuteW( + NULL as _, + NULL as _, + wexe.as_ptr() as _, + if arg.is_empty() { + NULL as _ + } else { + warg = wide_string(arg); + warg.as_ptr() as _ + }, + NULL as _, + SW_HIDE, + ); + return Ok(ret as i32 > 32); + } +} + +pub fn run_uac(exe: &str, arg: &str) -> ResultType { + let wop = wide_string("runas"); + let wexe = wide_string(exe); + let warg; + unsafe { + let ret = ShellExecuteW( + NULL as _, + wop.as_ptr() as _, + wexe.as_ptr() as _, + if arg.is_empty() { + NULL as _ + } else { + warg = wide_string(arg); + warg.as_ptr() as _ + }, + NULL as _, + SW_SHOWNORMAL, + ); + return Ok(ret as i32 > 32); + } +} + +pub fn check_super_user_permission() -> ResultType { + run_uac( + std::env::current_exe()? + .to_string_lossy() + .to_string() + .as_str(), + "--version", + ) +} + +pub fn elevate(arg: &str) -> ResultType { + run_uac( + std::env::current_exe()? + .to_string_lossy() + .to_string() + .as_str(), + arg, + ) +} + +pub fn run_as_system(arg: &str) -> ResultType<()> { + let exe = std::env::current_exe()?.to_string_lossy().to_string(); + if impersonate_system::run_as_system(&exe, arg).is_err() { + bail!(format!("Failed to run {} as system", exe)); + } + Ok(()) +} + +pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_system: bool) { + // avoid possible run recursively due to failed run. + log::info!( + "elevate: {} -> {:?}, run_as_system: {} -> {}", + is_elevate, + is_elevated(None), + is_run_as_system, + crate::username(), + ); + let arg_elevate = if is_setup { + "--noinstall --elevate" + } else { + "--elevate" + }; + let arg_run_as_system = if is_setup { + "--noinstall --run-as-system" + } else { + "--run-as-system" + }; + if is_root() { + if is_run_as_system { + log::info!("run portable service"); + crate::portable_service::server::run_portable_service(); + } + } else { + match is_elevated(None) { + Ok(elevated) => { + if elevated { + if !is_run_as_system { + if run_as_system(arg_run_as_system).is_ok() { + std::process::exit(0); + } else { + log::error!( + "Failed to run as system, error {}", + io::Error::last_os_error() + ); + } + } + } else { + if !is_elevate { + if let Ok(true) = elevate(arg_elevate) { + std::process::exit(0); + } else { + log::error!("Failed to elevate, error {}", io::Error::last_os_error()); + } + } + } + } + Err(_) => log::error!( + "Failed to get elevation status, error {}", + io::Error::last_os_error() + ), + } + } +} + +pub fn is_elevated(process_id: Option) -> ResultType { + use hbb_common::platform::windows::RAIIHandle; + unsafe { + let handle: HANDLE = match process_id { + Some(process_id) => OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_id), + None => GetCurrentProcess(), + }; + if handle == NULL { + bail!( + "Failed to open process, error {}", + io::Error::last_os_error() + ) + } + let _handle = RAIIHandle(handle); + let mut token: HANDLE = mem::zeroed(); + if OpenProcessToken(handle, TOKEN_QUERY, &mut token) == FALSE { + bail!( + "Failed to open process token, error {}", + io::Error::last_os_error() + ) + } + let _token = RAIIHandle(token); + let mut token_elevation: TOKEN_ELEVATION = mem::zeroed(); + let mut size: DWORD = 0; + if GetTokenInformation( + token, + TokenElevation, + (&mut token_elevation) as *mut _ as *mut c_void, + mem::size_of::() as _, + &mut size, + ) == FALSE + { + bail!( + "Failed to get token information, error {}", + io::Error::last_os_error() + ) + } + + Ok(token_elevation.TokenIsElevated != 0) + } +} + +pub fn is_foreground_window_elevated() -> ResultType { + unsafe { + let mut process_id: DWORD = 0; + GetWindowThreadProcessId(GetForegroundWindow(), &mut process_id); + if process_id == 0 { + bail!( + "Failed to get processId, error {}", + io::Error::last_os_error() + ) + } + is_elevated(Some(process_id)) + } +} + +fn get_current_pid() -> u32 { + unsafe { GetCurrentProcessId() } +} + +pub fn get_double_click_time() -> u32 { + unsafe { GetDoubleClickTime() } +} + +pub fn wide_string(s: &str) -> Vec { + use std::os::windows::prelude::OsStrExt; + std::ffi::OsStr::new(s) + .encode_wide() + .chain(Some(0).into_iter()) + .collect() +} + +/// send message to currently shown window +pub fn send_message_to_hnwd( + class_name: &str, + window_name: &str, + dw_data: usize, + data: &str, + show_window: bool, +) -> bool { + unsafe { + let class_name_utf16 = wide_string(class_name); + let window_name_utf16 = wide_string(window_name); + let window = FindWindowW(class_name_utf16.as_ptr(), window_name_utf16.as_ptr()); + if window.is_null() { + log::warn!("no such window {}:{}", class_name, window_name); + return false; + } + let mut data_struct = COPYDATASTRUCT::default(); + data_struct.dwData = dw_data; + let mut data_zero: String = data.chars().chain(Some('\0').into_iter()).collect(); + println!("send {:?}", data_zero); + data_struct.cbData = data_zero.len() as _; + data_struct.lpData = data_zero.as_mut_ptr() as _; + SendMessageW( + window, + WM_COPYDATA, + 0, + &data_struct as *const COPYDATASTRUCT as _, + ); + if show_window { + ShowWindow(window, SW_NORMAL); + SetForegroundWindow(window); + } + } + return true; +} + +pub fn get_logon_user_token(user: &str, pwd: &str) -> ResultType { + let user_split = user.split("\\").collect::>(); + let wuser = wide_string(user_split.get(1).unwrap_or(&user)); + let wpc = wide_string(user_split.get(0).unwrap_or(&"")); + let wpwd = wide_string(pwd); + let mut ph_token: HANDLE = std::ptr::null_mut(); + let res = unsafe { + LogonUserW( + wuser.as_ptr(), + wpc.as_ptr(), + wpwd.as_ptr(), + LOGON32_LOGON_INTERACTIVE, + LOGON32_PROVIDER_DEFAULT, + &mut ph_token as _, + ) + }; + if res == FALSE { + bail!( + "Failed to log on user {}: {}", + user, + std::io::Error::last_os_error() + ); + } else { + if ph_token.is_null() { + bail!( + "Failed to log on user {}: {}", + user, + std::io::Error::last_os_error() + ); + } + Ok(ph_token) + } +} + +// Ensure the token returned is a primary token. +// If the provided token is an impersonation token, it duplicates it to a primary token. +// If the provided token is already a primary token, it returns it as is. +// The caller is responsible for closing the returned token handle. +pub fn ensure_primary_token(user_token: HANDLE) -> ResultType { + if user_token.is_null() || user_token == INVALID_HANDLE_VALUE { + bail!("Invalid user token provided"); + } + + unsafe { + let mut token_type: TOKEN_TYPE = 0; + let mut return_length: DWORD = 0; + + if GetTokenInformation( + user_token, + TokenType, + &mut token_type as *mut _ as *mut _, + std::mem::size_of::() as DWORD, + &mut return_length, + ) == FALSE + { + bail!( + "Failed to get token type, error {}", + io::Error::last_os_error() + ); + } + + if token_type == TokenImpersonation { + let mut duplicate_token: HANDLE = std::ptr::null_mut(); + let dup_res = DuplicateToken(user_token, SecurityImpersonation, &mut duplicate_token); + CloseHandle(user_token); + if dup_res == FALSE { + bail!( + "Failed to duplicate token, error {}", + io::Error::last_os_error() + ); + } + Ok(duplicate_token) + } else { + Ok(user_token) + } + } +} + +pub fn is_user_token_admin(user_token: HANDLE) -> ResultType { + if user_token.is_null() || user_token == INVALID_HANDLE_VALUE { + bail!("Invalid user token provided"); + } + + unsafe { + let mut dw_size: DWORD = 0; + GetTokenInformation( + user_token, + TokenGroups, + std::ptr::null_mut(), + 0, + &mut dw_size, + ); + + let last_error = GetLastError(); + if last_error != ERROR_INSUFFICIENT_BUFFER { + bail!( + "Failed to get token groups buffer size, error: {}", + last_error + ); + } + if dw_size == 0 { + bail!("Token groups buffer size is zero"); + } + + let mut buffer = vec![0u8; dw_size as usize]; + if GetTokenInformation( + user_token, + TokenGroups, + buffer.as_mut_ptr() as *mut _, + dw_size, + &mut dw_size, + ) == FALSE + { + bail!( + "Failed to get token groups information, error: {}", + io::Error::last_os_error() + ); + } + + let p_token_groups = buffer.as_ptr() as *const TOKEN_GROUPS; + let group_count = (*p_token_groups).GroupCount; + + if group_count == 0 { + return Ok(false); + } + + let mut nt_authority: SID_IDENTIFIER_AUTHORITY = SID_IDENTIFIER_AUTHORITY { + Value: SECURITY_NT_AUTHORITY, + }; + let mut administrators_group: PSID = std::ptr::null_mut(); + if AllocateAndInitializeSid( + &mut nt_authority, + 2, + SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, + 0, + 0, + 0, + 0, + 0, + 0, + &mut administrators_group, + ) == FALSE + { + bail!( + "Failed to allocate administrators group SID, error: {}", + io::Error::last_os_error() + ); + } + if administrators_group.is_null() { + bail!("Failed to create administrators group SID"); + } + + let mut is_admin = false; + let groups = + std::slice::from_raw_parts((*p_token_groups).Groups.as_ptr(), group_count as usize); + for group in groups { + if EqualSid(administrators_group, group.Sid) == TRUE { + is_admin = true; + break; + } + } + + if !administrators_group.is_null() { + FreeSid(administrators_group); + } + + Ok(is_admin) + } +} + +pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> ResultType<()> { + let last_error_table = HashMap::from([ + ( + ERROR_LOGON_FAILURE, + "The user name or password is incorrect.", + ), + (ERROR_ACCESS_DENIED, "Access is denied."), + ]); + + unsafe { + let user_split = user.split("\\").collect::>(); + let wuser = wide_string(user_split.get(1).unwrap_or(&user)); + let wpc = wide_string(user_split.get(0).unwrap_or(&"")); + let wpwd = wide_string(pwd); + let cmd = if arg.is_empty() { + format!("\"{}\"", exe) + } else { + format!("\"{}\" {}", exe, arg) + }; + let mut wcmd = wide_string(&cmd); + let mut si: STARTUPINFOW = mem::zeroed(); + si.wShowWindow = SW_HIDE as _; + si.lpDesktop = NULL as _; + si.cb = std::mem::size_of::() as _; + si.dwFlags = STARTF_USESHOWWINDOW; + let mut pi: PROCESS_INFORMATION = mem::zeroed(); + let wexe = wide_string(exe); + if FALSE + == CreateProcessWithLogonW( + wuser.as_ptr(), + wpc.as_ptr(), + wpwd.as_ptr(), + LOGON_WITH_PROFILE, + wexe.as_ptr(), + wcmd.as_mut_ptr(), + CREATE_UNICODE_ENVIRONMENT, + NULL, + NULL as _, + &mut si as *mut STARTUPINFOW, + &mut pi as *mut PROCESS_INFORMATION, + ) + { + let last_error = GetLastError(); + bail!( + "CreateProcessWithLogonW failed : \"{}\", error {}", + last_error_table + .get(&last_error) + .unwrap_or(&"Unknown error"), + io::Error::from_raw_os_error(last_error as _) + ); + } + } + return Ok(()); +} + +pub fn set_path_permission(dir: &Path, permission: &str) -> ResultType<()> { + std::process::Command::new("icacls") + .arg(dir.as_os_str()) + .arg("/grant") + .arg(format!("*S-1-1-0:(OI)(CI){}", permission)) + .arg("/T") + .spawn()?; + Ok(()) +} + +#[inline] +fn str_to_device_name(name: &str) -> [u16; 32] { + let mut device_name: Vec = wide_string(name); + if device_name.len() < 32 { + device_name.resize(32, 0); + } + let mut result = [0; 32]; + result.copy_from_slice(&device_name[..32]); + result +} + +pub fn resolutions(name: &str) -> Vec { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + let mut v = vec![]; + let mut num = 0; + let device_name = str_to_device_name(name); + loop { + if EnumDisplaySettingsW(device_name.as_ptr(), num, &mut dm) == 0 { + break; + } + let r = Resolution { + width: dm.dmPelsWidth as _, + height: dm.dmPelsHeight as _, + ..Default::default() + }; + if !v.contains(&r) { + v.push(r); + } + num += 1; + } + v + } +} + +pub fn current_resolution(name: &str) -> ResultType { + let device_name = str_to_device_name(name); + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + dm.dmSize = std::mem::size_of::() as _; + if EnumDisplaySettingsW(device_name.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) == 0 { + bail!( + "failed to get current resolution, error {}", + io::Error::last_os_error() + ); + } + let r = Resolution { + width: dm.dmPelsWidth as _, + height: dm.dmPelsHeight as _, + ..Default::default() + }; + Ok(r) + } +} + +pub(super) fn change_resolution_directly( + name: &str, + width: usize, + height: usize, +) -> ResultType<()> { + let device_name = str_to_device_name(name); + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + dm.dmSize = std::mem::size_of::() as _; + dm.dmPelsWidth = width as _; + dm.dmPelsHeight = height as _; + dm.dmFields = DM_PELSHEIGHT | DM_PELSWIDTH; + let res = ChangeDisplaySettingsExW( + device_name.as_ptr(), + &mut dm, + NULL as _, + CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_RESET, + NULL, + ); + if res != DISP_CHANGE_SUCCESSFUL { + bail!( + "ChangeDisplaySettingsExW failed, res={}, error {}", + res, + io::Error::last_os_error() + ); + } + Ok(()) + } +} + +pub fn user_accessible_folder() -> ResultType { + let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); + let dir1 = PathBuf::from(format!("{}\\ProgramData", disk)); + // NOTICE: "C:\Windows\Temp" requires permanent authorization. + let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk)); + let dir; + if dir1.exists() { + dir = dir1; + } else if dir2.exists() { + dir = dir2; + } else { + bail!("no valid user accessible folder"); + } + Ok(dir) +} + +#[inline] +pub fn uninstall_cert() -> ResultType<()> { + cert::uninstall_cert() +} + +mod cert { + use hbb_common::ResultType; + + extern "C" { + fn DeleteRustDeskTestCertsW(); + } + pub fn uninstall_cert() -> ResultType<()> { + unsafe { + DeleteRustDeskTestCertsW(); + } + Ok(()) + } +} + +#[inline] +pub fn get_char_from_vk(vk: u32) -> Option { + get_char_from_unicode(get_unicode_from_vk(vk)?) +} + +pub fn get_char_from_unicode(unicode: u16) -> Option { + let buff = [unicode]; + if let Some(chr) = String::from_utf16(&buff[..1]).ok()?.chars().next() { + if chr.is_control() { + return None; + } else { + Some(chr) + } + } else { + None + } +} + +pub fn get_unicode_from_vk(vk: u32) -> Option { + const BUF_LEN: i32 = 32; + let mut buff = [0_u16; BUF_LEN as usize]; + let buff_ptr = buff.as_mut_ptr(); + let len = unsafe { + let current_window_thread_id = GetWindowThreadProcessId(GetForegroundWindow(), null_mut()); + let layout = GetKeyboardLayout(current_window_thread_id); + + // refs: https://github.com/rustdesk-org/rdev/blob/25a99ce71ab42843ad253dd51e6a35e83e87a8a4/src/windows/keyboard.rs#L115 + let press_state = 129; + let mut state: [BYTE; 256] = [0; 256]; + let shift_left = rdev::get_modifier(rdev::Key::ShiftLeft); + let shift_right = rdev::get_modifier(rdev::Key::ShiftRight); + if shift_left { + state[VK_LSHIFT as usize] = press_state; + } + if shift_right { + state[VK_RSHIFT as usize] = press_state; + } + if shift_left || shift_right { + state[VK_SHIFT as usize] = press_state; + } + ToUnicodeEx(vk, 0x00, &state as _, buff_ptr, BUF_LEN, 0, layout) + }; + if len == 1 { + Some(buff[0]) + } else { + None + } +} + +pub fn is_process_consent_running() -> ResultType { + let output = std::process::Command::new("cmd") + .args(&["/C", "tasklist | findstr consent.exe"]) + .creation_flags(CREATE_NO_WINDOW) + .output()?; + Ok(output.status.success() && !output.stdout.is_empty()) +} + +pub struct WakeLock(u32); +// Failed to compile keepawake-rs on i686 +impl WakeLock { + pub fn new(display: bool, idle: bool, sleep: bool) -> Self { + let mut flag = ES_CONTINUOUS; + if display { + flag |= ES_DISPLAY_REQUIRED; + } + if idle { + flag |= ES_SYSTEM_REQUIRED; + } + if sleep { + flag |= ES_AWAYMODE_REQUIRED; + } + unsafe { SetThreadExecutionState(flag) }; + WakeLock(flag) + } + + pub fn set_display(&mut self, display: bool) -> ResultType<()> { + let flag = if display { + self.0 | ES_DISPLAY_REQUIRED + } else { + self.0 & !ES_DISPLAY_REQUIRED + }; + if flag != self.0 { + unsafe { SetThreadExecutionState(flag) }; + self.0 = flag; + } + Ok(()) + } +} + +impl Drop for WakeLock { + fn drop(&mut self) { + unsafe { SetThreadExecutionState(ES_CONTINUOUS) }; + } +} + +pub fn uninstall_service(show_new_window: bool, _: bool) -> bool { + log::info!("Uninstalling service..."); + let filter = format!(" /FI \"PID ne {}\"", get_current_pid()); + Config::set_option("stop-service".into(), "Y".into()); + let cmds = format!( + " + chcp 65001 + sc stop {app_name} + sc delete {app_name} + if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" + taskkill /F /IM {broker_exe} + taskkill /F /IM {app_name}.exe{filter} + ", + app_name = crate::get_app_name(), + broker_exe = WIN_TOPMOST_INJECTED_PROCESS_EXE, + ); + if let Err(err) = run_cmds(cmds, false, "uninstall") { + Config::set_option("stop-service".into(), "".into()); + log::debug!("{err}"); + return true; + } + run_after_run_cmds(!show_new_window); + std::process::exit(0); +} + +pub fn install_service() -> bool { + log::info!("Installing service..."); + let _installing = crate::platform::InstallingService::new(); + let (_, path, _, exe) = get_install_info(); + let tmp_path = std::env::temp_dir().to_string_lossy().to_string(); + let tray_shortcut = get_tray_shortcut(&path, &exe, &exe, &tmp_path).unwrap_or_default(); + let filter = format!(" /FI \"PID ne {}\"", get_current_pid()); + Config::set_option("stop-service".into(), "".into()); + crate::ipc::EXIT_RECV_CLOSE.store(false, Ordering::Relaxed); + let cmds = format!( + " +chcp 65001 +taskkill /F /IM {app_name}.exe{filter} +cscript \"{tray_shortcut}\" +copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\" +{import_config} +{create_service} +if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\" + ", + app_name = crate::get_app_name(), + import_config = get_import_config(&exe), + create_service = get_create_service(&exe), + ); + if let Err(err) = run_cmds(cmds, false, "install") { + Config::set_option("stop-service".into(), "Y".into()); + crate::ipc::EXIT_RECV_CLOSE.store(true, Ordering::Relaxed); + log::debug!("{err}"); + return true; + } + run_after_run_cmds(false); + std::process::exit(0); +} + +/// Calculate the total size of a directory in KB +/// Does not follow symlinks to prevent directory traversal attacks. +fn get_directory_size_kb(path: &str) -> u64 { + let mut total_size = 0u64; + let mut stack = vec![PathBuf::from(path)]; + + while let Some(current_path) = stack.pop() { + let entries = match std::fs::read_dir(¤t_path) { + Ok(entries) => entries, + Err(_) => continue, + }; + + for entry in entries { + let entry = match entry { + Ok(entry) => entry, + Err(_) => continue, + }; + + let metadata = match std::fs::symlink_metadata(entry.path()) { + Ok(metadata) => metadata, + Err(_) => continue, + }; + + if metadata.is_symlink() { + continue; + } + + if metadata.is_dir() { + stack.push(entry.path()); + } else { + total_size = total_size.saturating_add(metadata.len()); + } + } + } + + total_size / 1024 +} + +pub fn update_me(debug: bool) -> ResultType<()> { + let app_name = crate::get_app_name(); + let src_exe = std::env::current_exe()?.to_string_lossy().to_string(); + let (subkey, path, _, exe) = get_install_info(); + let is_installed = std::fs::metadata(&exe).is_ok(); + if !is_installed { + bail!("{} is not installed.", &app_name); + } + + let app_exe_name = &format!("{}.exe", &app_name); + let main_window_pids = + crate::platform::get_pids_of_process_with_args::<_, &str>(&app_exe_name, &[]); + let main_window_sessions = main_window_pids + .iter() + .map(|pid| get_session_id_of_process(pid.as_u32())) + .flatten() + .collect::>(); + kill_process_by_pids(&app_exe_name, main_window_pids)?; + let tray_pids = crate::platform::get_pids_of_process_with_args(&app_exe_name, &["--tray"]); + let tray_sessions = tray_pids + .iter() + .map(|pid| get_session_id_of_process(pid.as_u32())) + .flatten() + .collect::>(); + kill_process_by_pids(&app_exe_name, tray_pids)?; + let is_service_running = is_self_service_running(); + + let mut version_major = "0"; + let mut version_minor = "0"; + let mut version_build = "0"; + let versions: Vec<&str> = crate::VERSION.split(".").collect(); + if versions.len() > 0 { + version_major = versions[0]; + } + if versions.len() > 1 { + version_minor = versions[1]; + } + if versions.len() > 2 { + version_build = versions[2]; + } + let version = crate::VERSION.replace("-", "."); + let size = get_directory_size_kb(&path); + let build_date = crate::BUILD_DATE; + // Use the icon in the previous installation directory if possible. + let display_icon = get_custom_icon("", &exe).unwrap_or(exe.to_string()); + + let is_msi = is_msi_installed().ok(); + + fn get_reg_cmd( + subkey: &str, + is_msi: Option, + display_icon: &str, + version: &str, + build_date: &str, + version_major: &str, + version_minor: &str, + version_build: &str, + size: u64, + ) -> String { + let reg_display_icon = if is_msi.unwrap_or(false) { + "".to_string() + } else { + format!( + "reg add {} /f /v DisplayIcon /t REG_SZ /d \"{}\"", + subkey, display_icon + ) + }; + format!( + " +{reg_display_icon} +reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\" +reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\" +reg add {subkey} /f /v BuildDate /t REG_SZ /d \"{build_date}\" +reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {version_major} +reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {version_minor} +reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build} +reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size} + " + ) + } + + let reg_cmd = { + let reg_cmd_main = get_reg_cmd( + &subkey, + is_msi, + &display_icon, + &version, + &build_date, + &version_major, + &version_minor, + &version_build, + size, + ); + let reg_cmd_msi = if let Some(reg_msi_key) = get_reg_msi_key(&subkey, is_msi) { + get_reg_cmd( + ®_msi_key, + is_msi, + &display_icon, + &version, + &build_date, + &version_major, + &version_minor, + &version_build, + size, + ) + } else { + "".to_owned() + }; + format!("{}{}", reg_cmd_main, reg_cmd_msi) + }; + + let filter = format!(" /FI \"PID ne {}\"", get_current_pid()); + let restore_service_cmd = if is_service_running { + format!("sc start {}", &app_name) + } else { + "".to_owned() + }; + + // No need to check the install option here, `is_rd_printer_installed` rarely fails. + let is_printer_installed = remote_printer::is_rd_printer_installed(&app_name).unwrap_or(false); + // Do nothing if the printer is not installed or failed to query if the printer is installed. + let (uninstall_printer_cmd, install_printer_cmd) = if is_printer_installed { + ( + format!("\"{}\" --uninstall-remote-printer", &src_exe), + format!("\"{}\" --install-remote-printer", &src_exe), + ) + } else { + ("".to_owned(), "".to_owned()) + }; + + // We do not try to remove all files in the old version. + // Because I don't know whether additional files will be installed here after installation, such as drivers. + // Just copy files to the installation directory works fine. + //if exist \"{path}\" rd /s /q \"{path}\" + // md \"{path}\" + // + // We need `taskkill` because: + // 1. There may be some other processes like `rustdesk --connect` are running. + // 2. Sometimes, the main window and the tray icon are showing + // while I cannot find them by `tasklist` or the methods above. + // There's should be 4 processes running: service, server, tray and main window. + // But only 2 processes are shown in the tasklist. + let cmds = format!( + " +chcp 65001 +sc stop {app_name} +taskkill /F /IM {app_name}.exe{filter} +{reg_cmd} +{copy_exe} +{rename_exe} +{remove_meta_toml} +{restore_service_cmd} +{uninstall_printer_cmd} +{install_printer_cmd} +{sleep} + ", + app_name = app_name, + copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?, + rename_exe = rename_exe_cmd(&src_exe, &path)?, + remove_meta_toml = remove_meta_toml_cmd(is_msi.unwrap_or(true), &path), + sleep = if debug { "timeout 300" } else { "" }, + ); + + let _restore_session_guard = crate::common::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + let is_root = is_root(); + if tray_sessions.is_empty() { + log::info!("No tray process found."); + } else { + log::info!( + "Try to restore the tray process..., sessions: {:?}", + &tray_sessions + ); + // When not running as root, only spawn once since run_exe_direct + // doesn't target specific sessions. + let mut spawned_non_root_tray = false; + for s in tray_sessions.clone().into_iter() { + if s != 0 { + // We need to check if is_root here because if `update_me()` is called from + // the main window running with administrator permission, + // `run_exe_in_session()` will fail with error 1314 ("A required privilege is + // not held by the client"). + // + // This issue primarily affects the MSI-installed version running in Administrator + // session during testing, but we check permissions here to be safe. + if is_root { + allow_err!(run_exe_in_session(&exe, vec!["--tray"], s, true)); + } else if !spawned_non_root_tray { + // Only spawn once for non-root since run_exe_direct doesn't take session parameter + allow_err!(run_exe_direct(&exe, vec!["--tray"], false)); + spawned_non_root_tray = true; + } + } + } + } + if main_window_sessions.is_empty() { + log::info!("No main window process found."); + } else { + log::info!("Try to restore the main window process..."); + std::thread::sleep(std::time::Duration::from_millis(2000)); + // When not running as root, only spawn once since run_exe_direct + // doesn't target specific sessions. + let mut spawned_non_root_main = false; + for s in main_window_sessions.clone().into_iter() { + if s != 0 { + if is_root { + allow_err!(run_exe_in_session(&exe, vec![], s, true)); + } else if !spawned_non_root_main { + // Only spawn once for non-root since run_exe_direct doesn't take session parameter + allow_err!(run_exe_direct(&exe, vec![], false)); + spawned_non_root_main = true; + } + } + } + } + std::thread::sleep(std::time::Duration::from_millis(300)); + }), + }; + + run_cmds(cmds, debug, "update")?; + + std::thread::sleep(std::time::Duration::from_millis(2000)); + log::info!("Update completed."); + + Ok(()) +} + +fn get_reg_msi_key(subkey: &str, is_msi: Option) -> Option { + // Only proceed if it's a custom client and MSI is installed. + // `is_msi.unwrap_or(true)` is intentional: subsequent code validates the registry, + // hence no early return is required upon MSI detection failure. + if !(crate::common::is_custom_client() && is_msi.unwrap_or(true)) { + return None; + } + + // Get the uninstall string from registry + let uninstall_string = get_reg_of(subkey, "UninstallString"); + if uninstall_string.is_empty() { + return None; + } + + // Find the product code (GUID) in the uninstall string + // Handle both quoted and unquoted GUIDs: /X {GUID} or /X "{GUID}" + let start = uninstall_string.rfind('{')?; + let end = uninstall_string.rfind('}')?; + if start >= end { + return None; + } + let product_code = &uninstall_string[start..=end]; + + // Build the MSI registry key path + let pos = subkey.rfind('\\')?; + let reg_msi_key = format!("{}{}", &subkey[..=pos], product_code); + + Some(reg_msi_key) +} + +// Double confirm the process name +fn kill_process_by_pids(name: &str, pids: Vec) -> ResultType<()> { + let name = name.to_lowercase(); + let s = System::new_all(); + // No need to check all names of `pids` first, and kill them then. + // It's rare case that they're not matched. + for pid in pids { + if let Some(process) = s.process(pid) { + if process.name().to_lowercase() != name { + bail!("Failed to kill the process, the names are mismatched."); + } + if !process.kill() { + bail!("Failed to kill the process"); + } + } else { + bail!("Failed to kill the process, the pid is not found"); + } + } + Ok(()) +} + +pub fn handle_custom_client_staging_dir_before_update( + custom_client_staging_dir: &PathBuf, +) -> ResultType<()> { + let Some(current_exe_dir) = std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())) + else { + bail!("Failed to get current exe directory"); + }; + + // Clean up existing staging directory + if custom_client_staging_dir.exists() { + log::debug!( + "Removing existing custom client staging directory: {:?}", + custom_client_staging_dir + ); + if let Err(e) = remove_custom_client_staging_dir(custom_client_staging_dir) { + bail!( + "Failed to remove existing custom client staging directory {:?}: {}", + custom_client_staging_dir, + e + ); + } + } + + let src_path = current_exe_dir.join("custom.txt"); + if src_path.exists() { + // Verify that custom.txt is not a symlink before copying + let metadata = match std::fs::symlink_metadata(&src_path) { + Ok(m) => m, + Err(e) => { + bail!( + "Failed to read metadata for custom.txt at {:?}: {}", + src_path, + e + ); + } + }; + + if metadata.is_symlink() { + allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir)); + bail!( + "custom.txt at {:?} is a symlink, refusing to stage for security reasons.", + src_path + ); + } + + if metadata.is_file() { + if !custom_client_staging_dir.exists() { + if let Err(e) = std::fs::create_dir_all(custom_client_staging_dir) { + bail!("Failed to create parent directory {:?} when staging custom client files: {}", custom_client_staging_dir, e); + } + } + let dst_path = custom_client_staging_dir.join("custom.txt"); + if let Err(e) = std::fs::copy(&src_path, &dst_path) { + allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir)); + bail!( + "Failed to copy custom txt from {:?} to {:?}: {}", + src_path, + dst_path, + e + ); + } + } else { + log::warn!( + "custom.txt at {:?} is not a regular file, skipping.", + src_path + ); + } + } else { + log::info!("No custom txt found to stage for update."); + } + + Ok(()) +} + +// Used for auto update and manual update in the main window. +pub fn update_to(file: &str) -> ResultType<()> { + if file.ends_with(".exe") { + let custom_client_staging_dir = get_custom_client_staging_dir(); + if crate::is_custom_client() { + handle_custom_client_staging_dir_before_update(&custom_client_staging_dir)?; + } else { + // Clean up any residual staging directory from previous custom client + allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir)); + } + if !run_uac(file, "--update")? { + bail!( + "Failed to run the update exe with UAC, error: {:?}", + std::io::Error::last_os_error() + ); + } + } else if file.ends_with(".msi") { + if let Err(e) = update_me_msi(file, false) { + bail!("Failed to run the update msi: {}", e); + } + } else { + // unreachable!() + bail!("Unsupported update file format: {}", file); + } + Ok(()) +} + +// Don't launch tray app when running with `\qn`. +// 1. Because `/qn` requires administrator permission and the tray app should be launched with user permission. +// Or launching the main window from the tray app will cause the main window to be launched with administrator permission. +// 2. We are not able to launch the tray app if the UI is in the login screen. +// `fn update_me()` can handle the above cases, but for msi update, we need to do more work to handle the above cases. +// 1. Record the tray app session ids. +// 2. Do the update. +// 3. Restore the tray app sessions. +// `1` and `3` must be done in custom actions. +// We need also to handle the command line parsing to find the tray processes. +pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> { + let cmds = format!( + "chcp 65001 && msiexec /i {msi} {}", + if quiet { "/qn LAUNCH_TRAY_APP=N" } else { "" } + ); + run_cmds(cmds, false, "update-msi")?; + Ok(()) +} + +pub fn get_tray_shortcut( + install_dir: &str, + exe: &str, + icon_source_exe: &str, + tmp_path: &str, +) -> ResultType { + let shortcut_icon_location = get_shortcut_icon_location(install_dir, icon_source_exe); + Ok(write_cmds( + format!( + " +Set oWS = WScript.CreateObject(\"WScript.Shell\") +sLinkFile = \"{tmp_path}\\{app_name} Tray.lnk\" + +Set oLink = oWS.CreateShortcut(sLinkFile) + oLink.TargetPath = \"{exe}\" + oLink.Arguments = \"--tray\" + {shortcut_icon_location} +oLink.Save + ", + app_name = crate::get_app_name(), + ), + "vbs", + "tray_shortcut", + )? + .to_str() + .unwrap_or("") + .to_owned()) +} + +fn get_import_config(exe: &str) -> String { + if config::is_outgoing_only() { + return "".to_string(); + } + format!(" +sc stop {app_name} +sc delete {app_name} +sc create {app_name} binpath= \"\\\"{exe}\\\" --import-config \\\"{config_path}\\\"\" start= auto DisplayName= \"{app_name} Service\" +sc start {app_name} +sc stop {app_name} +sc delete {app_name} +", + app_name = crate::get_app_name(), + config_path=Config::file().to_str().unwrap_or(""), +) +} + +fn get_create_service(exe: &str) -> String { + if config::is_outgoing_only() { + return "".to_string(); + } + let stop = Config::get_option("stop-service") == "Y"; + if stop { + format!(" +if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" +", app_name = crate::get_app_name()) + } else { + format!(" +sc create {app_name} binpath= \"\\\"{exe}\\\" --service\" start= auto DisplayName= \"{app_name} Service\" +sc start {app_name} +", + app_name = crate::get_app_name()) + } +} + +fn run_after_run_cmds(silent: bool) { + let (_, _, _, exe) = get_install_info(); + if !silent { + log::debug!("Spawn new window"); + allow_err!(std::process::Command::new("cmd") + .args(&["/c", "timeout", "/t", "2", "&", &format!("{exe}")]) + .creation_flags(winapi::um::winbase::CREATE_NO_WINDOW) + .spawn()); + } + if Config::get_option("stop-service") != "Y" { + allow_err!(std::process::Command::new(&exe).arg("--tray").spawn()); + } + std::thread::sleep(std::time::Duration::from_millis(300)); +} + +#[inline] +pub fn try_remove_temp_update_files() { + let temp_dir = std::env::temp_dir(); + let Ok(entries) = std::fs::read_dir(&temp_dir) else { + log::debug!("Failed to read temp directory: {:?}", temp_dir); + return; + }; + + let one_hour = std::time::Duration::from_secs(60 * 60); + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { + // Match files like rustdesk-*.msi or rustdesk-*.exe + if file_name.starts_with("rustdesk-") + && (file_name.ends_with(".msi") || file_name.ends_with(".exe")) + { + // Skip files modified within the last hour to avoid deleting files being downloaded + if let Ok(metadata) = std::fs::metadata(&path) { + if let Ok(modified) = metadata.modified() { + if let Ok(elapsed) = modified.elapsed() { + if elapsed < one_hour { + continue; + } + } + } + } + if let Err(e) = std::fs::remove_file(&path) { + log::debug!("Failed to remove temp update file {:?}: {}", path, e); + } else { + log::info!("Removed temp update file: {:?}", path); + } + } + } + } + } +} + +#[inline] +pub fn try_kill_broker() { + allow_err!(std::process::Command::new("cmd") + .arg("/c") + .arg(&format!( + "taskkill /F /IM {}", + WIN_TOPMOST_INJECTED_PROCESS_EXE + )) + .creation_flags(winapi::um::winbase::CREATE_NO_WINDOW) + .spawn()); +} + +pub fn message_box(text: &str) { + let mut text = text.to_owned(); + let nodialog = std::env::var("NO_DIALOG").unwrap_or_default() == "Y"; + if !text.ends_with("!") || nodialog { + use arboard::Clipboard as ClipboardContext; + match ClipboardContext::new() { + Ok(mut ctx) => { + ctx.set_text(&text).ok(); + if !nodialog { + text = format!("{}\n\nAbove text has been copied to clipboard", &text); + } + } + _ => {} + } + } + if nodialog { + if std::env::var("PRINT_OUT").unwrap_or_default() == "Y" { + println!("{text}"); + } + if let Ok(x) = std::env::var("WRITE_TO_FILE") { + if !x.is_empty() { + allow_err!(std::fs::write(x, text)); + } + } + return; + } + let text = text + .encode_utf16() + .chain(std::iter::once(0)) + .collect::>(); + let caption = "RustDesk Output" + .encode_utf16() + .chain(std::iter::once(0)) + .collect::>(); + unsafe { MessageBoxW(std::ptr::null_mut(), text.as_ptr(), caption.as_ptr(), MB_OK) }; +} + +pub fn alloc_console() { + unsafe { + alloc_console_and_redirect(); + } +} + +fn get_license() -> Option { + let mut lic: CustomServer = Default::default(); + if let Ok(tmp) = get_license_from_exe_name() { + lic = tmp; + } else { + // for back compatibility from migrating from <= 1.2.1 to 1.2.2 + lic.key = get_reg("Key"); + lic.host = get_reg("Host"); + lic.api = get_reg("Api"); + } + if lic.key.is_empty() || lic.host.is_empty() { + return None; + } + Some(lic) +} + +pub struct WallPaperRemover { + old_path: String, +} + +impl WallPaperRemover { + pub fn new() -> ResultType { + let start = std::time::Instant::now(); + if !Self::need_remove() { + bail!("already solid color"); + } + let old_path = match Self::get_recent_wallpaper() { + Ok(old_path) => old_path, + Err(e) => { + log::info!("Failed to get recent wallpaper: {:?}, use fallback", e); + wallpaper::get().map_err(|e| anyhow!(e.to_string()))? + } + }; + Self::set_wallpaper(None)?; + log::info!( + "created wallpaper remover, old_path: {:?}, elapsed: {:?}", + old_path, + start.elapsed(), + ); + Ok(Self { old_path }) + } + + pub fn support() -> bool { + wallpaper::get().is_ok() || !Self::get_recent_wallpaper().unwrap_or_default().is_empty() + } + + fn get_recent_wallpaper() -> ResultType { + // SystemParametersInfoW may return %appdata%\Microsoft\Windows\Themes\TranscodedWallpaper, not real path and may not real cache + // https://www.makeuseof.com/find-desktop-wallpapers-file-location-windows-11/ + // https://superuser.com/questions/1218413/write-to-current-users-registry-through-a-different-admin-account + let (hkcu, sid) = if is_root() { + let sid = get_current_process_session_id().ok_or(anyhow!("failed to get sid"))?; + (RegKey::predef(HKEY_USERS), format!("{}\\", sid)) + } else { + (RegKey::predef(HKEY_CURRENT_USER), "".to_string()) + }; + let explorer_key = hkcu.open_subkey_with_flags( + &format!( + "{}Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Wallpapers", + sid + ), + KEY_READ, + )?; + Ok(explorer_key.get_value("BackgroundHistoryPath0")?) + } + + fn need_remove() -> bool { + if let Ok(wallpaper) = wallpaper::get() { + return !wallpaper.is_empty(); + } + false + } + + fn set_wallpaper(path: Option) -> ResultType<()> { + wallpaper::set_from_path(&path.unwrap_or_default()).map_err(|e| anyhow!(e.to_string())) + } +} + +impl Drop for WallPaperRemover { + fn drop(&mut self) { + // If the old background is a slideshow, it will be converted into an image. AnyDesk does the same. + allow_err!(Self::set_wallpaper(Some(self.old_path.clone()))); + } +} + +fn get_uninstall_amyuni_idd() -> String { + match std::env::current_exe() { + Ok(path) => format!("\"{}\" --uninstall-amyuni-idd", path.to_str().unwrap_or("")), + Err(e) => { + log::warn!("Failed to get current exe path, cannot get command of uninstalling idd, Zzerror: {:?}", e); + "".to_string() + } + } +} + +#[inline] +pub fn is_self_service_running() -> bool { + is_service_running(&crate::get_app_name()) +} + +pub fn is_service_running(service_name: &str) -> bool { + unsafe { + let service_name = wide_string(service_name); + is_service_running_w(service_name.as_ptr() as _) + } +} + +pub fn is_x64() -> bool { + const PROCESSOR_ARCHITECTURE_AMD64: u16 = 9; + + let mut sys_info = SYSTEM_INFO::default(); + unsafe { + GetNativeSystemInfo(&mut sys_info as _); + } + unsafe { sys_info.u.s().wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64 } +} + +pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> { + // Kill rustdesk.exe without extra arg, should only be called by --server + // We can find the exact process which occupies the ipc, see more from https://github.com/winsiderss/systeminformer + let app_name = crate::get_app_name().to_lowercase(); + log::info!("try kill main window process"); + use hbb_common::sysinfo::System; + let mut sys = System::new(); + sys.refresh_processes(); + let my_uid = sys + .process((std::process::id() as usize).into()) + .map(|x| x.user_id()) + .unwrap_or_default(); + let my_pid = std::process::id(); + if app_name.is_empty() { + bail!("app name is empty"); + } + for (_, p) in sys.processes().iter() { + let p_name = p.name().to_lowercase(); + // name equal + if !(p_name == app_name || p_name == app_name.clone() + ".exe") { + continue; + } + // arg more than 1 + if p.cmd().len() < 1 { + continue; + } + // first arg contain app name + if !p.cmd()[0].to_lowercase().contains(&p_name) { + continue; + } + // only one arg or the second arg is empty uni link + let is_empty_uni = p.cmd().len() == 2 && crate::common::is_empty_uni_link(&p.cmd()[1]); + if !(p.cmd().len() == 1 || is_empty_uni) { + continue; + } + // skip self + if p.pid().as_u32() == my_pid { + continue; + } + // because we call it with --server, so we can check user_id, remove this if call it with user process + if p.user_id() == my_uid { + log::info!("user id equal, continue"); + continue; + } + log::info!("try kill process: {:?}, pid = {:?}", p.cmd(), p.pid()); + nt_terminate_process(p.pid().as_u32())?; + log::info!("kill process success: {:?}, pid = {:?}", p.cmd(), p.pid()); + return Ok(()); + } + bail!("failed to find rustdesk main window process"); +} + +fn nt_terminate_process(process_id: DWORD) -> ResultType<()> { + type NtTerminateProcess = unsafe extern "system" fn(HANDLE, DWORD) -> DWORD; + unsafe { + let h_module = if is_win_10_or_greater() { + LoadLibraryExA( + CString::new("ntdll.dll")?.as_ptr(), + std::ptr::null_mut(), + LOAD_LIBRARY_SEARCH_SYSTEM32, + ) + } else { + LoadLibraryA(CString::new("ntdll.dll")?.as_ptr()) + }; + if !h_module.is_null() { + let f_nt_terminate_process: NtTerminateProcess = std::mem::transmute(GetProcAddress( + h_module, + CString::new("NtTerminateProcess")?.as_ptr(), + )); + let h_token = OpenProcess(PROCESS_ALL_ACCESS, 0, process_id); + if !h_token.is_null() { + if f_nt_terminate_process(h_token, 1) == 0 { + log::info!("terminate process {} success", process_id); + CloseHandle(h_token); + return Ok(()); + } else { + CloseHandle(h_token); + bail!("NtTerminateProcess {} failed", process_id); + } + } else { + bail!("OpenProcess {} failed", process_id); + } + } else { + bail!("Failed to load ntdll.dll"); + } + } +} + +pub fn try_set_window_foreground(window: HWND) { + let env_key = SET_FOREGROUND_WINDOW; + if let Ok(value) = std::env::var(env_key) { + if value == "1" { + unsafe { + SetForegroundWindow(window); + } + std::env::remove_var(env_key); + } + } +} + +pub mod reg_display_settings { + use hbb_common::ResultType; + use serde_derive::{Deserialize, Serialize}; + use std::collections::HashMap; + use winreg::{enums::*, RegValue}; + const REG_GRAPHICS_DRIVERS_PATH: &str = "SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers"; + const REG_CONNECTIVITY_PATH: &str = "Connectivity"; + + #[derive(Serialize, Deserialize, Debug)] + pub struct RegRecovery { + path: String, + key: String, + old: (Vec, isize), + new: (Vec, isize), + } + + pub fn read_reg_connectivity() -> ResultType>> { + let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); + let reg_connectivity = hklm.open_subkey_with_flags( + format!("{}\\{}", REG_GRAPHICS_DRIVERS_PATH, REG_CONNECTIVITY_PATH), + KEY_READ, + )?; + + let mut map_connectivity = HashMap::new(); + for key in reg_connectivity.enum_keys() { + let key = key?; + let mut map_item = HashMap::new(); + let reg_item = reg_connectivity.open_subkey_with_flags(&key, KEY_READ)?; + for value in reg_item.enum_values() { + let (name, value) = value?; + map_item.insert(name, value); + } + map_connectivity.insert(key, map_item); + } + Ok(map_connectivity) + } + + pub fn diff_recent_connectivity( + map1: HashMap>, + map2: HashMap>, + ) -> Option { + for (subkey, map_item2) in map2 { + if let Some(map_item1) = map1.get(&subkey) { + let key = "Recent"; + if let Some(value1) = map_item1.get(key) { + if let Some(value2) = map_item2.get(key) { + if value1 != value2 { + return Some(RegRecovery { + path: format!( + "{}\\{}\\{}", + REG_GRAPHICS_DRIVERS_PATH, REG_CONNECTIVITY_PATH, subkey + ), + key: key.to_owned(), + old: (value1.bytes.clone(), value1.vtype.clone() as isize), + new: (value2.bytes.clone(), value2.vtype.clone() as isize), + }); + } + } + } + } + } + None + } + + pub fn restore_reg_connectivity(reg_recovery: RegRecovery, force: bool) -> ResultType<()> { + let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); + let reg_item = hklm.open_subkey_with_flags(®_recovery.path, KEY_READ | KEY_WRITE)?; + if !force { + let cur_reg_value = reg_item.get_raw_value(®_recovery.key)?; + let new_reg_value = RegValue { + bytes: reg_recovery.new.0, + vtype: isize_to_reg_type(reg_recovery.new.1), + }; + // Compare if the current value is the same as the new value. + // If they are not the same, the registry value has been changed by other processes. + // So we do not restore the registry value. + if cur_reg_value != new_reg_value { + return Ok(()); + } + } + let reg_value = RegValue { + bytes: reg_recovery.old.0, + vtype: isize_to_reg_type(reg_recovery.old.1), + }; + reg_item.set_raw_value(®_recovery.key, ®_value)?; + Ok(()) + } + + #[inline] + fn isize_to_reg_type(i: isize) -> RegType { + match i { + 0 => RegType::REG_NONE, + 1 => RegType::REG_SZ, + 2 => RegType::REG_EXPAND_SZ, + 3 => RegType::REG_BINARY, + 4 => RegType::REG_DWORD, + 5 => RegType::REG_DWORD_BIG_ENDIAN, + 6 => RegType::REG_LINK, + 7 => RegType::REG_MULTI_SZ, + 8 => RegType::REG_RESOURCE_LIST, + 9 => RegType::REG_FULL_RESOURCE_DESCRIPTOR, + 10 => RegType::REG_RESOURCE_REQUIREMENTS_LIST, + 11 => RegType::REG_QWORD, + _ => RegType::REG_NONE, + } + } +} + +pub fn get_printer_names() -> ResultType> { + let mut needed_bytes = 0; + let mut returned_count = 0; + + unsafe { + // First call to get required buffer size + EnumPrintersW( + PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + std::ptr::null_mut(), + 1, + std::ptr::null_mut(), + 0, + &mut needed_bytes, + &mut returned_count, + ); + + let mut buffer = vec![0u8; needed_bytes as usize]; + + if EnumPrintersW( + PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + std::ptr::null_mut(), + 1, + buffer.as_mut_ptr() as *mut _, + needed_bytes, + &mut needed_bytes, + &mut returned_count, + ) == 0 + { + return Err(anyhow!("Failed to enumerate printers")); + } + + let ptr = buffer.as_ptr() as *const PRINTER_INFO_1W; + let printers = std::slice::from_raw_parts(ptr, returned_count as usize); + + Ok(printers + .iter() + .filter_map(|p| { + let name = p.pName; + if !name.is_null() { + let mut len = 0; + while len < 500 { + if name.add(len).is_null() || *name.add(len) == 0 { + break; + } + len += 1; + } + if len > 0 && len < 500 { + Some(String::from_utf16_lossy(std::slice::from_raw_parts( + name, len, + ))) + } else { + None + } + } else { + None + } + }) + .collect()) + } +} + +extern "C" { + fn PrintXPSRawData(printer_name: *const u16, raw_data: *const u8, data_size: c_ulong) -> DWORD; +} + +pub fn send_raw_data_to_printer(printer_name: Option, data: Vec) -> ResultType<()> { + let mut printer_name = printer_name.unwrap_or_default(); + if printer_name.is_empty() { + // use GetDefaultPrinter to get the default printer name + let mut needed_bytes = 0; + unsafe { + GetDefaultPrinterW(std::ptr::null_mut(), &mut needed_bytes); + } + if needed_bytes > 0 { + let mut default_printer_name = vec![0u16; needed_bytes as usize]; + unsafe { + GetDefaultPrinterW( + default_printer_name.as_mut_ptr() as *mut _, + &mut needed_bytes, + ); + } + printer_name = String::from_utf16_lossy(&default_printer_name[..needed_bytes as usize]); + } + } else { + if let Ok(names) = crate::platform::windows::get_printer_names() { + if !names.contains(&printer_name) { + // Don't set the first printer as current printer. + // It may not be the desired printer. + bail!("Printer name \"{}\" not found", &printer_name); + } + } + } + if printer_name.is_empty() { + return Err(anyhow!("Failed to get printer name")); + } + + log::info!("Sending data to printer: {}", &printer_name); + let printer_name = wide_string(&printer_name); + unsafe { + let res = PrintXPSRawData( + printer_name.as_ptr(), + data.as_ptr() as *const u8, + data.len() as c_ulong, + ); + if res != 0 { + bail!("Failed to send data to the printer, see logs in C:\\Windows\\temp\\test_rustdesk.log for more details."); + } else { + log::info!("Successfully sent data to the printer"); + } + } + + Ok(()) +} + +fn get_pids>(name: S) -> ResultType> { + let name = name.as_ref().to_lowercase(); + let mut pids = Vec::new(); + + unsafe { + let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)?; + if snapshot == WinHANDLE::default() { + return Ok(pids); + } + + let mut entry: PROCESSENTRY32W = std::mem::zeroed(); + entry.dwSize = std::mem::size_of::() as u32; + + if Process32FirstW(snapshot, &mut entry).is_ok() { + loop { + let proc_name = OsString::from_wide(&entry.szExeFile) + .to_string_lossy() + .to_lowercase(); + + if proc_name.contains(&name) { + pids.push(entry.th32ProcessID); + } + + if !Process32NextW(snapshot, &mut entry).is_ok() { + break; + } + } + } + + let _ = WinCloseHandle(snapshot); + } + + Ok(pids) +} + +pub fn is_msi_installed() -> std::io::Result { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let uninstall_key = hklm.open_subkey(format!( + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}", + crate::get_app_name() + ))?; + Ok(1 == uninstall_key.get_value::("WindowsInstaller")?) +} + +pub fn is_cur_exe_the_installed() -> bool { + let (_, _, _, exe) = get_install_info(); + // Check if is installed, because `exe` is the default path if is not installed. + if !std::fs::metadata(&exe).is_ok() { + return false; + } + let mut path = std::env::current_exe().unwrap_or_default(); + if let Ok(linked) = path.read_link() { + path = linked; + } + let path = path.to_string_lossy().to_lowercase(); + path == exe.to_lowercase() +} + +#[cfg(not(target_pointer_width = "64"))] +pub fn get_pids_with_first_arg_check_session, S2: AsRef>( + name: S1, + arg: S2, + same_session_id: bool, +) -> ResultType> { + // Though `wmic` can return the sessionId, for simplicity we only return processid. + let pids = get_pids_with_first_arg_by_wmic(name, arg); + if !same_session_id { + return Ok(pids); + } + let Some(cur_sid) = get_current_process_session_id() else { + bail!("Can't get current process session id"); + }; + let mut same_session_pids = vec![]; + for pid in pids.into_iter() { + let mut sid = 0; + if unsafe { ProcessIdToSessionId(pid.as_u32(), &mut sid) == TRUE } { + if sid == cur_sid { + same_session_pids.push(pid); + } + } else { + // Only log here, because this call almost never fails. + log::warn!( + "Failed to get session id of the process id, error: {:?}", + std::io::Error::last_os_error() + ); + } + } + Ok(same_session_pids) +} + +#[cfg(not(target_pointer_width = "64"))] +fn get_pids_with_args_from_wmic_output>( + output: std::borrow::Cow<'_, str>, + name: &str, + args: &[S2], +) -> Vec { + // CommandLine= + // ProcessId=33796 + // + // CommandLine= + // ProcessId=34668 + // + // CommandLine="C:\Program Files\RustDesk\RustDesk.exe" --tray + // ProcessId=13728 + // + // CommandLine="C:\Program Files\RustDesk\RustDesk.exe" + // ProcessId=10136 + let mut pids = Vec::new(); + let mut proc_found = false; + for line in output.lines() { + if line.starts_with("ProcessId=") { + if proc_found { + if let Ok(pid) = line["ProcessId=".len()..].trim().parse::() { + pids.push(hbb_common::sysinfo::Pid::from_u32(pid)); + } + proc_found = false; + } + } else if line.starts_with("CommandLine=") { + proc_found = false; + let cmd = line["CommandLine=".len()..].trim().to_lowercase(); + if args.is_empty() { + if cmd.ends_with(&name) || cmd.ends_with(&format!("{}\"", &name)) { + proc_found = true; + } + } else { + proc_found = args.iter().all(|arg| cmd.contains(arg.as_ref())); + } + } + } + pids +} + +// Note the args are not compared strictly, only check if the args are contained in the command line. +// If we want to check the args strictly, we need to parse the command line and compare each arg. +// Maybe we have to introduce some external crate like `shell_words` to do this. +#[cfg(not(target_pointer_width = "64"))] +pub(super) fn get_pids_with_args_by_wmic, S2: AsRef>( + name: S1, + args: &[S2], +) -> Vec { + let name = name.as_ref().to_lowercase(); + std::process::Command::new("wmic.exe") + .args([ + "process", + "where", + &format!("name='{}'", name), + "get", + "commandline,processid", + "/value", + ]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .map(|output| { + get_pids_with_args_from_wmic_output::( + String::from_utf8_lossy(&output.stdout), + &name, + args, + ) + }) + .unwrap_or_default() +} + +#[cfg(not(target_pointer_width = "64"))] +fn get_pids_with_first_arg_from_wmic_output( + output: std::borrow::Cow<'_, str>, + name: &str, + arg: &str, +) -> Vec { + let mut pids = Vec::new(); + let mut proc_found = false; + for line in output.lines() { + if line.starts_with("ProcessId=") { + if proc_found { + if let Ok(pid) = line["ProcessId=".len()..].trim().parse::() { + pids.push(hbb_common::sysinfo::Pid::from_u32(pid)); + } + proc_found = false; + } + } else if line.starts_with("CommandLine=") { + proc_found = false; + let cmd = line["CommandLine=".len()..].trim().to_lowercase(); + if cmd.is_empty() { + continue; + } + if !arg.is_empty() && cmd.starts_with(arg) { + proc_found = true; + } else { + for x in [&format!("{}\"", name), &format!("{}", name)] { + if cmd.contains(x) { + let cmd = cmd.split(x).collect::>()[1..].join(""); + if arg.is_empty() { + if cmd.trim().is_empty() { + proc_found = true; + } + } else if cmd.trim().starts_with(arg) { + proc_found = true; + } + break; + } + } + } + } + } + pids +} + +// Note the args are not compared strictly, only check if the args are contained in the command line. +// If we want to check the args strictly, we need to parse the command line and compare each arg. +// Maybe we have to introduce some external crate like `shell_words` to do this. +#[cfg(not(target_pointer_width = "64"))] +pub(super) fn get_pids_with_first_arg_by_wmic, S2: AsRef>( + name: S1, + arg: S2, +) -> Vec { + let name = name.as_ref().to_lowercase(); + let arg = arg.as_ref().to_lowercase(); + std::process::Command::new("wmic.exe") + .args([ + "process", + "where", + &format!("name='{}'", name), + "get", + "commandline,processid", + "/value", + ]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .map(|output| { + get_pids_with_first_arg_from_wmic_output( + String::from_utf8_lossy(&output.stdout), + &name, + &arg, + ) + }) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_uninstall_cert() { + println!("uninstall driver certs: {:?}", cert::uninstall_cert()); + } + + #[test] + fn test_get_unicode_char_by_vk() { + let chr = get_char_from_vk(0x41); // VK_A + assert_eq!(chr, Some('a')); + let chr = get_char_from_vk(VK_ESCAPE as u32); // VK_ESC + assert_eq!(chr, None) + } + + #[cfg(not(target_pointer_width = "64"))] + #[test] + fn test_get_pids_with_args_from_wmic_output() { + let output = r#" +CommandLine= +ProcessId=33796 + +CommandLine= +ProcessId=34668 + +CommandLine="C:\Program Files\testapp\TestApp.exe" --tray +ProcessId=13728 + +CommandLine="C:\Program Files\testapp\TestApp.exe" +ProcessId=10136 +"#; + let name = "testapp.exe"; + let args = vec!["--tray"]; + let pids = super::get_pids_with_args_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + &args, + ); + assert_eq!(pids.len(), 1); + assert_eq!(pids[0].as_u32(), 13728); + + let args: Vec<&str> = vec![]; + let pids = super::get_pids_with_args_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + &args, + ); + assert_eq!(pids.len(), 1); + assert_eq!(pids[0].as_u32(), 10136); + + let args = vec!["--other"]; + let pids = super::get_pids_with_args_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + &args, + ); + assert_eq!(pids.len(), 0); + } + + #[cfg(not(target_pointer_width = "64"))] + #[test] + fn test_get_pids_with_first_arg_from_wmic_output() { + let output = r#" +CommandLine= +ProcessId=33796 + +CommandLine= +ProcessId=34668 + +CommandLine="C:\Program Files\testapp\TestApp.exe" --tray +ProcessId=13728 + +CommandLine="C:\Program Files\testapp\TestApp.exe" +ProcessId=10136 + "#; + let name = "testapp.exe"; + let arg = "--tray"; + let pids = super::get_pids_with_first_arg_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + arg, + ); + assert_eq!(pids.len(), 1); + assert_eq!(pids[0].as_u32(), 13728); + + let arg = ""; + let pids = super::get_pids_with_first_arg_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + arg, + ); + assert_eq!(pids.len(), 1); + assert_eq!(pids[0].as_u32(), 10136); + + let arg = "--other"; + let pids = super::get_pids_with_first_arg_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + arg, + ); + assert_eq!(pids.len(), 0); + } +} diff --git a/vendor/rustdesk/src/platform/windows_delete_test_cert.cc b/vendor/rustdesk/src/platform/windows_delete_test_cert.cc new file mode 100644 index 0000000..94949a8 --- /dev/null +++ b/vendor/rustdesk/src/platform/windows_delete_test_cert.cc @@ -0,0 +1,406 @@ +// https://github.com/rustdesk/rustdesk/discussions/6444#discussioncomment-9010062 + +#include +#include +#include + +BOOL IsCertWdkTestCert(char* lpBlobData, DWORD cchBlobData) { + DWORD cchIdxBlobData = 0; + DWORD cchIdxTestCertBlob = 0; + DWORD cchSizeTestCertBlob = 0; +#pragma warning(push) +#pragma warning(disable: 4838) +#pragma warning(disable: 4309) + const char TestCertBlob[] = { + 0X30, 0X82, 0X03, 0X0C, 0X30, 0X82, 0X01, 0XF4, 0XA0, 0X03, 0X02, 0X01, 0X02, 0X02, 0X10, 0X17, + 0X93, 0X62, 0X03, 0XFA, 0XCD, 0X37, 0X83, 0X49, 0XE3, 0X33, 0X82, 0XC3, 0X14, 0XEC, 0X83, 0X30, + 0X0D, 0X06, 0X09, 0X2A, 0X86, 0X48, 0X86, 0XF7, 0X0D, 0X01, 0X01, 0X05, 0X05, 0X00, 0X30, 0X2F, + 0X31, 0X2D, 0X30, 0X2B, 0X06, 0X03, 0X55, 0X04, 0X03, 0X13, 0X24, 0X57, 0X44, 0X4B, 0X54, 0X65, + 0X73, 0X74, 0X43, 0X65, 0X72, 0X74, 0X20, 0X61, 0X64, 0X6D, 0X69, 0X6E, 0X2C, 0X31, 0X33, 0X33, + 0X32, 0X32, 0X35, 0X34, 0X33, 0X35, 0X37, 0X30, 0X32, 0X31, 0X31, 0X33, 0X35, 0X36, 0X37, 0X30, + 0X1E, 0X17, 0X0D, 0X32, 0X33, 0X30, 0X33, 0X30, 0X36, 0X30, 0X32, 0X33, 0X32, 0X35, 0X31, 0X5A, + 0X17, 0X0D, 0X33, 0X33, 0X30, 0X33, 0X30, 0X36, 0X30, 0X30, 0X30, 0X30, 0X30, 0X30, 0X5A, 0X30, + 0X2F, 0X31, 0X2D, 0X30, 0X2B, 0X06, 0X03, 0X55, 0X04, 0X03, 0X13, 0X24, 0X57, 0X44, 0X4B, 0X54, + 0X65, 0X73, 0X74, 0X43, 0X65, 0X72, 0X74, 0X20, 0X61, 0X64, 0X6D, 0X69, 0X6E, 0X2C, 0X31, 0X33, + 0X33, 0X32, 0X32, 0X35, 0X34, 0X33, 0X35, 0X37, 0X30, 0X32, 0X31, 0X31, 0X33, 0X35, 0X36, 0X37, + 0X30, 0X82, 0X01, 0X22, 0X30, 0X0D, 0X06, 0X09, 0X2A, 0X86, 0X48, 0X86, 0XF7, 0X0D, 0X01, 0X01, + 0X01, 0X05, 0X00, 0X03, 0X82, 0X01, 0X0F, 0X00, 0X30, 0X82, 0X01, 0X0A, 0X02, 0X82, 0X01, 0X01, + 0X00, 0XB8, 0X65, 0X75, 0XAC, 0XD1, 0X82, 0XFC, 0X3A, 0X08, 0XE4, 0X1D, 0XD9, 0X4D, 0X5A, 0XCD, + 0X88, 0X2B, 0XDC, 0X00, 0XFD, 0X6B, 0X43, 0X13, 0XED, 0XE2, 0XCB, 0XD1, 0X26, 0X11, 0X22, 0XBF, + 0X20, 0X31, 0X09, 0X9D, 0X06, 0X47, 0XF5, 0XAA, 0XCE, 0X7B, 0X13, 0X98, 0XE0, 0X76, 0X40, 0XDD, + 0X2C, 0XCA, 0X98, 0XD1, 0XBB, 0X7F, 0XE2, 0X25, 0XAF, 0X48, 0X3A, 0X4E, 0X9E, 0X24, 0X38, 0X4D, + 0X04, 0XF0, 0X68, 0XAD, 0X7C, 0X6F, 0XA6, 0XBB, 0XE4, 0X9B, 0XE3, 0X7C, 0X8E, 0X2E, 0X54, 0X7D, + 0X5E, 0X74, 0XE3, 0XA6, 0X3D, 0XD9, 0X04, 0X22, 0X0A, 0X3E, 0XC7, 0X5C, 0XAB, 0X1F, 0X4D, 0X10, + 0X06, 0X2A, 0X95, 0X1A, 0X1B, 0X03, 0X20, 0X75, 0X3E, 0X49, 0X36, 0X40, 0X06, 0X63, 0XDB, 0X54, + 0X74, 0X53, 0X3C, 0X2D, 0X47, 0XE0, 0X82, 0XDD, 0X14, 0X92, 0XCC, 0XF1, 0X1A, 0X5A, 0X7F, 0X5B, + 0X4F, 0X2E, 0X94, 0X1E, 0XCE, 0X5A, 0X73, 0XD4, 0X70, 0X47, 0XF3, 0X3E, 0X85, 0X5C, 0X62, 0XF5, + 0X79, 0X0F, 0X4B, 0XB9, 0X69, 0X51, 0X33, 0X05, 0XF1, 0XDF, 0XE5, 0X4E, 0X6E, 0X28, 0XC6, 0X88, + 0X89, 0X9A, 0XEF, 0X07, 0X62, 0X23, 0X53, 0X6A, 0X16, 0X2B, 0X3A, 0XF7, 0X10, 0X1B, 0X42, 0XCE, + 0XEE, 0X33, 0XB9, 0X01, 0X30, 0X8A, 0XAB, 0X14, 0X73, 0XC5, 0XC3, 0X94, 0X2D, 0XEB, 0X00, 0XAE, + 0X73, 0X7B, 0X78, 0X65, 0X8B, 0X8F, 0X44, 0XBD, 0XF8, 0XBC, 0XE8, 0XB3, 0X6A, 0X4E, 0XE3, 0X4F, + 0X92, 0XE3, 0X72, 0XD9, 0X6D, 0XD1, 0X88, 0X5E, 0X1C, 0XFF, 0X8D, 0XF1, 0X76, 0XBC, 0X37, 0X4B, + 0X11, 0X48, 0XB5, 0X8D, 0X1D, 0X1C, 0XEC, 0X82, 0X11, 0X50, 0XC6, 0XFF, 0X3A, 0X7E, 0X3A, 0X8C, + 0X18, 0XF7, 0XA6, 0XEB, 0XAA, 0X26, 0X8E, 0XC6, 0X01, 0X7B, 0X50, 0X6A, 0XFA, 0X33, 0X3C, 0XBE, + 0X29, 0X02, 0X03, 0X01, 0X00, 0X01, 0XA3, 0X24, 0X30, 0X22, 0X30, 0X0B, 0X06, 0X03, 0X55, 0X1D, + 0X0F, 0X04, 0X04, 0X03, 0X02, 0X04, 0X30, 0X30, 0X13, 0X06, 0X03, 0X55, 0X1D, 0X25, 0X04, 0X0C, + 0X30, 0X0A, 0X06, 0X08, 0X2B, 0X06, 0X01, 0X05, 0X05, 0X07, 0X03, 0X03, 0X30, 0X0D, 0X06, 0X09, + 0X2A, 0X86, 0X48, 0X86, 0XF7, 0X0D, 0X01, 0X01, 0X05, 0X05, 0X00, 0X03, 0X82, 0X01, 0X01, 0X00, + 0X00, 0X44, 0X78, 0XE3, 0XDB, 0X0C, 0X33, 0X2B, 0X57, 0X52, 0X91, 0XD0, 0X09, 0X80, 0X12, 0XB0, + 0X11, 0X7C, 0X32, 0XCF, 0X24, 0XA0, 0XA5, 0X47, 0X18, 0XDE, 0XAB, 0X9E, 0X0D, 0X4A, 0X50, 0X6B, + 0X7B, 0XD3, 0X23, 0X71, 0X32, 0XEE, 0X28, 0X1D, 0XE8, 0X2C, 0X0A, 0XDF, 0X89, 0X87, 0X9D, 0X7E, + 0XE3, 0X59, 0X05, 0XDD, 0XC2, 0X3C, 0X48, 0XC1, 0XD5, 0X88, 0X2D, 0X60, 0X29, 0XDE, 0XA1, 0X69, + 0XD8, 0X4E, 0X01, 0XF6, 0XBD, 0XCB, 0X41, 0XDF, 0XDF, 0X5B, 0X3D, 0X3D, 0X59, 0X93, 0X70, 0XD6, + 0XAC, 0X03, 0X84, 0X5E, 0X2B, 0XB6, 0X62, 0X10, 0X5B, 0XB2, 0X68, 0X97, 0XC7, 0XF9, 0X44, 0X68, + 0XBC, 0XC3, 0X26, 0XD7, 0XB5, 0X13, 0XBE, 0X0E, 0XE6, 0X7E, 0X74, 0XF0, 0XB9, 0X59, 0X63, 0XE8, + 0X6E, 0XE2, 0X96, 0X3C, 0XFE, 0X55, 0XB9, 0XAC, 0X1A, 0XB8, 0XC5, 0X98, 0XA9, 0XD3, 0XF5, 0X30, + 0XCB, 0X9E, 0X43, 0X89, 0X19, 0X9A, 0X5C, 0XB5, 0XFB, 0X76, 0XD5, 0X3B, 0XD4, 0X79, 0X02, 0X98, + 0XA0, 0XC7, 0X60, 0X96, 0X84, 0X66, 0X79, 0X25, 0XC9, 0XC2, 0X77, 0X54, 0X63, 0XA1, 0X0E, 0X27, + 0X7B, 0X2E, 0X37, 0XBE, 0X18, 0X99, 0XF6, 0X34, 0XE7, 0XCC, 0XE8, 0XE7, 0XEB, 0XE4, 0XB7, 0X37, + 0X05, 0X35, 0X77, 0XAD, 0X76, 0XAD, 0X35, 0X84, 0X62, 0XF7, 0X7F, 0X87, 0XAB, 0X29, 0X25, 0X10, + 0X73, 0XBF, 0X2C, 0X78, 0X93, 0XFF, 0XBF, 0X24, 0XD7, 0X49, 0X74, 0XC5, 0X07, 0X41, 0X17, 0XBA, + 0X87, 0XBB, 0X4E, 0XB3, 0X8F, 0XF3, 0X75, 0X77, 0X2B, 0X44, 0X7B, 0X0D, 0X18, 0X24, 0X8A, 0XCB, + 0XCC, 0X67, 0XB4, 0X00, 0XC6, 0X2A, 0XAC, 0XCD, 0X4C, 0X16, 0XF8, 0XB8, 0X61, 0X8D, 0XAF, 0X7B, + 0XF2, 0X45, 0XE2, 0X63, 0X02, 0X4C, 0XA8, 0XB9, 0XBD, 0XB2, 0X5E, 0XF2, 0X94, 0X8F, 0X30, 0X16 + }; +#pragma warning(pop) + + cchSizeTestCertBlob = sizeof(TestCertBlob) / sizeof(TestCertBlob[0]); + if (cchBlobData < cchSizeTestCertBlob) return FALSE; + cchIdxBlobData = cchBlobData - cchSizeTestCertBlob; + while (cchIdxTestCertBlob < cchSizeTestCertBlob) { + if (lpBlobData[cchIdxBlobData] != TestCertBlob[cchIdxTestCertBlob]) { + return FALSE; + } + ++cchIdxTestCertBlob; + ++cchIdxBlobData; + } + return TRUE; +} + +//************************************************************* +// +// RegDelTestCertW() +// +// Purpose: Compares and deletes a test cert. +// +// Parameters: hKeyRoot - Root key +// lpSubKey - SubKey to delete +// +// Return: TRUE if successful. +// FALSE if an error occurs. +// +//************************************************************* + +BOOL RegDelTestCertW(HKEY hKeyRoot, LPCWSTR lpSubKey) +{ + LONG lResult; + HKEY hKey; + DWORD dValueType; + DWORD cchBufferSize = 0; + BOOL bRes = FALSE; + + lResult = RegOpenKeyExW(hKeyRoot, lpSubKey, 0, KEY_READ, &hKey); + if (lResult != ERROR_SUCCESS) { + if (lResult == ERROR_FILE_NOT_FOUND) { + return TRUE; + } + else { + //printf("Error opening key.\n"); + return FALSE; + } + } + + do { + lResult = RegQueryValueExW(hKey, L"Blob", NULL, &dValueType, NULL, &cchBufferSize); + if (lResult == ERROR_SUCCESS) { + if (dValueType == REG_BINARY) { + LPSTR szBuffer = NULL; + LONG readResult = 0; + szBuffer = (LPSTR)malloc(cchBufferSize * sizeof(char)); + if (szBuffer == NULL) { + bRes = FALSE; + break; + } + + lResult = RegQueryValueExW(hKey, L"Blob", NULL, &dValueType, (LPBYTE)szBuffer, &cchBufferSize); + if (readResult == ERROR_SUCCESS) { + if (IsCertWdkTestCert(szBuffer, cchBufferSize)) { + free(szBuffer); + lResult = RegDeleteKeyW(hKeyRoot, lpSubKey); + if (lResult == ERROR_SUCCESS) { + bRes = TRUE; + } + else { + bRes = FALSE; + } + + break; + } + } + + free(szBuffer); + } + } + } while (FALSE); + RegCloseKey(hKey); + return bRes; +} + +//************************************************************* +// +// RegDelnodeRecurseW() +// +// Purpose: Deletes a registry key and all its subkeys / values. +// +// Parameters: hKeyRoot - Root key +// lpSubKey - SubKey to delete +// bOneLevel - Delete lpSubKey and its first level subdirectory +// +// Return: TRUE if successful. +// FALSE if an error occurs. +// +// Note: If bOneLevel is TRUE, only current key and its first level subkeys are deleted. +// The first level subkeys are deleted only if they do not have subkeys. +// +// If some subkeys have subkeys, but the previous empty subkeys are deleted. +// It's ok for the certificates, because the empty subkeys are not used +// and they can be created automatically. +// +//************************************************************* + +BOOL RegDelnodeRecurseW(HKEY hKeyRoot, LPWSTR lpSubKey, BOOL bOneLevel) +{ + LPWSTR lpEnd; + LONG lResult; + DWORD dwSize; + WCHAR szName[MAX_PATH]; + HKEY hKey; + FILETIME ftWrite; + + // First, see if we can delete the key without having + // to recurse. + + lResult = RegDeleteKeyW(hKeyRoot, lpSubKey); + + if (lResult == ERROR_SUCCESS) + return TRUE; + + lResult = RegOpenKeyExW(hKeyRoot, lpSubKey, 0, KEY_READ, &hKey); + + if (lResult != ERROR_SUCCESS) + { + if (lResult == ERROR_FILE_NOT_FOUND) { + //printf("Key not found.\n"); + return TRUE; + } + else { + //printf("Error opening key.\n"); + return FALSE; + } + } + + // Check for an ending slash and add one if it is missing. + + lpEnd = lpSubKey + lstrlenW(lpSubKey); + + if (*(lpEnd - 1) != L'\\') + { + *lpEnd = L'\\'; + lpEnd++; + *lpEnd = L'\0'; + } + + // Enumerate the keys + + dwSize = MAX_PATH; + lResult = RegEnumKeyExW(hKey, 0, szName, &dwSize, NULL, + NULL, NULL, &ftWrite); + + if (lResult == ERROR_SUCCESS) + { + do { + + *lpEnd = L'\0'; + StringCchCatW(lpSubKey, MAX_PATH * 2, szName); + + if (bOneLevel) { + lResult = RegDeleteKeyW(hKeyRoot, lpSubKey); + if (lResult != ERROR_SUCCESS) { + return FALSE; + } + } + else { + if (!RegDelnodeRecurseW(hKeyRoot, lpSubKey, bOneLevel)) { + break; + } + } + + dwSize = MAX_PATH; + + lResult = RegEnumKeyExW(hKey, 0, szName, &dwSize, NULL, + NULL, NULL, &ftWrite); + + } while (lResult == ERROR_SUCCESS); + } + + lpEnd--; + *lpEnd = L'\0'; + + RegCloseKey(hKey); + + // Try again to delete the key. + + lResult = RegDeleteKeyW(hKeyRoot, lpSubKey); + + if (lResult == ERROR_SUCCESS) + return TRUE; + + return FALSE; +} + +//************************************************************* +// +// RegDelnodeW() +// +// Purpose: Deletes a registry key and all its subkeys / values. +// +// Parameters: hKeyRoot - Root key +// lpSubKey - SubKey to delete +// bOneLevel - Delete lpSubKey and its first level subdirectory +// +// Return: TRUE if successful. +// FALSE if an error occurs. +// +//************************************************************* + +BOOL RegDelnodeW(HKEY hKeyRoot, LPCWSTR lpSubKey, BOOL bOneLevel) +{ + //return FALSE; // For Testing + + WCHAR szDelKey[MAX_PATH * 2]; + + StringCchCopyW(szDelKey, MAX_PATH * 2, lpSubKey); + return RegDelnodeRecurseW(hKeyRoot, szDelKey, bOneLevel); +} + +//************************************************************* +// +// DeleteRustDeskTestCertsW_SingleHive() +// +// Purpose: Deletes RustDesk Test certificates and wrong key stores +// +// Parameters: RootKey - Root key +// Prefix - SID if RootKey=HKEY_USERS +// +// Return: TRUE if successful. +// FALSE if an error occurs. +// +//************************************************************* + +BOOL DeleteRustDeskTestCertsW_SingleHive(HKEY RootKey, LPWSTR Prefix = NULL) { + // WDKTestCert to be removed from all stores + LPCWSTR lpCertFingerPrint = L"D1DBB672D5A500B9809689CAEA1CE49E799767F0"; + + // Wrong key stores to be removed completely + LPCSTR RootName = "ROOT"; + LPWSTR SubKeyPrefix = (LPWSTR)RootName; // sic! Convert of ANSI to UTF-16 + + LPWSTR lpSystemCertificatesPath = (LPWSTR)malloc(512 * sizeof(WCHAR)); + if (lpSystemCertificatesPath == 0) return FALSE; + if (Prefix == NULL) { + wsprintfW(lpSystemCertificatesPath, L"Software\\Microsoft\\SystemCertificates"); + } + else { + wsprintfW(lpSystemCertificatesPath, L"%s\\Software\\Microsoft\\SystemCertificates", Prefix); + } + + HKEY hRegSystemCertificates; + LONG res = RegOpenKeyExW(RootKey, lpSystemCertificatesPath, NULL, KEY_ALL_ACCESS, &hRegSystemCertificates); + if (res != ERROR_SUCCESS) + return FALSE; + + for (DWORD Index = 0; ; Index++) { + LPWSTR SubKeyName = (LPWSTR)malloc(255 * sizeof(WCHAR)); + if (SubKeyName == 0) break; + DWORD cName = 255; + LONG res = RegEnumKeyExW(hRegSystemCertificates, Index, SubKeyName, &cName, NULL, NULL, NULL, NULL); + if ((res != ERROR_SUCCESS) || (SubKeyName == NULL)) + break; + + // Remove test certificate + LPWSTR Complete = (LPWSTR)malloc(512 * sizeof(WCHAR)); + if (Complete == 0) break; + wsprintfW(Complete, L"%s\\%s\\Certificates\\%s", lpSystemCertificatesPath, SubKeyName, lpCertFingerPrint); + // std::wcout << "Try delete from: " << SubKeyName << std::endl; + RegDelTestCertW(RootKey, Complete); + free(Complete); + + // "佒呏..." key begins with "ROOT" encoded as UTF-16 + if ((SubKeyName[0] == SubKeyPrefix[0]) && (SubKeyName[1] == SubKeyPrefix[1])) { + // Remove wrong empty key store + { + LPWSTR Complete = (LPWSTR)malloc(512 * sizeof(WCHAR)); + if (Complete == 0) break; + wsprintfW(Complete, L"%s\\%s", lpSystemCertificatesPath, SubKeyName); + if (RegDelnodeW(RootKey, Complete, TRUE)) { + //std::wcout << "Rogue Key Deleted! \"" << Complete << "\"" << std::endl; // TODO: Why does this break the console? + std::cout << "Rogue key is deleted!" << std::endl; + Index--; // Because index has moved due to the deletion + } + else { + std::cout << "Rogue key deletion failed!" << std::endl; + } + free(Complete); + } + } + + free(SubKeyName); + } + RegCloseKey(hRegSystemCertificates); + return TRUE; +} + +//************************************************************* +// +// DeleteRustDeskTestCertsW() +// +// Purpose: Deletes RustDesk Test certificates and wrong key stores +// +// Parameters: None +// +// Return: None +// +//************************************************************* + +extern "C" void DeleteRustDeskTestCertsW() { + // Current user + std::wcout << "*** Current User" << std::endl; + DeleteRustDeskTestCertsW_SingleHive(HKEY_CURRENT_USER); + + // Local machine (requires admin rights) + std::wcout << "*** Local Machine" << std::endl; + DeleteRustDeskTestCertsW_SingleHive(HKEY_LOCAL_MACHINE); + + // Iterate through all users (requires admin rights) + LPCWSTR lpRoot = L""; + HKEY hRegUsers; + LONG res = RegOpenKeyExW(HKEY_USERS, lpRoot, NULL, KEY_READ, &hRegUsers); + if (res != ERROR_SUCCESS) return; + for (DWORD Index = 0; ; Index++) { + LPWSTR SubKeyName = (LPWSTR)malloc(255 * sizeof(WCHAR)); + if (SubKeyName == 0) break; + DWORD cName = 255; + LONG res = RegEnumKeyExW(hRegUsers, Index, SubKeyName, &cName, NULL, NULL, NULL, NULL); + if ((res != ERROR_SUCCESS) || (SubKeyName == NULL)) + break; + std::wcout << "*** User: " << SubKeyName << std::endl; + DeleteRustDeskTestCertsW_SingleHive(HKEY_USERS, SubKeyName); + } + RegCloseKey(hRegUsers); +} + +// int main() +// { +// DeleteRustDeskTestCertsW(); +// return 0; +// } diff --git a/vendor/rustdesk/src/plugin/callback_ext.rs b/vendor/rustdesk/src/plugin/callback_ext.rs new file mode 100644 index 0000000..715f47f --- /dev/null +++ b/vendor/rustdesk/src/plugin/callback_ext.rs @@ -0,0 +1,44 @@ +// External support for callback. +// 1. Support block input for some plugins. +// ----------------------------------------------------------------------------- + +use super::*; + +const EXT_SUPPORT_BLOCK_INPUT: &str = "block-input"; + +pub(super) fn ext_support_callback( + id: &str, + peer: &str, + msg: &super::callback_msg::MsgToExtSupport, +) -> PluginReturn { + match &msg.r#type as _ { + EXT_SUPPORT_BLOCK_INPUT => { + // let supported_plugins = []; + // let supported = supported_plugins.contains(&id); + let supported = true; + if supported { + if msg.data.len() != 1 { + return PluginReturn::new( + errno::ERR_CALLBACK_INVALID_ARGS, + "Invalid data length", + ); + } + let block = msg.data[0] != 0; + if crate::server::plugin_block_input(peer, block) == block { + PluginReturn::success() + } else { + PluginReturn::new(errno::ERR_CALLBACK_FAILED, "") + } + } else { + PluginReturn::new( + errno::ERR_CALLBACK_PLUGIN_ID, + &format!("This operation is not supported for plugin '{}', please contact the RustDesk team for support.", id), + ) + } + } + _ => PluginReturn::new( + errno::ERR_CALLBACK_TARGET_TYPE, + &format!("Unknown target type '{}'", &msg.r#type), + ), + } +} diff --git a/vendor/rustdesk/src/plugin/callback_msg.rs b/vendor/rustdesk/src/plugin/callback_msg.rs new file mode 100644 index 0000000..2a23b03 --- /dev/null +++ b/vendor/rustdesk/src/plugin/callback_msg.rs @@ -0,0 +1,411 @@ +use super::*; +use crate::hbbs_http::create_http_client; +use crate::{ + flutter::{self, APP_TYPE_CM, APP_TYPE_MAIN, SESSIONS}, + ui_interface::get_api_server, +}; +use hbb_common::{lazy_static, log, message_proto::PluginRequest}; +use serde_derive::{Deserialize, Serialize}; +use serde_json; +use std::{ + collections::HashMap, + ffi::{c_char, c_void}, + sync::Arc, + thread, + time::Duration, +}; + +const MSG_TO_RUSTDESK_TARGET: &str = "rustdesk"; +const MSG_TO_PEER_TARGET: &str = "peer"; +const MSG_TO_UI_TARGET: &str = "ui"; +const MSG_TO_CONFIG_TARGET: &str = "config"; +const MSG_TO_EXT_SUPPORT_TARGET: &str = "ext-support"; + +const MSG_TO_RUSTDESK_SIGNATURE_VERIFICATION: &str = "signature_verification"; + +#[allow(dead_code)] +const MSG_TO_UI_FLUTTER_CHANNEL_MAIN: u16 = 0x01 << 0; +#[allow(dead_code)] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +const MSG_TO_UI_FLUTTER_CHANNEL_CM: u16 = 0x01 << 1; +#[cfg(any(target_os = "android", target_os = "ios"))] +const MSG_TO_UI_FLUTTER_CHANNEL_CM: u16 = 0x01; +const MSG_TO_UI_FLUTTER_CHANNEL_REMOTE: u16 = 0x01 << 2; +#[allow(dead_code)] +const MSG_TO_UI_FLUTTER_CHANNEL_TRANSFER: u16 = 0x01 << 3; +#[allow(dead_code)] +const MSG_TO_UI_FLUTTER_CHANNEL_FORWARD: u16 = 0x01 << 4; + +lazy_static::lazy_static! { + static ref MSG_TO_UI_FLUTTER_CHANNELS: Arc> = { + let channels = HashMap::from([ + (MSG_TO_UI_FLUTTER_CHANNEL_MAIN, APP_TYPE_MAIN.to_string()), + (MSG_TO_UI_FLUTTER_CHANNEL_CM, APP_TYPE_CM.to_string()), + ]); + Arc::new(channels) + }; +} + +#[derive(Deserialize)] +pub struct MsgToRustDesk { + pub r#type: String, + pub data: Vec, +} + +#[derive(Deserialize)] +pub struct SignatureVerification { + pub version: String, + pub data: Vec, +} + +#[derive(Debug, Deserialize)] +struct ConfigToUi { + channel: u16, + location: String, +} + +#[derive(Debug, Deserialize)] +struct MsgToConfig { + r#type: String, + key: String, + value: String, + #[serde(skip_serializing_if = "Option::is_none")] + ui: Option, // If not None, send msg to ui. +} + +#[derive(Debug, Deserialize)] +pub(super) struct MsgToExtSupport { + pub r#type: String, + pub data: Vec, +} + +#[derive(Debug, Serialize)] +struct PluginSignReq { + plugin_id: String, + version: String, + msg: Vec, +} + +#[derive(Debug, Deserialize)] +struct PluginSignResp { + signed_msg: Vec, +} + +macro_rules! cb_msg_field { + ($field: ident) => { + let $field = match cstr_to_string($field) { + Err(e) => { + let msg = format!("Failed to convert {} to string, {}", stringify!($field), e); + log::error!("{}", &msg); + return PluginReturn::new(errno::ERR_CALLBACK_INVALID_ARGS, &msg); + } + Ok(v) => v, + }; + }; +} + +macro_rules! early_return_value { + ($e:expr, $code: ident, $($arg:tt)*) => { + match $e { + Err(e) => return PluginReturn::new( + errno::$code, + &format!("Failed to {} '{}'", format_args!($($arg)*), e), + ), + Ok(v) => v, + } + }; +} + +/// Callback to send message to peer or ui. +/// peer, target, id are utf8 strings(null terminated). +/// +/// peer: The peer id. +/// target: "peer" or "ui". +/// id: The id of this plugin. +/// content: The content. +/// len: The length of the content. +/// +/// Return null ptr if success. +/// Return the error message if failed. `i32-String` without dash, i32 is a signed little-endian number, the String is utf8 string. +/// The plugin allocate memory with `libc::malloc` and return the pointer. +#[no_mangle] +pub(super) extern "C" fn cb_msg( + peer: *const c_char, + target: *const c_char, + id: *const c_char, + content: *const c_void, + len: usize, +) -> PluginReturn { + cb_msg_field!(target); + cb_msg_field!(id); + + match &target as _ { + MSG_TO_PEER_TARGET => { + cb_msg_field!(peer); + if let Some(session) = SESSIONS.write().unwrap().get_mut(&peer) { + let content_slice = + unsafe { std::slice::from_raw_parts(content as *const u8, len) }; + let content_vec = Vec::from(content_slice); + let request = PluginRequest { + id, + content: bytes::Bytes::from(content_vec), + ..Default::default() + }; + session.send_plugin_request(request); + PluginReturn::success() + } else { + PluginReturn::new( + errno::ERR_CALLBACK_PEER_NOT_FOUND, + &format!("Failed to find session for peer '{}'", peer), + ) + } + } + MSG_TO_UI_TARGET => { + cb_msg_field!(peer); + let content_slice = unsafe { std::slice::from_raw_parts(content as *const u8, len) }; + let channel = u16::from_le_bytes([content_slice[0], content_slice[1]]); + let content = std::string::String::from_utf8(content_slice[2..].to_vec()) + .unwrap_or("".to_string()); + push_event_to_ui(channel, &peer, &content); + PluginReturn::success() + } + MSG_TO_CONFIG_TARGET => { + cb_msg_field!(peer); + let s = early_return_value!( + std::str::from_utf8(unsafe { std::slice::from_raw_parts(content as _, len) }), + ERR_CALLBACK_INVALID_MSG, + "parse msg string" + ); + // No need to merge the msgs. Handling the msg one by one is ok. + let msg = early_return_value!( + serde_json::from_str::(s), + ERR_CALLBACK_INVALID_MSG, + "parse msg '{}'", + s + ); + match &msg.r#type as _ { + config::CONFIG_TYPE_SHARED => { + let _r = early_return_value!( + config::SharedConfig::set(&id, &msg.key, &msg.value), + ERR_CALLBACK_INVALID_MSG, + "set local config" + ); + if let Some(ui) = &msg.ui { + // No need to set the peer id for location config. + push_option_to_ui(ui.channel, &id, "", &msg, ui); + } + PluginReturn::success() + } + config::CONFIG_TYPE_PEER => { + let _r = early_return_value!( + config::PeerConfig::set(&id, &peer, &msg.key, &msg.value), + ERR_CALLBACK_INVALID_MSG, + "set peer config" + ); + if let Some(ui) = &msg.ui { + push_option_to_ui(ui.channel, &id, &peer, &msg, ui); + } + PluginReturn::success() + } + _ => PluginReturn::new( + errno::ERR_CALLBACK_TARGET_TYPE, + &format!("Unknown target type '{}'", &msg.r#type), + ), + } + } + MSG_TO_EXT_SUPPORT_TARGET => { + cb_msg_field!(peer); + let s = early_return_value!( + std::str::from_utf8(unsafe { std::slice::from_raw_parts(content as _, len) }), + ERR_CALLBACK_INVALID_MSG, + "parse msg string" + ); + let msg = early_return_value!( + serde_json::from_str::(s), + ERR_CALLBACK_INVALID_MSG, + "parse msg '{}'", + s + ); + super::callback_ext::ext_support_callback(&id, &peer, &msg) + } + MSG_TO_RUSTDESK_TARGET => handle_msg_to_rustdesk(id, content, len), + _ => PluginReturn::new( + errno::ERR_CALLBACK_TARGET, + &format!("Unknown target '{}'", target), + ), + } +} + +#[inline] +fn is_peer_channel(channel: u16) -> bool { + channel & MSG_TO_UI_FLUTTER_CHANNEL_REMOTE != 0 + || channel & MSG_TO_UI_FLUTTER_CHANNEL_TRANSFER != 0 + || channel & MSG_TO_UI_FLUTTER_CHANNEL_FORWARD != 0 +} + +fn handle_msg_to_rustdesk(id: String, content: *const c_void, len: usize) -> PluginReturn { + let s = early_return_value!( + std::str::from_utf8(unsafe { std::slice::from_raw_parts(content as _, len) }), + ERR_CALLBACK_INVALID_MSG, + "parse msg string" + ); + let msg_to_rustdesk = early_return_value!( + serde_json::from_str::(s), + ERR_CALLBACK_INVALID_MSG, + "parse msg '{}'", + s + ); + match &msg_to_rustdesk.r#type as &str { + MSG_TO_RUSTDESK_SIGNATURE_VERIFICATION => request_plugin_sign(id, msg_to_rustdesk), + t => PluginReturn::new( + errno::ERR_CALLBACK_TARGET_TYPE, + &format!( + "Unknown target type '{}' for target {}", + t, MSG_TO_RUSTDESK_TARGET + ), + ), + } +} + +fn request_plugin_sign(id: String, msg_to_rustdesk: MsgToRustDesk) -> PluginReturn { + let signature_data = early_return_value!( + std::str::from_utf8(&msg_to_rustdesk.data), + ERR_CALLBACK_INVALID_MSG, + "parse signature data string" + ); + let signature_data = early_return_value!( + serde_json::from_str::(signature_data), + ERR_CALLBACK_INVALID_MSG, + "parse signature data '{}'", + signature_data + ); + thread::spawn(move || { + let sign_url = format!("{}/lic/web/api/plugin-sign", get_api_server()); + let client = create_http_client(); + let req = PluginSignReq { + plugin_id: id.clone(), + version: signature_data.version, + msg: signature_data.data, + }; + match client + .post(sign_url) + .json(&req) + .timeout(Duration::from_secs(10)) + .send() + { + Ok(response) => match response.json::() { + Ok(sign_resp) => { + match super::plugins::plugin_call( + &id, + super::plugins::METHOD_HANDLE_SIGNATURE_VERIFICATION, + "", + &sign_resp.signed_msg, + ) { + Ok(..) => { + match super::plugins::plugin_call_get_return( + &id, + super::plugins::METHOD_HANDLE_STATUS, + "", + &[], + ) { + Ok(ret) => { + debug_assert!(!ret.msg.is_null(), "msg is null"); + if ret.msg.is_null() { + // unreachable + log::error!( + "The returned message pointer of plugin status is null, plugin id: '{}', code: {}", + id, + ret.code, + ); + return; + } + let msg = cstr_to_string(ret.msg).unwrap_or_default(); + free_c_ptr(ret.msg as _); + if ret.code == super::errno::ERR_SUCCESS { + log::info!("Plugin '{}' status: '{}'", id, msg); + } else { + log::error!( + "Failed to handle plugin event, id: {}, method: {}, code: {}, msg: {}", + id, + std::string::String::from_utf8(super::plugins::METHOD_HANDLE_STATUS.to_vec()).unwrap_or_default(), + ret.code, + msg + ); + } + } + Err(e) => { + log::error!( + "Failed to call status for plugin '{}': {}", + &id, + e + ); + } + } + } + Err(e) => { + log::error!( + "Failed to call signature verification for plugin '{}': {}", + &id, + e + ); + } + } + } + Err(e) => { + log::error!("Failed to decode response for plugin '{}': {}", &id, e); + } + }, + Err(e) => { + log::error!("Failed to request sign for plugin '{}', {}", &id, e); + } + } + }); + PluginReturn::success() +} + +fn push_event_to_ui(channel: u16, peer: &str, content: &str) { + let mut m = HashMap::new(); + m.insert("name", MSG_TO_UI_TYPE_PLUGIN_EVENT); + m.insert("peer", &peer); + m.insert("content", &content); + let event = serde_json::to_string(&m).unwrap_or("".to_string()); + // Send to main and cm + for (k, v) in MSG_TO_UI_FLUTTER_CHANNELS.iter() { + if channel & k != 0 { + let _res = flutter::push_global_event(v as _, event.to_string()); + } + } + if !peer.is_empty() && is_peer_channel(channel) { + let _res = flutter::push_session_event( + &peer, + MSG_TO_UI_TYPE_PLUGIN_EVENT, + vec![("peer", &peer), ("content", &content)], + ); + } +} + +fn push_option_to_ui(channel: u16, id: &str, peer: &str, msg: &MsgToConfig, ui: &ConfigToUi) { + let v = [ + ("id", id), + ("location", &ui.location), + ("key", &msg.key), + ("value", &msg.value), + ]; + + // Send main and cm + let mut m = HashMap::from(v); + m.insert("name", MSG_TO_UI_TYPE_PLUGIN_OPTION); + let event = serde_json::to_string(&m).unwrap_or("".to_string()); + for (k, v) in MSG_TO_UI_FLUTTER_CHANNELS.iter() { + if channel & k != 0 { + let _res = flutter::push_global_event(v as _, event.to_string()); + } + } + + // Send remote, transfer and forward + if !peer.is_empty() && is_peer_channel(channel) { + let mut v = v.to_vec(); + v.push(("peer", &peer)); + let _res = flutter::push_session_event(&peer, MSG_TO_UI_TYPE_PLUGIN_OPTION, v); + } +} diff --git a/vendor/rustdesk/src/plugin/config.rs b/vendor/rustdesk/src/plugin/config.rs new file mode 100644 index 0000000..20cd02a --- /dev/null +++ b/vendor/rustdesk/src/plugin/config.rs @@ -0,0 +1,363 @@ +use super::{cstr_to_string, str_to_cstr_ret}; +use hbb_common::{allow_err, bail, config::Config as HbbConfig, lazy_static, log, ResultType}; +use serde_derive::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + ffi::c_char, + fs, + ops::{Deref, DerefMut}, + path::PathBuf, + ptr, + str::FromStr, + sync::{Arc, Mutex}, +}; + +lazy_static::lazy_static! { + static ref CONFIG_SHARED: Arc>> = Default::default(); + static ref CONFIG_PEERS: Arc>> = Default::default(); + static ref CONFIG_MANAGER: Arc> = { + let conf = hbb_common::config::load_path::(ManagerConfig::path()); + Arc::new(Mutex::new(conf)) + }; +} +use crate::ui_interface::get_id; + +pub(super) const CONFIG_TYPE_SHARED: &str = "shared"; +pub(super) const CONFIG_TYPE_PEER: &str = "peer"; + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct SharedConfig(HashMap); +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct PeerConfig(HashMap); +type PeersConfig = HashMap; + +#[inline] +fn path_plugins(id: &str) -> PathBuf { + HbbConfig::path("plugins").join(id) +} + +pub fn remove(id: &str) { + CONFIG_SHARED.lock().unwrap().remove(id); + CONFIG_PEERS.lock().unwrap().remove(id); + // allow_err is Ok here. + allow_err!(ManagerConfig::remove_plugin(id)); + if let Err(e) = fs::remove_dir_all(path_plugins(id)) { + log::error!("Failed to remove plugin '{}' directory: {}", id, e); + } +} + +impl Deref for SharedConfig { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SharedConfig { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Deref for PeerConfig { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for PeerConfig { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl SharedConfig { + #[inline] + fn path(id: &str) -> PathBuf { + path_plugins(id).join("shared.toml") + } + + #[inline] + fn load(id: &str) { + let mut lock = CONFIG_SHARED.lock().unwrap(); + if lock.contains_key(id) { + return; + } + let conf = hbb_common::config::load_path::>(Self::path(id)); + let mut conf = SharedConfig(conf); + if let Some(desc_conf) = super::plugins::get_desc_conf(id) { + for item in desc_conf.shared.iter() { + if !conf.contains_key(&item.key) { + conf.insert(item.key.to_owned(), item.default.to_owned()); + } + } + } + lock.insert(id.to_owned(), conf); + } + + #[inline] + fn load_if_not_exists(id: &str) { + if CONFIG_SHARED.lock().unwrap().contains_key(id) { + return; + } + Self::load(id); + } + + #[inline] + pub fn get(id: &str, key: &str) -> Option { + Self::load_if_not_exists(id); + CONFIG_SHARED + .lock() + .unwrap() + .get(id)? + .get(key) + .map(|s| s.to_owned()) + } + + #[inline] + pub fn set(id: &str, key: &str, value: &str) -> ResultType<()> { + Self::load_if_not_exists(id); + match CONFIG_SHARED.lock().unwrap().get_mut(id) { + Some(config) => { + config.insert(key.to_owned(), value.to_owned()); + hbb_common::config::store_path(Self::path(id), config) + } + None => { + // unreachable + bail!("No such plugin {}", id) + } + } + } +} + +impl PeerConfig { + #[inline] + fn path(id: &str, peer: &str) -> PathBuf { + path_plugins(id) + .join("peers") + .join(format!("{}.toml", peer)) + } + + #[inline] + fn load(id: &str, peer: &str) { + let mut lock = CONFIG_PEERS.lock().unwrap(); + if let Some(peers) = lock.get(id) { + if peers.contains_key(peer) { + return; + } + } + + let conf = hbb_common::config::load_path::>(Self::path(id, peer)); + let mut conf = PeerConfig(conf); + if let Some(desc_conf) = super::plugins::get_desc_conf(id) { + for item in desc_conf.peer.iter() { + if !conf.contains_key(&item.key) { + conf.insert(item.key.to_owned(), item.default.to_owned()); + } + } + } + + if let Some(peers) = lock.get_mut(id) { + peers.insert(peer.to_owned(), conf); + return; + } + + let mut peers = HashMap::new(); + peers.insert(peer.to_owned(), conf); + lock.insert(id.to_owned(), peers); + } + + #[inline] + fn load_if_not_exists(id: &str, peer: &str) { + if let Some(peers) = CONFIG_PEERS.lock().unwrap().get(id) { + if peers.contains_key(peer) { + return; + } + } + Self::load(id, peer); + } + + #[inline] + pub fn get(id: &str, peer: &str, key: &str) -> Option { + Self::load_if_not_exists(id, peer); + CONFIG_PEERS + .lock() + .unwrap() + .get(id)? + .get(peer)? + .get(key) + .map(|s| s.to_owned()) + } + + #[inline] + pub fn set(id: &str, peer: &str, key: &str, value: &str) -> ResultType<()> { + Self::load_if_not_exists(id, peer); + match CONFIG_PEERS.lock().unwrap().get_mut(id) { + Some(peers) => match peers.get_mut(peer) { + Some(config) => { + config.insert(key.to_owned(), value.to_owned()); + hbb_common::config::store_path(Self::path(id, peer), config) + } + None => { + // unreachable + bail!("No such peer {}", peer) + } + }, + None => { + // unreachable + bail!("No such plugin {}", id) + } + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PluginStatus { + pub enabled: bool, +} + +const MANAGER_VERSION: &str = "0.1.0"; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ManagerConfig { + pub version: String, + #[serde(default)] + pub options: HashMap, + #[serde(default)] + pub plugins: HashMap, +} + +impl Default for ManagerConfig { + fn default() -> Self { + Self { + version: MANAGER_VERSION.to_owned(), + options: HashMap::new(), + plugins: HashMap::new(), + } + } +} + +// Do not care about the `store_path` error, no need to store the old value and restore if failed. +impl ManagerConfig { + #[inline] + fn path() -> PathBuf { + HbbConfig::path("plugins").join("manager.toml") + } + + #[inline] + pub fn get_option(key: &str) -> Option { + CONFIG_MANAGER + .lock() + .unwrap() + .options + .get(key) + .map(|s| s.to_owned()) + } + + #[inline] + pub fn set_option(key: &str, value: &str) { + let mut lock = CONFIG_MANAGER.lock().unwrap(); + lock.options.insert(key.to_owned(), value.to_owned()); + allow_err!(hbb_common::config::store_path(Self::path(), &*lock)); + } + + #[inline] + pub fn get_plugin_option(id: &str, key: &str) -> Option { + let lock = CONFIG_MANAGER.lock().unwrap(); + match key { + "enabled" => { + let enabled = lock + .plugins + .get(id) + .map(|status| status.enabled.to_owned()) + .unwrap_or(true.to_owned()) + .to_string(); + Some(enabled) + } + _ => None, + } + } + + fn set_plugin_option_enabled(id: &str, enabled: bool) -> ResultType<()> { + let mut lock = CONFIG_MANAGER.lock().unwrap(); + if let Some(status) = lock.plugins.get_mut(id) { + status.enabled = enabled; + } else { + lock.plugins.insert(id.to_owned(), PluginStatus { enabled }); + } + hbb_common::config::store_path(Self::path(), &*lock) + } + + pub fn set_plugin_option(id: &str, key: &str, value: &str) { + match key { + "enabled" => { + let enabled = bool::from_str(value).unwrap_or(false); + allow_err!(Self::set_plugin_option_enabled(id, enabled)); + if enabled { + allow_err!(super::load_plugin(id)); + } else { + super::unload_plugin(id); + } + } + _ => log::error!("No such option {}", key), + } + } + + #[inline] + pub fn add_plugin(id: &str) -> ResultType<()> { + let mut lock = CONFIG_MANAGER.lock().unwrap(); + lock.plugins + .insert(id.to_owned(), PluginStatus { enabled: true }); + hbb_common::config::store_path(Self::path(), &*lock) + } + + #[inline] + pub fn remove_plugin(id: &str) -> ResultType<()> { + let mut lock = CONFIG_MANAGER.lock().unwrap(); + lock.plugins.remove(id); + hbb_common::config::store_path(Self::path(), &*lock) + } +} + +pub(super) extern "C" fn cb_get_local_peer_id() -> *const c_char { + str_to_cstr_ret(&get_id()) +} + +// Return shared config if peer is nullptr. +pub(super) extern "C" fn cb_get_conf( + peer: *const c_char, + id: *const c_char, + key: *const c_char, +) -> *const c_char { + match (cstr_to_string(id), cstr_to_string(key)) { + (Ok(id), Ok(key)) => { + if peer.is_null() { + SharedConfig::load_if_not_exists(&id); + if let Some(conf) = CONFIG_SHARED.lock().unwrap().get(&id) { + if let Some(value) = conf.get(&key) { + return str_to_cstr_ret(value); + } + } + } else { + match cstr_to_string(peer) { + Ok(peer) => { + PeerConfig::load_if_not_exists(&id, &peer); + if let Some(conf) = CONFIG_PEERS.lock().unwrap().get(&id) { + if let Some(conf) = conf.get(&peer) { + if let Some(value) = conf.get(&key) { + return str_to_cstr_ret(value); + } + } + } + } + Err(_) => {} + } + } + } + _ => {} + } + ptr::null() +} diff --git a/vendor/rustdesk/src/plugin/desc.rs b/vendor/rustdesk/src/plugin/desc.rs new file mode 100644 index 0000000..883f2af --- /dev/null +++ b/vendor/rustdesk/src/plugin/desc.rs @@ -0,0 +1,100 @@ +use hbb_common::ResultType; +use serde_derive::{Deserialize, Serialize}; +use serde_json; +use std::collections::HashMap; +use std::ffi::{c_char, CStr}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UiButton { + key: String, + text: String, + icon: String, // icon can be int in flutter, but string in other ui framework. And it is flexible to use string. + tooltip: String, + action: String, // The action to be triggered when the button is clicked. +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UiCheckbox { + key: String, + text: String, + tooltip: String, + action: String, // The action to be triggered when the checkbox is checked or unchecked. +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "t", content = "c")] +pub enum UiType { + Button(UiButton), + Checkbox(UiCheckbox), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Location { + pub ui: HashMap>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigItem { + pub key: String, + pub default: String, + pub description: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub shared: Vec, + pub peer: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublishInfo { + pub published: String, + pub last_released: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Meta { + pub id: String, + pub name: String, + pub version: String, + pub description: String, + #[serde(default)] + pub platforms: String, + pub author: String, + pub home: String, + pub license: String, + pub source: String, + pub publish_info: PublishInfo, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Desc { + meta: Meta, + need_reboot: bool, + location: Location, + config: Config, + listen_events: Vec, +} + +impl Desc { + pub fn from_cstr(s: *const c_char) -> ResultType { + let s = unsafe { CStr::from_ptr(s) }; + Ok(serde_json::from_str(s.to_str()?)?) + } + + pub fn meta(&self) -> &Meta { + &self.meta + } + + pub fn location(&self) -> &Location { + &self.location + } + + pub fn config(&self) -> &Config { + &self.config + } + + pub fn listen_events(&self) -> &Vec { + &self.listen_events + } +} diff --git a/vendor/rustdesk/src/plugin/errno.rs b/vendor/rustdesk/src/plugin/errno.rs new file mode 100644 index 0000000..6b1e361 --- /dev/null +++ b/vendor/rustdesk/src/plugin/errno.rs @@ -0,0 +1,50 @@ +#![allow(dead_code)] + +pub const ERR_SUCCESS: i32 = 0; + +// ====================================================== +// Errors from the plugins, must be handled by RustDesk + +pub const ERR_RUSTDESK_HANDLE_BASE: i32 = 10000; + +// not loaded +pub const ERR_PLUGIN_LOAD: i32 = 10001; +// not initialized +pub const ERR_PLUGIN_MSG_INIT: i32 = 10101; +pub const ERR_PLUGIN_MSG_INIT_INVALID: i32 = 10102; +pub const ERR_PLUGIN_MSG_GET_LOCAL_PEER_ID: i32 = 10103; +pub const ERR_PLUGIN_SIGNATURE_NOT_VERIFIED: i32 = 10104; +pub const ERR_PLUGIN_SIGNATURE_VERIFICATION_FAILED: i32 = 10105; +// invalid +pub const ERR_CALL_UNIMPLEMENTED: i32 = 10201; +pub const ERR_CALL_INVALID_METHOD: i32 = 10202; +pub const ERR_CALL_NOT_SUPPORTED_METHOD: i32 = 10203; +pub const ERR_CALL_INVALID_PEER: i32 = 10204; +// failed on calling +pub const ERR_CALL_INVALID_ARGS: i32 = 10301; +pub const ERR_PEER_ID_MISMATCH: i32 = 10302; +pub const ERR_CALL_CONFIG_VALUE: i32 = 10303; +// no handlers on calling +pub const ERR_NOT_HANDLED: i32 = 10401; + +// ====================================================== +// Errors from RustDesk callbacks. + +pub const ERR_CALLBACK_HANDLE_BASE: i32 = 20000; +pub const ERR_CALLBACK_PLUGIN_ID: i32 = 20001; +pub const ERR_CALLBACK_INVALID_ARGS: i32 = 20002; +pub const ERR_CALLBACK_INVALID_MSG: i32 = 20003; +pub const ERR_CALLBACK_TARGET: i32 = 20004; +pub const ERR_CALLBACK_TARGET_TYPE: i32 = 20005; +pub const ERR_CALLBACK_PEER_NOT_FOUND: i32 = 20006; + +pub const ERR_CALLBACK_FAILED: i32 = 21001; + +// ====================================================== +// Errors from the plugins, should be handled by the plugins. + +pub const ERR_PLUGIN_HANDLE_BASE: i32 = 30000; + +pub const EER_CALL_FAILED: i32 = 30021; +pub const ERR_PEER_ON_FAILED: i32 = 40012; +pub const ERR_PEER_OFF_FAILED: i32 = 40012; diff --git a/vendor/rustdesk/src/plugin/ipc.rs b/vendor/rustdesk/src/plugin/ipc.rs new file mode 100644 index 0000000..6a14ab0 --- /dev/null +++ b/vendor/rustdesk/src/plugin/ipc.rs @@ -0,0 +1,230 @@ +// to-do: Interdependence(This mod and crate::ipc) is not good practice here. +use crate::ipc::{connect, Connection, Data}; +use hbb_common::{allow_err, log, tokio, ResultType}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum InstallStatus { + Downloading(u8), + Installing, + Finished, + FailedCreating, + FailedDownloading, + FailedInstalling, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum Plugin { + Config(String, String, Option), + ManagerConfig(String, Option), + ManagerPluginConfig(String, String, Option), + Load(String), + Reload(String), + InstallStatus((String, InstallStatus)), + Uninstall(String), +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_config(id: &str, name: &str) -> ResultType> { + get_config_async(id, name, 1_000).await +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_config(id: &str, name: &str, value: String) -> ResultType<()> { + set_config_async(id, name, value).await +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_manager_config(name: &str) -> ResultType> { + get_manager_config_async(name, 1_000).await +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_manager_config(name: &str, value: String) -> ResultType<()> { + set_manager_config_async(name, value).await +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_manager_plugin_config(id: &str, name: &str) -> ResultType> { + get_manager_plugin_config_async(id, name, 1_000).await +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_manager_plugin_config(id: &str, name: &str, value: String) -> ResultType<()> { + set_manager_plugin_config_async(id, name, value).await +} + +#[tokio::main(flavor = "current_thread")] +pub async fn load_plugin(id: &str) -> ResultType<()> { + load_plugin_async(id).await +} + +#[tokio::main(flavor = "current_thread")] +pub async fn reload_plugin(id: &str) -> ResultType<()> { + reload_plugin_async(id).await +} + +#[tokio::main(flavor = "current_thread")] +pub async fn uninstall_plugin(id: &str) -> ResultType<()> { + uninstall_plugin_async(id).await +} + +async fn get_config_async(id: &str, name: &str, ms_timeout: u64) -> ResultType> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::Plugin(Plugin::Config( + id.to_owned(), + name.to_owned(), + None, + ))) + .await?; + if let Some(Data::Plugin(Plugin::Config(id2, name2, value))) = + c.next_timeout(ms_timeout).await? + { + if id == id2 && name == name2 { + return Ok(value); + } + } + return Ok(None); +} + +async fn set_config_async(id: &str, name: &str, value: String) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::Plugin(Plugin::Config( + id.to_owned(), + name.to_owned(), + Some(value), + ))) + .await?; + Ok(()) +} + +async fn get_manager_config_async(name: &str, ms_timeout: u64) -> ResultType> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::Plugin(Plugin::ManagerConfig(name.to_owned(), None))) + .await?; + if let Some(Data::Plugin(Plugin::ManagerConfig(name2, value))) = + c.next_timeout(ms_timeout).await? + { + if name == name2 { + return Ok(value); + } + } + return Ok(None); +} + +async fn set_manager_config_async(name: &str, value: String) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::Plugin(Plugin::ManagerConfig( + name.to_owned(), + Some(value), + ))) + .await?; + Ok(()) +} + +async fn get_manager_plugin_config_async( + id: &str, + name: &str, + ms_timeout: u64, +) -> ResultType> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::Plugin(Plugin::ManagerPluginConfig( + id.to_owned(), + name.to_owned(), + None, + ))) + .await?; + if let Some(Data::Plugin(Plugin::ManagerPluginConfig(id2, name2, value))) = + c.next_timeout(ms_timeout).await? + { + if id == id2 && name == name2 { + return Ok(value); + } + } + return Ok(None); +} + +async fn set_manager_plugin_config_async(id: &str, name: &str, value: String) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::Plugin(Plugin::ManagerPluginConfig( + id.to_owned(), + name.to_owned(), + Some(value), + ))) + .await?; + Ok(()) +} + +pub async fn load_plugin_async(id: &str) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::Plugin(Plugin::Load(id.to_owned()))).await?; + Ok(()) +} + +async fn reload_plugin_async(id: &str) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::Plugin(Plugin::Reload(id.to_owned()))).await?; + Ok(()) +} + +async fn uninstall_plugin_async(id: &str) -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::Plugin(Plugin::Uninstall(id.to_owned()))) + .await?; + Ok(()) +} + +pub async fn handle_plugin(plugin: Plugin, stream: &mut Connection) { + match plugin { + Plugin::Config(id, name, value) => match value { + None => { + let value = super::SharedConfig::get(&id, &name); + allow_err!( + stream + .send(&Data::Plugin(Plugin::Config(id, name, value))) + .await + ); + } + Some(value) => { + allow_err!(super::SharedConfig::set(&id, &name, &value)); + } + }, + Plugin::ManagerConfig(name, value) => match value { + None => { + let value = super::ManagerConfig::get_option(&name); + allow_err!( + stream + .send(&Data::Plugin(Plugin::ManagerConfig(name, value))) + .await + ); + } + Some(value) => { + super::ManagerConfig::set_option(&name, &value); + } + }, + Plugin::ManagerPluginConfig(id, name, value) => match value { + None => { + let value = super::ManagerConfig::get_plugin_option(&id, &name); + allow_err!( + stream + .send(&Data::Plugin(Plugin::ManagerPluginConfig(id, name, value))) + .await + ); + } + Some(value) => { + super::ManagerConfig::set_plugin_option(&id, &name, &value); + } + }, + Plugin::Load(id) => { + allow_err!(super::load_plugin(&id)); + } + Plugin::Reload(id) => { + allow_err!(super::reload_plugin(&id)); + } + Plugin::Uninstall(id) => { + super::manager::uninstall_plugin(&id, false); + } + _ => {} + } +} diff --git a/vendor/rustdesk/src/plugin/manager.rs b/vendor/rustdesk/src/plugin/manager.rs new file mode 100644 index 0000000..f59e4c9 --- /dev/null +++ b/vendor/rustdesk/src/plugin/manager.rs @@ -0,0 +1,600 @@ +// 1. Check update. +// 2. Install or uninstall. + +use super::{desc::Meta as PluginMeta, ipc::InstallStatus, *}; +use crate::flutter; +use crate::hbbs_http::create_http_client; +use hbb_common::{allow_err, bail, log, tokio, toml}; +use serde_derive::{Deserialize, Serialize}; +use serde_json; +use std::{ + collections::{HashMap, HashSet}, + fs::{read_to_string, remove_dir_all, OpenOptions}, + io::Write, + sync::{Arc, Mutex}, +}; + +const MSG_TO_UI_PLUGIN_MANAGER_LIST: &str = "plugin_list"; +const MSG_TO_UI_PLUGIN_MANAGER_INSTALL: &str = "plugin_install"; +const MSG_TO_UI_PLUGIN_MANAGER_UNINSTALL: &str = "plugin_uninstall"; + +const IPC_PLUGIN_POSTFIX: &str = "_plugin"; + +#[cfg(target_os = "windows")] +const PLUGIN_PLATFORM: &str = "windows"; +#[cfg(target_os = "linux")] +const PLUGIN_PLATFORM: &str = "linux"; +#[cfg(target_os = "macos")] +const PLUGIN_PLATFORM: &str = "macos"; + +lazy_static::lazy_static! { + static ref PLUGIN_INFO: Arc>> = Arc::new(Mutex::new(HashMap::new())); +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct ManagerMeta { + pub version: String, + pub description: String, + pub plugins: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PluginSource { + pub name: String, + pub url: String, + pub description: String, +} + +#[derive(Debug, Serialize)] +pub struct PluginInfo { + pub source: PluginSource, + pub meta: PluginMeta, + pub installed_version: String, + pub invalid_reason: String, +} + +static PLUGIN_SOURCE_LOCAL: &str = "local"; + +fn get_plugin_source_list() -> Vec { + // Only one source for now. + // vec![PluginSource { + // name: "rustdesk".to_string(), + // url: "https://raw.githubusercontent.com/fufesou/rustdesk-plugins/main".to_string(), + // description: "".to_string(), + // }] + vec![] +} + +fn get_source_plugins() -> HashMap { + let mut plugins = HashMap::new(); + for source in get_plugin_source_list().into_iter() { + let url = format!("{}/meta.toml", source.url); + match create_http_client().get(&url).send() { + Ok(resp) => { + if !resp.status().is_success() { + log::error!( + "Failed to get plugin list from '{}', status code: {}", + url, + resp.status() + ); + } + if let Ok(text) = resp.text() { + match toml::from_str::(&text) { + Ok(manager_meta) => { + for meta in manager_meta.plugins.iter() { + if !meta + .platforms + .to_uppercase() + .contains(&PLUGIN_PLATFORM.to_uppercase()) + { + continue; + } + plugins.insert( + meta.id.clone(), + PluginInfo { + source: source.clone(), + meta: meta.clone(), + installed_version: "".to_string(), + invalid_reason: "".to_string(), + }, + ); + } + } + Err(e) => log::error!("Failed to parse plugin list from '{}', {}", url, e), + } + } + } + Err(e) => log::error!("Failed to get plugin list from '{}', {}", url, e), + } + } + plugins +} + +fn send_plugin_list_event(plugins: &HashMap) { + let mut plugin_list = plugins.values().collect::>(); + plugin_list.sort_by(|a, b| a.meta.name.cmp(&b.meta.name)); + if let Ok(plugin_list) = serde_json::to_string(&plugin_list) { + let mut m = HashMap::new(); + m.insert("name", MSG_TO_UI_TYPE_PLUGIN_MANAGER); + m.insert(MSG_TO_UI_PLUGIN_MANAGER_LIST, &plugin_list); + if let Ok(event) = serde_json::to_string(&m) { + let _res = flutter::push_global_event(flutter::APP_TYPE_MAIN, event.clone()); + } + } +} + +pub fn load_plugin_list() { + let mut plugin_info_lock = PLUGIN_INFO.lock().unwrap(); + let mut plugins = get_source_plugins(); + + // A big read lock is needed to prevent race conditions. + // Loading plugin list may be slow. + // Users may call uninstall plugin in the middle. + let plugin_infos = super::plugins::get_plugin_infos(); + let plugin_infos_read_lock = plugin_infos.read().unwrap(); + for (id, info) in plugin_infos_read_lock.iter() { + if info.uninstalled { + continue; + } + + if let Some(p) = plugins.get_mut(id) { + p.installed_version = info.desc.meta().version.clone(); + p.invalid_reason = "".to_string(); + } else { + plugins.insert( + id.to_string(), + PluginInfo { + source: PluginSource { + name: PLUGIN_SOURCE_LOCAL.to_string(), + url: PLUGIN_SOURCE_LOCAL_DIR.to_string(), + description: "".to_string(), + }, + meta: info.desc.meta().clone(), + installed_version: info.desc.meta().version.clone(), + invalid_reason: "".to_string(), + }, + ); + } + } + send_plugin_list_event(&plugins); + *plugin_info_lock = plugins; +} + +#[cfg(target_os = "windows")] +fn elevate_install( + plugin_id: &str, + plugin_url: &str, + same_plugin_exists: bool, +) -> ResultType { + // to-do: Support args with space in quotes. 'arg 1' and "arg 2" + let args = if same_plugin_exists { + format!("--plugin-install {}", plugin_id) + } else { + format!("--plugin-install {} {}", plugin_id, plugin_url) + }; + crate::platform::elevate(&args) +} + +#[cfg(target_os = "linux")] +fn elevate_install( + plugin_id: &str, + plugin_url: &str, + same_plugin_exists: bool, +) -> ResultType { + let mut args = vec!["--plugin-install", plugin_id]; + if !same_plugin_exists { + args.push(&plugin_url); + } + crate::platform::elevate(args) +} + +#[cfg(target_os = "macos")] +fn elevate_install( + plugin_id: &str, + plugin_url: &str, + same_plugin_exists: bool, +) -> ResultType { + let mut args = vec!["--plugin-install", plugin_id]; + if !same_plugin_exists { + args.push(&plugin_url); + } + crate::platform::elevate(args, "RustDesk wants to install then plugin") +} + +#[inline] +#[cfg(target_os = "windows")] +fn elevate_uninstall(plugin_id: &str) -> ResultType { + crate::platform::elevate(&format!("--plugin-uninstall {}", plugin_id)) +} + +#[inline] +#[cfg(target_os = "linux")] +fn elevate_uninstall(plugin_id: &str) -> ResultType { + crate::platform::elevate(vec!["--plugin-uninstall", plugin_id]) +} + +#[inline] +#[cfg(target_os = "macos")] +fn elevate_uninstall(plugin_id: &str) -> ResultType { + crate::platform::elevate( + vec!["--plugin-uninstall", plugin_id], + "RustDesk wants to uninstall the plugin", + ) +} + +pub fn install_plugin(id: &str) -> ResultType<()> { + match PLUGIN_INFO.lock().unwrap().get(id) { + Some(plugin) => { + let mut same_plugin_exists = false; + if let Some(version) = super::plugins::get_version(id) { + if version == plugin.meta.version { + same_plugin_exists = true; + } + } + let plugin_url = format!( + "{}/plugins/{}/{}/{}_{}.zip", + plugin.source.url, + plugin.meta.id, + PLUGIN_PLATFORM, + plugin.meta.id, + plugin.meta.version + ); + let allowed_install = elevate_install(id, &plugin_url, same_plugin_exists)?; + if allowed_install && same_plugin_exists { + super::ipc::load_plugin(id)?; + super::plugins::load_plugin(id)?; + super::plugins::mark_uninstalled(id, false); + push_install_event(id, "finished"); + } + Ok(()) + } + None => { + bail!("Plugin not found: {}", id); + } + } +} + +fn get_uninstalled_plugins(uninstalled_plugin_set: &HashSet) -> ResultType> { + let plugins_dir = super::get_plugins_dir()?; + let mut plugins = Vec::new(); + if plugins_dir.exists() { + for entry in std::fs::read_dir(plugins_dir)? { + match entry { + Ok(entry) => { + let plugin_dir = entry.path(); + if plugin_dir.is_dir() { + if let Some(id) = plugin_dir.file_name().and_then(|n| n.to_str()) { + if uninstalled_plugin_set.contains(id) { + plugins.push(id.to_string()); + } + } + } + } + Err(e) => { + log::error!("Failed to read plugins dir entry, {}", e); + } + } + } + } + Ok(plugins) +} + +pub fn remove_uninstalled() -> ResultType<()> { + let mut uninstalled_plugin_set = get_uninstall_id_set()?; + for id in get_uninstalled_plugins(&uninstalled_plugin_set)?.iter() { + super::config::remove(id as _); + if let Ok(dir) = super::get_plugin_dir(id as _) { + allow_err!(remove_dir_all(dir.clone())); + if !dir.exists() { + uninstalled_plugin_set.remove(id); + } + } + } + allow_err!(update_uninstall_id_set(uninstalled_plugin_set)); + Ok(()) +} + +pub fn uninstall_plugin(id: &str, called_by_ui: bool) { + if called_by_ui { + match elevate_uninstall(id) { + Ok(true) => { + if let Err(e) = super::ipc::uninstall_plugin(id) { + log::error!("Failed to uninstall plugin '{}': {}", id, e); + push_uninstall_event(id, "failed"); + return; + } + super::plugins::unload_plugin(id); + super::plugins::mark_uninstalled(id, true); + super::config::remove(id); + push_uninstall_event(id, ""); + } + Ok(false) => { + return; + } + Err(e) => { + log::error!( + "Failed to uninstall plugin '{}', check permission error: {}", + id, + e + ); + push_uninstall_event(id, "failed"); + return; + } + } + } + + if super::is_server_running() { + super::plugins::unload_plugin(&id); + } +} + +fn push_event(id: &str, r#type: &str, msg: &str) { + let mut m = HashMap::new(); + m.insert("name", MSG_TO_UI_TYPE_PLUGIN_MANAGER); + m.insert("id", id); + m.insert(r#type, msg); + if let Ok(event) = serde_json::to_string(&m) { + let _res = flutter::push_global_event(flutter::APP_TYPE_MAIN, event.clone()); + } +} + +#[inline] +fn push_uninstall_event(id: &str, msg: &str) { + push_event(id, MSG_TO_UI_PLUGIN_MANAGER_UNINSTALL, msg); +} + +#[inline] +fn push_install_event(id: &str, msg: &str) { + push_event(id, MSG_TO_UI_PLUGIN_MANAGER_INSTALL, msg); +} + +async fn handle_conn(mut stream: crate::ipc::Connection) { + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::trace!("plugin ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match &data { + crate::ipc::Data::Plugin(super::ipc::Plugin::InstallStatus((id, status))) => { + match status { + InstallStatus::Downloading(n) => { + push_install_event(&id, &format!("downloading-{}", n)); + }, + InstallStatus::Installing => { + push_install_event(&id, "installing"); + } + InstallStatus::Finished => { + allow_err!(super::plugins::load_plugin(&id)); + allow_err!(super::ipc::load_plugin_async(id).await); + std::thread::spawn(load_plugin_list); + push_install_event(&id, "finished"); + } + InstallStatus::FailedCreating => { + push_install_event(&id, "failed-creating"); + } + InstallStatus::FailedDownloading => { + push_install_event(&id, "failed-downloading"); + } + InstallStatus::FailedInstalling => { + push_install_event(&id, "failed-installing"); + } + } + } + _ => {} + } + } + _ => { + } + } + } + } + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[tokio::main] +pub async fn start_ipc() { + match crate::ipc::new_listener(IPC_PLUGIN_POSTFIX).await { + Ok(mut incoming) => { + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + log::debug!("Got new connection"); + tokio::spawn(handle_conn(crate::ipc::Connection::new(stream))); + } + Err(err) => { + log::error!("Couldn't get plugin client: {:?}", err); + } + } + } + } + Err(err) => { + log::error!("Failed to start plugin ipc server: {}", err); + } + } +} + +pub(super) fn get_uninstall_id_set() -> ResultType> { + let uninstall_file_path = super::get_uninstall_file_path()?; + if !uninstall_file_path.exists() { + std::fs::create_dir_all(&super::get_plugins_dir()?)?; + return Ok(HashSet::new()); + } + let s = read_to_string(uninstall_file_path)?; + Ok(serde_json::from_str::>(&s)?) +} + +fn update_uninstall_id_set(set: HashSet) -> ResultType<()> { + let content = serde_json::to_string(&set)?; + let file = OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(super::get_uninstall_file_path()?)?; + let mut writer = std::io::BufWriter::new(file); + writer.write_all(content.as_bytes())?; + Ok(()) +} + +// install process +pub(super) mod install { + use super::IPC_PLUGIN_POSTFIX; + use crate::hbbs_http::create_http_client; + use crate::{ + ipc::{connect, Data}, + plugin::ipc::{InstallStatus, Plugin}, + }; + use hbb_common::{allow_err, bail, log, tokio, ResultType}; + use std::{ + fs::File, + io::{BufReader, BufWriter, Write}, + path::Path, + }; + use zip::ZipArchive; + + #[tokio::main(flavor = "current_thread")] + async fn send_install_status(id: &str, status: InstallStatus) { + allow_err!(_send_install_status(id, status).await); + } + + async fn _send_install_status(id: &str, status: InstallStatus) -> ResultType<()> { + let mut c = connect(1_000, IPC_PLUGIN_POSTFIX).await?; + c.send(&Data::Plugin(Plugin::InstallStatus(( + id.to_string(), + status, + )))) + .await?; + Ok(()) + } + + fn download_to_file(url: &str, file: File) -> ResultType<()> { + let resp = match create_http_client().get(url).send() { + Ok(resp) => resp, + Err(e) => { + bail!("get plugin from '{}', {}", url, e); + } + }; + + if !resp.status().is_success() { + bail!("get plugin from '{}', status code: {}", url, resp.status()); + } + + let mut writer = BufWriter::new(file); + writer.write_all(resp.bytes()?.as_ref())?; + Ok(()) + } + + fn download_file(id: &str, url: &str, filename: &Path) -> bool { + let file = match File::create(filename) { + Ok(f) => f, + Err(e) => { + log::error!("Failed to create plugin file: {}", e); + send_install_status(id, InstallStatus::FailedCreating); + return false; + } + }; + if let Err(e) = download_to_file(url, file) { + log::error!("Failed to download plugin '{}', {}", id, e); + send_install_status(id, InstallStatus::FailedDownloading); + return false; + } + true + } + + fn do_install_file(filename: &Path, target_dir: &Path) -> ResultType<()> { + let mut zip = ZipArchive::new(BufReader::new(File::open(filename)?))?; + for i in 0..zip.len() { + let mut file = zip.by_index(i)?; + let file_path = target_dir.join(file.name()); + if file.name().ends_with("/") { + std::fs::create_dir_all(&file_path)?; + } else { + if let Some(p) = file_path.parent() { + if !p.exists() { + std::fs::create_dir_all(&p)?; + } + } + let mut outfile = File::create(&file_path)?; + std::io::copy(&mut file, &mut outfile)?; + } + } + Ok(()) + } + + pub fn change_uninstall_plugin(id: &str, add: bool) { + match super::get_uninstall_id_set() { + Ok(mut set) => { + if add { + set.insert(id.to_string()); + } else { + set.remove(id); + } + if let Err(e) = super::update_uninstall_id_set(set) { + log::error!("Failed to write uninstall list, {}", e); + } + } + Err(e) => log::error!( + "Failed to get plugins dir, unable to read uninstall list, {}", + e + ), + } + } + + pub fn install_plugin_with_url(id: &str, url: &str) { + log::info!("Installing plugin '{}', url: {}", id, url); + let plugin_dir = match super::super::get_plugin_dir(id) { + Ok(d) => d, + Err(e) => { + send_install_status(id, InstallStatus::FailedCreating); + log::error!("Failed to get plugin dir: {}", e); + return; + } + }; + if !plugin_dir.exists() { + if let Err(e) = std::fs::create_dir_all(&plugin_dir) { + send_install_status(id, InstallStatus::FailedCreating); + log::error!("Failed to create plugin dir: {}", e); + return; + } + } + + let filename = match url.rsplit('/').next() { + Some(filename) => plugin_dir.join(filename), + None => { + send_install_status(id, InstallStatus::FailedDownloading); + log::error!("Failed to download plugin file, invalid url: {}", url); + return; + } + }; + + let filename_to_remove = filename.clone(); + let _call_on_ret = crate::common::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + if let Err(e) = std::fs::remove_file(&filename_to_remove) { + log::error!("Failed to remove plugin file: {}", e); + } + }), + }; + + // download + if !download_file(id, url, &filename) { + return; + } + + // install + send_install_status(id, InstallStatus::Installing); + if let Err(e) = do_install_file(&filename, &plugin_dir) { + log::error!("Failed to install plugin: {}", e); + send_install_status(id, InstallStatus::FailedInstalling); + return; + } + + // finished + send_install_status(id, InstallStatus::Finished); + } +} diff --git a/vendor/rustdesk/src/plugin/mod.rs b/vendor/rustdesk/src/plugin/mod.rs new file mode 100644 index 0000000..bd4b21b --- /dev/null +++ b/vendor/rustdesk/src/plugin/mod.rs @@ -0,0 +1,188 @@ +use hbb_common::{bail, libc, log, ResultType}; +#[cfg(target_os = "windows")] +use std::env; +use std::{ + ffi::{c_char, c_int, c_void, CStr}, + path::PathBuf, + ptr::null, +}; + +mod callback_ext; +mod callback_msg; +mod config; +pub mod desc; +mod errno; +pub mod ipc; +mod manager; +pub mod native; +pub mod native_handlers; +mod plog; +mod plugins; + +pub use manager::{ + install::{change_uninstall_plugin, install_plugin_with_url}, + install_plugin, load_plugin_list, remove_uninstalled, uninstall_plugin, +}; +pub use plugins::{ + handle_client_event, handle_listen_event, handle_server_event, handle_ui_event, load_plugin, + reload_plugin, sync_ui, unload_plugin, +}; + +const MSG_TO_UI_TYPE_PLUGIN_EVENT: &str = "plugin_event"; +const MSG_TO_UI_TYPE_PLUGIN_RELOAD: &str = "plugin_reload"; +const MSG_TO_UI_TYPE_PLUGIN_OPTION: &str = "plugin_option"; +const MSG_TO_UI_TYPE_PLUGIN_MANAGER: &str = "plugin_manager"; + +pub const EVENT_ON_CONN_CLIENT: &str = "on_conn_client"; +pub const EVENT_ON_CONN_SERVER: &str = "on_conn_server"; +pub const EVENT_ON_CONN_CLOSE_CLIENT: &str = "on_conn_close_client"; +pub const EVENT_ON_CONN_CLOSE_SERVER: &str = "on_conn_close_server"; + +static PLUGIN_SOURCE_LOCAL_DIR: &str = "plugins"; + +pub use config::{ManagerConfig, PeerConfig, SharedConfig}; + +/// Common plugin return. +/// +/// [Note] +/// The msg must be nullptr if code is errno::ERR_SUCCESS. +/// The msg must be freed by caller if code is not errno::ERR_SUCCESS. +#[repr(C)] +#[derive(Debug)] +pub struct PluginReturn { + pub code: c_int, + pub msg: *const c_char, +} + +impl PluginReturn { + pub fn success() -> Self { + Self { + code: errno::ERR_SUCCESS, + msg: null(), + } + } + + #[inline] + pub fn is_success(&self) -> bool { + self.code == errno::ERR_SUCCESS + } + + pub fn new(code: c_int, msg: &str) -> Self { + Self { + code, + msg: str_to_cstr_ret(msg), + } + } + + pub fn get_code_msg(&mut self, id: &str) -> (i32, String) { + if self.is_success() { + (self.code, "".to_owned()) + } else { + if self.msg.is_null() { + log::warn!( + "The message pointer from the plugin '{}' is null, but the error code is {}", + id, + self.code + ); + return (self.code, "".to_owned()); + } + let msg = cstr_to_string(self.msg).unwrap_or_default(); + free_c_ptr(self.msg as _); + self.msg = null(); + (self.code as _, msg) + } + } +} + +fn is_server_running() -> bool { + crate::common::is_server() || crate::common::is_server_running() +} + +pub fn init() { + if !is_server_running() { + std::thread::spawn(move || manager::start_ipc()); + } else { + if let Err(e) = remove_uninstalled() { + log::error!("Failed to remove plugins: {}", e); + } + } + match manager::get_uninstall_id_set() { + Ok(ids) => { + if let Err(e) = plugins::load_plugins(&ids) { + log::error!("Failed to load plugins: {}", e); + } + } + Err(e) => { + log::error!("Failed to load plugins: {}", e); + } + } +} + +#[inline] +#[cfg(target_os = "windows")] +fn get_share_dir() -> ResultType { + Ok(PathBuf::from(env::var("ProgramData")?)) +} + +#[inline] +#[cfg(target_os = "linux")] +fn get_share_dir() -> ResultType { + Ok(PathBuf::from("/usr/share")) +} + +#[inline] +#[cfg(target_os = "macos")] +fn get_share_dir() -> ResultType { + Ok(PathBuf::from("/Library/Application Support")) +} + +#[inline] +fn get_plugins_dir() -> ResultType { + Ok(get_share_dir()? + .join("RustDesk") + .join(PLUGIN_SOURCE_LOCAL_DIR)) +} + +#[inline] +fn get_plugin_dir(id: &str) -> ResultType { + Ok(get_plugins_dir()?.join(id)) +} + +#[inline] +fn get_uninstall_file_path() -> ResultType { + Ok(get_plugins_dir()?.join("uninstall_list")) +} + +#[inline] +fn cstr_to_string(cstr: *const c_char) -> ResultType { + if cstr.is_null() { + bail!("failed to convert string, the pointer is null"); + } + Ok(String::from_utf8(unsafe { + CStr::from_ptr(cstr).to_bytes().to_vec() + })?) +} + +#[inline] +fn str_to_cstr_ret(s: &str) -> *const c_char { + let mut s = s.as_bytes().to_vec(); + s.push(0); + unsafe { + let r = libc::malloc(s.len()) as *mut c_char; + libc::memcpy( + r as *mut libc::c_void, + s.as_ptr() as *const libc::c_void, + s.len(), + ); + r + } +} + +#[inline] +fn free_c_ptr(p: *mut c_void) { + if !p.is_null() { + unsafe { + libc::free(p); + } + } +} diff --git a/vendor/rustdesk/src/plugin/native.rs b/vendor/rustdesk/src/plugin/native.rs new file mode 100644 index 0000000..ce885c7 --- /dev/null +++ b/vendor/rustdesk/src/plugin/native.rs @@ -0,0 +1,40 @@ +use std::{ + ffi::{c_char, c_int, c_void}, + os::raw::c_uint, +}; + +use hbb_common::log::error; + +use super::{ + cstr_to_string, + errno::ERR_NOT_HANDLED, + native_handlers::{Callable, NATIVE_HANDLERS_REGISTRAR}, +}; +/// The native returned value from librustdesk native. +/// +/// [Note] +/// The data is owned by librustdesk. +#[repr(C)] +pub struct NativeReturnValue { + pub return_type: c_int, + pub data: *const c_void, +} + +pub(super) extern "C" fn cb_native_data( + method: *const c_char, + json: *const c_char, + raw: *const c_void, + raw_len: usize, +) -> NativeReturnValue { + let ret = match cstr_to_string(method) { + Ok(method) => NATIVE_HANDLERS_REGISTRAR.call(&method, json, raw, raw_len), + Err(err) => { + error!("cb_native_data error: {}", err); + None + } + }; + return ret.unwrap_or(NativeReturnValue { + return_type: ERR_NOT_HANDLED, + data: std::ptr::null(), + }); +} diff --git a/vendor/rustdesk/src/plugin/native_handlers/macros.rs b/vendor/rustdesk/src/plugin/native_handlers/macros.rs new file mode 100644 index 0000000..82d7e10 --- /dev/null +++ b/vendor/rustdesk/src/plugin/native_handlers/macros.rs @@ -0,0 +1,27 @@ +#[macro_export] +macro_rules! return_if_not_method { + ($call: ident, $prefix: ident) => { + if $call.starts_with($prefix) { + return None; + } + }; +} + +#[macro_export] +macro_rules! call_if_method { + ($call: ident ,$method: literal, $block: block) => { + if ($call != $method) { + $block + } + }; +} + +#[macro_export] +macro_rules! define_method_prefix { + ($prefix: literal) => { + #[inline] + fn method_prefix(&self) -> &'static str { + $prefix + } + }; +} diff --git a/vendor/rustdesk/src/plugin/native_handlers/mod.rs b/vendor/rustdesk/src/plugin/native_handlers/mod.rs new file mode 100644 index 0000000..7d590ab --- /dev/null +++ b/vendor/rustdesk/src/plugin/native_handlers/mod.rs @@ -0,0 +1,126 @@ +use std::{ + ffi::c_void, + sync::{Arc, RwLock}, + vec, +}; + +use hbb_common::libc::c_char; +use lazy_static::lazy_static; +use serde_json::Map; + +use crate::return_if_not_method; + +use self::{session::PluginNativeSessionHandler, ui::PluginNativeUIHandler}; + +use super::cstr_to_string; + +mod macros; +pub mod session; +pub mod ui; + +pub type NR = super::native::NativeReturnValue; +pub type PluginNativeHandlerRegistrar = NativeHandlerRegistrar>; + +lazy_static! { + pub static ref NATIVE_HANDLERS_REGISTRAR: Arc = + Arc::new(PluginNativeHandlerRegistrar::default()); +} + +#[derive(Clone)] +pub struct NativeHandlerRegistrar { + handlers: Arc>>, +} + +impl Default for PluginNativeHandlerRegistrar { + fn default() -> Self { + Self { + handlers: Arc::new(RwLock::new(vec![ + // Add prebuilt native handlers here. + Box::new(PluginNativeSessionHandler::default()), + Box::new(PluginNativeUIHandler::default()), + ])), + } + } +} + +pub(self) trait PluginNativeHandler { + /// The method prefix handled by this handler.s + fn method_prefix(&self) -> &'static str; + + /// Try to handle the method with the given data. + /// + /// Returns: None for the message does not be handled by this handler. + fn on_message(&self, method: &str, data: &Map) -> Option; + + /// Try to handle the method with the given data and extra void binary data. + /// + /// Returns: None for the message does not be handled by this handler. + fn on_message_raw( + &self, + method: &str, + data: &Map, + raw: *const c_void, + raw_len: usize, + ) -> Option; +} + +pub trait Callable { + fn call( + &self, + method: &String, + json: *const c_char, + raw: *const c_void, + raw_len: usize, + ) -> Option { + None + } +} + +impl Callable for T +where + T: PluginNativeHandler + Send + Sync, +{ + fn call( + &self, + method: &String, + json: *const c_char, + raw: *const c_void, + raw_len: usize, + ) -> Option { + let prefix = self.method_prefix(); + return_if_not_method!(method, prefix); + match cstr_to_string(json) { + Ok(s) => { + if let Ok(json) = serde_json::from_str(s.as_str()) { + let method_suffix = &method[prefix.len()..]; + if raw != std::ptr::null() && raw_len > 0 { + return self.on_message_raw(method_suffix, &json, raw, raw_len); + } else { + return self.on_message(method_suffix, &json); + } + } else { + return None; + } + } + Err(_) => return None, + } + } +} + +impl Callable for PluginNativeHandlerRegistrar { + fn call( + &self, + method: &String, + json: *const c_char, + raw: *const c_void, + raw_len: usize, + ) -> Option { + for handler in self.handlers.read().unwrap().iter() { + let ret = handler.call(method, json, raw, raw_len); + if ret.is_some() { + return ret; + } + } + None + } +} diff --git a/vendor/rustdesk/src/plugin/native_handlers/session.rs b/vendor/rustdesk/src/plugin/native_handlers/session.rs new file mode 100644 index 0000000..3a3f62f --- /dev/null +++ b/vendor/rustdesk/src/plugin/native_handlers/session.rs @@ -0,0 +1,219 @@ +use std::{ + collections::HashMap, + ffi::{c_char, c_void}, + ptr::addr_of_mut, + sync::{Arc, RwLock}, +}; + +use flutter_rust_bridge::StreamSink; + +use crate::{define_method_prefix, flutter_ffi::EventToUI}; + +const MSG_TO_UI_TYPE_SESSION_CREATED: &str = "session_created"; + +use super::PluginNativeHandler; + +pub type OnSessionRgbaCallback = unsafe extern "C" fn( + *const c_char, // Session ID + *mut c_void, // raw data + *mut usize, // width + *mut usize, // height, + *mut usize, // stride, + *mut scrap::ImageFormat, // ImageFormat +); + +#[derive(Default)] +/// Session related handler for librustdesk core. +pub struct PluginNativeSessionHandler { + sessions: Arc>>, + cbs: Arc>>, +} + +lazy_static::lazy_static! { + pub static ref SESSION_HANDLER: Arc = Arc::new(PluginNativeSessionHandler::default()); +} + +impl PluginNativeHandler for PluginNativeSessionHandler { + define_method_prefix!("session_"); + + fn on_message( + &self, + method: &str, + data: &serde_json::Map, + ) -> Option { + match method { + "create_session" => { + if let Some(id) = data.get("id") { + if let Some(id) = id.as_str() { + return Some(super::NR { + return_type: 1, + data: SESSION_HANDLER.create_session(id.to_string()).as_ptr() as _, + }); + } + } + } + "start_session" => { + if let Some(id) = data.get("id") { + if let Some(id) = id.as_str() { + let sessions = SESSION_HANDLER.sessions.read().unwrap(); + for session in sessions.iter() { + if session.id == id { + let round = + session.connection_round_state.lock().unwrap().new_round(); + crate::ui_session_interface::io_loop(session.clone(), round); + } + } + } + } + } + "remove_session_hook" => { + if let Some(id) = data.get("id") { + if let Some(id) = id.as_str() { + SESSION_HANDLER.remove_session_hook(id.to_string()); + return Some(super::NR { + return_type: 0, + data: std::ptr::null(), + }); + } + } + } + "remove_session" => { + if let Some(id) = data.get("id") { + if let Some(id) = id.as_str() { + SESSION_HANDLER.remove_session(id.to_owned()); + return Some(super::NR { + return_type: 0, + data: std::ptr::null(), + }); + } + } + } + _ => {} + } + None + } + + fn on_message_raw( + &self, + method: &str, + data: &serde_json::Map, + raw: *const std::ffi::c_void, + _raw_len: usize, + ) -> Option { + match method { + "add_session_hook" => { + if let Some(id) = data.get("id") { + if let Some(id) = id.as_str() { + let cb: OnSessionRgbaCallback = unsafe { std::mem::transmute(raw) }; + SESSION_HANDLER.add_session_hook(id.to_string(), cb); + return Some(super::NR { + return_type: 0, + data: std::ptr::null(), + }); + } + } + } + _ => {} + } + None + } +} + +impl PluginNativeSessionHandler { + fn create_session(&self, session_id: String) -> String { + let session = + crate::flutter::session_add(&session_id, false, false, false, "", false, "".to_owned()); + if let Ok(session) = session { + let mut sessions = self.sessions.write().unwrap(); + sessions.push(session); + // push a event to notify flutter to bind a event stream for this session. + let mut m = HashMap::new(); + m.insert("name", MSG_TO_UI_TYPE_SESSION_CREATED); + m.insert("session_id", &session_id); + // todo: APP_TYPE_DESKTOP_REMOTE is not used anymore. + // crate::flutter::APP_TYPE_DESKTOP_REMOTE + window id, is used for multi-window support. + crate::flutter::push_global_event( + crate::flutter::APP_TYPE_DESKTOP_REMOTE, + serde_json::to_string(&m).unwrap_or("".to_string()), + ); + return session_id; + } else { + return "".to_string(); + } + } + + fn add_session_hook(&self, session_id: String, cb: OnSessionRgbaCallback) { + let sessions = self.sessions.read().unwrap(); + for session in sessions.iter() { + if session.id == session_id { + self.cbs.write().unwrap().insert(session_id.to_owned(), cb); + session.ui_handler.add_session_hook( + session_id, + crate::flutter::SessionHook::OnSessionRgba(session_rgba_cb), + ); + break; + } + } + } + + fn remove_session_hook(&self, session_id: String) { + let sessions = self.sessions.read().unwrap(); + for session in sessions.iter() { + if session.id == session_id { + session.ui_handler.remove_session_hook(&session_id); + } + } + } + + fn remove_session(&self, session_id: String) { + let _ = self.cbs.write().unwrap().remove(&session_id); + let mut sessions = self.sessions.write().unwrap(); + for i in 0..sessions.len() { + if sessions[i].id == session_id { + sessions[i].close_event_stream(); + sessions[i].close(); + sessions.remove(i); + } + } + } + + #[inline] + // The callback function for rgba data + fn session_rgba_cb(&self, session_id: String, rgb: &mut scrap::ImageRgb) { + let cbs = self.cbs.read().unwrap(); + if let Some(cb) = cbs.get(&session_id) { + unsafe { + cb( + session_id.as_ptr() as _, + rgb.raw.as_mut_ptr() as _, + addr_of_mut!(rgb.w), + addr_of_mut!(rgb.h), + addr_of_mut!(rgb.stride), + addr_of_mut!(rgb.fmt), + ); + } + } + } + + #[inline] + // The callback function for rgba data + fn session_register_event_stream(&self, session_id: String, stream: StreamSink) { + let sessions = self.sessions.read().unwrap(); + for session in sessions.iter() { + if session.id == session_id { + *session.event_stream.write().unwrap() = Some(stream); + break; + } + } + } +} + +#[inline] +fn session_rgba_cb(id: String, rgb: &mut scrap::ImageRgb) { + SESSION_HANDLER.session_rgba_cb(id, rgb); +} + +#[inline] +pub fn session_register_event_stream(id: String, stream: StreamSink) { + SESSION_HANDLER.session_register_event_stream(id, stream); +} diff --git a/vendor/rustdesk/src/plugin/native_handlers/ui.rs b/vendor/rustdesk/src/plugin/native_handlers/ui.rs new file mode 100644 index 0000000..aec7fac --- /dev/null +++ b/vendor/rustdesk/src/plugin/native_handlers/ui.rs @@ -0,0 +1,143 @@ +use std::{collections::HashMap, ffi::c_void, os::raw::c_int}; + +use serde_json::json; + +use crate::{define_method_prefix, flutter::APP_TYPE_MAIN}; + +use super::PluginNativeHandler; + +#[derive(Default)] +pub struct PluginNativeUIHandler; + +/// Callback for UI interface. +/// +/// [Note] +/// We will transfer the native callback to u64 and post it to flutter. +/// The flutter thread will directly call this method. +/// +/// an example of `data` is: +/// ``` +/// { +/// "cb": 0x1234567890 +/// } +/// ``` +/// [Safety] +/// Please make sure the callback u provided is VALID, or memory or calling issues may occur to cause the program crash! +pub type OnUIReturnCallback = + extern "C" fn(return_code: c_int, data: *const c_void, data_len: u64, user_data: *const c_void); + +impl PluginNativeHandler for PluginNativeUIHandler { + define_method_prefix!("ui_"); + + fn on_message( + &self, + method: &str, + data: &serde_json::Map, + ) -> Option { + match method { + "select_peers_async" => { + if let Some(cb) = data.get("cb") { + if let Some(cb) = cb.as_u64() { + let user_data = match data.get("user_data") { + Some(user_data) => user_data.as_u64().unwrap_or(0), + None => 0, + }; + self.select_peers_async(cb, user_data); + return Some(super::NR { + return_type: 0, + data: std::ptr::null(), + }); + } + } + return Some(super::NR { + return_type: -1, + data: "missing cb field message".as_ptr() as _, + }); + } + "register_ui_entry" => { + let title; + if let Some(v) = data.get("title") { + title = v.as_str().unwrap_or(""); + } else { + title = ""; + } + if let Some(on_tap_cb) = data.get("on_tap_cb") { + if let Some(on_tap_cb) = on_tap_cb.as_u64() { + let user_data = match data.get("user_data") { + Some(user_data) => user_data.as_u64().unwrap_or(0), + None => 0, + }; + self.register_ui_entry(title, on_tap_cb, user_data); + return Some(super::NR { + return_type: 0, + data: std::ptr::null(), + }); + } + } + return Some(super::NR { + return_type: -1, + data: "missing cb field message".as_ptr() as _, + }); + } + _ => {} + } + None + } + + fn on_message_raw( + &self, + method: &str, + data: &serde_json::Map, + raw: *const std::ffi::c_void, + _raw_len: usize, + ) -> Option { + None + } +} + +impl PluginNativeUIHandler { + /// Call with method `select_peers_async` and the following json: + /// ```json + /// { + /// "cb": 0, // The function address + /// "user_data": 0 // An opaque pointer value passed to the callback. + /// } + /// ``` + /// + /// [Arguments] + /// @param cb: the function address with type [OnUIReturnCallback]. + /// @param user_data: the function will be called with this value. + fn select_peers_async(&self, cb: u64, user_data: u64) { + let mut param = HashMap::new(); + param.insert("name", json!("native_ui")); + param.insert("action", json!("select_peers")); + param.insert("cb", json!(cb)); + param.insert("user_data", json!(user_data)); + crate::flutter::push_global_event( + APP_TYPE_MAIN, + serde_json::to_string(¶m).unwrap_or("".to_string()), + ); + } + + /// Call with method `register_ui_entry` and the following json: + /// ``` + /// { + /// + /// "on_tap_cb": 0, // The function address + /// "user_data": 0, // An opaque pointer value passed to the callback. + /// "title": "entry name" + /// } + /// ``` + fn register_ui_entry(&self, title: &str, on_tap_cb: u64, user_data: u64) { + let mut param = HashMap::new(); + param.insert("name", json!("native_ui")); + param.insert("action", json!("register_ui_entry")); + param.insert("title", json!(title)); + param.insert("cb", json!(on_tap_cb)); + param.insert("user_data", json!(user_data)); + crate::flutter::push_global_event( + APP_TYPE_MAIN, + serde_json::to_string(¶m).unwrap_or("".to_string()), + ); + } +} diff --git a/vendor/rustdesk/src/plugin/plog.rs b/vendor/rustdesk/src/plugin/plog.rs new file mode 100644 index 0000000..f1e78d3 --- /dev/null +++ b/vendor/rustdesk/src/plugin/plog.rs @@ -0,0 +1,34 @@ +use hbb_common::log; +use std::ffi::c_char; + +const LOG_LEVEL_TRACE: &[u8; 6] = b"trace\0"; +const LOG_LEVEL_DEBUG: &[u8; 6] = b"debug\0"; +const LOG_LEVEL_INFO: &[u8; 5] = b"info\0"; +const LOG_LEVEL_WARN: &[u8; 5] = b"warn\0"; +const LOG_LEVEL_ERROR: &[u8; 6] = b"error\0"; + +#[inline] +fn is_level(level: *const c_char, level_bytes: &[u8]) -> bool { + level_bytes == unsafe { std::slice::from_raw_parts(level as *const u8, level_bytes.len()) } +} + +#[no_mangle] +pub(super) extern "C" fn plugin_log(level: *const c_char, msg: *const c_char) { + if level.is_null() || msg.is_null() { + return; + } + + if let Ok(msg) = super::cstr_to_string(msg) { + if is_level(level, LOG_LEVEL_TRACE) { + log::trace!("{}", msg); + } else if is_level(level, LOG_LEVEL_DEBUG) { + log::debug!("{}", msg); + } else if is_level(level, LOG_LEVEL_INFO) { + log::info!("{}", msg); + } else if is_level(level, LOG_LEVEL_WARN) { + log::warn!("{}", msg); + } else if is_level(level, LOG_LEVEL_ERROR) { + log::error!("{}", msg); + } + } +} diff --git a/vendor/rustdesk/src/plugin/plugins.rs b/vendor/rustdesk/src/plugin/plugins.rs new file mode 100644 index 0000000..bf980ee --- /dev/null +++ b/vendor/rustdesk/src/plugin/plugins.rs @@ -0,0 +1,659 @@ +use super::{desc::Desc, errno::*, *}; +#[cfg(not(debug_assertions))] +use crate::common::is_server; +use crate::flutter; +use hbb_common::{ + bail, + dlopen::symbor::Library, + lazy_static, log, + message_proto::{Message, Misc, PluginFailure, PluginRequest}, + ResultType, +}; +use serde_derive::Serialize; +use std::{ + collections::{HashMap, HashSet}, + ffi::{c_char, c_void}, + path::Path, + sync::{Arc, RwLock}, +}; + +pub const METHOD_HANDLE_STATUS: &[u8; 14] = b"handle_status\0"; +pub const METHOD_HANDLE_SIGNATURE_VERIFICATION: &[u8; 30] = b"handle_signature_verification\0"; +const METHOD_HANDLE_UI: &[u8; 10] = b"handle_ui\0"; +const METHOD_HANDLE_PEER: &[u8; 12] = b"handle_peer\0"; +pub const METHOD_HANDLE_LISTEN_EVENT: &[u8; 20] = b"handle_listen_event\0"; + +lazy_static::lazy_static! { + static ref PLUGIN_INFO: Arc>> = Default::default(); + static ref PLUGINS: Arc>> = Default::default(); +} + +pub(super) struct PluginInfo { + pub path: String, + pub uninstalled: bool, + pub desc: Desc, +} + +/// Initialize the plugins. +/// +/// data: The initialize data. +type PluginFuncInit = extern "C" fn(data: *const InitData) -> PluginReturn; +/// Reset the plugin. +/// +/// data: The initialize data. +type PluginFuncReset = extern "C" fn(data: *const InitData) -> PluginReturn; +/// Clear the plugin. +type PluginFuncClear = extern "C" fn() -> PluginReturn; +/// Get the description of the plugin. +/// Return the description. The plugin allocate memory with `libc::malloc` and return the pointer. +type PluginFuncDesc = extern "C" fn() -> *const c_char; +/// Callback to send message to peer or ui. +/// peer, target, id are utf8 strings(null terminated). +/// +/// peer: The peer id. +/// target: "peer" or "ui". +/// id: The id of this plugin. +/// content: The content. +/// len: The length of the content. +type CallbackMsg = extern "C" fn( + peer: *const c_char, + target: *const c_char, + id: *const c_char, + content: *const c_void, + len: usize, +) -> PluginReturn; +/// Callback to get the config. +/// peer, key are utf8 strings(null terminated). +/// +/// peer: The peer id. +/// id: The id of this plugin. +/// key: The key of the config. +/// +/// The returned string is utf8 string(null terminated) and must be freed by caller. +type CallbackGetConf = + extern "C" fn(peer: *const c_char, id: *const c_char, key: *const c_char) -> *const c_char; +/// Get local peer id. +/// +/// The returned string is utf8 string(null terminated) and must be freed by caller. +type CallbackGetId = extern "C" fn() -> *const c_char; +/// Callback to log. +/// +/// level, msg are utf8 strings(null terminated). +/// level: "error", "warn", "info", "debug", "trace". +/// msg: The message. +type CallbackLog = extern "C" fn(level: *const c_char, msg: *const c_char); + +/// Callback to the librustdesk core. +/// +/// method: the method name of this callback. +/// json: the json data for the parameters. The argument *must* be non-null. +/// raw: the binary data for this call, nullable. +/// raw_len: the length of this binary data, only valid when we pass raw data to `raw`. +type CallbackNative = extern "C" fn( + method: *const c_char, + json: *const c_char, + raw: *const c_void, + raw_len: usize, +) -> super::native::NativeReturnValue; +/// The main function of the plugin. +/// +/// method: The method. "handle_ui" or "handle_peer" +/// peer: The peer id. +/// args: The arguments. +/// len: The length of the arguments. +type PluginFuncCall = extern "C" fn( + method: *const c_char, + peer: *const c_char, + args: *const c_void, + len: usize, +) -> PluginReturn; +/// The main function of the plugin. +/// This function is called mainly for handling messages from the peer, +/// and then send messages back to the peer. +/// +/// method: The method. "handle_ui" or "handle_peer" +/// peer: The peer id. +/// args: The arguments. +/// len: The length of the arguments. +/// out: The output. +/// The plugin allocate memory with `libc::malloc` and return the pointer. +/// out_len: The length of the output. +type PluginFuncCallWithOutData = extern "C" fn( + method: *const c_char, + peer: *const c_char, + args: *const c_void, + len: usize, + out: *mut *mut c_void, + out_len: *mut usize, +) -> PluginReturn; + +/// The plugin callbacks. +/// msg: The callback to send message to peer or ui. +/// get_conf: The callback to get the config. +/// log: The callback to log. +#[repr(C)] +#[derive(Copy, Clone)] +struct Callbacks { + msg: CallbackMsg, + get_conf: CallbackGetConf, + get_id: CallbackGetId, + log: CallbackLog, + native: CallbackNative, +} + +#[derive(Serialize)] +#[repr(C)] +struct InitInfo { + is_server: bool, +} + +/// The plugin initialize data. +/// version: The version of the plugin, can't be nullptr. +/// local_peer_id: The local peer id, can't be nullptr. +/// cbs: The callbacks. +#[repr(C)] +struct InitData { + version: *const c_char, + info: *const c_char, + cbs: Callbacks, +} + +impl Drop for InitData { + fn drop(&mut self) { + free_c_ptr(self.version as _); + free_c_ptr(self.info as _); + } +} + +macro_rules! make_plugin { + ($($field:ident : $tp:ty),+) => { + #[allow(dead_code)] + pub struct Plugin { + _lib: Library, + id: Option, + path: String, + $($field: $tp),+ + } + + impl Plugin { + fn new(path: &str) -> ResultType { + let lib = match Library::open(path) { + Ok(lib) => lib, + Err(e) => { + bail!("Failed to load library {}, {}", path, e); + } + }; + + $(let $field = match unsafe { lib.symbol::<$tp>(stringify!($field)) } { + Ok(m) => { + *m + }, + Err(e) => { + bail!("Failed to load {} func {}, {}", path, stringify!($field), e); + } + } + ;)+ + + Ok(Self { + _lib: lib, + id: None, + path: path.to_string(), + $( $field ),+ + }) + } + + fn desc(&self) -> ResultType { + let desc_ret = (self.desc)(); + let desc = Desc::from_cstr(desc_ret); + free_c_ptr(desc_ret as _); + desc + } + + fn init(&self, data: &InitData, path: &str) -> ResultType<()> { + let mut init_ret = (self.init)(data as _); + if !init_ret.is_success() { + let (code, msg) = init_ret.get_code_msg(path); + bail!( + "Failed to init plugin {}, code: {}, msg: {}", + path, + code, + msg + ); + } + Ok(()) + } + + fn clear(&self, id: &str) { + let mut clear_ret = (self.clear)(); + if !clear_ret.is_success() { + let (code, msg) = clear_ret.get_code_msg(id); + log::error!( + "Failed to clear plugin {}, code: {}, msg: {}", + id, + code, + msg + ); + } + } + } + + impl Drop for Plugin { + fn drop(&mut self) { + let id = self.id.as_ref().unwrap_or(&self.path); + self.clear(id); + } + } + } +} + +make_plugin!( + init: PluginFuncInit, + reset: PluginFuncReset, + clear: PluginFuncClear, + desc: PluginFuncDesc, + call: PluginFuncCall, + call_with_out_data: PluginFuncCallWithOutData +); + +#[derive(Serialize)] +pub struct MsgListenEvent { + pub event: String, +} + +#[cfg(target_os = "windows")] +const DYLIB_SUFFIX: &str = ".dll"; +#[cfg(target_os = "linux")] +const DYLIB_SUFFIX: &str = ".so"; +#[cfg(target_os = "macos")] +const DYLIB_SUFFIX: &str = ".dylib"; + +pub(super) fn load_plugins(uninstalled_ids: &HashSet) -> ResultType<()> { + let plugins_dir = super::get_plugins_dir()?; + if !plugins_dir.exists() { + std::fs::create_dir_all(&plugins_dir)?; + } else { + for entry in std::fs::read_dir(plugins_dir)? { + match entry { + Ok(entry) => { + let plugin_dir = entry.path(); + if plugin_dir.is_dir() { + if let Some(plugin_id) = plugin_dir.file_name().and_then(|f| f.to_str()) { + if uninstalled_ids.contains(plugin_id) { + log::debug!( + "Ignore loading '{}' as it should be uninstalled", + plugin_id + ); + continue; + } + load_plugin_dir(&plugin_dir); + } + } + } + Err(e) => { + log::error!("Failed to read plugins dir entry, {}", e); + } + } + } + } + Ok(()) +} + +fn load_plugin_dir(dir: &Path) { + log::debug!("Begin load plugin dir: {}", dir.display()); + if let Ok(rd) = std::fs::read_dir(dir) { + for entry in rd { + match entry { + Ok(entry) => { + let path = entry.path(); + if path.is_file() { + let filename = entry.file_name(); + let filename = filename.to_str().unwrap_or(""); + if filename.starts_with("plugin_") && filename.ends_with(DYLIB_SUFFIX) { + if let Some(path) = path.to_str() { + if let Err(e) = load_plugin_path(path) { + log::error!("Failed to load plugin {}, {}", filename, e); + } + } + } + } + } + Err(e) => { + log::error!( + "Failed to read '{}' dir entry, {}", + dir.file_name().and_then(|f| f.to_str()).unwrap_or(""), + e + ); + } + } + } + } +} + +pub fn unload_plugin(id: &str) { + log::info!("Plugin {} unloaded", id); + PLUGINS.write().unwrap().remove(id); +} + +pub(super) fn mark_uninstalled(id: &str, uninstalled: bool) { + log::info!("Plugin {} uninstall", id); + PLUGIN_INFO + .write() + .unwrap() + .get_mut(id) + .map(|info| info.uninstalled = uninstalled); +} + +pub fn reload_plugin(id: &str) -> ResultType<()> { + let path = match PLUGIN_INFO.read().unwrap().get(id) { + Some(plugin) => plugin.path.clone(), + None => bail!("Plugin {} not found", id), + }; + unload_plugin(id); + load_plugin_path(&path) +} + +fn load_plugin_path(path: &str) -> ResultType<()> { + log::info!("Begin load plugin {}", path); + + let plugin = Plugin::new(path)?; + let desc = plugin.desc()?; + + // to-do validate plugin + // to-do check the plugin id (make sure it does not use another plugin's id) + + let id = desc.meta().id.clone(); + let plugin_info = PluginInfo { + path: path.to_string(), + uninstalled: false, + desc: desc.clone(), + }; + PLUGIN_INFO.write().unwrap().insert(id.clone(), plugin_info); + + let init_info = serde_json::to_string(&InitInfo { + is_server: super::is_server_running(), + })?; + let init_data = InitData { + version: str_to_cstr_ret(crate::VERSION), + info: str_to_cstr_ret(&init_info) as _, + cbs: Callbacks { + msg: callback_msg::cb_msg, + get_conf: config::cb_get_conf, + get_id: config::cb_get_local_peer_id, + log: super::plog::plugin_log, + native: super::native::cb_native_data, + }, + }; + // If do not load the plugin when init failed, the ui will not show the installed plugin. + if let Err(e) = plugin.init(&init_data, path) { + log::error!("Failed to init plugin '{}', {}", desc.meta().id, e); + } + + if super::is_server_running() { + super::config::ManagerConfig::add_plugin(&desc.meta().id)?; + } + + // update ui + // Ui may be not ready now, so we need to update again once ui is ready. + reload_ui(&desc, None); + + // add plugins + PLUGINS.write().unwrap().insert(id.clone(), plugin); + + log::info!("Plugin {} loaded, {}", id, path); + Ok(()) +} + +pub fn sync_ui(sync_to: String) { + for plugin in PLUGIN_INFO.read().unwrap().values() { + reload_ui(&plugin.desc, Some(&sync_to)); + } +} + +#[inline] +pub fn load_plugin(id: &str) -> ResultType<()> { + load_plugin_dir(&super::get_plugin_dir(id)?); + Ok(()) +} + +#[inline] +fn handle_event(method: &[u8], id: &str, peer: &str, event: &[u8]) -> ResultType<()> { + let mut peer: String = peer.to_owned(); + peer.push('\0'); + plugin_call(id, method, &peer, event) +} + +pub fn plugin_call(id: &str, method: &[u8], peer: &str, event: &[u8]) -> ResultType<()> { + let mut ret = plugin_call_get_return(id, method, peer, event)?; + if ret.is_success() { + Ok(()) + } else { + let (code, msg) = ret.get_code_msg(id); + bail!( + "Failed to handle plugin event, id: {}, method: {}, code: {}, msg: {}", + id, + std::string::String::from_utf8(method.to_vec()).unwrap_or_default(), + code, + msg + ); + } +} + +#[inline] +pub fn plugin_call_get_return( + id: &str, + method: &[u8], + peer: &str, + event: &[u8], +) -> ResultType { + match PLUGINS.read().unwrap().get(id) { + Some(plugin) => Ok((plugin.call)( + method.as_ptr() as _, + peer.as_ptr() as _, + event.as_ptr() as _, + event.len(), + )), + None => bail!("Plugin {} not found", id), + } +} + +#[inline] +pub fn handle_ui_event(id: &str, peer: &str, event: &[u8]) -> ResultType<()> { + handle_event(METHOD_HANDLE_UI, id, peer, event) +} + +#[inline] +pub fn handle_server_event(id: &str, peer: &str, event: &[u8]) -> ResultType<()> { + handle_event(METHOD_HANDLE_PEER, id, peer, event) +} + +fn _handle_listen_event(event: String, peer: String) { + let mut plugins = Vec::new(); + for info in PLUGIN_INFO.read().unwrap().values() { + if info.desc.listen_events().contains(&event.to_string()) { + plugins.push(info.desc.meta().id.clone()); + } + } + + if plugins.is_empty() { + return; + } + + if let Ok(evt) = serde_json::to_string(&MsgListenEvent { + event: event.clone(), + }) { + let mut evt_bytes = evt.as_bytes().to_vec(); + evt_bytes.push(0); + let mut peer: String = peer.to_owned(); + peer.push('\0'); + for id in plugins { + match PLUGINS.read().unwrap().get(&id) { + Some(plugin) => { + let mut ret = (plugin.call)( + METHOD_HANDLE_LISTEN_EVENT.as_ptr() as _, + peer.as_ptr() as _, + evt_bytes.as_ptr() as _, + evt_bytes.len(), + ); + if !ret.is_success() { + let (code, msg) = ret.get_code_msg(&id); + log::error!( + "Failed to handle plugin listen event, id: {}, event: {}, code: {}, msg: {}", + id, + event, + code, + msg + ); + } + } + None => { + log::error!("Plugin {} not found when handle_listen_event", id); + } + } + } + } +} + +#[inline] +pub fn handle_listen_event(event: String, peer: String) { + std::thread::spawn(|| _handle_listen_event(event, peer)); +} + +#[inline] +pub fn handle_client_event(id: &str, peer: &str, event: &[u8]) -> Message { + let mut peer: String = peer.to_owned(); + peer.push('\0'); + match PLUGINS.read().unwrap().get(id) { + Some(plugin) => { + let mut out = std::ptr::null_mut(); + let mut out_len: usize = 0; + let mut ret = (plugin.call_with_out_data)( + METHOD_HANDLE_PEER.as_ptr() as _, + peer.as_ptr() as _, + event.as_ptr() as _, + event.len(), + &mut out as _, + &mut out_len as _, + ); + if ret.is_success() { + let msg = make_plugin_request(id, out, out_len); + free_c_ptr(out as _); + msg + } else { + let (code, msg) = ret.get_code_msg(id); + if code > ERR_RUSTDESK_HANDLE_BASE && code < ERR_PLUGIN_HANDLE_BASE { + log::debug!( + "Plugin {} failed to handle client event, code: {}, msg: {}", + id, + code, + msg + ); + let name = match PLUGIN_INFO.read().unwrap().get(id) { + Some(plugin) => &plugin.desc.meta().name, + None => "???", + } + .to_owned(); + match code { + ERR_CALL_NOT_SUPPORTED_METHOD => { + make_plugin_failure(id, &name, "Plugin method is not supported") + } + ERR_CALL_INVALID_ARGS => { + make_plugin_failure(id, &name, "Plugin arguments is invalid") + } + _ => make_plugin_failure(id, &name, &msg), + } + } else { + log::error!( + "Plugin {} failed to handle client event, code: {}, msg: {}", + id, + code, + msg + ); + let msg = make_plugin_request(id, out, out_len); + free_c_ptr(out as _); + msg + } + } + } + None => make_plugin_failure(id, "", "Plugin not found"), + } +} + +fn make_plugin_request(id: &str, content: *const c_void, len: usize) -> Message { + let mut misc = Misc::new(); + misc.set_plugin_request(PluginRequest { + id: id.to_owned(), + content: unsafe { std::slice::from_raw_parts(content as *const u8, len) } + .clone() + .into(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out +} + +fn make_plugin_failure(id: &str, name: &str, msg: &str) -> Message { + let mut misc = Misc::new(); + misc.set_plugin_failure(PluginFailure { + id: id.to_owned(), + name: name.to_owned(), + msg: msg.to_owned(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out +} + +fn reload_ui(desc: &Desc, sync_to: Option<&str>) { + for (location, ui) in desc.location().ui.iter() { + if let Ok(ui) = serde_json::to_string(&ui) { + let make_event = |ui: &str| { + let mut m = HashMap::new(); + m.insert("name", MSG_TO_UI_TYPE_PLUGIN_RELOAD); + m.insert("id", &desc.meta().id); + m.insert("location", &location); + // Do not depend on the "location" and plugin desc on the ui side. + // Send the ui field to ensure the ui is valid. + m.insert("ui", ui); + serde_json::to_string(&m).unwrap_or("".to_owned()) + }; + match sync_to { + Some(channel) => { + let _res = flutter::push_global_event(channel, make_event(&ui)); + } + None => { + let v: Vec<&str> = location.split('|').collect(); + // The first element is the "client" or "host". + // The second element is the "main", "remote", "cm", "file transfer", "port forward". + if v.len() >= 2 { + let available_channels = flutter::get_global_event_channels(); + if available_channels.contains(&v[1]) { + let _res = flutter::push_global_event(v[1], make_event(&ui)); + } + } + } + } + } + } +} + +pub(super) fn get_plugin_infos() -> Arc>> { + PLUGIN_INFO.clone() +} + +pub(super) fn get_desc_conf(id: &str) -> Option { + PLUGIN_INFO + .read() + .unwrap() + .get(id) + .map(|info| info.desc.config().clone()) +} + +pub(super) fn get_version(id: &str) -> Option { + PLUGIN_INFO + .read() + .unwrap() + .get(id) + .map(|info| info.desc.meta().version.clone()) +} diff --git a/vendor/rustdesk/src/port_forward.rs b/vendor/rustdesk/src/port_forward.rs new file mode 100644 index 0000000..61d6bfd --- /dev/null +++ b/vendor/rustdesk/src/port_forward.rs @@ -0,0 +1,220 @@ +use std::sync::{Arc, RwLock}; + +use crate::client::*; +use hbb_common::{ + allow_err, bail, + config::READ_TIMEOUT, + futures::{SinkExt, StreamExt}, + log, + message_proto::*, + protobuf::Message as _, + rendezvous_proto::ConnType, + tcp, timeout, + tokio::{self, net::TcpStream, sync::mpsc}, + tokio_util::codec::{BytesCodec, Framed}, + ResultType, Stream, +}; + +fn run_rdp(port: u16) { + std::process::Command::new("cmdkey") + .arg("/delete:localhost") + .output() + .ok(); + let username = std::env::var("rdp_username").unwrap_or_default(); + let password = std::env::var("rdp_password").unwrap_or_default(); + if !username.is_empty() || !password.is_empty() { + let mut args = vec!["/generic:localhost".to_owned()]; + if !username.is_empty() { + args.push(format!("/user:{}", username)); + } + if !password.is_empty() { + args.push(format!("/pass:{}", password)); + } + println!("{:?}", args); + std::process::Command::new("cmdkey") + .args(&args) + .output() + .ok(); + } + std::process::Command::new("mstsc") + .arg(format!("/v:localhost:{}", port)) + .spawn() + .ok(); +} + +pub async fn listen( + id: String, + password: String, + port: i32, + interface: impl Interface, + ui_receiver: mpsc::UnboundedReceiver, + key: &str, + token: &str, + lc: Arc>, + remote_host: String, + remote_port: i32, +) -> ResultType<()> { + let listener = tcp::new_listener(format!("127.0.0.1:{}", port), true).await?; + let addr = listener.local_addr()?; + log::info!("listening on port {:?}", addr); + let is_rdp = port == 0; + if is_rdp { + run_rdp(addr.port()); + } + let mut ui_receiver = ui_receiver; + loop { + tokio::select! { + Ok((forward, addr)) = listener.accept() => { + log::info!("new connection from {:?}", addr); + lc.write().unwrap().port_forward = (remote_host.clone(), remote_port); + let id = id.clone(); + let password = password.clone(); + let mut forward = Framed::new(forward, BytesCodec::new()); + match connect_and_login(&id, &password, &mut ui_receiver, interface.clone(), &mut forward, key, token, is_rdp).await { + Ok(Some(stream)) => { + let interface = interface.clone(); + tokio::spawn(async move { + if let Err(err) = run_forward(forward, stream).await { + interface.msgbox("error", "Error", &err.to_string(), ""); + } + log::info!("connection from {:?} closed", addr); + }); + } + Err(err) => { + interface.on_establish_connection_error(err.to_string()); + } + _ => {} + } + } + d = ui_receiver.recv() => { + match d { + Some(Data::Close) => { + break; + } + Some(Data::NewRDP) => { + println!("receive run_rdp from ui_receiver"); + run_rdp(addr.port()); + } + _ => {} + } + } + } + } + Ok(()) +} + +async fn connect_and_login( + id: &str, + password: &str, + ui_receiver: &mut mpsc::UnboundedReceiver, + interface: impl Interface, + forward: &mut Framed, + key: &str, + token: &str, + is_rdp: bool, +) -> ResultType> { + let conn_type = if is_rdp { + ConnType::RDP + } else { + ConnType::PORT_FORWARD + }; + let ((mut stream, direct, _pk, _kcp, _stream_type), (feedback, rendezvous_server)) = + Client::start(id, key, token, conn_type, interface.clone()).await?; + interface.update_direct(Some(direct)); + let mut buffer = Vec::new(); + let mut received = false; + + let _keep_it = hc_connection(feedback, rendezvous_server, token).await; + + loop { + tokio::select! { + res = timeout(READ_TIMEOUT, stream.next()) => match res { + Err(_) => { + bail!("Timeout"); + } + Ok(Some(Ok(bytes))) => { + if !received { + received = true; + interface.update_received(true); + } + let msg_in = Message::parse_from_bytes(&bytes)?; + match msg_in.union { + Some(message::Union::Hash(hash)) => { + interface.handle_hash(password, hash, &mut stream).await; + } + Some(message::Union::LoginResponse(lr)) => match lr.union { + Some(login_response::Union::Error(err)) => { + if !interface.handle_login_error(&err) { + return Ok(None); + } + } + Some(login_response::Union::PeerInfo(pi)) => { + interface.handle_peer_info(pi); + break; + } + _ => {} + } + Some(message::Union::TestDelay(t)) => { + interface.handle_test_delay(t, &mut stream).await; + } + _ => {} + } + } + Ok(Some(Err(err))) => { + bail!("Connection closed: {}", err); + } + _ => { + bail!("Reset by the peer"); + } + }, + d = ui_receiver.recv() => { + match d { + Some(Data::Login((os_username, os_password, password, remember))) => { + interface.handle_login_from_ui(os_username, os_password, password, remember, &mut stream).await; + } + Some(Data::Message(msg)) => { + allow_err!(stream.send(&msg).await); + } + _ => {} + } + }, + res = forward.next() => { + if let Some(Ok(bytes)) = res { + buffer.extend(bytes); + } else { + return Ok(None); + } + }, + } + } + stream.set_raw(); + if !buffer.is_empty() { + allow_err!(stream.send_bytes(buffer.into()).await); + } + Ok(Some(stream)) +} + +async fn run_forward(forward: Framed, stream: Stream) -> ResultType<()> { + log::info!("new port forwarding connection started"); + let mut forward = forward; + let mut stream = stream; + loop { + tokio::select! { + res = forward.next() => { + if let Some(Ok(bytes)) = res { + allow_err!(stream.send_bytes(bytes.into()).await); + } else { + break; + } + }, + res = stream.next() => { + if let Some(Ok(bytes)) = res { + allow_err!(forward.send(bytes).await); + } else { + break; + } + }, + } + } + Ok(()) +} diff --git a/vendor/rustdesk/src/privacy_mode.rs b/vendor/rustdesk/src/privacy_mode.rs new file mode 100644 index 0000000..234004d --- /dev/null +++ b/vendor/rustdesk/src/privacy_mode.rs @@ -0,0 +1,431 @@ +use crate::ui_interface::get_option; +#[cfg(windows)] +use crate::{ + display_service, + ipc::{connect, Data}, + platform::is_installed, +}; +#[cfg(windows)] +use hbb_common::tokio; +use hbb_common::{anyhow::anyhow, bail, lazy_static, tokio::sync::oneshot, ResultType}; +use serde_derive::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +#[cfg(windows)] +pub mod win_exclude_from_capture; +#[cfg(windows)] +mod win_input; +#[cfg(windows)] +pub mod win_mag; +#[cfg(windows)] +pub mod win_topmost_window; + +#[cfg(target_os = "macos")] +pub mod macos; + +#[cfg(windows)] +mod win_virtual_display; +#[cfg(windows)] +pub use win_virtual_display::restore_reg_connectivity; + +pub const INVALID_PRIVACY_MODE_CONN_ID: i32 = 0; +pub const OCCUPIED: &'static str = "Privacy occupied by another one."; +pub const TURN_OFF_OTHER_ID: &'static str = + "Failed to turn off privacy mode that belongs to someone else."; +pub const NO_PHYSICAL_DISPLAYS: &'static str = "no_need_privacy_mode_no_physical_displays_tip"; + +pub const PRIVACY_MODE_IMPL_WIN_MAG: &str = "privacy_mode_impl_mag"; +pub const PRIVACY_MODE_IMPL_WIN_EXCLUDE_FROM_CAPTURE: &str = + "privacy_mode_impl_exclude_from_capture"; +pub const PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY: &str = "privacy_mode_impl_virtual_display"; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "t", content = "c")] +pub enum PrivacyModeState { + OffSucceeded, + OffByPeer, + OffUnknown, +} + +pub trait PrivacyMode: Sync + Send { + fn is_async_privacy_mode(&self) -> bool; + + fn init(&self) -> ResultType<()>; + fn clear(&mut self); + fn turn_on_privacy(&mut self, conn_id: i32) -> ResultType; + fn turn_off_privacy(&mut self, conn_id: i32, state: Option) + -> ResultType<()>; + + fn pre_conn_id(&self) -> i32; + + fn get_impl_key(&self) -> &str; + + #[inline] + fn check_on_conn_id(&self, conn_id: i32) -> ResultType { + let pre_conn_id = self.pre_conn_id(); + if pre_conn_id == conn_id { + return Ok(true); + } + if pre_conn_id != INVALID_PRIVACY_MODE_CONN_ID { + bail!(OCCUPIED); + } + Ok(false) + } + + #[inline] + fn check_off_conn_id(&self, conn_id: i32) -> ResultType<()> { + let pre_conn_id = self.pre_conn_id(); + if pre_conn_id != INVALID_PRIVACY_MODE_CONN_ID + && conn_id != INVALID_PRIVACY_MODE_CONN_ID + && pre_conn_id != conn_id + { + bail!(TURN_OFF_OTHER_ID) + } + Ok(()) + } +} + +lazy_static::lazy_static! { + pub static ref DEFAULT_PRIVACY_MODE_IMPL: String = { + #[cfg(windows)] + { + if win_exclude_from_capture::is_supported() { + PRIVACY_MODE_IMPL_WIN_EXCLUDE_FROM_CAPTURE + } else { + if display_service::is_privacy_mode_mag_supported() { + PRIVACY_MODE_IMPL_WIN_MAG + } else { + if is_installed() { + PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY + } else { + "" + } + } + }.to_owned() + } + #[cfg(not(windows))] + { + #[cfg(target_os = "macos")] + { + macos::PRIVACY_MODE_IMPL.to_owned() + } + #[cfg(not(target_os = "macos"))] + { + "".to_owned() + } + } + }; + + static ref PRIVACY_MODE: Arc>>> = { + let mut cur_impl = get_option("privacy-mode-impl-key".to_owned()); + if !get_supported_privacy_mode_impl().iter().any(|(k, _)| k == &cur_impl) { + cur_impl = DEFAULT_PRIVACY_MODE_IMPL.to_owned(); + } + + let privacy_mode = match PRIVACY_MODE_CREATOR.lock().unwrap().get(&(&cur_impl as &str)) { + Some(creator) => Some(creator(&cur_impl)), + None => None, + }; + Arc::new(Mutex::new(privacy_mode)) + }; +} + +pub type PrivacyModeCreator = fn(impl_key: &str) -> Box; +lazy_static::lazy_static! { + static ref PRIVACY_MODE_CREATOR: Arc>> = { + #[cfg(not(windows))] + let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new(); + #[cfg(target_os = "macos")] + { + map.insert(macos::PRIVACY_MODE_IMPL, |impl_key: &str| { + Box::new(macos::PrivacyModeImpl::new(impl_key)) + }); + } + #[cfg(windows)] + let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new(); + #[cfg(windows)] + { + if win_exclude_from_capture::is_supported() { + map.insert(win_exclude_from_capture::PRIVACY_MODE_IMPL, |impl_key: &str| { + Box::new(win_exclude_from_capture::PrivacyModeImpl::new(impl_key)) + }); + } else { + map.insert(win_mag::PRIVACY_MODE_IMPL, |impl_key: &str| { + Box::new(win_mag::PrivacyModeImpl::new(impl_key)) + }); + } + + map.insert(win_virtual_display::PRIVACY_MODE_IMPL, |impl_key: &str| { + Box::new(win_virtual_display::PrivacyModeImpl::new(impl_key)) + }); + } + Arc::new(Mutex::new(map)) + }; +} + +#[inline] +pub fn init() -> Option> { + Some(PRIVACY_MODE.lock().unwrap().as_ref()?.init()) +} + +#[inline] +pub fn clear() -> Option<()> { + Some(PRIVACY_MODE.lock().unwrap().as_mut()?.clear()) +} + +#[inline] +pub fn switch(impl_key: &str) { + let mut privacy_mode_lock = PRIVACY_MODE.lock().unwrap(); + if let Some(privacy_mode) = privacy_mode_lock.as_ref() { + if privacy_mode.get_impl_key() == impl_key { + return; + } + } + + if let Some(creator) = PRIVACY_MODE_CREATOR.lock().unwrap().get(impl_key) { + *privacy_mode_lock = Some(creator(impl_key)); + } +} + +fn get_supported_impl(impl_key: &str) -> String { + let supported_impls = get_supported_privacy_mode_impl(); + if supported_impls.iter().any(|(k, _)| k == &impl_key) { + return impl_key.to_owned(); + }; + // TODO: Is it a good idea to use fallback here? Because user do not know the fallback. + // fallback + let mut cur_impl = get_option("privacy-mode-impl-key".to_owned()); + if !get_supported_privacy_mode_impl() + .iter() + .any(|(k, _)| k == &cur_impl) + { + // fallback + cur_impl = DEFAULT_PRIVACY_MODE_IMPL.to_owned(); + } + cur_impl +} + +pub async fn turn_on_privacy(impl_key: &str, conn_id: i32) -> Option> { + if is_async_privacy_mode() { + turn_on_privacy_async(impl_key.to_string(), conn_id).await + } else { + turn_on_privacy_sync(impl_key, conn_id) + } +} + +#[inline] +fn is_async_privacy_mode() -> bool { + PRIVACY_MODE + .lock() + .unwrap() + .as_ref() + .map_or(false, |m| m.is_async_privacy_mode()) +} + +#[inline] +async fn turn_on_privacy_async(impl_key: String, conn_id: i32) -> Option> { + let (tx, rx) = oneshot::channel(); + std::thread::spawn(move || { + let res = turn_on_privacy_sync(&impl_key, conn_id); + let _ = tx.send(res); + }); + // Wait at most 7.5 seconds for the result. + // Because it may take a long time to turn on the privacy mode with amyuni idd. + // Some laptops may take time to plug in a virtual display. + match hbb_common::timeout(7500, rx).await { + Ok(res) => match res { + Ok(res) => res, + Err(e) => Some(Err(anyhow!(e.to_string()))), + }, + Err(e) => Some(Err(anyhow!(e.to_string()))), + } +} + +fn turn_on_privacy_sync(impl_key: &str, conn_id: i32) -> Option> { + // Check if privacy mode is already on or occupied by another one + let mut privacy_mode_lock = PRIVACY_MODE.lock().unwrap(); + + // Check or switch privacy mode implementation + let impl_key = get_supported_impl(impl_key); + + let mut cur_impl_key = "".to_string(); + if let Some(privacy_mode) = privacy_mode_lock.as_ref() { + cur_impl_key = privacy_mode.get_impl_key().to_string(); + let check_on_conn_id = privacy_mode.check_on_conn_id(conn_id); + match check_on_conn_id.as_ref() { + Ok(true) => { + if cur_impl_key == impl_key { + // Same peer, same implementation. + return Some(Ok(true)); + } else { + // Same peer, switch to new implementation. + } + } + Err(_) => return Some(check_on_conn_id), + _ => {} + } + } + + if cur_impl_key != impl_key { + if let Some(creator) = PRIVACY_MODE_CREATOR + .lock() + .unwrap() + .get(&(&impl_key as &str)) + { + if let Some(privacy_mode) = privacy_mode_lock.as_mut() { + privacy_mode.clear(); + } + + *privacy_mode_lock = Some(creator(&impl_key)); + } else { + return Some(Err(anyhow!("Unsupported privacy mode: {}", impl_key))); + } + } + + // turn on privacy mode + Some(privacy_mode_lock.as_mut()?.turn_on_privacy(conn_id)) +} + +#[inline] +pub fn turn_off_privacy(conn_id: i32, state: Option) -> Option> { + Some( + PRIVACY_MODE + .lock() + .unwrap() + .as_mut()? + .turn_off_privacy(conn_id, state), + ) +} + +#[inline] +pub fn check_on_conn_id(conn_id: i32) -> Option> { + Some( + PRIVACY_MODE + .lock() + .unwrap() + .as_ref()? + .check_on_conn_id(conn_id), + ) +} + +#[cfg(windows)] +#[tokio::main(flavor = "current_thread")] +async fn set_privacy_mode_state( + conn_id: i32, + state: PrivacyModeState, + impl_key: String, + ms_timeout: u64, +) -> ResultType<()> { + let mut c = connect(ms_timeout, "_cm").await?; + c.send(&Data::PrivacyModeState((conn_id, state, impl_key))) + .await +} + +pub fn get_supported_privacy_mode_impl() -> Vec<(&'static str, &'static str)> { + #[cfg(target_os = "windows")] + { + let mut vec_impls = Vec::new(); + + if win_exclude_from_capture::is_supported() { + vec_impls.push(( + PRIVACY_MODE_IMPL_WIN_EXCLUDE_FROM_CAPTURE, + "privacy_mode_impl_mag_tip", + )); + } else { + if display_service::is_privacy_mode_mag_supported() { + vec_impls.push((PRIVACY_MODE_IMPL_WIN_MAG, "privacy_mode_impl_mag_tip")); + } + } + + if is_installed() && crate::platform::windows::is_self_service_running() { + vec_impls.push(( + PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY, + "privacy_mode_impl_virtual_display_tip", + )); + } + + vec_impls + } + #[cfg(target_os = "macos")] + { + // No translation is intended for privacy_mode_impl_macos_tip as it is a + // placeholder for macOS specific privacy mode implementation which currently + // doesn't provide multiple modes like Windows does. + vec![(macos::PRIVACY_MODE_IMPL, "privacy_mode_impl_macos_tip")] + } + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + { + Vec::new() + } +} + +#[inline] +pub fn get_cur_impl_key() -> Option { + PRIVACY_MODE + .lock() + .unwrap() + .as_ref() + .map(|pm| pm.get_impl_key().to_owned()) +} + +#[inline] +pub fn is_current_privacy_mode_impl(impl_key: &str) -> bool { + PRIVACY_MODE + .lock() + .unwrap() + .as_ref() + .map(|pm| pm.get_impl_key() == impl_key) + .unwrap_or(false) +} + +#[inline] +#[cfg(not(windows))] +pub fn check_privacy_mode_err( + _privacy_mode_id: i32, + _display_idx: usize, + _timeout_millis: u64, +) -> String { + "".to_owned() +} + +#[inline] +#[cfg(windows)] +pub fn check_privacy_mode_err( + privacy_mode_id: i32, + display_idx: usize, + timeout_millis: u64, +) -> String { + // win magnifier implementation requires a test of creating a capturer. + if is_current_privacy_mode_impl(PRIVACY_MODE_IMPL_WIN_MAG) { + crate::video_service::test_create_capturer(privacy_mode_id, display_idx, timeout_millis) + } else { + "".to_owned() + } +} + +#[inline] +pub fn is_privacy_mode_supported() -> bool { + !DEFAULT_PRIVACY_MODE_IMPL.is_empty() +} + +#[inline] +pub fn get_privacy_mode_conn_id() -> Option { + PRIVACY_MODE + .lock() + .unwrap() + .as_ref() + .map(|pm| pm.pre_conn_id()) +} + +#[inline] +pub fn is_in_privacy_mode() -> bool { + PRIVACY_MODE + .lock() + .unwrap() + .as_ref() + .map(|pm| pm.pre_conn_id() != INVALID_PRIVACY_MODE_CONN_ID) + .unwrap_or(false) +} diff --git a/vendor/rustdesk/src/privacy_mode/macos.rs b/vendor/rustdesk/src/privacy_mode/macos.rs new file mode 100644 index 0000000..e6ea11e --- /dev/null +++ b/vendor/rustdesk/src/privacy_mode/macos.rs @@ -0,0 +1,81 @@ +use super::{PrivacyMode, PrivacyModeState}; +use hbb_common::{anyhow::anyhow, ResultType}; + +extern "C" { + fn MacSetPrivacyMode(on: bool) -> bool; +} + +pub const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_macos"; + +pub struct PrivacyModeImpl { + impl_key: String, + conn_id: i32, +} + +impl PrivacyModeImpl { + pub fn new(impl_key: &str) -> Self { + Self { + impl_key: impl_key.to_owned(), + conn_id: 0, + } + } +} + +impl PrivacyMode for PrivacyModeImpl { + fn is_async_privacy_mode(&self) -> bool { + false + } + + fn init(&self) -> ResultType<()> { + Ok(()) + } + + fn clear(&mut self) { + unsafe { + MacSetPrivacyMode(false); + } + self.conn_id = 0; + } + + fn turn_on_privacy(&mut self, conn_id: i32) -> ResultType { + if self.check_on_conn_id(conn_id)? { + return Ok(true); + } + let success = unsafe { MacSetPrivacyMode(true) }; + if !success { + return Err(anyhow!("Failed to turn on privacy mode")); + } + self.conn_id = conn_id; + Ok(true) + } + + fn turn_off_privacy(&mut self, conn_id: i32, _state: Option) -> ResultType<()> { + // Note: The `_state` parameter is intentionally ignored on macOS. + // On Windows, it's used to notify the connection manager about privacy mode state changes + // (see win_topmost_window.rs). macOS currently has a simpler single-mode implementation + // without the need for such cross-component state synchronization. + self.check_off_conn_id(conn_id)?; + let success = unsafe { MacSetPrivacyMode(false) }; + if !success { + return Err(anyhow!("Failed to turn off privacy mode")); + } + self.conn_id = 0; + Ok(()) + } + + fn pre_conn_id(&self) -> i32 { + self.conn_id + } + + fn get_impl_key(&self) -> &str { + &self.impl_key + } +} + +impl Drop for PrivacyModeImpl { + fn drop(&mut self) { + // Use the same cleanup logic as other code paths to keep conn_id consistent + // and ensure all cleanup is centralized in one place. + self.clear(); + } +} diff --git a/vendor/rustdesk/src/privacy_mode/win_exclude_from_capture.rs b/vendor/rustdesk/src/privacy_mode/win_exclude_from_capture.rs new file mode 100644 index 0000000..7d68001 --- /dev/null +++ b/vendor/rustdesk/src/privacy_mode/win_exclude_from_capture.rs @@ -0,0 +1,11 @@ +use hbb_common::platform::windows::is_windows_version_or_greater; + +pub use super::win_topmost_window::PrivacyModeImpl; + +pub(super) const PRIVACY_MODE_IMPL: &str = super::PRIVACY_MODE_IMPL_WIN_EXCLUDE_FROM_CAPTURE; + +pub(super) fn is_supported() -> bool { + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowdisplayaffinity + // https://en.wikipedia.org/wiki/Windows_10_version_history + is_windows_version_or_greater(10, 0, 19041, 0, 0) +} diff --git a/vendor/rustdesk/src/privacy_mode/win_input.rs b/vendor/rustdesk/src/privacy_mode/win_input.rs new file mode 100644 index 0000000..29c87dc --- /dev/null +++ b/vendor/rustdesk/src/privacy_mode/win_input.rs @@ -0,0 +1,276 @@ +use hbb_common::{allow_err, bail, lazy_static, log, ResultType}; +use std::{ + io::Error, + sync::{ + mpsc::{channel, Sender}, + Mutex, + }, +}; +use winapi::{ + ctypes::c_int, + shared::{ + minwindef::{DWORD, FALSE, HMODULE, LOBYTE, LPARAM, LRESULT, UINT, WPARAM}, + ntdef::NULL, + windef::{HHOOK, POINT}, + }, + um::{libloaderapi::GetModuleHandleExA, processthreadsapi::GetCurrentThreadId, winuser::*}, +}; + +const GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT: u32 = 2; +const GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS: u32 = 4; + +const WM_USER_EXIT_HOOK: u32 = WM_USER + 1; + +lazy_static::lazy_static! { + static ref CUR_HOOK_THREAD_ID: Mutex = Mutex::new(0); +} + +fn do_hook(tx: Sender) -> ResultType<(HHOOK, HHOOK)> { + let invalid_ret = (0 as HHOOK, 0 as HHOOK); + + let mut cur_hook_thread_id = CUR_HOOK_THREAD_ID.lock().unwrap(); + if *cur_hook_thread_id != 0 { + // unreachable! + tx.send("Already hooked".to_owned())?; + return Ok(invalid_ret); + } + + unsafe { + let mut hm_keyboard = 0 as HMODULE; + if 0 == GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + DefWindowProcA as _, + &mut hm_keyboard as _, + ) { + tx.send(format!( + "Failed to GetModuleHandleExA, error: {}", + Error::last_os_error() + ))?; + return Ok(invalid_ret); + } + let mut hm_mouse = 0 as HMODULE; + if 0 == GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + DefWindowProcA as _, + &mut hm_mouse as _, + ) { + tx.send(format!( + "Failed to GetModuleHandleExA, error: {}", + Error::last_os_error() + ))?; + return Ok(invalid_ret); + } + + let hook_keyboard = SetWindowsHookExA( + WH_KEYBOARD_LL, + Some(privacy_mode_hook_keyboard), + hm_keyboard, + 0, + ); + if hook_keyboard.is_null() { + tx.send(format!( + " SetWindowsHookExA keyboard, error {}", + Error::last_os_error() + ))?; + return Ok(invalid_ret); + } + + let hook_mouse = SetWindowsHookExA(WH_MOUSE_LL, Some(privacy_mode_hook_mouse), hm_mouse, 0); + if hook_mouse.is_null() { + if FALSE == UnhookWindowsHookEx(hook_keyboard) { + // Fatal error + log::error!( + " UnhookWindowsHookEx keyboard, error {}", + Error::last_os_error() + ); + } + tx.send(format!( + " SetWindowsHookExA mouse, error {}", + Error::last_os_error() + ))?; + return Ok(invalid_ret); + } + + *cur_hook_thread_id = GetCurrentThreadId(); + tx.send("".to_owned())?; + return Ok((hook_keyboard, hook_mouse)); + } +} + +pub fn hook() -> ResultType<()> { + let (tx, rx) = channel(); + std::thread::spawn(move || { + let hook_keyboard; + let hook_mouse; + unsafe { + match do_hook(tx.clone()) { + Ok(hooks) => { + hook_keyboard = hooks.0; + hook_mouse = hooks.1; + } + Err(e) => { + // Fatal error + allow_err!(tx.send(format!("Unexpected err when hook {}", e))); + return; + } + } + if hook_keyboard.is_null() { + return; + } + + let mut msg = MSG { + hwnd: NULL as _, + message: 0 as _, + wParam: 0 as _, + lParam: 0 as _, + time: 0 as _, + pt: POINT { + x: 0 as _, + y: 0 as _, + }, + }; + while FALSE != GetMessageA(&mut msg, NULL as _, 0, 0) { + if msg.message == WM_USER_EXIT_HOOK { + break; + } + + TranslateMessage(&msg); + DispatchMessageA(&msg); + } + + if FALSE == UnhookWindowsHookEx(hook_keyboard as _) { + // Fatal error + log::error!( + "Failed UnhookWindowsHookEx keyboard, error {}", + Error::last_os_error() + ); + } + + if FALSE == UnhookWindowsHookEx(hook_mouse as _) { + // Fatal error + log::error!( + "Failed UnhookWindowsHookEx mouse, error {}", + Error::last_os_error() + ); + } + + *CUR_HOOK_THREAD_ID.lock().unwrap() = 0; + } + }); + + match rx.recv() { + Ok(msg) => { + if msg == "" { + Ok(()) + } else { + bail!(msg) + } + } + Err(e) => { + bail!("Failed to wait hook result {}", e) + } + } +} + +pub fn unhook() -> ResultType<()> { + unsafe { + let cur_hook_thread_id = CUR_HOOK_THREAD_ID.lock().unwrap(); + if *cur_hook_thread_id != 0 { + if FALSE == PostThreadMessageA(*cur_hook_thread_id, WM_USER_EXIT_HOOK, 0, 0) { + bail!( + "Failed to post message to exit hook, error {}", + Error::last_os_error() + ); + } + } + } + Ok(()) +} + +#[no_mangle] +pub extern "system" fn privacy_mode_hook_keyboard( + code: c_int, + w_param: WPARAM, + l_param: LPARAM, +) -> LRESULT { + if code < 0 { + unsafe { + return CallNextHookEx(NULL as _, code, w_param, l_param); + } + } + + let ks = l_param as PKBDLLHOOKSTRUCT; + let w_param2 = w_param as UINT; + + unsafe { + if (*ks).dwExtraInfo != enigo::ENIGO_INPUT_EXTRA_VALUE { + // Disable alt key. Alt + Tab will switch windows. + if (*ks).flags & LLKHF_ALTDOWN == LLKHF_ALTDOWN { + return 1; + } + + match w_param2 { + WM_KEYDOWN => { + // Disable all keys other than P and Ctrl. + if ![80, 162, 163].contains(&(*ks).vkCode) { + return 1; + } + + // NOTE: GetKeyboardState may not work well... + + // Check if Ctrl + P is pressed + let cltr_down = (GetKeyState(VK_CONTROL) as u16) & (0x8000 as u16) > 0; + let key = LOBYTE((*ks).vkCode as _); + if cltr_down && (key == 'p' as u8 || key == 'P' as u8) { + // Ctrl + P is pressed, turn off privacy mode + if let Some(Err(e)) = super::turn_off_privacy( + super::INVALID_PRIVACY_MODE_CONN_ID, + Some(super::PrivacyModeState::OffByPeer), + ) { + log::error!("Failed to off_privacy {}", e); + } + } + } + WM_KEYUP => { + log::trace!("WM_KEYUP {}", (*ks).vkCode); + } + _ => { + log::trace!("KEYBOARD OTHER {} {}", w_param2, (*ks).vkCode); + } + } + } + } + unsafe { CallNextHookEx(NULL as _, code, w_param, l_param) } +} + +#[no_mangle] +pub extern "system" fn privacy_mode_hook_mouse( + code: c_int, + w_param: WPARAM, + l_param: LPARAM, +) -> LRESULT { + if code < 0 { + unsafe { + return CallNextHookEx(NULL as _, code, w_param, l_param); + } + } + + let ms = l_param as PMOUSEHOOKSTRUCT; + unsafe { + if (*ms).dwExtraInfo != enigo::ENIGO_INPUT_EXTRA_VALUE { + return 1; + } + } + unsafe { CallNextHookEx(NULL as _, code, w_param, l_param) } +} + +mod test { + #[test] + fn privacy_hook() { + //use super::*; + + // privacy_hook::hook().unwrap(); + // std::thread::sleep(std::time::Duration::from_millis(50)); + // privacy_hook::unhook().unwrap(); + } +} diff --git a/vendor/rustdesk/src/privacy_mode/win_mag.rs b/vendor/rustdesk/src/privacy_mode/win_mag.rs new file mode 100644 index 0000000..6235c24 --- /dev/null +++ b/vendor/rustdesk/src/privacy_mode/win_mag.rs @@ -0,0 +1,57 @@ +use super::win_topmost_window::PRIVACY_WINDOW_NAME; +use hbb_common::{bail, log, ResultType}; +use std::time::Instant; + +pub use super::win_topmost_window::PrivacyModeImpl; + +pub(super) const PRIVACY_MODE_IMPL: &str = super::PRIVACY_MODE_IMPL_WIN_MAG; + +pub fn create_capturer( + privacy_mode_id: i32, + origin: (i32, i32), + width: usize, + height: usize, +) -> ResultType> { + if !super::is_current_privacy_mode_impl(PRIVACY_MODE_IMPL) { + return Ok(None); + } + + match scrap::CapturerMag::new(origin, width, height) { + Ok(mut c1) => { + let mut ok = false; + let check_begin = Instant::now(); + while check_begin.elapsed().as_secs() < 5 { + match c1.exclude("", PRIVACY_WINDOW_NAME) { + Ok(false) => { + ok = false; + std::thread::sleep(std::time::Duration::from_millis(500)); + } + Err(e) => { + bail!( + "Failed to exclude privacy window {} - {}, err: {}", + "", + PRIVACY_WINDOW_NAME, + e + ); + } + _ => { + ok = true; + break; + } + } + } + if !ok { + bail!( + "Failed to exclude privacy window {} - {} ", + "", + PRIVACY_WINDOW_NAME + ); + } + log::debug!("Create magnifier capture for {}", privacy_mode_id); + Ok(Some(c1)) + } + Err(e) => { + bail!(format!("Failed to create magnifier capture {}", e)); + } + } +} diff --git a/vendor/rustdesk/src/privacy_mode/win_topmost_window.rs b/vendor/rustdesk/src/privacy_mode/win_topmost_window.rs new file mode 100644 index 0000000..a7f80a0 --- /dev/null +++ b/vendor/rustdesk/src/privacy_mode/win_topmost_window.rs @@ -0,0 +1,383 @@ +use super::{PrivacyMode, INVALID_PRIVACY_MODE_CONN_ID}; +use crate::{platform::windows::get_user_token, privacy_mode::PrivacyModeState}; +use hbb_common::{allow_err, bail, log, ResultType}; +use std::{ + ffi::CString, + io::Error, + time::{Duration, Instant}, +}; +use winapi::{ + shared::{ + minwindef::FALSE, + ntdef::{HANDLE, NULL}, + windef::HWND, + }, + um::{ + handleapi::CloseHandle, + libloaderapi::{GetModuleHandleA, GetProcAddress}, + memoryapi::{VirtualAllocEx, WriteProcessMemory}, + processthreadsapi::{ + CreateProcessAsUserW, QueueUserAPC, ResumeThread, TerminateProcess, + PROCESS_INFORMATION, STARTUPINFOW, + }, + winbase::{WTSGetActiveConsoleSessionId, CREATE_SUSPENDED, DETACHED_PROCESS}, + winnt::{MEM_COMMIT, PAGE_READWRITE}, + winuser::*, + }, +}; + +pub(super) const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_mag"; + +pub const ORIGIN_PROCESS_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe"; +pub const WIN_TOPMOST_INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe"; +pub const INJECTED_PROCESS_EXE: &'static str = WIN_TOPMOST_INJECTED_PROCESS_EXE; +pub(super) const PRIVACY_WINDOW_NAME: &'static str = "RustDeskPrivacyWindow"; + +struct WindowHandlers { + hthread: u64, + hprocess: u64, +} + +impl Drop for WindowHandlers { + fn drop(&mut self) { + self.reset(); + } +} + +impl WindowHandlers { + fn reset(&mut self) { + unsafe { + if self.hprocess != 0 { + let _res = TerminateProcess(self.hprocess as _, 0); + CloseHandle(self.hprocess as _); + } + self.hprocess = 0; + if self.hthread != 0 { + CloseHandle(self.hthread as _); + } + self.hthread = 0; + } + } + + fn is_default(&self) -> bool { + self.hthread == 0 && self.hprocess == 0 + } +} + +pub struct PrivacyModeImpl { + impl_key: String, + conn_id: i32, + handlers: WindowHandlers, + hwnd: u64, +} + +impl PrivacyMode for PrivacyModeImpl { + fn is_async_privacy_mode(&self) -> bool { + false + } + + fn init(&self) -> ResultType<()> { + Ok(()) + } + + fn clear(&mut self) { + allow_err!(self.turn_off_privacy(self.conn_id, None)); + } + + fn turn_on_privacy(&mut self, conn_id: i32) -> ResultType { + if self.check_on_conn_id(conn_id)? { + log::debug!("Privacy mode of conn {} is already on", conn_id); + return Ok(true); + } + + let exe_file = std::env::current_exe()?; + if let Some(cur_dir) = exe_file.parent() { + if !cur_dir.join("WindowInjection.dll").exists() { + return Ok(false); + } + } else { + bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ); + } + + if self.handlers.is_default() { + log::info!("turn_on_privacy, dll not found when started, try start"); + self.start()?; + std::thread::sleep(std::time::Duration::from_millis(1_000)); + } + + let hwnd = wait_find_privacy_hwnd(0)?; + if hwnd.is_null() { + bail!("No privacy window created"); + } + super::win_input::hook()?; + unsafe { + ShowWindow(hwnd as _, SW_SHOW); + } + self.conn_id = conn_id; + self.hwnd = hwnd as _; + Ok(true) + } + + fn turn_off_privacy( + &mut self, + conn_id: i32, + state: Option, + ) -> ResultType<()> { + self.check_off_conn_id(conn_id)?; + super::win_input::unhook()?; + + unsafe { + let hwnd = wait_find_privacy_hwnd(0)?; + if !hwnd.is_null() { + ShowWindow(hwnd, SW_HIDE); + } + } + + if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID { + if let Some(state) = state { + allow_err!(super::set_privacy_mode_state( + conn_id, + state, + PRIVACY_MODE_IMPL.to_string(), + 1_000 + )); + } + self.conn_id = INVALID_PRIVACY_MODE_CONN_ID.to_owned(); + } + + Ok(()) + } + + #[inline] + fn pre_conn_id(&self) -> i32 { + self.conn_id + } + + #[inline] + fn get_impl_key(&self) -> &str { + &self.impl_key + } +} + +impl PrivacyModeImpl { + pub fn new(impl_key: &str) -> Self { + Self { + impl_key: impl_key.to_owned(), + conn_id: INVALID_PRIVACY_MODE_CONN_ID, + handlers: WindowHandlers { + hthread: 0, + hprocess: 0, + }, + hwnd: 0, + } + } + + #[inline] + pub fn get_hwnd(&self) -> u64 { + self.hwnd + } + + pub fn start(&mut self) -> ResultType<()> { + if self.handlers.hprocess != 0 { + return Ok(()); + } + + log::info!("Start privacy mode window broker, check_update_broker_process"); + if let Err(e) = crate::platform::windows::check_update_broker_process() { + log::warn!( + "Failed to check update broker process. Privacy mode may not work properly. {}", + e + ); + } + + let exe_file = std::env::current_exe()?; + let Some(cur_dir) = exe_file.parent() else { + bail!("Cannot get parent of current exe file"); + }; + + let dll_file = cur_dir.join("WindowInjection.dll"); + if !dll_file.exists() { + bail!( + "Failed to find required file {}", + dll_file.to_string_lossy().as_ref() + ); + } + + let hwnd = wait_find_privacy_hwnd(1_000)?; + if !hwnd.is_null() { + log::info!("Privacy window is ready"); + return Ok(()); + } + + // let cmdline = cur_dir.join("MiniBroker.exe").to_string_lossy().to_string(); + let cmdline = cur_dir + .join(INJECTED_PROCESS_EXE) + .to_string_lossy() + .to_string(); + + unsafe { + let cmd_utf16: Vec = cmdline.encode_utf16().chain(Some(0).into_iter()).collect(); + + let mut start_info = STARTUPINFOW { + cb: 0, + lpReserved: NULL as _, + lpDesktop: NULL as _, + lpTitle: NULL as _, + dwX: 0, + dwY: 0, + dwXSize: 0, + dwYSize: 0, + dwXCountChars: 0, + dwYCountChars: 0, + dwFillAttribute: 0, + dwFlags: 0, + wShowWindow: 0, + cbReserved2: 0, + lpReserved2: NULL as _, + hStdInput: NULL as _, + hStdOutput: NULL as _, + hStdError: NULL as _, + }; + let mut proc_info = PROCESS_INFORMATION { + hProcess: NULL as _, + hThread: NULL as _, + dwProcessId: 0, + dwThreadId: 0, + }; + + let session_id = WTSGetActiveConsoleSessionId(); + let token = get_user_token(session_id, true); + if token.is_null() { + bail!("Failed to get token of current user"); + } + + let create_res = CreateProcessAsUserW( + token, + NULL as _, + cmd_utf16.as_ptr() as _, + NULL as _, + NULL as _, + FALSE, + CREATE_SUSPENDED | DETACHED_PROCESS, + NULL, + NULL as _, + &mut start_info, + &mut proc_info, + ); + CloseHandle(token); + if 0 == create_res { + bail!( + "Failed to create privacy window process {}, error {}", + cmdline, + Error::last_os_error() + ); + }; + + inject_dll( + proc_info.hProcess, + proc_info.hThread, + dll_file.to_string_lossy().as_ref(), + )?; + + if 0xffffffff == ResumeThread(proc_info.hThread) { + // CloseHandle + CloseHandle(proc_info.hThread); + CloseHandle(proc_info.hProcess); + + bail!( + "Failed to create privacy window process, error {}", + Error::last_os_error() + ); + } + + self.handlers.hthread = proc_info.hThread as _; + self.handlers.hprocess = proc_info.hProcess as _; + + let hwnd = wait_find_privacy_hwnd(1_000)?; + if hwnd.is_null() { + bail!("Failed to get hwnd after started"); + } + } + + Ok(()) + } + + #[inline] + pub fn stop(&mut self) { + self.handlers.reset(); + } +} + +impl Drop for PrivacyModeImpl { + fn drop(&mut self) { + if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID { + allow_err!(self.turn_off_privacy(self.conn_id, None)); + } + } +} + +unsafe fn inject_dll<'a>(hproc: HANDLE, hthread: HANDLE, dll_file: &'a str) -> ResultType<()> { + let dll_file_utf16: Vec = dll_file.encode_utf16().chain(Some(0).into_iter()).collect(); + + let buf = VirtualAllocEx( + hproc, + NULL as _, + dll_file_utf16.len() * 2, + MEM_COMMIT, + PAGE_READWRITE, + ); + if buf.is_null() { + bail!("Failed VirtualAllocEx"); + } + + let mut written: usize = 0; + if 0 == WriteProcessMemory( + hproc, + buf, + dll_file_utf16.as_ptr() as _, + dll_file_utf16.len() * 2, + &mut written, + ) { + bail!("Failed WriteProcessMemory"); + } + + let kernel32_modulename = CString::new("kernel32")?; + let hmodule = GetModuleHandleA(kernel32_modulename.as_ptr() as _); + if hmodule.is_null() { + bail!("Failed GetModuleHandleA"); + } + + let load_librarya_name = CString::new("LoadLibraryW")?; + let load_librarya = GetProcAddress(hmodule, load_librarya_name.as_ptr() as _); + if load_librarya.is_null() { + bail!("Failed GetProcAddress of LoadLibraryW"); + } + + if 0 == QueueUserAPC(Some(std::mem::transmute(load_librarya)), hthread, buf as _) { + bail!("Failed QueueUserAPC"); + } + + Ok(()) +} + +pub(super) fn wait_find_privacy_hwnd(msecs: u128) -> ResultType { + let tm_begin = Instant::now(); + let wndname = CString::new(PRIVACY_WINDOW_NAME)?; + loop { + unsafe { + let hwnd = FindWindowA(NULL as _, wndname.as_ptr() as _); + if !hwnd.is_null() { + return Ok(hwnd); + } + } + + if msecs == 0 || tm_begin.elapsed().as_millis() > msecs { + return Ok(NULL as _); + } + + std::thread::sleep(Duration::from_millis(100)); + } +} diff --git a/vendor/rustdesk/src/privacy_mode/win_virtual_display.rs b/vendor/rustdesk/src/privacy_mode/win_virtual_display.rs new file mode 100644 index 0000000..f521cba --- /dev/null +++ b/vendor/rustdesk/src/privacy_mode/win_virtual_display.rs @@ -0,0 +1,586 @@ +use super::{PrivacyMode, PrivacyModeState, INVALID_PRIVACY_MODE_CONN_ID, NO_PHYSICAL_DISPLAYS}; +use crate::{platform::windows::reg_display_settings, virtual_display_manager}; +use hbb_common::{allow_err, bail, config::Config, log, ResultType}; +use std::{ + io::Error, + ops::{Deref, DerefMut}, + thread, + time::Duration, +}; +use virtual_display::MonitorMode; +use winapi::{ + shared::{ + minwindef::{DWORD, FALSE}, + ntdef::{NULL, WCHAR}, + }, + um::{ + wingdi::{ + DEVMODEW, DISPLAY_DEVICEW, DISPLAY_DEVICE_ACTIVE, DISPLAY_DEVICE_ATTACHED_TO_DESKTOP, + DISPLAY_DEVICE_MIRRORING_DRIVER, DISPLAY_DEVICE_PRIMARY_DEVICE, DM_POSITION, + }, + winuser::{ + ChangeDisplaySettingsExW, EnumDisplayDevicesW, EnumDisplaySettingsExW, + EnumDisplaySettingsW, CDS_NORESET, CDS_RESET, CDS_SET_PRIMARY, CDS_UPDATEREGISTRY, + DISP_CHANGE_FAILED, DISP_CHANGE_SUCCESSFUL, EDD_GET_DEVICE_INTERFACE_NAME, + ENUM_CURRENT_SETTINGS, ENUM_REGISTRY_SETTINGS, + }, + }, +}; + +pub(super) const PRIVACY_MODE_IMPL: &str = super::PRIVACY_MODE_IMPL_WIN_VIRTUAL_DISPLAY; + +const CONFIG_KEY_REG_RECOVERY: &str = "reg_recovery"; + +struct Display { + dm: DEVMODEW, + name: [WCHAR; 32], + primary: bool, +} + +pub struct PrivacyModeImpl { + impl_key: String, + conn_id: i32, + displays: Vec, + virtual_displays: Vec, + virtual_displays_added: Vec, +} + +struct TurnOnGuard<'a> { + privacy_mode: &'a mut PrivacyModeImpl, + succeeded: bool, +} + +impl<'a> Deref for TurnOnGuard<'a> { + type Target = PrivacyModeImpl; + + fn deref(&self) -> &Self::Target { + self.privacy_mode + } +} + +impl<'a> DerefMut for TurnOnGuard<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.privacy_mode + } +} + +impl<'a> Drop for TurnOnGuard<'a> { + fn drop(&mut self) { + if !self.succeeded { + self.privacy_mode + .turn_off_privacy(INVALID_PRIVACY_MODE_CONN_ID, None) + .ok(); + } + } +} + +impl PrivacyModeImpl { + pub fn new(impl_key: &str) -> Self { + Self { + impl_key: impl_key.to_owned(), + conn_id: INVALID_PRIVACY_MODE_CONN_ID, + displays: Vec::new(), + virtual_displays: Vec::new(), + virtual_displays_added: Vec::new(), + } + } + + // mainly from https://github.com/rustdesk-org/rustdesk/blob/44c3a52ca8502cf53b58b59db130611778d34dbe/libs/scrap/src/dxgi/mod.rs#L365 + fn set_displays(&mut self) { + self.displays.clear(); + self.virtual_displays.clear(); + + let mut i: DWORD = 0; + loop { + #[allow(invalid_value)] + let mut dd: DISPLAY_DEVICEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + dd.cb = std::mem::size_of::() as _; + let ok = unsafe { EnumDisplayDevicesW(std::ptr::null(), i, &mut dd as _, 0) }; + if ok == FALSE { + break; + } + i += 1; + if 0 == (dd.StateFlags & DISPLAY_DEVICE_ACTIVE) + || (dd.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER) > 0 + { + continue; + } + #[allow(invalid_value)] + let mut dm: DEVMODEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + dm.dmSize = std::mem::size_of::() as _; + dm.dmDriverExtra = 0; + unsafe { + if FALSE + == EnumDisplaySettingsExW( + dd.DeviceName.as_ptr(), + ENUM_CURRENT_SETTINGS, + &mut dm as _, + 0, + ) + { + if FALSE + == EnumDisplaySettingsExW( + dd.DeviceName.as_ptr(), + ENUM_REGISTRY_SETTINGS, + &mut dm as _, + 0, + ) + { + continue; + } + } + } + + let primary = (dd.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) > 0; + let display = Display { + dm, + name: dd.DeviceName, + primary, + }; + + let ds = virtual_display_manager::get_cur_device_string(); + if let Ok(s) = String::from_utf16(&dd.DeviceString) { + if s.len() >= ds.len() && &s[..ds.len()] == ds { + self.virtual_displays.push(display); + continue; + } + } + self.displays.push(display); + } + } + + fn restore_plug_out_monitor(&mut self) { + let _ = virtual_display_manager::plug_out_monitor_indices( + &self.virtual_displays_added, + true, + false, + ); + self.virtual_displays_added.clear(); + } + + #[inline] + fn change_display_settings_ex_err_msg(rc: i32) -> String { + if rc != DISP_CHANGE_FAILED { + format!("ret: {}", rc) + } else { + format!( + "ret: {}, last error: {:?}", + rc, + std::io::Error::last_os_error() + ) + } + } + + fn set_primary_display(&mut self) -> ResultType { + // Multiple virtual displays with different origins are tested. + let display = &self.virtual_displays[0]; + let display_name = std::string::String::from_utf16(&display.name)?; + + #[allow(invalid_value)] + let mut new_primary_dm: DEVMODEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + new_primary_dm.dmSize = std::mem::size_of::() as _; + new_primary_dm.dmDriverExtra = 0; + unsafe { + if FALSE + == EnumDisplaySettingsW( + display.name.as_ptr(), + ENUM_CURRENT_SETTINGS, + &mut new_primary_dm, + ) + { + bail!( + "Failed EnumDisplaySettingsW, device name: {:?}, error: {}", + std::string::String::from_utf16(&display.name), + Error::last_os_error() + ); + } + + // Windows 24H2 requires the virtual display to be set first. + // No idea why, maybe the same issue: https://developercommunity.visualstudio.com/t/Windows-11-Enterprise-24H2-using-WinApi/10851936?sort=newest + let flags = CDS_UPDATEREGISTRY | CDS_NORESET; + let offx = new_primary_dm.u1.s2().dmPosition.x; + let offy = new_primary_dm.u1.s2().dmPosition.y; + new_primary_dm.u1.s2_mut().dmPosition.x = 0; + new_primary_dm.u1.s2_mut().dmPosition.y = 0; + new_primary_dm.dmFields |= DM_POSITION; + let rc = ChangeDisplaySettingsExW( + display.name.as_ptr(), + &mut new_primary_dm, + NULL as _, + flags | CDS_SET_PRIMARY, + NULL, + ); + if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); + log::error!( + "Failed ChangeDisplaySettingsEx, the virtual display, {}", + &err + ); + bail!("Failed ChangeDisplaySettingsEx, {}", err); + } + + let mut i: DWORD = 0; + loop { + #[allow(invalid_value)] + let mut dd: DISPLAY_DEVICEW = std::mem::MaybeUninit::uninit().assume_init(); + dd.cb = std::mem::size_of::() as _; + if FALSE + == EnumDisplayDevicesW(NULL as _, i, &mut dd, EDD_GET_DEVICE_INTERFACE_NAME) + { + break; + } + i += 1; + if (dd.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) == 0 { + continue; + } + // Skip the virtual display. + if dd.DeviceName == display.name { + continue; + } + + #[allow(invalid_value)] + let mut dm: DEVMODEW = std::mem::MaybeUninit::uninit().assume_init(); + dm.dmSize = std::mem::size_of::() as _; + dm.dmDriverExtra = 0; + if FALSE + == EnumDisplaySettingsW(dd.DeviceName.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) + { + bail!( + "Failed EnumDisplaySettingsW, device name: {:?}, error: {}", + std::string::String::from_utf16(&dd.DeviceName), + Error::last_os_error() + ); + } + + dm.u1.s2_mut().dmPosition.x -= offx; + dm.u1.s2_mut().dmPosition.y -= offy; + dm.dmFields |= DM_POSITION; + let rc = ChangeDisplaySettingsExW( + dd.DeviceName.as_ptr(), + &mut dm, + NULL as _, + flags, + NULL, + ); + if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); + log::error!( + "Failed ChangeDisplaySettingsEx, device name: {:?}, flags: {}, {}", + std::string::String::from_utf16(&dd.DeviceName), + flags, + &err + ); + bail!("Failed ChangeDisplaySettingsEx, {}", err); + } + + // If we want to set dpi, the following references may be helpful. + // And setting dpi should be called after changing the display settings. + // https://stackoverflow.com/questions/35233182/how-can-i-change-windows-10-display-scaling-programmatically-using-c-sharp + // https://github.com/lihas/windows-DPI-scaling-sample/blob/master/DPIHelper/DpiHelper.cpp + // + // But the official API does not provide a way to get/set dpi. + // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ne-wingdi-displayconfig_device_info_type + // https://github.com/lihas/windows-DPI-scaling-sample/blob/738ac18b7a7ce2d8fdc157eb825de9cb5eee0448/DPIHelper/DpiHelper.h#L37 + } + } + + Ok(display_name) + } + + // NOTE: We can't detect if the other virtual displays are physical displays or not. + // We can only use `DeviceString` == `virtual_display_manager::get_cur_device_string()` to detect if the display is a virtual display. + // The other virtual displays can't be restored after exiting the privacy mode on Windows 24H2. + fn disable_physical_displays(&self) -> ResultType<()> { + for display in &self.displays { + let mut dm = display.dm.clone(); + unsafe { + dm.u1.s2_mut().dmPosition.x = 10000; + dm.u1.s2_mut().dmPosition.y = 10000; + dm.dmPelsHeight = 0; + dm.dmPelsWidth = 0; + let flags = CDS_UPDATEREGISTRY | CDS_NORESET; + let rc = ChangeDisplaySettingsExW( + display.name.as_ptr(), + &mut dm, + NULL as _, + flags, + NULL as _, + ); + if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); + log::error!( + "Failed ChangeDisplaySettingsEx, device name: {:?}, flags: {}, {}", + std::string::String::from_utf16(&display.name), + flags, + &err + ); + bail!("Failed ChangeDisplaySettingsEx, {}", err); + } + } + } + Ok(()) + } + + #[inline] + fn default_display_modes() -> Vec { + vec![MonitorMode { + width: 1920, + height: 1080, + sync: 60, + }] + } + + // This function will wait at most 6 seconds for the virtual displays to be ready. + // It's ok to wait, because: + // 1. A new thread is created to handle the async privacy mode. + // 2. The user is usually not in a hurry to turn on the privacy mode. + pub fn ensure_virtual_display(&mut self, is_async_mode: bool) -> ResultType<()> { + if self.virtual_displays.is_empty() { + let displays = + virtual_display_manager::plug_in_peer_request(vec![Self::default_display_modes()])?; + if is_async_mode { + thread::sleep(Duration::from_secs(1)); + } + self.set_displays(); + // No physical displays, no need to use the privacy mode. + if self.displays.is_empty() { + virtual_display_manager::plug_out_monitor_indices(&displays, false, false)?; + bail!(NO_PHYSICAL_DISPLAYS); + } + + if is_async_mode { + let now = std::time::Instant::now(); + while self.virtual_displays.is_empty() + && now.elapsed() < Duration::from_millis(5000) + { + thread::sleep(Duration::from_millis(500)); + self.set_displays(); + } + } + + self.virtual_displays_added.extend(displays); + } + + Ok(()) + } + + #[inline] + fn commit_change_display(flags: DWORD) -> ResultType<()> { + unsafe { + // use winapi::{ + // shared::windef::HDESK, + // um::{ + // processthreadsapi::GetCurrentThreadId, + // winnt::MAXIMUM_ALLOWED, + // winuser::{CloseDesktop, GetThreadDesktop, OpenInputDesktop, SetThreadDesktop}, + // }, + // }; + // let mut desk_input: HDESK = NULL as _; + // let desk_current: HDESK = GetThreadDesktop(GetCurrentThreadId()); + // if !desk_current.is_null() { + // desk_input = OpenInputDesktop(0, FALSE, MAXIMUM_ALLOWED); + // if desk_input.is_null() { + // SetThreadDesktop(desk_input); + // } + // } + + let rc = ChangeDisplaySettingsExW(NULL as _, NULL as _, NULL as _, flags, NULL as _); + if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); + bail!("Failed ChangeDisplaySettingsEx, {}", err); + } + + // if !desk_current.is_null() { + // SetThreadDesktop(desk_current); + // } + // if !desk_input.is_null() { + // CloseDesktop(desk_input); + // } + } + Ok(()) + } + + fn restore(&mut self) { + Self::restore_displays(&self.displays); + Self::restore_displays(&self.virtual_displays); + allow_err!(Self::commit_change_display(0)); + self.displays.clear(); + self.virtual_displays.clear(); + let is_virtual_display_added = self.virtual_displays_added.len() > 0; + if is_virtual_display_added { + self.restore_plug_out_monitor(); + } else { + // https://github.com/rustdesk/rustdesk/pull/12114#issuecomment-2983054370 + // No virtual displays added, we need to change the display combination to force the display settings to be reloaded. + // This function changes the user behavior of the virtual displays. + // But it makes the privacy mode more stable. + // No need to restore the virtual displays. It's easy to notice that the virtual displays are plugged out. + let _ = virtual_display_manager::plug_out_monitor(-1, true, false); + + // We can't replug the virtual dislays here. + // TODO: plug out + plug in the virtual displays (`IDD_IMPL_AMYUNI`) in a short time makes the server side crash. + } + } + + fn restore_displays(displays: &[Display]) { + for display in displays { + unsafe { + let mut dm = display.dm.clone(); + let flags = if display.primary { + CDS_NORESET | CDS_UPDATEREGISTRY | CDS_SET_PRIMARY + } else { + CDS_NORESET | CDS_UPDATEREGISTRY + }; + ChangeDisplaySettingsExW( + display.name.as_ptr(), + &mut dm, + std::ptr::null_mut(), + flags, + std::ptr::null_mut(), + ); + } + } + } +} + +impl PrivacyMode for PrivacyModeImpl { + fn is_async_privacy_mode(&self) -> bool { + virtual_display_manager::is_amyuni_idd() + } + + fn init(&self) -> ResultType<()> { + Ok(()) + } + + fn clear(&mut self) { + allow_err!(self.turn_off_privacy(self.conn_id, None)); + } + + fn turn_on_privacy(&mut self, conn_id: i32) -> ResultType { + if !virtual_display_manager::is_virtual_display_supported() { + bail!("idd_not_support_under_win10_2004_tip"); + } + + if self.check_on_conn_id(conn_id)? { + log::debug!("Privacy mode of conn {} is already on", conn_id); + return Ok(true); + } + self.set_displays(); + if self.displays.is_empty() { + log::debug!("{}", NO_PHYSICAL_DISPLAYS); + bail!(NO_PHYSICAL_DISPLAYS); + } + + let is_async_mode = self.is_async_privacy_mode(); + let mut guard = TurnOnGuard { + privacy_mode: self, + succeeded: false, + }; + + guard.ensure_virtual_display(is_async_mode)?; + if guard.virtual_displays.is_empty() { + log::debug!("No virtual displays"); + bail!("No virtual displays."); + } + + let reg_connectivity_1 = reg_display_settings::read_reg_connectivity()?; + let primary_display_name = guard.set_primary_display()?; + guard.disable_physical_displays()?; + Self::commit_change_display(CDS_RESET)?; + // Explicitly set the resolution(virtual display) to 1920x1080. + allow_err!(crate::platform::change_resolution( + &primary_display_name, + 1920, + 1080 + )); + let reg_connectivity_2 = reg_display_settings::read_reg_connectivity()?; + + if let Some(reg_recovery) = + reg_display_settings::diff_recent_connectivity(reg_connectivity_1, reg_connectivity_2) + { + Config::set_option( + CONFIG_KEY_REG_RECOVERY.to_owned(), + serde_json::to_string(®_recovery)?, + ); + } else { + reset_config_reg_connectivity(); + }; + + // OpenInputDesktop and block the others' input ? + guard.conn_id = conn_id; + guard.succeeded = true; + + allow_err!(super::win_input::hook()); + + Ok(true) + } + + fn turn_off_privacy( + &mut self, + conn_id: i32, + state: Option, + ) -> ResultType<()> { + self.check_off_conn_id(conn_id)?; + super::win_input::unhook()?; + let _tmp_ignore_changed_holder = crate::display_service::temp_ignore_displays_changed(); + self.restore(); + // We need to force restore the registry connectivity. + // This is because the registry connection may be changed by `self.restore()`, but will not be fully restored. + restore_reg_connectivity(false, true); + + if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID { + if let Some(state) = state { + allow_err!(super::set_privacy_mode_state( + conn_id, + state, + PRIVACY_MODE_IMPL.to_string(), + 1_000 + )); + } + self.conn_id = INVALID_PRIVACY_MODE_CONN_ID.to_owned(); + } + + Ok(()) + } + + #[inline] + fn pre_conn_id(&self) -> i32 { + self.conn_id + } + + #[inline] + fn get_impl_key(&self) -> &str { + &self.impl_key + } +} + +impl Drop for PrivacyModeImpl { + fn drop(&mut self) { + if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID { + allow_err!(self.turn_off_privacy(self.conn_id, None)); + } + } +} + +#[inline] +fn reset_config_reg_connectivity() { + Config::set_option(CONFIG_KEY_REG_RECOVERY.to_owned(), "".to_owned()); +} + +pub fn restore_reg_connectivity(plug_out_monitors: bool, force: bool) { + let config_recovery_value = Config::get_option(CONFIG_KEY_REG_RECOVERY); + if config_recovery_value.is_empty() { + return; + } + if plug_out_monitors { + let _ = virtual_display_manager::plug_out_monitor(-1, true, false); + } + if let Ok(reg_recovery) = + serde_json::from_str::(&config_recovery_value) + { + if let Err(e) = reg_display_settings::restore_reg_connectivity(reg_recovery, force) { + log::error!("Failed restore_reg_connectivity, error: {}", e); + } + } + reset_config_reg_connectivity(); +} diff --git a/vendor/rustdesk/src/rendezvous_mediator.rs b/vendor/rustdesk/src/rendezvous_mediator.rs new file mode 100644 index 0000000..3ef280a --- /dev/null +++ b/vendor/rustdesk/src/rendezvous_mediator.rs @@ -0,0 +1,933 @@ +use std::{ + net::SocketAddr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, RwLock, + }, + time::{Duration, Instant}, +}; + +use uuid::Uuid; + +use hbb_common::{ + allow_err, + anyhow::{self, bail}, + config::{ + self, keys::*, option2bool, use_ws, Config, CONNECT_TIMEOUT, REG_INTERVAL, RENDEZVOUS_PORT, + }, + futures::future::join_all, + log, + protobuf::Message as _, + rendezvous_proto::*, + sleep, + socket_client::{self, connect_tcp, is_ipv4, new_direct_udp_for, new_udp_for}, + tokio::{self, select, sync::Mutex, time::interval}, + udp::FramedSocket, + AddrMangle, IntoTargetAddr, ResultType, Stream, TargetAddr, +}; + +use crate::{ + check_port, + server::{check_zombie, new as new_server, ServerPtr}, +}; + +type Message = RendezvousMessage; + +lazy_static::lazy_static! { + static ref SOLVING_PK_MISMATCH: Mutex = Default::default(); + static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now())); + static ref LAST_RELAY_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now())); +} +static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); +static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); +static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false); + +#[derive(Clone)] +pub struct RendezvousMediator { + addr: TargetAddr<'static>, + host: String, + host_prefix: String, + keep_alive: i32, +} + +impl RendezvousMediator { + pub fn restart() { + SHOULD_EXIT.store(true, Ordering::SeqCst); + MANUAL_RESTARTED.store(true, Ordering::SeqCst); + log::info!("server restart"); + } + + pub async fn start_all() { + crate::test_nat_type(); + if config::is_outgoing_only() { + loop { + sleep(1.).await; + } + } + crate::hbbs_http::sync::start(); + #[cfg(target_os = "windows")] + if crate::platform::is_installed() && crate::is_server() { + crate::updater::start_auto_update(); + } + check_zombie(); + let server = new_server(); + if config::option2bool("stop-service", &Config::get_option("stop-service")) { + crate::test_rendezvous_server(); + } + let server_cloned = server.clone(); + tokio::spawn(async move { + direct_server(server_cloned).await; + }); + #[cfg(target_os = "android")] + let start_lan_listening = true; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let start_lan_listening = crate::platform::is_installed(); + if start_lan_listening { + std::thread::spawn(move || { + allow_err!(super::lan::start_listening()); + }); + } + // It is ok to run xdesktop manager when the headless function is not allowed. + #[cfg(target_os = "linux")] + if crate::is_server() { + crate::platform::linux_desktop_manager::start_xdesktop(); + } + scrap::codec::test_av1(); + loop { + let timeout = Arc::new(RwLock::new(CONNECT_TIMEOUT)); + let conn_start_time = Instant::now(); + *SOLVING_PK_MISMATCH.lock().await = "".to_owned(); + if !config::option2bool("stop-service", &Config::get_option("stop-service")) + && !crate::platform::installing_service() + { + let mut futs = Vec::new(); + let servers = Config::get_rendezvous_servers(); + SHOULD_EXIT.store(false, Ordering::SeqCst); + MANUAL_RESTARTED.store(false, Ordering::SeqCst); + for host in servers.clone() { + let server = server.clone(); + let timeout = timeout.clone(); + futs.push(tokio::spawn(async move { + if let Err(err) = Self::start(server, host).await { + let err = format!("rendezvous mediator error: {err}"); + // When user reboot, there might be below error, waiting too long + // (CONNECT_TIMEOUT 18s) will make user think there is bug + if err.contains("10054") || err.contains("11001") { + // No such host is known. (os error 11001) + // An existing connection was forcibly closed by the remote host. (os error 10054): also happens for UDP + *timeout.write().unwrap() = 3000; + } + log::error!("{err}"); + } + // SHOULD_EXIT here is to ensure once one exits, the others also exit. + SHOULD_EXIT.store(true, Ordering::SeqCst); + })); + } + join_all(futs).await; + } else { + server.write().unwrap().close_connections(); + } + Config::reset_online(); + let timeout = *timeout.read().unwrap(); + if !MANUAL_RESTARTED.load(Ordering::SeqCst) { + let elapsed = conn_start_time.elapsed().as_millis() as u64; + if elapsed < timeout { + sleep(((timeout - elapsed) / 1000) as _).await; + } + } else { + // https://github.com/rustdesk/rustdesk/issues/12233 + sleep(0.033).await; + } + } + } + + fn get_host_prefix(host: &str) -> String { + host.split(".") + .next() + .map(|x| { + if x.parse::().is_ok() { + host.to_owned() + } else { + x.to_owned() + } + }) + .unwrap_or(host.to_owned()) + } + + pub async fn start_udp(server: ServerPtr, host: String) -> ResultType<()> { + let host = check_port(&host, RENDEZVOUS_PORT); + log::info!("start udp: {host}"); + let (mut socket, mut addr) = new_udp_for(&host, CONNECT_TIMEOUT).await?; + let mut rz = Self { + addr: addr.clone(), + host: host.clone(), + host_prefix: Self::get_host_prefix(&host), + keep_alive: crate::DEFAULT_KEEP_ALIVE, + }; + + let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT)); + const MIN_REG_TIMEOUT: i64 = 3_000; + const MAX_REG_TIMEOUT: i64 = 30_000; + let mut reg_timeout = MIN_REG_TIMEOUT; + const MAX_FAILS1: i64 = 2; + const MAX_FAILS2: i64 = 4; + const DNS_INTERVAL: i64 = 60_000; + let mut fails = 0; + let mut last_register_resp: Option = None; + let mut last_register_sent: Option = None; + let mut last_dns_check = Instant::now(); + let mut old_latency = 0; + let mut ema_latency = 0; + loop { + let mut update_latency = || { + last_register_resp = Some(Instant::now()); + fails = 0; + reg_timeout = MIN_REG_TIMEOUT; + let mut latency = last_register_sent + .map(|x| x.elapsed().as_micros() as i64) + .unwrap_or(0); + last_register_sent = None; + if latency < 0 || latency > 1_000_000 { + return; + } + if ema_latency == 0 { + ema_latency = latency; + } else { + ema_latency = latency / 30 + (ema_latency * 29 / 30); + latency = ema_latency; + } + let mut n = latency / 5; + if n < 3000 { + n = 3000; + } + if (latency - old_latency).abs() > n || old_latency <= 0 { + Config::update_latency(&host, latency); + log::debug!("Latency of {}: {}ms", host, latency as f64 / 1000.); + old_latency = latency; + } + }; + select! { + n = socket.next() => { + match n { + Some(Ok((bytes, _))) => { + if let Ok(msg) = Message::parse_from_bytes(&bytes) { + rz.handle_resp(msg.union, Sink::Framed(&mut socket, &addr), &server, &mut update_latency).await?; + } else { + log::debug!("Non-protobuf message bytes received: {:?}", bytes); + } + }, + Some(Err(e)) => bail!("Failed to receive next: {}", e), // maybe socks5 tcp disconnected + None => { + bail!("Socket receive none. Maybe socks5 server is down."); + }, + } + }, + _ = timer.tick() => { + if SHOULD_EXIT.load(Ordering::SeqCst) { + break; + } + let now = Some(Instant::now()); + let expired = last_register_resp.map(|x| x.elapsed().as_millis() as i64 >= REG_INTERVAL).unwrap_or(true); + let timeout = last_register_sent.map(|x| x.elapsed().as_millis() as i64 >= reg_timeout).unwrap_or(false); + // temporarily disable exponential backoff for android before we add wakeup trigger to force connect in android + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if crate::using_public_server() { // only turn on this for public server, may help DDNS self-hosting user. + if timeout && reg_timeout < MAX_REG_TIMEOUT { + reg_timeout += MIN_REG_TIMEOUT; + } + } + if timeout || (last_register_sent.is_none() && expired) { + if timeout { + fails += 1; + if fails >= MAX_FAILS2 { + Config::update_latency(&host, -1); + old_latency = 0; + if last_dns_check.elapsed().as_millis() as i64 > DNS_INTERVAL { + // in some case of network reconnect (dial IP network), + // old UDP socket not work any more after network recover + if let Some((s, new_addr)) = socket_client::rebind_udp_for(&rz.host).await? { + socket = s; + rz.addr = new_addr.clone(); + addr = new_addr; + } + last_dns_check = Instant::now(); + } + } else if fails >= MAX_FAILS1 { + Config::update_latency(&host, 0); + old_latency = 0; + } + } + rz.register_peer(Sink::Framed(&mut socket, &addr)).await?; + last_register_sent = now; + } + } + } + } + Ok(()) + } + + #[inline] + async fn handle_resp( + &mut self, + msg: Option, + sink: Sink<'_>, + server: &ServerPtr, + update_latency: &mut impl FnMut(), + ) -> ResultType<()> { + match msg { + Some(rendezvous_message::Union::RegisterPeerResponse(rpr)) => { + update_latency(); + if rpr.request_pk { + log::info!("request_pk received from {}", self.host); + self.register_pk(sink).await?; + } + } + Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => { + update_latency(); + match rpr.result.enum_value() { + Ok(register_pk_response::Result::OK) => { + Config::set_key_confirmed(true); + Config::set_host_key_confirmed(&self.host_prefix, true); + *SOLVING_PK_MISMATCH.lock().await = "".to_owned(); + } + Ok(register_pk_response::Result::UUID_MISMATCH) => { + self.handle_uuid_mismatch(sink).await?; + } + _ => { + log::error!("unknown RegisterPkResponse"); + } + } + if rpr.keep_alive > 0 { + self.keep_alive = rpr.keep_alive * 1000; + log::info!("keep_alive: {}ms", self.keep_alive); + } + } + Some(rendezvous_message::Union::PunchHole(ph)) => { + let rz = self.clone(); + let server = server.clone(); + tokio::spawn(async move { + allow_err!(rz.handle_punch_hole(ph, server).await); + }); + } + Some(rendezvous_message::Union::RequestRelay(rr)) => { + let rz = self.clone(); + let server = server.clone(); + tokio::spawn(async move { + allow_err!(rz.handle_request_relay(rr, server).await); + }); + } + Some(rendezvous_message::Union::FetchLocalAddr(fla)) => { + let rz = self.clone(); + let server = server.clone(); + tokio::spawn(async move { + allow_err!(rz.handle_intranet(fla, server).await); + }); + } + Some(rendezvous_message::Union::ConfigureUpdate(cu)) => { + let v0 = Config::get_rendezvous_servers(); + Config::set_option( + "rendezvous-servers".to_owned(), + cu.rendezvous_servers.join(","), + ); + Config::set_serial(cu.serial); + if v0 != Config::get_rendezvous_servers() { + Self::restart(); + } + } + _ => {} + } + Ok(()) + } + + pub async fn start_tcp(server: ServerPtr, host: String) -> ResultType<()> { + let host = check_port(&host, RENDEZVOUS_PORT); + log::info!("start tcp: {}", hbb_common::websocket::check_ws(&host)); + let mut conn = connect_tcp(host.clone(), CONNECT_TIMEOUT).await?; + let key = crate::get_key(true).await; + crate::secure_tcp(&mut conn, &key).await?; + let mut rz = Self { + addr: conn.local_addr().into_target_addr()?, + host: host.clone(), + host_prefix: Self::get_host_prefix(&host), + keep_alive: crate::DEFAULT_KEEP_ALIVE, + }; + let mut timer = crate::rustdesk_interval(interval(crate::TIMER_OUT)); + let mut last_register_sent: Option = None; + let mut last_recv_msg = Instant::now(); + // we won't support connecting to multiple rendzvous servers any more, so we can use a global variable here. + Config::set_host_key_confirmed(&rz.host_prefix, false); + loop { + let mut update_latency = || { + let latency = last_register_sent + .map(|x| x.elapsed().as_micros() as i64) + .unwrap_or(0); + Config::update_latency(&host, latency); + log::debug!("Latency of {}: {}ms", host, latency as f64 / 1000.); + }; + select! { + res = conn.next() => { + last_recv_msg = Instant::now(); + let bytes = res.ok_or_else(|| anyhow::anyhow!("Rendezvous connection is reset by the peer"))??; + if bytes.is_empty() { + // After fixing frequent register_pk, for websocket, nginx need to set proxy_read_timeout to more than 60 seconds, eg: 120s + // https://serverfault.com/questions/1060525/why-is-my-websocket-connection-gets-closed-in-60-seconds + conn.send_bytes(bytes::Bytes::new()).await?; + continue; // heartbeat + } + let msg = Message::parse_from_bytes(&bytes)?; + rz.handle_resp(msg.union, Sink::Stream(&mut conn), &server, &mut update_latency).await? + } + _ = timer.tick() => { + if SHOULD_EXIT.load(Ordering::SeqCst) { + break; + } + // https://www.emqx.com/en/blog/mqtt-keep-alive + if last_recv_msg.elapsed().as_millis() as u64 > rz.keep_alive as u64 * 3 / 2 { + bail!("Rendezvous connection is timeout"); + } + if (!Config::get_key_confirmed() || + !Config::get_host_key_confirmed(&rz.host_prefix)) && + last_register_sent.map(|x| x.elapsed().as_millis() as i64).unwrap_or(REG_INTERVAL) >= REG_INTERVAL { + rz.register_pk(Sink::Stream(&mut conn)).await?; + last_register_sent = Some(Instant::now()); + } + } + } + } + Ok(()) + } + + pub async fn start(server: ServerPtr, host: String) -> ResultType<()> { + log::info!("start rendezvous mediator of {}", host); + //If the investment agent type is http or https, then tcp forwarding is enabled. + if (cfg!(debug_assertions) && option_env!("TEST_TCP").is_some()) + || Config::is_proxy() + || use_ws() + || crate::is_udp_disabled() + { + Self::start_tcp(server, host).await + } else { + Self::start_udp(server, host).await + } + } + + async fn handle_request_relay(&self, rr: RequestRelay, server: ServerPtr) -> ResultType<()> { + let addr = AddrMangle::decode(&rr.socket_addr); + let last = *LAST_RELAY_MSG.lock().await; + *LAST_RELAY_MSG.lock().await = (addr, Instant::now()); + // skip duplicate relay request messages + if last.0 == addr && last.1.elapsed().as_millis() < 100 { + return Ok(()); + } + + self.create_relay( + rr.socket_addr.into(), + rr.relay_server, + rr.uuid, + server, + rr.secure, + false, + Default::default(), + rr.control_permissions.clone().into_option(), + ) + .await + } + + async fn create_relay( + &self, + socket_addr: Vec, + relay_server: String, + uuid: String, + server: ServerPtr, + secure: bool, + initiate: bool, + socket_addr_v6: bytes::Bytes, + control_permissions: Option, + ) -> ResultType<()> { + let peer_addr = AddrMangle::decode(&socket_addr); + log::info!( + "create_relay requested from {:?}, relay_server: {}, uuid: {}, secure: {}", + peer_addr, + relay_server, + uuid, + secure, + ); + + let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?; + + let mut msg_out = Message::new(); + let mut rr = RelayResponse { + socket_addr: socket_addr.into(), + version: crate::VERSION.to_owned(), + socket_addr_v6, + ..Default::default() + }; + if initiate { + rr.uuid = uuid.clone(); + rr.relay_server = relay_server.clone(); + rr.set_id(Config::get_id()); + } + msg_out.set_relay_response(rr); + socket.send(&msg_out).await?; + crate::create_relay_connection( + server, + relay_server, + uuid, + peer_addr, + secure, + is_ipv4(&self.addr), + control_permissions, + ) + .await; + Ok(()) + } + + async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> { + let addr = AddrMangle::decode(&fla.socket_addr); + let last = *LAST_MSG.lock().await; + *LAST_MSG.lock().await = (addr, Instant::now()); + // skip duplicate punch hole messages + if last.0 == addr && last.1.elapsed().as_millis() < 100 { + return Ok(()); + } + let peer_addr_v6 = hbb_common::AddrMangle::decode(&fla.socket_addr_v6); + let relay_server = self.get_relay_server(fla.relay_server.clone()); + let relay = use_ws() || Config::is_proxy(); + let mut socket_addr_v6 = Default::default(); + if peer_addr_v6.port() > 0 && !relay { + socket_addr_v6 = start_ipv6( + peer_addr_v6, + addr, + server.clone(), + fla.control_permissions.clone().into_option(), + ) + .await; + } + if is_ipv4(&self.addr) && !relay && !config::is_disable_tcp_listen() { + if let Err(err) = self + .handle_intranet_( + fla.clone(), + server.clone(), + relay_server.clone(), + socket_addr_v6.clone(), + ) + .await + { + log::debug!("Failed to handle intranet: {:?}, will try relay", err); + } else { + return Ok(()); + } + } + let uuid = Uuid::new_v4().to_string(); + self.create_relay( + fla.socket_addr.into(), + relay_server, + uuid, + server, + true, + true, + socket_addr_v6, + fla.control_permissions.into_option(), + ) + .await + } + + async fn handle_intranet_( + &self, + fla: FetchLocalAddr, + server: ServerPtr, + relay_server: String, + socket_addr_v6: bytes::Bytes, + ) -> ResultType<()> { + let peer_addr = AddrMangle::decode(&fla.socket_addr); + log::debug!("Handle intranet from {:?}", peer_addr); + let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?; + let local_addr = socket.local_addr(); + // we saw invalid local_addr while using proxy, local_addr.ip() == "::1" + let local_addr: SocketAddr = + format!("{}:{}", local_addr.ip(), local_addr.port()).parse()?; + let mut msg_out = Message::new(); + msg_out.set_local_addr(LocalAddr { + id: Config::get_id(), + socket_addr: AddrMangle::encode(peer_addr).into(), + local_addr: AddrMangle::encode(local_addr).into(), + relay_server, + version: crate::VERSION.to_owned(), + socket_addr_v6, + ..Default::default() + }); + let bytes = msg_out.write_to_bytes()?; + socket.send_raw(bytes).await?; + crate::accept_connection( + server.clone(), + socket, + peer_addr, + true, + fla.control_permissions.into_option(), + ) + .await; + Ok(()) + } + + async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> { + let mut peer_addr = AddrMangle::decode(&ph.socket_addr); + let last = *LAST_MSG.lock().await; + *LAST_MSG.lock().await = (peer_addr, Instant::now()); + // skip duplicate punch hole messages + if last.0 == peer_addr && last.1.elapsed().as_millis() < 100 { + return Ok(()); + } + let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6); + let relay = use_ws() || Config::is_proxy() || ph.force_relay; + let mut socket_addr_v6 = Default::default(); + let control_permissions = ph.control_permissions.into_option(); + if peer_addr_v6.port() > 0 && !relay { + socket_addr_v6 = start_ipv6( + peer_addr_v6, + peer_addr, + server.clone(), + control_permissions.clone(), + ) + .await; + } + let relay_server = self.get_relay_server(ph.relay_server); + // for ensure, websocket go relay directly + if ph.nat_type.enum_value() == Ok(NatType::SYMMETRIC) + || Config::get_nat_type() == NatType::SYMMETRIC as i32 + || relay + || (config::is_disable_tcp_listen() && ph.udp_port <= 0) + { + let uuid = Uuid::new_v4().to_string(); + return self + .create_relay( + ph.socket_addr.into(), + relay_server, + uuid, + server, + true, + true, + socket_addr_v6.clone(), + control_permissions, + ) + .await; + } + use hbb_common::protobuf::Enum; + let nat_type = NatType::from_i32(Config::get_nat_type()).unwrap_or(NatType::UNKNOWN_NAT); + let msg_punch = PunchHoleSent { + socket_addr: ph.socket_addr, + id: Config::get_id(), + relay_server, + nat_type: nat_type.into(), + version: crate::VERSION.to_owned(), + socket_addr_v6, + ..Default::default() + }; + if ph.udp_port > 0 { + peer_addr.set_port(ph.udp_port as u16); + self.punch_udp_hole(peer_addr, server, msg_punch, control_permissions) + .await?; + return Ok(()); + } + log::debug!("Punch tcp hole to {:?}", peer_addr); + let mut socket = { + let socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?; + let local_addr = socket.local_addr(); + // key important here for punch hole to tell my gateway incoming peer is safe. + // it can not be async here, because local_addr can not be reused, we must close the connection before use it again. + allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await); + socket + }; + let mut msg_out = Message::new(); + msg_out.set_punch_hole_sent(msg_punch); + let bytes = msg_out.write_to_bytes()?; + socket.send_raw(bytes).await?; + crate::accept_connection(server.clone(), socket, peer_addr, true, control_permissions) + .await; + Ok(()) + } + + async fn punch_udp_hole( + &self, + peer_addr: SocketAddr, + server: ServerPtr, + msg_punch: PunchHoleSent, + control_permissions: Option, + ) -> ResultType<()> { + let mut msg_out = Message::new(); + msg_out.set_punch_hole_sent(msg_punch); + let (socket, addr) = new_direct_udp_for(&self.host).await?; + let data = msg_out.write_to_bytes()?; + socket.send_to(&data, addr).await?; + let socket_cloned = socket.clone(); + tokio::spawn(async move { + for _ in 0..2 { + let tm = (hbb_common::time_based_rand() % 20 + 10) as f32 / 1000.; + hbb_common::sleep(tm).await; + socket.send_to(&data, addr).await.ok(); + } + }); + udp_nat_listen( + socket_cloned.clone(), + peer_addr, + peer_addr, + server, + control_permissions, + ) + .await?; + Ok(()) + } + + async fn register_pk(&mut self, socket: Sink<'_>) -> ResultType<()> { + let mut msg_out = Message::new(); + let pk = Config::get_key_pair().1; + let uuid = hbb_common::get_uuid(); + let id = Config::get_id(); + msg_out.set_register_pk(RegisterPk { + id, + uuid: uuid.into(), + pk: pk.into(), + no_register_device: Config::no_register_device(), + ..Default::default() + }); + socket.send(&msg_out).await?; + SENT_REGISTER_PK.store(true, Ordering::SeqCst); + Ok(()) + } + + async fn handle_uuid_mismatch(&mut self, socket: Sink<'_>) -> ResultType<()> { + { + let mut solving = SOLVING_PK_MISMATCH.lock().await; + if solving.is_empty() || *solving == self.host { + log::info!("UUID_MISMATCH received from {}", self.host); + Config::set_key_confirmed(false); + Config::update_id(); + *solving = self.host.clone(); + } else { + return Ok(()); + } + } + self.register_pk(socket).await + } + + async fn register_peer(&mut self, socket: Sink<'_>) -> ResultType<()> { + let solving = SOLVING_PK_MISMATCH.lock().await; + if !(solving.is_empty() || *solving == self.host) { + return Ok(()); + } + drop(solving); + if !Config::get_key_confirmed() || !Config::get_host_key_confirmed(&self.host_prefix) { + log::info!( + "register_pk of {} due to key not confirmed", + self.host_prefix + ); + return self.register_pk(socket).await; + } + let id = Config::get_id(); + log::trace!( + "Register my id {:?} to rendezvous server {:?}", + id, + self.addr, + ); + let mut msg_out = Message::new(); + let serial = Config::get_serial(); + msg_out.set_register_peer(RegisterPeer { + id, + serial, + ..Default::default() + }); + socket.send(&msg_out).await?; + Ok(()) + } + + fn get_relay_server(&self, provided_by_rendezvous_server: String) -> String { + let mut relay_server = Config::get_option("relay-server"); + if relay_server.is_empty() { + relay_server = provided_by_rendezvous_server; + } + if relay_server.is_empty() { + relay_server = crate::increase_port(&self.host, 1); + } + relay_server + } +} + +fn get_direct_port() -> i32 { + let mut port = Config::get_option("direct-access-port") + .parse::() + .unwrap_or(0); + if port <= 0 { + port = RENDEZVOUS_PORT + 2; + } + port +} + +async fn direct_server(server: ServerPtr) { + let mut listener = None; + let mut port = 0; + loop { + let disabled = !option2bool( + OPTION_DIRECT_SERVER, + &Config::get_option(OPTION_DIRECT_SERVER), + ) || option2bool("stop-service", &Config::get_option("stop-service")); + if !disabled && listener.is_none() { + port = get_direct_port(); + match hbb_common::tcp::listen_any(port as _).await { + Ok(l) => { + listener = Some(l); + log::info!( + "Direct server listening on: {:?}", + listener.as_ref().map(|l| l.local_addr()) + ); + } + Err(err) => { + // to-do: pass to ui + log::error!( + "Failed to start direct server on port: {}, error: {}", + port, + err + ); + loop { + if port != get_direct_port() { + break; + } + sleep(1.).await; + } + } + } + } + if let Some(l) = listener.as_mut() { + if disabled || port != get_direct_port() { + log::info!("Exit direct access listen"); + listener = None; + continue; + } + if let Ok(Ok((stream, addr))) = hbb_common::timeout(1000, l.accept()).await { + stream.set_nodelay(true).ok(); + log::info!("direct access from {}", addr); + let local_addr = stream + .local_addr() + .unwrap_or(Config::get_any_listen_addr(true)); + let server = server.clone(); + tokio::spawn(async move { + allow_err!( + crate::server::create_tcp_connection( + server, + hbb_common::Stream::from(stream, local_addr), + addr, + false, + None, // Direct connections don't have control_permissions + ) + .await + ); + }); + } else { + sleep(0.1).await; + } + } else { + sleep(1.).await; + } + } +} + +enum Sink<'a> { + Framed(&'a mut FramedSocket, &'a TargetAddr<'a>), + Stream(&'a mut Stream), +} + +impl Sink<'_> { + async fn send(self, msg: &Message) -> ResultType<()> { + match self { + Sink::Framed(socket, addr) => socket.send(msg, addr.to_owned()).await, + Sink::Stream(stream) => stream.send(msg).await, + } + } +} + +async fn start_ipv6( + peer_addr_v6: SocketAddr, + peer_addr_v4: SocketAddr, + server: ServerPtr, + control_permissions: Option, +) -> bytes::Bytes { + crate::test_ipv6().await; + if let Some((socket, local_addr_v6)) = crate::get_ipv6_socket().await { + let server = server.clone(); + tokio::spawn(async move { + allow_err!( + udp_nat_listen( + socket.clone(), + peer_addr_v6, + peer_addr_v4, + server, + control_permissions + ) + .await + ); + }); + return local_addr_v6; + } + Default::default() +} + +async fn udp_nat_listen( + socket: Arc, + peer_addr: SocketAddr, + peer_addr_v4: SocketAddr, + server: ServerPtr, + control_permissions: Option, +) -> ResultType<()> { + let tm = Instant::now(); + let socket_cloned = socket.clone(); + let func = async { + socket.connect(peer_addr).await?; + let res = crate::punch_udp(socket.clone(), true).await?; + let stream = crate::kcp_stream::KcpStream::accept( + socket, + Duration::from_millis(CONNECT_TIMEOUT as _), + res, + ) + .await?; + crate::server::create_tcp_connection( + server, + stream.1, + peer_addr_v4, + true, + control_permissions, + ) + .await?; + Ok(()) + }; + func.await.map_err(|e: anyhow::Error| { + anyhow::anyhow!( + "Stop listening on {:?} for remote {peer_addr} with KCP, {:?} elapsed: {e}", + socket_cloned.local_addr(), + tm.elapsed() + ) + })?; + Ok(()) +} + +// When config is not yet synced from root, register_pk may have already been sent with a new generated pk. +// After config sync completes, the pk may change. This struct detects pk changes and triggers +// a re-registration by setting key_confirmed to false. +// NOTE: +// This only corrects PK registration for the current ID. If root uses a non-default mac-generated ID, +// this does not resolve the multi-ID issue by itself. +pub struct CheckIfResendPk { + pk: Option>, +} +impl CheckIfResendPk { + pub fn new() -> Self { + Self { + pk: Config::get_cached_pk(), + } + } +} +impl Drop for CheckIfResendPk { + fn drop(&mut self) { + if SENT_REGISTER_PK.load(Ordering::SeqCst) && Config::get_cached_pk() != self.pk { + Config::set_key_confirmed(false); + log::info!("Set key_confirmed to false due to pk changed, will resend register_pk"); + } + } +} diff --git a/vendor/rustdesk/src/server.rs b/vendor/rustdesk/src/server.rs new file mode 100644 index 0000000..dddc762 --- /dev/null +++ b/vendor/rustdesk/src/server.rs @@ -0,0 +1,834 @@ +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, + time::Duration, +}; + +use bytes::Bytes; + +pub use connection::*; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::config::Config2; +use hbb_common::tcp::{self, new_listener}; +use hbb_common::{ + allow_err, + anyhow::Context, + 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 scrap::camera; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use service::ServiceTmpl; +use service::{EmptyExtraFieldService, GenericService, Service, Subscriber}; +use video_service::VideoSource; + +use crate::ipc::Data; + +pub mod audio_service; +#[cfg(target_os = "windows")] +pub mod terminal_helper; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod terminal_service; +cfg_if::cfg_if! { +if #[cfg(not(target_os = "ios"))] { +mod clipboard_service; +#[cfg(target_os = "android")] +pub use clipboard_service::is_clipboard_service_ok; +#[cfg(target_os = "linux")] +pub(crate) mod wayland; +#[cfg(target_os = "linux")] +pub mod uinput; +#[cfg(target_os = "linux")] +pub mod rdp_input; +#[cfg(target_os = "linux")] +pub mod dbus; +#[cfg(not(target_os = "android"))] +pub mod input_service; +} else { +mod clipboard_service { +pub const NAME: &'static str = ""; +} +} +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +pub mod input_service { + pub const NAME_CURSOR: &'static str = ""; + pub const NAME_POS: &'static str = ""; + pub const NAME_WINDOW_FOCUS: &'static str = ""; +} + +mod connection; +pub mod display_service; +#[cfg(windows)] +pub mod portable_service; +mod service; +mod video_qos; +pub mod video_service; + +#[cfg(all(target_os = "windows", feature = "flutter"))] +pub mod printer_service; + +pub type Childs = Arc>>; +type ConnMap = HashMap; + +#[cfg(any(target_os = "macos", target_os = "linux"))] +const CONFIG_SYNC_INTERVAL_SECS: f32 = 0.3; +#[cfg(any(target_os = "macos", target_os = "linux"))] +// 3s is enough for at least one initial sync attempt: +// 0.3s backoff + up to 1s connect timeout + up to 1s response timeout. +const CONFIG_SYNC_INITIAL_WAIT_SECS: u64 = 3; + +lazy_static::lazy_static! { + pub static ref CHILD_PROCESS: Childs = Default::default(); + // A client server used to provide local services(audio, video, clipboard, etc.) + // for all initiative connections. + // + // [Note] + // ugly + // Now we use this [`CLIENT_SERVER`] to do following operations: + // - record local audio, and send to remote + pub static ref CLIENT_SERVER: ServerPtr = new(); +} + +pub struct Server { + connections: ConnMap, + services: HashMap>, + id_count: i32, +} + +pub type ServerPtr = Arc>; +pub type ServerPtrWeak = Weak>; + +pub fn new() -> ServerPtr { + let mut server = Server { + connections: HashMap::new(), + services: HashMap::new(), + id_count: hbb_common::rand::random::() % 1000 + 1000, // ensure positive + }; + server.add_service(Box::new(audio_service::new())); + #[cfg(not(target_os = "ios"))] + { + server.add_service(Box::new(display_service::new())); + server.add_service(Box::new(clipboard_service::new( + clipboard_service::NAME.to_owned(), + ))); + #[cfg(feature = "unix-file-copy-paste")] + server.add_service(Box::new(clipboard_service::new( + clipboard_service::FILE_NAME.to_owned(), + ))); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if !display_service::capture_cursor_embedded() { + server.add_service(Box::new(input_service::new_cursor())); + server.add_service(Box::new(input_service::new_pos())); + #[cfg(target_os = "linux")] + if scrap::is_x11() { + // wayland does not support multiple displays currently + server.add_service(Box::new(input_service::new_window_focus())); + } + #[cfg(not(target_os = "linux"))] + server.add_service(Box::new(input_service::new_window_focus())); + } + } + #[cfg(all(target_os = "windows", feature = "flutter"))] + { + match printer_service::init(&crate::get_app_name()) { + Ok(()) => { + log::info!("printer service initialized"); + server.add_service(Box::new(printer_service::new( + printer_service::NAME.to_owned(), + ))); + } + Err(e) => { + log::error!("printer service init failed: {}", e); + } + } + } + // Terminal service is created per connection, not globally + Arc::new(RwLock::new(server)) +} + +async fn accept_connection_( + server: ServerPtr, + socket: Stream, + secure: bool, + control_permissions: Option, +) -> ResultType<()> { + let local_addr = socket.local_addr(); + drop(socket); + // even we drop socket, below still may fail if not use reuse_addr, + // there is TIME_WAIT before socket really released, so sometimes we + // see "Only one usage of each socket address is normally permitted" on windows sometimes, + let listener = new_listener(local_addr, true).await?; + log::info!("Server listening on: {}", &listener.local_addr()?); + if let Ok((stream, addr)) = timeout(CONNECT_TIMEOUT, listener.accept()).await? { + stream.set_nodelay(true).ok(); + let stream_addr = stream.local_addr()?; + create_tcp_connection( + server, + Stream::from(stream, stream_addr), + addr, + secure, + control_permissions, + ) + .await?; + } + Ok(()) +} + +pub async fn create_tcp_connection( + server: ServerPtr, + stream: Stream, + addr: SocketAddr, + secure: bool, + control_permissions: Option, +) -> ResultType<()> { + let mut stream = stream; + let id = server.write().unwrap().get_new_id(); + let (sk, pk) = Config::get_key_pair(); + if secure && pk.len() == sign::PUBLICKEYBYTES && sk.len() == sign::SECRETKEYBYTES { + let mut sk_ = [0u8; sign::SECRETKEYBYTES]; + sk_[..].copy_from_slice(&sk); + let sk = sign::SecretKey(sk_); + let mut msg_out = Message::new(); + let (our_pk_b, our_sk_b) = box_::gen_keypair(); + msg_out.set_signed_id(SignedId { + id: sign::sign( + &IdPk { + id: Config::get_id(), + pk: Bytes::from(our_pk_b.0.to_vec()), + ..Default::default() + } + .write_to_bytes() + .unwrap_or_default(), + &sk, + ) + .into(), + ..Default::default() + }); + timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??; + match timeout(CONNECT_TIMEOUT, stream.next()).await? { + Some(res) => { + let bytes = res?; + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + if let Some(message::Union::PublicKey(pk)) = msg_in.union { + if pk.asymmetric_value.len() == box_::PUBLICKEYBYTES { + stream.set_key(tcp::Encrypt::decode( + &pk.symmetric_value, + &pk.asymmetric_value, + &our_sk_b, + )?); + } else if pk.asymmetric_value.is_empty() { + Config::set_key_confirmed(false); + log::info!("Force to update pk"); + } else { + bail!("Handshake failed: invalid public sign key length from peer"); + } + } else { + log::error!("Handshake failed: invalid message type"); + } + } else { + bail!("Handshake failed: invalid message format"); + } + } + None => { + bail!("Failed to receive public key"); + } + } + } + + #[cfg(target_os = "macos")] + { + use std::process::Command; + if let Ok(task) = Command::new("/usr/bin/caffeinate") + .arg("-u") + .arg("-t 5") + .spawn() + { + super::CHILD_PROCESS.lock().unwrap().push(task); + } + log::info!("wake up macos"); + } + Connection::start( + addr, + stream, + id, + Arc::downgrade(&server), + control_permissions, + ) + .await; + Ok(()) +} + +pub async fn accept_connection( + server: ServerPtr, + socket: Stream, + peer_addr: SocketAddr, + secure: bool, + control_permissions: Option, +) { + if let Err(err) = accept_connection_(server, socket, secure, control_permissions).await { + log::warn!("Failed to accept connection from {}: {}", peer_addr, err); + } +} + +pub async fn create_relay_connection( + server: ServerPtr, + relay_server: String, + uuid: String, + peer_addr: SocketAddr, + secure: bool, + ipv4: bool, + control_permissions: Option, +) { + if let Err(err) = create_relay_connection_( + server, + relay_server, + uuid.clone(), + peer_addr, + secure, + ipv4, + control_permissions, + ) + .await + { + log::error!( + "Failed to create relay connection for {} with uuid {}: {}", + peer_addr, + uuid, + err + ); + } +} + +async fn create_relay_connection_( + server: ServerPtr, + relay_server: String, + uuid: String, + peer_addr: SocketAddr, + secure: bool, + ipv4: bool, + control_permissions: Option, +) -> ResultType<()> { + let mut stream = socket_client::connect_tcp( + socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4), + CONNECT_TIMEOUT, + ) + .await?; + let mut msg_out = RendezvousMessage::new(); + let licence_key = crate::get_key(true).await; + msg_out.set_request_relay(RequestRelay { + licence_key, + uuid, + ..Default::default() + }); + stream.send(&msg_out).await?; + create_tcp_connection(server, stream, peer_addr, secure, control_permissions).await?; + Ok(()) +} + +impl Server { + fn is_video_service_name(name: &str) -> bool { + name.starts_with(VideoSource::Monitor.service_name_prefix()) + || name.starts_with(VideoSource::Camera.service_name_prefix()) + } + + pub fn try_add_primary_camera_service(&mut self) { + if !camera::primary_camera_exists() { + return; + } + let primary_camera_name = + video_service::get_service_name(VideoSource::Camera, camera::PRIMARY_CAMERA_IDX); + if !self.contains(&primary_camera_name) { + self.add_service(Box::new(video_service::new( + VideoSource::Camera, + camera::PRIMARY_CAMERA_IDX, + ))); + } + } + + pub fn try_add_primay_video_service(&mut self) { + let primary_video_service_name = video_service::get_service_name( + VideoSource::Monitor, + *display_service::PRIMARY_DISPLAY_IDX, + ); + if !self.contains(&primary_video_service_name) { + self.add_service(Box::new(video_service::new( + VideoSource::Monitor, + *display_service::PRIMARY_DISPLAY_IDX, + ))); + } + } + + pub fn add_camera_connection(&mut self, conn: ConnInner) { + if camera::primary_camera_exists() { + let primary_camera_name = + video_service::get_service_name(VideoSource::Camera, camera::PRIMARY_CAMERA_IDX); + if let Some(s) = self.services.get(&primary_camera_name) { + s.on_subscribe(conn.clone()); + } + } + self.connections.insert(conn.id(), conn); + } + + pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) { + let primary_video_service_name = video_service::get_service_name( + VideoSource::Monitor, + *display_service::PRIMARY_DISPLAY_IDX, + ); + for s in self.services.values() { + let name = s.name(); + if Self::is_video_service_name(&name) && name != primary_video_service_name { + continue; + } + if !noperms.contains(&(&name as _)) { + s.on_subscribe(conn.clone()); + } + } + #[cfg(target_os = "macos")] + self.update_enable_retina(); + self.connections.insert(conn.id(), conn); + } + + pub fn remove_connection(&mut self, conn: &ConnInner) { + for s in self.services.values() { + s.on_unsubscribe(conn.id()); + } + self.connections.remove(&conn.id()); + #[cfg(target_os = "macos")] + self.update_enable_retina(); + } + + pub fn close_connections(&mut self) { + let conn_inners: Vec<_> = self.connections.values_mut().collect(); + for c in conn_inners { + let mut misc = Misc::new(); + misc.set_stop_service(true); + let mut msg = Message::new(); + msg.set_misc(misc); + c.send(Arc::new(msg)); + } + } + + fn add_service(&mut self, service: Box) { + let name = service.name(); + self.services.insert(name, service); + } + + pub fn contains(&self, name: &str) -> bool { + self.services.contains_key(name) + } + + pub fn subscribe(&mut self, name: &str, conn: ConnInner, sub: bool) { + if let Some(s) = self.services.get(name) { + if s.is_subed(conn.id()) == sub { + return; + } + if sub { + s.on_subscribe(conn.clone()); + } else { + s.on_unsubscribe(conn.id()); + } + #[cfg(target_os = "macos")] + self.update_enable_retina(); + } + } + + // get a new unique id + pub fn get_new_id(&mut self) -> i32 { + self.id_count += 1; + self.id_count + } + + pub fn set_video_service_opt( + &self, + display: Option<(VideoSource, usize)>, + opt: &str, + value: &str, + ) { + for (k, v) in self.services.iter() { + if let Some((source, display)) = display { + if k != &video_service::get_service_name(source, display) { + continue; + } + } + + if Self::is_video_service_name(k) { + v.set_option(opt, value); + } + } + } + + fn get_subbed_displays_count(&self, conn_id: i32) -> usize { + self.services + .keys() + .filter(|k| { + Self::is_video_service_name(k) + && self + .services + .get(*k) + .map(|s| s.is_subed(conn_id)) + .unwrap_or(false) + }) + .count() + } + + fn capture_displays( + &mut self, + conn: ConnInner, + source: VideoSource, + displays: &[usize], + include: bool, + exclude: bool, + ) { + let displays = displays + .iter() + .map(|d| video_service::get_service_name(source, *d)) + .collect::>(); + let keys = self.services.keys().cloned().collect::>(); + for name in keys.iter() { + if Self::is_video_service_name(&name) { + if displays.contains(&name) { + if include { + self.subscribe(&name, conn.clone(), true); + } + } else { + if exclude { + self.subscribe(&name, conn.clone(), false); + } + } + } + } + } + + #[cfg(target_os = "macos")] + fn update_enable_retina(&self) { + let mut video_service_count = 0; + for (name, service) in self.services.iter() { + if Self::is_video_service_name(&name) && service.ok() { + video_service_count += 1; + } + } + *scrap::quartz::ENABLE_RETINA.lock().unwrap() = video_service_count < 2; + } +} + +impl Drop for Server { + fn drop(&mut self) { + for s in self.services.values() { + s.join(); + } + #[cfg(target_os = "linux")] + wayland::clear(); + } +} + +pub fn check_zombie() { + std::thread::spawn(|| loop { + let mut lock = CHILD_PROCESS.lock().unwrap(); + let mut i = 0; + while i != lock.len() { + let c = &mut (*lock)[i]; + if let Ok(Some(_)) = c.try_wait() { + lock.remove(i); + } else { + i += 1; + } + } + drop(lock); + std::thread::sleep(Duration::from_millis(100)); + }); +} + +/// Start the host server that allows the remote peer to control the current machine. +/// +/// # Arguments +/// +/// * `is_server` - Whether the current client is definitely the server. +/// If true, the server will be started. +/// Otherwise, client will check if there's already a server and start one if not. +#[cfg(any(target_os = "android", target_os = "ios"))] +#[tokio::main] +pub async fn start_server(_is_server: bool) { + crate::RendezvousMediator::start_all().await; +} + +/// Start the host server that allows the remote peer to control the current machine. +/// +/// # Arguments +/// +/// * `is_server` - Whether the current client is definitely the server. +/// If true, the server will be started. +/// Otherwise, client will check if there's already a server and start one if not. +/// * `no_server` - If `is_server` is false, whether to start a server if not found. +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[tokio::main] +pub async fn start_server(is_server: bool, no_server: bool) { + use std::sync::Once; + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + #[cfg(target_os = "linux")] + { + log::info!("DISPLAY={:?}", std::env::var("DISPLAY")); + log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY")); + } + #[cfg(windows)] + hbb_common::platform::windows::start_cpu_performance_monitor(); + }); + + if is_server { + crate::common::set_server_running(true); + std::thread::spawn(move || { + if let Err(err) = crate::ipc::start("") { + log::error!("Failed to start ipc: {}", err); + if crate::is_server() { + log::error!("ipc is occupied by another process, try kill it"); + std::thread::spawn(stop_main_window_process).join().ok(); + } + std::process::exit(-1); + } + }); + input_service::fix_key_down_timeout_loop(); + #[cfg(target_os = "linux")] + if input_service::wayland_use_uinput() { + allow_err!(input_service::setup_uinput(0, 1920, 0, 1080).await); + } + #[cfg(any(target_os = "macos", target_os = "linux"))] + wait_initial_config_sync().await; + #[cfg(target_os = "windows")] + crate::platform::try_kill_broker(); + #[cfg(feature = "hwcodec")] + scrap::hwcodec::start_check_process(); + crate::RendezvousMediator::start_all().await; + } else { + match crate::ipc::connect(1000, "").await { + Ok(mut conn) => { + if conn.send(&Data::SyncConfig(None)).await.is_ok() { + if let Ok(Some(data)) = conn.next_timeout(1000).await { + match data { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; + if Config::set(config) { + log::info!("config synced"); + } + if Config2::set(config2) { + log::info!("config2 synced"); + } + } + _ => {} + } + } + } + #[cfg(feature = "hwcodec")] + #[cfg(any(target_os = "windows", target_os = "linux"))] + crate::ipc::client_get_hwcodec_config_thread(0); + } + Err(err) => { + log::info!("server not started: {err:?}, no_server: {no_server}"); + if no_server { + hbb_common::sleep(1.0).await; + std::thread::spawn(|| start_server(false, true)); + } else { + log::info!("try start server"); + std::thread::spawn(|| start_server(true, false)); + } + } + } + } +} + +#[cfg(target_os = "macos")] +#[tokio::main(flavor = "current_thread")] +pub async fn start_ipc_url_server() { + log::debug!("Start an ipc server for listening to url schemes"); + match crate::ipc::new_listener("_url").await { + Ok(mut incoming) => { + while let Some(Ok(conn)) = incoming.next().await { + let mut conn = crate::ipc::Connection::new(conn); + match conn.next_timeout(1000).await { + Ok(Some(data)) => match data { + #[cfg(feature = "flutter")] + Data::UrlLink(url) => { + let mut m = HashMap::new(); + m.insert("name", "on_url_scheme_received"); + m.insert("url", url.as_str()); + let event = serde_json::to_string(&m).unwrap_or("".to_owned()); + match crate::flutter::push_global_event( + crate::flutter::APP_TYPE_MAIN, + event, + ) { + None => log::warn!("No main window app found!"), + Some(..) => {} + } + } + _ => { + log::warn!("An unexpected data was sent to the ipc url server.") + } + }, + Err(err) => { + log::error!("{}", err); + } + _ => {} + } + } + } + Err(err) => { + log::error!("{}", err); + } + } +} + +#[cfg(any(target_os = "macos", target_os = "linux"))] +async fn wait_initial_config_sync() { + if crate::platform::is_root() { + return; + } + + // Non-server process should not block startup, but still keeps background sync/watch alive. + if !crate::is_server() { + tokio::spawn(async move { + sync_and_watch_config_dir(None).await; + }); + return; + } + + let (sync_done_tx, mut sync_done_rx) = tokio::sync::oneshot::channel::<()>(); + tokio::spawn(async move { + sync_and_watch_config_dir(Some(sync_done_tx)).await; + }); + + // Server process waits up to N seconds for initial root->local sync to reduce stale-start window. + tokio::select! { + _ = &mut sync_done_rx => { + } + _ = tokio::time::sleep(Duration::from_secs(CONFIG_SYNC_INITIAL_WAIT_SECS)) => { + log::warn!( + "timed out waiting {}s for initial config sync, continue startup and keep syncing in background", + CONFIG_SYNC_INITIAL_WAIT_SECS + ); + } + } +} + +#[cfg(any(target_os = "macos", target_os = "linux"))] +async fn sync_and_watch_config_dir(sync_done_tx: Option>) { + let mut cfg0 = (Config::get(), Config2::get()); + let mut synced = false; + let mut is_root_config_empty = false; + let mut sync_done_tx = sync_done_tx; + let tries = if crate::is_server() { 30 } else { 3 }; + log::debug!("#tries of ipc service connection: {}", tries); + use hbb_common::sleep; + for i in 1..=tries { + sleep(i as f32 * CONFIG_SYNC_INTERVAL_SECS).await; + match crate::ipc::connect(1000, "_service").await { + Ok(mut conn) => { + if !synced { + if conn.send(&Data::SyncConfig(None)).await.is_ok() { + if let Ok(Some(data)) = conn.next_timeout(1000).await { + match data { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; + let _chk = crate::ipc::CheckIfRestart::new(); + #[cfg(target_os = "macos")] + let _chk_pk = crate::CheckIfResendPk::new(); + if !config.is_empty() { + if cfg0.0 != config { + cfg0.0 = config.clone(); + Config::set(config); + log::info!("sync config from root"); + } + if cfg0.1 != config2 { + cfg0.1 = config2.clone(); + Config2::set(config2); + log::info!("sync config2 from root"); + } + } else { + // only on macos, because this issue was only reproduced on macos + #[cfg(target_os = "macos")] + { + // root config is empty, mark for sync in watch loop + // to prevent root from generating a new config on login screen + is_root_config_empty = true; + } + } + synced = true; + // Notify startup waiter once initial sync phase finishes successfully. + if let Some(tx) = sync_done_tx.take() { + let _ = tx.send(()); + } + } + _ => {} + }; + }; + } + } + + loop { + sleep(CONFIG_SYNC_INTERVAL_SECS).await; + let cfg = (Config::get(), Config2::get()); + let should_sync = + cfg != cfg0 || (is_root_config_empty && !cfg.0.is_empty()); + if should_sync { + if is_root_config_empty { + log::info!("root config is empty, sync our config to root"); + } else { + log::info!("config updated, sync to root"); + } + match conn.send(&Data::SyncConfig(Some(cfg.clone().into()))).await { + Err(e) => { + log::error!("sync config to root failed: {}", e); + match crate::ipc::connect(1000, "_service").await { + Ok(mut _conn) => { + conn = _conn; + log::info!("reconnected to ipc_service"); + } + _ => {} + } + } + _ => { + cfg0 = cfg; + conn.next_timeout(1000).await.ok(); + is_root_config_empty = false; + } + } + } + } + } + Err(_) => { + log::info!("#{} try: failed to connect to ipc_service", i); + } + } + } + // Notify startup waiter even when initial sync is skipped/failed, to avoid unnecessary waiting. + if let Some(tx) = sync_done_tx.take() { + let _ = tx.send(()); + } + log::warn!("skipped config sync"); +} + +#[tokio::main(flavor = "current_thread")] +pub async fn stop_main_window_process() { + // this may also kill another --server process, + // but --server usually can be auto restarted by --service, so it is ok + if let Ok(mut conn) = crate::ipc::connect(1000, "").await { + conn.send(&crate::ipc::Data::Close).await.ok(); + } + #[cfg(windows)] + { + // in case above failure, e.g. zombie process + if let Err(e) = crate::platform::try_kill_rustdesk_main_window_process() { + log::error!("kill failed: {}", e); + } + } +} diff --git a/vendor/rustdesk/src/server/audio_service.rs b/vendor/rustdesk/src/server/audio_service.rs new file mode 100644 index 0000000..d1bb2d8 --- /dev/null +++ b/vendor/rustdesk/src/server/audio_service.rs @@ -0,0 +1,527 @@ +// both soundio and cpal use wasapi on windows and coreaudio on mac, they do not support loopback. +// libpulseaudio support loopback because pulseaudio is a standalone audio service with some +// configuration, but need to install the library and start the service on OS, not a good choice. +// windows: https://docs.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording +// mac: https://github.com/mattingalls/Soundflower +// https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nn-audioclient-iaudioclient +// https://github.com/ExistentialAudio/BlackHole + +// if pactl not work, please run +// sudo apt-get --purge --reinstall install pulseaudio +// https://askubuntu.com/questions/403416/how-to-listen-live-sounds-from-input-from-external-sound-card +// https://wiki.debian.org/audio-loopback +// https://github.com/krruzic/pulsectl + +use super::*; +#[cfg(not(any(target_os = "linux", target_os = "android")))] +use hbb_common::anyhow::anyhow; +use magnum_opus::{Application::*, Channels::*, Encoder}; +use std::sync::atomic::{AtomicBool, Ordering}; + +pub const NAME: &'static str = "audio"; +pub const AUDIO_DATA_SIZE_U8: usize = 960 * 4; // 10ms in 48000 stereo +static RESTARTING: AtomicBool = AtomicBool::new(false); + +lazy_static::lazy_static! { + static ref VOICE_CALL_INPUT_DEVICE: Arc::>> = Default::default(); +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn new() -> GenericService { + let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); + GenericService::repeat::(&svc.clone(), 33, cpal_impl::run); + svc.sp +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn new() -> GenericService { + let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); + GenericService::run(&svc.clone(), pa_impl::run); + svc.sp +} + +#[inline] +pub fn get_voice_call_input_device() -> Option { + VOICE_CALL_INPUT_DEVICE.lock().unwrap().clone() +} + +#[inline] +pub fn set_voice_call_input_device(device: Option, set_if_present: bool) { + if !set_if_present && VOICE_CALL_INPUT_DEVICE.lock().unwrap().is_some() { + return; + } + + if *VOICE_CALL_INPUT_DEVICE.lock().unwrap() == device { + return; + } + *VOICE_CALL_INPUT_DEVICE.lock().unwrap() = device; + restart(); +} + +#[inline] +fn get_audio_input() -> String { + VOICE_CALL_INPUT_DEVICE + .lock() + .unwrap() + .clone() + .unwrap_or(Config::get_option("audio-input")) +} + +pub fn restart() { + log::info!("restart the audio service, freezing now..."); + if RESTARTING.load(Ordering::SeqCst) { + return; + } + RESTARTING.store(true, Ordering::SeqCst); +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +mod pa_impl { + use super::*; + + // SAFETY: constrains of hbb_common::mem::aligned_u8_vec must be held + unsafe fn align_to_32(data: Vec) -> Vec { + if (data.as_ptr() as usize & 3) == 0 { + return data; + } + + let mut buf = vec![]; + buf = unsafe { hbb_common::mem::aligned_u8_vec(data.len(), 4) }; + buf.extend_from_slice(data.as_ref()); + buf + } + + #[tokio::main(flavor = "current_thread")] + pub async fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc + RESTARTING.store(false, Ordering::SeqCst); + #[cfg(target_os = "linux")] + let mut stream = crate::ipc::connect(1000, "_pa").await?; + unsafe { + AUDIO_ZERO_COUNT = 0; + } + let mut encoder = Encoder::new(crate::platform::PA_SAMPLE_RATE, Stereo, LowDelay)?; + #[cfg(target_os = "linux")] + allow_err!( + stream + .send(&crate::ipc::Data::Config(( + "audio-input".to_owned(), + Some(super::get_audio_input()) + ))) + .await + ); + #[cfg(target_os = "linux")] + let zero_audio_frame: Vec = vec![0.; AUDIO_DATA_SIZE_U8 / 4]; + #[cfg(target_os = "android")] + let mut android_data = vec![]; + while sp.ok() && !RESTARTING.load(Ordering::SeqCst) { + sp.snapshot(|sps| { + sps.send(create_format_msg(crate::platform::PA_SAMPLE_RATE, 2)); + Ok(()) + })?; + + #[cfg(target_os = "linux")] + if let Ok(data) = stream.next_raw().await { + if data.len() == 0 { + send_f32(&zero_audio_frame, &mut encoder, &sp); + continue; + } + + if data.len() != AUDIO_DATA_SIZE_U8 { + continue; + } + + let data = unsafe { align_to_32(data.into()) }; + let data = unsafe { + std::slice::from_raw_parts::(data.as_ptr() as _, data.len() / 4) + }; + send_f32(data, &mut encoder, &sp); + } + + #[cfg(target_os = "android")] + if scrap::android::ffi::get_audio_raw(&mut android_data, &mut vec![]).is_some() { + let data = unsafe { + android_data = align_to_32(android_data); + std::slice::from_raw_parts::( + android_data.as_ptr() as _, + android_data.len() / 4, + ) + }; + send_f32(data, &mut encoder, &sp); + } else { + hbb_common::sleep(0.1).await; + } + } + Ok(()) + } +} + +#[inline] +#[cfg(feature = "screencapturekit")] +pub fn is_screen_capture_kit_available() -> bool { + cpal::available_hosts() + .iter() + .any(|host| *host == cpal::HostId::ScreenCaptureKit) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +mod cpal_impl { + use self::service::{Reset, ServiceSwap}; + use super::*; + use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + BufferSize, Device, Host, InputCallbackInfo, StreamConfig, SupportedStreamConfig, + }; + + lazy_static::lazy_static! { + static ref HOST: Host = cpal::default_host(); + static ref INPUT_BUFFER: Arc>> = Default::default(); + } + + #[cfg(feature = "screencapturekit")] + lazy_static::lazy_static! { + static ref HOST_SCREEN_CAPTURE_KIT: Result = cpal::host_from_id(cpal::HostId::ScreenCaptureKit); + } + + #[derive(Default)] + pub struct State { + stream: Option<(Box, Arc)>, + } + + impl super::service::Reset for State { + fn reset(&mut self) { + self.stream.take(); + } + } + + fn run_restart(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { + state.reset(); + sp.snapshot(|_sps: ServiceSwap<_>| Ok(()))?; + match &state.stream { + None => { + state.stream = Some(play(&sp)?); + } + _ => {} + } + if let Some((_, format)) = &state.stream { + sp.send_shared(format.clone()); + } + RESTARTING.store(false, Ordering::SeqCst); + Ok(()) + } + + fn run_serv_snapshot(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { + sp.snapshot(|sps| { + match &state.stream { + None => { + state.stream = Some(play(&sp)?); + } + _ => {} + } + if let Some((_, format)) = &state.stream { + sps.send_shared(format.clone()); + } + Ok(()) + })?; + Ok(()) + } + + pub fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { + if !RESTARTING.load(Ordering::SeqCst) { + run_serv_snapshot(sp, state) + } else { + run_restart(sp, state) + } + } + + fn send( + data: Vec, + sample_rate0: u32, + sample_rate: u32, + device_channel: u16, + encode_channel: u16, + encoder: &mut Encoder, + sp: &GenericService, + ) { + let mut data = data; + if sample_rate0 != sample_rate { + data = crate::common::audio_resample(&data, sample_rate0, sample_rate, device_channel); + } + if device_channel != encode_channel { + data = crate::common::audio_rechannel( + data, + sample_rate, + sample_rate, + device_channel, + encode_channel, + ) + } + send_f32(&data, encoder, sp); + } + + #[cfg(feature = "screencapturekit")] + fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { + let audio_input = super::get_audio_input(); + if !audio_input.is_empty() { + return get_audio_input(&audio_input); + } + if !is_screen_capture_kit_available() { + return get_audio_input(""); + } + let device = HOST_SCREEN_CAPTURE_KIT + .as_ref()? + .default_input_device() + .with_context(|| "Failed to get default input device for loopback")?; + let format = device + .default_input_config() + .map_err(|e| anyhow!(e)) + .with_context(|| "Failed to get input output format")?; + log::info!("Default input format: {:?}", format); + Ok((device, format)) + } + + #[cfg(windows)] + fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { + let audio_input = super::get_audio_input(); + if !audio_input.is_empty() { + return get_audio_input(&audio_input); + } + let device = HOST + .default_output_device() + .with_context(|| "Failed to get default output device for loopback")?; + log::info!( + "Default output device: {}", + device.name().unwrap_or("".to_owned()) + ); + let format = device + .default_output_config() + .map_err(|e| anyhow!(e)) + .with_context(|| "Failed to get default output format")?; + log::info!("Default output format: {:?}", format); + Ok((device, format)) + } + + #[cfg(not(any(windows, feature = "screencapturekit")))] + fn get_device() -> ResultType<(Device, SupportedStreamConfig)> { + let audio_input = super::get_audio_input(); + get_audio_input(&audio_input) + } + + fn get_audio_input(audio_input: &str) -> ResultType<(Device, SupportedStreamConfig)> { + let mut device = None; + #[cfg(feature = "screencapturekit")] + if !audio_input.is_empty() && is_screen_capture_kit_available() { + for d in HOST_SCREEN_CAPTURE_KIT + .as_ref()? + .devices() + .with_context(|| "Failed to get audio devices")? + { + if d.name().unwrap_or("".to_owned()) == audio_input { + device = Some(d); + break; + } + } + } + if device.is_none() && !audio_input.is_empty() { + for d in HOST + .devices() + .with_context(|| "Failed to get audio devices")? + { + if d.name().unwrap_or("".to_owned()) == audio_input { + device = Some(d); + break; + } + } + } + let device = device.unwrap_or( + HOST.default_input_device() + .with_context(|| "Failed to get default input device for loopback")?, + ); + log::info!("Input device: {}", device.name().unwrap_or("".to_owned())); + let format = device + .default_input_config() + .map_err(|e| anyhow!(e)) + .with_context(|| "Failed to get default input format")?; + log::info!("Default input format: {:?}", format); + Ok((device, format)) + } + + fn play(sp: &GenericService) -> ResultType<(Box, Arc)> { + use cpal::SampleFormat::*; + let (device, config) = get_device()?; + let sp = sp.clone(); + // Sample rate must be one of 8000, 12000, 16000, 24000, or 48000. + let sample_rate_0 = config.sample_rate().0; + let sample_rate = if sample_rate_0 < 12000 { + 8000 + } else if sample_rate_0 < 16000 { + 12000 + } else if sample_rate_0 < 24000 { + 16000 + } else if sample_rate_0 < 48000 { + 24000 + } else { + 48000 + }; + let ch = if config.channels() > 1 { Stereo } else { Mono }; + let stream = match config.sample_format() { + I8 => build_input_stream::(device, &config, sp, sample_rate, ch)?, + I16 => build_input_stream::(device, &config, sp, sample_rate, ch)?, + I32 => build_input_stream::(device, &config, sp, sample_rate, ch)?, + I64 => build_input_stream::(device, &config, sp, sample_rate, ch)?, + U8 => build_input_stream::(device, &config, sp, sample_rate, ch)?, + U16 => build_input_stream::(device, &config, sp, sample_rate, ch)?, + U32 => build_input_stream::(device, &config, sp, sample_rate, ch)?, + U64 => build_input_stream::(device, &config, sp, sample_rate, ch)?, + F32 => build_input_stream::(device, &config, sp, sample_rate, ch)?, + F64 => build_input_stream::(device, &config, sp, sample_rate, ch)?, + f => bail!("unsupported audio format: {:?}", f), + }; + stream.play()?; + Ok(( + Box::new(stream), + Arc::new(create_format_msg(sample_rate, ch as _)), + )) + } + + fn build_input_stream( + device: cpal::Device, + config: &cpal::SupportedStreamConfig, + sp: GenericService, + sample_rate: u32, + encode_channel: magnum_opus::Channels, + ) -> ResultType + where + T: cpal::SizedSample + dasp::sample::ToSample, + { + let err_fn = move |err| { + // too many UnknownErrno, will improve later + log::trace!("an error occurred on stream: {}", err); + }; + let sample_rate_0 = config.sample_rate().0; + log::debug!("Audio sample rate : {}", sample_rate); + unsafe { + AUDIO_ZERO_COUNT = 0; + } + let device_channel = config.channels(); + let mut encoder = Encoder::new(sample_rate, encode_channel, LowDelay)?; + // https://www.opus-codec.org/docs/html_api/group__opusencoder.html#gace941e4ef26ed844879fde342ffbe546 + // https://chromium.googlesource.com/chromium/deps/opus/+/1.1.1/include/opus.h + // Do not set `frame_size = sample_rate as usize / 100;` + // Because we find `sample_rate as usize / 100` will cause encoder error in `encoder.encode_vec_float()` sometimes. + // https://github.com/xiph/opus/blob/2554a89e02c7fc30a980b4f7e635ceae1ecba5d6/src/opus_encoder.c#L725 + let frame_size = sample_rate_0 as usize / 100; // 10 ms + let encode_len = frame_size * encode_channel as usize; + let rechannel_len = encode_len * device_channel as usize / encode_channel as usize; + INPUT_BUFFER.lock().unwrap().clear(); + let timeout = None; + let stream_config = StreamConfig { + channels: device_channel, + sample_rate: config.sample_rate(), + buffer_size: BufferSize::Default, + }; + let stream = device.build_input_stream( + &stream_config, + move |data: &[T], _: &InputCallbackInfo| { + let buffer: Vec = data.iter().map(|s| T::to_sample(*s)).collect(); + let mut lock = INPUT_BUFFER.lock().unwrap(); + lock.extend(buffer); + while lock.len() >= rechannel_len { + let frame: Vec = lock.drain(0..rechannel_len).collect(); + send( + frame, + sample_rate_0, + sample_rate, + device_channel, + encode_channel as _, + &mut encoder, + &sp, + ); + } + }, + err_fn, + timeout, + )?; + Ok(stream) + } +} + +fn create_format_msg(sample_rate: u32, channels: u16) -> Message { + let format = AudioFormat { + sample_rate, + channels: channels as _, + ..Default::default() + }; + let mut misc = Misc::new(); + misc.set_audio_format(format); + let mut msg = Message::new(); + msg.set_misc(misc); + msg +} + +// use AUDIO_ZERO_COUNT for the Noise(Zero) Gate Attack Time +// every audio data length is set to 480 +// MAX_AUDIO_ZERO_COUNT=800 is similar as Gate Attack Time 3~5s(Linux) || 6~8s(Windows) +const MAX_AUDIO_ZERO_COUNT: u16 = 800; +static mut AUDIO_ZERO_COUNT: u16 = 0; + +fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { + if data.iter().filter(|x| **x != 0.).next().is_some() { + unsafe { + AUDIO_ZERO_COUNT = 0; + } + } else { + unsafe { + if AUDIO_ZERO_COUNT > MAX_AUDIO_ZERO_COUNT { + if AUDIO_ZERO_COUNT == MAX_AUDIO_ZERO_COUNT + 1 { + log::debug!("Audio Zero Gate Attack"); + AUDIO_ZERO_COUNT += 1; + } + return; + } + AUDIO_ZERO_COUNT += 1; + } + } + #[cfg(target_os = "android")] + { + // the permitted opus data size are 120, 240, 480, 960, 1920, and 2880 + // if data size is bigger than BATCH_SIZE, AND is an integer multiple of BATCH_SIZE + // then upload in batches + const BATCH_SIZE: usize = 960; + let input_size = data.len(); + if input_size > BATCH_SIZE && input_size % BATCH_SIZE == 0 { + let n = input_size / BATCH_SIZE; + for i in 0..n { + match encoder + .encode_vec_float(&data[i * BATCH_SIZE..(i + 1) * BATCH_SIZE], BATCH_SIZE) + { + Ok(data) => { + let mut msg_out = Message::new(); + msg_out.set_audio_frame(AudioFrame { + data: data.into(), + ..Default::default() + }); + sp.send(msg_out); + } + Err(_) => {} + } + } + } else { + log::debug!("invalid audio data size:{} ", input_size); + return; + } + } + + #[cfg(not(target_os = "android"))] + match encoder.encode_vec_float(data, data.len() * 6) { + Ok(data) => { + let mut msg_out = Message::new(); + msg_out.set_audio_frame(AudioFrame { + data: data.into(), + ..Default::default() + }); + sp.send(msg_out); + } + Err(_) => {} + } +} diff --git a/vendor/rustdesk/src/server/clipboard_service.rs b/vendor/rustdesk/src/server/clipboard_service.rs new file mode 100644 index 0000000..1d2f0a3 --- /dev/null +++ b/vendor/rustdesk/src/server/clipboard_service.rs @@ -0,0 +1,274 @@ +use super::*; +#[cfg(not(target_os = "android"))] +use crate::clipboard::clipboard_listener; +#[cfg(not(target_os = "android"))] +pub use crate::clipboard::{check_clipboard, ClipboardContext, ClipboardSide}; +pub use crate::clipboard::{CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME}; +#[cfg(windows)] +use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data}; +#[cfg(feature = "unix-file-copy-paste")] +pub use crate::{ + clipboard::{check_clipboard_files, FILE_CLIPBOARD_NAME as FILE_NAME}, + clipboard_file::unix_file_clip, +}; +#[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}; +#[cfg(target_os = "android")] +use std::sync::atomic::{AtomicBool, Ordering}; +use std::{ + io, + sync::mpsc::{channel, RecvTimeoutError}, + time::Duration, +}; +#[cfg(windows)] +use tokio::runtime::Runtime; + +#[cfg(target_os = "android")] +static CLIPBOARD_SERVICE_OK: AtomicBool = AtomicBool::new(false); + +#[cfg(not(target_os = "android"))] +struct Handler { + ctx: Option, + #[cfg(target_os = "windows")] + stream: Option>, + #[cfg(target_os = "windows")] + rt: Option, +} + +#[cfg(target_os = "android")] +pub fn is_clipboard_service_ok() -> bool { + CLIPBOARD_SERVICE_OK.load(Ordering::SeqCst) +} + +pub fn new(name: String) -> GenericService { + let svc = EmptyExtraFieldService::new(name, false); + GenericService::run(&svc.clone(), run); + svc.sp +} + +#[cfg(not(target_os = "android"))] +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] + let _fuse_call_on_ret = { + if sp.name() == FILE_NAME { + Some(init_fuse_context(false).map(|_| crate::SimpleCallOnReturn { + b: true, + f: Box::new(|| { + uninit_fuse_context(false); + }), + })) + } else { + None + } + }; + + let (tx_cb_result, rx_cb_result) = channel(); + let ctx = Some(ClipboardContext::new().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?); + clipboard_listener::subscribe(sp.name(), tx_cb_result)?; + let mut handler = Handler { + ctx, + #[cfg(target_os = "windows")] + stream: None, + #[cfg(target_os = "windows")] + rt: None, + }; + + while sp.ok() { + match rx_cb_result.recv_timeout(Duration::from_millis(INTERVAL)) { + Ok(CallbackResult::Next) => { + #[cfg(feature = "unix-file-copy-paste")] + if sp.name() == FILE_NAME { + handler.check_clipboard_file(); + continue; + } + if let Some(msg) = handler.get_clipboard_msg() { + sp.send(msg); + } + } + Ok(CallbackResult::Stop) => { + log::debug!("Clipboard listener stopped"); + break; + } + Ok(CallbackResult::StopWithError(err)) => { + bail!("Clipboard listener stopped with error: {}", err); + } + Err(RecvTimeoutError::Timeout) => {} + Err(RecvTimeoutError::Disconnected) => { + log::error!("Clipboard listener disconnected"); + break; + } + } + } + + clipboard_listener::unsubscribe(&sp.name()); + + Ok(()) +} + +#[cfg(not(target_os = "android"))] +impl Handler { + #[cfg(feature = "unix-file-copy-paste")] + fn check_clipboard_file(&mut self) { + if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Host, false) { + if !urls.is_empty() { + #[cfg(target_os = "macos")] + if crate::clipboard::is_file_url_set_by_rustdesk(&urls) { + return; + } + match clipboard::platform::unix::serv_files::sync_files(&urls) { + Ok(()) => { + // Use `send_data()` here to reuse `handle_file_clip()` in `connection.rs`. + hbb_common::allow_err!(clipboard::send_data( + 0, + unix_file_clip::get_format_list() + )); + } + Err(e) => { + log::error!("Failed to sync clipboard files: {}", e); + } + } + } + } + } + + fn get_clipboard_msg(&mut self) -> Option { + #[cfg(target_os = "windows")] + if crate::common::is_server() && crate::platform::is_root() { + match self.read_clipboard_from_cm_ipc() { + Err(e) => { + log::error!("Failed to read clipboard from cm: {}", e); + } + Ok(data) => { + // Skip sending empty clipboard data. + // Maybe there's something wrong reading the clipboard data in cm, but no error msg is returned. + // The clipboard data should not be empty, the last line will try again to get the clipboard data. + if !data.is_empty() { + let mut msg = Message::new(); + let multi_clipboards = MultiClipboards { + clipboards: data + .into_iter() + .map(|c| Clipboard { + compress: c.compress, + content: c.content, + width: c.width, + height: c.height, + format: ClipboardFormat::from_i32(c.format) + .unwrap_or(ClipboardFormat::Text) + .into(), + special_name: c.special_name, + ..Default::default() + }) + .collect(), + ..Default::default() + }; + msg.set_multi_clipboards(multi_clipboards); + return Some(msg); + } + } + } + } + + check_clipboard(&mut self.ctx, ClipboardSide::Host, false) + } + + // Read clipboard data from cm using ipc. + // + // We cannot use `#[tokio::main(flavor = "current_thread")]` here, + // because the auto-managed tokio runtime (async context) will be dropped after the call. + // The next call will create a new runtime, which will cause the previous stream to be unusable. + // So we need to manage the tokio runtime manually. + #[cfg(windows)] + fn read_clipboard_from_cm_ipc(&mut self) -> ResultType> { + if self.rt.is_none() { + self.rt = Some(Runtime::new()?); + } + let Some(rt) = &self.rt else { + // unreachable! + bail!("failed to get tokio runtime"); + }; + let mut is_sent = false; + if let Some(stream) = &mut self.stream { + // If previous stream is still alive, reuse it. + // If the previous stream is dead, `is_sent` will trigger reconnect. + is_sent = match rt.block_on(stream.send(&Data::ClipboardNonFile(None))) { + Ok(_) => true, + Err(e) => { + log::debug!("Failed to send to cm: {}", e); + false + } + }; + } + if !is_sent { + let mut stream = rt.block_on(crate::ipc::connect(100, "_cm"))?; + rt.block_on(stream.send(&Data::ClipboardNonFile(None)))?; + self.stream = Some(stream); + } + + if let Some(stream) = &mut self.stream { + loop { + match rt.block_on(stream.next_timeout(800))? { + Some(Data::ClipboardNonFile(Some((err, mut contents)))) => { + if !err.is_empty() { + bail!("{}", err); + } else { + if contents.iter().any(|c| c.next_raw) { + // Wrap the future with a `Timeout` in an async block to avoid panic. + // We cannot use `rt.block_on(timeout(1000, stream.next_raw()))` here, because it causes panic: + // thread '' panicked at D:\Projects\rust\rustdesk\libs\hbb_common\src\lib.rs:98:5: + // there is no reactor running, must be called from the context of a Tokio 1.x runtime + // note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace + match rt.block_on(async { timeout(1000, stream.next_raw()).await }) + { + Ok(Ok(mut data)) => { + for c in &mut contents { + if c.next_raw { + // No need to check the length because sum(content_len) == data.len(). + c.content = data.split_to(c.content_len).into(); + } + } + } + Ok(Err(e)) => { + // reset by peer + self.stream = None; + bail!("failed to get raw clipboard data: {}", e); + } + Err(e) => { + // Reconnect to avoid the next raw data remaining in the buffer. + self.stream = None; + log::debug!("Failed to get raw clipboard data: {}", e); + } + } + } + return Ok(contents); + } + } + Some(Data::ClipboardFile(ClipboardFile::MonitorReady)) => { + // ClipboardFile::MonitorReady is the first message sent by cm. + } + _ => { + bail!("failed to get clipboard data from cm"); + } + } + } + } + // unreachable! + bail!("failed to get clipboard data from cm"); + } +} + +#[cfg(target_os = "android")] +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + CLIPBOARD_SERVICE_OK.store(sp.ok(), Ordering::SeqCst); + while sp.ok() { + if let Some(msg) = crate::clipboard::get_clipboards_msg(false) { + sp.send(msg); + } + std::thread::sleep(Duration::from_millis(INTERVAL)); + } + CLIPBOARD_SERVICE_OK.store(false, Ordering::SeqCst); + Ok(()) +} diff --git a/vendor/rustdesk/src/server/connection.rs b/vendor/rustdesk/src/server/connection.rs new file mode 100644 index 0000000..bd5327b --- /dev/null +++ b/vendor/rustdesk/src/server/connection.rs @@ -0,0 +1,5786 @@ +use super::{input_service::*, *}; +#[cfg(feature = "unix-file-copy-paste")] +use crate::clipboard::try_empty_clipboard_files; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::{update_clipboard, ClipboardSide}; +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +use crate::clipboard_file::*; +#[cfg(target_os = "android")] +use crate::keyboard::client::map_key_to_control_key; +#[cfg(target_os = "linux")] +use crate::platform::linux_desktop_manager; +#[cfg(any(target_os = "windows", target_os = "linux"))] +use crate::platform::WallPaperRemover; +#[cfg(windows)] +use crate::portable_service::client as portable_client; +use crate::{ + client::{ + new_voice_call_request, new_voice_call_response, start_audio_thread, MediaData, MediaSender, + }, + display_service, ipc, privacy_mode, video_service, VERSION, +}; +#[cfg(any(target_os = "android", target_os = "ios"))] +use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; +use cidr_utils::cidr::IpCidr; +#[cfg(target_os = "linux")] +use hbb_common::platform::linux::run_cmds; +#[cfg(target_os = "android")] +use hbb_common::protobuf::EnumOrUnknown; +use hbb_common::{ + config::decode_permanent_password_h1_from_storage, + config::{self, keys, 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, + tokio::{ + net::TcpStream, + sync::mpsc, + time::{self, Duration, Instant}, + }, + tokio_util::codec::{BytesCodec, Framed}, +}; +#[cfg(any(target_os = "android", target_os = "ios"))] +use scrap::android::{call_main_service_key_event, call_main_service_pointer_input}; +use scrap::camera; +use serde_derive::Serialize; +use serde_json::{json, value::Value}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use std::sync::atomic::Ordering; +use std::{ + collections::HashSet, + net::Ipv6Addr, + num::NonZeroI64, + path::PathBuf, + str::FromStr, + sync::{atomic::AtomicI64, mpsc as std_mpsc}, +}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use system_shutdown; +#[cfg(target_os = "windows")] +use windows::Win32::Foundation::{CloseHandle, HANDLE}; + +#[cfg(windows)] +use crate::virtual_display_manager; +pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; + +lazy_static::lazy_static! { + static ref LOGIN_FAILURES: [Arc::>>; 2] = Default::default(); + static ref SESSIONS: Arc::>> = Default::default(); + static ref ALIVE_CONNS: Arc::>> = Default::default(); + pub static ref AUTHED_CONNS: Arc::>> = Default::default(); + pub static ref CONTROL_PERMISSIONS_ARRAY: Arc::>> = Default::default(); + static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); + static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); + static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::>> = Default::default(); +} + +fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + // Avoid data-dependent early exits. + let mut x: u8 = 0; + for i in 0..a.len() { + x |= a[i] ^ b[i]; + } + x == 0 +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +lazy_static::lazy_static! { + static ref WALLPAPER_REMOVER: Arc>> = Default::default(); +} +pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0); +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0); + +#[cfg(all(feature = "flutter", feature = "plugin_framework"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + static ref PLUGIN_BLOCK_INPUT_TXS: Arc>>> = Default::default(); + static ref PLUGIN_BLOCK_INPUT_TX_RX: (Arc>>, Arc>>) = { + let (tx, rx) = std_mpsc::channel(); + (Arc::new(Mutex::new(tx)), Arc::new(Mutex::new(rx))) + }; +} + +// Block input is required for some special cases, such as privacy mode. +#[cfg(all(feature = "flutter", feature = "plugin_framework"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn plugin_block_input(peer: &str, block: bool) -> bool { + if let Some(tx) = PLUGIN_BLOCK_INPUT_TXS.lock().unwrap().get(peer) { + let _ = tx.send(if block { + MessageInput::BlockOnPlugin(peer.to_string()) + } else { + MessageInput::BlockOffPlugin(peer.to_string()) + }); + match PLUGIN_BLOCK_INPUT_TX_RX + .1 + .lock() + .unwrap() + .recv_timeout(std::time::Duration::from_millis(3_000)) + { + Ok(b) => b == block, + Err(..) => { + log::error!("plugin_block_input timeout"); + false + } + } + } else { + false + } +} + +#[derive(Clone, Default)] +pub struct ConnInner { + id: i32, + tx: Option, + tx_video: Option, +} + +struct InputMouse { + msg: MouseEvent, + conn_id: i32, + username: String, + argb: u32, + simulate: bool, + show_cursor: bool, +} + +enum MessageInput { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Mouse(InputMouse), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Key((KeyEvent, bool)), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Pointer((PointerDeviceEvent, i32)), + BlockOn, + BlockOff, + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + BlockOnPlugin(String), + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + BlockOffPlugin(String), +} + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct SessionKey { + peer_id: String, + name: String, + session_id: u64, +} + +#[derive(Clone, Debug)] +struct Session { + last_recv_time: Arc>, + random_password: String, + tfa: bool, +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +struct StartCmIpcPara { + rx_to_cm: mpsc::UnboundedReceiver, + tx_from_cm: mpsc::UnboundedSender, + rx_desktop_ready: mpsc::Receiver<()>, + tx_cm_stream_ready: mpsc::Sender<()>, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum AuthConnType { + Remote, + FileTransfer, + PortForward, + ViewCamera, + Terminal, +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[derive(Clone, Debug)] +enum TerminalUserToken { + SelfUser, + #[cfg(target_os = "windows")] + CurrentLogonUser(crate::terminal_service::UserToken), +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl TerminalUserToken { + fn to_terminal_service_token(&self) -> Option { + match self { + TerminalUserToken::SelfUser => None, + #[cfg(target_os = "windows")] + TerminalUserToken::CurrentLogonUser(token) => Some(*token), + } + } +} +pub struct Connection { + inner: ConnInner, + display_idx: usize, + stream: super::Stream, + server: super::ServerPtrWeak, + hash: Hash, + read_jobs: Vec, + timer: crate::RustDeskInterval, + file_timer: crate::RustDeskInterval, + file_transfer: Option<(String, bool)>, + view_camera: bool, + terminal: bool, + port_forward_socket: Option>, + port_forward_address: String, + tx_to_cm: mpsc::UnboundedSender, + authorized: bool, + require_2fa: Option, + keyboard: bool, + clipboard: bool, + audio: bool, + file: bool, + restart: bool, + recording: bool, + block_input: bool, + privacy_mode: bool, + control_permissions: Option, + last_test_delay: Option, + network_delay: u32, + lock_after_session_end: bool, + show_remote_cursor: bool, + // by peer + ip: String, + // by peer + disable_keyboard: bool, + // by peer + #[cfg(not(any(target_os = "android", target_os = "ios")))] + show_my_cursor: bool, + // by peer + disable_clipboard: bool, + // by peer + disable_audio: bool, + // by peer + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + enable_file_transfer: bool, + // by peer + audio_sender: Option, + // audio by the remote peer/client + tx_input: std_mpsc::Sender, + // handle input messages + video_ack_required: bool, + server_audit_conn: String, + server_audit_file: String, + lr: LoginRequest, + peer_argb: u32, + session_last_recv_time: Option>>, + chat_unanswered: bool, + file_transferred: bool, + #[cfg(windows)] + portable: PortableState, + from_switch: bool, + voice_call_request_timestamp: Option, + voice_calling: bool, + options_in_login: Option, + #[cfg(not(any(target_os = "ios")))] + pressed_modifiers: HashSet, + #[cfg(target_os = "linux")] + linux_headless_handle: LinuxHeadlessHandle, + closed: bool, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + start_cm_ipc_para: Option, + auto_disconnect_timer: Option<(Instant, u64)>, + authed_conn_id: Option, + file_remove_log_control: FileRemoveLogControl, + last_supported_encoding: Option, + services_subed: bool, + delayed_read_dir: Option<(String, bool)>, + #[cfg(target_os = "macos")] + retina: Retina, + follow_remote_cursor: bool, + follow_remote_window: bool, + multi_ui_session: bool, + tx_from_authed: mpsc::UnboundedSender, + printer_data: Vec<(Instant, String, Vec)>, + // For post requests that need to be sent sequentially. + // eg. post_conn_audit + tx_post_seq: mpsc::UnboundedSender<(String, Value)>, + // Tracks read job IDs delegated to CM process. + // When a read job is delegated to CM (via FS::ReadFile), the job id is added here. + // Used to filter stale responses (FileBlockFromCM, FileReadDone, etc.) for + // cancelled or unknown jobs. + cm_read_job_ids: HashSet, + terminal_service_id: String, + terminal_persistent: bool, + // The user token must be set when terminal is enabled. + // 0 indicates SYSTEM user + // other values indicate current user + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal_user_token: Option, + terminal_generic_service: Option>, +} + +impl ConnInner { + pub fn new(id: i32, tx: Option, tx_video: Option) -> Self { + Self { id, tx, tx_video } + } +} + +impl Subscriber for ConnInner { + #[inline] + fn id(&self) -> i32 { + self.id + } + + #[inline] + fn send(&mut self, msg: Arc) { + // Send SwitchDisplay on the same channel as VideoFrame to avoid send order problems. + let tx_by_video = match &msg.union { + Some(message::Union::VideoFrame(_)) => true, + Some(message::Union::Misc(misc)) => match &misc.union { + Some(misc::Union::SwitchDisplay(_)) => true, + _ => false, + }, + _ => false, + }; + let tx = if tx_by_video { + self.tx_video.as_mut() + } else { + self.tx.as_mut() + }; + tx.map(|tx| { + allow_err!(tx.send((Instant::now(), msg))); + }); + } +} + +const TEST_DELAY_TIMEOUT: Duration = Duration::from_secs(1); +const SEC30: Duration = Duration::from_secs(30); +const H1: Duration = Duration::from_secs(3600); +const MILLI1: Duration = Duration::from_millis(1); +const SEND_TIMEOUT_VIDEO: u64 = 12_000; +const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; +const SESSION_TIMEOUT: Duration = Duration::from_secs(30); + +impl Connection { + pub async fn start( + addr: SocketAddr, + stream: super::Stream, + id: i32, + server: super::ServerPtrWeak, + control_permissions: Option, + ) { + // Android is not supported yet, so we always set control_permissions to None. + #[cfg(target_os = "android")] + let control_permissions = None; + let _raii_id = raii::ConnectionID::new(id); + let _raii_control_permissions_id = + raii::ControlPermissionsID::new(id, &control_permissions); + let hash = Hash { + salt: Config::get_salt(), + challenge: Config::get_auto_password(6), + ..Default::default() + }; + let (tx_from_cm_holder, mut rx_from_cm) = mpsc::unbounded_channel::(); + // holding tx_from_cm_holder to avoid cpu burning of rx_from_cm.recv when all sender closed + let tx_from_cm = tx_from_cm_holder.clone(); + let (tx_to_cm, rx_to_cm) = mpsc::unbounded_channel::(); + let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); + let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); + let (tx_input, _rx_input) = std_mpsc::channel(); + let (tx_from_authed, mut rx_from_authed) = mpsc::unbounded_channel::(); + let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let (_tx_desktop_ready, rx_desktop_ready) = mpsc::channel(1); + #[cfg(target_os = "linux")] + let linux_headless_handle = + LinuxHeadlessHandle::new(_rx_cm_stream_ready, _tx_desktop_ready); + + let (tx_post_seq, rx_post_seq) = mpsc::unbounded_channel(); + tokio::spawn(async move { + Self::post_seq_loop(rx_post_seq).await; + }); + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let tx_cloned = tx.clone(); + let mut conn = Self { + inner: ConnInner { + id, + tx: Some(tx), + tx_video: Some(tx_video), + }, + require_2fa: crate::auth_2fa::get_2fa(None), + display_idx: *display_service::PRIMARY_DISPLAY_IDX, + stream, + server, + hash, + read_jobs: Vec::new(), + timer: crate::rustdesk_interval(time::interval(SEC30)), + file_timer: crate::rustdesk_interval(time::interval(SEC30)), + file_transfer: None, + view_camera: false, + terminal: false, + port_forward_socket: None, + port_forward_address: "".to_owned(), + tx_to_cm, + authorized: false, + keyboard: Self::permission(keys::OPTION_ENABLE_KEYBOARD, &control_permissions), + clipboard: Self::permission(keys::OPTION_ENABLE_CLIPBOARD, &control_permissions), + audio: Self::permission(keys::OPTION_ENABLE_AUDIO, &control_permissions), + // to-do: make sure is the option correct here + file: Self::permission(keys::OPTION_ENABLE_FILE_TRANSFER, &control_permissions), + restart: Self::permission(keys::OPTION_ENABLE_REMOTE_RESTART, &control_permissions), + recording: Self::permission(keys::OPTION_ENABLE_RECORD_SESSION, &control_permissions), + block_input: Self::permission(keys::OPTION_ENABLE_BLOCK_INPUT, &control_permissions), + privacy_mode: Self::permission(keys::OPTION_ENABLE_PRIVACY_MODE, &control_permissions), + control_permissions, + last_test_delay: None, + network_delay: 0, + lock_after_session_end: false, + show_remote_cursor: false, + follow_remote_cursor: false, + follow_remote_window: false, + multi_ui_session: false, + ip: "".to_owned(), + disable_audio: false, + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + enable_file_transfer: false, + disable_clipboard: false, + disable_keyboard: false, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + show_my_cursor: false, + tx_input, + video_ack_required: false, + server_audit_conn: "".to_owned(), + server_audit_file: "".to_owned(), + lr: Default::default(), + peer_argb: 0u32, + session_last_recv_time: None, + chat_unanswered: false, + file_transferred: false, + #[cfg(windows)] + portable: Default::default(), + from_switch: false, + audio_sender: None, + voice_call_request_timestamp: None, + voice_calling: false, + options_in_login: None, + #[cfg(not(any(target_os = "ios")))] + pressed_modifiers: Default::default(), + #[cfg(target_os = "linux")] + linux_headless_handle, + closed: false, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + start_cm_ipc_para: Some(StartCmIpcPara { + rx_to_cm, + tx_from_cm, + rx_desktop_ready, + tx_cm_stream_ready, + }), + auto_disconnect_timer: None, + authed_conn_id: None, + file_remove_log_control: FileRemoveLogControl::new(id), + last_supported_encoding: None, + services_subed: false, + delayed_read_dir: None, + #[cfg(target_os = "macos")] + retina: Retina::default(), + tx_from_authed, + printer_data: Vec::new(), + tx_post_seq, + cm_read_job_ids: HashSet::new(), + terminal_service_id: "".to_owned(), + terminal_persistent: false, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal_user_token: None, + terminal_generic_service: None, + }; + let addr = hbb_common::try_into_v4(addr); + if !conn.on_open(addr).await { + conn.closed = true; + // sleep to ensure msg got received. + sleep(1.).await; + return; + } + #[cfg(target_os = "android")] + start_channel(rx_to_cm, tx_from_cm); + #[cfg(target_os = "android")] + conn.send_permission(Permission::Keyboard, conn.keyboard) + .await; + #[cfg(not(target_os = "android"))] + if !conn.keyboard { + conn.send_permission(Permission::Keyboard, false).await; + } + if !conn.clipboard { + conn.send_permission(Permission::Clipboard, false).await; + } + if !conn.audio { + conn.send_permission(Permission::Audio, false).await; + } + if !conn.file { + conn.send_permission(Permission::File, false).await; + } + if !conn.restart { + conn.send_permission(Permission::Restart, false).await; + } + if !conn.recording { + conn.send_permission(Permission::Recording, false).await; + } + if !conn.block_input { + conn.send_permission(Permission::BlockInput, false).await; + } + if !conn.privacy_mode { + conn.send_permission(Permission::PrivacyMode, false).await; + } + let mut test_delay_timer = + crate::rustdesk_interval(time::interval_at(Instant::now(), TEST_DELAY_TIMEOUT)); + let mut last_recv_time = Instant::now(); + + conn.stream.set_send_timeout( + if conn.file_transfer.is_some() || conn.port_forward_socket.is_some() || conn.terminal { + SEND_TIMEOUT_OTHER + } else { + SEND_TIMEOUT_VIDEO + }, + ); + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + std::thread::spawn(move || Self::handle_input(_rx_input, tx_cloned)); + let mut second_timer = crate::rustdesk_interval(time::interval(Duration::from_secs(1))); + + #[cfg(feature = "unix-file-copy-paste")] + let rx_clip_holder; + let mut rx_clip; + let _tx_clip: mpsc::UnboundedSender; + #[cfg(feature = "unix-file-copy-paste")] + { + rx_clip_holder = ( + clipboard::get_rx_cliprdr_server(id), + crate::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + clipboard::remove_channel_by_conn_id(id); + }), + }, + ); + rx_clip = rx_clip_holder.0.lock().await; + } + #[cfg(not(feature = "unix-file-copy-paste"))] + { + (_tx_clip, rx_clip) = mpsc::unbounded_channel::(); + } + + loop { + tokio::select! { + // biased; // video has higher priority // causing test_delay_timer failed while transferring big file + + Some(data) = rx_from_cm.recv() => { + match data { + ipc::Data::Authorize => { + conn.require_2fa.take(); + if !conn.send_logon_response_and_keep_alive().await { + break; + } + if conn.port_forward_socket.is_some() { + break; + } + } + ipc::Data::Close => { + conn.chat_unanswered = false; // seen + conn.file_transferred = false; //seen + conn.send_close_reason_no_retry("").await; + conn.on_close("connection manager", true).await; + break; + } + ipc::Data::CmErr(e) => { + if e != "expected" { + // cm closed before connection + conn.on_close(&format!("connection manager error: {}", e), false).await; + break; + } + } + ipc::Data::ChatMessage{text} => { + let mut misc = Misc::new(); + misc.set_chat_message(ChatMessage { + text, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + conn.send(msg_out).await; + conn.chat_unanswered = false; + } + ipc::Data::SwitchPermission{name, enabled} => { + log::info!("Change permission {} -> {}", name, enabled); + if &name == "keyboard" { + conn.keyboard = enabled; + conn.send_permission(Permission::Keyboard, enabled).await; + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::NAME, + conn.inner.clone(), conn.can_sub_clipboard_service()); + #[cfg(feature = "unix-file-copy-paste")] + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + conn.inner.clone(), + conn.can_sub_file_clipboard_service(), + ); + s.write().unwrap().subscribe( + NAME_CURSOR, + conn.inner.clone(), enabled || conn.show_remote_cursor); + } + } else if &name == "clipboard" { + conn.clipboard = enabled; + conn.send_permission(Permission::Clipboard, enabled).await; + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::NAME, + conn.inner.clone(), conn.can_sub_clipboard_service()); + } + } else if &name == "audio" { + conn.audio = enabled; + conn.send_permission(Permission::Audio, enabled).await; + if conn.authorized { + if let Some(s) = conn.server.upgrade() { + if conn.is_authed_view_camera_conn() { + if conn.voice_calling || !conn.audio_enabled() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } + } else { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } + } + } + } else if &name == "file" { + conn.file = enabled; + conn.send_permission(Permission::File, enabled).await; + #[cfg(feature = "unix-file-copy-paste")] + if !enabled { + conn.try_empty_file_clipboard(); + } + #[cfg(feature = "unix-file-copy-paste")] + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + conn.inner.clone(), + conn.can_sub_file_clipboard_service(), + ); + } + } else if &name == "restart" { + conn.restart = enabled; + conn.send_permission(Permission::Restart, enabled).await; + } else if &name == "recording" { + conn.recording = enabled; + conn.send_permission(Permission::Recording, enabled).await; + } else if &name == "block_input" { + conn.block_input = enabled; + conn.send_permission(Permission::BlockInput, enabled).await; + } else if &name == "privacy_mode" { + // Keep permission state and runtime state consistent: + // when revoking the permission, try to leave privacy mode first. + // Otherwise we could end up in an inconsistent state where + // permission looks disabled while privacy mode is still active. + if !enabled && privacy_mode::is_in_privacy_mode() { + if let Some(conn_id) = privacy_mode::get_privacy_mode_conn_id() { + if conn_id == conn.inner.id() { + let impl_key = + privacy_mode::get_cur_impl_key().unwrap_or_default(); + let turn_off_res = + privacy_mode::turn_off_privacy(conn_id, None); + match turn_off_res { + Some(Ok(_)) => { + let msg_out = crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOffByPeer, + impl_key.clone(), + ); + conn.send(msg_out).await; + } + _ => { + let msg_out = Self::turn_off_privacy_result_to_msg( + turn_off_res, + impl_key, + ); + conn.send(msg_out).await; + // Turn-off failed, so revert CM's optimistic toggle + // and keep the previous permission value. + conn.send_to_cm(ipc::Data::SwitchPermission { + name: "privacy_mode".to_owned(), + enabled: conn.privacy_mode, + }); + continue; + } + } + } + } + } + conn.privacy_mode = enabled; + conn.send_permission(Permission::PrivacyMode, enabled).await; + } + } + ipc::Data::RawMessage(bytes) => { + allow_err!(conn.stream.send_raw(bytes).await); + } + #[cfg(target_os = "windows")] + ipc::Data::ClipboardFile(clip) => { + if !conn.is_remote() { + continue; + } + match clip { + clipboard::ClipboardFile::Files { files } => { + let files = files.into_iter().map(|(f, s)| { + (f, s as i64) + }).collect::>(); + conn.post_file_audit( + FileAuditType::RemoteSend, + "", + files, + json!({}), + ); + } + _ => { + allow_err!(conn.stream.send(&clip_2_msg(clip)).await); + } + } + } + ipc::Data::PrivacyModeState((_, state, impl_key)) => { + let msg_out = match state { + privacy_mode::PrivacyModeState::OffSucceeded => { + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOffSucceeded, + impl_key, + ) + } + privacy_mode::PrivacyModeState::OffByPeer => { + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOffByPeer, + impl_key, + ) + } + privacy_mode::PrivacyModeState::OffUnknown => { + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOffUnknown, + impl_key, + ) + } + }; + conn.send(msg_out).await; + } + #[cfg(windows)] + ipc::Data::DataPortableService(ipc::DataPortableService::RequestStart) => { + if let Err(e) = portable_client::start_portable_service(portable_client::StartPara::Direct) { + log::error!("Failed to start portable service from cm: {:?}", e); + } + } + ipc::Data::SwitchSidesBack => { + let mut misc = Misc::new(); + misc.set_switch_back(SwitchBack::default()); + let mut msg = Message::new(); + msg.set_misc(misc); + conn.send(msg).await; + } + ipc::Data::VoiceCallResponse(accepted) => { + conn.handle_voice_call(accepted).await; + } + ipc::Data::CloseVoiceCall(_reason) => { + log::debug!("Close the voice call from the ipc."); + conn.close_voice_call().await; + // Notify the peer that we closed the voice call. + let msg = new_voice_call_request(false); + conn.send(msg).await; + } + ipc::Data::ReadJobInitResult { id, file_num, include_hidden, conn_id, result } => { + if conn_id == conn.inner.id() { + conn.handle_read_job_init_result(id, file_num, include_hidden, result).await; + } + } + ipc::Data::FileBlockFromCM { id, file_num, data, compressed, conn_id } => { + if conn_id == conn.inner.id() { + conn.handle_file_block_from_cm(id, file_num, data, compressed).await; + } + } + ipc::Data::FileReadDone { id, file_num, conn_id } => { + if conn_id == conn.inner.id() { + conn.handle_file_read_done(id, file_num).await; + } + } + ipc::Data::FileReadError { id, file_num, err, conn_id } => { + if conn_id == conn.inner.id() { + conn.handle_file_read_error(id, file_num, err).await; + } + } + ipc::Data::FileDigestFromCM { id, file_num, last_modified, file_size, is_resume, conn_id } => { + if conn_id == conn.inner.id() { + conn.handle_file_digest_from_cm(id, file_num, last_modified, file_size, is_resume).await; + } + } + ipc::Data::AllFilesResult { id, conn_id, path, result } => { + if conn_id == conn.inner.id() { + conn.handle_all_files_result(id, path, result).await; + } + } + _ => {} + } + }, + res = conn.stream.next() => { + if let Some(res) = res { + match res { + Err(err) => { + conn.on_close(&err.to_string(), true).await; + break; + }, + Ok(bytes) => { + last_recv_time = Instant::now(); + conn.session_last_recv_time.as_mut().map(|t| *t.lock().unwrap() = Instant::now()); + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + if !conn.on_message(msg_in).await { + break; + } + if conn.port_forward_socket.is_some() && conn.authorized { + log::info!("Port forward, last_test_delay is none: {}", conn.last_test_delay.is_none()); + // Avoid TestDelay reply injection into rdp data stream + if conn.last_test_delay.is_none() { + break; + } + } + } + } + } + } else { + conn.on_close("Reset by the peer", true).await; + break; + } + }, + _ = conn.file_timer.tick() => { + if !conn.read_jobs.is_empty() { + conn.send_to_cm(ipc::Data::FileTransferLog(("transfer".to_string(), fs::serialize_transfer_jobs(&conn.read_jobs)))); + match fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await { + Ok(log) => { + if !log.is_empty() { + conn.send_to_cm(ipc::Data::FileTransferLog(("transfer".to_string(), log))); + } + } + Err(err) => { + conn.on_close(&err.to_string(), false).await; + break; + } + } + } else { + conn.file_timer = crate::rustdesk_interval(time::interval_at(Instant::now() + SEC30, SEC30)); + } + } + Ok(conns) = hbbs_rx.recv() => { + if conns.contains(&id) { + conn.send_close_reason_no_retry("Closed manually by web console").await; + conn.on_close("web console", true).await; + break; + } + } + Some((instant, value)) = rx_video.recv() => { + if !conn.video_ack_required { + if let Some(message::Union::VideoFrame(vf)) = &value.union { + video_service::notify_video_frame_fetched(vf.display as usize, id, Some(instant.into())); + } + } + if let Err(err) = conn.stream.send(&value as &Message).await { + conn.on_close(&err.to_string(), false).await; + break; + } + }, + Some((instant, value)) = rx.recv() => { + let latency = instant.elapsed().as_millis() as i64; + #[allow(unused_mut)] + let mut msg = value; + + if latency > 1000 { + match &msg.union { + Some(message::Union::AudioFrame(_)) => { + // log::info!("audio frame latency {}", instant.elapsed().as_secs_f32()); + continue; + } + _ => {} + } + } + match &msg.union { + Some(message::Union::Misc(m)) => { + match &m.union { + Some(misc::Union::StopService(_)) => { + conn.send_close_reason_no_retry("").await; + conn.on_close("stop service", false).await; + break; + } + _ => {}, + } + } + Some(message::Union::PeerInfo(_pi)) => { + conn.refresh_video_display(None); + #[cfg(target_os = "macos")] + conn.retina.set_displays(&_pi.displays); + } + Some(message::Union::CursorPosition(pos)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if conn.follow_remote_cursor { + conn.handle_cursor_switch_display(pos.clone()).await; + } + } + #[cfg(target_os = "macos")] + if let Some(new_msg) = conn.retina.on_cursor_pos(&pos, conn.display_idx) { + msg = Arc::new(new_msg); + } + } + Some(message::Union::MultiClipboards(_multi_clipboards)) => { + #[cfg(not(target_os = "ios"))] + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip(&conn.lr.version, &conn.lr.my_platform, _multi_clipboards) { + if let Err(err) = conn.stream.send(&msg_out).await { + conn.on_close(&err.to_string(), false).await; + break; + } + continue; + } + } + _ => {} + } + + let msg: &Message = &msg; + if let Err(err) = conn.stream.send(msg).await { + conn.on_close(&err.to_string(), false).await; + break; + } + }, + Some(data) = rx_from_authed.recv() => { + match data { + #[cfg(all(target_os = "windows", feature = "flutter"))] + ipc::Data::PrinterData(data) => { + if Self::permission(keys::OPTION_ENABLE_REMOTE_PRINTER, &conn.control_permissions) { + conn.send_printer_request(data).await; + } else { + conn.send_remote_printing_disallowed().await; + } + } + _ => {} + } + } + _ = second_timer.tick() => { + #[cfg(windows)] + conn.portable_check(); + raii::AuthedConnID::check_wake_lock_on_setting_changed(); + if let Some((instant, minute)) = conn.auto_disconnect_timer.as_ref() { + if instant.elapsed().as_secs() > minute * 60 { + conn.send_close_reason_no_retry("Connection failed due to inactivity").await; + conn.on_close("auto disconnect", true).await; + break; + } + } + conn.file_remove_log_control.on_timer().drain(..).map(|x| conn.send_to_cm(x)).count(); + #[cfg(feature = "hwcodec")] + conn.update_supported_encoding(); + } + _ = test_delay_timer.tick() => { + if last_recv_time.elapsed() >= SEC30 { + conn.on_close("Timeout", true).await; + break; + } + // The control end will jump out of the loop after receiving LoginResponse and will not reply to the TestDelay + if conn.last_test_delay.is_none() && !(conn.port_forward_socket.is_some() && conn.authorized) { + conn.last_test_delay = Some(Instant::now()); + let mut msg_out = Message::new(); + msg_out.set_test_delay(TestDelay{ + last_delay: conn.network_delay, + target_bitrate: video_service::VIDEO_QOS.lock().unwrap().bitrate(), + ..Default::default() + }); + conn.send(msg_out.into()).await; + } + if conn.is_authed_remote_conn() || conn.view_camera { + if let Some(last_test_delay) = conn.last_test_delay { + video_service::VIDEO_QOS.lock().unwrap().user_delay_response_elapsed(id, last_test_delay.elapsed().as_millis()); + } + } + } + clip_file = rx_clip.recv() => match clip_file { + Some(_clip) => { + #[cfg(feature = "unix-file-copy-paste")] + if crate::is_support_file_copy_paste(&conn.lr.version) + { + conn.handle_file_clip(_clip).await; + } + } + None => { + // + } + }, + } + } + + #[cfg(feature = "unix-file-copy-paste")] + { + conn.try_empty_file_clipboard(); + } + + if let Some(video_privacy_conn_id) = privacy_mode::get_privacy_mode_conn_id() { + if video_privacy_conn_id == id { + let _ = Self::turn_off_privacy_to_msg(id, String::new()); + } + } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::plugin::handle_listen_event( + crate::plugin::EVENT_ON_CONN_CLOSE_SERVER.to_owned(), + conn.lr.my_id.clone(), + ); + video_service::notify_video_frame_fetched_by_conn_id(id, None); + if conn.authorized { + password::update_temporary_password(); + } + if let Err(err) = conn.try_port_forward_loop(&mut rx_from_cm).await { + conn.on_close(&err.to_string(), false).await; + raii::AuthedConnID::check_remove_session(conn.inner.id(), conn.session_key()); + } + + conn.post_conn_audit(json!({ + "action": "close", + })); + if let Some(s) = conn.server.upgrade() { + let mut s = s.write().unwrap(); + s.remove_connection(&conn.inner); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + try_stop_record_cursor_pos(); + } + conn.on_close("End", true).await; + log::info!("#{} connection loop exited", id); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn handle_input(receiver: std_mpsc::Receiver, tx: Sender) { + let mut block_input_mode = false; + #[cfg(any(target_os = "windows", target_os = "macos"))] + { + rdev::set_mouse_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); + rdev::set_keyboard_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); + } + #[cfg(target_os = "macos")] + reset_input_ondisconn(); + loop { + match receiver.recv_timeout(std::time::Duration::from_millis(500)) { + Ok(v) => match v { + MessageInput::Mouse(mouse_input) => { + handle_mouse( + &mouse_input.msg, + mouse_input.conn_id, + mouse_input.username, + mouse_input.argb, + mouse_input.simulate, + mouse_input.show_cursor, + ); + } + MessageInput::Key((mut msg, press)) => { + // Set the press state to false, use `down` only in `handle_key()`. + msg.press = false; + if press { + msg.down = true; + } + handle_key(&msg); + if press { + msg.down = false; + handle_key(&msg); + } + } + MessageInput::Pointer((msg, id)) => { + handle_pointer(&msg, id); + } + MessageInput::BlockOn => { + let (ok, msg) = crate::platform::block_input(true); + if ok { + block_input_mode = true; + } else { + Self::send_block_input_error( + &tx, + back_notification::BlockInputState::BlkOnFailed, + msg, + ); + } + } + MessageInput::BlockOff => { + let (ok, msg) = crate::platform::block_input(false); + if ok { + block_input_mode = false; + } else { + Self::send_block_input_error( + &tx, + back_notification::BlockInputState::BlkOffFailed, + msg, + ); + } + } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + MessageInput::BlockOnPlugin(_peer) => { + let (ok, _msg) = crate::platform::block_input(true); + if ok { + block_input_mode = true; + } + let _r = PLUGIN_BLOCK_INPUT_TX_RX + .0 + .lock() + .unwrap() + .send(block_input_mode); + } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + MessageInput::BlockOffPlugin(_peer) => { + let (ok, _msg) = crate::platform::block_input(false); + if ok { + block_input_mode = false; + } + let _r = PLUGIN_BLOCK_INPUT_TX_RX + .0 + .lock() + .unwrap() + .send(block_input_mode); + } + }, + Err(err) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if block_input_mode { + let _ = crate::platform::block_input(true); + } + if std_mpsc::RecvTimeoutError::Disconnected == err { + break; + } + } + } + } + #[cfg(target_os = "linux")] + clear_remapped_keycode(); + log::debug!("Input thread exited"); + } + + async fn post_seq_loop(mut rx: mpsc::UnboundedReceiver<(String, Value)>) { + while let Some((url, v)) = rx.recv().await { + allow_err!(Self::post_audit_async(url, v).await); + } + log::debug!("post_seq_loop exited"); + } + + async fn try_port_forward_loop( + &mut self, + rx_from_cm: &mut mpsc::UnboundedReceiver, + ) -> ResultType<()> { + let mut last_recv_time = Instant::now(); + if let Some(mut forward) = self.port_forward_socket.take() { + log::info!("Running port forwarding loop"); + self.stream.set_raw(); + let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); + loop { + tokio::select! { + Some(data) = rx_from_cm.recv() => { + match data { + ipc::Data::Close => { + bail!("Close requested from connection manager"); + } + ipc::Data::CmErr(e) => { + log::error!("Connection manager error: {e}"); + bail!("{e}"); + } + _ => {} + } + } + res = forward.next() => { + if let Some(res) = res { + last_recv_time = Instant::now(); + self.stream.send_bytes(res?.into()).await?; + } else { + bail!("Forward reset by the peer"); + } + }, + res = self.stream.next() => { + if let Some(res) = res { + last_recv_time = Instant::now(); + timeout(SEND_TIMEOUT_OTHER, forward.send(res?)).await??; + } else { + bail!("Stream reset by the peer"); + } + }, + _ = self.timer.tick() => { + if last_recv_time.elapsed() >= H1 { + bail!("Timeout"); + } + } + Ok(conns) = hbbs_rx.recv() => { + if conns.contains(&self.inner.id) { + // todo: check reconnect + bail!("Closed manually by the web console"); + } + } + } + } + } + Ok(()) + } + + async fn send_permission(&mut self, permission: Permission, enabled: bool) { + let mut misc = Misc::new(); + misc.set_permission_info(PermissionInfo { + permission: permission.into(), + enabled, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(msg_out).await; + } + + async fn check_privacy_mode_on(&mut self) -> bool { + if privacy_mode::is_in_privacy_mode() { + self.send_login_error("Someone turns on privacy mode, exit") + .await; + false + } else { + true + } + } + + async fn check_whitelist(&mut self, addr: &SocketAddr) -> bool { + let whitelist: Vec = Config::get_option("whitelist") + .split(",") + .filter(|x| !x.is_empty()) + .map(|x| x.to_owned()) + .collect(); + if !whitelist.is_empty() + && whitelist + .iter() + .filter(|x| x == &"0.0.0.0") + .next() + .is_none() + && whitelist + .iter() + .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) + .next() + .is_none() + { + self.send_login_error("Your ip is blocked by the peer") + .await; + Self::post_alarm_audit( + AlarmAuditType::IpWhitelist, //"ip whitelist", + json!({ "ip":addr.ip() }), + ); + return false; + } + true + } + + async fn on_open(&mut self, addr: SocketAddr) -> bool { + log::debug!("#{} Connection opened from {}.", self.inner.id, addr); + if !self.check_whitelist(&addr).await { + return false; + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if crate::is_server() && Config::get_option("allow-only-conn-window-open") == "Y" { + if !crate::check_process("", !crate::platform::is_root()) { + self.send_login_error("The main window is not open").await; + return false; + } + } + self.ip = addr.ip().to_string(); + let mut msg_out = Message::new(); + msg_out.set_hash(self.hash.clone()); + self.send(msg_out).await; + self.get_api_server(); + self.post_conn_audit(json!({ + "ip": addr.ip(), + "action": "new", + })); + true + } + + fn get_api_server(&mut self) { + self.server_audit_conn = crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + "conn".to_owned(), + ); + self.server_audit_file = crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + "file".to_owned(), + ); + } + + fn post_conn_audit(&self, v: Value) { + if self.server_audit_conn.is_empty() { + return; + } + let url = self.server_audit_conn.clone(); + let mut v = v; + v["id"] = json!(Config::get_id()); + v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); + v["conn_id"] = json!(self.inner.id); + v["session_id"] = json!(self.lr.session_id); + allow_err!(self.tx_post_seq.send((url, v))); + } + + fn get_files_for_audit(job_type: fs::JobType, mut files: Vec) -> Vec<(String, i64)> { + files + .drain(..) + .map(|f| { + ( + if job_type == fs::JobType::Printer { + "Remote print".to_owned() + } else { + f.name + }, + f.size as _, + ) + }) + .collect() + } + + fn post_file_audit( + &self, + r#type: FileAuditType, + path: &str, + files: Vec<(String, i64)>, + info: Value, + ) { + if self.server_audit_file.is_empty() { + return; + } + let url = self.server_audit_file.clone(); + let file_num = files.len(); + let mut files = files; + files.sort_by(|a, b| b.1.cmp(&a.1)); + files.truncate(10); + let is_file = files.len() == 1 && files[0].0.is_empty(); + let mut info = info; + info["ip"] = json!(self.ip.clone()); + info["name"] = json!(self.lr.my_name.clone()); + info["num"] = json!(file_num); + info["files"] = json!(files); + let v = json!({ + "id":json!(Config::get_id()), + "uuid":json!(crate::encode64(hbb_common::get_uuid())), + "peer_id":json!(self.lr.my_id), + "type": r#type as i8, + "path":path, + "is_file":is_file, + "info":json!(info).to_string(), + }); + tokio::spawn(async move { + allow_err!(Self::post_audit_async(url, v).await); + }); + } + + pub fn post_alarm_audit(typ: AlarmAuditType, info: Value) { + let url = crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + "alarm".to_owned(), + ); + if url.is_empty() { + return; + } + let mut v = Value::default(); + v["id"] = json!(Config::get_id()); + v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); + v["typ"] = json!(typ as i8); + v["info"] = serde_json::Value::String(info.to_string()); + tokio::spawn(async move { + allow_err!(Self::post_audit_async(url, v).await); + }); + } + + #[inline] + async fn post_audit_async(url: String, v: Value) -> ResultType { + crate::post_request(url, v.to_string(), "").await + } + + fn normalize_port_forward_target(pf: &mut PortForward) -> (String, bool) { + let mut is_rdp = false; + if pf.host == "RDP" && pf.port == 0 { + pf.host = "localhost".to_owned(); + pf.port = 3389; + is_rdp = true; + } + if pf.host.is_empty() { + pf.host = "localhost".to_owned(); + } + (format!("{}:{}", pf.host, pf.port), is_rdp) + } + + async fn connect_port_forward_if_needed(&mut self) -> bool { + if self.port_forward_socket.is_some() { + return true; + } + let Some(login_request::Union::PortForward(pf)) = self.lr.union.as_ref() else { + return true; + }; + let mut pf = pf.clone(); + let (mut addr, is_rdp) = Self::normalize_port_forward_target(&mut pf); + self.port_forward_address = addr.clone(); + match timeout(3000, TcpStream::connect(&addr)).await { + Ok(Ok(sock)) => { + self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new())); + true + } + Ok(Err(e)) => { + log::warn!("Port forward connect failed for {}: {}", addr, e); + if is_rdp { + addr = "RDP".to_owned(); + } + self.send_login_error(format!( + "Failed to access remote {}. Please make sure it is reachable/open.", + addr + )) + .await; + false + } + Err(e) => { + log::warn!("Port forward connect timed out for {}: {}", addr, e); + if is_rdp { + addr = "RDP".to_owned(); + } + self.send_login_error(format!( + "Failed to access remote {}. Please make sure it is reachable/open.", + addr + )) + .await; + false + } + } + } + + // Returns whether this connection should be kept alive. + // `true` does not necessarily mean authorization succeeded (e.g. REQUIRE_2FA case). + async fn send_logon_response_and_keep_alive(&mut self) -> bool { + if self.authorized { + return true; + } + if self.require_2fa.is_some() && !self.is_recent_session(true) && !self.from_switch { + self.require_2fa.as_ref().map(|totp| { + let bot = crate::auth_2fa::TelegramBot::get(); + let bot = match bot { + Ok(Some(bot)) => bot, + Err(err) => { + log::error!("Failed to get telegram bot: {}", err); + return; + } + _ => return, + }; + let code = totp.generate_current(); + if let Ok(code) = code { + let text = format!( + "2FA code: {}\n\nA new connection has been established to your device with ID {}. The source IP address is {}.", + code, + Config::get_id(), + self.ip, + ); + tokio::spawn(async move { + if let Err(err) = + crate::auth_2fa::send_2fa_code_to_telegram(&text, bot).await + { + log::error!("Failed to send 2fa code to telegram bot: {}", err); + } + }); + } + }); + self.send_login_error(crate::client::REQUIRE_2FA).await; + // Keep the connection alive so the client can continue with 2FA. + return true; + } + if !self.connect_port_forward_if_needed().await { + return false; + } + self.authorized = true; + let (conn_type, auth_conn_type) = if self.file_transfer.is_some() { + (1, AuthConnType::FileTransfer) + } else if self.port_forward_socket.is_some() { + (2, AuthConnType::PortForward) + } else if self.view_camera { + (3, AuthConnType::ViewCamera) + } else if self.terminal { + (4, AuthConnType::Terminal) + } else { + (0, AuthConnType::Remote) + }; + self.authed_conn_id = Some(self::raii::AuthedConnID::new( + self.inner.id(), + auth_conn_type, + self.session_key(), + self.tx_from_authed.clone(), + self.lr.clone(), + )); + self.session_last_recv_time = SESSIONS + .lock() + .unwrap() + .get(&self.session_key()) + .map(|s| s.last_recv_time.clone()); + self.post_conn_audit( + json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type}), + ); + #[allow(unused_mut)] + let mut username = crate::platform::get_active_username(); + let mut res = LoginResponse::new(); + let mut pi = PeerInfo { + username: username.clone(), + version: VERSION.to_owned(), + ..Default::default() + }; + + #[cfg(not(target_os = "android"))] + { + pi.hostname = crate::whoami_hostname(); + pi.platform = hbb_common::whoami::platform().to_string(); + } + #[cfg(target_os = "android")] + { + pi.hostname = DEVICE_NAME.lock().unwrap().clone(); + pi.platform = "Android".into(); + } + #[cfg(all(target_os = "macos", not(feature = "unix-file-copy-paste")))] + let mut platform_additions = serde_json::Map::new(); + #[cfg(any( + target_os = "windows", + target_os = "linux", + all(target_os = "macos", feature = "unix-file-copy-paste") + ))] + let mut platform_additions = serde_json::Map::new(); + #[cfg(target_os = "linux")] + { + if crate::platform::current_is_wayland() { + platform_additions.insert("is_wayland".into(), json!(true)); + } + #[cfg(target_os = "linux")] + if crate::platform::is_headless_allowed() { + if linux_desktop_manager::is_headless() { + platform_additions.insert("headless".into(), json!(true)); + } + } + } + #[cfg(target_os = "windows")] + { + platform_additions.insert( + "is_installed".into(), + json!(crate::platform::is_installed()), + ); + if crate::platform::is_installed() { + platform_additions.extend(virtual_display_manager::get_platform_additions()); + } + platform_additions.insert( + "supported_privacy_mode_impl".into(), + json!(privacy_mode::get_supported_privacy_mode_impl()), + ); + } + #[cfg(target_os = "macos")] + { + platform_additions.insert( + "supported_privacy_mode_impl".into(), + json!(privacy_mode::get_supported_privacy_mode_impl()), + ); + } + + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + { + let is_both_windows = cfg!(target_os = "windows") + && self.lr.my_platform == hbb_common::whoami::Platform::Windows.to_string(); + #[cfg(feature = "unix-file-copy-paste")] + let is_unix_and_peer_supported = crate::is_support_file_copy_paste(&self.lr.version); + #[cfg(not(feature = "unix-file-copy-paste"))] + let is_unix_and_peer_supported = false; + let is_both_macos = cfg!(target_os = "macos") + && self.lr.my_platform == hbb_common::whoami::Platform::MacOS.to_string(); + let is_peer_support_paste_if_macos = + crate::is_support_file_paste_if_macos(&self.lr.version); + let has_file_clipboard = is_both_windows + || (is_unix_and_peer_supported + && (!is_both_macos || is_peer_support_paste_if_macos)); + platform_additions.insert("has_file_clipboard".into(), json!(has_file_clipboard)); + } + + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + platform_additions.insert("support_view_camera".into(), json!(true)); + } + + #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] + if !platform_additions.is_empty() { + pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into()); + } + + if self.port_forward_socket.is_some() { + let mut msg_out = Message::new(); + res.set_peer_info(pi); + msg_out.set_login_response(res); + self.send(msg_out).await; + return true; + } + #[cfg(target_os = "linux")] + if self.is_remote() { + let mut msg = "".to_string(); + if crate::platform::linux::is_login_screen_wayland() { + msg = crate::client::LOGIN_SCREEN_WAYLAND.to_owned() + } else { + let dtype = crate::platform::linux::get_display_server(); + if dtype != crate::platform::linux::DISPLAY_SERVER_X11 + && dtype != crate::platform::linux::DISPLAY_SERVER_WAYLAND + { + msg = format!( + "Unsupported display server type \"{}\", x11 or wayland expected", + dtype + ); + } + } + if !msg.is_empty() { + res.set_error(msg); + let mut msg_out = Message::new(); + msg_out.set_login_response(res); + self.send(msg_out).await; + return true; + } + } + #[allow(unused_mut)] + let mut sas_enabled = false; + #[cfg(windows)] + if crate::platform::is_root() { + sas_enabled = true; + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.file_transfer.is_some() { + if crate::platform::is_prelogin() { + // }|| self.tx_to_cm.send(ipc::Data::Test).is_err() { + username = "".to_owned(); + } + } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + PLUGIN_BLOCK_INPUT_TXS + .lock() + .unwrap() + .insert(self.lr.my_id.clone(), self.tx_input.clone()); + + // Terminal feature is supported on desktop only + #[allow(unused_mut)] + let mut terminal = cfg!(not(any(target_os = "android", target_os = "ios"))); + #[cfg(target_os = "windows")] + { + terminal = terminal && portable_pty::win::check_support().is_ok(); + } + pi.username = username; + pi.sas_enabled = sas_enabled; + pi.features = Some(Features { + privacy_mode: privacy_mode::is_privacy_mode_supported(), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal, + ..Default::default() + }) + .into(); + + let mut sub_service = false; + #[allow(unused_mut)] + let mut wait_session_id_confirm = false; + #[cfg(windows)] + if !self.terminal { + self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); + } + if self.file_transfer.is_some() || self.terminal { + res.set_peer_info(pi); + } else if self.view_camera { + let supported_encoding = scrap::codec::Encoder::supported_encoding(); + self.last_supported_encoding = Some(supported_encoding.clone()); + log::info!("peer info supported_encoding: {:?}", supported_encoding); + pi.encoding = Some(supported_encoding).into(); + + pi.displays = camera::Cameras::all_info().unwrap_or(Vec::new()); + pi.current_display = camera::PRIMARY_CAMERA_IDX as _; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: camera::Cameras::get_camera_resolution( + pi.current_display as usize, + ) + .ok() + .into_iter() + .collect(), + ..Default::default() + }) + .into(); + } + res.set_peer_info(pi); + self.update_codec_on_login(); + } else { + let supported_encoding = scrap::codec::Encoder::supported_encoding(); + self.last_supported_encoding = Some(supported_encoding.clone()); + log::info!("peer info supported_encoding: {:?}", supported_encoding); + pi.encoding = Some(supported_encoding).into(); + if let Some(msg_out) = super::display_service::is_inited_msg() { + self.send(msg_out).await; + } + + try_activate_screen(); + + match super::display_service::update_get_sync_displays_on_login().await { + Err(err) => { + res.set_error(format!("{}", err)); + } + Ok(displays) => { + // For compatibility with old versions, we need to send the displays to the peer. + // But the displays may be updated later, before creating the video capturer. + #[cfg(target_os = "macos")] + { + self.retina.set_displays(&displays); + } + pi.displays = displays; + pi.current_display = self.display_idx as _; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: pi + .displays + .get(self.display_idx) + .map(|d| crate::platform::resolutions(&d.name)) + .unwrap_or(vec![]), + ..Default::default() + }) + .into(); + } + res.set_peer_info(pi); + sub_service = true; + + #[cfg(target_os = "linux")] + { + // use rdp_input when uinput is not available in wayland. Ex: flatpak + if input_service::wayland_use_rdp_input() { + let _ = setup_rdp_input().await; + } + } + } + } + self.on_remote_authorized(); + } + let mut msg_out = Message::new(); + msg_out.set_login_response(res); + self.send(msg_out).await; + if let Some(o) = self.options_in_login.take() { + self.update_options(&o).await; + } + if let Some((dir, show_hidden)) = self.file_transfer.clone() { + let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() { + &dir + } else { + "" + }; + if !wait_session_id_confirm { + self.read_dir(dir, show_hidden); + } else { + self.delayed_read_dir = Some((dir.to_owned(), show_hidden)); + } + } else if self.terminal { + self.keyboard = false; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.init_terminal_service().await; + } else if self.view_camera { + if !wait_session_id_confirm { + self.try_sub_camera_displays(); + } + self.keyboard = false; + self.send_permission(Permission::Keyboard, false).await; + } else if sub_service { + if !wait_session_id_confirm { + self.try_sub_monitor_services(); + } + } + true + } + + fn try_sub_camera_displays(&mut self) { + if let Some(s) = self.server.upgrade() { + let mut s = s.write().unwrap(); + + s.try_add_primary_camera_service(); + s.add_camera_connection(self.inner.clone()); + } + } + + #[inline] + fn is_remote(&self) -> bool { + self.file_transfer.is_none() + && self.port_forward_socket.is_none() + && !self.view_camera + && !self.terminal + } + + fn try_sub_monitor_services(&mut self) { + let is_remote = self.is_remote(); + if is_remote && !self.services_subed { + self.services_subed = true; + if let Some(s) = self.server.upgrade() { + let mut noperms = Vec::new(); + if !self.peer_keyboard_enabled() && !self.show_remote_cursor { + noperms.push(NAME_CURSOR); + } + if !self.show_remote_cursor { + noperms.push(NAME_POS); + } + if !self.follow_remote_window { + noperms.push(NAME_WINDOW_FOCUS); + } + if !self.can_sub_clipboard_service() { + noperms.push(super::clipboard_service::NAME); + } + #[cfg(feature = "unix-file-copy-paste")] + if !self.can_sub_file_clipboard_service() { + noperms.push(super::clipboard_service::FILE_NAME); + } + if !self.audio_enabled() { + noperms.push(super::audio_service::NAME); + } + let mut s = s.write().unwrap(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let _h = try_start_record_cursor_pos(); + self.auto_disconnect_timer = Self::get_auto_disconenct_timer(); + s.try_add_primay_video_service(); + s.add_connection(self.inner.clone(), &noperms); + } + } + } + + #[cfg(windows)] + fn handle_windows_specific_session( + &mut self, + pi: &mut PeerInfo, + wait_session_id_confirm: &mut bool, + ) { + let sessions = crate::platform::get_available_sessions(true); + if let Some(current_sid) = crate::platform::get_current_process_session_id() { + if crate::platform::is_installed() + && crate::platform::is_share_rdp() + && raii::AuthedConnID::non_port_forward_conn_count() == 1 + && sessions.len() > 1 + && sessions.iter().any(|e| e.sid == current_sid) + && get_version_number(&self.lr.version) >= get_version_number("1.2.4") + { + pi.windows_sessions = Some(WindowsSessions { + sessions, + current_sid, + ..Default::default() + }) + .into(); + *wait_session_id_confirm = true; + } + } + } + + fn on_remote_authorized(&self) { + self.update_codec_on_login(); + #[cfg(any(target_os = "windows", target_os = "linux"))] + if config::option2bool( + "allow-remove-wallpaper", + &Config::get_option("allow-remove-wallpaper"), + ) { + // multi connections set once + let mut wallpaper = WALLPAPER_REMOVER.lock().unwrap(); + if wallpaper.is_none() { + match crate::platform::WallPaperRemover::new() { + Ok(remover) => { + *wallpaper = Some(remover); + } + Err(e) => { + log::info!("create wallpaper remover failed: {:?}", e); + } + } + } + } + } + + fn peer_keyboard_enabled(&self) -> bool { + self.keyboard && !self.disable_keyboard + } + + fn clipboard_enabled(&self) -> bool { + self.clipboard && !self.disable_clipboard + } + + #[inline] + fn can_sub_clipboard_service(&self) -> bool { + self.clipboard_enabled() + && self.peer_keyboard_enabled() + && crate::get_builtin_option(keys::OPTION_ONE_WAY_CLIPBOARD_REDIRECTION) != "Y" + } + + fn audio_enabled(&self) -> bool { + self.audio && !self.disable_audio + } + + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + fn file_transfer_enabled(&self) -> bool { + self.file && self.enable_file_transfer + } + + #[cfg(feature = "unix-file-copy-paste")] + fn can_sub_file_clipboard_service(&self) -> bool { + self.clipboard_enabled() + && self.file_transfer_enabled() + && crate::get_builtin_option(keys::OPTION_ONE_WAY_FILE_TRANSFER) != "Y" + } + + fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) { + self.send_to_cm(ipc::Data::Login { + id: self.inner.id(), + is_file_transfer: self.file_transfer.is_some(), + is_view_camera: self.view_camera, + is_terminal: self.terminal, + port_forward: self.port_forward_address.clone(), + peer_id, + name, + avatar: self.lr.avatar.clone(), + authorized, + keyboard: self.keyboard, + clipboard: self.clipboard, + audio: self.audio, + file: self.file, + file_transfer_enabled: self.file, + restart: self.restart, + recording: self.recording, + block_input: self.block_input, + privacy_mode: self.privacy_mode, + from_switch: self.from_switch, + }); + } + + #[inline] + fn send_to_cm(&mut self, data: ipc::Data) { + self.tx_to_cm.send(data).ok(); + } + + #[inline] + fn send_fs(&mut self, data: ipc::FS) { + self.send_to_cm(ipc::Data::FS(data)); + } + + async fn send_login_error(&mut self, err: T) { + let mut msg_out = Message::new(); + let mut res = LoginResponse::new(); + res.set_error(err.to_string()); + if err.to_string() == crate::client::REQUIRE_2FA { + res.enable_trusted_devices = Self::enable_trusted_devices(); + } + msg_out.set_login_response(res); + self.send(msg_out).await; + } + + #[inline] + pub fn send_block_input_error( + s: &Sender, + state: back_notification::BlockInputState, + details: String, + ) { + let mut misc = Misc::new(); + let mut back_notification = BackNotification { + details, + ..Default::default() + }; + back_notification.set_block_input_state(state); + misc.set_back_notification(back_notification); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + s.send((Instant::now(), Arc::new(msg_out))).ok(); + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn input_mouse( + &self, + msg: MouseEvent, + conn_id: i32, + username: String, + argb: u32, + simulate: bool, + show_cursor: bool, + ) { + self.tx_input + .send(MessageInput::Mouse(InputMouse { + msg, + conn_id, + username, + argb, + simulate, + show_cursor, + })) + .ok(); + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn input_pointer(&self, msg: PointerDeviceEvent, conn_id: i32) { + self.tx_input + .send(MessageInput::Pointer((msg, conn_id))) + .ok(); + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn input_key(&self, msg: KeyEvent, press: bool) { + // to-do: if is the legacy mode, and the key is function key "LockScreen". + // Switch to the primary display. + self.tx_input.send(MessageInput::Key((msg, press))).ok(); + } + + fn verify_h1(&self, h1: &[u8]) -> bool { + let mut hasher2 = Sha256::new(); + hasher2.update(h1); + hasher2.update(self.hash.challenge.as_bytes()); + // A normal `==` on slices may short-circuit on the first mismatch, which can leak how many leading + // bytes matched via timing. In typical remote scenarios this is difficult to exploit due to network + // jitter, changing challenges, and login attempt throttling, but a constant-time comparison here is + // low-cost defensive programming. + constant_time_eq(&hasher2.finalize()[..], &self.lr.password[..]) + } + + fn validate_password_plain(&self, password: &str) -> bool { + if password.is_empty() { + return false; + } + + let mut hasher = Sha256::new(); + hasher.update(password.as_bytes()); + hasher.update(self.hash.salt.as_bytes()); + let h1_plain = hasher.finalize(); + self.verify_h1(&h1_plain[..]) + } + + fn validate_password_storage(&self, storage: &str) -> bool { + if storage.is_empty() { + return false; + } + + // Use strict decode success to detect hashed storage. + // If decode fails, treat as legacy plaintext storage for compatibility. + if let Some(h1) = decode_permanent_password_h1_from_storage(storage) { + return self.verify_h1(&h1[..]); + } + + // Legacy plaintext storage path. + self.validate_password_plain(storage) + } + + // This is coarse brute-force protection for the current temporary password value. + // We only care whether the active temporary password itself was presented correctly, + // not whether later authorization steps succeed. A successful temporary-password + // match clears this state immediately, and the counter also resets whenever the + // temporary password changes or is rotated. + fn check_update_temporary_password(&self, temporary_password_success: bool) { + const MAX_CONSECUTIVE_FAILURES: i32 = 10; + #[derive(Default)] + struct State { + password: String, + failures: i32, + } + lazy_static::lazy_static! { + static ref TEMPORARY_PASSWORD_FAILURES: Mutex = + Mutex::new(State::default()); + } + + if !password::temporary_enabled() { + return; + } + + let mut state = TEMPORARY_PASSWORD_FAILURES.lock().unwrap(); + let current_password = password::temporary_password(); + if current_password.is_empty() { + return; + } + if state.password != current_password { + state.password = current_password; + state.failures = 0; + } + + if temporary_password_success { + state.failures = 0; + return; + } + state.failures += 1; + + if state.failures < MAX_CONSECUTIVE_FAILURES { + return; + } + + password::update_temporary_password(); + let new_password = password::temporary_password(); + log::warn!( + "Temporary password rotated after too many consecutive wrong attempts: failures={}, ip={}", + state.failures, + self.ip, + ); + state.password = new_password; + state.failures = 0; + } + + fn validate_password(&mut self, allow_permanent_password: bool) -> bool { + if password::temporary_enabled() { + let password = password::temporary_password(); + if self.validate_password_plain(&password) { + raii::AuthedConnID::update_or_insert_session( + self.session_key(), + Some(password), + Some(false), + ); + self.check_update_temporary_password(true); + return true; + } + } + if password::permanent_enabled() || allow_permanent_password { + let print_fallback = || { + if allow_permanent_password && !password::permanent_enabled() { + log::info!("Permanent password accepted via logon-screen fallback"); + } + }; + // Since hashed storage uses a prefix-based encoding, a hard plaintext that + // happens to look like hashed storage could be mis-detected. Validate local storage + // and hard/preset plaintext via separate paths to avoid that ambiguity. + let (local_storage, _) = Config::get_local_permanent_password_storage_and_salt(); + if !local_storage.is_empty() { + if self.validate_password_storage(&local_storage) { + print_fallback(); + return true; + } + } else { + let hard = config::HARD_SETTINGS + .read() + .unwrap() + .get("password") + .cloned() + .unwrap_or_default(); + if !hard.is_empty() && self.validate_password_plain(&hard) { + print_fallback(); + return true; + } + } + } + false + } + + fn is_recent_session(&mut self, tfa: bool) -> bool { + SESSIONS + .lock() + .unwrap() + .retain(|_, s| s.last_recv_time.lock().unwrap().elapsed() < SESSION_TIMEOUT); + let session = SESSIONS + .lock() + .unwrap() + .get(&self.session_key()) + .map(|s| s.to_owned()); + // last_recv_time is a mutex variable shared with connection, can be updated lively. + if let Some(session) = session { + if !self.lr.password.is_empty() + && (tfa && session.tfa + || !tfa && self.validate_password_plain(&session.random_password)) + { + log::info!("is recent session"); + return true; + } + } + false + } + + #[inline] + pub fn is_permission_enabled_locally(enable_prefix_option: &str) -> bool { + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let access_mode = Config::get_option("access-mode"); + if access_mode == "full" { + return true; + } else if access_mode == "view" { + return false; + } + } + config::option2bool( + enable_prefix_option, + &Config::get_option(enable_prefix_option), + ) + } + + fn permission( + enable_prefix_option: &str, + control_permissions: &Option, + ) -> bool { + use hbb_common::rendezvous_proto::control_permissions::Permission; + if let Some(control_permissions) = control_permissions { + let permission = match enable_prefix_option { + keys::OPTION_ENABLE_KEYBOARD => Some(Permission::keyboard), + keys::OPTION_ENABLE_REMOTE_PRINTER => Some(Permission::remote_printer), + keys::OPTION_ENABLE_CLIPBOARD => Some(Permission::clipboard), + keys::OPTION_ENABLE_FILE_TRANSFER => Some(Permission::file), + keys::OPTION_ENABLE_AUDIO => Some(Permission::audio), + keys::OPTION_ENABLE_CAMERA => Some(Permission::camera), + keys::OPTION_ENABLE_TERMINAL => Some(Permission::terminal), + keys::OPTION_ENABLE_TUNNEL => Some(Permission::tunnel), + keys::OPTION_ENABLE_REMOTE_RESTART => Some(Permission::restart), + keys::OPTION_ENABLE_RECORD_SESSION => Some(Permission::recording), + keys::OPTION_ENABLE_BLOCK_INPUT => Some(Permission::block_input), + keys::OPTION_ENABLE_PRIVACY_MODE => Some(Permission::privacy_mode), + _ => None, + }; + if let Some(permission) = permission { + if let Some(enabled) = + crate::get_control_permission(control_permissions.permissions, permission) + { + return enabled; + } + } + } + Self::is_permission_enabled_locally(enable_prefix_option) + } + + fn update_codec_on_login(&self) { + use scrap::codec::{Encoder, EncodingUpdate::*}; + if let Some(o) = self.lr.clone().option.as_ref() { + if let Some(q) = o.supported_decoding.clone().take() { + Encoder::update(Update(self.inner.id(), q)); + } else { + Encoder::update(NewOnlyVP9(self.inner.id())); + } + } else { + Encoder::update(NewOnlyVP9(self.inner.id())); + } + } + + #[inline] + fn enable_trusted_devices() -> bool { + config::option2bool( + keys::OPTION_ENABLE_TRUSTED_DEVICES, + &Config::get_option(keys::OPTION_ENABLE_TRUSTED_DEVICES), + ) + } + + async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) { + self.lr = lr.clone(); + self.peer_argb = crate::str2color(&format!("{}{}", &lr.my_id, &lr.my_platform), 0xff); + if let Some(o) = lr.option.as_ref() { + self.options_in_login = Some(o.clone()); + } + if self.require_2fa.is_some() && !lr.hwid.is_empty() && Self::enable_trusted_devices() { + let devices = Config::get_trusted_devices(); + if let Some(device) = devices.iter().find(|d| d.hwid == lr.hwid) { + if !device.outdate() + && device.id == lr.my_id + && device.name == lr.my_name + && device.platform == lr.my_platform + { + log::info!("2FA bypassed by trusted devices"); + self.require_2fa = None; + } + } + } + self.video_ack_required = lr.video_ack_required; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn try_start_cm_ipc(&mut self) { + if let Some(p) = self.start_cm_ipc_para.take() { + tokio::spawn(async move { + #[cfg(windows)] + let tx_from_cm_clone = p.tx_from_cm.clone(); + if let Err(err) = start_ipc( + p.rx_to_cm, + p.tx_from_cm, + p.rx_desktop_ready, + p.tx_cm_stream_ready, + ) + .await + { + log::warn!("ipc to connection manager exit: {}", err); + // https://github.com/rustdesk/rustdesk-server-pro/discussions/382#discussioncomment-10525725, cm may start failed + #[cfg(windows)] + if !crate::platform::is_prelogin() + && !err.to_string().contains(crate::platform::EXPLORER_EXE) + && !crate::hbbs_http::sync::is_pro() + { + allow_err!(tx_from_cm_clone.send(Data::CmErr(err.to_string()))); + } + } + }); + #[cfg(all(windows, feature = "flutter"))] + std::thread::spawn(move || { + if crate::is_server() && !crate::check_process("--tray", false) { + crate::platform::run_as_user(vec!["--tray"]).ok(); + } + }); + } + } + + async fn on_message(&mut self, msg: Message) -> bool { + if let Some(message::Union::Misc(misc)) = &msg.union { + // Move the CloseReason forward, as this message needs to be received when unauthorized, especially for kcp. + if let Some(misc::Union::CloseReason(s)) = &misc.union { + log::info!("receive close reason: {}", s); + self.on_close("Peer close", true).await; + raii::AuthedConnID::check_remove_session(self.inner.id(), self.session_key()); + return false; + } + } + // After handling CloseReason messages, proceed to process other message types + if let Some(message::Union::LoginRequest(lr)) = msg.union { + self.handle_login_request_without_validation(&lr).await; + if self.authorized { + return true; + } + match lr.union { + Some(login_request::Union::FileTransfer(ft)) => { + if !Self::permission( + keys::OPTION_ENABLE_FILE_TRANSFER, + &self.control_permissions, + ) { + self.send_login_error("No permission of file transfer") + .await; + sleep(1.).await; + return false; + } + self.file_transfer = Some((ft.dir, ft.show_hidden)); + } + Some(login_request::Union::ViewCamera(_vc)) => { + if !Self::permission(keys::OPTION_ENABLE_CAMERA, &self.control_permissions) { + self.send_login_error("No permission of viewing camera") + .await; + sleep(1.).await; + return false; + } + self.view_camera = true; + } + Some(login_request::Union::Terminal(terminal)) => { + if !Self::permission(keys::OPTION_ENABLE_TERMINAL, &self.control_permissions) { + self.send_login_error("No permission of terminal").await; + sleep(1.).await; + return false; + } + #[cfg(target_os = "windows")] + if !lr.os_login.username.is_empty() && !crate::platform::is_installed() { + self.send_login_error("Supported only in the installed version.") + .await; + sleep(1.).await; + return false; + } + + self.terminal = true; + if let Some(o) = self.options_in_login.as_ref() { + self.terminal_persistent = + o.terminal_persistent.enum_value() == Ok(BoolOption::Yes); + } + self.terminal_service_id = terminal.service_id; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(msg) = + self.fill_terminal_user_token(&lr.os_login.username, &lr.os_login.password) + { + self.send_login_error(msg).await; + sleep(1.).await; + return false; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(is_user) = + terminal_service::is_service_specified_user(&self.terminal_service_id) + { + if let Some(user_token) = &self.terminal_user_token { + let has_service_token = + user_token.to_terminal_service_token().is_some(); + if is_user != has_service_token { + // This occurs when the service id (in the configuration) is manually changed by the user, causing a mismatch in validation. + log::error!("Terminal service user mismatch detected. The service ID may have been manually changed in the configuration, causing validation to fail."); + // No need to translate the following message, because it is in an abnormal case. + self.send_login_error("Terminal service user mismatch detected.") + .await; + sleep(1.).await; + return false; + } + } + } + } + Some(login_request::Union::PortForward(mut pf)) => { + if !Self::permission(keys::OPTION_ENABLE_TUNNEL, &self.control_permissions) { + self.send_login_error("No permission of IP tunneling").await; + sleep(1.).await; + return false; + } + let (addr, _is_rdp) = Self::normalize_port_forward_target(&mut pf); + self.port_forward_address = addr; + } + _ => { + if !self.check_privacy_mode_on().await { + return false; + } + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.try_start_cm_ipc(); + + #[cfg(not(target_os = "linux"))] + let err_msg = "".to_owned(); + #[cfg(target_os = "linux")] + let err_msg = self + .linux_headless_handle + .try_start_desktop(lr.os_login.as_ref()); + + // If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password. + if !err_msg.is_empty() && err_msg != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY + { + self.send_login_error(err_msg).await; + return true; + } + + // https://github.com/rustdesk/rustdesk-server-pro/discussions/646 + // `is_logon` is used to check login with `OPTION_ALLOW_LOGON_SCREEN_PASSWORD` == "Y". + // `is_logon_ui()` is a fallback for logon UI detection on Windows. + #[cfg(target_os = "windows")] + let is_logon = || { + crate::platform::is_prelogin() || crate::platform::is_locked() || { + match crate::platform::is_logon_ui() { + Ok(result) => result, + Err(e) => { + log::error!("Failed to detect logon UI: {:?}", e); + false + } + } + } + }; + #[cfg(any(target_os = "linux", target_os = "macos"))] + let is_logon = || crate::platform::is_prelogin() || crate::platform::is_locked(); + #[cfg(any(target_os = "android", target_os = "ios"))] + let is_logon = || crate::platform::is_prelogin(); + + let allow_logon_screen_password = + crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y" + && is_logon(); + + if !hbb_common::is_ip_str(&lr.username) + && !hbb_common::is_domain_port_str(&lr.username) + && lr.username != Config::get_id() + { + self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) + .await; + return false; + } else if (password::approve_mode() == ApproveMode::Click + && !allow_logon_screen_password) + || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() + { + self.try_start_cm(lr.my_id, lr.my_name, false); + if hbb_common::get_version_number(&lr.version) + >= hbb_common::get_version_number("1.2.0") + { + self.send_login_error(crate::client::LOGIN_MSG_NO_PASSWORD_ACCESS) + .await; + } + return true; + } else if self.is_recent_session(false) { + if err_msg.is_empty() { + #[cfg(target_os = "linux")] + self.linux_headless_handle.wait_desktop_cm_ready().await; + if !self.send_logon_response_and_keep_alive().await { + return false; + } + self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), self.authorized); + } else { + self.send_login_error(err_msg).await; + } + } else if lr.password.is_empty() { + if err_msg.is_empty() { + self.try_start_cm(lr.my_id, lr.my_name, false); + } else { + self.send_login_error( + crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY, + ) + .await; + } + } else { + let (failure, res) = self.check_failure(0).await; + if !res { + return true; + } + if !self.validate_password(allow_logon_screen_password) { + self.update_failure(failure, false, 0); + self.check_update_temporary_password(false); + if err_msg.is_empty() { + self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) + .await; + self.try_start_cm(lr.my_id, lr.my_name, false); + } else { + self.send_login_error( + crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG, + ) + .await; + } + } else { + self.update_failure(failure, true, 0); + if err_msg.is_empty() { + #[cfg(target_os = "linux")] + self.linux_headless_handle.wait_desktop_cm_ready().await; + if !self.send_logon_response_and_keep_alive().await { + return false; + } + self.try_start_cm(lr.my_id, lr.my_name, self.authorized); + } else { + self.send_login_error(err_msg).await; + } + } + } + } else if let Some(message::Union::Auth2fa(tfa)) = msg.union { + let (failure, res) = self.check_failure(1).await; + if !res { + return true; + } + if let Some(totp) = self.require_2fa.as_ref() { + if let Ok(res) = totp.check_current(&tfa.code) { + if res { + self.update_failure(failure, true, 1); + self.require_2fa.take(); + raii::AuthedConnID::set_session_2fa(self.session_key()); + if !self.send_logon_response_and_keep_alive().await { + return false; + } + self.try_start_cm( + self.lr.my_id.to_owned(), + self.lr.my_name.to_owned(), + self.authorized, + ); + if !tfa.hwid.is_empty() && Self::enable_trusted_devices() { + Config::add_trusted_device(TrustedDevice { + hwid: tfa.hwid, + time: hbb_common::get_time(), + id: self.lr.my_id.clone(), + name: self.lr.my_name.clone(), + platform: self.lr.my_platform.clone(), + }); + } + } else { + self.update_failure(failure, false, 1); + self.send_login_error(crate::client::LOGIN_MSG_2FA_WRONG) + .await; + } + } + } + } else if let Some(message::Union::TestDelay(t)) = msg.union { + if t.from_client { + let mut msg_out = Message::new(); + msg_out.set_test_delay(t); + self.inner.send(msg_out.into()); + } else { + if let Some(tm) = self.last_test_delay { + self.last_test_delay = None; + let new_delay = tm.elapsed().as_millis() as u32; + video_service::VIDEO_QOS + .lock() + .unwrap() + .user_network_delay(self.inner.id(), new_delay); + self.network_delay = new_delay; + } + } + } else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union { + #[cfg(feature = "flutter")] + if let Some(lr) = _s.lr.clone().take() { + self.handle_login_request_without_validation(&lr).await; + SWITCH_SIDES_UUID + .lock() + .unwrap() + .retain(|_, v| v.0.elapsed() < Duration::from_secs(10)); + let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id); + if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) { + if let Some((_instant, uuid_old)) = uuid_old { + if uuid == uuid_old { + self.from_switch = true; + if !self.send_logon_response_and_keep_alive().await { + return false; + } + self.try_start_cm( + lr.my_id.clone(), + lr.my_name.clone(), + self.authorized, + ); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.try_start_cm_ipc(); + } + } + } + } + } else if self.authorized { + if self.port_forward_socket.is_some() { + return true; + } + match msg.union { + #[allow(unused_mut)] + Some(message::Union::MouseEvent(mut me)) => { + if self.is_authed_view_camera_conn() { + return true; + } + #[cfg(any(target_os = "android", target_os = "ios"))] + if let Err(e) = call_main_service_pointer_input("mouse", me.mask, me.x, me.y) { + log::debug!("call_main_service_pointer_input fail:{}", e); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.peer_keyboard_enabled() { + if is_left_up(&me) { + CLICK_TIME.store(get_time(), Ordering::SeqCst); + } else { + MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst); + } + #[cfg(target_os = "macos")] + self.retina.on_mouse_event(&mut me, self.display_idx); + self.input_mouse( + me, + self.inner.id(), + self.lr.my_name.clone(), + self.peer_argb, + true, + self.show_my_cursor, + ); + } else if self.show_my_cursor { + #[cfg(target_os = "macos")] + self.retina.on_mouse_event(&mut me, self.display_idx); + self.input_mouse( + me, + self.inner.id(), + self.lr.my_name.clone(), + self.peer_argb, + false, + true, + ); + } + self.update_auto_disconnect_timer(); + } + Some(message::Union::PointerDeviceEvent(pde)) => { + if self.is_authed_view_camera_conn() { + return true; + } + #[cfg(any(target_os = "android", target_os = "ios"))] + if let Err(e) = match pde.union { + Some(pointer_device_event::Union::TouchEvent(touch)) => match touch.union { + Some(touch_event::Union::PanStart(pan_start)) => { + call_main_service_pointer_input( + "touch", + 4, + pan_start.x, + pan_start.y, + ) + } + Some(touch_event::Union::PanUpdate(pan_update)) => { + call_main_service_pointer_input( + "touch", + 5, + pan_update.x, + pan_update.y, + ) + } + Some(touch_event::Union::PanEnd(pan_end)) => { + call_main_service_pointer_input("touch", 6, pan_end.x, pan_end.y) + } + _ => Ok(()), + }, + _ => Ok(()), + } { + log::debug!("call_main_service_pointer_input fail:{}", e); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.peer_keyboard_enabled() { + MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst); + self.input_pointer(pde, self.inner.id()); + } + self.update_auto_disconnect_timer(); + } + #[cfg(any(target_os = "ios"))] + Some(message::Union::KeyEvent(..)) => {} + #[cfg(any(target_os = "android"))] + Some(message::Union::KeyEvent(mut me)) => { + if self.is_authed_view_camera_conn() { + return true; + } + let key = match me.mode.enum_value() { + Ok(KeyboardMode::Map) => { + Some(crate::keyboard::keycode_to_rdev_key(me.chr())) + } + Ok(KeyboardMode::Translate) => { + if let Some(key_event::Union::Chr(code)) = me.union { + Some(crate::keyboard::keycode_to_rdev_key(code & 0x0000FFFF)) + } else { + None + } + } + _ => None, + } + .filter(crate::keyboard::is_modifier); + + let is_press = + (me.press || me.down) && !(crate::is_modifier(&me) || key.is_some()); + + if let Some(key) = key { + if is_press { + self.pressed_modifiers.insert(key); + } else { + self.pressed_modifiers.remove(&key); + } + } + + let mut modifiers = vec![]; + + for key in self.pressed_modifiers.iter() { + if let Some(control_key) = map_key_to_control_key(key) { + modifiers.push(EnumOrUnknown::new(control_key)); + } + } + + me.modifiers = modifiers; + + let encode_result = me.write_to_bytes(); + + match encode_result { + Ok(data) => { + let result = call_main_service_key_event(&data); + if let Err(e) = result { + log::debug!("call_main_service_key_event fail: {}", e); + } + } + Err(e) => { + log::debug!("encode key event fail: {}", e); + } + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(message::Union::KeyEvent(me)) => { + if self.is_authed_view_camera_conn() { + return true; + } + if self.peer_keyboard_enabled() { + if is_enter(&me) { + CLICK_TIME.store(get_time(), Ordering::SeqCst); + } + // https://github.com/rustdesk/rustdesk/issues/8633 + MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst); + + let key = match me.mode.enum_value() { + Ok(KeyboardMode::Map) => { + Some(crate::keyboard::keycode_to_rdev_key(me.chr())) + } + Ok(KeyboardMode::Translate) => { + if let Some(key_event::Union::Chr(code)) = me.union { + Some(crate::keyboard::keycode_to_rdev_key(code & 0x0000FFFF)) + } else { + None + } + } + _ => None, + } + .filter(crate::keyboard::is_modifier); + + // handle all down as press + // fix unexpected repeating key on remote linux, seems also fix abnormal alt/shift, which + // make sure all key are released + // https://github.com/rustdesk/rustdesk/issues/6793 + let is_press = if cfg!(target_os = "linux") { + (me.press || me.down) && !(crate::is_modifier(&me) || key.is_some()) + } else { + me.press + }; + + if let Some(key) = key { + if is_press { + self.pressed_modifiers.insert(key); + } else { + self.pressed_modifiers.remove(&key); + } + } + + if is_press { + match me.union { + Some(key_event::Union::Unicode(_)) + | Some(key_event::Union::Seq(_)) => { + self.input_key(me, false); + } + _ => { + self.input_key(me, true); + } + } + } else { + self.input_key(me, false); + } + } + self.update_auto_disconnect_timer(); + } + Some(message::Union::Clipboard(cb)) => { + if self.clipboard { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_clipboard(vec![cb], ClipboardSide::Host); + // ios as the controlled side is actually not supported for now. + // The following code is only used to preserve the logic of handling text clipboard on mobile. + #[cfg(target_os = "ios")] + { + let content = if cb.compress { + hbb_common::compress::decompress(&cb.content) + } else { + cb.content.into() + }; + if let Ok(content) = String::from_utf8(content) { + let data = + HashMap::from([("name", "clipboard"), ("content", &content)]); + if let Ok(data) = serde_json::to_string(&data) { + let _ = crate::flutter::push_global_event( + crate::flutter::APP_TYPE_MAIN, + data, + ); + } + } + } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_clipboard(cb); + } + } + Some(message::Union::MultiClipboards(_mcb)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(_mcb.clipboards, ClipboardSide::Host); + } + #[cfg(target_os = "android")] + crate::clipboard::handle_msg_multi_clipboards(_mcb); + } + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + Some(message::Union::Cliprdr(clip)) => { + if let Some(cliprdr::Union::Files(files)) = &clip.union { + self.post_file_audit( + FileAuditType::RemoteReceive, + "", + files + .files + .iter() + .map(|f| (f.name.clone(), f.size as i64)) + .collect::>(), + json!({}), + ); + } else if let Some(clip) = msg_2_clip(clip) { + #[cfg(target_os = "windows")] + { + self.send_to_cm(ipc::Data::ClipboardFile(clip)); + } + #[cfg(feature = "unix-file-copy-paste")] + if crate::is_support_file_copy_paste(&self.lr.version) { + let mut out_msgs = vec![]; + + #[cfg(target_os = "macos")] + if clipboard::platform::unix::macos::should_handle_msg(&clip) { + if let Err(e) = clipboard::ContextSend::make_sure_enabled() { + log::error!("failed to restart clipboard context: {}", e); + } else { + let _ = + clipboard::ContextSend::proc(|context| -> ResultType<()> { + context + .server_clip_file(self.inner.id(), clip) + .map_err(|e| e.into()) + }); + } + } else { + out_msgs = unix_file_clip::serve_clip_messages( + ClipboardSide::Host, + clip, + self.inner.id(), + ); + } + + #[cfg(not(target_os = "macos"))] + { + out_msgs = unix_file_clip::serve_clip_messages( + ClipboardSide::Host, + clip, + self.inner.id(), + ); + } + + for msg in out_msgs.into_iter() { + if let Some(message::Union::Cliprdr(cliprdr)) = msg.union.as_ref() { + if let Some(cliprdr::Union::Files(files)) = + cliprdr.union.as_ref() + { + self.post_file_audit( + FileAuditType::RemoteSend, + "", + files + .files + .iter() + .map(|f| (f.name.clone(), f.size as i64)) + .collect::>(), + json!({}), + ); + continue; + } + } + self.send(msg).await; + } + } + } + } + Some(message::Union::FileAction(fa)) => { + let mut handle_fa = self.file_transfer.is_some(); + if !handle_fa { + if let Some(file_action::Union::Send(s)) = fa.union.as_ref() { + if JobType::from_proto(s.file_type) == JobType::Printer { + handle_fa = true; + } + } + } + if handle_fa { + if self.delayed_read_dir.is_some() { + if let Some(file_action::Union::ReadDir(rd)) = fa.union { + self.delayed_read_dir = Some((rd.path, rd.include_hidden)); + } + return true; + } + if crate::get_builtin_option(keys::OPTION_ONE_WAY_FILE_TRANSFER) == "Y" { + let mut job_id = None; + match &fa.union { + Some(file_action::Union::Send(s)) => { + job_id = Some(s.id); + } + Some(file_action::Union::RemoveFile(rf)) => { + job_id = Some(rf.id); + } + Some(file_action::Union::Rename(r)) => { + job_id = Some(r.id); + } + Some(file_action::Union::Create(c)) => { + job_id = Some(c.id); + } + Some(file_action::Union::RemoveDir(rd)) => { + job_id = Some(rd.id); + } + _ => {} + } + if let Some(job_id) = job_id { + self.send(fs::new_error(job_id, "one-way-file-transfer-tip", 0)) + .await; + return true; + } + } + match fa.union { + Some(file_action::Union::ReadEmptyDirs(rd)) => { + self.read_empty_dirs(&rd.path, rd.include_hidden); + } + Some(file_action::Union::ReadDir(rd)) => { + self.read_dir(&rd.path, rd.include_hidden); + } + Some(file_action::Union::AllFiles(f)) => { + if crate::common::need_fs_cm_send_files() { + self.send_fs(ipc::FS::ReadAllFiles { + path: f.path, + id: f.id, + include_hidden: f.include_hidden, + conn_id: self.inner.id(), + }); + } else { + match fs::get_recursive_files(&f.path, f.include_hidden) { + Err(err) => { + log::error!( + "Failed to get recursive files for {}: {}", + f.path, + err + ); + self.send(fs::new_error(f.id, err, -1)).await; + } + Ok(files) => { + if let Err(msg) = + crate::ui_cm_interface::check_file_count_limit( + files.len(), + ) + { + self.send(fs::new_error(f.id, msg, -1)).await; + } else { + self.send(fs::new_dir(f.id, f.path, files)).await; + } + } + } + } + } + Some(file_action::Union::Send(s)) => { + // server to client + let id = s.id; + let path = s.path.clone(); + let job_type = JobType::from_proto(s.file_type); + match job_type { + JobType::Generic => { + let od = can_enable_overwrite_detection( + get_version_number(&self.lr.version), + ); + if crate::common::need_fs_cm_send_files() { + // Delegate file reading to CM on Windows + self.cm_read_job_ids.insert(id); + self.send_fs(ipc::FS::ReadFile { + path, + id, + file_num: s.file_num, + include_hidden: s.include_hidden, + conn_id: self.inner.id(), + overwrite_detection: od, + }); + } else { + // Handle file reading in Connection on non-Windows + let data_source = + fs::DataSource::FilePath(PathBuf::from(&path)); + self.create_and_start_read_job( + id, + job_type, + data_source, + s.file_num, + s.include_hidden, + od, + path, + true, // check file count limit + ) + .await; + } + } + JobType::Printer => { + if let Some((_, _, data)) = self + .printer_data + .iter() + .position(|(_, p, _)| *p == path) + .map(|index| self.printer_data.remove(index)) + { + let data_source = fs::DataSource::MemoryCursor( + std::io::Cursor::new(data), + ); + // Printer jobs don't need file count limit check + self.create_and_start_read_job( + id, + job_type, + data_source, + s.file_num, + s.include_hidden, + true, // always enable overwrite detection for printer + path, + false, // no file count limit for printer + ) + .await; + } else { + // Ignore this message if the printer data is not found + return true; + } + } + } + self.file_transferred = true; + } + Some(file_action::Union::Receive(r)) => { + // client to server + // note: 1.1.10 introduced identical file detection, which breaks original logic of send/recv files + // whenever got send/recv request, check peer version to ensure old version of rustdesk + let od = can_enable_overwrite_detection(get_version_number( + &self.lr.version, + )); + self.send_fs(ipc::FS::NewWrite { + path: r.path.clone(), + id: r.id, + file_num: r.file_num, + files: r + .files + .to_vec() + .drain(..) + .map(|f| (f.name, f.modified_time)) + .collect(), + overwrite_detection: od, + total_size: r.total_size, + conn_id: self.inner.id(), + }); + self.post_file_audit( + FileAuditType::RemoteReceive, + &r.path, + Self::get_files_for_audit(fs::JobType::Generic, r.files), + json!({}), + ); + self.file_transferred = true; + } + Some(file_action::Union::RemoveDir(d)) => { + self.send_fs(ipc::FS::RemoveDir { + path: d.path.clone(), + id: d.id, + recursive: d.recursive, + }); + self.file_remove_log_control.on_remove_dir(d); + } + Some(file_action::Union::RemoveFile(f)) => { + self.send_fs(ipc::FS::RemoveFile { + path: f.path.clone(), + id: f.id, + file_num: f.file_num, + }); + self.file_remove_log_control.on_remove_file(f); + } + Some(file_action::Union::Create(c)) => { + self.send_fs(ipc::FS::CreateDir { + path: c.path.clone(), + id: c.id, + }); + self.send_to_cm(ipc::Data::FileTransferLog(( + "create_dir".to_string(), + serde_json::to_string(&FileActionLog { + id: c.id, + conn_id: self.inner.id(), + path: c.path, + dir: true, + }) + .unwrap_or_default(), + ))); + } + Some(file_action::Union::Cancel(c)) => { + self.send_fs(ipc::FS::CancelWrite { id: c.id }); + let _ = self.cm_read_job_ids.remove(&c.id); + self.send_fs(ipc::FS::CancelRead { + id: c.id, + conn_id: self.inner.id(), + }); + if let Some(job) = fs::remove_job(c.id, &mut self.read_jobs) { + self.send_to_cm(ipc::Data::FileTransferLog(( + "transfer".to_string(), + fs::serialize_transfer_job(&job, false, true, ""), + ))); + } + } + Some(file_action::Union::SendConfirm(r)) => { + if let Some(job) = fs::get_job(r.id, &mut self.read_jobs) { + job.confirm(&r).await; + } else if self.cm_read_job_ids.contains(&r.id) { + // Forward to CM for CM-read jobs + self.send_fs(ipc::FS::SendConfirmForRead { + id: r.id, + file_num: r.file_num, + skip: r.skip(), + offset_blk: r.offset_blk(), + conn_id: self.inner.id(), + }); + } else { + if let Ok(sc) = r.write_to_bytes() { + self.send_fs(ipc::FS::SendConfirm(sc)); + } + } + } + Some(file_action::Union::Rename(r)) => { + self.send_fs(ipc::FS::Rename { + id: r.id, + path: r.path.clone(), + new_name: r.new_name.clone(), + }); + self.send_to_cm(ipc::Data::FileTransferLog(( + "rename".to_string(), + serde_json::to_string(&FileRenameLog { + conn_id: self.inner.id(), + path: r.path, + new_name: r.new_name, + }) + .unwrap_or_default(), + ))); + } + _ => {} + } + } + } + Some(message::Union::FileResponse(fr)) => match fr.union { + Some(file_response::Union::Block(block)) => { + self.send_fs(ipc::FS::WriteBlock { + id: block.id, + file_num: block.file_num, + data: block.data, + compressed: block.compressed, + }); + } + Some(file_response::Union::Done(d)) => { + self.send_fs(ipc::FS::WriteDone { + id: d.id, + file_num: d.file_num, + }); + } + Some(file_response::Union::Digest(d)) => self.send_fs(ipc::FS::CheckDigest { + id: d.id, + file_num: d.file_num, + file_size: d.file_size, + last_modified: d.last_modified, + is_upload: true, + is_resume: d.is_resume, + }), + Some(file_response::Union::Error(e)) => { + self.send_fs(ipc::FS::WriteError { + id: e.id, + file_num: e.file_num, + err: e.error, + }); + } + _ => {} + }, + Some(message::Union::Misc(misc)) => match misc.union { + Some(misc::Union::SwitchDisplay(s)) => { + self.handle_switch_display(s).await; + } + Some(misc::Union::CaptureDisplays(displays)) => { + let add = displays.add.iter().map(|d| *d as usize).collect::>(); + let sub = displays.sub.iter().map(|d| *d as usize).collect::>(); + let set = displays.set.iter().map(|d| *d as usize).collect::>(); + self.capture_displays(&add, &sub, &set).await; + } + #[cfg(windows)] + Some(misc::Union::ToggleVirtualDisplay(t)) => { + self.toggle_virtual_display(t).await; + } + Some(misc::Union::TogglePrivacyMode(t)) => { + self.toggle_privacy_mode(t).await; + } + Some(misc::Union::ChatMessage(c)) => { + self.send_to_cm(ipc::Data::ChatMessage { text: c.text }); + self.chat_unanswered = true; + self.update_auto_disconnect_timer(); + } + Some(misc::Union::Option(o)) => { + self.update_options(&o).await; + } + Some(misc::Union::RefreshVideo(r)) => { + if r { + // Refresh all videos. + // Compatibility with old versions and sciter(remote). + self.refresh_video_display(None); + } + self.update_auto_disconnect_timer(); + } + Some(misc::Union::RefreshVideoDisplay(display)) => { + self.refresh_video_display(Some(display as usize)); + self.update_auto_disconnect_timer(); + } + Some(misc::Union::VideoReceived(_)) => { + video_service::notify_video_frame_fetched_by_conn_id( + self.inner.id, + Some(Instant::now().into()), + ); + } + Some(misc::Union::RestartRemoteDevice(_)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.restart { + // force_reboot, not work on linux vm and macos 14 + #[cfg(any(target_os = "linux", target_os = "windows"))] + match system_shutdown::force_reboot() { + Ok(_) => log::info!("Restart by the peer"), + Err(e) => log::error!("Failed to restart: {}", e), + } + #[cfg(any(target_os = "linux", target_os = "macos"))] + match system_shutdown::reboot() { + Ok(_) => log::info!("Restart by the peer"), + Err(e) => log::error!("Failed to restart: {}", e), + } + } + } + #[cfg(windows)] + Some(misc::Union::ElevationRequest(r)) => match r.union { + Some(elevation_request::Union::Direct(_)) => { + self.handle_elevation_request(portable_client::StartPara::Direct) + .await; + } + Some(elevation_request::Union::Logon(r)) => { + self.handle_elevation_request(portable_client::StartPara::Logon( + r.username, r.password, + )) + .await; + } + _ => {} + }, + Some(misc::Union::AudioFormat(format)) => { + if !self.disable_audio { + // Drop the audio sender previously. + drop(std::mem::replace(&mut self.audio_sender, None)); + self.audio_sender = Some(start_audio_thread()); + self.audio_sender + .as_ref() + .map(|a| allow_err!(a.send(MediaData::AudioFormat(format)))); + } + } + #[cfg(feature = "flutter")] + Some(misc::Union::SwitchSidesRequest(s)) => { + if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) { + crate::run_me(vec![ + "--connect", + &self.lr.my_id, + "--switch_uuid", + uuid.to_string().as_ref(), + ]) + .ok(); + self.on_close("switch sides", false).await; + return false; + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::ChangeResolution(r)) => self.change_resolution(None, &r), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::ChangeDisplayResolution(dr)) => { + self.change_resolution(Some(dr.display as _), &dr.resolution) + } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::PluginRequest(p)) => { + let msg = + crate::plugin::handle_client_event(&p.id, &self.lr.my_id, &p.content); + self.send(msg).await; + } + Some(misc::Union::AutoAdjustFps(fps)) => video_service::VIDEO_QOS + .lock() + .unwrap() + .user_auto_adjust_fps(self.inner.id(), fps), + Some(misc::Union::ClientRecordStatus(status)) => video_service::VIDEO_QOS + .lock() + .unwrap() + .user_record(self.inner.id(), status), + #[cfg(windows)] + Some(misc::Union::SelectedSid(sid)) => { + if let Some(current_process_sid) = + crate::platform::get_current_process_session_id() + { + let sessions = crate::platform::get_available_sessions(false); + if crate::platform::is_installed() + && crate::platform::is_share_rdp() + && raii::AuthedConnID::non_port_forward_conn_count() == 1 + && sessions.len() > 1 + && current_process_sid != sid + && sessions.iter().any(|e| e.sid == sid) + { + std::thread::spawn(move || { + let _ = ipc::connect_to_user_session(Some(sid)); + }); + return false; + } + if self.file_transfer.is_some() { + if let Some((dir, show_hidden)) = self.delayed_read_dir.take() { + self.read_dir(&dir, show_hidden); + } + } else if self.view_camera { + self.try_sub_camera_displays(); + } else if !self.terminal { + self.try_sub_monitor_services(); + } + } + } + Some(misc::Union::MessageQuery(mq)) => { + if let Some(msg_out) = video_service::make_display_changed_msg( + mq.switch_display as _, + None, + self.video_source(), + ) { + self.send(msg_out).await; + } + } + _ => {} + }, + Some(message::Union::AudioFrame(frame)) => { + if !self.disable_audio { + if let Some(sender) = &self.audio_sender { + allow_err!(sender.send(MediaData::AudioFrame(Box::new(frame)))); + } else { + log::warn!( + "Processing audio frame without the voice call audio sender." + ); + } + } + } + Some(message::Union::VoiceCallRequest(request)) => { + if request.is_connect { + self.voice_call_request_timestamp = Some( + NonZeroI64::new(request.req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); + // Notify the connection manager. + self.send_to_cm(Data::VoiceCallIncoming); + } else { + self.close_voice_call().await; + } + } + Some(message::Union::VoiceCallResponse(_response)) => { + // TODO: Maybe we can do a voice call from cm directly. + } + Some(message::Union::ScreenshotRequest(request)) => { + if let Some(tx) = self.inner.tx.clone() { + crate::video_service::set_take_screenshot( + request.display as _, + request.sid.clone(), + tx, + ); + self.refresh_video_display(Some(request.display as usize)); + } + } + Some(message::Union::TerminalAction(action)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + allow_err!(self.handle_terminal_action(action).await); + #[cfg(any(target_os = "android", target_os = "ios"))] + log::warn!("Terminal action received but not supported on this platform"); + } + _ => {} + } + } + true + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + fn fill_terminal_user_token( + &mut self, + _username: &str, + _password: &str, + ) -> Option<&'static str> { + self.terminal_user_token = Some(TerminalUserToken::SelfUser); + None + } + + // Try to fill user token for terminal connection. + // If username is empty, use the user token of the current session. + // If username is not empty, try to logon and check if the user is an administrator. + // If the user is an administrator, use the user token of current process (SYSTEM). + // If the user is not an administrator, return an error message. + // Note: Only local and domain users are supported, Microsoft account (online account) not supported for now. + #[cfg(target_os = "windows")] + fn fill_terminal_user_token(&mut self, username: &str, password: &str) -> Option<&'static str> { + // No need to check if the password is empty. + if !username.is_empty() { + return self.handle_administrator_check(username, password); + } + + if crate::platform::is_prelogin() { + self.terminal_user_token = None; + return Some("No active console user logged on, please connect and logon first."); + } + + if crate::platform::is_installed() { + return self.handle_installed_user(); + } + + self.terminal_user_token = Some(TerminalUserToken::SelfUser); + None + } + + #[cfg(target_os = "windows")] + fn handle_administrator_check( + &mut self, + username: &str, + password: &str, + ) -> Option<&'static str> { + let check_admin_res = + crate::platform::get_logon_user_token(username, password).map(|token| { + let is_token_admin = crate::platform::is_user_token_admin(token); + unsafe { + hbb_common::allow_err!(CloseHandle(HANDLE(token as _))); + }; + is_token_admin + }); + match check_admin_res { + Ok(Ok(b)) => { + if b { + self.terminal_user_token = Some(TerminalUserToken::SelfUser); + None + } else { + Some("The user is not an administrator.") + } + } + Ok(Err(e)) => { + log::error!("Failed to check if the user is an administrator: {}", e); + Some("Failed to check if the user is an administrator.") + } + Err(e) => { + log::error!("Failed to get logon user token: {}", e); + Some("Incorrect username or password.") + } + } + } + + #[cfg(target_os = "windows")] + fn handle_installed_user(&mut self) -> Option<&'static str> { + let session_id = crate::platform::get_current_session_id(true); + if session_id == 0xFFFFFFFF { + return Some("Failed to get current session id."); + } + let token = crate::platform::get_user_token(session_id, true); + if !token.is_null() { + match crate::platform::ensure_primary_token(token) { + Ok(t) => { + self.terminal_user_token = Some(TerminalUserToken::CurrentLogonUser( + crate::terminal_service::UserToken::new(t as usize), + )); + } + Err(e) => { + log::error!("Failed to ensure primary token: {}", e); + self.terminal_user_token = Some(TerminalUserToken::CurrentLogonUser( + crate::terminal_service::UserToken::new(token as usize), + )); + } + } + None + } else { + log::error!( + "Failed to get user token for terminal action, {}", + std::io::Error::last_os_error() + ); + Some("Failed to get user token.") + } + } + + // Try to parse connection IP as IPv6 address, returning /64, /56, and /48 prefixes. + // Parsing an IPv4 address just returns None. + // note: we specifically don't use hbb_common::is_ipv6_str to avoid divergence issues + // between its regex and the system std::net::Ipv6Addr implementation. + fn get_ipv6_prefixes(&self) -> Option<(String, String, String)> { + fn mask_u128(addr: u128, prefix: u8) -> u128 { + let mask = if prefix == 0 || prefix > 128 { + 0 + } else { + (!0u128) << (128 - prefix) + }; + addr & mask + } + // eliminate zone-ids like "fe80::1%eth0" + let ip_only = self.ip.split('%').next().unwrap_or(&self.ip).trim(); + let ip = Ipv6Addr::from_str(ip_only).ok()?; + + let as_u128 = u128::from_be_bytes(ip.octets()); + + let p64 = Ipv6Addr::from(mask_u128(as_u128, 64).to_be_bytes()).to_string() + "/64"; + let p56 = Ipv6Addr::from(mask_u128(as_u128, 56).to_be_bytes()).to_string() + "/56"; + let p48 = Ipv6Addr::from(mask_u128(as_u128, 48).to_be_bytes()).to_string() + "/48"; + + Some((p64, p56, p48)) + } + + fn update_failure(&self, (failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) { + fn bump(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) { + if cur.0 == time { + cur.1 += 1; + cur.2 += 1; + } else { + cur.0 = time; + cur.1 = 1; + cur.2 += 1; + } + cur + } + let map_mutex = &LOGIN_FAILURES[i]; + if remove { + if failure.0 != 0 { + if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() { + let mut m = map_mutex.lock().unwrap(); + m.remove(&p64); + m.remove(&p56); + m.remove(&p48); + m.remove(&self.ip); + } else { + map_mutex.lock().unwrap().remove(&self.ip); + } + } + return; + } + // Bump the prefixes, fetching existing values + if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() { + let mut m = map_mutex.lock().unwrap(); + for key in [p64, p56, p48] { + let cur = m.get(&key).copied().unwrap_or((0, 0, 0)); + m.insert(key, bump(cur, time)); + } + // Update full IP: bump from the *original* passed-in failure + m.insert(self.ip.clone(), bump(failure, time)); + } else { + // Update full IP: bump from the *original* passed-in failure + let mut m = map_mutex.lock().unwrap(); + m.insert(self.ip.clone(), bump(failure, time)); + } + } + + async fn check_failure_ipv6_prefix( + &mut self, + i: usize, + time: i32, + prefix: &str, + prefix_num: i8, + thresh: i32, + ) -> Option<(((i32, i32, i32), i32), bool)> { + let failure_prefix = LOGIN_FAILURES[i] + .lock() + .unwrap() + .get(prefix) + .copied() + .unwrap_or((0, 0, 0)); + + if failure_prefix.2 > thresh { + self.send_login_error(format!( + "Too many wrong attempts for IPv6 prefix /{}", + prefix_num + )) + .await; + Self::post_alarm_audit( + AlarmAuditType::ExceedIPv6PrefixAttempts, + json!({ + "ip": self.ip, + "id": self.lr.my_id.clone(), + "name": self.lr.my_name.clone(), + }), + ); + Some(((failure_prefix, time), false)) + } else { + None + } + } + + async fn check_failure(&mut self, i: usize) -> (((i32, i32, i32), i32), bool) { + let time = (get_time() / 60_000) as i32; + + // IPv6 addresses are cheap to make so we check prefix/netblock as well + if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() { + if let Some(res) = self.check_failure_ipv6_prefix(i, time, &p64, 64, 60).await { + return res; + } + if let Some(res) = self.check_failure_ipv6_prefix(i, time, &p56, 56, 80).await { + return res; + } + if let Some(res) = self.check_failure_ipv6_prefix(i, time, &p48, 48, 100).await { + return res; + } + } + + // checks IPv6 and IPv4 direct addresses + let failure = LOGIN_FAILURES[i] + .lock() + .unwrap() + .get(&self.ip) + .copied() + .unwrap_or((0, 0, 0)); + + let res = if failure.2 > 30 { + self.send_login_error("Too many wrong attempts").await; + Self::post_alarm_audit( + AlarmAuditType::ExceedThirtyAttempts, + json!({ + "ip": self.ip, + "id": self.lr.my_id.clone(), + "name": self.lr.my_name.clone(), + }), + ); + false + } else if time == failure.0 && failure.1 > 6 { + self.send_login_error("Please try 1 minute later").await; + Self::post_alarm_audit( + AlarmAuditType::SixAttemptsWithinOneMinute, + json!({ + "ip": self.ip, + "id": self.lr.my_id.clone(), + "name": self.lr.my_name.clone(), + }), + ); + false + } else { + true + }; + ((failure, time), res) + } + + fn refresh_video_display(&self, display: Option) { + video_service::refresh(); + self.server.upgrade().map(|s| { + s.read().unwrap().set_video_service_opt( + display.map(|d| (self.video_source(), d)), + video_service::OPTION_REFRESH, + super::service::SERVICE_OPTION_VALUE_TRUE, + ); + }); + } + + async fn handle_switch_display(&mut self, s: SwitchDisplay) { + let display_idx = s.display as usize; + if self.display_idx != display_idx { + if let Some(server) = self.server.upgrade() { + self.switch_display_to(display_idx, server.clone()); + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if s.width != 0 && s.height != 0 { + self.change_resolution( + None, + &Resolution { + width: s.width, + height: s.height, + ..Default::default() + }, + ); + } + } + + // Send display changed message. + // 1. For compatibility with old versions ( < 1.2.4 ). + // 2. Sciter version. + // 3. Update `SupportedResolutions`. + if let Some(msg_out) = + video_service::make_display_changed_msg(self.display_idx, None, self.video_source()) + { + self.send(msg_out).await; + } + } + } + + fn video_source(&self) -> VideoSource { + if self.view_camera { + VideoSource::Camera + } else { + VideoSource::Monitor + } + } + + fn switch_display_to(&mut self, display_idx: usize, server: Arc>) { + let new_service_name = video_service::get_service_name(self.video_source(), display_idx); + let old_service_name = + video_service::get_service_name(self.video_source(), self.display_idx); + let mut lock = server.write().unwrap(); + if display_idx != *display_service::PRIMARY_DISPLAY_IDX { + if !lock.contains(&new_service_name) { + lock.add_service(Box::new(video_service::new( + self.video_source(), + display_idx, + ))); + } + } + // For versions greater than 1.2.4, a `CaptureDisplays` message will be sent immediately. + // Unnecessary capturers will be removed then. + if !crate::common::is_support_multi_ui_session(&self.lr.version) { + lock.subscribe(&old_service_name, self.inner.clone(), false); + } + lock.subscribe(&new_service_name, self.inner.clone(), true); + self.display_idx = display_idx; + } + + #[cfg(windows)] + async fn handle_elevation_request(&mut self, para: portable_client::StartPara) { + let mut err; + if !self.keyboard { + err = "No permission".to_string(); + } else { + err = "No need to elevate".to_string(); + if !crate::platform::is_installed() && !portable_client::running() { + err = portable_client::start_portable_service(para) + .err() + .map_or("".to_string(), |e| e.to_string()); + } + } + + let mut misc = Misc::new(); + misc.set_elevation_response(err); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(msg).await; + self.update_auto_disconnect_timer(); + } + + async fn capture_displays(&mut self, add: &[usize], sub: &[usize], set: &[usize]) { + let video_source = self.video_source(); + if let Some(sever) = self.server.upgrade() { + let mut lock = sever.write().unwrap(); + for display in add.iter() { + let service_name = video_service::get_service_name(video_source, *display); + if !lock.contains(&service_name) { + lock.add_service(Box::new(video_service::new(video_source, *display))); + } + } + for display in set.iter() { + let service_name = video_service::get_service_name(video_source, *display); + if !lock.contains(&service_name) { + lock.add_service(Box::new(video_service::new(video_source, *display))); + } + } + if !add.is_empty() { + lock.capture_displays(self.inner.clone(), video_source, add, true, false); + } else if !sub.is_empty() { + lock.capture_displays(self.inner.clone(), video_source, sub, false, true); + } else { + lock.capture_displays(self.inner.clone(), video_source, set, true, true); + } + self.multi_ui_session = lock.get_subbed_displays_count(self.inner.id()) > 1; + if self.follow_remote_window { + lock.subscribe( + NAME_WINDOW_FOCUS, + self.inner.clone(), + !self.multi_ui_session, + ); + } + drop(lock); + } + } + + #[cfg(windows)] + async fn toggle_virtual_display(&mut self, t: ToggleVirtualDisplay) { + let make_msg = |text: String| { + let mut msg_out = Message::new(); + let res = MessageBox { + msgtype: "nook-nocancel-hasclose".to_owned(), + title: "Virtual display".to_owned(), + text, + link: "".to_owned(), + ..Default::default() + }; + msg_out.set_message_box(res); + msg_out + }; + + if t.on { + if !virtual_display_manager::is_virtual_display_supported() { + self.send(make_msg("idd_not_support_under_win10_2004_tip".to_string())) + .await; + } else { + if let Err(e) = virtual_display_manager::plug_in_monitor(t.display as _, Vec::new()) + { + log::error!("Failed to plug in virtual display: {}", e); + self.send(make_msg(format!( + "Failed to plug in virtual display: {}", + e + ))) + .await; + } + } + } else { + if let Err(e) = virtual_display_manager::plug_out_monitor(t.display, false, true) { + log::error!("Failed to plug out virtual display {}: {}", t.display, e); + self.send(make_msg(format!( + "Failed to plug out virtual displays: {}", + e + ))) + .await; + } + } + } + + async fn toggle_privacy_mode(&mut self, t: TogglePrivacyMode) { + if t.on { + self.turn_on_privacy(t.impl_key).await; + } else { + self.turn_off_privacy(t.impl_key).await; + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn change_resolution(&mut self, d: Option, r: &Resolution) { + if self.keyboard { + if let Ok(displays) = display_service::try_get_displays() { + let display_idx = d.unwrap_or(self.display_idx); + if let Some(display) = displays.get(display_idx) { + let name = display.name(); + #[cfg(windows)] + if let Some(_ok) = + virtual_display_manager::rustdesk_idd::change_resolution_if_is_virtual_display( + &name, + r.width as _, + r.height as _, + ) + { + return; + } + #[allow(unused_mut)] + let mut record_changed = true; + #[cfg(windows)] + if virtual_display_manager::amyuni_idd::is_my_display(&name) { + record_changed = false; + } + #[cfg(not(target_os = "macos"))] + let scale = 1.0; + #[cfg(target_os = "macos")] + let scale = display.scale(); + let original = ( + ((display.width() as f64) / scale).round() as _, + (display.height() as f64 / scale).round() as _, + ); + if record_changed { + display_service::set_last_changed_resolution( + &name, + original, + (r.width, r.height), + ); + } + if let Err(e) = + crate::platform::change_resolution(&name, r.width as _, r.height as _) + { + log::error!( + "Failed to change resolution '{}' to ({},{}): {:?}", + &name, + r.width, + r.height, + e + ); + } + } + } + } + } + + pub async fn handle_voice_call(&mut self, accepted: bool) { + if let Some(ts) = self.voice_call_request_timestamp.take() { + let msg = new_voice_call_response(ts.get(), accepted); + if accepted { + crate::audio_service::set_voice_call_input_device( + crate::get_default_sound_input(), + false, + ); + self.send_to_cm(Data::StartVoiceCall); + } else { + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); + } + self.send(msg).await; + self.voice_calling = accepted; + if self.is_authed_view_camera_conn() { + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled() && accepted, + ); + } + } + } else { + log::warn!("Possible a voice call attack."); + } + } + + pub async fn close_voice_call(&mut self) { + crate::audio_service::set_voice_call_input_device(None, true); + // Notify the connection manager that the voice call has been closed. + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); + self.voice_calling = false; + if self.is_authed_view_camera_conn() { + if let Some(s) = self.server.upgrade() { + s.write() + .unwrap() + .subscribe(super::audio_service::NAME, self.inner.clone(), false); + } + } + } + + async fn update_options(&mut self, o: &OptionMessage) { + log::info!("Option update: {:?}", o); + if let Ok(q) = o.image_quality.enum_value() { + let image_quality; + if let ImageQuality::NotSet = q { + if o.custom_image_quality > 0 { + image_quality = o.custom_image_quality; + } else { + image_quality = -1; + } + } else { + image_quality = q.value(); + } + if image_quality > 0 { + video_service::VIDEO_QOS + .lock() + .unwrap() + .user_image_quality(self.inner.id(), image_quality); + } + } + if o.custom_fps > 0 { + video_service::VIDEO_QOS + .lock() + .unwrap() + .user_custom_fps(self.inner.id(), o.custom_fps as _); + } + if let Some(q) = o.supported_decoding.clone().take() { + scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Update(self.inner.id(), q)); + } + if let Ok(q) = o.lock_after_session_end.enum_value() { + if q != BoolOption::NotSet { + self.lock_after_session_end = q == BoolOption::Yes; + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(q) = o.show_remote_cursor.enum_value() { + if q != BoolOption::NotSet { + self.show_remote_cursor = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + NAME_CURSOR, + self.inner.clone(), + self.peer_keyboard_enabled() || self.show_remote_cursor, + ); + s.write().unwrap().subscribe( + NAME_POS, + self.inner.clone(), + self.show_remote_cursor, + ); + } + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(q) = o.follow_remote_cursor.enum_value() { + if q != BoolOption::NotSet { + self.follow_remote_cursor = q == BoolOption::Yes; + } + } + if let Ok(q) = o.follow_remote_window.enum_value() { + if q != BoolOption::NotSet { + self.follow_remote_window = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + NAME_WINDOW_FOCUS, + self.inner.clone(), + self.follow_remote_window, + ); + } + } + } + if let Ok(q) = o.disable_audio.enum_value() { + if q != BoolOption::NotSet { + self.disable_audio = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + if self.is_authed_view_camera_conn() { + if self.voice_calling || !self.audio_enabled() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled(), + ); + } + } else { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled(), + ); + } + } + } + } + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + if let Ok(q) = o.enable_file_transfer.enum_value() { + if q != BoolOption::NotSet { + self.enable_file_transfer = q == BoolOption::Yes; + #[cfg(target_os = "windows")] + self.send_to_cm(ipc::Data::ClipboardFileEnabled( + self.file_transfer_enabled(), + )); + #[cfg(feature = "unix-file-copy-paste")] + if !self.enable_file_transfer { + self.try_empty_file_clipboard(); + } + #[cfg(feature = "unix-file-copy-paste")] + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + self.inner.clone(), + self.can_sub_file_clipboard_service(), + ); + } + } + } + if let Ok(q) = o.disable_clipboard.enum_value() { + if q != BoolOption::NotSet { + self.disable_clipboard = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::NAME, + self.inner.clone(), + self.can_sub_clipboard_service(), + ); + } + } + } + if let Ok(q) = o.disable_keyboard.enum_value() { + if q != BoolOption::NotSet { + self.disable_keyboard = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::NAME, + self.inner.clone(), + self.can_sub_clipboard_service(), + ); + #[cfg(feature = "unix-file-copy-paste")] + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + self.inner.clone(), + self.can_sub_file_clipboard_service(), + ); + s.write().unwrap().subscribe( + NAME_CURSOR, + self.inner.clone(), + self.peer_keyboard_enabled() || self.show_remote_cursor, + ); + } + } + } + // For compatibility with old versions ( < 1.2.4 ). + if hbb_common::get_version_number(&self.lr.version) + < hbb_common::get_version_number("1.2.4") + { + if let Ok(q) = o.privacy_mode.enum_value() { + if self.keyboard { + match q { + BoolOption::Yes => { + self.turn_on_privacy("".to_owned()).await; + } + BoolOption::No => { + self.turn_off_privacy("".to_owned()).await; + } + _ => {} + } + } + } + } + if let Ok(q) = o.block_input.enum_value() { + if self.keyboard && self.block_input { + match q { + BoolOption::Yes => { + self.tx_input.send(MessageInput::BlockOn).ok(); + } + BoolOption::No => { + self.tx_input.send(MessageInput::BlockOff).ok(); + } + _ => {} + } + } else { + if q != BoolOption::NotSet { + let state = if q == BoolOption::Yes { + back_notification::BlockInputState::BlkOnFailed + } else { + back_notification::BlockInputState::BlkOffFailed + }; + if let Some(tx) = &self.inner.tx { + Self::send_block_input_error(tx, state, "No permission".to_string()); + } + } + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(q) = o.terminal_persistent.enum_value() { + if q != BoolOption::NotSet { + self.update_terminal_persistence(q == BoolOption::Yes).await; + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(q) = o.show_my_cursor.enum_value() { + if q != BoolOption::NotSet { + use crate::whiteboard; + self.show_my_cursor = q == BoolOption::Yes; + #[cfg(target_os = "windows")] + let is_lower_win10 = !crate::platform::windows::is_win_10_or_greater(); + #[cfg(not(target_os = "windows"))] + let is_lower_win10 = false; + #[cfg(target_os = "linux")] + let is_linux_supported = crate::whiteboard::is_supported(); + #[cfg(not(target_os = "linux"))] + let is_linux_supported = false; + let not_support_msg = if is_lower_win10 { + "Windows 10 or greater is required." + } else if cfg!(target_os = "linux") && !is_linux_supported { + "This feature is not supported on native Wayland, please install XWayland or switch to X11." + } else { + "" + }; + if q == BoolOption::Yes { + if not_support_msg.is_empty() { + whiteboard::register_whiteboard(whiteboard::get_key_cursor(self.inner.id)); + } else { + let mut msg_out = Message::new(); + let res = MessageBox { + msgtype: "nook-nocancel-hasclose".to_owned(), + title: "Show my cursor".to_owned(), + text: not_support_msg.to_owned(), + link: "".to_owned(), + ..Default::default() + }; + msg_out.set_message_box(res); + self.send(msg_out).await; + } + } else { + if not_support_msg.is_empty() { + whiteboard::unregister_whiteboard(whiteboard::get_key_cursor( + self.inner.id, + )); + } + } + } + } + } + + async fn turn_on_privacy(&mut self, impl_key: String) { + if !self.is_authed_remote_conn() || !self.privacy_mode { + let msg_out = crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOnFailedDenied, + impl_key, + ); + self.send(msg_out).await; + return; + } + + let msg_out = if !privacy_mode::is_privacy_mode_supported() { + crate::common::make_privacy_mode_msg_with_details( + back_notification::PrivacyModeState::PrvNotSupported, + "Unsupported. 1 Multi-screen is not supported. 2 Please confirm the license is activated.".to_string(), + impl_key, + ) + } else { + let is_pre_privacy_on = privacy_mode::is_in_privacy_mode(); + let pre_impl_key = privacy_mode::get_cur_impl_key(); + + if is_pre_privacy_on { + if let Some(pre_impl_key) = pre_impl_key { + if !privacy_mode::is_current_privacy_mode_impl(&pre_impl_key) { + let off_msg = crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOffSucceeded, + pre_impl_key, + ); + self.send(off_msg).await; + } + } + } + + let turn_on_res = privacy_mode::turn_on_privacy(&impl_key, self.inner.id).await; + match turn_on_res { + Some(Ok(res)) => { + if res { + let err_msg = privacy_mode::check_privacy_mode_err( + self.inner.id, + self.display_idx, + 5_000, + ); + if err_msg.is_empty() { + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOnSucceeded, + impl_key, + ) + } else { + log::error!( + "Check privacy mode failed: {}, turn off privacy mode.", + &err_msg + ); + let _ = Self::turn_off_privacy_to_msg(self.inner.id, String::new()); + crate::common::make_privacy_mode_msg_with_details( + back_notification::PrivacyModeState::PrvOnFailed, + err_msg, + impl_key, + ) + } + } else { + crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOnFailedPlugin, + impl_key, + ) + } + } + Some(Err(e)) => { + log::error!("Failed to turn on privacy mode. {}", e); + if privacy_mode::is_in_privacy_mode() { + let _ = Self::turn_off_privacy_to_msg( + privacy_mode::INVALID_PRIVACY_MODE_CONN_ID, + String::new(), + ); + } + crate::common::make_privacy_mode_msg_with_details( + back_notification::PrivacyModeState::PrvOnFailed, + e.to_string(), + impl_key, + ) + } + None => crate::common::make_privacy_mode_msg_with_details( + back_notification::PrivacyModeState::PrvOffFailed, + "Not supported".to_string(), + impl_key, + ), + } + }; + self.send(msg_out).await; + } + + async fn turn_off_privacy(&mut self, impl_key: String) { + let msg_out = if !privacy_mode::is_privacy_mode_supported() { + crate::common::make_privacy_mode_msg_with_details( + back_notification::PrivacyModeState::PrvNotSupported, + // This error message is used for magnifier. It is ok to use it here. + "Unsupported. 1 Multi-screen is not supported. 2 Please confirm the license is activated.".to_string(), + impl_key, + ) + } else { + Self::turn_off_privacy_to_msg(self.inner.id, impl_key) + }; + self.send(msg_out).await; + } + + pub fn turn_off_privacy_to_msg(_conn_id: i32, impl_key: String) -> Message { + Self::turn_off_privacy_result_to_msg( + privacy_mode::turn_off_privacy(_conn_id, None), + impl_key, + ) + } + + fn turn_off_privacy_result_to_msg( + turn_off_res: Option>, + impl_key: String, + ) -> Message { + match turn_off_res { + Some(Ok(_)) => crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOffSucceeded, + impl_key, + ), + Some(Err(e)) => { + log::error!("Failed to turn off privacy mode {}", e); + crate::common::make_privacy_mode_msg_with_details( + back_notification::PrivacyModeState::PrvOffFailed, + e.to_string(), + impl_key, + ) + } + None => crate::common::make_privacy_mode_msg_with_details( + back_notification::PrivacyModeState::PrvOffFailed, + "Not supported".to_string(), + impl_key, + ), + } + } + + async fn on_close(&mut self, reason: &str, lock: bool) { + if self.closed { + return; + } + self.closed = true; + // If voice A,B -> C, and A,B has voice call + // B disconnects, C will reset the voice call input. + // + // It may be acceptable, because it's not a common case, + // and it's immediately known when the input device changes. + // C can change the input device manually in cm interface. + // + // We can add a (Vec, input device) to avoid this. + // But it's not necessary now and we have to consider two audio services(client, server). + crate::audio_service::set_voice_call_input_device(None, true); + log::info!("#{} Connection closed: {}", self.inner.id(), reason); + if lock && self.lock_after_session_end && self.keyboard { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + lock_screen().await; + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let data = if self.chat_unanswered || self.file_transferred && cfg!(feature = "flutter") { + ipc::Data::Disconnected + } else { + ipc::Data::Close + }; + #[cfg(any(target_os = "android", target_os = "ios"))] + let data = ipc::Data::Close; + self.tx_to_cm.send(data).ok(); + self.port_forward_socket.take(); + } + + // The `reason` should be consistent with `check_if_retry` if not empty + async fn send_close_reason_no_retry(&mut self, reason: &str) { + let mut misc = Misc::new(); + if reason.is_empty() { + misc.set_close_reason("Closed manually by the peer".to_string()); + } else { + misc.set_close_reason(reason.to_string()); + } + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(msg_out).await; + raii::AuthedConnID::check_remove_session(self.inner.id(), self.session_key()); + } + + async fn handle_read_job_init_result( + &mut self, + id: i32, + _file_num: i32, + _include_hidden: bool, + result: Result, String>, + ) { + // Check if this response is still expected (not stale/cancelled) + if !self.cm_read_job_ids.contains(&id) { + log::warn!( + "Received ReadJobInitResult for unknown or stale job id={}, ignoring", + id + ); + return; + } + + match result { + Err(error) => { + self.cm_read_job_ids.remove(&id); + self.send(fs::new_error(id, error, 0)).await; + } + Ok(dir_bytes) => { + // Deserialize FileDirectory from protobuf bytes + let dir = match FileDirectory::parse_from_bytes(&dir_bytes) { + Ok(d) => d, + Err(e) => { + log::error!("Failed to parse FileDirectory: {}", e); + self.cm_read_job_ids.remove(&id); + self.send(fs::new_error(id, "internal error".to_string(), 0)) + .await; + return; + } + }; + + let path_str = dir.path.clone(); + let file_entries: Vec = dir.entries.into(); + + // Send file directory to client + self.send(fs::new_dir(id, path_str.clone(), file_entries.clone())) + .await; + + // Post audit for file transfer + self.post_file_audit( + FileAuditType::RemoteSend, + &path_str, + Self::get_files_for_audit(fs::JobType::Generic, file_entries), + json!({}), + ); + + // CM will handle the actual file reading and send blocks via IPC + self.file_transferred = true; + } + } + } + + async fn handle_file_block_from_cm( + &mut self, + id: i32, + file_num: i32, + data: bytes::Bytes, + compressed: bool, + ) { + // Check if the job is still valid (not cancelled) + if !self.cm_read_job_ids.contains(&id) { + log::debug!( + "Dropping file block for cancelled/unknown job id={}, file_num={}", + id, + file_num + ); + return; + } + + // Forward file block to client + let mut block = FileTransferBlock::new(); + block.id = id; + block.file_num = file_num; + block.data = data.to_vec().into(); + block.compressed = compressed; + + let mut msg = Message::new(); + let mut fr = FileResponse::new(); + fr.set_block(block); + msg.set_file_response(fr); + self.send(msg).await; + } + + async fn handle_file_read_done(&mut self, id: i32, file_num: i32) { + // Drop stale completions for cancelled/unknown jobs + if !self.cm_read_job_ids.remove(&id) { + log::debug!( + "Dropping FileReadDone for cancelled/unknown job id={}, file_num={}", + id, + file_num + ); + return; + } + + // Forward done message to client + let mut done = FileTransferDone::new(); + done.id = id; + done.file_num = file_num; + + let mut msg = Message::new(); + let mut fr = FileResponse::new(); + fr.set_done(done); + msg.set_file_response(fr); + self.send(msg).await; + } + + async fn handle_file_read_error(&mut self, id: i32, file_num: i32, err: String) { + // Drop stale errors for cancelled/unknown jobs + if !self.cm_read_job_ids.remove(&id) { + log::debug!( + "Dropping FileReadError for cancelled/unknown job id={}, file_num={}", + id, + file_num + ); + return; + } + + // Forward error to client + self.send(fs::new_error(id, err, file_num)).await; + } + + async fn handle_file_digest_from_cm( + &mut self, + id: i32, + file_num: i32, + last_modified: u64, + file_size: u64, + is_resume: bool, + ) { + // Check if the job is still valid (not cancelled) + if !self.cm_read_job_ids.contains(&id) { + log::debug!( + "Dropping digest for cancelled/unknown job id={}, file_num={}", + id, + file_num + ); + return; + } + + // Forward digest to client for overwrite detection + let mut digest = FileTransferDigest::new(); + digest.id = id; + digest.file_num = file_num; + digest.last_modified = last_modified; + digest.file_size = file_size; + digest.is_upload = false; // Server sending to client + digest.is_resume = is_resume; + + let mut msg = Message::new(); + let mut fr = FileResponse::new(); + fr.set_digest(digest); + msg.set_file_response(fr); + self.send(msg).await; + } + + async fn process_new_read_job(&mut self, mut job: fs::TransferJob, path: String) { + let files = job.files().to_owned(); + let job_type = job.r#type; + self.send(fs::new_dir(job.id, path.clone(), files.clone())) + .await; + job.is_remote = true; + job.conn_id = self.inner.id(); + self.read_jobs.push(job); + self.file_timer = crate::rustdesk_interval(time::interval(MILLI1)); + let audit_path = if job_type == fs::JobType::Printer { + "Remote print".to_owned() + } else { + path + }; + self.post_file_audit( + FileAuditType::RemoteSend, + &audit_path, + Self::get_files_for_audit(job_type, files), + json!({}), + ); + } + + async fn handle_all_files_result( + &mut self, + id: i32, + path: String, + result: Result, String>, + ) { + match result { + Err(err) => { + self.send(fs::new_error(id, err, -1)).await; + } + Ok(bytes) => { + // Deserialize FileDirectory from protobuf bytes and send as FileResponse + match FileDirectory::parse_from_bytes(&bytes) { + Ok(fd) => { + let mut msg = Message::new(); + let mut fr = FileResponse::new(); + fr.set_dir(fd); + msg.set_file_response(fr); + self.send(msg).await; + } + Err(e) => { + self.send(fs::new_error( + id, + format!("deserialize failed for {}: {}", path, e), + -1, + )) + .await; + } + } + } + } + } + + fn read_empty_dirs(&mut self, dir: &str, include_hidden: bool) { + let dir = dir.to_string(); + self.send_fs(ipc::FS::ReadEmptyDirs { + dir, + include_hidden, + }); + } + + fn read_dir(&mut self, dir: &str, include_hidden: bool) { + let dir = dir.to_string(); + self.send_fs(ipc::FS::ReadDir { + dir, + include_hidden, + }); + } + + /// Create a new read job and start processing it (Connection-side). + /// + /// This is a generic Connection-side read job creation helper used for: + /// - Generic file transfers on non-Windows platforms + /// - Printer jobs on all platforms (including Windows) + /// + /// On Windows, generic file reads are delegated to CM via `start_read_job()` in + /// `src/ui_cm_interface.rs` for elevated access. Printer jobs bypass this delegation + /// since they read from in-memory data (`MemoryCursor`), not the filesystem. + /// + /// Both Connection-side and CM-side implementations use `TransferJob::new_read()` + /// with similar parameters. When modifying job creation logic, ensure both paths + /// stay in sync. + async fn create_and_start_read_job( + &mut self, + id: i32, + job_type: fs::JobType, + data_source: fs::DataSource, + file_num: i32, + include_hidden: bool, + overwrite_detection: bool, + path: String, + check_file_limit: bool, + ) { + match fs::TransferJob::new_read( + id, + job_type, + "".to_string(), + data_source, + file_num, + include_hidden, + false, + overwrite_detection, + ) { + Err(err) => { + self.send(fs::new_error(id, err, 0)).await; + } + Ok(job) => { + if check_file_limit { + if let Err(msg) = + crate::ui_cm_interface::check_file_count_limit(job.files().len()) + { + self.send(fs::new_error(id, msg, -1)).await; + return; + } + } + self.process_new_read_job(job, path).await; + } + } + } + + #[inline] + async fn send(&mut self, msg: Message) { + allow_err!(self.stream.send(&msg).await); + } + + pub fn alive_conns() -> Vec { + ALIVE_CONNS.lock().unwrap().clone() + } + + #[cfg(windows)] + fn portable_check(&mut self) { + if self.portable.is_installed || !self.is_remote() || !self.keyboard { + return; + } + let running = portable_client::running(); + let show_elevation = !running; + self.send_to_cm(ipc::Data::DataPortableService( + ipc::DataPortableService::CmShowElevation(show_elevation), + )); + if self.authorized { + let p = &mut self.portable; + if Some(running) != p.last_running { + p.last_running = Some(running); + let mut misc = Misc::new(); + misc.set_portable_service_running(running); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); + if p.last_uac != uac { + p.last_uac = uac; + if !uac || !running { + let mut misc = Misc::new(); + misc.set_uac(uac); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + } + let foreground_window_elevated = crate::video_service::IS_FOREGROUND_WINDOW_ELEVATED + .lock() + .unwrap() + .clone(); + if p.last_foreground_window_elevated != foreground_window_elevated { + p.last_foreground_window_elevated = foreground_window_elevated; + if !foreground_window_elevated || !running { + let mut misc = Misc::new(); + misc.set_foreground_window_elevated(foreground_window_elevated); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + } + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn release_pressed_modifiers(&mut self) { + for modifier in self.pressed_modifiers.iter() { + rdev::simulate(&rdev::EventType::KeyRelease(*modifier)).ok(); + } + self.pressed_modifiers.clear(); + } + + fn get_auto_disconenct_timer() -> Option<(Instant, u64)> { + if Config::get_option("allow-auto-disconnect") == "Y" { + let mut minute: u64 = Config::get_option("auto-disconnect-timeout") + .parse() + .unwrap_or(10); + if minute == 0 { + minute = 10; + } + Some((Instant::now(), minute)) + } else { + None + } + } + + fn update_auto_disconnect_timer(&mut self) { + self.auto_disconnect_timer + .as_mut() + .map(|t| t.0 = Instant::now()); + } + + #[cfg(feature = "hwcodec")] + fn update_supported_encoding(&mut self) { + let Some(last) = &self.last_supported_encoding else { + return; + }; + let usable = scrap::codec::Encoder::usable_encoding(); + let Some(usable) = usable else { + return; + }; + if usable.vp8 != last.vp8 + || usable.av1 != last.av1 + || usable.h264 != last.h264 + || usable.h265 != last.h265 + { + let mut misc: Misc = Misc::new(); + let supported_encoding = SupportedEncoding { + vp8: usable.vp8, + av1: usable.av1, + h264: usable.h264, + h265: usable.h265, + ..last.clone() + }; + log::info!("update supported encoding: {:?}", supported_encoding); + self.last_supported_encoding = Some(supported_encoding.clone()); + misc.set_supported_encoding(supported_encoding); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + }; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn handle_cursor_switch_display(&mut self, pos: CursorPosition) { + if self.multi_ui_session { + return; + } + let displays = super::display_service::get_sync_displays(); + let d_index = displays.iter().position(|d| { + let scale = d.scale; + pos.x >= d.x + && pos.y >= d.y + && (pos.x - d.x) as f64 * scale < d.width as f64 + && (pos.y - d.y) as f64 * scale < d.height as f64 + }); + if let Some(d_index) = d_index { + if self.display_idx != d_index { + let mut misc = Misc::new(); + misc.set_follow_current_display(d_index as i32); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(msg_out).await; + } + } + } + + #[inline] + fn session_key(&self) -> SessionKey { + SessionKey { + peer_id: self.lr.my_id.clone(), + name: self.lr.my_name.clone(), + session_id: self.lr.session_id, + } + } + + fn is_authed_remote_conn(&self) -> bool { + if let Some(id) = self.authed_conn_id.as_ref() { + return id.conn_type() == AuthConnType::Remote; + } + false + } + + fn is_authed_view_camera_conn(&self) -> bool { + if let Some(id) = self.authed_conn_id.as_ref() { + return id.conn_type() == AuthConnType::ViewCamera; + } + false + } + + #[cfg(feature = "unix-file-copy-paste")] + async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) { + let is_stopping_allowed = clip.is_stopping_allowed(); + let file_transfer_enabled = self.file_transfer_enabled(); + let stop = is_stopping_allowed && !file_transfer_enabled; + log::debug!( + "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; + // 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`. + if clip.is_beginning_message() + && crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y" + { + // If one way file transfer is enabled, don't send clipboard file to client + } else { + // Maybe we should end the connection, because copy&paste files causes everything to wait. + allow_err!( + self.stream + .send(&crate::clipboard_file::clip_2_msg(clip)) + .await + ); + } + } + } + + #[inline] + #[cfg(feature = "unix-file-copy-paste")] + fn try_empty_file_clipboard(&mut self) { + try_empty_clipboard_files(ClipboardSide::Host, self.inner.id()); + } + + #[cfg(all(target_os = "windows", feature = "flutter"))] + async fn send_printer_request(&mut self, data: Vec) { + // This path is only used to identify the printer job. + let path = format!("RustDesk://FsJob//Printer/{}", get_time()); + + let msg = fs::new_send(0, fs::JobType::Printer, path.clone(), 1, false); + self.send(msg).await; + self.printer_data + .retain(|(t, _, _)| t.elapsed().as_secs() < 60); + self.printer_data.push((Instant::now(), path, data)); + } + + #[cfg(all(target_os = "windows", feature = "flutter"))] + async fn send_remote_printing_disallowed(&mut self) { + let mut msg_out = Message::new(); + let res = MessageBox { + msgtype: "custom-nook-nocancel-hasclose".to_owned(), + title: "remote-printing-disallowed-tile-tip".to_owned(), + text: "remote-printing-disallowed-text-tip".to_owned(), + link: "".to_owned(), + ..Default::default() + }; + msg_out.set_message_box(res); + self.send(msg_out).await; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn update_terminal_persistence(&mut self, persistent: bool) { + self.terminal_persistent = persistent; + terminal_service::set_persistent(&self.terminal_service_id, persistent).ok(); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn init_terminal_service(&mut self) { + debug_assert!(self.terminal_user_token.is_some()); + let Some(user_token) = self.terminal_user_token.clone() else { + // unreachable, but keep it for safety + log::error!("Terminal user token is not set."); + return; + }; + if self.terminal_service_id.is_empty() { + self.terminal_service_id = terminal_service::generate_service_id(); + } + let s = Box::new(terminal_service::new( + self.terminal_service_id.clone(), + self.terminal_persistent, + user_token.to_terminal_service_token(), + )); + s.on_subscribe(self.inner.clone()); + self.terminal_generic_service = Some(s); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn handle_terminal_action(&mut self, action: TerminalAction) -> ResultType<()> { + debug_assert!(self.terminal_user_token.is_some()); + let Some(user_token) = self.terminal_user_token.clone() else { + // unreacheable, but keep it for safety + bail!("Terminal user token is not set."); + }; + let mut proxy = terminal_service::TerminalServiceProxy::new( + self.terminal_service_id.clone(), + Some(self.terminal_persistent), + user_token.to_terminal_service_token(), + ); + + match proxy.handle_action(&action) { + Ok(Some(response)) => { + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + self.send(msg_out).await; + } + Ok(None) => { + // No response needed + } + Err(err) => { + let mut response = TerminalResponse::new(); + let mut error = TerminalError::new(); + error.message = format!("Failed to handle action: {}", err); + response.set_error(error); + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + self.send(msg_out).await; + } + } + + Ok(()) + } +} + +pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { + SWITCH_SIDES_UUID + .lock() + .unwrap() + .insert(id, (tokio::time::Instant::now(), uuid)); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +async fn start_ipc( + mut rx_to_cm: mpsc::UnboundedReceiver, + tx_from_cm: mpsc::UnboundedSender, + mut _rx_desktop_ready: mpsc::Receiver<()>, + tx_stream_ready: mpsc::Sender<()>, +) -> ResultType<()> { + use hbb_common::anyhow::anyhow; + + loop { + if !crate::platform::is_prelogin() { + break; + } + sleep(1.).await; + } + let mut stream = None; + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { + stream = Some(s); + } else { + #[allow(unused_mut)] + #[allow(unused_assignments)] + let mut args = vec!["--cm"]; + #[allow(unused_mut)] + #[cfg(target_os = "linux")] + let mut user = None; + + // Cm run as user, wait until desktop session is ready. + #[cfg(target_os = "linux")] + if crate::platform::is_headless_allowed() && linux_desktop_manager::is_headless() { + let mut username = linux_desktop_manager::get_username(); + loop { + if !username.is_empty() { + break; + } + let _res = timeout(1_000, _rx_desktop_ready.recv()).await; + username = linux_desktop_manager::get_username(); + } + let uid = { + let output = run_cmds(&format!("id -u {}", &username))?; + let output = output.trim(); + if output.is_empty() || !output.parse::().is_ok() { + bail!("Invalid username {}", &username); + } + output.to_string() + }; + user = Some((uid, username)); + args = vec!["--cm-no-ui"]; + } + let run_done; + if crate::platform::is_root() { + let mut res = Ok(None); + for _ in 0..10 { + #[cfg(not(any(target_os = "linux")))] + { + log::debug!("Start cm"); + res = crate::platform::run_as_user(args.clone()); + } + #[cfg(target_os = "linux")] + { + log::debug!("Start cm"); + res = crate::platform::run_as_user( + args.clone(), + user.clone(), + None::<(&str, &str)>, + ); + } + if res.is_ok() { + break; + } + log::error!("Failed to run cm: {res:?}"); + sleep(1.).await; + } + if let Some(task) = res? { + super::CHILD_PROCESS.lock().unwrap().push(task); + } + run_done = true; + } else { + run_done = false; + } + if !run_done { + log::debug!("Start cm"); + super::CHILD_PROCESS + .lock() + .unwrap() + .push(crate::run_me(args)?); + } + for _ in 0..20 { + sleep(0.3).await; + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { + stream = Some(s); + break; + } + } + if stream.is_none() { + bail!("Failed to connect to connection manager"); + } + } + + let _res = tx_stream_ready.send(()).await; + let mut stream = stream.ok_or(anyhow!("none stream"))?; + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + return Err(err.into()); + } + Ok(Some(data)) => { + match data { + ipc::Data::ClickTime(_)=> { + let ct = CLICK_TIME.load(Ordering::SeqCst); + let data = ipc::Data::ClickTime(ct); + stream.send(&data).await?; + } + // FileBlockFromCM: data is always sent separately via send_raw. + // The data field has #[serde(skip)], so it's empty after deserialization. + // Read the raw data bytes following this message. + // + // Note: Empty data (for empty files) is correctly handled. BytesCodec with + // raw=false adds a length prefix, so next_raw() returns empty BytesMut for + // zero-length frames. This mirrors the WriteBlock pattern below. + ipc::Data::FileBlockFromCM { id, file_num, data: _, compressed, conn_id } => { + let raw_data = stream.next_raw().await?; + tx_from_cm.send(ipc::Data::FileBlockFromCM { + id, + file_num, + data: raw_data.into(), + compressed, + conn_id, + })?; + } + _ => { + tx_from_cm.send(data)?; + } + } + } + _ => {} + } + } + res = rx_to_cm.recv() => { + match res { + Some(data) => { + if let Data::FS(ipc::FS::WriteBlock{id, + file_num, + data, + compressed}) = data { + stream.send(&Data::FS(ipc::FS::WriteBlock{id, file_num, data: Bytes::new(), compressed})).await?; + stream.send_raw(data).await?; + } else { + stream.send(&data).await?; + } + } + None => { + bail!("expected"); + } + } + } + } + } +} + +// in case screen is sleep and blank, here to activate it +fn try_activate_screen() { + #[cfg(windows)] + std::thread::spawn(|| { + mouse_move_relative(-6, -6); + std::thread::sleep(std::time::Duration::from_millis(30)); + mouse_move_relative(6, 6); + }); +} + +pub enum AlarmAuditType { + IpWhitelist = 0, + ExceedThirtyAttempts = 1, + SixAttemptsWithinOneMinute = 2, + // ExceedThirtyLoginAttempts = 3, + // MultipleLoginsAttemptsWithinOneMinute = 4, + // MultipleLoginsAttemptsWithinOneHour = 5, + ExceedIPv6PrefixAttempts = 6, +} + +pub enum FileAuditType { + RemoteSend = 0, + RemoteReceive = 1, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct FileActionLog { + id: i32, + conn_id: i32, + path: String, + dir: bool, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct FileRenameLog { + conn_id: i32, + path: String, + new_name: String, +} + +struct FileRemoveLogControl { + conn_id: i32, + instant: Instant, + removed_files: Vec, + removed_dirs: Vec, +} + +impl FileRemoveLogControl { + fn new(conn_id: i32) -> Self { + FileRemoveLogControl { + conn_id, + instant: Instant::now(), + removed_files: vec![], + removed_dirs: vec![], + } + } + + fn on_remove_file(&mut self, f: FileRemoveFile) -> Option { + self.instant = Instant::now(); + self.removed_files.push(f.clone()); + Some(ipc::Data::FileTransferLog(( + "remove".to_string(), + serde_json::to_string(&FileActionLog { + id: f.id, + conn_id: self.conn_id, + path: f.path, + dir: false, + }) + .unwrap_or_default(), + ))) + } + + fn on_remove_dir(&mut self, d: FileRemoveDir) -> Option { + self.instant = Instant::now(); + let direct_child = |parent: &str, child: &str| { + PathBuf::from(child).parent().map(|x| x.to_path_buf()) == Some(PathBuf::from(parent)) + }; + self.removed_files + .retain(|f| !direct_child(&f.path, &d.path)); + self.removed_dirs + .retain(|x| !direct_child(&d.path, &x.path)); + if !self + .removed_dirs + .iter() + .any(|x| direct_child(&x.path, &d.path)) + { + self.removed_dirs.push(d.clone()); + } + Some(ipc::Data::FileTransferLog(( + "remove".to_string(), + serde_json::to_string(&FileActionLog { + id: d.id, + conn_id: self.conn_id, + path: d.path, + dir: true, + }) + .unwrap_or_default(), + ))) + } + + fn on_timer(&mut self) -> Vec { + if self.instant.elapsed().as_secs() < 1 { + return vec![]; + } + let mut v: Vec = vec![]; + self.removed_files + .drain(..) + .map(|f| { + v.push(ipc::Data::FileTransferLog(( + "remove".to_string(), + serde_json::to_string(&FileActionLog { + id: f.id, + conn_id: self.conn_id, + path: f.path, + dir: false, + }) + .unwrap_or_default(), + ))); + }) + .count(); + self.removed_dirs + .drain(..) + .map(|d| { + v.push(ipc::Data::FileTransferLog(( + "remove".to_string(), + serde_json::to_string(&FileActionLog { + id: d.id, + conn_id: self.conn_id, + path: d.path, + dir: true, + }) + .unwrap_or_default(), + ))); + }) + .count(); + v + } +} + +fn start_wakelock_thread() -> std::sync::mpsc::Sender<(usize, usize)> { + // Check if we should keep awake during incoming sessions + use crate::platform::{get_wakelock, WakeLock}; + let (tx, rx) = std::sync::mpsc::channel::<(usize, usize)>(); + std::thread::spawn(move || { + let mut wakelock: Option = None; + let mut last_display = false; + loop { + match rx.recv() { + Ok((conn_count, remote_count)) => { + let keep_awake = config::Config::get_bool_option( + keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS, + ); + *WAKELOCK_KEEP_AWAKE_OPTION.lock().unwrap() = Some(keep_awake); + if conn_count == 0 || !keep_awake { + if wakelock.is_some() { + wakelock = None; + log::info!("drop wakelock"); + } + } else { + let mut display = remote_count > 0; + if let Some(_w) = wakelock.as_mut() { + if display != last_display { + #[cfg(any(target_os = "windows", target_os = "macos"))] + { + log::info!("set wakelock display to {display}"); + if let Err(e) = _w.set_display(display) { + log::error!( + "failed to set wakelock display to {display}: {e:?}" + ); + } + } + } + } else { + if cfg!(target_os = "linux") { + display = true; + } + wakelock = Some(get_wakelock(display)); + } + last_display = display; + } + } + Err(e) => { + log::error!("wakelock receive error: {e:?}"); + break; + } + } + } + }); + tx +} + +#[cfg(all(target_os = "windows", feature = "flutter"))] +pub fn on_printer_data(data: Vec) { + crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.printer) + .next() + .map(|c| { + c.sender.send(Data::PrinterData(data)).ok(); + }); +} + +#[cfg(windows)] +pub struct PortableState { + pub last_uac: bool, + pub last_foreground_window_elevated: bool, + pub last_running: Option, + pub is_installed: bool, +} + +#[cfg(windows)] +impl Default for PortableState { + fn default() -> Self { + Self { + is_installed: crate::platform::is_installed(), + last_uac: Default::default(), + last_foreground_window_elevated: Default::default(), + last_running: Default::default(), + } + } +} + +impl Drop for Connection { + fn drop(&mut self) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.release_pressed_modifiers(); + + if let Some(s) = self.terminal_generic_service.as_ref() { + s.join(); + } + + #[cfg(target_os = "windows")] + if let Some(TerminalUserToken::CurrentLogonUser(token)) = self.terminal_user_token.take() { + if token.as_raw() != 0 { + unsafe { + hbb_common::allow_err!(CloseHandle(HANDLE(token.as_raw() as _))); + }; + } + } + } +} + +#[cfg(target_os = "linux")] +struct LinuxHeadlessHandle { + pub is_headless_allowed: bool, + pub is_headless: bool, + pub wait_ipc_timeout: u64, + pub rx_cm_stream_ready: mpsc::Receiver<()>, + pub tx_desktop_ready: mpsc::Sender<()>, +} + +#[cfg(target_os = "linux")] +impl LinuxHeadlessHandle { + pub fn new(rx_cm_stream_ready: mpsc::Receiver<()>, tx_desktop_ready: mpsc::Sender<()>) -> Self { + let is_headless_allowed = crate::is_server() && crate::platform::is_headless_allowed(); + let is_headless = is_headless_allowed && linux_desktop_manager::is_headless(); + Self { + is_headless_allowed, + is_headless, + wait_ipc_timeout: 10_000, + rx_cm_stream_ready, + tx_desktop_ready, + } + } + + pub fn try_start_desktop(&mut self, os_login: Option<&OSLogin>) -> String { + if self.is_headless_allowed { + match os_login { + Some(os_login) => { + linux_desktop_manager::try_start_desktop(&os_login.username, &os_login.password) + } + None => linux_desktop_manager::try_start_desktop("", ""), + } + } else { + "".to_string() + } + } + + pub async fn wait_desktop_cm_ready(&mut self) { + if self.is_headless { + self.tx_desktop_ready.send(()).await.ok(); + let _res = timeout(self.wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await; + } + } +} + +extern "C" fn connection_shutdown_hook() { + // https://stackoverflow.com/questions/35980148/why-does-an-atexit-handler-panic-when-it-accesses-stdout + // Please make sure there is no print in the call stack + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + *WALLPAPER_REMOVER.lock().unwrap() = None; + } +} + +#[cfg(target_os = "macos")] +#[derive(Debug, Default)] +struct Retina { + displays: Vec, +} + +#[cfg(target_os = "macos")] +impl Retina { + #[inline] + fn set_displays(&mut self, displays: &Vec) { + self.displays = displays.clone(); + } + + #[inline] + fn on_mouse_event(&mut self, e: &mut MouseEvent, current: usize) { + let evt_type = e.mask & crate::input::MOUSE_TYPE_MASK; + // Delta-based events do not contain absolute coordinates. + // Avoid applying Retina coordinate scaling to them. + if evt_type == crate::input::MOUSE_TYPE_WHEEL + || evt_type == crate::input::MOUSE_TYPE_TRACKPAD + || evt_type == crate::input::MOUSE_TYPE_MOVE_RELATIVE + { + return; + } + let Some(d) = self.displays.get(current) else { + return; + }; + let s = d.scale; + if s > 1.0 && e.x >= d.x && e.y >= d.y && e.x < d.x + d.width && e.y < d.y + d.height { + e.x = d.x + ((e.x - d.x) as f64 / s) as i32; + e.y = d.y + ((e.y - d.y) as f64 / s) as i32; + } + } + + #[inline] + fn on_cursor_pos(&mut self, pos: &CursorPosition, current: usize) -> Option { + let Some(d) = self.displays.get(current) else { + return None; + }; + let s = d.scale; + if s > 1.0 + && pos.x >= d.x + && pos.y >= d.y + && (pos.x - d.x) as f64 * s < d.width as f64 + && (pos.y - d.y) as f64 * s < d.height as f64 + { + let mut pos = pos.clone(); + pos.x = d.x + ((pos.x - d.x) as f64 * s) as i32; + pos.y = d.y + ((pos.y - d.y) as f64 * s) as i32; + let mut msg = Message::new(); + msg.set_cursor_position(pos); + return Some(msg); + } + None + } +} + +/// Get control permission state from CONTROL_PERMISSIONS_ARRAY. +/// Returns: Some(false) if any disable, Some(true) if any enable (and no disable), None if not set. +pub fn get_control_permission_state( + permission: hbb_common::rendezvous_proto::control_permissions::Permission, + disable_if_has_disabled: bool, +) -> Option { + let control_permissions = CONTROL_PERMISSIONS_ARRAY.lock().unwrap(); + let mut has_enable = false; + let mut has_disable = false; + for (_, cp) in control_permissions.iter() { + match crate::get_control_permission(cp.permissions, permission) { + Some(false) => has_disable = true, + Some(true) => has_enable = true, + None => {} + } + } + if disable_if_has_disabled { + if has_disable { + Some(false) + } else if has_enable { + Some(true) + } else { + None + } + } else { + if has_enable { + Some(true) + } else if has_disable { + Some(false) + } else { + None + } + } +} + +pub struct AuthedConn { + pub conn_id: i32, + pub conn_type: AuthConnType, + pub session_key: SessionKey, + pub sender: mpsc::UnboundedSender, + pub printer: bool, +} + +mod raii { + // ALIVE_CONNS: all connections, including unauthorized connections + // AUTHED_CONNS: all authorized connections + // CONTROL_PERMISSIONS_ARRAY: all non-None control permissions + + use super::*; + pub struct ConnectionID(i32); + + impl ConnectionID { + pub fn new(id: i32) -> Self { + ALIVE_CONNS.lock().unwrap().push(id); + Self(id) + } + } + + impl Drop for ConnectionID { + fn drop(&mut self) { + let mut active_conns_lock = ALIVE_CONNS.lock().unwrap(); + active_conns_lock.retain(|&c| c != self.0); + } + } + + pub struct AuthedConnID(i32, AuthConnType); + + impl AuthedConnID { + pub fn new( + conn_id: i32, + conn_type: AuthConnType, + session_key: SessionKey, + sender: mpsc::UnboundedSender, + lr: LoginRequest, + ) -> Self { + let printer = conn_type == crate::server::AuthConnType::Remote + && crate::is_support_remote_print(&lr.version) + && lr.my_platform == hbb_common::whoami::Platform::Windows.to_string(); + AUTHED_CONNS.lock().unwrap().push(AuthedConn { + conn_id, + conn_type, + session_key, + sender, + printer, + }); + Self::check_wake_lock(); + use std::sync::Once; + static _ONCE: Once = Once::new(); + _ONCE.call_once(|| { + shutdown_hooks::add_shutdown_hook(connection_shutdown_hook); + }); + if conn_type == AuthConnType::Remote || conn_type == AuthConnType::ViewCamera { + video_service::VIDEO_QOS + .lock() + .unwrap() + .on_connection_open(conn_id); + } + Self(conn_id, conn_type) + } + + fn check_wake_lock() { + let conn_count = AUTHED_CONNS.lock().unwrap().len(); + let remote_count = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.conn_type == AuthConnType::Remote) + .count(); + allow_err!(WAKELOCK_SENDER + .lock() + .unwrap() + .send((conn_count, remote_count))); + } + + pub fn check_wake_lock_on_setting_changed() { + let current = + config::Config::get_bool_option(keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS); + let cached = *WAKELOCK_KEEP_AWAKE_OPTION.lock().unwrap(); + if cached != Some(current) { + Self::check_wake_lock(); + } + } + + #[cfg(windows)] + pub fn non_port_forward_conn_count() -> usize { + AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.conn_type != AuthConnType::PortForward) + .count() + } + + pub fn check_remove_session(conn_id: i32, key: SessionKey) { + let mut lock = SESSIONS.lock().unwrap(); + let contains = lock.contains_key(&key); + if contains { + // No two remote connections with the same session key, just for ensure. + let is_remote = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .any(|c| c.conn_id == conn_id && c.conn_type == AuthConnType::Remote); + // If there are 2 connections with the same peer_id and session_id, a remote connection and a file transfer or port forward connection, + // If any of the connections is closed allowing retry, this will not be called; + // If the file transfer/port forward connection is closed with no retry, the session should be kept for remote control menu action; + // If the remote connection is closed with no retry, keep the session is not reasonable in case there is a retry button in the remote side, and ignore network fluctuations. + let another_remote = AUTHED_CONNS.lock().unwrap().iter().any(|c| { + c.conn_id != conn_id + && c.session_key == key + && c.conn_type == AuthConnType::Remote + }); + if is_remote || !another_remote { + lock.remove(&key); + log::info!("remove session"); + } else { + // Keep the session if there is another remote connection with same peer_id and session_id. + log::info!("skip remove session"); + } + } + } + + pub fn update_or_insert_session( + key: SessionKey, + password: Option, + tfa: Option, + ) { + let mut lock = SESSIONS.lock().unwrap(); + let session = lock.get_mut(&key); + if let Some(session) = session { + if let Some(password) = password { + session.random_password = password; + } + if let Some(tfa) = tfa { + session.tfa = tfa; + } + } else { + lock.insert( + key, + Session { + random_password: password.unwrap_or_default(), + tfa: tfa.unwrap_or_default(), + last_recv_time: Arc::new(Mutex::new(Instant::now())), + }, + ); + } + } + + pub fn set_session_2fa(key: SessionKey) { + let mut lock = SESSIONS.lock().unwrap(); + let session = lock.get_mut(&key); + if let Some(session) = session { + session.tfa = true; + } else { + lock.insert( + key, + Session { + last_recv_time: Arc::new(Mutex::new(Instant::now())), + random_password: "".to_owned(), + tfa: true, + }, + ); + } + } + + pub fn conn_type(&self) -> AuthConnType { + self.1 + } + } + + impl Drop for AuthedConnID { + fn drop(&mut self) { + if self.1 == AuthConnType::Remote || self.1 == AuthConnType::ViewCamera { + scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Remove(self.0)); + video_service::VIDEO_QOS + .lock() + .unwrap() + .on_connection_close(self.0); + } + // Clear per-connection state to avoid stale behavior if conn ids are reused. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + clear_relative_mouse_active(self.0); + AUTHED_CONNS.lock().unwrap().retain(|c| c.conn_id != self.0); + let remote_count = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.conn_type == AuthConnType::Remote) + .count(); + if remote_count == 0 { + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + *WALLPAPER_REMOVER.lock().unwrap() = None; + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + display_service::restore_resolutions(); + #[cfg(windows)] + let _ = virtual_display_manager::reset_all(); + #[cfg(target_os = "linux")] + scrap::wayland::pipewire::try_close_session(); + } + Self::check_wake_lock(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + use crate::whiteboard; + whiteboard::unregister_whiteboard(whiteboard::get_key_cursor(self.0)); + } + } + } + + pub struct ControlPermissionsID { + id: i32, + control_permissions: Option, + } + + impl Drop for ControlPermissionsID { + fn drop(&mut self) { + if self.control_permissions.is_some() { + let mut lock = CONTROL_PERMISSIONS_ARRAY.lock().unwrap(); + lock.retain(|(conn_id, _)| *conn_id != self.id); + } + } + } + impl ControlPermissionsID { + pub fn new(id: i32, control_permissions: &Option) -> Self { + if let Some(s) = control_permissions { + CONTROL_PERMISSIONS_ARRAY + .lock() + .unwrap() + .push((id, s.clone())); + } + Self { + id, + control_permissions: control_permissions.clone(), + } + } + } +} + +mod test { + #[allow(unused)] + use super::*; + + #[cfg(target_os = "macos")] + #[test] + fn retina() { + let mut retina = Retina { + displays: vec![DisplayInfo { + x: 10, + y: 10, + width: 1000, + height: 1000, + scale: 2.0, + ..Default::default() + }], + }; + let mut mouse: MouseEvent = MouseEvent { + x: 510, + y: 510, + ..Default::default() + }; + retina.on_mouse_event(&mut mouse, 0); + assert_eq!(mouse.x, 260); + assert_eq!(mouse.y, 260); + let pos = CursorPosition { + x: 260, + y: 260, + ..Default::default() + }; + let msg = retina.on_cursor_pos(&pos, 0).unwrap(); + let pos = msg.cursor_position(); + assert_eq!(pos.x, 510); + assert_eq!(pos.y, 510); + } + + #[test] + fn ipv6() { + assert!(Ipv6Addr::from_str("::1").is_ok()); + assert!(Ipv6Addr::from_str("127.0.0.1").is_err()); + assert!(Ipv6Addr::from_str("0").is_err()); + } +} diff --git a/vendor/rustdesk/src/server/dbus.rs b/vendor/rustdesk/src/server/dbus.rs new file mode 100644 index 0000000..8eb4447 --- /dev/null +++ b/vendor/rustdesk/src/server/dbus.rs @@ -0,0 +1,92 @@ +/// Url handler based on dbus +/// +/// Note: +/// On linux, we use dbus to communicate multiple rustdesk process. +/// [Flutter]: handle uni links for linux +use dbus::blocking::Connection; +use dbus_crossroads::{Crossroads, IfaceBuilder}; +use hbb_common::log; +#[cfg(feature = "flutter")] +use std::collections::HashMap; +use std::{error::Error, fmt, time::Duration}; + +const DBUS_NAME: &str = "org.rustdesk.rustdesk"; +const DBUS_PREFIX: &str = "/dbus"; +const DBUS_METHOD_NEW_CONNECTION: &str = "NewConnection"; +const DBUS_METHOD_NEW_CONNECTION_ID: &str = "id"; +const DBUS_METHOD_RETURN: &str = "ret"; +const DBUS_METHOD_RETURN_SUCCESS: &str = "ok"; +const DBUS_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(Debug)] +struct DbusError(String); + +impl fmt::Display for DbusError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "RustDesk DBus Error: {}", self.0) + } +} + +impl Error for DbusError {} + +/// invoke new connection from dbus +/// +/// [Tips]: +/// How to test by CLI: +/// - use dbus-send command: +/// `dbus-send --session --print-reply --dest=org.rustdesk.rustdesk /dbus org.rustdesk.rustdesk.NewConnection string:'PEER_ID'` +pub fn invoke_new_connection(uni_links: String) -> Result<(), Box> { + log::info!("Starting dbus service for uni"); + let conn = Connection::new_session()?; + let proxy = conn.with_proxy(DBUS_NAME, DBUS_PREFIX, DBUS_TIMEOUT); + let (ret,): (String,) = + proxy.method_call(DBUS_NAME, DBUS_METHOD_NEW_CONNECTION, (uni_links,))?; + if ret != DBUS_METHOD_RETURN_SUCCESS { + log::error!("error on call new connection to dbus server"); + return Err(Box::new(DbusError("not success".to_string()))); + } + Ok(()) +} + +/// start dbus server +/// +/// [Blocking]: +/// The function will block current thread to serve dbus server. +/// So it's suitable to spawn a new thread dedicated to dbus server. +pub fn start_dbus_server() -> Result<(), Box> { + let conn: Connection = Connection::new_session()?; + let _ = conn.request_name(DBUS_NAME, false, true, false)?; + let mut cr = Crossroads::new(); + let token = cr.register(DBUS_NAME, handle_client_message); + cr.insert(DBUS_PREFIX, &[token], ()); + cr.serve(&conn)?; + Ok(()) +} + +fn handle_client_message(builder: &mut IfaceBuilder<()>) { + // register new connection dbus + builder.method( + DBUS_METHOD_NEW_CONNECTION, + (DBUS_METHOD_NEW_CONNECTION_ID,), + (DBUS_METHOD_RETURN,), + move |_, _, (_uni_links,): (String,)| { + #[cfg(feature = "flutter")] + { + use crate::flutter; + let data = HashMap::from([ + ("name", "on_url_scheme_received"), + ("url", _uni_links.as_str()), + ]); + let event = serde_json::ser::to_string(&data).unwrap_or("".to_string()); + match crate::flutter::push_global_event(flutter::APP_TYPE_MAIN, event) { + None => log::error!("failed to find main event stream"), + Some(false) => { + log::error!("failed to add dbus message to flutter global dbus stream.") + } + Some(true) => {} + } + } + return Ok((DBUS_METHOD_RETURN_SUCCESS.to_string(),)); + }, + ); +} diff --git a/vendor/rustdesk/src/server/display_service.rs b/vendor/rustdesk/src/server/display_service.rs new file mode 100644 index 0000000..fe3621f --- /dev/null +++ b/vendor/rustdesk/src/server/display_service.rs @@ -0,0 +1,488 @@ +use super::*; +use crate::common::SimpleCallOnReturn; +#[cfg(target_os = "linux")] +use crate::platform::linux::is_x11; +#[cfg(windows)] +use crate::virtual_display_manager; +#[cfg(windows)] +use hbb_common::get_version_number; +use hbb_common::protobuf::MessageField; +use scrap::Display; +use std::sync::atomic::{AtomicBool, Ordering}; + +// https://github.com/rustdesk/rustdesk/discussions/6042, avoiding dbus call + +pub const NAME: &'static str = "display"; + +#[cfg(windows)] +const DUMMY_DISPLAY_SIDE_MAX_SIZE: usize = 1024; + +struct ChangedResolution { + original: (i32, i32), + changed: (i32, i32), +} + +lazy_static::lazy_static! { + static ref IS_CAPTURER_MAGNIFIER_SUPPORTED: bool = is_capturer_mag_supported(); + static ref CHANGED_RESOLUTIONS: Arc>> = Default::default(); + // Initial primary display index. + // It should not be updated when displays changed. + pub static ref PRIMARY_DISPLAY_IDX: usize = get_primary(); + static ref SYNC_DISPLAYS: Arc> = Default::default(); +} + +// https://github.com/rustdesk/rustdesk/pull/8537 +static TEMP_IGNORE_DISPLAYS_CHANGED: AtomicBool = AtomicBool::new(false); + +#[derive(Default)] +struct SyncDisplaysInfo { + displays: Vec, + is_synced: bool, +} + +impl SyncDisplaysInfo { + fn check_changed(&mut self, displays: Vec) { + if self.displays.len() != displays.len() { + self.displays = displays; + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + self.is_synced = false; + } + return; + } + for (i, d) in displays.iter().enumerate() { + if d != &self.displays[i] { + self.displays = displays; + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + self.is_synced = false; + } + return; + } + } + } + + fn get_update_sync_displays(&mut self) -> Option> { + if self.is_synced { + return None; + } + self.is_synced = true; + Some(self.displays.clone()) + } +} + +pub fn temp_ignore_displays_changed() -> SimpleCallOnReturn { + TEMP_IGNORE_DISPLAYS_CHANGED.store(true, std::sync::atomic::Ordering::Relaxed); + SimpleCallOnReturn { + b: true, + f: Box::new(move || { + // Wait for a while to make sure check_display_changed() is called + // after video service has sending its `SwitchDisplay` message(`try_broadcast_display_changed()`). + std::thread::sleep(Duration::from_millis(1000)); + TEMP_IGNORE_DISPLAYS_CHANGED.store(false, Ordering::Relaxed); + // Trigger the display changed message. + SYNC_DISPLAYS.lock().unwrap().is_synced = false; + }), + } +} + +// This function is really useful, though a duplicate check if display changed. +// The video server will then send the following messages to the client: +// 1. the supported resolutions of the {idx} display +// 2. the switch resolution message, so that the client can record the custom resolution. +pub(super) fn check_display_changed( + ndisplay: usize, + idx: usize, + (x, y, w, h): (i32, i32, usize, usize), +) -> Option { + #[cfg(target_os = "linux")] + { + // wayland do not support changing display for now + if !is_x11() { + return None; + } + } + + let lock = SYNC_DISPLAYS.lock().unwrap(); + // If plugging out a monitor && lock.displays.get(idx) is None. + // 1. The client version < 1.2.4. The client side has to reconnect. + // 2. The client version > 1.2.4, The client side can handle the case because sync peer info message will be sent. + // But it is acceptable to for the user to reconnect manually, because the monitor is unplugged. + let d = lock.displays.get(idx)?; + if ndisplay != lock.displays.len() { + return Some(d.clone()); + } + if !(d.x == x && d.y == y && d.width == w as i32 && d.height == h as i32) { + Some(d.clone()) + } else { + None + } +} + +#[inline] +pub fn set_last_changed_resolution(display_name: &str, original: (i32, i32), changed: (i32, i32)) { + let mut lock = CHANGED_RESOLUTIONS.write().unwrap(); + match lock.get_mut(display_name) { + Some(res) => res.changed = changed, + None => { + lock.insert( + display_name.to_owned(), + ChangedResolution { original, changed }, + ); + } + } +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn restore_resolutions() { + for (name, res) in CHANGED_RESOLUTIONS.read().unwrap().iter() { + let (w, h) = res.original; + log::info!("Restore resolution of display '{}' to ({}, {})", name, w, h); + if let Err(e) = crate::platform::change_resolution(name, w as _, h as _) { + log::error!( + "Failed to restore resolution of display '{}' to ({},{}): {}", + name, + w, + h, + e + ); + } + } + // Can be cleared because restore resolutions is called when there is no client connected. + CHANGED_RESOLUTIONS.write().unwrap().clear(); +} + +#[inline] +fn is_capturer_mag_supported() -> bool { + #[cfg(windows)] + return scrap::CapturerMag::is_supported(); + #[cfg(not(windows))] + false +} + +#[inline] +pub fn capture_cursor_embedded() -> bool { + scrap::is_cursor_embedded() +} + +#[inline] +#[cfg(windows)] +pub fn is_privacy_mode_mag_supported() -> bool { + return *IS_CAPTURER_MAGNIFIER_SUPPORTED + && get_version_number(&crate::VERSION) > get_version_number("1.1.9"); +} + +pub fn new() -> GenericService { + let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); + GenericService::run(&svc.clone(), run); + svc.sp +} + +fn displays_to_msg(displays: Vec) -> Message { + let mut pi = PeerInfo { + ..Default::default() + }; + pi.displays = displays.clone(); + + #[cfg(windows)] + if crate::platform::is_installed() { + let m = crate::virtual_display_manager::get_platform_additions(); + pi.platform_additions = serde_json::to_string(&m).unwrap_or_default(); + } + + // current_display should not be used in server. + // It is set to 0 for compatibility with old clients. + pi.current_display = 0; + let mut msg_out = Message::new(); + msg_out.set_peer_info(pi); + msg_out +} + +fn check_get_displays_changed_msg() -> Option { + #[cfg(target_os = "linux")] + { + if !is_x11() { + return get_displays_msg(); + } + } + check_update_displays(&try_get_displays().ok()?); + get_displays_msg() +} + +pub fn check_displays_changed() -> ResultType<()> { + #[cfg(target_os = "linux")] + { + // Currently, wayland need to call wayland::clear() before call Display::all(), otherwise it will cause + // block, or even crash here, https://github.com/rustdesk/rustdesk/blob/0bb4d43e9ea9d9dfb9c46c8d27d1a97cd0ad6bea/libs/scrap/src/wayland/pipewire.rs#L235 + if !is_x11() { + return Ok(()); + } + } + check_update_displays(&try_get_displays()?); + Ok(()) +} + +fn get_displays_msg() -> Option { + let displays = SYNC_DISPLAYS.lock().unwrap().get_update_sync_displays()?; + Some(displays_to_msg(displays)) +} + +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + while sp.ok() { + sp.snapshot(|sps| { + if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) { + if sps.has_subscribes() { + SYNC_DISPLAYS.lock().unwrap().is_synced = false; + bail!("new subscriber"); + } + } + Ok(()) + })?; + + if let Some(msg_out) = check_get_displays_changed_msg() { + sp.send(msg_out); + log::info!("Displays changed"); + } + std::thread::sleep(Duration::from_millis(300)); + } + + Ok(()) +} + +#[inline] +pub(super) fn get_original_resolution( + display_name: &str, + w: usize, + h: usize, +) -> MessageField { + #[cfg(windows)] + let is_rustdesk_virtual_display = + crate::virtual_display_manager::rustdesk_idd::is_virtual_display(&display_name); + #[cfg(not(windows))] + let is_rustdesk_virtual_display = false; + Some(if is_rustdesk_virtual_display { + Resolution { + width: 0, + height: 0, + ..Default::default() + } + } else { + let changed_resolutions = CHANGED_RESOLUTIONS.write().unwrap(); + let (width, height) = match changed_resolutions.get(display_name) { + Some(res) => { + res.original + /* + The resolution change may not happen immediately, `changed` has been updated, + but the actual resolution is old, it will be mistaken for a third-party change. + if res.changed.0 != w as i32 || res.changed.1 != h as i32 { + // If the resolution is changed by third process, remove the record in changed_resolutions. + changed_resolutions.remove(display_name); + (w as _, h as _) + } else { + res.original + } + */ + } + None => (w as _, h as _), + }; + Resolution { + width, + height, + ..Default::default() + } + }) + .into() +} + +pub(super) fn get_sync_displays() -> Vec { + SYNC_DISPLAYS.lock().unwrap().displays.clone() +} + +pub(super) fn get_display_info(idx: usize) -> Option { + SYNC_DISPLAYS.lock().unwrap().displays.get(idx).cloned() +} + +// Display to DisplayInfo +// The DisplayInfo is be sent to the peer. +pub(super) fn check_update_displays(all: &Vec) { + // For compatibility: if only one display, scale remains 1.0 and we use the physical size for `uinput`. + // If there are multiple displays, we use the logical size for `uinput` by setting scale to d.scale(). + #[cfg(target_os = "linux")] + let use_logical_scale = !is_x11() + && crate::is_server() + && scrap::wayland::display::get_displays().displays.len() > 1; + let displays = all + .iter() + .map(|d| { + let display_name = d.name(); + #[allow(unused_assignments)] + #[allow(unused_mut)] + let mut scale = 1.0; + #[cfg(target_os = "macos")] + { + scale = d.scale(); + } + #[cfg(target_os = "linux")] + { + if use_logical_scale { + scale = d.scale(); + } + } + let original_resolution = get_original_resolution( + &display_name, + ((d.width() as f64) / scale).round() as usize, + (d.height() as f64 / scale).round() as usize, + ); + DisplayInfo { + x: d.origin().0 as _, + y: d.origin().1 as _, + width: d.width() as _, + height: d.height() as _, + name: display_name, + online: d.is_online(), + cursor_embedded: false, + original_resolution, + scale, + ..Default::default() + } + }) + .collect::>(); + SYNC_DISPLAYS.lock().unwrap().check_changed(displays); +} + +pub fn is_inited_msg() -> Option { + #[cfg(target_os = "linux")] + if !is_x11() { + return super::wayland::is_inited(); + } + None +} + +pub async fn update_get_sync_displays_on_login() -> ResultType> { + #[cfg(target_os = "linux")] + { + if !is_x11() { + return super::wayland::get_displays().await; + } + } + #[cfg(not(windows))] + let displays = display_service::try_get_displays(); + #[cfg(windows)] + let displays = display_service::try_get_displays_add_amyuni_headless(); + check_update_displays(&displays?); + Ok(SYNC_DISPLAYS.lock().unwrap().displays.clone()) +} + +#[inline] +pub fn get_primary() -> usize { + #[cfg(target_os = "linux")] + { + if !is_x11() { + return match super::wayland::get_primary() { + Ok(n) => n, + Err(_) => 0, + }; + } + } + + try_get_displays().map(|d| get_primary_2(&d)).unwrap_or(0) +} + +#[inline] +pub fn get_primary_2(all: &Vec) -> usize { + all.iter().position(|d| d.is_primary()).unwrap_or(0) +} + +#[inline] +#[cfg(windows)] +fn no_displays(displays: &Vec) -> bool { + let display_len = displays.len(); + if display_len == 0 { + true + } else if display_len == 1 { + let display = &displays[0]; + if display.width() > DUMMY_DISPLAY_SIDE_MAX_SIZE + || display.height() > DUMMY_DISPLAY_SIDE_MAX_SIZE + { + return false; + } + let any_real = crate::platform::resolutions(&display.name()) + .iter() + .any(|r| { + (r.height as usize) > DUMMY_DISPLAY_SIDE_MAX_SIZE + || (r.width as usize) > DUMMY_DISPLAY_SIDE_MAX_SIZE + }); + !any_real + } else { + false + } +} + +#[inline] +#[cfg(not(windows))] +pub fn try_get_displays() -> ResultType> { + Ok(Display::all()?) +} + +#[inline] +#[cfg(windows)] +pub fn try_get_displays() -> ResultType> { + try_get_displays_(false) +} + +// We can't get full control of the virtual display if we use amyuni idd. +// If we add a virtual display, we cannot remove it automatically. +// So when using amyuni idd, we only add a virtual display for headless if it is required. +// eg. when the client is connecting. +#[inline] +#[cfg(windows)] +pub fn try_get_displays_add_amyuni_headless() -> ResultType> { + try_get_displays_(true) +} + +#[inline] +#[cfg(windows)] +pub fn try_get_displays_(add_amyuni_headless: bool) -> ResultType> { + let mut displays = Display::all()?; + + // Do not add virtual display if the platform is not installed or the virtual display is not supported. + if !crate::platform::is_installed() || !virtual_display_manager::is_virtual_display_supported() + { + return Ok(displays); + } + + // Enable headless virtual display when + // 1. `amyuni` idd is not used. + // 2. `amyuni` idd is used and `add_amyuni_headless` is true. + if virtual_display_manager::is_amyuni_idd() && !add_amyuni_headless { + return Ok(displays); + } + + // The following code causes a bug. + // The virtual display cannot be added when there's no session(eg. when exiting from RDP). + // Because `crate::platform::desktop_changed()` always returns true at that time. + // + // The code only solves a rare case: + // 1. The control side is connecting. + // 2. The windows session is switching, no displays are detected, but they're there. + // Then the controlled side plugs in a virtual display for "headless". + // + // No need to do the following check. But the code is kept here for marking the issue. + // If there're someones reporting the issue, we may add a better check by waiting for a while. (switching session). + // But I don't think it's good to add the timeout check without any issue. + // + // If is switching session, no displays may be detected. + // if displays.is_empty() && crate::platform::desktop_changed() { + // return Ok(displays); + // } + + let no_displays_v = no_displays(&displays); + if no_displays_v { + log::debug!("no displays, create virtual display"); + if let Err(e) = virtual_display_manager::plug_in_headless() { + log::error!("plug in headless failed {}", e); + } else { + displays = Display::all()?; + } + } + Ok(displays) +} diff --git a/vendor/rustdesk/src/server/input_service.rs b/vendor/rustdesk/src/server/input_service.rs new file mode 100644 index 0000000..97dc787 --- /dev/null +++ b/vendor/rustdesk/src/server/input_service.rs @@ -0,0 +1,2373 @@ +#[cfg(target_os = "linux")] +use super::rdp_input::client::{RdpInputKeyboard, RdpInputMouse}; +use super::*; +use crate::input::*; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::whiteboard; +#[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 rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey}; +#[cfg(target_os = "macos")] +use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; +#[cfg(target_os = "linux")] +use scrap::wayland::pipewire::RDP_SESSION_INFO; +#[cfg(target_os = "linux")] +use std::sync::mpsc; +use std::{ + convert::TryFrom, + ops::{Deref, DerefMut}, + sync::atomic::{AtomicBool, Ordering}, + thread, + time::{self, Duration, Instant}, +}; + +#[cfg(windows)] +use winapi::um::winuser::WHEEL_DELTA; + +const INVALID_CURSOR_POS: i32 = i32::MIN; +const INVALID_DISPLAY_IDX: i32 = -1; + +#[derive(Default)] +struct StateCursor { + hcursor: u64, + cursor_data: Arc, + cached_cursor_data: HashMap>, +} + +impl super::service::Reset for StateCursor { + fn reset(&mut self) { + *self = Default::default(); + crate::platform::reset_input_cache(); + fix_key_down_timeout(true); + } +} + +struct StatePos { + cursor_pos: (i32, i32), +} + +impl Default for StatePos { + fn default() -> Self { + Self { + cursor_pos: (INVALID_CURSOR_POS, INVALID_CURSOR_POS), + } + } +} + +impl super::service::Reset for StatePos { + fn reset(&mut self) { + self.cursor_pos = (INVALID_CURSOR_POS, INVALID_CURSOR_POS); + } +} + +impl StatePos { + #[inline] + fn is_valid(&self) -> bool { + self.cursor_pos.0 != INVALID_CURSOR_POS + } + + #[inline] + fn is_moved(&self, x: i32, y: i32) -> bool { + self.is_valid() && (self.cursor_pos.0 != x || self.cursor_pos.1 != y) + } +} + +#[derive(Default)] +struct StateWindowFocus { + display_idx: i32, +} + +impl super::service::Reset for StateWindowFocus { + fn reset(&mut self) { + self.display_idx = INVALID_DISPLAY_IDX; + } +} + +impl StateWindowFocus { + #[inline] + fn is_valid(&self) -> bool { + self.display_idx != INVALID_DISPLAY_IDX + } + + #[inline] + fn is_changed(&self, disp_idx: i32) -> bool { + self.is_valid() && self.display_idx != disp_idx + } +} + +#[derive(Default, Clone, Copy)] +struct Input { + conn: i32, + time: i64, + x: i32, + y: i32, +} + +const KEY_CHAR_START: u64 = 9999; + +// XKB keycode for Insert key (evdev KEY_INSERT code 110 + 8 for XKB offset) +#[cfg(target_os = "linux")] +const XKB_KEY_INSERT: u16 = evdev::Key::KEY_INSERT.code() + 8; + +#[derive(Clone, Default)] +pub struct MouseCursorSub { + inner: ConnInner, + cached: HashMap>, +} + +impl From for MouseCursorSub { + fn from(inner: ConnInner) -> Self { + Self { + inner, + cached: HashMap::new(), + } + } +} + +impl Subscriber for MouseCursorSub { + #[inline] + fn id(&self) -> i32 { + self.inner.id() + } + + #[inline] + fn send(&mut self, msg: Arc) { + if let Some(message::Union::CursorData(cd)) = &msg.union { + if let Some(msg) = self.cached.get(&cd.id) { + self.inner.send(msg.clone()); + } else { + self.inner.send(msg.clone()); + let mut tmp = Message::new(); + // only send id out, require client side cache also + tmp.set_cursor_id(cd.id); + self.cached.insert(cd.id, Arc::new(tmp)); + } + } else { + self.inner.send(msg); + } + } +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +struct LockModesHandler { + caps_lock_changed: bool, + num_lock_changed: bool, +} + +#[cfg(target_os = "macos")] +struct LockModesHandler; + +impl LockModesHandler { + #[inline] + fn is_modifier_enabled(key_event: &KeyEvent, modifier: ControlKey) -> bool { + key_event.modifiers.contains(&modifier.into()) + } + + #[inline] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + fn new_handler(key_event: &KeyEvent, _is_numpad_key: bool) -> Self { + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + Self::new(key_event, _is_numpad_key) + } + #[cfg(target_os = "macos")] + { + Self::new(key_event) + } + } + + #[cfg(target_os = "linux")] + fn sleep_to_ensure_locked(v: bool, k: enigo::Key, en: &mut Enigo) { + if wayland_use_uinput() { + // Sleep at most 500ms to ensure the lock state is applied. + for _ in 0..50 { + std::thread::sleep(std::time::Duration::from_millis(10)); + if en.get_key_state(k) == v { + break; + } + } + } else if wayland_use_rdp_input() { + // We can't call `en.get_key_state(k)` because there's no api for this. + std::thread::sleep(std::time::Duration::from_millis(50)); + } + } + + #[cfg(any(target_os = "windows", target_os = "linux"))] + fn new(key_event: &KeyEvent, is_numpad_key: bool) -> Self { + let mut en = ENIGO.lock().unwrap(); + let event_caps_enabled = Self::is_modifier_enabled(key_event, ControlKey::CapsLock); + let local_caps_enabled = en.get_key_state(enigo::Key::CapsLock); + let caps_lock_changed = event_caps_enabled != local_caps_enabled; + if caps_lock_changed { + en.key_click(enigo::Key::CapsLock); + #[cfg(target_os = "linux")] + Self::sleep_to_ensure_locked(event_caps_enabled, enigo::Key::CapsLock, &mut en); + } + + let mut num_lock_changed = false; + #[allow(unused)] + let mut event_num_enabled = false; + if is_numpad_key { + let local_num_enabled = en.get_key_state(enigo::Key::NumLock); + event_num_enabled = Self::is_modifier_enabled(key_event, ControlKey::NumLock); + num_lock_changed = event_num_enabled != local_num_enabled; + } else if is_legacy_mode(key_event) { + #[cfg(target_os = "windows")] + { + num_lock_changed = + should_disable_numlock(key_event) && en.get_key_state(enigo::Key::NumLock); + } + } + if num_lock_changed { + en.key_click(enigo::Key::NumLock); + #[cfg(target_os = "linux")] + Self::sleep_to_ensure_locked(event_num_enabled, enigo::Key::NumLock, &mut en); + } + + Self { + caps_lock_changed, + num_lock_changed, + } + } + + #[cfg(target_os = "macos")] + fn new(key_event: &KeyEvent) -> Self { + let event_caps_enabled = Self::is_modifier_enabled(key_event, ControlKey::CapsLock); + // Do not use the following code to detect `local_caps_enabled`. + // Because the state of get_key_state will not affect simulation of `VIRTUAL_INPUT_STATE` in this file. + // + // let local_caps_enabled = VirtualInput::get_key_state( + // CGEventSourceStateID::CombinedSessionState, + // rdev::kVK_CapsLock, + // ); + let local_caps_enabled = unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + VIRTUAL_INPUT_STATE + .as_ref() + .map_or(false, |input| input.capslock_down) + }; + if event_caps_enabled && !local_caps_enabled { + press_capslock(); + } else if !event_caps_enabled && local_caps_enabled { + release_capslock(); + } + + Self {} + } +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +impl Drop for LockModesHandler { + fn drop(&mut self) { + // Do not change led state if is Wayland uinput. + // Because there must be a delay to ensure the lock state is applied on Wayland uinput, + // which may affect the user experience. + #[cfg(target_os = "linux")] + if wayland_use_uinput() { + return; + } + + let mut en = ENIGO.lock().unwrap(); + if self.caps_lock_changed { + en.key_click(enigo::Key::CapsLock); + } + if self.num_lock_changed { + en.key_click(enigo::Key::NumLock); + } + } +} + +#[inline] +#[cfg(target_os = "windows")] +fn should_disable_numlock(evt: &KeyEvent) -> bool { + // disable numlock if press home etc when numlock is on, + // because we will get numpad value (7,8,9 etc) if not + match (&evt.union, evt.mode.enum_value_or(KeyboardMode::Legacy)) { + (Some(key_event::Union::ControlKey(ck)), KeyboardMode::Legacy) => { + return NUMPAD_KEY_MAP.contains_key(&ck.value()); + } + _ => {} + } + false +} + +pub const NAME_CURSOR: &'static str = "mouse_cursor"; +pub const NAME_POS: &'static str = "mouse_pos"; +pub const NAME_WINDOW_FOCUS: &'static str = "window_focus"; +#[derive(Clone)] +pub struct MouseCursorService { + pub sp: ServiceTmpl, +} + +impl Deref for MouseCursorService { + type Target = ServiceTmpl; + + fn deref(&self) -> &Self::Target { + &self.sp + } +} + +impl DerefMut for MouseCursorService { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sp + } +} + +impl MouseCursorService { + pub fn new(name: String, need_snapshot: bool) -> Self { + Self { + sp: ServiceTmpl::::new(name, need_snapshot), + } + } +} + +pub fn new_cursor() -> ServiceTmpl { + let svc = MouseCursorService::new(NAME_CURSOR.to_owned(), true); + ServiceTmpl::::repeat::(&svc.clone(), 33, run_cursor); + svc.sp +} + +pub fn new_pos() -> GenericService { + let svc = EmptyExtraFieldService::new(NAME_POS.to_owned(), false); + GenericService::repeat::(&svc.clone(), 33, run_pos); + svc.sp +} + +pub fn new_window_focus() -> GenericService { + let svc = EmptyExtraFieldService::new(NAME_WINDOW_FOCUS.to_owned(), false); + GenericService::repeat::(&svc.clone(), 33, run_window_focus); + svc.sp +} + +#[inline] +fn update_last_cursor_pos(x: i32, y: i32) { + let mut lock = LATEST_SYS_CURSOR_POS.lock().unwrap(); + if lock.1 .0 != x || lock.1 .1 != y { + (lock.0, lock.1) = (Some(Instant::now()), (x, y)) + } +} + +fn run_pos(sp: EmptyExtraFieldService, state: &mut StatePos) -> ResultType<()> { + let (_, (x, y)) = *LATEST_SYS_CURSOR_POS.lock().unwrap(); + if x == INVALID_CURSOR_POS || y == INVALID_CURSOR_POS { + return Ok(()); + } + + if state.is_moved(x, y) { + let mut msg_out = Message::new(); + msg_out.set_cursor_position(CursorPosition { + x, + y, + ..Default::default() + }); + let exclude = { + let now = get_time(); + let lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap(); + if now - lock.time < 300 { + lock.conn + } else { + 0 + } + }; + sp.send_without(msg_out, exclude); + } + state.cursor_pos = (x, y); + + sp.snapshot(|sps| { + let mut msg_out = Message::new(); + msg_out.set_cursor_position(CursorPosition { + x: state.cursor_pos.0, + y: state.cursor_pos.1, + ..Default::default() + }); + sps.send(msg_out); + Ok(()) + })?; + Ok(()) +} + +fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> { + if let Some(hcursor) = crate::get_cursor()? { + if hcursor != state.hcursor { + let msg; + if let Some(cached) = state.cached_cursor_data.get(&hcursor) { + super::log::trace!("Cursor data cached, hcursor: {}", hcursor); + msg = cached.clone(); + } else { + let mut data = crate::get_cursor_data(hcursor)?; + data.colors = hbb_common::compress::compress(&data.colors[..]).into(); + let mut tmp = Message::new(); + tmp.set_cursor_data(data); + msg = Arc::new(tmp); + state.cached_cursor_data.insert(hcursor, msg.clone()); + super::log::trace!("Cursor data updated, hcursor: {}", hcursor); + } + state.hcursor = hcursor; + sp.send_shared(msg.clone()); + state.cursor_data = msg; + } + } + sp.snapshot(|sps| { + sps.send_shared(state.cursor_data.clone()); + Ok(()) + })?; + Ok(()) +} + +fn run_window_focus(sp: EmptyExtraFieldService, state: &mut StateWindowFocus) -> ResultType<()> { + let displays = super::display_service::get_sync_displays(); + if displays.len() <= 1 { + return Ok(()); + } + let disp_idx = crate::get_focused_display(displays); + if let Some(disp_idx) = disp_idx.map(|id| id as i32) { + if state.is_changed(disp_idx) { + let mut misc = Misc::new(); + misc.set_follow_current_display(disp_idx as i32); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + sp.send(msg_out); + } + state.display_idx = disp_idx; + } + Ok(()) +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +enum KeysDown { + RdevKey(RawKey), + EnigoKey(u64), +} + +lazy_static::lazy_static! { + static ref ENIGO: Arc> = { + Arc::new(Mutex::new(Enigo::new())) + }; + static ref KEYS_DOWN: Arc>> = Default::default(); + static ref LATEST_PEER_INPUT_CURSOR: Arc> = Default::default(); + static ref LATEST_SYS_CURSOR_POS: Arc, (i32, i32))>> = Arc::new(Mutex::new((None, (INVALID_CURSOR_POS, INVALID_CURSOR_POS)))); + // Track connections that are currently using relative mouse movement. + // Used to disable whiteboard/cursor display for all events while in relative mode. + static ref RELATIVE_MOUSE_CONNS: Arc>> = Default::default(); +} + +#[inline] +fn set_relative_mouse_active(conn: i32, active: bool) { + let mut lock = RELATIVE_MOUSE_CONNS.lock().unwrap(); + if active { + lock.insert(conn); + } else { + lock.remove(&conn); + } +} + +#[inline] +fn is_relative_mouse_active(conn: i32) -> bool { + RELATIVE_MOUSE_CONNS.lock().unwrap().contains(&conn) +} + +/// Clears the relative mouse mode state for a connection. +/// +/// This must be called when an authenticated connection is dropped (during connection teardown) +/// to avoid leaking the connection id in `RELATIVE_MOUSE_CONNS` (a `Mutex>`). +/// Callers are responsible for invoking this on disconnect. +#[inline] +pub(crate) fn clear_relative_mouse_active(conn: i32) { + set_relative_mouse_active(conn, false); +} + +static EXITING: AtomicBool = AtomicBool::new(false); + +const MOUSE_MOVE_PROTECTION_TIMEOUT: Duration = Duration::from_millis(1_000); +// Actual diff of (x,y) is (1,1) here. But 5 may be tolerant. +const MOUSE_ACTIVE_DISTANCE: i32 = 5; + +static RECORD_CURSOR_POS_RUNNING: AtomicBool = AtomicBool::new(false); + +// https://github.com/rustdesk/rustdesk/issues/9729 +// We need to do some special handling for macOS when using the legacy mode. +#[cfg(target_os = "macos")] +static LAST_KEY_LEGACY_MODE: AtomicBool = AtomicBool::new(true); +// We use enigo to +// 1. Simulate mouse events +// 2. Simulate the legacy mode key events +// 3. Simulate the functioin key events, like LockScreen +#[inline] +#[cfg(target_os = "macos")] +fn enigo_ignore_flags() -> bool { + !LAST_KEY_LEGACY_MODE.load(Ordering::SeqCst) +} +#[inline] +#[cfg(target_os = "macos")] +fn set_last_legacy_mode(v: bool) { + LAST_KEY_LEGACY_MODE.store(v, Ordering::SeqCst); + ENIGO.lock().unwrap().set_ignore_flags(!v); +} + +pub fn try_start_record_cursor_pos() -> Option> { + if RECORD_CURSOR_POS_RUNNING.load(Ordering::SeqCst) { + return None; + } + + RECORD_CURSOR_POS_RUNNING.store(true, Ordering::SeqCst); + let handle = thread::spawn(|| { + let interval = time::Duration::from_millis(33); + loop { + if !RECORD_CURSOR_POS_RUNNING.load(Ordering::SeqCst) { + break; + } + + let now = time::Instant::now(); + if let Some((x, y)) = crate::get_cursor_pos() { + update_last_cursor_pos(x, y); + } + let elapsed = now.elapsed(); + if elapsed < interval { + thread::sleep(interval - elapsed); + } + } + update_last_cursor_pos(INVALID_CURSOR_POS, INVALID_CURSOR_POS); + }); + Some(handle) +} + +pub fn try_stop_record_cursor_pos() { + let remote_count = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.conn_type == AuthConnType::Remote) + .count(); + if remote_count > 0 { + return; + } + RECORD_CURSOR_POS_RUNNING.store(false, Ordering::SeqCst); +} + +// mac key input must be run in main thread, otherwise crash on >= osx 10.15 +#[cfg(target_os = "macos")] +lazy_static::lazy_static! { + static ref QUEUE: Queue = Queue::main(); +} + +#[cfg(target_os = "macos")] +struct VirtualInputState { + virtual_input: VirtualInput, + capslock_down: bool, +} + +#[cfg(target_os = "macos")] +impl VirtualInputState { + fn new() -> Option { + VirtualInput::new( + CGEventSourceStateID::CombinedSessionState, + // Note: `CGEventTapLocation::Session` will be affected by the mouse events. + // When we're simulating key events, then move the physical mouse, the key events will be affected. + // It looks like https://github.com/rustdesk/rustdesk/issues/9729#issuecomment-2432306822 + // 1. Press "Command" key in RustDesk + // 2. Move the physical mouse + // 3. Press "V" key in RustDesk + // Then the controlled side just prints "v" instead of pasting. + // + // Changing `CGEventTapLocation::Session` to `CGEventTapLocation::HID` fixes it. + // But we do not consider this as a bug, because it's not a common case, + // we consider only RustDesk operates the controlled side. + // + // https://developer.apple.com/documentation/coregraphics/cgeventtaplocation/ + CGEventTapLocation::Session, + ) + .map(|virtual_input| Self { + virtual_input, + capslock_down: false, + }) + .ok() + } + + #[inline] + fn simulate(&self, event_type: &EventType) -> ResultType<()> { + Ok(self.virtual_input.simulate(&event_type)?) + } +} + +#[cfg(target_os = "macos")] +static mut VIRTUAL_INPUT_MTX: Mutex<()> = Mutex::new(()); +#[cfg(target_os = "macos")] +static mut VIRTUAL_INPUT_STATE: Option = None; + +// First call set_uinput() will create keyboard and mouse clients. +// The clients are ipc connections that must live shorter than tokio runtime. +// Thus this function must not be called in a temporary runtime. +#[cfg(target_os = "linux")] +pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { + // Keyboard and mouse both open /dev/uinput + // TODO: Make sure there's no race + set_uinput_resolution(minx, maxx, miny, maxy).await?; + + let keyboard = super::uinput::client::UInputKeyboard::new().await?; + log::info!("UInput keyboard created"); + let mouse = super::uinput::client::UInputMouse::new().await?; + log::info!("UInput mouse created"); + + ENIGO + .lock() + .unwrap() + .set_custom_keyboard(Box::new(keyboard)); + ENIGO.lock().unwrap().set_custom_mouse(Box::new(mouse)); + Ok(()) +} + +#[cfg(target_os = "linux")] +pub async fn setup_rdp_input() -> ResultType<(), Box> { + let mut en = ENIGO.lock()?; + let rdp_info_lock = RDP_SESSION_INFO.lock()?; + let rdp_info = rdp_info_lock.as_ref().ok_or("RDP session is None")?; + + let keyboard = RdpInputKeyboard::new(rdp_info.conn.clone(), rdp_info.session.clone())?; + en.set_custom_keyboard(Box::new(keyboard)); + log::info!("RdpInput keyboard created"); + + if let Some(stream) = rdp_info.streams.clone().into_iter().next() { + let resolution = rdp_info + .resolution + .lock() + .unwrap() + .unwrap_or(stream.get_size()); + let mouse = RdpInputMouse::new( + rdp_info.conn.clone(), + rdp_info.session.clone(), + stream, + resolution, + )?; + en.set_custom_mouse(Box::new(mouse)); + log::info!("RdpInput mouse created"); + } + + Ok(()) +} + +#[cfg(target_os = "linux")] +pub async fn update_mouse_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { + set_uinput_resolution(minx, maxx, miny, maxy).await?; + + std::thread::spawn(|| { + if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() { + if let Some(mouse) = mouse + .as_mut_any() + .downcast_mut::() + { + allow_err!(mouse.send_refresh()); + } else { + log::error!("failed downcast uinput mouse"); + } + } + }); + + Ok(()) +} + +#[cfg(target_os = "linux")] +async fn set_uinput_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { + super::uinput::client::set_resolution(minx, maxx, miny, maxy).await +} + +pub fn is_left_up(evt: &MouseEvent) -> bool { + let buttons = evt.mask >> 3; + let evt_type = evt.mask & MOUSE_TYPE_MASK; + buttons == MOUSE_BUTTON_LEFT && evt_type == MOUSE_TYPE_UP +} + +#[cfg(windows)] +pub fn mouse_move_relative(x: i32, y: i32) { + crate::platform::windows::try_change_desktop(); + let mut en = ENIGO.lock().unwrap(); + en.mouse_move_relative(x, y); +} + +#[cfg(windows)] +fn modifier_sleep() { + // sleep for a while, this is only for keying in rdp in peer so far + std::thread::sleep(std::time::Duration::from_nanos(1)); +} + +#[inline] +#[cfg(not(target_os = "macos"))] +fn is_pressed(key: &Key, en: &mut Enigo) -> bool { + get_modifier_state(key.clone(), en) +} + +// Sleep for 8ms is enough in my tests, but we sleep 12ms to be safe. +// sleep 12ms In my test, the characters are already output in real time. +#[inline] +#[cfg(target_os = "macos")] +fn key_sleep() { + // https://www.reddit.com/r/rustdesk/comments/1kn1w5x/typing_lags_when_connecting_to_macos_clients/ + // + // There's a strange bug when running by `launchctl load -w /Library/LaunchAgents/abc.plist` + // `std::thread::sleep(Duration::from_millis(20));` may sleep 90ms or more. + // Though `/Applications/RustDesk.app/Contents/MacOS/rustdesk --server` in terminal is ok. + let now = Instant::now(); + while now.elapsed() < Duration::from_millis(12) { + std::thread::sleep(Duration::from_millis(1)); + } +} + +#[inline] +fn get_modifier_state(key: Key, en: &mut Enigo) -> bool { + // https://github.com/rustdesk/rustdesk/issues/332 + // on Linux, if RightAlt is down, RightAlt status is false, Alt status is true + // but on Windows, both are true + let x = en.get_key_state(key.clone()); + match key { + Key::Shift => x || en.get_key_state(Key::RightShift), + Key::Control => x || en.get_key_state(Key::RightControl), + Key::Alt => x || en.get_key_state(Key::RightAlt), + Key::Meta => x || en.get_key_state(Key::RWin), + Key::RightShift => x || en.get_key_state(Key::Shift), + Key::RightControl => x || en.get_key_state(Key::Control), + Key::RightAlt => x || en.get_key_state(Key::Alt), + Key::RWin => x || en.get_key_state(Key::Meta), + _ => x, + } +} + +#[allow(unreachable_code)] +pub fn handle_mouse( + evt: &MouseEvent, + conn: i32, + username: String, + argb: u32, + simulate: bool, + show_cursor: bool, +) { + #[cfg(target_os = "macos")] + { + // having GUI (--server has tray, it is GUI too), run main GUI thread, otherwise crash + let evt = evt.clone(); + QUEUE.exec_async(move || handle_mouse_(&evt, conn, username, argb, simulate, show_cursor)); + return; + } + #[cfg(windows)] + crate::portable_service::client::handle_mouse(evt, conn, username, argb, simulate, show_cursor); + #[cfg(not(windows))] + handle_mouse_(evt, conn, username, argb, simulate, show_cursor); +} + +// to-do: merge handle_mouse and handle_pointer +#[allow(unreachable_code)] +pub fn handle_pointer(evt: &PointerDeviceEvent, conn: i32) { + #[cfg(target_os = "macos")] + { + // having GUI, run main GUI thread, otherwise crash + let evt = evt.clone(); + QUEUE.exec_async(move || handle_pointer_(&evt, conn)); + return; + } + #[cfg(windows)] + crate::portable_service::client::handle_pointer(evt, conn); + #[cfg(not(windows))] + handle_pointer_(evt, conn); +} + +pub fn fix_key_down_timeout_loop() { + std::thread::spawn(move || loop { + std::thread::sleep(std::time::Duration::from_millis(10_000)); + fix_key_down_timeout(false); + }); + if let Err(err) = ctrlc::set_handler(move || { + fix_key_down_timeout_at_exit(); + std::process::exit(0); // will call atexit on posix, but not on Windows + }) { + log::error!("Failed to set Ctrl-C handler: {}", err); + } +} + +pub fn fix_key_down_timeout_at_exit() { + if EXITING.load(Ordering::SeqCst) { + return; + } + EXITING.store(true, Ordering::SeqCst); + fix_key_down_timeout(true); + log::info!("fix_key_down_timeout_at_exit"); +} + +#[inline] +#[cfg(target_os = "linux")] +pub fn clear_remapped_keycode() { + ENIGO.lock().unwrap().tfc_clear_remapped(); +} + +#[inline] +fn record_key_is_control_key(record_key: u64) -> bool { + record_key < KEY_CHAR_START +} + +#[inline] +fn record_key_is_chr(record_key: u64) -> bool { + record_key >= KEY_CHAR_START +} + +#[inline] +fn record_key_to_key(record_key: u64) -> Option { + if record_key_is_control_key(record_key) { + control_key_value_to_key(record_key as _) + } else if record_key_is_chr(record_key) { + let chr: u32 = (record_key - KEY_CHAR_START) as _; + Some(char_value_to_key(chr)) + } else { + None + } +} + +pub fn release_device_modifiers() { + let mut en = ENIGO.lock().unwrap(); + for modifier in [ + Key::Shift, + Key::Control, + Key::Alt, + Key::Meta, + Key::RightShift, + Key::RightControl, + Key::RightAlt, + Key::RWin, + ] { + if get_modifier_state(modifier, &mut en) { + en.key_up(modifier); + } + } +} + +#[inline] +fn release_record_key(record_key: KeysDown) { + let func = move || match record_key { + KeysDown::RdevKey(raw_key) => { + simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); + } + KeysDown::EnigoKey(key) => { + if let Some(key) = record_key_to_key(key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); + } + } + }; + + #[cfg(target_os = "macos")] + QUEUE.exec_async(func); + #[cfg(not(target_os = "macos"))] + func(); +} + +fn fix_key_down_timeout(force: bool) { + let key_down = KEYS_DOWN.lock().unwrap(); + if key_down.is_empty() { + return; + } + let cloned = (*key_down).clone(); + drop(key_down); + + for (record_key, time) in cloned.into_iter() { + if force || time.elapsed().as_millis() >= 360_000 { + record_pressed_key(record_key, false); + release_record_key(record_key); + } + } +} + +// e.g. current state of ctrl is down, but ctrl not in modifier, we should change ctrl to up, to make modifier state sync between remote and local +#[inline] +fn fix_modifier( + modifiers: &[EnumOrUnknown], + key0: ControlKey, + key1: Key, + en: &mut Enigo, +) { + if get_modifier_state(key1, en) && !modifiers.contains(&EnumOrUnknown::new(key0)) { + #[cfg(windows)] + if key0 == ControlKey::Control && get_modifier_state(Key::Alt, en) { + // AltGr case + return; + } + en.key_up(key1); + log::debug!("Fixed {:?}", key1); + } +} + +fn fix_modifiers(modifiers: &[EnumOrUnknown], en: &mut Enigo, ck: i32) { + if ck != ControlKey::Shift.value() { + fix_modifier(modifiers, ControlKey::Shift, Key::Shift, en); + } + if ck != ControlKey::RShift.value() { + fix_modifier(modifiers, ControlKey::Shift, Key::RightShift, en); + } + if ck != ControlKey::Alt.value() { + fix_modifier(modifiers, ControlKey::Alt, Key::Alt, en); + } + if ck != ControlKey::RAlt.value() { + fix_modifier(modifiers, ControlKey::Alt, Key::RightAlt, en); + } + if ck != ControlKey::Control.value() { + fix_modifier(modifiers, ControlKey::Control, Key::Control, en); + } + if ck != ControlKey::RControl.value() { + fix_modifier(modifiers, ControlKey::Control, Key::RightControl, en); + } + if ck != ControlKey::Meta.value() { + fix_modifier(modifiers, ControlKey::Meta, Key::Meta, en); + } + if ck != ControlKey::RWin.value() { + fix_modifier(modifiers, ControlKey::Meta, Key::RWin, en); + } +} + +// Update time to avoid send cursor position event to the peer. +// See `run_pos` --> `set_cursor_position` --> `exclude` +#[inline] +pub fn update_latest_input_cursor_time(conn: i32) { + let mut lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap(); + lock.conn = conn; + lock.time = get_time(); +} + +#[inline] +fn get_last_input_cursor_pos() -> (i32, i32) { + let lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap(); + (lock.x, lock.y) +} + +// check if mouse is moved by the controlled side user to make controlled side has higher mouse priority than remote. +fn active_mouse_(_conn: i32) -> bool { + true + /* this method is buggy (not working on macOS, making fast moving mouse event discarded here) and added latency (this is blocking way, must do in async way), so we disable it for now + // out of time protection + if LATEST_SYS_CURSOR_POS + .lock() + .unwrap() + .0 + .map(|t| t.elapsed() > MOUSE_MOVE_PROTECTION_TIMEOUT) + .unwrap_or(true) + { + return true; + } + + // last conn input may be protected + if LATEST_PEER_INPUT_CURSOR.lock().unwrap().conn != conn { + return false; + } + + let in_active_dist = |a: i32, b: i32| -> bool { (a - b).abs() < MOUSE_ACTIVE_DISTANCE }; + + // Check if input is in valid range + match crate::get_cursor_pos() { + Some((x, y)) => { + let (last_in_x, last_in_y) = get_last_input_cursor_pos(); + let mut can_active = in_active_dist(last_in_x, x) && in_active_dist(last_in_y, y); + // The cursor may not have been moved to last input position if system is busy now. + // While this is not a common case, we check it again after some time later. + if !can_active { + // 100 micros may be enough for system to move cursor. + // Mouse inputs on macOS are asynchronous. 1. Put in a queue to process in main thread. 2. Send event async. + // More reties are needed on macOS. + #[cfg(not(target_os = "macos"))] + let retries = 10; + #[cfg(target_os = "macos")] + let retries = 100; + #[cfg(not(target_os = "macos"))] + let sleep_interval: u64 = 10; + #[cfg(target_os = "macos")] + let sleep_interval: u64 = 30; + for _retry in 0..retries { + std::thread::sleep(std::time::Duration::from_micros(sleep_interval)); + // Sleep here can also somehow suppress delay accumulation. + if let Some((x2, y2)) = crate::get_cursor_pos() { + let (last_in_x, last_in_y) = get_last_input_cursor_pos(); + can_active = in_active_dist(last_in_x, x2) && in_active_dist(last_in_y, y2); + if can_active { + break; + } + } + } + } + if !can_active { + let mut lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap(); + lock.x = INVALID_CURSOR_POS / 2; + lock.y = INVALID_CURSOR_POS / 2; + } + can_active + } + None => true, + } + */ +} + +pub fn handle_pointer_(evt: &PointerDeviceEvent, conn: i32) { + if !active_mouse_(conn) { + return; + } + + if EXITING.load(Ordering::SeqCst) { + return; + } + + match &evt.union { + Some(TouchEvent(evt)) => match &evt.union { + Some(ScaleUpdate(_scale_evt)) => { + #[cfg(target_os = "windows")] + handle_scale(_scale_evt.scale); + } + _ => {} + }, + _ => {} + } +} + +pub fn handle_mouse_( + evt: &MouseEvent, + conn: i32, + _username: String, + _argb: u32, + simulate: bool, + _show_cursor: bool, +) { + if simulate { + handle_mouse_simulation_(evt, conn); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let evt_type = evt.mask & MOUSE_TYPE_MASK; + // Relative (delta) mouse events do not include absolute coordinates, so + // whiteboard/cursor rendering must be disabled during relative mode to prevent + // incorrect cursor/whiteboard updates. We check both is_relative_mouse_active(conn) + // (connection already in relative mode from prior events) and evt_type (current + // event is relative) to guard against the first relative event before the flag is set. + if _show_cursor && !is_relative_mouse_active(conn) && evt_type != MOUSE_TYPE_MOVE_RELATIVE { + handle_mouse_show_cursor_(evt, conn, _username, _argb); + } + } +} + +pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) { + if !active_mouse_(conn) { + return; + } + + if EXITING.load(Ordering::SeqCst) { + return; + } + + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + let buttons = evt.mask >> 3; + let evt_type = evt.mask & MOUSE_TYPE_MASK; + let mut en = ENIGO.lock().unwrap(); + #[cfg(target_os = "macos")] + en.set_ignore_flags(enigo_ignore_flags()); + #[cfg(not(target_os = "macos"))] + let mut to_release = Vec::new(); + if evt_type == MOUSE_TYPE_DOWN { + fix_modifiers(&evt.modifiers[..], &mut en, 0); + #[cfg(target_os = "macos")] + en.reset_flag(); + for ref ck in evt.modifiers.iter() { + if let Some(key) = KEY_MAP.get(&ck.value()) { + #[cfg(target_os = "macos")] + en.add_flag(key); + #[cfg(not(target_os = "macos"))] + if key != &Key::CapsLock && key != &Key::NumLock { + if !get_modifier_state(key.clone(), &mut en) { + en.key_down(key.clone()).ok(); + #[cfg(windows)] + modifier_sleep(); + to_release.push(key); + } + } + } + } + } + match evt_type { + MOUSE_TYPE_MOVE => { + // Switching back to absolute movement implicitly disables relative mouse mode. + set_relative_mouse_active(conn, false); + en.mouse_move_to(evt.x, evt.y); + *LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input { + conn, + time: get_time(), + x: evt.x, + y: evt.y, + }; + } + // MOUSE_TYPE_MOVE_RELATIVE: Relative mouse movement for gaming/3D applications. + // Each client independently decides whether to use relative mode. + // Multiple clients can mix absolute and relative movements without conflict, + // as the server simply applies the delta to the current cursor position. + MOUSE_TYPE_MOVE_RELATIVE => { + set_relative_mouse_active(conn, true); + // Clamp delta to prevent extreme/malicious values from reaching OS APIs. + // This matches the Flutter client's kMaxRelativeMouseDelta constant. + const MAX_RELATIVE_MOUSE_DELTA: i32 = 10000; + let dx = evt + .x + .clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA); + let dy = evt + .y + .clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA); + en.mouse_move_relative(dx, dy); + // Get actual cursor position after relative movement for tracking + if let Some((x, y)) = crate::get_cursor_pos() { + *LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input { + conn, + time: get_time(), + x, + y, + }; + } + } + MOUSE_TYPE_DOWN => match buttons { + MOUSE_BUTTON_LEFT => { + allow_err!(en.mouse_down(MouseButton::Left)); + } + MOUSE_BUTTON_RIGHT => { + allow_err!(en.mouse_down(MouseButton::Right)); + } + MOUSE_BUTTON_WHEEL => { + allow_err!(en.mouse_down(MouseButton::Middle)); + } + MOUSE_BUTTON_BACK => { + allow_err!(en.mouse_down(MouseButton::Back)); + } + MOUSE_BUTTON_FORWARD => { + allow_err!(en.mouse_down(MouseButton::Forward)); + } + _ => {} + }, + MOUSE_TYPE_UP => match buttons { + MOUSE_BUTTON_LEFT => { + en.mouse_up(MouseButton::Left); + } + MOUSE_BUTTON_RIGHT => { + en.mouse_up(MouseButton::Right); + } + MOUSE_BUTTON_WHEEL => { + en.mouse_up(MouseButton::Middle); + } + MOUSE_BUTTON_BACK => { + en.mouse_up(MouseButton::Back); + } + MOUSE_BUTTON_FORWARD => { + en.mouse_up(MouseButton::Forward); + } + _ => {} + }, + MOUSE_TYPE_WHEEL | MOUSE_TYPE_TRACKPAD => { + #[allow(unused_mut)] + let mut x = -evt.x; + #[allow(unused_mut)] + let mut y = evt.y; + #[cfg(not(windows))] + { + y = -y; + } + + #[cfg(any(target_os = "macos", target_os = "windows"))] + let is_track_pad = evt_type == MOUSE_TYPE_TRACKPAD; + + #[cfg(target_os = "macos")] + { + // TODO: support track pad on win. + + // fix shift + scroll(down/up) + if !is_track_pad + && evt + .modifiers + .contains(&EnumOrUnknown::new(ControlKey::Shift)) + { + x = y; + y = 0; + } + + if x != 0 { + en.mouse_scroll_x(x, is_track_pad); + } + if y != 0 { + en.mouse_scroll_y(y, is_track_pad); + } + } + + #[cfg(windows)] + if !is_track_pad { + x *= WHEEL_DELTA as i32; + y *= WHEEL_DELTA as i32; + } + + #[cfg(not(target_os = "macos"))] + { + if y != 0 { + en.mouse_scroll_y(y); + } + if x != 0 { + en.mouse_scroll_x(x); + } + } + } + _ => {} + } + #[cfg(not(target_os = "macos"))] + for key in to_release { + en.key_up(key.clone()); + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn handle_mouse_show_cursor_(evt: &MouseEvent, conn: i32, username: String, argb: u32) { + let buttons = evt.mask >> 3; + let evt_type = evt.mask & MOUSE_TYPE_MASK; + match evt_type { + MOUSE_TYPE_MOVE => { + whiteboard::update_whiteboard( + whiteboard::get_key_cursor(conn), + whiteboard::CustomEvent::Cursor(whiteboard::Cursor { + x: evt.x as _, + y: evt.y as _, + argb, + btns: 0, + text: username, + }), + ); + } + MOUSE_TYPE_UP => { + if buttons == MOUSE_BUTTON_LEFT { + // Some clients intentionally send button events without coordinates. + // Fall back to the last known cursor position to avoid jumping to (0, 0). + // TODO(protocol): (0, 0) is a valid screen coordinate. Consider using a dedicated + // sentinel value (e.g. INVALID_CURSOR_POS) or a protocol-level flag to distinguish + // "coordinates not provided" from "coordinates are (0, 0)". Impact is minor since + // this only affects whiteboard rendering and clicking exactly at (0, 0) is rare. + let (x, y) = if evt.x == 0 && evt.y == 0 { + get_last_input_cursor_pos() + } else { + (evt.x, evt.y) + }; + whiteboard::update_whiteboard( + whiteboard::get_key_cursor(conn), + whiteboard::CustomEvent::Cursor(whiteboard::Cursor { + x: x as _, + y: y as _, + argb, + btns: buttons, + text: username, + }), + ); + } + } + _ => {} + } +} + +#[cfg(target_os = "windows")] +fn handle_scale(scale: i32) { + let mut en = ENIGO.lock().unwrap(); + if scale == 0 { + en.key_up(Key::Control); + } else { + if en.key_down(Key::Control).is_ok() { + en.mouse_scroll_y(scale); + } + } +} + +pub fn is_enter(evt: &KeyEvent) -> bool { + if let Some(key_event::Union::ControlKey(ck)) = evt.union { + if ck.value() == ControlKey::Return.value() || ck.value() == ControlKey::NumpadEnter.value() + { + return true; + } + } + return false; +} + +pub async fn lock_screen() { + cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + // xdg_screensaver lock not work on Linux from our service somehow + // loginctl lock-session also not work, they both work run rustdesk from cmd + std::thread::spawn(|| { + let mut key_event = KeyEvent::new(); + + key_event.set_chr('l' as _); + key_event.modifiers.push(ControlKey::Meta.into()); + key_event.mode = KeyboardMode::Legacy.into(); + + key_event.down = true; + handle_key(&key_event); + + key_event.down = false; + handle_key(&key_event); + }); + } else if #[cfg(target_os = "macos")] { + // CGSession -suspend not real lock screen, it is user switch + std::thread::spawn(|| { + let mut key_event = KeyEvent::new(); + + key_event.set_chr('q' as _); + key_event.modifiers.push(ControlKey::Meta.into()); + key_event.modifiers.push(ControlKey::Control.into()); + key_event.mode = KeyboardMode::Legacy.into(); + + key_event.down = true; + handle_key(&key_event); + key_event.down = false; + handle_key(&key_event); + }); + } else { + crate::platform::lock_screen(); + } + } +} + +#[inline] +#[cfg(target_os = "linux")] +pub fn handle_key(evt: &KeyEvent) { + handle_key_(evt); +} + +#[inline] +#[cfg(target_os = "windows")] +pub fn handle_key(evt: &KeyEvent) { + crate::portable_service::client::handle_key(evt); +} + +#[inline] +#[cfg(target_os = "macos")] +pub fn handle_key(evt: &KeyEvent) { + // having GUI, run main GUI thread, otherwise crash + let evt = evt.clone(); + QUEUE.exec_async(move || handle_key_(&evt)); + // Key sleep is required for macOS. + // If we don't sleep, the key press/release events may not take effect. + // + // For example, the controlled side osx `12.7.6` or `15.1.1` + // If we input characters quickly and continuously, and press or release "Shift" for a short period of time, + // it is possible that after releasing "Shift", the controlled side will still print uppercase characters. + // Though it is not very easy to reproduce. + key_sleep(); +} + +#[cfg(target_os = "macos")] +#[inline] +fn reset_input() { + unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + VIRTUAL_INPUT_STATE = VirtualInputState::new(); + } +} + +#[cfg(target_os = "macos")] +pub fn reset_input_ondisconn() { + QUEUE.exec_async(reset_input); +} + +fn sim_rdev_rawkey_position(code: KeyCode, keydown: bool) { + #[cfg(target_os = "windows")] + let rawkey = RawKey::ScanCode(code); + #[cfg(target_os = "linux")] + let rawkey = RawKey::LinuxXorgKeycode(code); + // // to-do: test android + // #[cfg(target_os = "android")] + // let rawkey = RawKey::LinuxConsoleKeycode(code); + #[cfg(target_os = "macos")] + let rawkey = RawKey::MacVirtualKeycode(code); + + // map mode(1): Send keycode according to the peer platform. + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); + + let event_type = if keydown { + EventType::KeyPress(RdevKey::RawKey(rawkey)) + } else { + EventType::KeyRelease(RdevKey::RawKey(rawkey)) + }; + simulate_(&event_type); +} + +#[cfg(target_os = "windows")] +fn sim_rdev_rawkey_virtual(code: u32, keydown: bool) { + let rawkey = RawKey::WinVirtualKeycode(code); + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); + let event_type = if keydown { + EventType::KeyPress(RdevKey::RawKey(rawkey)) + } else { + EventType::KeyRelease(RdevKey::RawKey(rawkey)) + }; + simulate_(&event_type); +} + +#[inline] +#[cfg(target_os = "macos")] +fn simulate_(event_type: &EventType) { + unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + if let Some(input) = VIRTUAL_INPUT_STATE.as_ref() { + let _ = input.simulate(&event_type); + } + } +} + +#[inline] +#[cfg(target_os = "macos")] +fn press_capslock() { + let caps_key = RdevKey::RawKey(rdev::RawKey::MacVirtualKeycode(rdev::kVK_CapsLock)); + unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + if let Some(input) = VIRTUAL_INPUT_STATE.as_mut() { + if input.simulate(&EventType::KeyPress(caps_key)).is_ok() { + input.capslock_down = true; + key_sleep(); + } + } + } +} + +#[cfg(target_os = "macos")] +#[inline] +fn release_capslock() { + let caps_key = RdevKey::RawKey(rdev::RawKey::MacVirtualKeycode(rdev::kVK_CapsLock)); + unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + if let Some(input) = VIRTUAL_INPUT_STATE.as_mut() { + if input.simulate(&EventType::KeyRelease(caps_key)).is_ok() { + input.capslock_down = false; + key_sleep(); + } + } + } +} + +#[cfg(not(target_os = "macos"))] +#[inline] +fn simulate_(event_type: &EventType) { + match rdev::simulate(&event_type) { + Ok(()) => (), + Err(_simulate_error) => { + log::error!("Could not send {:?}", &event_type); + } + } +} + +#[inline] +fn control_key_value_to_key(value: i32) -> Option { + KEY_MAP.get(&value).and_then(|k| Some(*k)) +} + +#[inline] +fn char_value_to_key(value: u32) -> Key { + Key::Layout(std::char::from_u32(value).unwrap_or('\0')) +} + +fn map_keyboard_mode(evt: &KeyEvent) { + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + + // Wayland + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() { + wayland_send_raw_key(evt.chr() as u16, evt.down); + return; + } + + sim_rdev_rawkey_position(evt.chr() as _, evt.down); +} + +/// Send raw keycode on Wayland via the active backend (uinput or RemoteDesktop portal). +/// The keycode is expected to be a Linux keycode (evdev code + 8 for X11 compatibility). +#[cfg(target_os = "linux")] +#[inline] +fn wayland_send_raw_key(code: u16, down: bool) { + let mut en = ENIGO.lock().unwrap(); + if down { + en.key_down(enigo::Key::Raw(code)).ok(); + } else { + en.key_up(enigo::Key::Raw(code)); + } +} + +#[cfg(target_os = "macos")] +fn add_flags_to_enigo(en: &mut Enigo, key_event: &KeyEvent) { + // When long-pressed the command key, then press and release + // the Tab key, there should be CGEventFlagCommand in the flag. + en.reset_flag(); + for ck in key_event.modifiers.iter() { + if let Some(key) = KEY_MAP.get(&ck.value()) { + en.add_flag(key); + } + } +} + +fn get_control_key_value(key_event: &KeyEvent) -> i32 { + if let Some(key_event::Union::ControlKey(ck)) = key_event.union { + ck.value() + } else { + -1 + } +} + +#[inline] +fn has_hotkey_modifiers(key_event: &KeyEvent) -> bool { + key_event.modifiers.iter().any(|ck| { + let v = ck.value(); + v == ControlKey::Control.value() + || v == ControlKey::RControl.value() + || v == ControlKey::Meta.value() + || v == ControlKey::RWin.value() + || { + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + v == ControlKey::Alt.value() || v == ControlKey::RAlt.value() + } + #[cfg(target_os = "macos")] + { + false + } + } + }) +} + +fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { + let ck_value = get_control_key_value(key_event); + fix_modifiers(&key_event.modifiers[..], en, ck_value); +} + +#[cfg(target_os = "linux")] +fn is_altgr_pressed() -> bool { + let altgr_rawkey = RawKey::LinuxXorgKeycode(ControlKey::RAlt.value() as _); + KEYS_DOWN + .lock() + .unwrap() + .get(&KeysDown::RdevKey(altgr_rawkey)) + .is_some() +} + +#[cfg(not(target_os = "macos"))] +fn press_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec) { + for ref ck in key_event.modifiers.iter() { + if let Some(key) = control_key_value_to_key(ck.value()) { + if !is_pressed(&key, en) { + #[cfg(target_os = "linux")] + if key == Key::Alt && is_altgr_pressed() { + continue; + } + en.key_down(key.clone()).ok(); + to_release.push(key.clone()); + #[cfg(windows)] + modifier_sleep(); + } + } + } +} + +fn sync_modifiers(en: &mut Enigo, key_event: &KeyEvent, _to_release: &mut Vec) { + #[cfg(target_os = "macos")] + add_flags_to_enigo(en, key_event); + + if key_event.down { + release_unpressed_modifiers(en, key_event); + #[cfg(not(target_os = "macos"))] + press_modifiers(en, key_event, _to_release); + } +} + +fn process_control_key(en: &mut Enigo, ck: &EnumOrUnknown, down: bool) { + if let Some(key) = control_key_value_to_key(ck.value()) { + if down { + en.key_down(key).ok(); + } else { + en.key_up(key); + } + } +} + +#[inline] +fn need_to_uppercase(en: &mut Enigo) -> bool { + get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en) +} + +fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) { + // On Wayland with uinput mode, use clipboard for character input + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() && wayland_use_uinput() { + // Skip clipboard for hotkeys (Ctrl/Alt/Meta pressed) + if !is_hotkey_modifier_pressed(en) { + if down { + if let Ok(c) = char::try_from(chr) { + input_char_via_clipboard_server(en, c); + } + } + return; + } + } + + #[cfg(any(target_os = "macos", target_os = "windows"))] + if !_hotkey { + if down { + if let Ok(chr) = char::try_from(chr) { + en.key_sequence(&chr.to_string()); + } + } + return; + } + + let key = char_value_to_key(chr); + + if down { + if en.key_down(key).is_ok() { + } else { + if let Ok(chr) = char::try_from(chr) { + let mut s = chr.to_string(); + if need_to_uppercase(en) { + s = s.to_uppercase(); + } + en.key_sequence(&s); + }; + } + } else { + en.key_up(key); + } +} + +fn process_unicode(en: &mut Enigo, chr: u32) { + // On Wayland with uinput mode, use clipboard for character input + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() && wayland_use_uinput() { + if let Ok(c) = char::try_from(chr) { + input_char_via_clipboard_server(en, c); + } + return; + } + + if let Ok(chr) = char::try_from(chr) { + en.key_sequence(&chr.to_string()); + } +} + +fn process_seq(en: &mut Enigo, sequence: &str) { + // On Wayland with uinput mode, use clipboard for text input + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() && wayland_use_uinput() { + input_text_via_clipboard_server(en, sequence); + return; + } + + en.key_sequence(&sequence); +} + +/// Delay in milliseconds to wait for clipboard to sync on Wayland. +/// This is an empirical value — Wayland provides no callback or event to confirm +/// clipboard content has been received by the compositor. Under heavy system load, +/// this delay may be insufficient, but there is no reliable alternative mechanism. +#[cfg(target_os = "linux")] +const CLIPBOARD_SYNC_DELAY_MS: u64 = 50; + +/// Internal: Set clipboard content without delay. +/// Returns true if clipboard was set successfully. +#[cfg(target_os = "linux")] +fn set_clipboard_content(text: &str) -> bool { + use arboard::{Clipboard, LinuxClipboardKind, SetExtLinux}; + + let mut clipboard = match Clipboard::new() { + Ok(cb) => cb, + Err(e) => { + log::error!("set_clipboard_content: failed to create clipboard: {:?}", e); + return false; + } + }; + + // Set both CLIPBOARD and PRIMARY selections + // Terminal uses PRIMARY for Shift+Insert, GUI apps use CLIPBOARD + if let Err(e) = clipboard + .set() + .clipboard(LinuxClipboardKind::Clipboard) + .text(text.to_owned()) + { + log::error!("set_clipboard_content: failed to set CLIPBOARD: {:?}", e); + return false; + } + if let Err(e) = clipboard + .set() + .clipboard(LinuxClipboardKind::Primary) + .text(text.to_owned()) + { + log::warn!("set_clipboard_content: failed to set PRIMARY: {:?}", e); + // Continue anyway, CLIPBOARD might work + } + + true +} + +/// Set clipboard content for paste operation (sync version for use in blocking contexts). +/// +/// Note: The original clipboard content is intentionally NOT restored after paste. +/// Restoring clipboard could cause race conditions where subsequent keystrokes +/// might accidentally paste the old clipboard content instead of the intended input. +/// This trade-off prioritizes input reliability over preserving clipboard state. +#[cfg(target_os = "linux")] +#[inline] +pub(super) fn set_clipboard_for_paste_sync(text: &str) -> bool { + if !set_clipboard_content(text) { + return false; + } + std::thread::sleep(std::time::Duration::from_millis(CLIPBOARD_SYNC_DELAY_MS)); + true +} + +/// Check if a character is ASCII printable (0x20-0x7E). +#[cfg(target_os = "linux")] +#[inline] +pub(super) fn is_ascii_printable(c: char) -> bool { + c as u32 >= 0x20 && c as u32 <= 0x7E +} + +/// Input a single character via clipboard + Shift+Insert in server process. +#[cfg(target_os = "linux")] +#[inline] +fn input_char_via_clipboard_server(en: &mut Enigo, chr: char) { + input_text_via_clipboard_server(en, &chr.to_string()); +} + +/// Input text via clipboard + Shift+Insert in server process. +/// Shift+Insert is more universal than Ctrl+V, works in both GUI apps and terminals. +/// +/// Note: Clipboard content is NOT restored after paste - see `set_clipboard_for_paste_sync` for rationale. +#[cfg(target_os = "linux")] +fn input_text_via_clipboard_server(en: &mut Enigo, text: &str) { + if text.is_empty() { + return; + } + if !set_clipboard_for_paste_sync(text) { + return; + } + + // Use ENIGO's custom_keyboard directly to avoid creating new IPC connections + // which would cause excessive logging and keyboard device creation/destruction + if en.key_down(Key::Shift).is_err() { + log::error!("input_text_via_clipboard_server: failed to press Shift, skipping paste"); + return; + } + if en.key_down(Key::Raw(XKB_KEY_INSERT)).is_err() { + log::error!("input_text_via_clipboard_server: failed to press Insert, releasing Shift"); + en.key_up(Key::Shift); + return; + } + en.key_up(Key::Raw(XKB_KEY_INSERT)); + en.key_up(Key::Shift); + + // Brief delay to allow the target application to process the paste event. + // Empirical value — no reliable synchronization mechanism exists on Wayland. + std::thread::sleep(std::time::Duration::from_millis(20)); +} + +#[cfg(not(target_os = "macos"))] +fn release_keys(en: &mut Enigo, to_release: &Vec) { + for key in to_release { + en.key_up(key.clone()); + } +} + +fn record_pressed_key(record_key: KeysDown, down: bool) { + let mut key_down = KEYS_DOWN.lock().unwrap(); + if down { + key_down.insert(record_key, Instant::now()); + } else { + key_down.remove(&record_key); + } +} + +fn is_function_key(ck: &EnumOrUnknown) -> bool { + let mut res = false; + if ck.value() == ControlKey::CtrlAltDel.value() { + // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. + #[cfg(windows)] + std::thread::spawn(|| { + allow_err!(send_sas()); + }); + res = true; + } else if ck.value() == ControlKey::LockScreen.value() { + std::thread::spawn(|| { + lock_screen_2(); + }); + res = true; + } + return res; +} + +/// Check if any hotkey modifier (Ctrl/Alt/Meta) is currently pressed. +/// Used to detect hotkey combinations like Ctrl+C, Alt+Tab, etc. +/// +/// Note: Shift is intentionally NOT checked here. Shift+character produces a different +/// character (e.g., Shift+a → 'A'), which is normal text input, not a hotkey. +/// Shift is only relevant as a hotkey modifier when combined with Ctrl/Alt/Meta +/// (e.g., Ctrl+Shift+Z), in which case this function already returns true via Ctrl. +#[cfg(target_os = "linux")] +#[inline] +fn is_hotkey_modifier_pressed(en: &mut Enigo) -> bool { + get_modifier_state(Key::Control, en) + || get_modifier_state(Key::RightControl, en) + || get_modifier_state(Key::Alt, en) + || get_modifier_state(Key::RightAlt, en) + || get_modifier_state(Key::Meta, en) + || get_modifier_state(Key::RWin, en) +} + +/// Release Shift keys before character input in Legacy/Translate mode. +/// In these modes, the character has already been converted by the client, +/// so we should input it directly without Shift modifier affecting the result. +/// +/// Note: Does NOT release Shift if hotkey modifiers (Ctrl/Alt/Meta) are pressed, +/// to preserve combinations like Ctrl+Shift+Z. +#[cfg(target_os = "linux")] +fn release_shift_for_char_input(en: &mut Enigo) { + // Don't release Shift if hotkey modifiers (Ctrl/Alt/Meta) are pressed. + // This preserves combinations like Ctrl+Shift+Z. + if is_hotkey_modifier_pressed(en) { + return; + } + + // In translate mode, the client has already converted the keystroke to a character + // (e.g., Shift+a → 'A'). We release Shift here so the server inputs the character + // directly without Shift affecting the result. + // + // Shift is intentionally NOT restored after input — the client will send an explicit + // Shift key_up event when the user physically releases Shift. Restoring it here would + // cause a brief Shift re-press that could interfere with the next input event. + + let is_x11 = crate::platform::linux::is_x11(); + + if get_modifier_state(Key::Shift, en) { + if !is_x11 { + en.key_up(Key::Shift); + } else { + simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); + } + } + if get_modifier_state(Key::RightShift, en) { + if !is_x11 { + en.key_up(Key::RightShift); + } else { + simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + } + } +} + +fn legacy_keyboard_mode(evt: &KeyEvent) { + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + let mut to_release: Vec = Vec::new(); + + let mut en = ENIGO.lock().unwrap(); + sync_modifiers(&mut en, &evt, &mut to_release); + + let down = evt.down; + match evt.union { + Some(key_event::Union::ControlKey(ck)) => { + if is_function_key(&ck) { + return; + } + let record_key = ck.value() as u64; + record_pressed_key(KeysDown::EnigoKey(record_key), down); + process_control_key(&mut en, &ck, down) + } + Some(key_event::Union::Chr(chr)) => { + // For character input in Legacy mode, we need to release Shift first. + // The character has already been converted by the client, so we should + // input it directly without Shift modifier affecting the result. + // Only Ctrl/Alt/Meta should be kept for hotkeys like Ctrl+C. + #[cfg(target_os = "linux")] + release_shift_for_char_input(&mut en); + + let record_key = chr as u64 + KEY_CHAR_START; + record_pressed_key(KeysDown::EnigoKey(record_key), down); + process_chr(&mut en, chr, down, has_hotkey_modifiers(evt)) + } + Some(key_event::Union::Unicode(chr)) => { + // Same as Chr: release Shift for Unicode input + #[cfg(target_os = "linux")] + release_shift_for_char_input(&mut en); + + process_unicode(&mut en, chr) + } + Some(key_event::Union::Seq(ref seq)) => process_seq(&mut en, seq), + _ => {} + } + + #[cfg(not(target_os = "macos"))] + release_keys(&mut en, &to_release); +} + +#[cfg(target_os = "windows")] +fn translate_process_code(code: u32, down: bool) { + crate::platform::windows::try_change_desktop(); + match code >> 16 { + 0 => sim_rdev_rawkey_position(code as _, down), + vk_code => sim_rdev_rawkey_virtual(vk_code, down), + }; +} + +fn translate_keyboard_mode(evt: &KeyEvent) { + match &evt.union { + Some(key_event::Union::Seq(seq)) => { + // On Wayland, handle character input directly in this (--server) process using clipboard. + // This function runs in the --server process (logged-in user session), which has + // WAYLAND_DISPLAY and XDG_RUNTIME_DIR — so clipboard operations work here. + // + // Why not let it go through uinput IPC: + // 1. For uinput mode: the uinput service thread runs in the --service (root) process, + // which typically lacks user session environment. Clipboard operations there are + // unreliable. Handling clipboard here avoids that issue. + // 2. For RDP input mode: Portal's notify_keyboard_keysym API interprets keysyms + // based on its internal modifier state, which may not match our released state. + // Using clipboard bypasses this issue entirely. + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() { + let mut en = ENIGO.lock().unwrap(); + + // Check if this is a hotkey (Ctrl/Alt/Meta pressed) + // For hotkeys, we send character-based key events via Enigo instead of + // using the clipboard. This relies on the local keyboard layout for + // mapping characters to physical keys. + // This assumes client and server use the same keyboard layout (common case). + // Note: For non-Latin keyboards (e.g., Arabic), hotkeys may not work + // correctly if the character cannot be mapped to a key via KEY_MAP_LAYOUT. + // This is a known limitation - most common hotkeys (Ctrl+A/C/V/Z) use Latin + // characters which are mappable on most keyboard layouts. + if is_hotkey_modifier_pressed(&mut en) { + // For hotkeys, send character-based key events via Enigo. + // This relies on the local keyboard layout mapping (KEY_MAP_LAYOUT). + for chr in seq.chars() { + if !is_ascii_printable(chr) { + log::warn!( + "Hotkey with non-ASCII character may not work correctly on non-Latin keyboard layouts" + ); + } + en.key_click(Key::Layout(chr)); + } + return; + } + + // Normal text input: release Shift and use clipboard + release_shift_for_char_input(&mut en); + + input_text_via_clipboard_server(&mut en, seq); + return; + } + + // Fr -> US + // client: Shift + & => 1(send to remote) + // remote: Shift + 1 => ! + // + // Try to release shift first. + // remote: Shift + 1 => 1 + let mut en = ENIGO.lock().unwrap(); + + #[cfg(target_os = "macos")] + en.key_sequence(seq); + #[cfg(any(target_os = "linux", target_os = "windows"))] + { + #[cfg(target_os = "windows")] + let simulate_win_hot_key = is_hot_key_modifiers_down(&mut en); + #[cfg(target_os = "linux")] + let simulate_win_hot_key = false; + if !simulate_win_hot_key { + #[cfg(target_os = "linux")] + release_shift_for_char_input(&mut en); + #[cfg(target_os = "windows")] + { + if get_modifier_state(Key::Shift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); + } + if get_modifier_state(Key::RightShift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + } + } + } + for chr in seq.chars() { + // char in rust is 4 bytes. + // But for this case, char comes from keyboard. We only need 2 bytes. + #[cfg(target_os = "windows")] + if simulate_win_hot_key { + rdev::simulate_char(chr, true).ok(); + } else { + rdev::simulate_unicode(chr as _).ok(); + } + #[cfg(target_os = "linux")] + en.key_click(Key::Layout(chr)); + } + } + } + Some(key_event::Union::Chr(..)) => { + #[cfg(target_os = "windows")] + translate_process_code(evt.chr(), evt.down); + #[cfg(target_os = "linux")] + { + if !crate::platform::linux::is_x11() { + // Wayland: use uinput to send raw keycode + wayland_send_raw_key(evt.chr() as u16, evt.down); + } else { + sim_rdev_rawkey_position(evt.chr() as _, evt.down); + } + } + #[cfg(target_os = "macos")] + sim_rdev_rawkey_position(evt.chr() as _, evt.down); + } + Some(key_event::Union::Unicode(..)) => { + // Do not handle unicode for now. + } + #[cfg(target_os = "windows")] + Some(key_event::Union::Win2winHotkey(code)) => { + simulate_win2win_hotkey(*code, evt.down); + } + _ => { + log::debug!( + "Unreachable. Unexpected key event (mode={:?}, down={:?})", + &evt.mode, + &evt.down + ); + } + } +} + +#[inline] +#[cfg(target_os = "windows")] +fn is_hot_key_modifiers_down(en: &mut Enigo) -> bool { + en.get_key_state(Key::Control) + || en.get_key_state(Key::RightControl) + || en.get_key_state(Key::Alt) + || en.get_key_state(Key::RightAlt) + || en.get_key_state(Key::Meta) + || en.get_key_state(Key::RWin) +} + +#[cfg(target_os = "windows")] +fn simulate_win2win_hotkey(code: u32, down: bool) { + let unicode: u16 = (code & 0x0000FFFF) as u16; + if down { + if rdev::simulate_key_unicode(unicode, false).is_ok() { + return; + } + } + + let keycode: u16 = ((code >> 16) & 0x0000FFFF) as u16; + let scan = rdev::vk_to_scancode(keycode as _); + allow_err!(rdev::simulate_code(None, Some(scan), down)); +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +fn skip_led_sync_control_key(_key: &ControlKey) -> bool { + false +} + +// LockModesHandler should not be created when single meta is pressing and releasing. +// Because the drop function may insert "CapsLock Click" and "NumLock Click", which breaks single meta click. +// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1496936687 +// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1500415822 +// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1500773473 +#[cfg(any(target_os = "windows", target_os = "linux"))] +fn skip_led_sync_control_key(key: &ControlKey) -> bool { + matches!( + key, + ControlKey::Control + | ControlKey::RControl + | ControlKey::Meta + | ControlKey::Shift + | ControlKey::RShift + | ControlKey::Alt + | ControlKey::RAlt + | ControlKey::Tab + | ControlKey::Return + ) +} + +#[inline] +#[cfg(any(target_os = "windows", target_os = "linux"))] +fn is_numpad_control_key(key: &ControlKey) -> bool { + matches!( + key, + ControlKey::Numpad0 + | ControlKey::Numpad1 + | ControlKey::Numpad2 + | ControlKey::Numpad3 + | ControlKey::Numpad4 + | ControlKey::Numpad5 + | ControlKey::Numpad6 + | ControlKey::Numpad7 + | ControlKey::Numpad8 + | ControlKey::Numpad9 + | ControlKey::NumpadEnter + ) +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +fn skip_led_sync_rdev_key(_key: &RdevKey) -> bool { + false +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +fn skip_led_sync_rdev_key(key: &RdevKey) -> bool { + matches!( + key, + RdevKey::ControlLeft + | RdevKey::ControlRight + | RdevKey::MetaLeft + | RdevKey::MetaRight + | RdevKey::ShiftLeft + | RdevKey::ShiftRight + | RdevKey::Alt + | RdevKey::AltGr + | RdevKey::Tab + | RdevKey::Return + ) +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn is_legacy_mode(evt: &KeyEvent) -> bool { + evt.mode.enum_value_or(KeyboardMode::Legacy) == KeyboardMode::Legacy +} + +pub fn handle_key_(evt: &KeyEvent) { + if EXITING.load(Ordering::SeqCst) { + return; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let mut _lock_mode_handler = None; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + match &evt.union { + Some(key_event::Union::Unicode(..)) | Some(key_event::Union::Seq(..)) => { + _lock_mode_handler = Some(LockModesHandler::new_handler(&evt, false)); + } + Some(key_event::Union::ControlKey(ck)) => { + let key = ck.enum_value_or(ControlKey::Unknown); + if !skip_led_sync_control_key(&key) { + #[cfg(target_os = "macos")] + let is_numpad_key = false; + #[cfg(any(target_os = "windows", target_os = "linux"))] + let is_numpad_key = is_numpad_control_key(&key); + _lock_mode_handler = Some(LockModesHandler::new_handler(&evt, is_numpad_key)); + } + } + Some(key_event::Union::Chr(code)) => { + if is_legacy_mode(&evt) { + _lock_mode_handler = Some(LockModesHandler::new_handler(evt, false)); + } else { + let key = crate::keyboard::keycode_to_rdev_key(*code); + if !skip_led_sync_rdev_key(&key) { + #[cfg(target_os = "macos")] + let is_numpad_key = false; + #[cfg(any(target_os = "windows", target_os = "linux"))] + let is_numpad_key = crate::keyboard::is_numpad_rdev_key(&key); + _lock_mode_handler = Some(LockModesHandler::new_handler(evt, is_numpad_key)); + } + } + } + _ => {} + } + + match evt.mode.enum_value() { + Ok(KeyboardMode::Map) => { + #[cfg(target_os = "macos")] + set_last_legacy_mode(false); + map_keyboard_mode(evt); + } + Ok(KeyboardMode::Translate) => { + #[cfg(target_os = "macos")] + set_last_legacy_mode(false); + translate_keyboard_mode(evt); + } + _ => { + // All key down events are started from here, + // so we can reset the flag of last legacy mode here. + #[cfg(target_os = "macos")] + set_last_legacy_mode(true); + legacy_keyboard_mode(evt); + } + } +} + +#[tokio::main(flavor = "current_thread")] +async fn lock_screen_2() { + lock_screen().await; +} + +#[cfg(windows)] +#[tokio::main(flavor = "current_thread")] +async fn send_sas() -> ResultType<()> { + if crate::platform::is_physical_console_session().unwrap_or(true) { + let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; + timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??; + } else { + crate::platform::send_sas(); + }; + Ok(()) +} + +#[inline] +#[cfg(target_os = "linux")] +pub fn wayland_use_uinput() -> bool { + !crate::platform::is_x11() && crate::is_server() +} + +#[inline] +#[cfg(target_os = "linux")] +pub fn wayland_use_rdp_input() -> bool { + !crate::platform::is_x11() && !crate::is_server() +} + +#[cfg(target_os = "linux")] +pub struct TemporaryMouseMoveHandle { + thread_handle: Option>, + tx: Option>, +} + +#[cfg(target_os = "linux")] +impl TemporaryMouseMoveHandle { + pub fn new() -> Self { + let (tx, rx) = mpsc::channel::<(i32, i32)>(); + let thread_handle = std::thread::spawn(move || { + log::debug!("TemporaryMouseMoveHandle thread started"); + for (x, y) in rx { + ENIGO.lock().unwrap().mouse_move_to(x, y); + } + log::debug!("TemporaryMouseMoveHandle thread exiting"); + }); + TemporaryMouseMoveHandle { + thread_handle: Some(thread_handle), + tx: Some(tx), + } + } + + pub fn move_mouse_to(&self, x: i32, y: i32) { + if let Some(tx) = &self.tx { + let _ = tx.send((x, y)); + } + } +} + +#[cfg(target_os = "linux")] +impl Drop for TemporaryMouseMoveHandle { + fn drop(&mut self) { + log::debug!("Dropping TemporaryMouseMoveHandle"); + // Close the channel to signal the thread to exit. + self.tx.take(); + // Wait for the thread to finish. + if let Some(thread_handle) = self.thread_handle.take() { + if let Err(e) = thread_handle.join() { + log::error!("Error joining TemporaryMouseMoveHandle thread: {:?}", e); + } + } + } +} + +lazy_static::lazy_static! { + static ref MODIFIER_MAP: HashMap = [ + (ControlKey::Alt, Key::Alt), + (ControlKey::RAlt, Key::RightAlt), + (ControlKey::Control, Key::Control), + (ControlKey::RControl, Key::RightControl), + (ControlKey::Shift, Key::Shift), + (ControlKey::RShift, Key::RightShift), + (ControlKey::Meta, Key::Meta), + (ControlKey::RWin, Key::RWin), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); + static ref KEY_MAP: HashMap = + [ + (ControlKey::Alt, Key::Alt), + (ControlKey::Backspace, Key::Backspace), + (ControlKey::CapsLock, Key::CapsLock), + (ControlKey::Control, Key::Control), + (ControlKey::Delete, Key::Delete), + (ControlKey::DownArrow, Key::DownArrow), + (ControlKey::End, Key::End), + (ControlKey::Escape, Key::Escape), + (ControlKey::F1, Key::F1), + (ControlKey::F10, Key::F10), + (ControlKey::F11, Key::F11), + (ControlKey::F12, Key::F12), + (ControlKey::F2, Key::F2), + (ControlKey::F3, Key::F3), + (ControlKey::F4, Key::F4), + (ControlKey::F5, Key::F5), + (ControlKey::F6, Key::F6), + (ControlKey::F7, Key::F7), + (ControlKey::F8, Key::F8), + (ControlKey::F9, Key::F9), + (ControlKey::Home, Key::Home), + (ControlKey::LeftArrow, Key::LeftArrow), + (ControlKey::Meta, Key::Meta), + (ControlKey::Option, Key::Option), + (ControlKey::PageDown, Key::PageDown), + (ControlKey::PageUp, Key::PageUp), + (ControlKey::Return, Key::Return), + (ControlKey::RightArrow, Key::RightArrow), + (ControlKey::Shift, Key::Shift), + (ControlKey::Space, Key::Space), + (ControlKey::Tab, Key::Tab), + (ControlKey::UpArrow, Key::UpArrow), + (ControlKey::Numpad0, Key::Numpad0), + (ControlKey::Numpad1, Key::Numpad1), + (ControlKey::Numpad2, Key::Numpad2), + (ControlKey::Numpad3, Key::Numpad3), + (ControlKey::Numpad4, Key::Numpad4), + (ControlKey::Numpad5, Key::Numpad5), + (ControlKey::Numpad6, Key::Numpad6), + (ControlKey::Numpad7, Key::Numpad7), + (ControlKey::Numpad8, Key::Numpad8), + (ControlKey::Numpad9, Key::Numpad9), + (ControlKey::Cancel, Key::Cancel), + (ControlKey::Clear, Key::Clear), + (ControlKey::Menu, Key::Alt), + (ControlKey::Pause, Key::Pause), + (ControlKey::Kana, Key::Kana), + (ControlKey::Hangul, Key::Hangul), + (ControlKey::Junja, Key::Junja), + (ControlKey::Final, Key::Final), + (ControlKey::Hanja, Key::Hanja), + (ControlKey::Kanji, Key::Kanji), + (ControlKey::Convert, Key::Convert), + (ControlKey::Select, Key::Select), + (ControlKey::Print, Key::Print), + (ControlKey::Execute, Key::Execute), + (ControlKey::Snapshot, Key::Snapshot), + (ControlKey::Insert, Key::Insert), + (ControlKey::Help, Key::Help), + (ControlKey::Sleep, Key::Sleep), + (ControlKey::Separator, Key::Separator), + (ControlKey::Scroll, Key::Scroll), + (ControlKey::NumLock, Key::NumLock), + (ControlKey::RWin, Key::RWin), + (ControlKey::Apps, Key::Apps), + (ControlKey::Multiply, Key::Multiply), + (ControlKey::Add, Key::Add), + (ControlKey::Subtract, Key::Subtract), + (ControlKey::Decimal, Key::Decimal), + (ControlKey::Divide, Key::Divide), + (ControlKey::Equals, Key::Equals), + (ControlKey::NumpadEnter, Key::NumpadEnter), + (ControlKey::RAlt, Key::RightAlt), + (ControlKey::RControl, Key::RightControl), + (ControlKey::RShift, Key::RightShift), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); + static ref NUMPAD_KEY_MAP: HashMap = + [ + (ControlKey::Home, true), + (ControlKey::UpArrow, true), + (ControlKey::PageUp, true), + (ControlKey::LeftArrow, true), + (ControlKey::RightArrow, true), + (ControlKey::End, true), + (ControlKey::DownArrow, true), + (ControlKey::PageDown, true), + (ControlKey::Insert, true), + (ControlKey::Delete, true), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); +} diff --git a/vendor/rustdesk/src/server/portable_service.rs b/vendor/rustdesk/src/server/portable_service.rs new file mode 100644 index 0000000..6f56950 --- /dev/null +++ b/vendor/rustdesk/src/server/portable_service.rs @@ -0,0 +1,992 @@ +use core::slice; +use hbb_common::{ + allow_err, + anyhow::anyhow, + bail, libc, log, + message_proto::{KeyEvent, MouseEvent}, + protobuf::Message, + tokio::{self, sync::mpsc}, + ResultType, +}; +#[cfg(feature = "vram")] +use scrap::AdapterDevice; +use scrap::{Capturer, Frame, TraitCapturer, TraitPixelBuffer}; +use shared_memory::*; +use std::{ + mem::size_of, + ops::{Deref, DerefMut}, + path::Path, + sync::{Arc, Mutex}, + time::Duration, +}; +use winapi::{ + shared::minwindef::{BOOL, FALSE, TRUE}, + um::winuser::{self, CURSORINFO, PCURSORINFO}, +}; + +use crate::{ + ipc::{self, new_listener, Connection, Data, DataPortableService}, + platform::set_path_permission, +}; + +use super::video_qos; + +const SIZE_COUNTER: usize = size_of::() * 2; +const FRAME_ALIGN: usize = 64; + +const ADDR_CURSOR_PARA: usize = 0; +const ADDR_CURSOR_COUNTER: usize = ADDR_CURSOR_PARA + size_of::(); + +const ADDR_CAPTURER_PARA: usize = ADDR_CURSOR_COUNTER + SIZE_COUNTER; +const ADDR_CAPTURE_FRAME_INFO: usize = ADDR_CAPTURER_PARA + size_of::(); +const ADDR_CAPTURE_WOULDBLOCK: usize = ADDR_CAPTURE_FRAME_INFO + size_of::(); +const ADDR_CAPTURE_FRAME_COUNTER: usize = ADDR_CAPTURE_WOULDBLOCK + size_of::(); + +const ADDR_CAPTURE_FRAME: usize = + (ADDR_CAPTURE_FRAME_COUNTER + SIZE_COUNTER + FRAME_ALIGN - 1) / FRAME_ALIGN * FRAME_ALIGN; + +const IPC_SUFFIX: &str = "_portable_service"; +pub const SHMEM_NAME: &str = "_portable_service"; +const MAX_NACK: usize = 3; +const MAX_DXGI_FAIL_TIME: usize = 5; + +pub struct SharedMemory { + inner: Shmem, +} + +unsafe impl Send for SharedMemory {} +unsafe impl Sync for SharedMemory {} + +impl Deref for SharedMemory { + type Target = Shmem; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for SharedMemory { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl SharedMemory { + pub fn create(name: &str, size: usize) -> ResultType { + let flink = Self::flink(name.to_string())?; + let shmem = match ShmemConf::new() + .size(size) + .flink(&flink) + .force_create_flink() + .create() + { + Ok(m) => m, + Err(ShmemError::LinkExists) => { + bail!( + "Unable to force create shmem flink {}, which should not happen.", + flink + ) + } + Err(e) => { + bail!("Unable to create shmem flink {} : {}", flink, e); + } + }; + log::info!("Create shared memory, size: {}, flink: {}", size, flink); + set_path_permission(Path::new(&flink), "F").ok(); + Ok(SharedMemory { inner: shmem }) + } + + pub fn open_existing(name: &str) -> ResultType { + let flink = Self::flink(name.to_string())?; + let shmem = match ShmemConf::new().flink(&flink).allow_raw(true).open() { + Ok(m) => m, + Err(e) => { + bail!("Unable to open existing shmem flink {} : {}", flink, e); + } + }; + log::info!("open existing shared memory, flink: {:?}", flink); + Ok(SharedMemory { inner: shmem }) + } + + pub fn write(&self, addr: usize, data: &[u8]) { + unsafe { + debug_assert!(addr + data.len() <= self.inner.len()); + let ptr = self.inner.as_ptr().add(addr); + let shared_mem_slice = slice::from_raw_parts_mut(ptr, data.len()); + shared_mem_slice.copy_from_slice(data); + } + } + + fn flink(name: String) -> ResultType { + let mut dir = crate::platform::user_accessible_folder()?; + dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); + if !dir.exists() { + std::fs::create_dir(&dir)?; + set_path_permission(&dir, "F").ok(); + } + Ok(dir + .join(format!("shared_memory{}", name)) + .to_string_lossy() + .to_string()) + } +} + +mod utils { + use core::slice; + use std::mem::size_of; + + use super::{ + CapturerPara, FrameInfo, SharedMemory, ADDR_CAPTURER_PARA, ADDR_CAPTURE_FRAME_INFO, + }; + + #[inline] + pub fn i32_to_vec(i: i32) -> Vec { + i.to_ne_bytes().to_vec() + } + + #[inline] + pub fn ptr_to_i32(ptr: *const u8) -> i32 { + unsafe { + let v = slice::from_raw_parts(ptr, size_of::()); + i32::from_ne_bytes([v[0], v[1], v[2], v[3]]) + } + } + + #[inline] + pub fn counter_ready(counter: *const u8) -> bool { + unsafe { + let wptr = counter; + let rptr = counter.add(size_of::()); + let iw = ptr_to_i32(wptr); + let ir = ptr_to_i32(rptr); + if ir != iw { + std::ptr::copy_nonoverlapping(wptr, rptr as *mut _, size_of::()); + true + } else { + false + } + } + } + + #[inline] + pub fn counter_equal(counter: *const u8) -> bool { + unsafe { + let wptr = counter; + let rptr = counter.add(size_of::()); + let iw = ptr_to_i32(wptr); + let ir = ptr_to_i32(rptr); + iw == ir + } + } + + #[inline] + pub fn increase_counter(counter: *mut u8) { + unsafe { + let wptr = counter; + let rptr = counter.add(size_of::()); + let iw = ptr_to_i32(counter); + let ir = ptr_to_i32(counter); + let iw_plus1 = if iw == i32::MAX { 0 } else { iw + 1 }; + let v = i32_to_vec(iw_plus1); + std::ptr::copy_nonoverlapping(v.as_ptr(), wptr, size_of::()); + if ir == iw_plus1 { + let v = i32_to_vec(iw); + std::ptr::copy_nonoverlapping(v.as_ptr(), rptr, size_of::()); + } + } + } + + #[inline] + pub fn align(v: usize, align: usize) -> usize { + (v + align - 1) / align * align + } + + #[inline] + pub fn set_para(shmem: &SharedMemory, para: CapturerPara) { + let para_ptr = ¶ as *const CapturerPara as *const u8; + let para_data; + unsafe { + para_data = slice::from_raw_parts(para_ptr, size_of::()); + } + shmem.write(ADDR_CAPTURER_PARA, para_data); + } + + #[inline] + pub fn set_frame_info(shmem: &SharedMemory, info: FrameInfo) { + let ptr = &info as *const FrameInfo as *const u8; + let data; + unsafe { + data = slice::from_raw_parts(ptr, size_of::()); + } + shmem.write(ADDR_CAPTURE_FRAME_INFO, data); + } +} + +// functions called in separate SYSTEM user process. +pub mod server { + use hbb_common::message_proto::PointerDeviceEvent; + + use crate::display_service; + + use super::*; + + lazy_static::lazy_static! { + static ref EXIT: Arc> = Default::default(); + } + + pub fn run_portable_service() { + let shmem = match SharedMemory::open_existing(SHMEM_NAME) { + Ok(shmem) => Arc::new(shmem), + Err(e) => { + log::error!("Failed to open existing shared memory: {:?}", e); + return; + } + }; + let shmem1 = shmem.clone(); + let shmem2 = shmem.clone(); + let mut threads = vec![]; + threads.push(std::thread::spawn(|| { + run_get_cursor_info(shmem1); + })); + threads.push(std::thread::spawn(|| { + run_capture(shmem2); + })); + threads.push(std::thread::spawn(|| { + run_ipc_client(); + })); + threads.push(std::thread::spawn(|| { + run_exit_check(); + })); + let record_pos_handle = crate::input_service::try_start_record_cursor_pos(); + for th in threads.drain(..) { + th.join().ok(); + log::info!("thread joined"); + } + + crate::input_service::try_stop_record_cursor_pos(); + if let Some(handle) = record_pos_handle { + match handle.join() { + Ok(_) => log::info!("record_pos_handle joined"), + Err(e) => log::error!("record_pos_handle join error {:?}", &e), + } + } + } + + fn run_exit_check() { + loop { + if EXIT.lock().unwrap().clone() { + std::thread::sleep(Duration::from_millis(50)); + std::process::exit(0); + } + std::thread::sleep(Duration::from_millis(50)); + } + } + + fn run_get_cursor_info(shmem: Arc) { + loop { + if EXIT.lock().unwrap().clone() { + break; + } + unsafe { + let para = shmem.as_ptr().add(ADDR_CURSOR_PARA) as *mut CURSORINFO; + (*para).cbSize = size_of::() as _; + let result = winuser::GetCursorInfo(para); + if result == TRUE { + utils::increase_counter(shmem.as_ptr().add(ADDR_CURSOR_COUNTER)); + } + } + // more frequent in case of `Error of mouse_cursor service` + std::thread::sleep(Duration::from_millis(15)); + } + } + + fn run_capture(shmem: Arc) { + let mut c = None; + let mut last_current_display = usize::MAX; + let mut last_timeout_ms: i32 = 33; + let mut spf = Duration::from_millis(last_timeout_ms as _); + let mut first_frame_captured = false; + let mut dxgi_failed_times = 0; + let mut display_width = 0; + let mut display_height = 0; + loop { + if EXIT.lock().unwrap().clone() { + break; + } + unsafe { + let para_ptr = shmem.as_ptr().add(ADDR_CAPTURER_PARA); + let para = para_ptr as *const CapturerPara; + let recreate = (*para).recreate; + let current_display = (*para).current_display; + let timeout_ms = (*para).timeout_ms; + if c.is_none() { + let Ok(mut displays) = display_service::try_get_displays() else { + log::error!("Failed to get displays"); + *EXIT.lock().unwrap() = true; + return; + }; + if displays.len() <= current_display { + log::error!("Invalid display index:{}", current_display); + *EXIT.lock().unwrap() = true; + return; + } + let display = displays.remove(current_display); + display_width = display.width(); + display_height = display.height(); + match Capturer::new(display) { + Ok(mut v) => { + c = { + last_current_display = current_display; + first_frame_captured = false; + if dxgi_failed_times > MAX_DXGI_FAIL_TIME { + dxgi_failed_times = 0; + v.set_gdi(); + } + utils::set_para( + &shmem, + CapturerPara { + recreate: false, + current_display: (*para).current_display, + timeout_ms: (*para).timeout_ms, + }, + ); + Some(v) + } + } + Err(e) => { + log::error!("Failed to create gdi capturer: {:?}", e); + std::thread::sleep(std::time::Duration::from_secs(1)); + continue; + } + } + } else { + if recreate || current_display != last_current_display { + log::info!( + "create capturer, display: {} -> {}", + last_current_display, + current_display, + ); + c = None; + continue; + } + if timeout_ms != last_timeout_ms + && timeout_ms >= 1000 / video_qos::MAX_FPS as i32 + && timeout_ms <= 1000 / video_qos::MIN_FPS as i32 + { + last_timeout_ms = timeout_ms; + spf = Duration::from_millis(timeout_ms as _); + } + } + if first_frame_captured { + if !utils::counter_equal(shmem.as_ptr().add(ADDR_CAPTURE_FRAME_COUNTER)) { + std::thread::sleep(std::time::Duration::from_millis(1)); + continue; + } + } + match c.as_mut().map(|f| f.frame(spf)) { + Some(Ok(f)) => match f { + Frame::PixelBuffer(f) => { + utils::set_frame_info( + &shmem, + FrameInfo { + length: f.data().len(), + width: display_width, + height: display_height, + }, + ); + shmem.write(ADDR_CAPTURE_FRAME, f.data()); + shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE)); + utils::increase_counter(shmem.as_ptr().add(ADDR_CAPTURE_FRAME_COUNTER)); + first_frame_captured = true; + dxgi_failed_times = 0; + } + Frame::Texture(_) => { + // should not happen + } + }, + Some(Err(e)) => { + if crate::platform::windows::desktop_changed() { + crate::platform::try_change_desktop(); + c = None; + std::thread::sleep(spf); + continue; + } + if e.kind() != std::io::ErrorKind::WouldBlock { + // DXGI_ERROR_INVALID_CALL after each success on Microsoft GPU driver + // log::error!("capture frame failed: {:?}", e); + if c.as_ref().map(|c| c.is_gdi()) == Some(false) { + // nog gdi + dxgi_failed_times += 1; + } + if dxgi_failed_times > MAX_DXGI_FAIL_TIME { + c = None; + shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(FALSE)); + std::thread::sleep(spf); + } + } else { + shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE)); + } + } + _ => { + println!("unreachable!"); + } + } + } + } + } + + #[tokio::main(flavor = "current_thread")] + async fn run_ipc_client() { + use DataPortableService::*; + + let postfix = IPC_SUFFIX; + + match ipc::connect(1000, postfix).await { + Ok(mut stream) => { + let mut timer = + crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1))); + let mut nack = 0; + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::error!( + "ipc{} connection closed: {}", + postfix, + err + ); + break; + } + Ok(Some(Data::DataPortableService(data))) => match data { + Ping => { + allow_err!( + stream + .send(&Data::DataPortableService(Pong)) + .await + ); + } + Pong => { + nack = 0; + } + ConnCount(Some(n)) => { + if n == 0 { + log::info!("Connection count equals 0, exit"); + stream.send(&Data::DataPortableService(WillClose)).await.ok(); + break; + } + } + Mouse((v, conn, username, argb, simulate, show_cursor)) => { + if let Ok(evt) = MouseEvent::parse_from_bytes(&v) { + crate::input_service::handle_mouse_(&evt, conn, username, argb, simulate, show_cursor); + } + } + Pointer((v, conn)) => { + if let Ok(evt) = PointerDeviceEvent::parse_from_bytes(&v) { + crate::input_service::handle_pointer_(&evt, conn); + } + } + Key(v) => { + if let Ok(evt) = KeyEvent::parse_from_bytes(&v) { + crate::input_service::handle_key_(&evt); + } + } + _ => {} + }, + _ => {} + } + } + _ = timer.tick() => { + nack+=1; + if nack > MAX_NACK { + log::info!("max ping nack, exit"); + break; + } + stream.send(&Data::DataPortableService(Ping)).await.ok(); + stream.send(&Data::DataPortableService(ConnCount(None))).await.ok(); + } + } + } + } + Err(e) => { + log::error!("Failed to connect portable service ipc: {:?}", e); + } + } + + *EXIT.lock().unwrap() = true; + } +} + +// functions called in main process. +pub mod client { + use super::*; + use crate::display_service; + use hbb_common::{anyhow::Context, message_proto::PointerDeviceEvent}; + use scrap::PixelBuffer; + + lazy_static::lazy_static! { + static ref RUNNING: Arc> = Default::default(); + static ref SHMEM: Arc>> = Default::default(); + static ref SENDER : Mutex> = Mutex::new(client::start_ipc_server()); + static ref QUICK_SUPPORT: Arc> = Default::default(); + } + + pub enum StartPara { + Direct, + Logon(String, String), + } + + pub(crate) fn start_portable_service(para: StartPara) -> ResultType<()> { + log::info!("start portable service"); + if RUNNING.lock().unwrap().clone() { + bail!("already running"); + } + if SHMEM.lock().unwrap().is_none() { + let displays = scrap::Display::all()?; + if displays.is_empty() { + bail!("no display available!"); + } + let mut max_pixel = 0; + let align = 64; + for d in displays { + let resolutions = crate::platform::resolutions(&d.name()); + for r in resolutions { + let pixel = + utils::align(r.width as _, align) * utils::align(r.height as _, align); + if max_pixel < pixel { + max_pixel = pixel; + } + } + } + let shmem_size = utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align); + // os error 112, no enough space + *SHMEM.lock().unwrap() = Some(crate::portable_service::SharedMemory::create( + crate::portable_service::SHMEM_NAME, + shmem_size, + )?); + shutdown_hooks::add_shutdown_hook(drop_portable_service_shared_memory); + } + if let Some(shmem) = SHMEM.lock().unwrap().as_mut() { + unsafe { + libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + } + } + match para { + StartPara::Direct => { + if let Err(e) = crate::platform::run_background( + &std::env::current_exe()?.to_string_lossy().to_string(), + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process: {}", e); + } + } + StartPara::Logon(username, password) => { + #[allow(unused_mut)] + let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); + #[cfg(feature = "flutter")] + { + if let Some(dir) = Path::new(&exe).parent() { + if set_path_permission(Path::new(dir), "RX").is_err() { + *SHMEM.lock().unwrap() = None; + bail!("Failed to set permission of {:?}", dir); + } + } + } + #[cfg(not(feature = "flutter"))] + match hbb_common::directories_next::UserDirs::new() { + Some(user_dir) => { + let dir = user_dir + .home_dir() + .join("AppData") + .join("Local") + .join("rustdesk-sciter"); + if std::fs::create_dir_all(&dir).is_ok() { + let dst = dir.join("rustdesk.exe"); + if std::fs::copy(&exe, &dst).is_ok() { + if dst.exists() { + if set_path_permission(&dir, "RX").is_ok() { + exe = dst.to_string_lossy().to_string(); + } + } + } + } + } + None => {} + } + if let Err(e) = crate::platform::windows::create_process_with_logon( + username.as_str(), + password.as_str(), + &exe, + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process: {}", e); + } + } + } + let _sender = SENDER.lock().unwrap(); + Ok(()) + } + + pub extern "C" fn drop_portable_service_shared_memory() { + // https://stackoverflow.com/questions/35980148/why-does-an-atexit-handler-panic-when-it-accesses-stdout + // Please make sure there is no print in the call stack + let mut lock = SHMEM.lock().unwrap(); + if lock.is_some() { + *lock = None; + } + } + + pub fn set_quick_support(v: bool) { + *QUICK_SUPPORT.lock().unwrap() = v; + } + + pub struct CapturerPortable { + width: usize, + height: usize, + } + + impl CapturerPortable { + pub fn new(current_display: usize) -> Self + where + Self: Sized, + { + let mut option = SHMEM.lock().unwrap(); + if let Some(shmem) = option.as_mut() { + unsafe { + libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + } + utils::set_para( + shmem, + CapturerPara { + recreate: true, + current_display, + timeout_ms: 33, + }, + ); + shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE)); + } + let (mut width, mut height) = (0, 0); + if let Ok(displays) = display_service::try_get_displays() { + if let Some(display) = displays.get(current_display) { + width = display.width(); + height = display.height(); + } + } + CapturerPortable { width, height } + } + } + + impl TraitCapturer for CapturerPortable { + fn frame<'a>(&'a mut self, timeout: Duration) -> std::io::Result> { + let mut lock = SHMEM.lock().unwrap(); + let shmem = lock.as_mut().ok_or(std::io::Error::new( + std::io::ErrorKind::Other, + "shmem dropped".to_string(), + ))?; + unsafe { + let base = shmem.as_ptr(); + let para_ptr = base.add(ADDR_CAPTURER_PARA); + let para = para_ptr as *const CapturerPara; + if timeout.as_millis() != (*para).timeout_ms as _ { + utils::set_para( + shmem, + CapturerPara { + recreate: (*para).recreate, + current_display: (*para).current_display, + timeout_ms: timeout.as_millis() as _, + }, + ); + } + if utils::counter_ready(base.add(ADDR_CAPTURE_FRAME_COUNTER)) { + let frame_info_ptr = shmem.as_ptr().add(ADDR_CAPTURE_FRAME_INFO); + let frame_info = frame_info_ptr as *const FrameInfo; + if (*frame_info).width != self.width || (*frame_info).height != self.height { + log::info!( + "skip frame, ({},{}) != ({},{})", + (*frame_info).width, + (*frame_info).height, + self.width, + self.height, + ); + return Err(std::io::Error::new( + std::io::ErrorKind::WouldBlock, + "wouldblock error".to_string(), + )); + } + let frame_ptr = base.add(ADDR_CAPTURE_FRAME); + let data = slice::from_raw_parts(frame_ptr, (*frame_info).length); + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( + data, + self.width, + self.height, + ))) + } else { + let ptr = base.add(ADDR_CAPTURE_WOULDBLOCK); + let wouldblock = utils::ptr_to_i32(ptr); + if wouldblock == TRUE { + Err(std::io::Error::new( + std::io::ErrorKind::WouldBlock, + "wouldblock error".to_string(), + )) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "other error".to_string(), + )) + } + } + } + } + + // control by itself + fn is_gdi(&self) -> bool { + true + } + + fn set_gdi(&mut self) -> bool { + true + } + + #[cfg(feature = "vram")] + fn device(&self) -> AdapterDevice { + AdapterDevice::default() + } + + #[cfg(feature = "vram")] + fn set_output_texture(&mut self, _texture: bool) {} + } + + pub(super) fn start_ipc_server() -> mpsc::UnboundedSender { + let (tx, rx) = mpsc::unbounded_channel::(); + std::thread::spawn(move || start_ipc_server_async(rx)); + tx + } + + #[tokio::main(flavor = "current_thread")] + async fn start_ipc_server_async(rx: mpsc::UnboundedReceiver) { + use DataPortableService::*; + let rx = Arc::new(tokio::sync::Mutex::new(rx)); + let postfix = IPC_SUFFIX; + let quick_support = QUICK_SUPPORT.lock().unwrap().clone(); + + match new_listener(postfix).await { + Ok(mut incoming) => loop { + { + tokio::select! { + Some(result) = incoming.next() => { + match result { + Ok(stream) => { + log::info!("Got portable service ipc connection"); + let rx_clone = rx.clone(); + tokio::spawn(async move { + let mut stream = Connection::new(stream); + let postfix = postfix.to_owned(); + let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1))); + let mut nack = 0; + let mut rx = rx_clone.lock().await; + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::info!( + "ipc{} connection closed: {}", + postfix, + err + ); + break; + } + Ok(Some(Data::DataPortableService(data))) => match data { + Ping => { + stream.send(&Data::DataPortableService(Pong)).await.ok(); + } + Pong => { + nack = 0; + *RUNNING.lock().unwrap() = true; + }, + ConnCount(None) => { + if !quick_support { + let remote_count = crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.conn_type == crate::server::AuthConnType::Remote) + .count(); + stream.send(&Data::DataPortableService(ConnCount(Some(remote_count)))).await.ok(); + } + }, + WillClose => { + log::info!("portable service will close"); + break; + } + _=>{} + } + _=>{} + } + } + _ = timer.tick() => { + nack+=1; + if nack > MAX_NACK { + // In fact, this will not happen, ipc will be closed before max nack. + log::error!("max ipc nack"); + break; + } + stream.send(&Data::DataPortableService(Ping)).await.ok(); + } + Some(data) = rx.recv() => { + allow_err!(stream.send(&data).await); + } + } + } + *RUNNING.lock().unwrap() = false; + }); + } + Err(err) => { + log::error!("Couldn't get portable client: {:?}", err); + } + } + } + } + } + }, + Err(err) => { + log::error!("Failed to start portable service ipc server: {}", err); + } + } + } + + fn ipc_send(data: Data) -> ResultType<()> { + let sender = SENDER.lock().unwrap(); + sender + .send(data) + .map_err(|e| anyhow!("ipc send error:{:?}", e)) + } + + fn get_cursor_info_(shmem: &mut SharedMemory, pci: PCURSORINFO) -> BOOL { + unsafe { + let shmem_addr_para = shmem.as_ptr().add(ADDR_CURSOR_PARA); + if utils::counter_ready(shmem.as_ptr().add(ADDR_CURSOR_COUNTER)) { + std::ptr::copy_nonoverlapping(shmem_addr_para, pci as _, size_of::()); + return TRUE; + } + FALSE + } + } + + fn handle_mouse_( + evt: &MouseEvent, + conn: i32, + username: String, + argb: u32, + simulate: bool, + show_cursor: bool, + ) -> ResultType<()> { + let mut v = vec![]; + evt.write_to_vec(&mut v)?; + ipc_send(Data::DataPortableService(DataPortableService::Mouse(( + v, + conn, + username, + argb, + simulate, + show_cursor, + )))) + } + + fn handle_pointer_(evt: &PointerDeviceEvent, conn: i32) -> ResultType<()> { + let mut v = vec![]; + evt.write_to_vec(&mut v)?; + ipc_send(Data::DataPortableService(DataPortableService::Pointer(( + v, conn, + )))) + } + + fn handle_key_(evt: &KeyEvent) -> ResultType<()> { + let mut v = vec![]; + evt.write_to_vec(&mut v)?; + ipc_send(Data::DataPortableService(DataPortableService::Key(v))) + } + + pub fn create_capturer( + current_display: usize, + display: scrap::Display, + portable_service_running: bool, + ) -> ResultType> { + if portable_service_running != RUNNING.lock().unwrap().clone() { + log::info!("portable service status mismatch"); + } + if portable_service_running && display.is_primary() { + log::info!("Create shared memory capturer"); + return Ok(Box::new(CapturerPortable::new(current_display))); + } else { + log::debug!("Create capturer dxgi|gdi"); + return Ok(Box::new( + Capturer::new(display).with_context(|| "Failed to create capturer")?, + )); + } + } + + pub fn get_cursor_info(pci: PCURSORINFO) -> BOOL { + if RUNNING.lock().unwrap().clone() { + let mut option = SHMEM.lock().unwrap(); + option + .as_mut() + .map_or(FALSE, |sheme| get_cursor_info_(sheme, pci)) + } else { + unsafe { winuser::GetCursorInfo(pci) } + } + } + + pub fn handle_mouse( + evt: &MouseEvent, + conn: i32, + username: String, + argb: u32, + simulate: bool, + show_cursor: bool, + ) { + if RUNNING.lock().unwrap().clone() { + crate::input_service::update_latest_input_cursor_time(conn); + handle_mouse_(evt, conn, username, argb, simulate, show_cursor).ok(); + } else { + crate::input_service::handle_mouse_(evt, conn, username, argb, simulate, show_cursor); + } + } + + pub fn handle_pointer(evt: &PointerDeviceEvent, conn: i32) { + if RUNNING.lock().unwrap().clone() { + crate::input_service::update_latest_input_cursor_time(conn); + handle_pointer_(evt, conn).ok(); + } else { + crate::input_service::handle_pointer_(evt, conn); + } + } + + pub fn handle_key(evt: &KeyEvent) { + if RUNNING.lock().unwrap().clone() { + handle_key_(evt).ok(); + } else { + crate::input_service::handle_key_(evt); + } + } + + pub fn running() -> bool { + RUNNING.lock().unwrap().clone() + } +} + +#[repr(C)] +pub struct CapturerPara { + recreate: bool, + current_display: usize, + timeout_ms: i32, +} + +#[repr(C)] +pub struct FrameInfo { + length: usize, + width: usize, + height: usize, +} diff --git a/vendor/rustdesk/src/server/printer_service.rs b/vendor/rustdesk/src/server/printer_service.rs new file mode 100644 index 0000000..edf5f3c --- /dev/null +++ b/vendor/rustdesk/src/server/printer_service.rs @@ -0,0 +1,163 @@ +use super::service::{EmptyExtraFieldService, GenericService, Service}; +use hbb_common::{bail, dlopen::symbor::Library, log, ResultType}; +use std::{ + sync::{Arc, Mutex}, + thread, + time::Duration, +}; + +pub const NAME: &'static str = "remote-printer"; + +const LIB_NAME_PRINTER_DRIVER_ADAPTER: &str = "printer_driver_adapter"; + +// Return 0 if success, otherwise return error code. +pub type Init = fn(tag_name: *const i8) -> i32; +pub type Uninit = fn(); +// dur_mills: Get the file generated in the last `dur_mills` milliseconds. +// data: The raw prn data, xps format. +// data_len: The length of the raw prn data. +pub type GetPrnData = fn(dur_mills: u32, data: *mut *mut i8, data_len: *mut u32); +// Free the prn data allocated by GetPrnData(). +pub type FreePrnData = fn(data: *mut i8); + +macro_rules! make_lib_wrapper { + ($($field:ident : $tp:ty),+) => { + struct LibWrapper { + _lib: Option, + $($field: Option<$tp>),+ + } + + impl LibWrapper { + fn new() -> Self { + let lib_name = match get_lib_name() { + Ok(name) => name, + Err(e) => { + log::warn!("Failed to get lib name, {}", e); + return Self { + _lib: None, + $( $field: None ),+ + }; + } + }; + let lib = match Library::open(&lib_name) { + Ok(lib) => Some(lib), + Err(e) => { + log::warn!("Failed to load library {}, {}", &lib_name, e); + None + } + }; + + $(let $field = if let Some(lib) = &lib { + match unsafe { lib.symbol::<$tp>(stringify!($field)) } { + Ok(m) => { + Some(*m) + }, + Err(e) => { + log::warn!("Failed to load func {}, {}", stringify!($field), e); + None + } + } + } else { + None + };)+ + + Self { + _lib: lib, + $( $field ),+ + } + } + } + + impl Default for LibWrapper { + fn default() -> Self { + Self::new() + } + } + } +} + +make_lib_wrapper!( + init: Init, + uninit: Uninit, + get_prn_data: GetPrnData, + free_prn_data: FreePrnData +); + +lazy_static::lazy_static! { + static ref LIB_WRAPPER: Arc> = Default::default(); +} + +fn get_lib_name() -> ResultType { + let exe_file = std::env::current_exe()?; + if let Some(cur_dir) = exe_file.parent() { + let dll_name = format!("{}.dll", LIB_NAME_PRINTER_DRIVER_ADAPTER); + let full_path = cur_dir.join(dll_name); + if !full_path.exists() { + bail!("{} not found", full_path.to_string_lossy().as_ref()); + } else { + Ok(full_path.to_string_lossy().into_owned()) + } + } else { + bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ); + } +} + +pub fn init(app_name: &str) -> ResultType<()> { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + let Some(fn_init) = lib_wrapper.init.as_ref() else { + bail!("Failed to load func init"); + }; + + let tag_name = std::ffi::CString::new(app_name)?; + let ret = fn_init(tag_name.as_ptr()); + if ret != 0 { + bail!("Failed to init printer driver"); + } + Ok(()) +} + +pub fn uninit() { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + if let Some(fn_uninit) = lib_wrapper.uninit.as_ref() { + fn_uninit(); + } +} + +fn get_prn_data(dur_mills: u32) -> ResultType> { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + if let Some(fn_get_prn_data) = lib_wrapper.get_prn_data.as_ref() { + let mut data = std::ptr::null_mut(); + let mut data_len = 0u32; + fn_get_prn_data(dur_mills, &mut data, &mut data_len); + if data.is_null() || data_len == 0 { + return Ok(Vec::new()); + } + let bytes = + Vec::from(unsafe { std::slice::from_raw_parts(data as *const u8, data_len as usize) }); + lib_wrapper.free_prn_data.map(|f| f(data)); + Ok(bytes) + } else { + bail!("Failed to load func get_prn_file"); + } +} + +pub fn new(name: String) -> GenericService { + let svc = EmptyExtraFieldService::new(name, false); + GenericService::run(&svc.clone(), run); + svc.sp +} + +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + while sp.ok() { + let bytes = get_prn_data(1000)?; + if !bytes.is_empty() { + log::info!("Got prn data, data len: {}", bytes.len()); + crate::server::on_printer_data(bytes); + } + thread::sleep(Duration::from_millis(300)); + } + Ok(()) +} diff --git a/vendor/rustdesk/src/server/rdp_input.rs b/vendor/rustdesk/src/server/rdp_input.rs new file mode 100644 index 0000000..5348f2f --- /dev/null +++ b/vendor/rustdesk/src/server/rdp_input.rs @@ -0,0 +1,605 @@ +use super::input_service::set_clipboard_for_paste_sync; +use crate::uinput::service::{can_input_via_keysym, char_to_keysym, map_key}; +use dbus::{blocking::SyncConnection, Path}; +use enigo::{Key, KeyboardControllable, MouseButton, MouseControllable}; +use hbb_common::{log, ResultType}; +use scrap::wayland::pipewire::{get_portal, PwStreamInfo}; +use scrap::wayland::remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop as remote_desktop_portal; +use std::collections::HashMap; +use std::sync::Arc; + +pub mod client { + use hbb_common::platform::linux::is_kde; + + use super::*; + + const EVDEV_MOUSE_LEFT: i32 = 272; + const EVDEV_MOUSE_RIGHT: i32 = 273; + const EVDEV_MOUSE_MIDDLE: i32 = 274; + + const PRESSED_DOWN_STATE: u32 = 1; + const PRESSED_UP_STATE: u32 = 0; + + /// Modifier key state tracking for RDP input. + /// Portal API doesn't provide a way to query key state, so we track it ourselves. + #[derive(Default)] + struct ModifierState { + shift_left: bool, + shift_right: bool, + ctrl_left: bool, + ctrl_right: bool, + alt_left: bool, + alt_right: bool, + meta_left: bool, + meta_right: bool, + } + + impl ModifierState { + fn update(&mut self, key: &Key, down: bool) { + match key { + Key::Shift => self.shift_left = down, + Key::RightShift => self.shift_right = down, + Key::Control => self.ctrl_left = down, + Key::RightControl => self.ctrl_right = down, + Key::Alt => self.alt_left = down, + Key::RightAlt => self.alt_right = down, + Key::Meta | Key::Super | Key::Windows | Key::Command => self.meta_left = down, + Key::RWin => self.meta_right = down, + // Handle raw keycodes for modifier keys (Linux evdev codes + 8) + // In translate mode, modifier keys may be sent as Chr events with raw keycodes. + // The +8 offset converts evdev codes to X11/XKB keycodes. + Key::Raw(code) => { + const EVDEV_OFFSET: u16 = 8; + const KEY_LEFTSHIFT: u16 = evdev::Key::KEY_LEFTSHIFT.code() + EVDEV_OFFSET; + const KEY_RIGHTSHIFT: u16 = evdev::Key::KEY_RIGHTSHIFT.code() + EVDEV_OFFSET; + const KEY_LEFTCTRL: u16 = evdev::Key::KEY_LEFTCTRL.code() + EVDEV_OFFSET; + const KEY_RIGHTCTRL: u16 = evdev::Key::KEY_RIGHTCTRL.code() + EVDEV_OFFSET; + const KEY_LEFTALT: u16 = evdev::Key::KEY_LEFTALT.code() + EVDEV_OFFSET; + const KEY_RIGHTALT: u16 = evdev::Key::KEY_RIGHTALT.code() + EVDEV_OFFSET; + const KEY_LEFTMETA: u16 = evdev::Key::KEY_LEFTMETA.code() + EVDEV_OFFSET; + const KEY_RIGHTMETA: u16 = evdev::Key::KEY_RIGHTMETA.code() + EVDEV_OFFSET; + match *code { + KEY_LEFTSHIFT => self.shift_left = down, + KEY_RIGHTSHIFT => self.shift_right = down, + KEY_LEFTCTRL => self.ctrl_left = down, + KEY_RIGHTCTRL => self.ctrl_right = down, + KEY_LEFTALT => self.alt_left = down, + KEY_RIGHTALT => self.alt_right = down, + KEY_LEFTMETA => self.meta_left = down, + KEY_RIGHTMETA => self.meta_right = down, + _ => {} + } + } + _ => {} + } + } + } + + pub struct RdpInputKeyboard { + conn: Arc, + session: Path<'static>, + modifier_state: ModifierState, + } + + impl RdpInputKeyboard { + pub fn new(conn: Arc, session: Path<'static>) -> ResultType { + Ok(Self { + conn, + session, + modifier_state: ModifierState::default(), + }) + } + } + + impl KeyboardControllable for RdpInputKeyboard { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn get_key_state(&mut self, key: Key) -> bool { + // Use tracked modifier state for supported keys + match key { + Key::Shift => self.modifier_state.shift_left, + Key::RightShift => self.modifier_state.shift_right, + Key::Control => self.modifier_state.ctrl_left, + Key::RightControl => self.modifier_state.ctrl_right, + Key::Alt => self.modifier_state.alt_left, + Key::RightAlt => self.modifier_state.alt_right, + Key::Meta | Key::Super | Key::Windows | Key::Command => { + self.modifier_state.meta_left + } + Key::RWin => self.modifier_state.meta_right, + _ => false, + } + } + + fn key_sequence(&mut self, s: &str) { + for c in s.chars() { + let keysym = char_to_keysym(c); + // ASCII characters: use keysym + if can_input_via_keysym(c, keysym) { + if let Err(e) = send_keysym(keysym, true, self.conn.clone(), &self.session) { + log::error!("Failed to send keysym down: {:?}", e); + } + if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) { + log::error!("Failed to send keysym up: {:?}", e); + } + } else { + // Non-ASCII: use clipboard + input_text_via_clipboard(&c.to_string(), self.conn.clone(), &self.session); + } + } + } + + fn key_down(&mut self, key: Key) -> enigo::ResultType { + if let Key::Layout(chr) = key { + let keysym = char_to_keysym(chr); + // ASCII characters: use keysym + if can_input_via_keysym(chr, keysym) { + send_keysym(keysym, true, self.conn.clone(), &self.session)?; + } else { + // Non-ASCII: use clipboard (complete key press in key_down) + input_text_via_clipboard(&chr.to_string(), self.conn.clone(), &self.session); + } + } else { + handle_key(true, key.clone(), self.conn.clone(), &self.session)?; + // Update modifier state only after successful send — + // if handle_key fails, we don't want stale "pressed" state + // affecting subsequent key event decisions. + self.modifier_state.update(&key, true); + } + Ok(()) + } + + fn key_up(&mut self, key: Key) { + // Intentionally asymmetric with key_down: update state BEFORE sending. + // On release, we always mark as released even if the send fails below, + // to avoid permanently stuck-modifier state in our tracker. The trade-off + // (tracker says "released" while OS may still have it pressed) is acceptable + // because such failures are rare and subsequent events will resynchronize. + self.modifier_state.update(&key, false); + + if let Key::Layout(chr) = key { + // ASCII characters: send keysym up if we also sent it on key_down + let keysym = char_to_keysym(chr); + if can_input_via_keysym(chr, keysym) { + if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) + { + log::error!("Failed to send keysym up: {:?}", e); + } + } + // Non-ASCII: already handled completely in key_down via clipboard paste, + // no corresponding release needed (clipboard paste is an atomic operation) + } else { + if let Err(e) = handle_key(false, key, self.conn.clone(), &self.session) { + log::error!("Failed to handle key up: {:?}", e); + } + } + } + + fn key_click(&mut self, key: Key) { + if let Key::Layout(chr) = key { + let keysym = char_to_keysym(chr); + // ASCII characters: use keysym + if can_input_via_keysym(chr, keysym) { + if let Err(e) = send_keysym(keysym, true, self.conn.clone(), &self.session) { + log::error!("Failed to send keysym down: {:?}", e); + } + if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) { + log::error!("Failed to send keysym up: {:?}", e); + } + } else { + // Non-ASCII: use clipboard + input_text_via_clipboard(&chr.to_string(), self.conn.clone(), &self.session); + } + } else { + if let Err(e) = handle_key(true, key.clone(), self.conn.clone(), &self.session) { + log::error!("Failed to handle key down: {:?}", e); + } else { + // Only mark modifier as pressed if key-down was actually delivered + self.modifier_state.update(&key, true); + } + // Always mark as released to avoid stuck-modifier state + self.modifier_state.update(&key, false); + if let Err(e) = handle_key(false, key, self.conn.clone(), &self.session) { + log::error!("Failed to handle key up: {:?}", e); + } + } + } + } + + /// Input text via clipboard + Shift+Insert. + /// Shift+Insert is more universal than Ctrl+V, works in both GUI apps and terminals. + /// + /// Note: Clipboard content is NOT restored after paste - see `set_clipboard_for_paste_sync` for rationale. + fn input_text_via_clipboard(text: &str, conn: Arc, session: &Path<'static>) { + if text.is_empty() { + return; + } + if !set_clipboard_for_paste_sync(text) { + return; + } + + let portal = get_portal(&conn); + let shift_keycode = evdev::Key::KEY_LEFTSHIFT.code() as i32; + let insert_keycode = evdev::Key::KEY_INSERT.code() as i32; + + // Send Shift+Insert (universal paste shortcut) + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + session, + HashMap::new(), + shift_keycode, + PRESSED_DOWN_STATE, + ) { + log::error!("input_text_via_clipboard: failed to press Shift: {:?}", e); + return; + } + + // Press Insert + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + session, + HashMap::new(), + insert_keycode, + PRESSED_DOWN_STATE, + ) { + log::error!("input_text_via_clipboard: failed to press Insert: {:?}", e); + // Still try to release Shift. + // Note: clipboard has already been set by set_clipboard_for_paste_sync but paste + // never happened. We don't attempt to restore the previous clipboard contents + // because reading the clipboard on Wayland requires focus/permission. + let _ = remote_desktop_portal::notify_keyboard_keycode( + &portal, + session, + HashMap::new(), + shift_keycode, + PRESSED_UP_STATE, + ); + return; + } + + // Release Insert + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + session, + HashMap::new(), + insert_keycode, + PRESSED_UP_STATE, + ) { + log::error!( + "input_text_via_clipboard: failed to release Insert: {:?}", + e + ); + } + + // Release Shift + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + session, + HashMap::new(), + shift_keycode, + PRESSED_UP_STATE, + ) { + log::error!("input_text_via_clipboard: failed to release Shift: {:?}", e); + } + } + + pub struct RdpInputMouse { + conn: Arc, + session: Path<'static>, + stream: PwStreamInfo, + resolution: (usize, usize), + scale: Option, + position: (f64, f64), + } + + impl RdpInputMouse { + pub fn new( + conn: Arc, + session: Path<'static>, + stream: PwStreamInfo, + resolution: (usize, usize), + ) -> ResultType { + // https://github.com/rustdesk/rustdesk/pull/9019#issuecomment-2295252388 + // There may be a bug in Rdp input on Gnome util Ubuntu 24.04 (Gnome 46) + // + // eg. Resolution 800x600, Fractional scale: 200% (logic size: 400x300) + // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.impl.portal.RemoteDesktop.html#:~:text=new%20pointer%20position-,in%20the%20streams%20logical%20coordinate%20space,-. + // Then (x,y) in `mouse_move_to()` and `mouse_move_relative()` should be scaled to the logic size(stream.get_size()), which is from (0,0) to (400,300). + // For Ubuntu 24.04(Gnome 46), (x,y) is restricted from (0,0) to (400,300), but the actual range in screen is: + // Logic coordinate from (0,0) to (200x150). + // Or physical coordinate from (0,0) to (400,300). + let scale = if is_kde() { + if resolution.0 == 0 || stream.get_size().0 == 0 { + Some(1.0f64) + } else { + Some(resolution.0 as f64 / stream.get_size().0 as f64) + } + } else { + None + }; + let pos = stream.get_position(); + Ok(Self { + conn, + session, + stream, + resolution, + scale, + position: (pos.0 as f64, pos.1 as f64), + }) + } + } + + impl MouseControllable for RdpInputMouse { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn mouse_move_to(&mut self, x: i32, y: i32) { + let x = if let Some(s) = self.scale { + x as f64 / s + } else { + x as f64 + }; + let y = if let Some(s) = self.scale { + y as f64 / s + } else { + y as f64 + }; + let x = x - self.position.0; + let y = y - self.position.1; + let portal = get_portal(&self.conn); + let _ = remote_desktop_portal::notify_pointer_motion_absolute( + &portal, + &self.session, + HashMap::new(), + self.stream.path as u32, + x, + y, + ); + } + fn mouse_move_relative(&mut self, x: i32, y: i32) { + let x = if let Some(s) = self.scale { + x as f64 / s + } else { + x as f64 + }; + let y = if let Some(s) = self.scale { + y as f64 / s + } else { + y as f64 + }; + let portal = get_portal(&self.conn); + let _ = remote_desktop_portal::notify_pointer_motion( + &portal, + &self.session, + HashMap::new(), + x, + y, + ); + } + fn mouse_down(&mut self, button: MouseButton) -> enigo::ResultType { + handle_mouse(true, button, self.conn.clone(), &self.session); + Ok(()) + } + fn mouse_up(&mut self, button: MouseButton) { + handle_mouse(false, button, self.conn.clone(), &self.session); + } + fn mouse_click(&mut self, button: MouseButton) { + handle_mouse(true, button, self.conn.clone(), &self.session); + handle_mouse(false, button, self.conn.clone(), &self.session); + } + fn mouse_scroll_x(&mut self, length: i32) { + let portal = get_portal(&self.conn); + let _ = remote_desktop_portal::notify_pointer_axis( + &portal, + &self.session, + HashMap::new(), + length as f64, + 0 as f64, + ); + } + fn mouse_scroll_y(&mut self, length: i32) { + let portal = get_portal(&self.conn); + let _ = remote_desktop_portal::notify_pointer_axis( + &portal, + &self.session, + HashMap::new(), + 0 as f64, + length as f64, + ); + } + } + + /// Send a keysym via RemoteDesktop portal. + fn send_keysym( + keysym: i32, + down: bool, + conn: Arc, + session: &Path<'static>, + ) -> ResultType<()> { + let state: u32 = if down { + PRESSED_DOWN_STATE + } else { + PRESSED_UP_STATE + }; + let portal = get_portal(&conn); + log::trace!( + "send_keysym: calling notify_keyboard_keysym, keysym={:#x}, state={}", + keysym, + state + ); + match remote_desktop_portal::notify_keyboard_keysym( + &portal, + session, + HashMap::new(), + keysym, + state, + ) { + Ok(_) => { + log::trace!("send_keysym: notify_keyboard_keysym succeeded"); + Ok(()) + } + Err(e) => Err(e.into()), + } + } + + fn get_raw_evdev_keycode(key: u16) -> i32 { + // 8 is the offset between xkb and evdev + let mut key = key as i32 - 8; + // fix for right_meta key + if key == 126 { + key = 125; + } + key + } + + fn handle_key( + down: bool, + key: Key, + conn: Arc, + session: &Path<'static>, + ) -> ResultType<()> { + let state: u32 = if down { + PRESSED_DOWN_STATE + } else { + PRESSED_UP_STATE + }; + let portal = get_portal(&conn); + match key { + Key::Raw(key) => { + let key = get_raw_evdev_keycode(key); + remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + key, + state, + )?; + } + _ => { + if let Ok((key, is_shift)) = map_key(&key) { + let shift_keycode = evdev::Key::KEY_LEFTSHIFT.code() as i32; + if down { + // Press: Shift down first, then key down + if is_shift { + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + shift_keycode, + state, + ) { + log::error!("handle_key: failed to press Shift: {:?}", e); + return Err(e.into()); + } + } + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + key.code() as i32, + state, + ) { + log::error!("handle_key: failed to press key: {:?}", e); + // Best-effort: release Shift if it was pressed + if is_shift { + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + shift_keycode, + PRESSED_UP_STATE, + ) { + log::warn!( + "handle_key: best-effort Shift release also failed: {:?}", + e + ); + } + } + return Err(e.into()); + } + } else { + // Release: key up first, then Shift up + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + key.code() as i32, + PRESSED_UP_STATE, + ) { + log::error!("handle_key: failed to release key: {:?}", e); + // Best-effort: still try to release Shift + if is_shift { + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + shift_keycode, + PRESSED_UP_STATE, + ) { + log::warn!( + "handle_key: best-effort Shift release also failed: {:?}", + e + ); + } + } + return Err(e.into()); + } + if is_shift { + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + shift_keycode, + PRESSED_UP_STATE, + ) { + log::error!("handle_key: failed to release Shift: {:?}", e); + return Err(e.into()); + } + } + } + } + } + } + Ok(()) + } + + fn handle_mouse( + down: bool, + button: MouseButton, + conn: Arc, + session: &Path<'static>, + ) { + let portal = get_portal(&conn); + let but_key = match button { + MouseButton::Left => EVDEV_MOUSE_LEFT, + MouseButton::Right => EVDEV_MOUSE_RIGHT, + MouseButton::Middle => EVDEV_MOUSE_MIDDLE, + _ => { + return; + } + }; + let state: u32 = if down { + PRESSED_DOWN_STATE + } else { + PRESSED_UP_STATE + }; + let _ = remote_desktop_portal::notify_pointer_button( + &portal, + &session, + HashMap::new(), + but_key, + state, + ); + } +} diff --git a/vendor/rustdesk/src/server/service.rs b/vendor/rustdesk/src/server/service.rs new file mode 100644 index 0000000..dac9754 --- /dev/null +++ b/vendor/rustdesk/src/server/service.rs @@ -0,0 +1,358 @@ +use super::*; +use std::{ + collections::HashSet, + ops::{Deref, DerefMut}, + thread::{self, JoinHandle}, + time, +}; + +pub trait Service: Send + Sync { + fn name(&self) -> String; + fn on_subscribe(&self, sub: ConnInner); + fn on_unsubscribe(&self, id: i32); + fn is_subed(&self, id: i32) -> bool; + fn join(&self); + fn get_option(&self, opt: &str) -> Option; + fn set_option(&self, opt: &str, val: &str) -> Option; + fn ok(&self) -> bool; +} + +pub trait Subscriber: Default + Send + Sync + 'static { + fn id(&self) -> i32; + fn send(&mut self, msg: Arc); +} + +#[derive(Default)] +pub struct ServiceInner> { + name: String, + handle: Option>, + subscribes: HashMap, + new_subscribes: HashMap, + active: bool, + need_snapshot: bool, + options: HashMap, +} + +pub trait Reset { + fn reset(&mut self); + fn init(&mut self) {} +} + +pub struct ServiceTmpl>(Arc>>); +pub struct ServiceSwap>(ServiceTmpl); +pub type GenericService = ServiceTmpl; +pub const HIBERNATE_TIMEOUT: u64 = 30; +pub const MAX_ERROR_TIMEOUT: u64 = 1_000; +pub const SERVICE_OPTION_VALUE_TRUE: &str = "1"; +pub const SERVICE_OPTION_VALUE_FALSE: &str = "0"; + +#[derive(Clone)] +pub struct EmptyExtraFieldService { + pub sp: GenericService, +} + +impl Deref for EmptyExtraFieldService { + type Target = ServiceTmpl; + + fn deref(&self) -> &Self::Target { + &self.sp + } +} + +impl DerefMut for EmptyExtraFieldService { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sp + } +} + +impl EmptyExtraFieldService { + pub fn new(name: String, need_snapshot: bool) -> Self { + Self { + sp: GenericService::new(name, need_snapshot), + } + } +} + +impl> ServiceInner { + fn send_new_subscribes(&mut self, msg: Arc) { + for s in self.new_subscribes.values_mut() { + s.send(msg.clone()); + } + } + + fn swap_new_subscribes(&mut self) { + for (_, s) in self.new_subscribes.drain() { + self.subscribes.insert(s.id(), s); + } + debug_assert!(self.new_subscribes.is_empty()); + } + + #[inline] + fn has_subscribes(&self) -> bool { + self.subscribes.len() > 0 || self.new_subscribes.len() > 0 + } +} + +impl> Service for ServiceTmpl { + #[inline] + fn name(&self) -> String { + self.0.read().unwrap().name.clone() + } + + fn is_subed(&self, id: i32) -> bool { + self.0.read().unwrap().subscribes.get(&id).is_some() + || self.0.read().unwrap().new_subscribes.get(&id).is_some() + } + + fn on_subscribe(&self, sub: ConnInner) { + let mut lock = self.0.write().unwrap(); + if lock.subscribes.get(&sub.id()).is_some() { + return; + } + if lock.need_snapshot { + lock.new_subscribes.insert(sub.id(), sub.into()); + } else { + lock.subscribes.insert(sub.id(), sub.into()); + } + } + + fn on_unsubscribe(&self, id: i32) { + let mut lock = self.0.write().unwrap(); + if let None = lock.subscribes.remove(&id) { + lock.new_subscribes.remove(&id); + } + } + + fn join(&self) { + self.0.write().unwrap().active = false; + let handle = self.0.write().unwrap().handle.take(); + if let Some(handle) = handle { + if let Err(e) = handle.join() { + log::error!("Failed to join thread for service {}, {:?}", self.name(), e); + } + } + } + + fn get_option(&self, opt: &str) -> Option { + self.0.read().unwrap().options.get(opt).cloned() + } + + fn set_option(&self, opt: &str, val: &str) -> Option { + self.0 + .write() + .unwrap() + .options + .insert(opt.to_string(), val.to_string()) + } + + #[inline] + fn ok(&self) -> bool { + let lock = self.0.read().unwrap(); + lock.active && lock.has_subscribes() + } +} + +impl> Clone for ServiceTmpl { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl> ServiceTmpl { + pub fn new(name: String, need_snapshot: bool) -> Self { + Self(Arc::new(RwLock::new(ServiceInner:: { + name, + active: true, + need_snapshot, + ..Default::default() + }))) + } + + #[inline] + pub fn is_option_true(&self, opt: &str) -> bool { + self.get_option(opt) + .map_or(false, |v| v == SERVICE_OPTION_VALUE_TRUE) + } + + #[inline] + pub fn set_option_bool(&self, opt: &str, val: bool) { + if val { + self.set_option(opt, SERVICE_OPTION_VALUE_TRUE); + } else { + self.set_option(opt, SERVICE_OPTION_VALUE_FALSE); + } + } + + #[inline] + pub fn has_subscribes(&self) -> bool { + self.0.read().unwrap().has_subscribes() + } + + pub fn snapshot(&self, callback: F) -> ResultType<()> + where + F: FnMut(ServiceSwap) -> ResultType<()>, + { + if self.0.read().unwrap().new_subscribes.len() > 0 { + log::info!("Call snapshot of {} service", self.name()); + let mut callback = callback; + callback(ServiceSwap::(self.clone()))?; + } + Ok(()) + } + + #[inline] + pub fn send(&self, msg: Message) { + self.send_shared(Arc::new(msg)); + } + + pub fn send_to(&self, msg: Message, id: i32) { + if let Some(s) = self.0.write().unwrap().subscribes.get_mut(&id) { + s.send(Arc::new(msg)); + } + } + + pub fn send_to_others(&self, msg: Message, id: i32) { + let msg = Arc::new(msg); + let mut lock = self.0.write().unwrap(); + for (sid, s) in lock.subscribes.iter_mut() { + if *sid != id { + s.send(msg.clone()); + } + } + } + + pub fn send_shared(&self, msg: Arc) { + let mut lock = self.0.write().unwrap(); + for s in lock.subscribes.values_mut() { + s.send(msg.clone()); + } + } + + pub fn send_video_frame(&self, msg: Message) -> HashSet { + self.send_video_frame_shared(Arc::new(msg)) + } + + pub fn send_video_frame_shared(&self, msg: Arc) -> HashSet { + let mut conn_ids = HashSet::new(); + let mut lock = self.0.write().unwrap(); + for s in lock.subscribes.values_mut() { + s.send(msg.clone()); + conn_ids.insert(s.id()); + } + conn_ids + } + + pub fn send_without(&self, msg: Message, sub: i32) { + let mut lock = self.0.write().unwrap(); + let msg = Arc::new(msg); + for s in lock.subscribes.values_mut() { + if sub != s.id() { + s.send(msg.clone()); + } + } + } + + pub fn repeat(svc: &Svc, interval_ms: u64, callback: F) + where + F: 'static + FnMut(Svc, &mut S) -> ResultType<()> + Send, + S: 'static + Default + Reset, + Svc: 'static + Clone + Send + DerefMut>, + { + let interval = time::Duration::from_millis(interval_ms); + let mut callback = callback; + let sp = svc.clone(); + let thread = thread::spawn(move || { + let mut state = S::default(); + let mut may_reset = false; + while sp.active() { + let now = time::Instant::now(); + if sp.has_subscribes() { + if !may_reset { + may_reset = true; + state.init(); + } + if let Err(err) = callback(sp.clone(), &mut state) { + log::error!("Error of {} service: {}", sp.name(), err); + thread::sleep(time::Duration::from_millis(MAX_ERROR_TIMEOUT)); + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + } + } else if may_reset { + state.reset(); + may_reset = false; + } + let elapsed = now.elapsed(); + if elapsed < interval { + thread::sleep(interval - elapsed); + } + } + log::info!("Service {} exit", sp.name()); + }); + svc.0.write().unwrap().handle = Some(thread); + } + + pub fn run(svc: &Svc, callback: F) + where + F: 'static + FnMut(Svc) -> ResultType<()> + Send, + Svc: 'static + Clone + Send + DerefMut>, + { + let sp = svc.clone(); + let mut callback = callback; + let thread = thread::spawn(move || { + let mut error_timeout = HIBERNATE_TIMEOUT; + while sp.active() { + if sp.has_subscribes() { + log::debug!("Enter {} service inner loop", sp.name()); + let tm = time::Instant::now(); + if let Err(err) = callback(sp.clone()) { + log::error!("Error of {} service: {}", sp.name(), err); + if tm.elapsed() > time::Duration::from_millis(MAX_ERROR_TIMEOUT) { + error_timeout = HIBERNATE_TIMEOUT; + } else { + error_timeout *= 2; + } + if error_timeout > MAX_ERROR_TIMEOUT { + error_timeout = MAX_ERROR_TIMEOUT; + } + thread::sleep(time::Duration::from_millis(error_timeout)); + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + } else { + log::debug!("Exit {} service inner loop", sp.name()); + } + } + thread::sleep(time::Duration::from_millis(HIBERNATE_TIMEOUT)); + } + log::info!("Service {} exit", sp.name()); + }); + svc.0.write().unwrap().handle = Some(thread); + } + + #[inline] + pub fn active(&self) -> bool { + self.0.read().unwrap().active + } +} + +impl> ServiceSwap { + #[inline] + pub fn send(&self, msg: Message) { + self.send_shared(Arc::new(msg)); + } + + #[inline] + pub fn send_shared(&self, msg: Arc) { + (self.0).0.write().unwrap().send_new_subscribes(msg); + } + + #[inline] + pub fn has_subscribes(&self) -> bool { + (self.0).0.read().unwrap().subscribes.len() > 0 + } +} + +impl> Drop for ServiceSwap { + fn drop(&mut self) { + (self.0).0.write().unwrap().swap_new_subscribes(); + } +} diff --git a/vendor/rustdesk/src/server/terminal_helper.rs b/vendor/rustdesk/src/server/terminal_helper.rs new file mode 100644 index 0000000..8edf462 --- /dev/null +++ b/vendor/rustdesk/src/server/terminal_helper.rs @@ -0,0 +1,1062 @@ +//! Terminal Helper Process +//! +//! This module implements a helper process that runs as the logged-in user and creates +//! the ConPTY + Shell. This is necessary because ConPTY has compatibility issues with +//! CreateProcessAsUserW when the ConPTY is created by a different user (SYSTEM service). +//! +//! Architecture: +//! ``` +//! SYSTEM Service (terminal_service.rs) +//! | +//! +-- CreateProcessAsUserW --> Terminal Helper (this module, runs as user) +//! | | +//! | +-- CreateProcessW + ConPTY --> Shell +//! | | +//! +-- Named Pipes <----------------+ +//! ``` +//! +//! This module also contains Windows-specific utility functions used by terminal_service.rs: +//! - Named pipe creation and connection +//! - User token and SID handling +//! - Helper process launching + +use hbb_common::{ + anyhow::{anyhow, Context, Result}, + log, +}; +use portable_pty::{CommandBuilder, MasterPty, PtySize}; +use std::{ + ffi::{c_void, OsStr}, + fs::File, + io::{Read, Write}, + os::windows::{ffi::OsStrExt, io::FromRawHandle, raw::HANDLE as RawHandle}, + ptr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, + thread, + time::Duration, +}; + +use windows::{ + core::{PCWSTR, PWSTR}, + Win32::{ + Foundation::{ + CloseHandle, LocalFree, ERROR_IO_PENDING, ERROR_PIPE_CONNECTED, HANDLE, HLOCAL, + INVALID_HANDLE_VALUE, WAIT_OBJECT_0, + }, + Security::{ + Authorization::{ + SetEntriesInAclW, EXPLICIT_ACCESS_W, SET_ACCESS, TRUSTEE_IS_SID, TRUSTEE_IS_USER, + TRUSTEE_W, + }, + CreateWellKnownSid, GetLengthSid, GetTokenInformation, InitializeSecurityDescriptor, + SetSecurityDescriptorDacl, TokenUser, WinLocalSystemSid, ACE_FLAGS, ACL, + PSECURITY_DESCRIPTOR, PSID, SECURITY_ATTRIBUTES, TOKEN_USER, + }, + Storage::FileSystem::{ + CreateFileW, FILE_ALL_ACCESS, FILE_FLAGS_AND_ATTRIBUTES, FILE_FLAG_OVERLAPPED, + FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE, + OPEN_EXISTING, + }, + System::{ + Environment::{CreateEnvironmentBlock, DestroyEnvironmentBlock}, + Pipes::{ + ConnectNamedPipe, CreateNamedPipeW, PIPE_READMODE_BYTE, PIPE_TYPE_BYTE, PIPE_WAIT, + }, + Threading::{ + CreateEventW, CreateProcessAsUserW, WaitForSingleObject, CREATE_NO_WINDOW, + CREATE_UNICODE_ENVIRONMENT, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION, + STARTUPINFOW, + }, + IO::{GetOverlappedResult, OVERLAPPED}, + }, + }, +}; + +// Re-export types needed by terminal_service.rs +pub use windows::Win32::{ + Foundation::{ + CloseHandle as WinCloseHandle, HANDLE as WinHANDLE, WAIT_OBJECT_0 as WIN_WAIT_OBJECT_0, + }, + System::Threading::{ + GetExitCodeProcess as WinGetExitCodeProcess, TerminateProcess as WinTerminateProcess, + WaitForSingleObject as WinWaitForSingleObject, + }, +}; + +/// User token wrapper for cross-module use. +/// +/// Using newtype pattern for type safety. The inner value is `usize` to match +/// platform pointer size (32-bit on x86, 64-bit on x64). +/// Windows HANDLE is defined as `*mut c_void`, which has the same size as `usize`. +/// +/// # Design Note +/// This type is defined here (terminal_helper.rs) for Windows and in +/// terminal_service.rs for non-Windows platforms. This avoids circular +/// dependencies while keeping the API consistent across platforms. +/// Both definitions MUST have identical public API (new, as_raw methods). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UserToken(pub usize); + +impl UserToken { + /// Create a new UserToken from a raw handle value. + pub fn new(handle: usize) -> Self { + Self(handle) + } + + /// Get the raw handle value. + pub fn as_raw(&self) -> usize { + self.0 + } +} + +// Windows pipe access mode constants (not exported by windows crate) +const PIPE_ACCESS_INBOUND: u32 = 0x00000001; +const PIPE_ACCESS_OUTBOUND: u32 = 0x00000002; + +// Named pipe configuration constants +const PIPE_BUFFER_SIZE: u32 = 65536; // 64KB for better throughput with large terminal output +const PIPE_DEFAULT_TIMEOUT_MS: u32 = 5000; +/// Timeout for waiting for helper process to connect to pipes +pub const PIPE_CONNECTION_TIMEOUT_MS: u32 = 10000; + +/// Message type constants for helper protocol. +/// Used to distinguish between terminal data and control commands. +/// Note: Using non-zero values to make debugging easier (0x00 could indicate uninitialized memory). +pub const MSG_TYPE_DATA: u8 = 0x01; +pub const MSG_TYPE_RESIZE: u8 = 0x02; + +/// Message header size: 1 byte type + 4 bytes length +pub const MSG_HEADER_SIZE: usize = 5; + +/// Maximum payload size to prevent denial of service from malicious messages. +/// 16MB should be more than enough for any legitimate terminal data. +const MAX_PAYLOAD_SIZE: usize = 16 * 1024 * 1024; + +/// Timeout in milliseconds to wait for helper process to exit gracefully before force termination. +/// Using 500ms to allow helper process enough time to clean up, especially under high system load. +pub const HELPER_GRACEFUL_EXIT_TIMEOUT_MS: u64 = 500; + +/// Information about a launched helper process. +/// Contains both the process handle and PID for tracking and status checks. +#[derive(Debug)] +pub struct HelperProcessInfo { + /// Process handle for termination and waiting + pub handle: HANDLE, + /// Process ID for logging and status display + pub pid: u32, +} + +/// Wrapper for Windows HANDLE that implements Send. +/// This is safe because Windows HANDLEs are valid across threads. +/// Note: We only implement Send, not Sync. The handle is protected by +/// Mutex in TerminalSession, so concurrent access is controlled there. +/// +/// # Ownership and Cleanup +/// This type intentionally does NOT implement Drop. The handle is owned by +/// `TerminalSession` and explicitly closed in `TerminalSession::close_internal()` +/// after graceful shutdown logic (waiting for helper to exit, force termination if needed). +/// Implementing Drop here would interfere with that cleanup sequence. +#[derive(Debug)] +pub struct SendableHandle(HANDLE); + +impl SendableHandle { + /// Create a new SendableHandle from a raw HANDLE. + pub fn new(handle: HANDLE) -> Self { + Self(handle) + } + + /// Get the raw HANDLE value. + pub fn as_raw(&self) -> HANDLE { + self.0 + } +} + +unsafe impl Send for SendableHandle {} + +/// RAII wrapper for Windows HANDLE that automatically closes the handle on drop. +/// This ensures proper resource cleanup even when errors occur or code paths diverge. +pub struct OwnedHandle(HANDLE); + +impl OwnedHandle { + /// Create a new OwnedHandle from a raw HANDLE. + /// The handle will be closed when this OwnedHandle is dropped. + pub fn new(handle: HANDLE) -> Self { + Self(handle) + } + + /// Consume the OwnedHandle and return the raw HANDLE without closing it. + /// Use this when transferring ownership to another resource (e.g., File). + pub fn into_raw(self) -> HANDLE { + let handle = self.0; + std::mem::forget(self); // Prevent Drop from closing the handle + handle + } + + /// Get the raw HANDLE value. + pub fn as_raw(&self) -> HANDLE { + self.0 + } +} + +impl Drop for OwnedHandle { + fn drop(&mut self) { + if self.0 != INVALID_HANDLE_VALUE && !self.0.is_invalid() { + unsafe { + let _ = CloseHandle(self.0); + } + } + } +} + +/// RAII guard for helper process that terminates the process on drop. +/// This prevents helper process leaks when pipe connection fails or other errors occur. +/// +/// Unlike OwnedHandle (which only closes the handle), this guard: +/// 1. Terminates the process using TerminateProcess +/// 2. Then closes the handle +/// +/// Use `disarm()` to prevent termination when the helper is successfully handed off +/// to the terminal session for proper lifecycle management. +pub struct HelperProcessGuard { + handle: HANDLE, + pid: u32, + armed: bool, +} + +impl HelperProcessGuard { + /// Create a new guard for a helper process. + pub fn new(handle: HANDLE, pid: u32) -> Self { + Self { + handle, + pid, + armed: true, + } + } + + /// Get the raw process HANDLE. + pub fn as_raw(&self) -> HANDLE { + self.handle + } + + /// Get the process ID. + pub fn pid(&self) -> u32 { + self.pid + } + + /// Disarm the guard and return the raw HANDLE. + /// After calling this, the guard will NOT terminate the process on drop. + /// Use this when successfully handing off the helper to session management. + pub fn disarm(self) -> HANDLE { + let handle = self.handle; + std::mem::forget(self); // Prevent Drop from running + handle + } +} + +impl Drop for HelperProcessGuard { + fn drop(&mut self) { + if self.armed && self.handle != INVALID_HANDLE_VALUE && !self.handle.is_invalid() { + log::warn!( + "HelperProcessGuard: terminating leaked helper process (PID {})", + self.pid + ); + unsafe { + // Terminate the process first + let _ = WinTerminateProcess(self.handle, 1); + // Then close the handle + let _ = CloseHandle(self.handle); + } + } + } +} + +/// Encode a message for the helper protocol. +/// Format: [type: u8][length: u32 LE][payload: bytes] +pub fn encode_helper_message(msg_type: u8, payload: &[u8]) -> Vec { + let mut msg = Vec::with_capacity(MSG_HEADER_SIZE + payload.len()); + msg.push(msg_type); + msg.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + msg.extend_from_slice(payload); + msg +} + +/// Encode a resize message for the helper protocol. +/// Payload: rows (u16 LE) + cols (u16 LE) +pub fn encode_resize_message(rows: u16, cols: u16) -> Vec { + let mut payload = Vec::with_capacity(4); + payload.extend_from_slice(&rows.to_le_bytes()); + payload.extend_from_slice(&cols.to_le_bytes()); + encode_helper_message(MSG_TYPE_RESIZE, &payload) +} + +/// Get the default shell for Windows. +pub fn get_default_shell() -> String { + // Try PowerShell Core first (absolute paths only) + let pwsh_paths = [ + "pwsh.exe", + r"C:\Program Files\PowerShell\7\pwsh.exe", + r"C:\Program Files\PowerShell\6\pwsh.exe", + ]; + + for path in &pwsh_paths { + if std::path::Path::new(path).exists() { + log::debug!("Found PowerShell Core: {}", path); + return path.to_string(); + } + } + + // Try Windows PowerShell + let powershell_path = r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"; + if std::path::Path::new(powershell_path).exists() { + return powershell_path.to_string(); + } + + // Fallback to cmd.exe + std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()) +} + +/// Get the SID of the user from a token. +/// Returns a Vec containing the SID bytes. +pub fn get_user_sid_from_token(user_token: UserToken) -> Result> { + let token_handle = HANDLE(user_token.as_raw() as _); + + // First call to get required buffer size + let mut return_length = 0u32; + let _ = unsafe { GetTokenInformation(token_handle, TokenUser, None, 0, &mut return_length) }; + + if return_length == 0 { + return Err(anyhow!( + "Failed to get token information size: {}", + std::io::Error::last_os_error() + )); + } + + // Allocate buffer and get token information + let mut buffer = vec![0u8; return_length as usize]; + unsafe { + GetTokenInformation( + token_handle, + TokenUser, + Some(buffer.as_mut_ptr() as *mut c_void), + return_length, + &mut return_length, + ) + .map_err(|e| anyhow!("Failed to get token information: {}", e))?; + } + + // Extract SID from TOKEN_USER structure + let token_user = unsafe { &*(buffer.as_ptr() as *const TOKEN_USER) }; + let sid_ptr = token_user.User.Sid; + + // Get SID length and copy to owned buffer + let sid_length = unsafe { GetLengthSid(sid_ptr) }; + + if sid_length == 0 { + return Err(anyhow!("Invalid SID length")); + } + + let mut sid_buffer = vec![0u8; sid_length as usize]; + unsafe { + ptr::copy_nonoverlapping( + sid_ptr.0 as *const u8, + sid_buffer.as_mut_ptr(), + sid_length as usize, + ); + } + + Ok(sid_buffer) +} + +/// Create a restricted DACL that only allows SYSTEM and a specific user. +/// Returns a pointer to the ACL that must be freed with LocalFree. +/// +/// # Safety +/// +/// This function is safe to call, but contains internal unsafe code that relies on +/// pointer lifetime guarantees: +/// +/// - The `user_sid` slice must contain valid SID binary data. +/// - Internally, raw pointers to `system_sid_buffer` (stack-allocated) and `user_sid` +/// are stored in `TRUSTEE_W.ptstrName` fields. These pointers are only used during +/// the `SetEntriesInAclW` call, which occurs before either buffer goes out of scope. +/// - The returned ACL pointer is allocated by Windows and must be freed with `LocalFree`. +pub fn create_restricted_dacl(user_sid: &[u8]) -> Result<*mut c_void> { + // Create SYSTEM SID (well-known SID: S-1-5-18) + // SAFETY: This buffer must outlive the TRUSTEE_W structures that reference it + let mut system_sid_buffer = vec![0u8; 64]; // Max SID size + let mut system_sid_size = system_sid_buffer.len() as u32; + unsafe { + CreateWellKnownSid( + WinLocalSystemSid, + None, // No domain SID + Some(PSID(system_sid_buffer.as_mut_ptr() as *mut c_void)), + &mut system_sid_size, + ) + .map_err(|e| anyhow!("Failed to create SYSTEM SID: {}", e))?; + } + + // Build EXPLICIT_ACCESS entries for SYSTEM and user + // SAFETY: The ptstrName pointers below reference system_sid_buffer and user_sid. + // These buffers must remain valid until SetEntriesInAclW returns. + let mut explicit_access: [EXPLICIT_ACCESS_W; 2] = unsafe { std::mem::zeroed() }; + + // Entry 0: SYSTEM - full access + explicit_access[0].grfAccessPermissions = FILE_ALL_ACCESS.0; + explicit_access[0].grfAccessMode = SET_ACCESS; + explicit_access[0].grfInheritance = ACE_FLAGS(0); // No inheritance for pipes + explicit_access[0].Trustee = TRUSTEE_W { + pMultipleTrustee: ptr::null_mut(), + MultipleTrusteeOperation: Default::default(), + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_USER, + ptstrName: PWSTR::from_raw(system_sid_buffer.as_ptr() as *mut u16), + }; + + // Entry 1: User - full access + explicit_access[1].grfAccessPermissions = FILE_ALL_ACCESS.0; + explicit_access[1].grfAccessMode = SET_ACCESS; + explicit_access[1].grfInheritance = ACE_FLAGS(0); // No inheritance for pipes + // SAFETY: When TrusteeForm is TRUSTEE_IS_SID, ptstrName is interpreted as a PSID + // pointer, not a string pointer. The Windows API reuses this field for different + // purposes based on TrusteeForm. The SID binary data in user_sid is valid for + // the duration of this function call (until SetEntriesInAclW returns). + explicit_access[1].Trustee = TRUSTEE_W { + pMultipleTrustee: ptr::null_mut(), + MultipleTrusteeOperation: Default::default(), + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_USER, + ptstrName: PWSTR::from_raw(user_sid.as_ptr() as *mut u16), + }; + + // Create ACL from explicit access entries + // After this call returns, system_sid_buffer and user_sid are no longer needed + let mut new_acl: *mut ACL = ptr::null_mut(); + let result = unsafe { + SetEntriesInAclW( + Some(&explicit_access), + None, // No existing ACL + &mut new_acl, + ) + }; + + if result.0 != 0 { + return Err(anyhow!( + "SetEntriesInAclW failed with error code: {}", + result.0 + )); + } + + if new_acl.is_null() { + return Err(anyhow!("SetEntriesInAclW returned null ACL")); + } + + Ok(new_acl as *mut c_void) +} + +/// Create a named pipe with a restricted DACL. +/// Only SYSTEM and the specified user can access the pipe. +/// +/// # Arguments +/// * `pipe_name` - The name of the pipe to create +/// * `for_input` - True if service writes to this pipe (helper reads), false otherwise +/// * `user_token` - Required user token for creating restricted DACL +/// +/// # Security +/// +/// The restricted DACL limits pipe access to: +/// - SYSTEM account (the service) +/// - The specific user whose token was provided (the helper process) +/// +/// This function requires a valid user_token and will fail if DACL creation fails, +/// rather than falling back to a less secure NULL DACL. +pub fn create_named_pipe_server( + pipe_name: &str, + for_input: bool, + user_token: UserToken, +) -> Result { + // SECURITY_DESCRIPTOR minimum length is 40 bytes on x64. + const SD_BUFFER_SIZE: usize = 64; + const _: () = assert!( + SD_BUFFER_SIZE >= 40, + "SD_BUFFER_SIZE must be at least 40 bytes for SECURITY_DESCRIPTOR" + ); + + let mut sd_buffer = [0u8; SD_BUFFER_SIZE]; + let sd_ptr = PSECURITY_DESCRIPTOR(sd_buffer.as_mut_ptr() as *mut c_void); + + // Initialize security descriptor + unsafe { + InitializeSecurityDescriptor(sd_ptr, 1) + .map_err(|e| anyhow!("Failed to initialize security descriptor: {}", e))?; + } + + // Create restricted DACL - fail if this doesn't work (no NULL DACL fallback) + let user_sid = get_user_sid_from_token(user_token) + .context("Failed to get user SID from token for pipe DACL")?; + let acl_ptr = + create_restricted_dacl(&user_sid).context("Failed to create restricted DACL for pipe")?; + + log::debug!("Created restricted DACL for pipe: {}", pipe_name); + + // Set DACL on security descriptor + unsafe { + SetSecurityDescriptorDacl(sd_ptr, true, Some(acl_ptr as *const _ as *const _), false) + .map_err(|e| { + // Clean up ACL on error (ignore result - cleanup is best-effort, original error takes precedence) + let _ = LocalFree(Some(HLOCAL(acl_ptr))); + anyhow!("Failed to set restricted DACL: {}", e) + })?; + } + + let sa = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: sd_buffer.as_mut_ptr() as *mut c_void, + bInheritHandle: false.into(), + }; + + let wide_name: Vec = OsStr::new(pipe_name) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + let access_mode = if for_input { + FILE_FLAGS_AND_ATTRIBUTES(PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED.0) + } else { + FILE_FLAGS_AND_ATTRIBUTES(PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED.0) + }; + + log::debug!( + "Creating named pipe: {} (for_input={}, restricted_dacl=true)", + pipe_name, + for_input + ); + + let handle = unsafe { + CreateNamedPipeW( + PCWSTR::from_raw(wide_name.as_ptr()), + access_mode, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + 1, // max instances + PIPE_BUFFER_SIZE, + PIPE_BUFFER_SIZE, + PIPE_DEFAULT_TIMEOUT_MS, + Some(&sa), + ) + }; + + // Clean up ACL after pipe creation (security descriptor has been applied) + // Ignore result: LocalFree failure is non-critical since the pipe is already created + unsafe { + let _ = LocalFree(Some(HLOCAL(acl_ptr))); + } + + if handle == INVALID_HANDLE_VALUE { + return Err(anyhow!( + "Failed to create named pipe {}: {}", + pipe_name, + std::io::Error::last_os_error() + )); + } + + log::debug!("Named pipe created: {}", pipe_name); + Ok(handle) +} + +/// Wait for client to connect to named pipe with timeout. +/// +/// # Ownership +/// This function **takes ownership** of the `pipe_handle` via OwnedHandle: +/// - On success: the handle is extracted and wrapped in a `File`. +/// - On failure: the handle is automatically closed when OwnedHandle drops. +pub fn wait_for_pipe_connection( + pipe_handle: OwnedHandle, + pipe_name: &str, + timeout_ms: u32, +) -> Result { + log::debug!("Waiting for pipe connection: {}", pipe_name); + + // Create an event for overlapped I/O (also wrapped in OwnedHandle for RAII) + let event = unsafe { CreateEventW(None, true, false, PCWSTR::null()) } + .map_err(|e| anyhow!("Failed to create event for pipe connection: {}", e))?; + let event_handle = OwnedHandle::new(event); + + let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() }; + overlapped.hEvent = event_handle.as_raw(); + + let result = unsafe { ConnectNamedPipe(pipe_handle.as_raw(), Some(&mut overlapped)) }; + if result.is_err() { + let err = std::io::Error::last_os_error(); + let err_code = err.raw_os_error().unwrap_or(0); + + // ERROR_PIPE_CONNECTED means client already connected, which is OK + if err_code == ERROR_PIPE_CONNECTED.0 as i32 { + log::debug!("Pipe already connected: {}", pipe_name); + return Ok(unsafe { File::from_raw_handle(pipe_handle.into_raw().0 as RawHandle) }); + } + + // ERROR_IO_PENDING means we need to wait + if err_code == ERROR_IO_PENDING.0 as i32 { + log::debug!("Pipe connection pending, waiting with timeout..."); + let wait_result = unsafe { WaitForSingleObject(event_handle.as_raw(), timeout_ms) }; + + if wait_result != WAIT_OBJECT_0 { + log::error!("Timeout waiting for pipe connection: {}", pipe_name); + return Err(anyhow!( + "Timeout waiting for pipe connection: {}", + pipe_name + )); + } + + // Check if connection was successful + let mut bytes_transferred = 0u32; + let overlapped_result = unsafe { + GetOverlappedResult( + pipe_handle.as_raw(), + &overlapped, + &mut bytes_transferred, + false, + ) + }; + if overlapped_result.is_err() { + let err = std::io::Error::last_os_error(); + log::error!("Failed to complete pipe connection {}: {}", pipe_name, err); + return Err(anyhow!( + "Failed to complete pipe connection {}: {}", + pipe_name, + err + )); + } + + log::debug!("Pipe connected: {}", pipe_name); + } else { + log::error!("Failed to connect named pipe {}: {}", pipe_name, err); + return Err(anyhow!( + "Failed to connect named pipe {}: {}", + pipe_name, + err + )); + } + } else { + log::debug!("Pipe connected immediately: {}", pipe_name); + } + + // Success: transfer pipe ownership to File, event_handle drops + Ok(unsafe { File::from_raw_handle(pipe_handle.into_raw().0 as RawHandle) }) +} + +/// Launch terminal helper process as the logged-in user using the provided token. +/// The helper process creates ConPTY and shell, communicating via named pipes. +/// This uses CreateProcessAsUserW directly with the user token, which works because +/// the helper process itself doesn't need ConPTY - it creates ConPTY internally. +/// +/// Returns HelperProcessInfo containing the process handle and PID. + +/// RAII guard for environment block cleanup. +/// Ensures DestroyEnvironmentBlock is called even if an error occurs. +struct EnvironmentBlockGuard { + ptr: *mut c_void, +} + +impl Drop for EnvironmentBlockGuard { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { + // Ignore result: DestroyEnvironmentBlock failure is non-critical during cleanup + let _ = DestroyEnvironmentBlock(self.ptr); + } + } + } +} + +pub fn launch_terminal_helper_with_token( + user_token: UserToken, + input_pipe_name: &str, + output_pipe_name: &str, + terminal_id: i32, + rows: u16, + cols: u16, +) -> Result { + let exe_path = + std::env::current_exe().map_err(|e| anyhow!("Failed to get current exe path: {}", e))?; + + // Build command line arguments (without exe path to avoid escaping issues) + // lpApplicationName will contain the exe path separately + let cmd_args = format!( + "--terminal-helper {} {} {} {} {}", + input_pipe_name, output_pipe_name, rows, cols, terminal_id + ); + + log::debug!("Launching terminal helper for terminal {}", terminal_id); + + // Convert exe path to wide string for lpApplicationName + let exe_path_wide: Vec = OsStr::new(exe_path.as_os_str()) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + // Command line must include exe name as first argument per Windows convention + let cmd_line = format!("\"{}\" {}", exe_path.display(), cmd_args); + let mut cmd_wide: Vec = OsStr::new(&cmd_line) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() }; + si.cb = std::mem::size_of::() as u32; + + let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() }; + + // Create environment block for the user with RAII cleanup + let mut environment: *mut c_void = ptr::null_mut(); + let env_ok = unsafe { + CreateEnvironmentBlock( + &mut environment, + Some(HANDLE(user_token.as_raw() as _)), + true, + ) + } + .is_ok(); + + // Use RAII guard to ensure cleanup even on error paths + let _env_guard = if env_ok && !environment.is_null() { + Some(EnvironmentBlockGuard { ptr: environment }) + } else { + if !env_ok { + log::warn!("Failed to create environment block, using default"); + } + None + }; + + let creation_flags = CREATE_NO_WINDOW + | if env_ok { + CREATE_UNICODE_ENVIRONMENT + } else { + PROCESS_CREATION_FLAGS(0) + }; + + // Use lpApplicationName to pass exe path separately from command line + // This avoids potential issues with special characters in the exe path + let result = unsafe { + CreateProcessAsUserW( + Some(HANDLE(user_token.as_raw() as _)), + PCWSTR::from_raw(exe_path_wide.as_ptr()), // lpApplicationName: exe path + Some(PWSTR::from_raw(cmd_wide.as_mut_ptr())), // lpCommandLine: full command + None, + None, + false, // Don't inherit handles + creation_flags, + if env_ok { Some(environment) } else { None }, + PCWSTR::null(), // Use default current directory + &si, + &mut pi, + ) + }; + + // Environment block cleanup is handled by _env_guard's Drop + + if let Err(e) = result { + log::error!("CreateProcessAsUserW failed: {}", e); + return Err(anyhow!("Failed to launch terminal helper: {}", e)); + } + + // Close thread handle - we only need the process handle for tracking + // Ignore result: CloseHandle failure here is non-critical since process is already launched + unsafe { + let _ = CloseHandle(pi.hThread); + } + + log::info!("Terminal helper launched with PID {}", pi.dwProcessId); + // Return process info for tracking + Ok(HelperProcessInfo { + handle: pi.hProcess, + pid: pi.dwProcessId, + }) +} + +/// Check if a helper process is still running. +/// Returns true if the process is running, false if it has exited. +pub fn is_helper_process_running(handle: HANDLE) -> bool { + let wait_result = unsafe { WaitForSingleObject(handle, 0) }; + // WAIT_TIMEOUT (258) means process is still running + // WAIT_OBJECT_0 (0) means process has exited + wait_result != WAIT_OBJECT_0 +} + +/// Run terminal helper process +/// Args: --terminal-helper +pub fn run_terminal_helper(args: &[String]) -> Result<()> { + if args.len() < 5 { + return Err(anyhow!( + "Usage: --terminal-helper " + )); + } + + let input_pipe_name = &args[0]; + let output_pipe_name = &args[1]; + let rows: u16 = args[2] + .parse() + .map_err(|e| anyhow!("Failed to parse rows '{}': {}", args[2], e))?; + let cols: u16 = args[3] + .parse() + .map_err(|e| anyhow!("Failed to parse cols '{}': {}", args[3], e))?; + let terminal_id: i32 = args[4] + .parse() + .map_err(|e| anyhow!("Failed to parse terminal_id '{}': {}", args[4], e))?; + + log::debug!( + "Terminal helper starting: terminal_id={}, size={}x{}", + terminal_id, + cols, + rows + ); + + // Open named pipes (created by the service) + let mut input_pipe = open_pipe(input_pipe_name, true)?; + let mut output_pipe = open_pipe(output_pipe_name, false)?; + + // Create ConPTY and shell + let pty_size = PtySize { + rows, + cols, + pixel_width: 0, + pixel_height: 0, + }; + + let pty_system = portable_pty::native_pty_system(); + let pty_pair = pty_system.openpty(pty_size).context("Failed to open PTY")?; + + let shell = get_default_shell(); + log::debug!("Using shell: {}", shell); + + let cmd = CommandBuilder::new(&shell); + let mut child = pty_pair + .slave + .spawn_command(cmd) + .context("Failed to spawn shell")?; + + // Explicitly drop slave after spawning to release resources + drop(pty_pair.slave); + + let pid = child.process_id().unwrap_or(0); + log::debug!("Shell started with PID: {}", pid); + + let mut pty_writer = pty_pair + .master + .take_writer() + .context("Failed to get PTY writer")?; + + let mut pty_reader = pty_pair + .master + .try_clone_reader() + .context("Failed to get PTY reader")?; + + // Wrap pty_pair.master in Arc for sharing with input thread (for resize). + let pty_master: Arc>> = Arc::new(Mutex::new(pty_pair.master)); + + let exiting = Arc::new(AtomicBool::new(false)); + + // Thread: Read from input pipe, parse messages, write data to PTY or handle control commands + let exiting_clone = exiting.clone(); + let pty_master_clone = pty_master.clone(); + let input_thread = thread::spawn(move || { + let mut input_pipe = input_pipe; + let mut header_buf = [0u8; MSG_HEADER_SIZE]; + let mut payload_buf = vec![0u8; 4096]; + + loop { + if exiting_clone.load(Ordering::SeqCst) { + break; + } + + // Read message header + match read_exact_or_eof(&mut input_pipe, &mut header_buf) { + Ok(false) => { + log::debug!("Input pipe EOF"); + break; + } + Ok(true) => {} + Err(e) => { + log::error!("Input pipe header read error: {}", e); + break; + } + } + + let msg_type = header_buf[0]; + let payload_len = + u32::from_le_bytes([header_buf[1], header_buf[2], header_buf[3], header_buf[4]]) + as usize; + + // Validate payload length to prevent denial of service + if payload_len > MAX_PAYLOAD_SIZE { + log::error!( + "Payload too large: {} bytes (max {})", + payload_len, + MAX_PAYLOAD_SIZE + ); + break; + } + + // Ensure payload buffer is large enough + if payload_buf.len() < payload_len { + payload_buf.resize(payload_len, 0); + } + + // Read payload + if payload_len > 0 { + match read_exact_or_eof(&mut input_pipe, &mut payload_buf[..payload_len]) { + Ok(false) => { + log::debug!("Input pipe EOF during payload read"); + break; + } + Ok(true) => {} + Err(e) => { + log::error!("Input pipe payload read error: {}", e); + break; + } + } + } + + match msg_type { + MSG_TYPE_DATA => { + // Write terminal data to PTY + if let Err(e) = pty_writer.write_all(&payload_buf[..payload_len]) { + log::error!("PTY write error: {}", e); + break; + } + if let Err(e) = pty_writer.flush() { + log::error!("PTY flush error: {}", e); + break; + } + } + MSG_TYPE_RESIZE => { + if payload_len >= 4 { + let rows = u16::from_le_bytes([payload_buf[0], payload_buf[1]]); + let cols = u16::from_le_bytes([payload_buf[2], payload_buf[3]]); + log::debug!("Resize: {}x{}", cols, rows); + if let Ok(master) = pty_master_clone.lock() { + let _ = master.resize(PtySize { + rows, + cols, + pixel_width: 0, + pixel_height: 0, + }); + } + } + } + _ => { + // Unknown type may indicate data corruption - stop to avoid parse errors + log::error!("Unknown message type: {}, terminating", msg_type); + break; + } + } + } + log::debug!("Input thread exiting"); + }); + + // Thread: Read from PTY, write to output pipe + let exiting_clone = exiting.clone(); + let output_thread = thread::spawn(move || { + let mut output_pipe = output_pipe; + let mut buf = vec![0u8; 4096]; + loop { + if exiting_clone.load(Ordering::SeqCst) { + break; + } + match pty_reader.read(&mut buf) { + Ok(0) => { + log::debug!("PTY EOF"); + break; + } + Ok(n) => { + if let Err(e) = output_pipe.write_all(&buf[..n]) { + log::error!("Output pipe write error: {}", e); + break; + } + if let Err(e) = output_pipe.flush() { + log::error!("Output pipe flush error: {}", e); + break; + } + } + Err(e) => { + if e.kind() != std::io::ErrorKind::WouldBlock { + log::error!("PTY read error: {}", e); + break; + } + thread::sleep(Duration::from_millis(10)); + } + } + } + log::debug!("Output thread exiting"); + }); + + // Wait for child process to exit + let exit_status = child.wait(); + log::info!("Shell exited: {:?}", exit_status); + + exiting.store(true, Ordering::SeqCst); + + // Wait for threads + let _ = input_thread.join(); + let _ = output_thread.join(); + + // pty_master will be dropped here, releasing PTY resources + drop(pty_master); + + log::info!("Terminal helper exiting"); + Ok(()) +} + +/// Read exactly `buf.len()` bytes from reader. +/// Returns Ok(true) if successful, Ok(false) on EOF, Err on error. +fn read_exact_or_eof(reader: &mut R, buf: &mut [u8]) -> std::io::Result { + let mut pos = 0; + while pos < buf.len() { + match reader.read(&mut buf[pos..]) { + Ok(0) => return Ok(false), // EOF + Ok(n) => pos += n, + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + } + } + Ok(true) +} + +/// Open a named pipe as a client. +/// `for_read`: true for reading (input pipe), false for writing (output pipe). +fn open_pipe(pipe_name: &str, for_read: bool) -> Result { + let wide_name: Vec = OsStr::new(pipe_name) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + let access = if for_read { + FILE_GENERIC_READ.0 + } else { + FILE_GENERIC_WRITE.0 + }; + + let handle = unsafe { + CreateFileW( + PCWSTR::from_raw(wide_name.as_ptr()), + access, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES(0), + None, + ) + }; + + match handle { + Ok(h) => Ok(unsafe { File::from_raw_handle(h.0 as _) }), + Err(e) => Err(anyhow!( + "Failed to open {} pipe '{}': {}", + if for_read { "input" } else { "output" }, + pipe_name, + e + )), + } +} diff --git a/vendor/rustdesk/src/server/terminal_service.rs b/vendor/rustdesk/src/server/terminal_service.rs new file mode 100644 index 0000000..fb6b4fd --- /dev/null +++ b/vendor/rustdesk/src/server/terminal_service.rs @@ -0,0 +1,1847 @@ +use super::*; +use hbb_common::{ + anyhow::{anyhow, Context, Result}, + compress, +}; +use portable_pty::{Child, CommandBuilder, PtySize}; +use std::{ + collections::{HashMap, VecDeque}, + io::{Read, Write}, + ops::{Deref, DerefMut}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{self, Receiver, SyncSender}, + Arc, Mutex, + }, + thread, + time::{Duration, Instant}, +}; + +// Windows-specific imports from terminal_helper module +#[cfg(target_os = "windows")] +use super::terminal_helper::{ + create_named_pipe_server, encode_helper_message, encode_resize_message, + is_helper_process_running, launch_terminal_helper_with_token, wait_for_pipe_connection, + HelperProcessGuard, OwnedHandle, SendableHandle, WinCloseHandle, WinTerminateProcess, + WinWaitForSingleObject, MSG_TYPE_DATA, PIPE_CONNECTION_TIMEOUT_MS, WIN_WAIT_OBJECT_0, +}; + +const MAX_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; // 1MB per terminal +const MAX_BUFFER_LINES: usize = 10000; +const MAX_SERVICES: usize = 100; // Maximum number of persistent terminal services +const SERVICE_IDLE_TIMEOUT: Duration = Duration::from_secs(3600); // 1 hour idle timeout +const CHANNEL_BUFFER_SIZE: usize = 500; // Channel buffer size. Max per-message size ~4KB (reader buffer), so worst case ~500*4KB ≈ 2MB/terminal. Increased from 100 to reduce data loss during disconnects. +const COMPRESS_THRESHOLD: usize = 512; // Compress terminal data larger than this + // Default max bytes for reconnection buffer replay. +const DEFAULT_RECONNECT_BUFFER_BYTES: usize = 8 * 1024; +const MAX_SIGWINCH_PHASE_ATTEMPTS: u8 = 3; // Max attempts per SIGWINCH phase before giving up + +/// Two-phase SIGWINCH trigger for TUI app redraw on reconnection. +/// +/// Why two phases? A single resize-then-restore done back-to-back is too fast: +/// by the time the TUI app handles the asynchronous SIGWINCH signal and calls +/// `ioctl(TIOCGWINSZ)`, the PTY size has already been restored to the original. +/// ncurses sees no size change and skips the full redraw. +/// +/// Splitting across two `read_outputs()` calls (~30ms apart) ensures the app +/// sees a real size change on each SIGWINCH, forcing a complete redraw. +#[derive(Debug, Clone)] +enum SigwinchPhase { + /// No SIGWINCH needed. + Idle, + /// Phase 1: Resize PTY to temp dimensions (rows±1). The app handles SIGWINCH + /// and redraws at the temporary size. + TempResize { retries: u8 }, + /// Phase 2: Restore PTY to correct dimensions. The app handles SIGWINCH, + /// detects the size change, and performs a full redraw at the correct size. + Restore { retries: u8 }, +} + +/// Which resize to perform in the two-phase SIGWINCH sequence. +enum SigwinchAction { + /// Phase 1: resize to temp dimensions (rows±1) to trigger SIGWINCH with a visible size change. + TempResize, + /// Phase 2: restore to correct dimensions to trigger SIGWINCH and force full redraw. + Restore, +} + +/// Session state machine for terminal streaming. +#[derive(Debug)] +enum SessionState { + /// Session is closed, not streaming data to client. + Closed, + /// Session is active, streaming data to client. + /// pending_buffer: historical buffer to send before real-time data (set on reconnection). + /// sigwinch: two-phase SIGWINCH trigger state for TUI app redraw. + Active { + pending_buffer: Option>, + sigwinch: SigwinchPhase, + }, +} + +lazy_static::lazy_static! { + // Global registry of persistent terminal services indexed by service_id + static ref TERMINAL_SERVICES: Arc>>>> = + Arc::new(Mutex::new(HashMap::new())); + + // Cleanup task handle + static ref CLEANUP_TASK: Arc>>> = Arc::new(Mutex::new(None)); + + // List of terminal child processes to check for zombies + static ref TERMINAL_TASKS: Arc>>> = Arc::new(Mutex::new(Vec::new())); +} + +/// Service metadata that is sent to clients +#[derive(Clone, Debug)] +pub struct ServiceMetadata { + pub service_id: String, + pub created_at: Instant, + pub terminal_count: usize, + pub is_persistent: bool, +} + +/// Generate a new persistent service ID +pub fn generate_service_id() -> String { + format!("ts_{}", uuid::Uuid::new_v4()) +} + +fn get_default_shell() -> String { + #[cfg(target_os = "windows")] + { + // Use shared implementation from terminal_helper + super::terminal_helper::get_default_shell() + } + #[cfg(not(target_os = "windows"))] + { + // First try the SHELL environment variable + if let Ok(shell) = std::env::var("SHELL") { + if !shell.is_empty() { + return shell; + } + } + + // Check for common shells in order of preference + let shells = ["/bin/bash", "/bin/zsh", "/bin/sh"]; + for shell in &shells { + if std::path::Path::new(shell).exists() { + return shell.to_string(); + } + } + + // Final fallback to /bin/sh which should exist on all POSIX systems + "/bin/sh".to_string() + } +} + +pub fn is_service_specified_user(service_id: &str) -> Option { + get_service(service_id).map(|s| s.lock().unwrap().is_specified_user) +} + +/// Get or create a persistent terminal service +fn get_or_create_service( + service_id: String, + is_persistent: bool, + is_specified_user: bool, +) -> Result>> { + let mut services = TERMINAL_SERVICES.lock().unwrap(); + + // Check service limit + if !services.contains_key(&service_id) && services.len() >= MAX_SERVICES { + return Err(anyhow!( + "Maximum number of terminal services ({}) reached", + MAX_SERVICES + )); + } + + let service = services + .entry(service_id.clone()) + .or_insert_with(|| { + log::info!( + "Creating new terminal service: {} (persistent: {})", + service_id, + is_persistent + ); + Arc::new(Mutex::new(PersistentTerminalService::new( + service_id.clone(), + is_persistent, + is_specified_user, + ))) + }) + .clone(); + + // Ensure cleanup task is running + ensure_cleanup_task(); + + service.lock().unwrap().reset_status(is_persistent); + + Ok(service) +} + +/// Remove a service from the global registry +fn remove_service(service_id: &str) { + let mut services = TERMINAL_SERVICES.lock().unwrap(); + if let Some(service) = services.remove(service_id) { + log::info!("Removed service: {}", service_id); + // Close all terminals in the service + let sessions = service.lock().unwrap().sessions.clone(); + for (_, session) in sessions.iter() { + let mut session = session.lock().unwrap(); + session.stop(); + } + } +} + +/// List all active terminal services +pub fn list_services() -> Vec { + let services = TERMINAL_SERVICES.lock().unwrap(); + services + .iter() + .filter_map(|(id, service)| { + service.lock().ok().map(|svc| ServiceMetadata { + service_id: id.clone(), + created_at: svc.created_at, + terminal_count: svc.sessions.len(), + is_persistent: svc.is_persistent, + }) + }) + .collect() +} + +/// Get service by ID +pub fn get_service(service_id: &str) -> Option>> { + let services = TERMINAL_SERVICES.lock().unwrap(); + services.get(service_id).cloned() +} + +/// Clean up inactive services +pub fn cleanup_inactive_services() { + let services = TERMINAL_SERVICES.lock().unwrap(); + let now = Instant::now(); + let mut to_remove = Vec::new(); + + for (service_id, service) in services.iter() { + if let Ok(svc) = service.lock() { + // Remove non-persistent services after idle timeout + if !svc.is_persistent && now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT { + to_remove.push(service_id.clone()); + log::info!("Cleaning up idle non-persistent service: {}", service_id); + } + // Remove persistent services with no active terminals after longer timeout + else if svc.is_persistent + && svc.sessions.is_empty() + && now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT * 2 + { + to_remove.push(service_id.clone()); + log::info!("Cleaning up empty persistent service: {}", service_id); + } + } + } + + // Remove outside of iteration to avoid deadlock + drop(services); + for id in to_remove { + remove_service(&id); + } +} + +/// Add a child process to the zombie reaper +fn add_to_reaper(child: Box) { + if let Ok(mut tasks) = TERMINAL_TASKS.lock() { + tasks.push(child); + } +} + +/// Check and reap zombie terminal processes +fn check_zombie_terminals() { + let mut tasks = match TERMINAL_TASKS.lock() { + Ok(t) => t, + Err(_) => return, + }; + + let mut i = 0; + while i < tasks.len() { + match tasks[i].try_wait() { + Ok(Some(_)) => { + // Process has exited, remove it + log::info!("Process exited: {:?}", tasks[i].process_id()); + tasks.remove(i); + } + Ok(None) => { + // Still running + i += 1; + } + Err(err) => { + // Error checking status, remove it + log::info!( + "Process exited with error: {:?}, err: {err}", + tasks[i].process_id() + ); + tasks.remove(i); + } + } + } +} + +/// Ensure the cleanup task is running +fn ensure_cleanup_task() { + let mut task_handle = CLEANUP_TASK.lock().unwrap(); + if task_handle.is_none() { + let handle = std::thread::spawn(|| { + log::info!("Started cleanup task"); + let mut last_service_cleanup = Instant::now(); + loop { + // Check for zombie processes every 100ms + check_zombie_terminals(); + + // Check for inactive services every 5 minutes + if last_service_cleanup.elapsed() > Duration::from_secs(300) { + cleanup_inactive_services(); + last_service_cleanup = Instant::now(); + } + + std::thread::sleep(Duration::from_millis(100)); + } + }); + *task_handle = Some(handle); + } +} + +#[cfg(target_os = "linux")] +pub fn get_terminal_session_count(include_zombie_tasks: bool) -> usize { + let mut c = TERMINAL_SERVICES.lock().unwrap().len(); + if include_zombie_tasks { + c += TERMINAL_TASKS.lock().unwrap().len(); + } + c +} + +/// User token wrapper for cross-module use. +/// +/// # Design Note +/// On Windows, this type is defined in terminal_helper.rs and re-exported here. +/// On non-Windows platforms, it's defined here directly. +/// This design avoids circular dependencies while keeping the API consistent. +/// Both definitions MUST have identical public API (new, as_raw methods). +#[cfg(not(target_os = "windows"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UserToken(pub usize); + +#[cfg(not(target_os = "windows"))] +impl UserToken { + pub fn new(handle: usize) -> Self { + Self(handle) + } + + pub fn as_raw(&self) -> usize { + self.0 + } +} + +#[cfg(target_os = "windows")] +pub use super::terminal_helper::UserToken; + +#[derive(Clone)] +pub struct TerminalService { + sp: GenericService, + user_token: Option, +} + +impl Deref for TerminalService { + type Target = ServiceTmpl; + + fn deref(&self) -> &Self::Target { + &self.sp + } +} + +impl DerefMut for TerminalService { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sp + } +} + +pub fn get_service_name(source: VideoSource, idx: usize) -> String { + format!("{}{}", source.service_name_prefix(), idx) +} + +pub fn new( + service_id: String, + is_persistent: bool, + user_token: Option, +) -> GenericService { + // Create the service with initial persistence setting + allow_err!(get_or_create_service( + service_id.clone(), + is_persistent, + user_token.is_some() + )); + let svc = TerminalService { + sp: GenericService::new(service_id.clone(), false), + user_token, + }; + GenericService::run(&svc.clone(), move |sp| run(sp, service_id.clone())); + svc.sp +} + +fn run(sp: TerminalService, service_id: String) -> ResultType<()> { + while sp.ok() { + let responses = TerminalServiceProxy::new(service_id.clone(), None, sp.user_token.clone()) + .read_outputs(); + for response in responses { + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + sp.send(msg_out); + } + + thread::sleep(Duration::from_millis(30)); // Read at ~33fps for responsive terminal + } + + // Clean up non-persistent service when loop exits + if let Some(service) = get_service(&service_id) { + let should_remove = !service.lock().unwrap().is_persistent; + if should_remove { + remove_service(&service_id); + } + } + + Ok(()) +} + +/// Output buffer for terminal session +struct OutputBuffer { + lines: VecDeque>, + total_size: usize, + last_line_incomplete: bool, +} + +impl OutputBuffer { + fn new() -> Self { + Self { + lines: VecDeque::new(), + total_size: 0, + last_line_incomplete: false, + } + } + + fn append(&mut self, data: &[u8]) { + if data.is_empty() { + return; + } + + // Handle incomplete lines + let mut start = 0; + if self.last_line_incomplete { + if let Some(last_line) = self.lines.back_mut() { + // Find first newline in new data + if let Some(newline_pos) = data.iter().position(|&b| b == b'\n') { + last_line.extend_from_slice(&data[..=newline_pos]); + start = newline_pos + 1; + self.last_line_incomplete = false; + } else { + // Still no newline, append all + last_line.extend_from_slice(data); + self.total_size += data.len(); + return; + } + } + } + + // Process remaining data + let remaining = &data[start..]; + let ends_with_newline = remaining.last() == Some(&b'\n'); + + // Split by lines + let lines: Vec<&[u8]> = remaining.split(|&b| b == b'\n').collect(); + + for (i, line) in lines.iter().enumerate() { + if i == lines.len() - 1 && !ends_with_newline && !line.is_empty() { + // Last line without newline + self.last_line_incomplete = true; + } + + if !line.is_empty() || i < lines.len() - 1 { + let mut line_data = line.to_vec(); + if i < lines.len() - 1 || ends_with_newline { + line_data.push(b'\n'); + } + + self.total_size += line_data.len(); + self.lines.push_back(line_data); + } + } + + // Trim old data if buffer is too large + while self.total_size > MAX_OUTPUT_BUFFER_SIZE || self.lines.len() > MAX_BUFFER_LINES { + if let Some(removed) = self.lines.pop_front() { + self.total_size -= removed.len(); + } + } + } + + fn get_recent(&self, max_bytes: usize) -> Vec { + if max_bytes == 0 { + return Vec::new(); + } + let mut chunks: Vec<&[u8]> = Vec::new(); + let mut size = 0; + + // Collect whole chunks from newest to oldest, preserving chronological continuity. + // If the newest chunk alone exceeds max_bytes, take its tail (truncation may split + // an ANSI escape, but the terminal will self-correct on subsequent output). + for line in self.lines.iter().rev() { + if size + line.len() > max_bytes { + if size == 0 && line.len() > max_bytes { + // Single oversized chunk: take the tail to preserve the most recent content. + // Align offset forward to a UTF-8 char boundary so that downstream + // clients (e.g. Dart) that decode the payload as UTF-8 text don't + // encounter split code points. The protobuf bytes field itself allows + // arbitrary bytes; this is a best-effort mitigation for client-side decoding. + let mut offset = line.len() - max_bytes; + // Skip at most 3 continuation bytes (UTF-8 max 4-byte sequence). + // Prevents runaway skipping on non-UTF-8 binary data. + let mut skipped = 0u8; + while skipped < 3 + && offset < line.len() + && (line[offset] & 0b1100_0000) == 0b1000_0000 + { + offset += 1; + skipped += 1; + } + // If we skipped past all remaining bytes (degenerate data), drop the + // chunk entirely rather than emitting a slice that decodes poorly on the client. + if offset < line.len() { + chunks.push(&line[offset..]); + size = line.len() - offset; + } + } + break; + } + size += line.len(); + chunks.push(line); + } + + // Reverse to restore chronological order and concatenate + chunks.reverse(); + let mut result = Vec::with_capacity(size); + for chunk in chunks { + result.extend_from_slice(chunk); + } + + result + } +} + +/// Try to send data through the output channel with rate-limited drop logging. +/// Returns `true` if the caller should break out of the read loop (channel disconnected). +fn try_send_output( + output_tx: &mpsc::SyncSender>, + data: Vec, + terminal_id: i32, + label: &str, + drop_count: &mut u64, + last_drop_warn: &mut Instant, +) -> bool { + match output_tx.try_send(data) { + Ok(_) => { + if *drop_count > 0 { + log::trace!( + "Terminal {}{} output channel recovered, dropped {} chunks since last report", + terminal_id, + label, + *drop_count + ); + *drop_count = 0; + } + false + } + Err(mpsc::TrySendError::Full(_)) => { + *drop_count += 1; + if last_drop_warn.elapsed() >= Duration::from_secs(5) { + log::trace!( + "Terminal {}{} output channel full, dropped {} chunks in last {:?}", + terminal_id, + label, + *drop_count, + last_drop_warn.elapsed() + ); + *drop_count = 0; + *last_drop_warn = Instant::now(); + } + false + } + Err(mpsc::TrySendError::Disconnected(_)) => { + log::debug!("Terminal {}{} output channel disconnected", terminal_id, label); + true + } + } +} + +pub struct TerminalSession { + pub created_at: Instant, + last_activity: Instant, + pty_pair: Option, + child: Option>, + // Channel for sending input to the writer thread + input_tx: Option>>, + // Channel for receiving output from the reader thread + output_rx: Option>>, + exiting: Arc, + // Thread handles + reader_thread: Option>, + writer_thread: Option>, + output_buffer: OutputBuffer, + title: String, + pid: u32, + rows: u16, + cols: u16, + // Track if we've already sent the closed message + closed_message_sent: bool, + // Session state machine for reconnection handling + state: SessionState, + // Helper mode: PTY is managed by helper process, communication via message protocol + #[cfg(target_os = "windows")] + is_helper_mode: bool, + // Handle to helper process for termination when session closes + #[cfg(target_os = "windows")] + helper_process_handle: Option, +} + +impl TerminalSession { + fn new(terminal_id: i32, rows: u16, cols: u16) -> Self { + Self { + created_at: Instant::now(), + last_activity: Instant::now(), + pty_pair: None, + child: None, + input_tx: None, + output_rx: None, + exiting: Arc::new(AtomicBool::new(false)), + reader_thread: None, + writer_thread: None, + output_buffer: OutputBuffer::new(), + title: format!("Terminal {}", terminal_id), + pid: 0, + rows, + cols, + closed_message_sent: false, + state: SessionState::Closed, + #[cfg(target_os = "windows")] + is_helper_mode: false, + #[cfg(target_os = "windows")] + helper_process_handle: None, + } + } + + fn update_activity(&mut self) { + self.last_activity = Instant::now(); + } + + // This helper function is to ensure that the threads are joined before the child process is dropped. + // Though this is not strictly necessary on macOS. + fn stop(&mut self) { + self.state = SessionState::Closed; + self.exiting.store(true, Ordering::SeqCst); + + // Drop the input channel to signal writer thread to exit + if let Some(input_tx) = self.input_tx.take() { + // Send a final newline to ensure the reader can read some data, and then exit. + // This is required on Windows and Linux. + // Although `self.pty_pair = None;` is called below, we can still send a final newline here. + #[cfg(target_os = "windows")] + let final_msg = if self.is_helper_mode { + encode_helper_message(MSG_TYPE_DATA, b"\r\n") + } else { + b"\r\n".to_vec() + }; + #[cfg(not(target_os = "windows"))] + let final_msg = b"\r\n".to_vec(); + + if let Err(e) = input_tx.send(final_msg) { + log::warn!("Failed to send final newline to the terminal: {}", e); + } + drop(input_tx); + } + self.output_rx = None; + + // CRITICAL: In helper mode, we must terminate the helper process BEFORE joining threads! + // The reader thread is blocking on output_pipe.read(), which only returns EOF when + // the helper process exits. If we try to join the reader thread first, we deadlock. + // + // Sequence for helper mode: + // 1. Signal exiting and close input channel (done above) + // 2. Terminate helper process (causes output pipe EOF) + // 3. Join reader thread (now unblocked due to EOF) + // 4. Join writer thread + #[cfg(target_os = "windows")] + if self.is_helper_mode { + if let Some(helper_handle) = self.helper_process_handle.take() { + let handle = helper_handle.as_raw(); + log::debug!("Helper mode: terminating helper process before joining threads..."); + + // Give helper a very short time to exit gracefully (it should detect pipe close) + // But don't wait too long - we need to unblock the reader thread + let wait_result = unsafe { WinWaitForSingleObject(handle, 100) }; + + if wait_result == WIN_WAIT_OBJECT_0 { + log::debug!("Helper process exited gracefully"); + } else { + // Force terminate to unblock reader thread + log::debug!("Force terminating helper process to unblock reader thread"); + unsafe { + let _ = WinTerminateProcess(handle, 0); + } + } + + unsafe { + let _ = WinCloseHandle(handle); + } + } + } + + // 1. Windows (non-helper mode) + // `pty_pair` uses pipe. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/win/conpty.rs#L16 + // `read()` may stuck at https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/filedescriptor/src/windows.rs#L345 + // We can close the pipe to signal the reader thread to exit. + // After https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/win/psuedocon.rs#L86, the reader reads `[27, 91, 63, 57, 48, 48, 49, 108, 27, 91, 63, 49, 48, 48, 52, 108]` in my tests. + // 2. Linux + // `pty_pair` uses `libc::openpty`. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/unix.rs#L32 + // We can also call the drop method first. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/unix.rs#L352 + // The reader will get [13, 10] after dropping the `pty_pair`. + // 3. macOS + // No stuck cases have been found so far, more testing is needed. + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + self.pty_pair = None; + } + + // Wait for threads to finish + // The reader thread should join before the writer thread on Windows. + if let Some(reader_thread) = self.reader_thread.take() { + let _ = reader_thread.join(); + } + + // The read can read the last "\r\n" after the writer thread (not the child process) exits + // on Linux in my tests. + // But we still send "\r\n" to the writer thread and let the reader thread exit first for safety. + if let Some(writer_thread) = self.writer_thread.take() { + let _ = writer_thread.join(); + } + + if let Some(mut child) = self.child.take() { + // Kill the process + let _ = child.kill(); + add_to_reaper(child); + } + } +} + +impl Drop for TerminalSession { + fn drop(&mut self) { + // Ensure child process is properly handled when session is dropped + self.stop(); + } +} + +/// Persistent terminal service that can survive connection drops +pub struct PersistentTerminalService { + service_id: String, + sessions: HashMap>>, + pub created_at: Instant, + last_activity: Instant, + pub is_persistent: bool, + needs_session_sync: bool, + is_specified_user: bool, +} + +impl PersistentTerminalService { + pub fn new(service_id: String, is_persistent: bool, is_specified_user: bool) -> Self { + Self { + service_id, + sessions: HashMap::new(), + created_at: Instant::now(), + last_activity: Instant::now(), + is_persistent, + needs_session_sync: false, + is_specified_user, + } + } + + fn update_activity(&mut self) { + self.last_activity = Instant::now(); + } + + /// Get list of terminal metadata + pub fn list_terminals(&self) -> Vec<(i32, String, u32, Instant)> { + self.sessions + .iter() + .map(|(id, session)| { + let s = session.lock().unwrap(); + (*id, s.title.clone(), s.pid, s.created_at) + }) + .collect() + } + + /// Get buffered output for a terminal + pub fn get_terminal_buffer(&self, terminal_id: i32, max_bytes: usize) -> Option> { + self.sessions.get(&terminal_id).map(|session| { + let session = session.lock().unwrap(); + session.output_buffer.get_recent(max_bytes) + }) + } + + /// Get terminal info for recovery + pub fn get_terminal_info(&self, terminal_id: i32) -> Option<(u16, u16, Vec)> { + self.sessions.get(&terminal_id).map(|session| { + let session = session.lock().unwrap(); + ( + session.rows, + session.cols, + session + .output_buffer + .get_recent(DEFAULT_RECONNECT_BUFFER_BYTES), + ) + }) + } + + /// Check if service has active terminals + pub fn has_active_terminals(&self) -> bool { + !self.sessions.is_empty() + } + + fn reset_status(&mut self, is_persistent: bool) { + self.is_persistent = is_persistent; + self.needs_session_sync = true; + for session in self.sessions.values() { + let mut session = session.lock().unwrap(); + session.state = SessionState::Closed; + } + } +} + +pub struct TerminalServiceProxy { + service_id: String, + is_persistent: bool, + #[cfg(target_os = "windows")] + user_token: Option, +} + +pub fn set_persistent(service_id: &str, is_persistent: bool) -> Result<()> { + if let Some(service) = get_service(service_id) { + service.lock().unwrap().is_persistent = is_persistent; + Ok(()) + } else { + Err(anyhow!("Service {} not found", service_id)) + } +} + +impl TerminalServiceProxy { + pub fn new( + service_id: String, + is_persistent: Option, + _user_token: Option, + ) -> Self { + // Get persistence from the service if it exists + let is_persistent = + is_persistent.unwrap_or(if let Some(service) = get_service(&service_id) { + service.lock().unwrap().is_persistent + } else { + false + }); + TerminalServiceProxy { + service_id, + is_persistent, + #[cfg(target_os = "windows")] + user_token: _user_token, + } + } + + pub fn get_service_id(&self) -> &str { + &self.service_id + } + + pub fn handle_action(&mut self, action: &TerminalAction) -> Result> { + let service = match get_service(&self.service_id) { + Some(s) => s, + None => { + let mut response = TerminalResponse::new(); + let mut error = TerminalError::new(); + error.message = format!("Terminal service {} not found", self.service_id); + response.set_error(error); + return Ok(Some(response)); + } + }; + service.lock().unwrap().update_activity(); + match &action.union { + Some(terminal_action::Union::Open(open)) => { + self.handle_open(&mut service.lock().unwrap(), open) + } + Some(terminal_action::Union::Resize(resize)) => { + let session = service + .lock() + .unwrap() + .sessions + .get(&resize.terminal_id) + .cloned(); + self.handle_resize(session, resize) + } + Some(terminal_action::Union::Data(data)) => { + let session = service + .lock() + .unwrap() + .sessions + .get(&data.terminal_id) + .cloned(); + self.handle_data(session, data) + } + Some(terminal_action::Union::Close(close)) => { + self.handle_close(&mut service.lock().unwrap(), close) + } + _ => Ok(None), + } + } + + fn handle_open( + &self, + service: &mut PersistentTerminalService, + open: &OpenTerminal, + ) -> Result> { + let mut response = TerminalResponse::new(); + + // When the client requests a terminal_id that doesn't exist but there are + // surviving persistent sessions, remap the lowest-ID session to the requested + // terminal_id. This handles the case where _nextTerminalId resets to 1 on + // reconnect but the server-side sessions have non-contiguous IDs (e.g. {2: htop}). + // + // The client's requested terminal_id may not match any surviving session ID + // (e.g. _nextTerminalId incremented beyond the surviving IDs). This remap is a + // one-time handle reassignment — only the first reconnect triggers it because + // needs_session_sync is cleared afterward. Remaining sessions are communicated + // back via `persistent_sessions` with their original server-side IDs. + if !service.sessions.contains_key(&open.terminal_id) + && service.needs_session_sync + && !service.sessions.is_empty() + { + if let Some(&lowest_id) = service.sessions.keys().min() { + log::info!( + "Remapping persistent session {} -> {} for reconnection", + lowest_id, + open.terminal_id + ); + if let Some(session_arc) = service.sessions.remove(&lowest_id) { + service.sessions.insert(open.terminal_id, session_arc); + } + } + } + + // Check if terminal already exists + if let Some(session_arc) = service.sessions.get(&open.terminal_id) { + // Reconnect to existing terminal + let mut session = session_arc.lock().unwrap(); + // Directly enter Active state with pending buffer for immediate streaming. + // Historical buffer is sent first by read_outputs(), then real-time data follows. + // No overlap: pending_buffer comes from output_buffer (pre-disconnect history), + // while received_data in read_outputs() comes from the channel (post-reconnect). + // During disconnect, the run loop (sp.ok()) exits so read_outputs() stops being + // called; output_buffer is not updated, and channel data may be lost if it fills up. + let buffer = session + .output_buffer + .get_recent(DEFAULT_RECONNECT_BUFFER_BYTES); + let has_pending = !buffer.is_empty(); + session.state = SessionState::Active { + pending_buffer: if has_pending { Some(buffer) } else { None }, + // Always trigger two-phase SIGWINCH on reconnect to force TUI app redraw, + // regardless of whether there's pending buffer data. This avoids edge cases + // where buffer is empty but a TUI app (top/htop) still needs a full redraw. + sigwinch: SigwinchPhase::TempResize { + retries: MAX_SIGWINCH_PHASE_ATTEMPTS, + }, + }; + let mut opened = TerminalOpened::new(); + opened.terminal_id = open.terminal_id; + opened.success = true; + opened.message = "Reconnected to existing terminal".to_string(); + opened.pid = session.pid; + opened.service_id = self.service_id.clone(); + if service.needs_session_sync { + if service.sessions.len() > 1 { + // No need to include the current terminal in the list. + // Because the `persistent_sessions` is used to restore the other sessions. + opened.persistent_sessions = service + .sessions + .keys() + .filter(|&id| *id != open.terminal_id) + .cloned() + .collect(); + } + service.needs_session_sync = false; + } + response.set_opened(opened); + + return Ok(Some(response)); + } + + // Windows with user_token: use helper process to run shell as the logged-in user + // This solves the ConPTY + CreateProcessAsUserW incompatibility issue where + // vim, Claude Code, and other TUI applications hang when ConPTY is created + // by SYSTEM service but shell runs as user via CreateProcessAsUserW. + #[cfg(target_os = "windows")] + if self.user_token.is_some() { + return self.handle_open_with_helper(service, open); + } + + // Create new terminal session + log::info!( + "Creating new terminal {} for service {}", + open.terminal_id, + service.service_id + ); + let mut session = + TerminalSession::new(open.terminal_id, open.rows as u16, open.cols as u16); + + let pty_size = PtySize { + rows: open.rows as u16, + cols: open.cols as u16, + pixel_width: 0, + pixel_height: 0, + }; + + log::debug!("Opening PTY with size: {}x{}", open.rows, open.cols); + let pty_system = portable_pty::native_pty_system(); + let pty_pair = pty_system.openpty(pty_size).context("Failed to open PTY")?; + + // Use default shell for the platform + let shell = get_default_shell(); + log::debug!("Using shell: {}", shell); + + #[allow(unused_mut)] + let mut cmd = CommandBuilder::new(&shell); + + // macOS-specific terminal configuration + // 1. Use login shell (-l) to load user's shell profile (~/.zprofile, ~/.bash_profile) + // This ensures PATH includes Homebrew paths (/opt/homebrew/bin, /usr/local/bin) + // 2. Set TERM environment variable for proper terminal behavior + // This fixes issues with control sequences (e.g., Delete/Backspace keys) + // macOS terminfo uses hex naming: '78' = 'x' for xterm entries + // Note: For Linux, `TERM` is set in src/platform/linux.rs try_start_server_() + #[cfg(target_os = "macos")] + { + // Start as login shell to load user environment (PATH, etc.) + cmd.arg("-l"); + log::debug!("Added -l flag for macOS login shell"); + + let term = if std::path::Path::new("/usr/share/terminfo/78/xterm-256color").exists() { + "xterm-256color" + } else { + "xterm" + }; + cmd.env("TERM", term); + log::debug!("Set TERM={} for macOS PTY", term); + } + + // Note: On Windows with user_token, we use helper mode (handle_open_with_helper) + // which is dispatched earlier in this function. This code path is only reached + // when user_token is None (e.g., running directly as user, not as SYSTEM service). + + log::debug!("Spawning shell process..."); + let child = pty_pair + .slave + .spawn_command(cmd) + .context("Failed to spawn command")?; + + let writer = pty_pair + .master + .take_writer() + .context("Failed to get writer")?; + + let reader = pty_pair + .master + .try_clone_reader() + .context("Failed to get reader")?; + + session.pid = child.process_id().unwrap_or(0) as u32; + + // Create channels for input/output + let (input_tx, input_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + let (output_tx, output_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + + // Spawn writer thread + let terminal_id = open.terminal_id; + let writer_thread = thread::spawn(move || { + let mut writer = writer; + while let Ok(data) = input_rx.recv() { + if let Err(e) = writer.write_all(&data) { + log::error!("Terminal {} write error: {}", terminal_id, e); + break; + } + if let Err(e) = writer.flush() { + log::error!("Terminal {} flush error: {}", terminal_id, e); + } + } + log::debug!("Terminal {} writer thread exiting", terminal_id); + }); + + let exiting = session.exiting.clone(); + // Spawn reader thread + let terminal_id = open.terminal_id; + let reader_thread = thread::spawn(move || { + let mut reader = reader; + let mut buf = vec![0u8; 4096]; + let mut drop_count: u64 = 0; + // Initialize to > 5s ago so the first drop triggers a warning immediately. + let mut last_drop_warn = Instant::now() - Duration::from_secs(6); + loop { + match reader.read(&mut buf) { + Ok(0) => { + // EOF + // This branch can be reached when the child process exits on macOS. + // But not on Linux and Windows in my tests. + break; + } + Ok(n) => { + if exiting.load(Ordering::SeqCst) { + break; + } + let data = buf[..n].to_vec(); + // Use try_send to avoid blocking the reader thread when channel is full. + // During disconnect, the run loop (sp.ok()) stops and read_outputs() is + // no longer called, so the channel won't be drained. Blocking send would + // deadlock the reader thread in that case. + // Note: data produced during disconnect may be lost if channel fills up, + // since output_buffer is only updated in read_outputs(). The buffer will + // contain history from before the disconnect, not data produced after it. + if try_send_output( + &output_tx, + data, + terminal_id, + "", + &mut drop_count, + &mut last_drop_warn, + ) { + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // This branch is not reached in my tests, but we still add `exiting` check to ensure we can exit. + if exiting.load(Ordering::SeqCst) { + break; + } + // For non-blocking I/O, sleep briefly + thread::sleep(Duration::from_millis(10)); + } + Err(e) => { + log::error!("Terminal {} read error: {}", terminal_id, e); + break; + } + } + } + log::debug!("Terminal {} reader thread exiting", terminal_id); + }); + + session.pty_pair = Some(pty_pair); + session.child = Some(child); + session.input_tx = Some(input_tx); + session.output_rx = Some(output_rx); + session.reader_thread = Some(reader_thread); + session.writer_thread = Some(writer_thread); + session.state = SessionState::Active { + pending_buffer: None, + sigwinch: SigwinchPhase::Idle, + }; + + let mut opened = TerminalOpened::new(); + opened.terminal_id = open.terminal_id; + opened.success = true; + opened.message = "Terminal opened".to_string(); + opened.pid = session.pid; + opened.service_id = service.service_id.clone(); + if service.needs_session_sync { + if !service.sessions.is_empty() { + opened.persistent_sessions = service.sessions.keys().cloned().collect(); + } + service.needs_session_sync = false; + } + response.set_opened(opened); + + log::info!( + "Terminal {} opened successfully with PID {}", + open.terminal_id, + session.pid + ); + + // Store the session + service + .sessions + .insert(open.terminal_id, Arc::new(Mutex::new(session))); + + Ok(Some(response)) + } + + /// Windows-only: Open terminal using helper process pattern + /// This solves the ConPTY + CreateProcessAsUserW incompatibility issue. + /// The helper process runs as the logged-in user and creates ConPTY + shell, + /// communicating with this service via named pipes. + #[cfg(target_os = "windows")] + fn handle_open_with_helper( + &self, + service: &mut PersistentTerminalService, + open: &OpenTerminal, + ) -> Result> { + let mut response = TerminalResponse::new(); + + log::info!( + "Creating new terminal {} using helper process for service: {}", + open.terminal_id, + service.service_id + ); + + let mut session = + TerminalSession::new(open.terminal_id, open.rows as u16, open.cols as u16); + + // Generate unique pipe names for this terminal + let pipe_id = uuid::Uuid::new_v4(); + let input_pipe_name = format!(r"\\.\pipe\rustdesk_term_in_{}", pipe_id); + let output_pipe_name = format!(r"\\.\pipe\rustdesk_term_out_{}", pipe_id); + + log::debug!( + "Creating pipes: input={}, output={}", + input_pipe_name, + output_pipe_name + ); + + // Get user_token early - needed for both DACL creation and helper launch + let user_token = self + .user_token + .ok_or_else(|| anyhow!("user_token is required for helper mode"))?; + + // Create pipes (server side, don't wait for connection yet) + // input_pipe: service WRITES to this, helper READS from this + // output_pipe: service READS from this, helper WRITES to this + // Using OwnedHandle for RAII - handles are automatically closed on error + // Pass user_token to create restricted DACL (only SYSTEM + user can access) + let input_pipe_handle = OwnedHandle::new(create_named_pipe_server( + &input_pipe_name, + false, + user_token, + )?); + let output_pipe_handle = OwnedHandle::new(create_named_pipe_server( + &output_pipe_name, + true, + user_token, + )?); + + let helper_process_info = launch_terminal_helper_with_token( + user_token, + &input_pipe_name, + &output_pipe_name, + open.terminal_id, + open.rows as u16, + open.cols as u16, + )?; + + // Use HelperProcessGuard for RAII cleanup - terminates process on error + // Unlike OwnedHandle which only closes the handle, this guard ensures + // the helper process is terminated if pipe connection fails or other errors occur. + let helper_process_guard = + HelperProcessGuard::new(helper_process_info.handle, helper_process_info.pid); + let helper_pid = helper_process_guard.pid(); + + // Wait for helper to connect to pipes + // If this fails, HelperProcessGuard will terminate the helper process + let mut input_pipe = wait_for_pipe_connection( + input_pipe_handle, + &input_pipe_name, + PIPE_CONNECTION_TIMEOUT_MS, + )?; + let mut output_pipe = wait_for_pipe_connection( + output_pipe_handle, + &output_pipe_name, + PIPE_CONNECTION_TIMEOUT_MS, + )?; + + // Check if helper process is still running after pipe connection + // This provides early detection if helper crashed during startup + if !is_helper_process_running(helper_process_guard.as_raw()) { + return Err(anyhow!( + "Helper process (PID {}) exited unexpectedly after pipe connection", + helper_pid + )); + } + + // Disarm the guard and transfer ownership to session + // From this point, the session is responsible for terminating the helper + let helper_raw_handle = helper_process_guard.disarm(); + + // Use helper process PID for session tracking + // Note: This is the helper process PID, not the actual shell PID. + // The real shell runs inside the helper process but its PID is not exposed here. + // For process management (termination, status), the helper PID is what we need. + session.pid = helper_pid; + + // Create channels for input/output (same as direct PTY mode) + let (input_tx, input_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + let (output_tx, output_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + + // Spawn writer thread: reads from channel, writes to input pipe + let terminal_id = open.terminal_id; + let writer_thread = thread::spawn(move || { + while let Ok(data) = input_rx.recv() { + if let Err(e) = input_pipe.write_all(&data) { + log::error!("Terminal {} pipe write error: {}", terminal_id, e); + break; + } + if let Err(e) = input_pipe.flush() { + log::error!("Terminal {} pipe flush error: {}", terminal_id, e); + } + } + log::debug!( + "Terminal {} writer thread (helper mode) exiting", + terminal_id + ); + }); + + // Spawn reader thread: reads from output pipe, sends to channel + // Note: The output pipe was created with FILE_FLAG_OVERLAPPED for timeout support + // during ConnectNamedPipe. However, once converted to a File handle, reads are + // performed synchronously. The WouldBlock handling below is defensive but may + // not be triggered in practice since File::read() blocks until data is available. + let exiting = session.exiting.clone(); + let terminal_id = open.terminal_id; + let reader_thread = thread::spawn(move || { + let mut buf = vec![0u8; 4096]; + let mut drop_count: u64 = 0; + // Initialize to > 5s ago so the first drop triggers a warning immediately. + let mut last_drop_warn = Instant::now() - Duration::from_secs(6); + loop { + match output_pipe.read(&mut buf) { + Ok(0) => { + // EOF - helper process exited + log::debug!("Terminal {} helper output EOF", terminal_id); + break; + } + Ok(n) => { + if exiting.load(Ordering::SeqCst) { + break; + } + let data = buf[..n].to_vec(); + // Use try_send to avoid blocking the reader thread (same as direct PTY mode) + if try_send_output( + &output_tx, + data, + terminal_id, + " (helper)", + &mut drop_count, + &mut last_drop_warn, + ) { + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // Defensive: WouldBlock is unlikely with synchronous File::read(), + // but handle it gracefully just in case. + if exiting.load(Ordering::SeqCst) { + break; + } + thread::sleep(Duration::from_millis(10)); + } + Err(e) => { + log::error!("Terminal {} pipe read error: {}", terminal_id, e); + break; + } + } + } + log::debug!( + "Terminal {} reader thread (helper mode) exiting", + terminal_id + ); + }); + + // In helper mode, we don't have pty_pair or child - helper manages those + session.pty_pair = None; + session.child = None; + session.input_tx = Some(input_tx); + session.output_rx = Some(output_rx); + session.reader_thread = Some(reader_thread); + session.writer_thread = Some(writer_thread); + session.state = SessionState::Active { + pending_buffer: None, + sigwinch: SigwinchPhase::Idle, + }; + session.is_helper_mode = true; + session.helper_process_handle = Some(SendableHandle::new(helper_raw_handle)); + + let mut opened = TerminalOpened::new(); + opened.terminal_id = open.terminal_id; + opened.success = true; + opened.message = "Terminal opened (helper mode)".to_string(); + opened.pid = session.pid; + opened.service_id = service.service_id.clone(); + if service.needs_session_sync { + if !service.sessions.is_empty() { + opened.persistent_sessions = service.sessions.keys().cloned().collect(); + } + service.needs_session_sync = false; + } + response.set_opened(opened); + + log::info!( + "Terminal {} opened successfully using helper process (PID {})", + open.terminal_id, + session.pid + ); + + service + .sessions + .insert(open.terminal_id, Arc::new(Mutex::new(session))); + + Ok(Some(response)) + } + + fn handle_resize( + &self, + session: Option>>, + resize: &ResizeTerminal, + ) -> Result> { + if let Some(session_arc) = session { + let mut session = session_arc.lock().unwrap(); + session.update_activity(); + session.rows = resize.rows as u16; + session.cols = resize.cols as u16; + + // Note: we do NOT clear the sigwinch phase here. The server-side two-phase + // SIGWINCH mechanism in read_outputs() is self-contained (temp resize → restore + // across two polling cycles), so client resize is purely a dimension sync and + // doesn't affect it. + + // Windows: handle helper mode vs direct PTY mode + #[cfg(target_os = "windows")] + { + if session.is_helper_mode { + // Helper mode: send resize command via message protocol + if let Some(input_tx) = &session.input_tx { + let msg = encode_resize_message(resize.rows as u16, resize.cols as u16); + if let Err(e) = input_tx.send(msg) { + log::error!("Failed to send resize to helper: {}", e); + } + } else { + log::warn!( + "Terminal {} is in helper mode but input_tx is None, cannot send resize", + resize.terminal_id + ); + } + } else { + // Direct PTY mode + Self::resize_pty(&session, resize)?; + } + } + + // Non-Windows: always direct PTY mode + #[cfg(not(target_os = "windows"))] + { + Self::resize_pty(&session, resize)?; + } + } + Ok(None) + } + + /// Resize PTY directly (used for non-helper mode) + fn resize_pty(session: &TerminalSession, resize: &ResizeTerminal) -> Result<()> { + if let Some(pty_pair) = &session.pty_pair { + pty_pair.master.resize(PtySize { + rows: resize.rows as u16, + cols: resize.cols as u16, + pixel_width: 0, + pixel_height: 0, + })?; + } + Ok(()) + } + + fn handle_data( + &self, + session: Option>>, + data: &TerminalData, + ) -> Result> { + if let Some(session_arc) = session { + let mut session = session_arc.lock().unwrap(); + session.update_activity(); + if let Some(input_tx) = &session.input_tx { + // Encode data for helper mode or send raw for direct PTY mode + #[cfg(target_os = "windows")] + let msg = if session.is_helper_mode { + encode_helper_message(MSG_TYPE_DATA, &data.data) + } else { + data.data.to_vec() + }; + #[cfg(not(target_os = "windows"))] + let msg = data.data.to_vec(); + + // Send data to writer thread + if let Err(e) = input_tx.send(msg) { + log::error!( + "Failed to send data to terminal {}: {}", + data.terminal_id, + e + ); + } + } + } + + Ok(None) + } + + fn handle_close( + &self, + service: &mut PersistentTerminalService, + close: &CloseTerminal, + ) -> Result> { + let mut response = TerminalResponse::new(); + + // Always close and remove the terminal + if let Some(session_arc) = service.sessions.remove(&close.terminal_id) { + let mut session = session_arc.lock().unwrap(); + let exit_code = if let Some(mut child) = session.child.take() { + child.kill()?; + add_to_reaper(child); + -1 // -1 indicates forced termination + } else { + 0 + }; + + let mut closed = TerminalClosed::new(); + closed.terminal_id = close.terminal_id; + closed.exit_code = exit_code; + response.set_closed(closed); + Ok(Some(response)) + } else { + Ok(None) + } + } + + /// Perform a single PTY resize as part of the two-phase SIGWINCH sequence. + /// Returns true if the resize succeeded. + /// + /// Takes individual field references to avoid borrowing the entire TerminalSession, + /// which would conflict with the mutable borrow of session.state in read_outputs(). + fn do_sigwinch_resize( + terminal_id: i32, + rows: u16, + cols: u16, + pty_pair: &Option, + input_tx: &Option>>, + _is_helper_mode: bool, + action: &SigwinchAction, + ) -> bool { + // Skip if dimensions are not initialized (shouldn't happen on reconnect, + // but guard against it to avoid resizing to nonsensical values). + if rows == 0 || cols == 0 { + return false; + } + + let target_rows = match action { + SigwinchAction::TempResize => { + // For very small terminals (≤2 rows), subtracting 1 would result in an unusable + // size (0 or 1 row), so we add 1 instead. Either direction triggers SIGWINCH. + if rows > 2 { + rows.saturating_sub(1) + } else { + rows.saturating_add(1) + } + } + SigwinchAction::Restore => rows, + }; + + let phase_name = match action { + SigwinchAction::TempResize => "temp resize", + SigwinchAction::Restore => "restore", + }; + + #[cfg(target_os = "windows")] + let use_helper = _is_helper_mode; + #[cfg(not(target_os = "windows"))] + let use_helper = false; + + if use_helper { + #[cfg(target_os = "windows")] + { + let input_tx = match input_tx { + Some(tx) => tx, + None => return false, + }; + let msg = encode_resize_message(target_rows, cols); + if let Err(e) = input_tx.try_send(msg) { + log::warn!( + "Terminal {} SIGWINCH {} via helper failed: {}", + terminal_id, + phase_name, + e + ); + return false; + } + true + } + #[cfg(not(target_os = "windows"))] + { + let _ = (input_tx, phase_name); + false + } + } else if let Some(pty_pair) = pty_pair { + if let Err(e) = pty_pair.master.resize(PtySize { + rows: target_rows, + cols, + pixel_width: 0, + pixel_height: 0, + }) { + log::warn!( + "Terminal {} SIGWINCH {} failed: {}", + terminal_id, + phase_name, + e + ); + return false; + } + true + } else { + false + } + } + + /// Helper to create a TerminalResponse with optional compression. + fn create_terminal_data_response(terminal_id: i32, data: Vec) -> TerminalResponse { + let mut response = TerminalResponse::new(); + let mut terminal_data = TerminalData::new(); + terminal_data.terminal_id = terminal_id; + + if data.len() > COMPRESS_THRESHOLD { + let compressed = compress::compress(&data); + if compressed.len() < data.len() { + terminal_data.data = bytes::Bytes::from(compressed); + terminal_data.compressed = true; + } else { + terminal_data.data = bytes::Bytes::from(data); + } + } else { + terminal_data.data = bytes::Bytes::from(data); + } + + response.set_data(terminal_data); + response + } + + pub fn read_outputs(&self) -> Vec { + let service = match get_service(&self.service_id) { + Some(s) => s, + None => { + return vec![]; + } + }; + + // Get session references with minimal service lock time + let sessions: Vec<(i32, Arc>)> = { + let service = service.lock().unwrap(); + service + .sessions + .iter() + .map(|(id, session)| (*id, session.clone())) + .collect() + }; + + let mut responses = Vec::new(); + let mut closed_terminals = Vec::new(); + + // Process each session with its own lock + for (terminal_id, session_arc) in sessions { + if let Ok(mut session) = session_arc.try_lock() { + // Check if reader thread is still alive and we haven't sent closed message yet + let mut should_send_closed = false; + if !session.closed_message_sent { + if let Some(thread) = &session.reader_thread { + if thread.is_finished() { + should_send_closed = true; + session.closed_message_sent = true; + } + } + } + // It's Ok to put the closed message here. + // Because the `reader_thread` is joined in `stop()`, + // and `stop()` is called before the session is dropped. + if should_send_closed { + closed_terminals.push(terminal_id); + } + + // Always drain the output channel regardless of session state. + // When Active: data is sent to client. When Closed (within the same + // connection): data is buffered in output_buffer for reconnection replay. + // Note: during actual disconnect, the run loop exits and read_outputs() + // is not called, so channel data produced after disconnect may be lost. + let mut has_activity = false; + let mut received_data = Vec::new(); + if let Some(output_rx) = &session.output_rx { + // Try to read all available data + while let Ok(data) = output_rx.try_recv() { + has_activity = true; + received_data.push(data); + } + } + + if has_activity { + session.update_activity(); + } + + // Update buffer (always buffer for reconnection support) + for data in &received_data { + session.output_buffer.append(data); + } + + // Skip sending responses if session is not Active. + // Data is already buffered above and will be sent on next reconnection. + // Use a scoped block to limit the mutable borrow of session.state, + // so we can immutably borrow other session fields afterwards. + let sigwinch_action = { + let (pending_buffer, sigwinch) = match &mut session.state { + SessionState::Active { + pending_buffer, + sigwinch, + } => (pending_buffer, sigwinch), + _ => continue, + }; + + // Send pending buffer response first (set on reconnection in handle_open). + // This ensures historical buffer is sent before any real-time data. + if let Some(buffer) = pending_buffer.take() { + if !buffer.is_empty() { + responses + .push(Self::create_terminal_data_response(terminal_id, buffer)); + } + } + + // Two-phase SIGWINCH: see SigwinchPhase doc comments for rationale. + // Each phase is a single PTY resize, spaced ~30ms apart by the polling + // interval, ensuring the TUI app sees a real size change on each signal. + match sigwinch { + SigwinchPhase::TempResize { retries } => { + if *retries == 0 { + log::warn!( + "Terminal {} SIGWINCH phase 1 (temp resize) failed after {} attempts, giving up", + terminal_id, MAX_SIGWINCH_PHASE_ATTEMPTS + ); + *sigwinch = SigwinchPhase::Idle; + None + } else { + *retries -= 1; + Some(SigwinchAction::TempResize) + } + } + SigwinchPhase::Restore { retries } => { + if *retries == 0 { + log::warn!( + "Terminal {} SIGWINCH phase 2 (restore) failed after {} attempts, giving up", + terminal_id, MAX_SIGWINCH_PHASE_ATTEMPTS + ); + *sigwinch = SigwinchPhase::Idle; + None + } else { + *retries -= 1; + Some(SigwinchAction::Restore) + } + } + SigwinchPhase::Idle => None, + } + }; + + // Execute SIGWINCH resize outside the mutable borrow scope of session.state. + if let Some(action) = sigwinch_action { + #[cfg(target_os = "windows")] + let is_helper = session.is_helper_mode; + #[cfg(not(target_os = "windows"))] + let is_helper = false; + let resize_ok = Self::do_sigwinch_resize( + terminal_id, + session.rows, + session.cols, + &session.pty_pair, + &session.input_tx, + is_helper, + &action, + ); + if let SessionState::Active { sigwinch, .. } = &mut session.state { + match action { + SigwinchAction::TempResize => { + if resize_ok { + // Phase 1 succeeded — advance to phase 2 (restore). + *sigwinch = SigwinchPhase::Restore { + retries: MAX_SIGWINCH_PHASE_ATTEMPTS, + }; + } + // If failed, retries already decremented; will retry phase 1. + } + SigwinchAction::Restore => { + if resize_ok { + // Phase 2 succeeded — SIGWINCH sequence complete. + *sigwinch = SigwinchPhase::Idle; + } + // If failed, retries already decremented; will retry phase 2. + } + } + } + } + + // Send real-time data after historical buffer + for data in received_data { + responses.push(Self::create_terminal_data_response(terminal_id, data)); + } + } + } + + // Clean up closed terminals (requires service lock briefly) + if !closed_terminals.is_empty() { + let mut sessions = service.lock().unwrap().sessions.clone(); + for terminal_id in closed_terminals { + let mut exit_code = 0; + + if !self.is_persistent { + if let Some(session_arc) = sessions.remove(&terminal_id) { + service.lock().unwrap().sessions.remove(&terminal_id); + let mut session = session_arc.lock().unwrap(); + // Take the child and add to zombie reaper + if let Some(mut child) = session.child.take() { + // Try to get exit code if available + if let Ok(Some(status)) = child.try_wait() { + exit_code = status.exit_code() as i32; + } + add_to_reaper(child); + } + } + } else { + // For persistent sessions, just clear the child reference + if let Some(session_arc) = sessions.get(&terminal_id) { + let mut session = session_arc.lock().unwrap(); + if let Some(mut child) = session.child.take() { + // Try to get exit code if available + if let Ok(Some(status)) = child.try_wait() { + exit_code = status.exit_code() as i32; + } + add_to_reaper(child); + } + } + } + + let mut response = TerminalResponse::new(); + let mut closed = TerminalClosed::new(); + closed.terminal_id = terminal_id; + closed.exit_code = exit_code; + response.set_closed(closed); + responses.push(response); + } + } + + responses + } + + /// Cleanup when connection drops + pub fn on_disconnect(&self) { + if !self.is_persistent { + // Remove non-persistent service + remove_service(&self.service_id); + } + } +} diff --git a/vendor/rustdesk/src/server/uinput.rs b/vendor/rustdesk/src/server/uinput.rs new file mode 100644 index 0000000..a808b4a --- /dev/null +++ b/vendor/rustdesk/src/server/uinput.rs @@ -0,0 +1,1307 @@ +use crate::ipc::{self, new_listener, Connection, Data, DataKeyboard, DataMouse}; +use enigo::{Key, KeyboardControllable, MouseButton, MouseControllable}; +use evdev::{ + uinput::{VirtualDevice, VirtualDeviceBuilder}, + AttributeSet, EventType, InputEvent, +}; +use hbb_common::{ + allow_err, bail, log, + tokio::{self, runtime::Runtime}, + ResultType, +}; + +static IPC_CONN_TIMEOUT: u64 = 1000; +static IPC_REQUEST_TIMEOUT: u64 = 1000; +static IPC_POSTFIX_KEYBOARD: &str = "_uinput_keyboard"; +static IPC_POSTFIX_MOUSE: &str = "_uinput_mouse"; +static IPC_POSTFIX_CONTROL: &str = "_uinput_control"; + +pub mod client { + use super::*; + + pub struct UInputKeyboard { + conn: Connection, + rt: Runtime, + } + + impl UInputKeyboard { + pub async fn new() -> ResultType { + let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_KEYBOARD).await?; + let rt = Runtime::new()?; + Ok(Self { conn, rt }) + } + + fn send(&mut self, data: Data) -> ResultType<()> { + self.rt.block_on(self.conn.send(&data)) + } + + fn send_get_key_state(&mut self, data: Data) -> ResultType { + self.rt.block_on(self.conn.send(&data))?; + + match self + .rt + .block_on(self.conn.next_timeout(IPC_REQUEST_TIMEOUT)) + { + Ok(Some(Data::KeyboardResponse(ipc::DataKeyboardResponse::GetKeyState(state)))) => { + Ok(state) + } + Ok(Some(resp)) => { + // FATAL error!!! + bail!( + "FATAL error, wait keyboard result other response: {:?}", + &resp + ); + } + Ok(None) => { + // FATAL error!!! + // Maybe wait later + bail!("FATAL error, wait keyboard result, receive None",); + } + Err(e) => { + // FATAL error!!! + bail!( + "FATAL error, wait keyboard result timeout {}, {}", + &e, + IPC_REQUEST_TIMEOUT + ); + } + } + } + } + + impl KeyboardControllable for UInputKeyboard { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn get_key_state(&mut self, key: Key) -> bool { + match self.send_get_key_state(Data::Keyboard(DataKeyboard::GetKeyState(key))) { + Ok(state) => state, + Err(e) => { + // unreachable!() + log::error!("Failed to get key state {}", &e); + false + } + } + } + + fn key_sequence(&mut self, sequence: &str) { + // Sequence events are normally handled in the --server process before reaching here. + // Forward via IPC as a fallback — input_text_wayland can still handle ASCII chars + // via keysym/uinput, though non-ASCII will be skipped (no clipboard in --service). + log::debug!( + "UInputKeyboard::key_sequence called (len={})", + sequence.len() + ); + allow_err!(self.send(Data::Keyboard(DataKeyboard::Sequence(sequence.to_string())))); + } + + // TODO: handle error??? + fn key_down(&mut self, key: Key) -> enigo::ResultType { + allow_err!(self.send(Data::Keyboard(DataKeyboard::KeyDown(key)))); + Ok(()) + } + fn key_up(&mut self, key: Key) { + allow_err!(self.send(Data::Keyboard(DataKeyboard::KeyUp(key)))); + } + fn key_click(&mut self, key: Key) { + allow_err!(self.send(Data::Keyboard(DataKeyboard::KeyClick(key)))); + } + } + + pub struct UInputMouse { + conn: Connection, + rt: Runtime, + } + + impl UInputMouse { + pub async fn new() -> ResultType { + let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_MOUSE).await?; + let rt = Runtime::new()?; + Ok(Self { conn, rt }) + } + + fn send(&mut self, data: Data) -> ResultType<()> { + self.rt.block_on(self.conn.send(&data)) + } + + pub fn send_refresh(&mut self) -> ResultType<()> { + self.send(Data::Mouse(DataMouse::Refresh)) + } + } + + impl MouseControllable for UInputMouse { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + + fn mouse_move_to(&mut self, x: i32, y: i32) { + allow_err!(self.send(Data::Mouse(DataMouse::MoveTo(x, y)))); + } + fn mouse_move_relative(&mut self, x: i32, y: i32) { + allow_err!(self.send(Data::Mouse(DataMouse::MoveRelative(x, y)))); + } + // TODO: handle error??? + fn mouse_down(&mut self, button: MouseButton) -> enigo::ResultType { + allow_err!(self.send(Data::Mouse(DataMouse::Down(button)))); + Ok(()) + } + fn mouse_up(&mut self, button: MouseButton) { + allow_err!(self.send(Data::Mouse(DataMouse::Up(button)))); + } + fn mouse_click(&mut self, button: MouseButton) { + allow_err!(self.send(Data::Mouse(DataMouse::Click(button)))); + } + fn mouse_scroll_x(&mut self, length: i32) { + allow_err!(self.send(Data::Mouse(DataMouse::ScrollX(length)))); + } + fn mouse_scroll_y(&mut self, length: i32) { + allow_err!(self.send(Data::Mouse(DataMouse::ScrollY(length)))); + } + } + + pub async fn set_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { + let mut conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_CONTROL).await?; + conn.send(&Data::Control(ipc::DataControl::Resolution { + minx, + maxx, + miny, + maxy, + })) + .await?; + let _ = conn.next().await?; + Ok(()) + } +} + +pub mod service { + use super::*; + use hbb_common::lazy_static; + use scrap::wayland::{ + pipewire::RDP_SESSION_INFO, remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop, + }; + use std::{collections::HashMap, sync::Mutex}; + + lazy_static::lazy_static! { + static ref KEY_MAP: HashMap = HashMap::from( + [ + (enigo::Key::Alt, evdev::Key::KEY_LEFTALT), + (enigo::Key::Backspace, evdev::Key::KEY_BACKSPACE), + (enigo::Key::CapsLock, evdev::Key::KEY_CAPSLOCK), + (enigo::Key::Control, evdev::Key::KEY_LEFTCTRL), + (enigo::Key::Delete, evdev::Key::KEY_DELETE), + (enigo::Key::DownArrow, evdev::Key::KEY_DOWN), + (enigo::Key::End, evdev::Key::KEY_END), + (enigo::Key::Escape, evdev::Key::KEY_ESC), + (enigo::Key::F1, evdev::Key::KEY_F1), + (enigo::Key::F10, evdev::Key::KEY_F10), + (enigo::Key::F11, evdev::Key::KEY_F11), + (enigo::Key::F12, evdev::Key::KEY_F12), + (enigo::Key::F2, evdev::Key::KEY_F2), + (enigo::Key::F3, evdev::Key::KEY_F3), + (enigo::Key::F4, evdev::Key::KEY_F4), + (enigo::Key::F5, evdev::Key::KEY_F5), + (enigo::Key::F6, evdev::Key::KEY_F6), + (enigo::Key::F7, evdev::Key::KEY_F7), + (enigo::Key::F8, evdev::Key::KEY_F8), + (enigo::Key::F9, evdev::Key::KEY_F9), + (enigo::Key::Home, evdev::Key::KEY_HOME), + (enigo::Key::LeftArrow, evdev::Key::KEY_LEFT), + (enigo::Key::Meta, evdev::Key::KEY_LEFTMETA), + (enigo::Key::Option, evdev::Key::KEY_OPTION), + (enigo::Key::PageDown, evdev::Key::KEY_PAGEDOWN), + (enigo::Key::PageUp, evdev::Key::KEY_PAGEUP), + (enigo::Key::Return, evdev::Key::KEY_ENTER), + (enigo::Key::RightArrow, evdev::Key::KEY_RIGHT), + (enigo::Key::Shift, evdev::Key::KEY_LEFTSHIFT), + (enigo::Key::Space, evdev::Key::KEY_SPACE), + (enigo::Key::Tab, evdev::Key::KEY_TAB), + (enigo::Key::UpArrow, evdev::Key::KEY_UP), + (enigo::Key::Numpad0, evdev::Key::KEY_KP0), // check if correct? + (enigo::Key::Numpad1, evdev::Key::KEY_KP1), + (enigo::Key::Numpad2, evdev::Key::KEY_KP2), + (enigo::Key::Numpad3, evdev::Key::KEY_KP3), + (enigo::Key::Numpad4, evdev::Key::KEY_KP4), + (enigo::Key::Numpad5, evdev::Key::KEY_KP5), + (enigo::Key::Numpad6, evdev::Key::KEY_KP6), + (enigo::Key::Numpad7, evdev::Key::KEY_KP7), + (enigo::Key::Numpad8, evdev::Key::KEY_KP8), + (enigo::Key::Numpad9, evdev::Key::KEY_KP9), + (enigo::Key::Cancel, evdev::Key::KEY_CANCEL), + (enigo::Key::Clear, evdev::Key::KEY_CLEAR), + (enigo::Key::Alt, evdev::Key::KEY_LEFTALT), + (enigo::Key::Pause, evdev::Key::KEY_PAUSE), + (enigo::Key::Kana, evdev::Key::KEY_KATAKANA), // check if correct? + (enigo::Key::Hangul, evdev::Key::KEY_HANGEUL), // check if correct? + // (enigo::Key::Junja, evdev::Key::KEY_JUNJA), // map? + // (enigo::Key::Final, evdev::Key::KEY_FINAL), // map? + (enigo::Key::Hanja, evdev::Key::KEY_HANJA), + // (enigo::Key::Kanji, evdev::Key::KEY_KANJI), // map? + // (enigo::Key::Convert, evdev::Key::KEY_CONVERT), + (enigo::Key::Select, evdev::Key::KEY_SELECT), + (enigo::Key::Print, evdev::Key::KEY_PRINT), + // (enigo::Key::Execute, evdev::Key::KEY_EXECUTE), + (enigo::Key::Snapshot, evdev::Key::KEY_SYSRQ), + (enigo::Key::Insert, evdev::Key::KEY_INSERT), + (enigo::Key::Help, evdev::Key::KEY_HELP), + (enigo::Key::Sleep, evdev::Key::KEY_SLEEP), + // (enigo::Key::Separator, evdev::Key::KEY_SEPARATOR), + (enigo::Key::Scroll, evdev::Key::KEY_SCROLLLOCK), + (enigo::Key::NumLock, evdev::Key::KEY_NUMLOCK), + (enigo::Key::RWin, evdev::Key::KEY_RIGHTMETA), + (enigo::Key::Apps, evdev::Key::KEY_COMPOSE), // it's a little strange that the key is mapped to KEY_COMPOSE, not KEY_MENU + (enigo::Key::Multiply, evdev::Key::KEY_KPASTERISK), + (enigo::Key::Add, evdev::Key::KEY_KPPLUS), + (enigo::Key::Subtract, evdev::Key::KEY_KPMINUS), + (enigo::Key::Decimal, evdev::Key::KEY_KPCOMMA), // KEY_KPDOT and KEY_KPCOMMA are exchanged? + (enigo::Key::Divide, evdev::Key::KEY_KPSLASH), + (enigo::Key::Equals, evdev::Key::KEY_KPEQUAL), + (enigo::Key::NumpadEnter, evdev::Key::KEY_KPENTER), + (enigo::Key::RightAlt, evdev::Key::KEY_RIGHTALT), + (enigo::Key::RightControl, evdev::Key::KEY_RIGHTCTRL), + (enigo::Key::RightShift, evdev::Key::KEY_RIGHTSHIFT), + ]); + + static ref KEY_MAP_LAYOUT: HashMap = HashMap::from( + [ + ('a', (evdev::Key::KEY_A, false)), + ('b', (evdev::Key::KEY_B, false)), + ('c', (evdev::Key::KEY_C, false)), + ('d', (evdev::Key::KEY_D, false)), + ('e', (evdev::Key::KEY_E, false)), + ('f', (evdev::Key::KEY_F, false)), + ('g', (evdev::Key::KEY_G, false)), + ('h', (evdev::Key::KEY_H, false)), + ('i', (evdev::Key::KEY_I, false)), + ('j', (evdev::Key::KEY_J, false)), + ('k', (evdev::Key::KEY_K, false)), + ('l', (evdev::Key::KEY_L, false)), + ('m', (evdev::Key::KEY_M, false)), + ('n', (evdev::Key::KEY_N, false)), + ('o', (evdev::Key::KEY_O, false)), + ('p', (evdev::Key::KEY_P, false)), + ('q', (evdev::Key::KEY_Q, false)), + ('r', (evdev::Key::KEY_R, false)), + ('s', (evdev::Key::KEY_S, false)), + ('t', (evdev::Key::KEY_T, false)), + ('u', (evdev::Key::KEY_U, false)), + ('v', (evdev::Key::KEY_V, false)), + ('w', (evdev::Key::KEY_W, false)), + ('x', (evdev::Key::KEY_X, false)), + ('y', (evdev::Key::KEY_Y, false)), + ('z', (evdev::Key::KEY_Z, false)), + ('0', (evdev::Key::KEY_0, false)), + ('1', (evdev::Key::KEY_1, false)), + ('2', (evdev::Key::KEY_2, false)), + ('3', (evdev::Key::KEY_3, false)), + ('4', (evdev::Key::KEY_4, false)), + ('5', (evdev::Key::KEY_5, false)), + ('6', (evdev::Key::KEY_6, false)), + ('7', (evdev::Key::KEY_7, false)), + ('8', (evdev::Key::KEY_8, false)), + ('9', (evdev::Key::KEY_9, false)), + ('`', (evdev::Key::KEY_GRAVE, false)), + ('-', (evdev::Key::KEY_MINUS, false)), + ('=', (evdev::Key::KEY_EQUAL, false)), + ('[', (evdev::Key::KEY_LEFTBRACE, false)), + (']', (evdev::Key::KEY_RIGHTBRACE, false)), + ('\\', (evdev::Key::KEY_BACKSLASH, false)), + (',', (evdev::Key::KEY_COMMA, false)), + ('.', (evdev::Key::KEY_DOT, false)), + ('/', (evdev::Key::KEY_SLASH, false)), + (';', (evdev::Key::KEY_SEMICOLON, false)), + ('\'', (evdev::Key::KEY_APOSTROPHE, false)), + // Space is intentionally in both KEY_MAP_LAYOUT (char-to-evdev for text input) + // and KEY_MAP (Key::Space for key events). Both maps serve different lookup paths. + (' ', (evdev::Key::KEY_SPACE, false)), + + // Shift + key + ('A', (evdev::Key::KEY_A, true)), + ('B', (evdev::Key::KEY_B, true)), + ('C', (evdev::Key::KEY_C, true)), + ('D', (evdev::Key::KEY_D, true)), + ('E', (evdev::Key::KEY_E, true)), + ('F', (evdev::Key::KEY_F, true)), + ('G', (evdev::Key::KEY_G, true)), + ('H', (evdev::Key::KEY_H, true)), + ('I', (evdev::Key::KEY_I, true)), + ('J', (evdev::Key::KEY_J, true)), + ('K', (evdev::Key::KEY_K, true)), + ('L', (evdev::Key::KEY_L, true)), + ('M', (evdev::Key::KEY_M, true)), + ('N', (evdev::Key::KEY_N, true)), + ('O', (evdev::Key::KEY_O, true)), + ('P', (evdev::Key::KEY_P, true)), + ('Q', (evdev::Key::KEY_Q, true)), + ('R', (evdev::Key::KEY_R, true)), + ('S', (evdev::Key::KEY_S, true)), + ('T', (evdev::Key::KEY_T, true)), + ('U', (evdev::Key::KEY_U, true)), + ('V', (evdev::Key::KEY_V, true)), + ('W', (evdev::Key::KEY_W, true)), + ('X', (evdev::Key::KEY_X, true)), + ('Y', (evdev::Key::KEY_Y, true)), + ('Z', (evdev::Key::KEY_Z, true)), + (')', (evdev::Key::KEY_0, true)), + ('!', (evdev::Key::KEY_1, true)), + ('@', (evdev::Key::KEY_2, true)), + ('#', (evdev::Key::KEY_3, true)), + ('$', (evdev::Key::KEY_4, true)), + ('%', (evdev::Key::KEY_5, true)), + ('^', (evdev::Key::KEY_6, true)), + ('&', (evdev::Key::KEY_7, true)), + ('*', (evdev::Key::KEY_8, true)), + ('(', (evdev::Key::KEY_9, true)), + ('~', (evdev::Key::KEY_GRAVE, true)), + ('_', (evdev::Key::KEY_MINUS, true)), + ('+', (evdev::Key::KEY_EQUAL, true)), + ('{', (evdev::Key::KEY_LEFTBRACE, true)), + ('}', (evdev::Key::KEY_RIGHTBRACE, true)), + ('|', (evdev::Key::KEY_BACKSLASH, true)), + ('<', (evdev::Key::KEY_COMMA, true)), + ('>', (evdev::Key::KEY_DOT, true)), + ('?', (evdev::Key::KEY_SLASH, true)), + (':', (evdev::Key::KEY_SEMICOLON, true)), + ('"', (evdev::Key::KEY_APOSTROPHE, true)), + ]); + + // ((minx, maxx), (miny, maxy)) + static ref RESOLUTION: Mutex<((i32, i32), (i32, i32))> = Mutex::new(((0, 0), (0, 0))); + } + + /// Input text on Wayland using layout-independent methods. + /// ASCII chars (0x20-0x7E): Portal keysym or uinput fallback + /// Non-ASCII chars: skipped — this runs in the --service (root) process where clipboard + /// operations are unreliable (typically no user session environment). + /// Non-ASCII input is normally handled by the --server process via input_text_via_clipboard_server. + fn input_text_wayland(text: &str, keyboard: &mut VirtualDevice) { + let portal_info = { + let session_info = RDP_SESSION_INFO.lock().unwrap(); + session_info + .as_ref() + .map(|info| (info.conn.clone(), info.session.clone())) + }; + + for c in text.chars() { + let keysym = char_to_keysym(c); + if can_input_via_keysym(c, keysym) { + // Try Portal first — down+up on the same channel + if let Some((ref conn, ref session)) = portal_info { + let portal = scrap::wayland::pipewire::get_portal(conn); + if portal + .notify_keyboard_keysym(session, HashMap::new(), keysym, 1) + .is_ok() + { + if let Err(e) = + portal.notify_keyboard_keysym(session, HashMap::new(), keysym, 0) + { + log::warn!( + "input_text_wayland: portal key-up failed for keysym {:#x}: {:?}", + keysym, + e + ); + } + continue; + } + } + // Portal unavailable or failed, fallback to uinput (down+up together) + let key = enigo::Key::Layout(c); + if let Ok((evdev_key, is_shift)) = map_key(&key) { + let mut shift_pressed = false; + if is_shift { + let shift_down = + InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1); + if keyboard.emit(&[shift_down]).is_ok() { + shift_pressed = true; + } else { + log::warn!("input_text_wayland: failed to press Shift for '{}'", c); + } + } + let key_down = InputEvent::new(EventType::KEY, evdev_key.code(), 1); + let key_up = InputEvent::new(EventType::KEY, evdev_key.code(), 0); + allow_err!(keyboard.emit(&[key_down, key_up])); + if shift_pressed { + let shift_up = + InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 0); + allow_err!(keyboard.emit(&[shift_up])); + } + } + } else { + log::debug!("Skipping non-ASCII character in uinput service (no clipboard access)"); + } + } + } + + /// Send a single key down or up event for a Layout character. + /// Used by KeyDown/KeyUp to maintain correct press/release semantics. + /// `down`: true for key press, false for key release. + fn input_char_wayland_key_event(chr: char, down: bool, keyboard: &mut VirtualDevice) { + let keysym = char_to_keysym(chr); + let portal_state: u32 = if down { 1 } else { 0 }; + + if can_input_via_keysym(chr, keysym) { + let portal_info = { + let session_info = RDP_SESSION_INFO.lock().unwrap(); + session_info + .as_ref() + .map(|info| (info.conn.clone(), info.session.clone())) + }; + if let Some((ref conn, ref session)) = portal_info { + let portal = scrap::wayland::pipewire::get_portal(conn); + if portal + .notify_keyboard_keysym(session, HashMap::new(), keysym, portal_state) + .is_ok() + { + return; + } + } + // Portal unavailable or failed, fallback to uinput + let key = enigo::Key::Layout(chr); + if let Ok((evdev_key, is_shift)) = map_key(&key) { + if down { + // Press: Shift↓ (if needed) → Key↓ + if is_shift { + let shift_down = + InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1); + if let Err(e) = keyboard.emit(&[shift_down]) { + log::warn!("input_char_wayland_key_event: failed to press Shift for '{}': {:?}", chr, e); + } + } + let key_down = InputEvent::new(EventType::KEY, evdev_key.code(), 1); + allow_err!(keyboard.emit(&[key_down])); + } else { + // Release: Key↑ → Shift↑ (if needed) + let key_up = InputEvent::new(EventType::KEY, evdev_key.code(), 0); + allow_err!(keyboard.emit(&[key_up])); + if is_shift { + let shift_up = + InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 0); + if let Err(e) = keyboard.emit(&[shift_up]) { + log::warn!("input_char_wayland_key_event: failed to release Shift for '{}': {:?}", chr, e); + } + } + } + } + } else { + // Non-ASCII: no reliable down/up semantics available. + // Clipboard paste is atomic and handled elsewhere. + log::debug!( + "Skipping non-ASCII character key {} in uinput service", + if down { "down" } else { "up" } + ); + } + } + + /// Check if character can be input via keysym (ASCII printable with valid keysym). + #[inline] + pub(crate) fn can_input_via_keysym(c: char, keysym: i32) -> bool { + // ASCII printable: 0x20 (space) to 0x7E (tilde) + (c as u32 >= 0x20 && c as u32 <= 0x7E) && keysym != 0 + } + + /// Convert a Unicode character to X11 keysym. + pub(crate) fn char_to_keysym(c: char) -> i32 { + let codepoint = c as u32; + if codepoint == 0 { + // Null character has no keysym + 0 + } else if (0x20..=0x7E).contains(&codepoint) { + // ASCII printable (0x20-0x7E): keysym == Unicode codepoint + codepoint as i32 + } else if (0xA0..=0xFF).contains(&codepoint) { + // Latin-1 supplement (0xA0-0xFF): keysym == Unicode codepoint (per X11 keysym spec) + codepoint as i32 + } else { + // Everything else (control chars 0x01-0x1F, DEL 0x7F, and all other non-ASCII Unicode): + // keysym = 0x01000000 | codepoint (X11 Unicode keysym encoding) + (0x0100_0000 | codepoint) as i32 + } + } + + fn create_uinput_keyboard() -> ResultType { + // TODO: ensure keys here + let mut keys = AttributeSet::::new(); + for i in evdev::Key::KEY_ESC.code()..(evdev::Key::BTN_TRIGGER_HAPPY40.code() + 1) { + let key = evdev::Key::new(i); + if !format!("{:?}", &key).contains("unknown key") { + keys.insert(key); + } + } + let mut leds = AttributeSet::::new(); + leds.insert(evdev::LedType::LED_NUML); + leds.insert(evdev::LedType::LED_CAPSL); + leds.insert(evdev::LedType::LED_SCROLLL); + let mut miscs = AttributeSet::::new(); + miscs.insert(evdev::MiscType::MSC_SCAN); + let keyboard = VirtualDeviceBuilder::new()? + .name("RustDesk UInput Keyboard") + .with_keys(&keys)? + .with_leds(&leds)? + .with_miscs(&miscs)? + .build()?; + Ok(keyboard) + } + + pub fn map_key(key: &enigo::Key) -> ResultType<(evdev::Key, bool)> { + if let Some(k) = KEY_MAP.get(&key) { + log::trace!("mapkey matched in KEY_MAP, evdev={:?}", &k); + return Ok((k.clone(), false)); + } else { + match key { + enigo::Key::Layout(c) => { + if let Some((k, is_shift)) = KEY_MAP_LAYOUT.get(&c) { + log::trace!("mapkey Layout matched, evdev={:?}", k); + return Ok((k.clone(), is_shift.clone())); + } + } + // enigo::Key::Raw(c) => { + // let k = evdev::Key::new(c); + // if !format!("{:?}", &k).contains("unknown key") { + // return Ok(k.clone()); + // } + // } + _ => {} + } + } + bail!("Failed to map key {:?}", &key); + } + + async fn ipc_send_data(stream: &mut Connection, data: &Data) { + allow_err!(stream.send(data).await); + } + + async fn handle_keyboard( + stream: &mut Connection, + keyboard: &mut VirtualDevice, + data: &DataKeyboard, + ) { + let data_desc = match data { + DataKeyboard::Sequence(seq) => format!("Sequence(len={})", seq.len()), + DataKeyboard::KeyDown(Key::Layout(_)) + | DataKeyboard::KeyUp(Key::Layout(_)) + | DataKeyboard::KeyClick(Key::Layout(_)) => "Layout()".to_string(), + _ => format!("{:?}", data), + }; + log::trace!("handle_keyboard received: {}", data_desc); + match data { + DataKeyboard::Sequence(seq) => { + // Normally handled by --server process (input_text_via_clipboard_server). + // Fallback: input_text_wayland handles ASCII via keysym/uinput; + // non-ASCII will be skipped (no clipboard access in --service process). + if !seq.is_empty() { + input_text_wayland(seq, keyboard); + } + } + DataKeyboard::KeyDown(enigo::Key::Raw(code)) => { + if *code < 8 { + log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code); + } else { + let down_event = InputEvent::new(EventType::KEY, *code - 8, 1); + allow_err!(keyboard.emit(&[down_event])); + } + } + DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { + if *code < 8 { + log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code); + } else { + let up_event = InputEvent::new(EventType::KEY, *code - 8, 0); + allow_err!(keyboard.emit(&[up_event])); + } + } + DataKeyboard::KeyDown(key) => { + if let Key::Layout(chr) = key { + input_char_wayland_key_event(*chr, true, keyboard); + } else { + if let Ok((k, _is_shift)) = map_key(key) { + let down_event = InputEvent::new(EventType::KEY, k.code(), 1); + allow_err!(keyboard.emit(&[down_event])); + } + } + } + DataKeyboard::KeyUp(key) => { + if let Key::Layout(chr) = key { + input_char_wayland_key_event(*chr, false, keyboard); + } else { + if let Ok((k, _)) = map_key(key) { + let up_event = InputEvent::new(EventType::KEY, k.code(), 0); + allow_err!(keyboard.emit(&[up_event])); + } + } + } + DataKeyboard::KeyClick(key) => { + if let Key::Layout(chr) = key { + input_text_wayland(&chr.to_string(), keyboard); + } else { + if let Ok((k, _is_shift)) = map_key(key) { + let down_event = InputEvent::new(EventType::KEY, k.code(), 1); + let up_event = InputEvent::new(EventType::KEY, k.code(), 0); + allow_err!(keyboard.emit(&[down_event, up_event])); + } + } + } + DataKeyboard::GetKeyState(key) => { + let key_state = if enigo::Key::CapsLock == *key { + match keyboard.get_led_state() { + Ok(leds) => leds.contains(evdev::LedType::LED_CAPSL), + Err(_e) => { + // log::debug!("Failed to get led state {}", &_e); + false + } + } + } else if enigo::Key::NumLock == *key { + match keyboard.get_led_state() { + Ok(leds) => leds.contains(evdev::LedType::LED_NUML), + Err(_e) => { + // log::debug!("Failed to get led state {}", &_e); + false + } + } + } else { + match keyboard.get_key_state() { + Ok(keys) => match key { + enigo::Key::Shift => { + keys.contains(evdev::Key::KEY_LEFTSHIFT) + || keys.contains(evdev::Key::KEY_RIGHTSHIFT) + } + enigo::Key::Control => { + keys.contains(evdev::Key::KEY_LEFTCTRL) + || keys.contains(evdev::Key::KEY_RIGHTCTRL) + } + enigo::Key::Alt => { + keys.contains(evdev::Key::KEY_LEFTALT) + || keys.contains(evdev::Key::KEY_RIGHTALT) + } + enigo::Key::Meta => { + keys.contains(evdev::Key::KEY_LEFTMETA) + || keys.contains(evdev::Key::KEY_RIGHTMETA) + } + _ => false, + }, + Err(_e) => { + // log::debug!("Failed to get key state: {}", &_e); + false + } + } + }; + ipc_send_data( + stream, + &Data::KeyboardResponse(ipc::DataKeyboardResponse::GetKeyState(key_state)), + ) + .await; + } + } + } + + fn handle_mouse(mouse: &mut mouce::UInputMouseManager, data: &DataMouse) { + log::trace!("handle_mouse {:?}", &data); + match data { + DataMouse::MoveTo(x, y) => { + allow_err!(mouse.move_to(*x as _, *y as _)) + } + DataMouse::MoveRelative(x, y) => { + allow_err!(mouse.move_relative(*x, *y)) + } + DataMouse::Down(button) => { + let btn = match button { + enigo::MouseButton::Left => mouce::MouseButton::Left, + enigo::MouseButton::Middle => mouce::MouseButton::Middle, + enigo::MouseButton::Right => mouce::MouseButton::Right, + _ => { + return; + } + }; + allow_err!(mouse.press_button(&btn)) + } + DataMouse::Up(button) => { + let btn = match button { + enigo::MouseButton::Left => mouce::MouseButton::Left, + enigo::MouseButton::Middle => mouce::MouseButton::Middle, + enigo::MouseButton::Right => mouce::MouseButton::Right, + _ => { + return; + } + }; + allow_err!(mouse.release_button(&btn)) + } + DataMouse::Click(button) => { + let btn = match button { + enigo::MouseButton::Left => mouce::MouseButton::Left, + enigo::MouseButton::Middle => mouce::MouseButton::Middle, + enigo::MouseButton::Right => mouce::MouseButton::Right, + _ => { + return; + } + }; + allow_err!(mouse.click_button(&btn)) + } + DataMouse::ScrollX(_length) => { + // TODO: not supported for now + } + DataMouse::ScrollY(length) => { + let mut length = *length; + + let scroll = if length < 0 { + mouce::ScrollDirection::Up + } else { + mouce::ScrollDirection::Down + }; + + if length < 0 { + length = -length; + } + + for _ in 0..length { + allow_err!(mouse.scroll_wheel(&scroll)) + } + } + DataMouse::Refresh => { + // unreachable!() + } + } + } + + fn spawn_keyboard_handler(mut stream: Connection) { + log::debug!("spawn_keyboard_handler: new keyboard handler connection"); + tokio::spawn(async move { + let mut keyboard = match create_uinput_keyboard() { + Ok(keyboard) => { + log::debug!("UInput keyboard device created successfully"); + keyboard + } + Err(e) => { + log::error!("Failed to create keyboard {}", e); + return; + } + }; + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::info!("UInput keyboard ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match data { + Data::Keyboard(data) => { + handle_keyboard(&mut stream, &mut keyboard, &data).await; + } + _ => { + log::warn!("Unexpected data type in keyboard handler"); + } + } + } + _ => {} + } + } + } + } + }); + } + + fn spawn_mouse_handler(mut stream: ipc::Connection) { + let resolution = RESOLUTION.lock().unwrap(); + if resolution.0 .0 == resolution.0 .1 || resolution.1 .0 == resolution.1 .1 { + return; + } + let rng_x = resolution.0.clone(); + let rng_y = resolution.1.clone(); + tokio::spawn(async move { + log::info!( + "Create uinput mouce with rng_x: ({}, {}), rng_y: ({}, {})", + rng_x.0, + rng_x.1, + rng_y.0, + rng_y.1 + ); + let mut mouse = match mouce::UInputMouseManager::new(rng_x, rng_y) { + Ok(mouse) => mouse, + Err(e) => { + log::error!("Failed to create mouse, {}", e); + return; + } + }; + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::info!("UInput mouse ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match data { + Data::Mouse(data) => { + if let DataMouse::Refresh = data { + let resolution = RESOLUTION.lock().unwrap(); + let rng_x = resolution.0.clone(); + let rng_y = resolution.1.clone(); + log::info!( + "Refresh uinput mouce with rng_x: ({}, {}), rng_y: ({}, {})", + rng_x.0, + rng_x.1, + rng_y.0, + rng_y.1 + ); + mouse = match mouce::UInputMouseManager::new(rng_x, rng_y) { + Ok(mouse) => mouse, + Err(e) => { + log::error!("Failed to create mouse, {}", e); + return; + } + } + } else { + handle_mouse(&mut mouse, &data); + } + } + _ => { + } + } + } + _ => {} + } + } + } + } + }); + } + + fn spawn_controller_handler(mut stream: ipc::Connection) { + tokio::spawn(async move { + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(_err) => { + // log::info!("UInput controller ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match data { + Data::Control(data) => match data { + ipc::DataControl::Resolution{ + minx, + maxx, + miny, + maxy, + } => { + *RESOLUTION.lock().unwrap() = ((minx, maxx), (miny, maxy)); + allow_err!(stream.send(&Data::Empty).await); + } + } + _ => { + } + } + } + _ => {} + } + } + } + } + }); + } + + /// Start uinput service. + async fn start_service(postfix: &str, handler: F) { + match new_listener(postfix).await { + Ok(mut incoming) => { + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + log::debug!("Got new connection of uinput ipc {}", postfix); + handler(Connection::new(stream)); + } + Err(err) => { + log::error!("Couldn't get uinput mouse client: {:?}", err); + } + } + } + } + Err(err) => { + log::error!("Failed to start uinput mouse ipc service: {}", err); + } + } + } + + /// Start uinput keyboard service. + #[tokio::main(flavor = "current_thread")] + pub async fn start_service_keyboard() { + log::info!("start uinput keyboard service"); + start_service(IPC_POSTFIX_KEYBOARD, spawn_keyboard_handler).await; + } + + /// Start uinput mouse service. + #[tokio::main(flavor = "current_thread")] + pub async fn start_service_mouse() { + log::info!("start uinput mouse service"); + start_service(IPC_POSTFIX_MOUSE, spawn_mouse_handler).await; + } + + /// Start uinput mouse service. + #[tokio::main(flavor = "current_thread")] + pub async fn start_service_control() { + log::info!("start uinput control service"); + start_service(IPC_POSTFIX_CONTROL, spawn_controller_handler).await; + } + + pub fn stop_service_keyboard() { + log::info!("stop uinput keyboard service"); + } + pub fn stop_service_mouse() { + log::info!("stop uinput mouse service"); + } + pub fn stop_service_control() { + log::info!("stop uinput control service"); + } +} + +// https://github.com/emrebicer/mouce +mod mouce { + use std::{ + fs::File, + io::{Error, ErrorKind, Result}, + mem::size_of, + os::{ + raw::{c_char, c_int, c_long, c_uint, c_ulong, c_ushort}, + unix::{fs::OpenOptionsExt, io::AsRawFd}, + }, + thread, + time::Duration, + }; + + pub const O_NONBLOCK: c_int = 2048; + + /// ioctl and uinput definitions + const UI_ABS_SETUP: c_ulong = 1075598596; + const UI_SET_EVBIT: c_ulong = 1074025828; + const UI_SET_KEYBIT: c_ulong = 1074025829; + const UI_SET_RELBIT: c_ulong = 1074025830; + const UI_SET_ABSBIT: c_ulong = 1074025831; + const UI_DEV_SETUP: c_ulong = 1079792899; + const UI_DEV_CREATE: c_ulong = 21761; + const UI_DEV_DESTROY: c_uint = 21762; + + pub const EV_KEY: c_int = 0x01; + pub const EV_REL: c_int = 0x02; + pub const EV_ABS: c_int = 0x03; + pub const REL_X: c_uint = 0x00; + pub const REL_Y: c_uint = 0x01; + pub const ABS_X: c_uint = 0x00; + pub const ABS_Y: c_uint = 0x01; + pub const REL_WHEEL: c_uint = 0x08; + pub const REL_HWHEEL: c_uint = 0x06; + pub const BTN_LEFT: c_int = 0x110; + pub const BTN_RIGHT: c_int = 0x111; + pub const BTN_MIDDLE: c_int = 0x112; + pub const BTN_SIDE: c_int = 0x113; + pub const BTN_EXTRA: c_int = 0x114; + pub const BTN_FORWARD: c_int = 0x115; + pub const BTN_BACK: c_int = 0x116; + pub const BTN_TASK: c_int = 0x117; + const SYN_REPORT: c_int = 0x00; + const EV_SYN: c_int = 0x00; + const BUS_USB: c_ushort = 0x03; + + /// uinput types + #[repr(C)] + struct UInputSetup { + id: InputId, + name: [c_char; UINPUT_MAX_NAME_SIZE], + ff_effects_max: c_ulong, + } + + #[repr(C)] + struct InputId { + bustype: c_ushort, + vendor: c_ushort, + product: c_ushort, + version: c_ushort, + } + + #[repr(C)] + pub struct InputEvent { + pub time: TimeVal, + pub r#type: c_ushort, + pub code: c_ushort, + pub value: c_int, + } + + #[repr(C)] + pub struct TimeVal { + pub tv_sec: c_ulong, + pub tv_usec: c_ulong, + } + + #[repr(C)] + pub struct UinputAbsSetup { + pub code: c_ushort, + pub absinfo: InputAbsinfo, + } + + #[repr(C)] + pub struct InputAbsinfo { + pub value: c_int, + pub minimum: c_int, + pub maximum: c_int, + pub fuzz: c_int, + pub flat: c_int, + pub resolution: c_int, + } + + extern "C" { + fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int; + fn write(fd: c_int, buf: *mut InputEvent, count: usize) -> c_long; + } + + #[derive(Debug, Copy, Clone)] + pub enum MouseButton { + Left, + Middle, + Side, + Extra, + Right, + Back, + Forward, + Task, + } + + #[derive(Debug, Copy, Clone)] + pub enum ScrollDirection { + Up, + Down, + Right, + Left, + } + + const UINPUT_MAX_NAME_SIZE: usize = 80; + + pub struct UInputMouseManager { + uinput_file: File, + } + + impl UInputMouseManager { + pub fn new(rng_x: (i32, i32), rng_y: (i32, i32)) -> Result { + let manager = UInputMouseManager { + uinput_file: File::options() + .write(true) + .custom_flags(O_NONBLOCK) + .open("/dev/uinput")?, + }; + let fd = manager.uinput_file.as_raw_fd(); + unsafe { + // For press events (also needed for mouse movement) + ioctl(fd, UI_SET_EVBIT, EV_KEY); + ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); + ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT); + ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE); + + // For mouse movement + ioctl(fd, UI_SET_EVBIT, EV_ABS); + ioctl(fd, UI_SET_ABSBIT, ABS_X); + ioctl( + fd, + UI_ABS_SETUP, + &UinputAbsSetup { + code: ABS_X as _, + absinfo: InputAbsinfo { + value: 0, + minimum: rng_x.0, + maximum: rng_x.1, + fuzz: 0, + flat: 0, + resolution: 0, + }, + }, + ); + ioctl(fd, UI_SET_ABSBIT, ABS_Y); + ioctl( + fd, + UI_ABS_SETUP, + &UinputAbsSetup { + code: ABS_Y as _, + absinfo: InputAbsinfo { + value: 0, + minimum: rng_y.0, + maximum: rng_y.1, + fuzz: 0, + flat: 0, + resolution: 0, + }, + }, + ); + + ioctl(fd, UI_SET_EVBIT, EV_REL); + ioctl(fd, UI_SET_RELBIT, REL_X); + ioctl(fd, UI_SET_RELBIT, REL_Y); + ioctl(fd, UI_SET_RELBIT, REL_WHEEL); + ioctl(fd, UI_SET_RELBIT, REL_HWHEEL); + } + + let mut usetup = UInputSetup { + id: InputId { + bustype: BUS_USB, + // Random vendor and product + vendor: 0x2222, + product: 0x3333, + version: 0, + }, + name: [0; UINPUT_MAX_NAME_SIZE], + ff_effects_max: 0, + }; + + let mut device_bytes: Vec = "mouce-library-fake-mouse" + .chars() + .map(|ch| ch as c_char) + .collect(); + + // Fill the rest of the name buffer with empty chars + for _ in 0..UINPUT_MAX_NAME_SIZE - device_bytes.len() { + device_bytes.push('\0' as c_char); + } + + usetup.name.copy_from_slice(&device_bytes); + + unsafe { + ioctl(fd, UI_DEV_SETUP, &usetup); + ioctl(fd, UI_DEV_CREATE); + } + + // On UI_DEV_CREATE the kernel will create the device node for this + // device. We are inserting a pause here so that userspace has time + // to detect, initialize the new device, and can start listening to + // the event, otherwise it will not notice the event we are about to send. + thread::sleep(Duration::from_millis(300)); + + Ok(manager) + } + + /// Write the given event to the uinput file + fn emit(&self, r#type: c_int, code: c_int, value: c_int) -> Result<()> { + let mut event = InputEvent { + time: TimeVal { + tv_sec: 0, + tv_usec: 0, + }, + r#type: r#type as c_ushort, + code: code as c_ushort, + value, + }; + let fd = self.uinput_file.as_raw_fd(); + + unsafe { + let count = size_of::(); + let written_bytes = write(fd, &mut event, count); + if written_bytes == -1 || written_bytes != count as c_long { + return Err(Error::new( + ErrorKind::Other, + format!("failed while trying to write to a file"), + )); + } + } + + Ok(()) + } + + /// Syncronize the device + fn syncronize(&self) -> Result<()> { + self.emit(EV_SYN, SYN_REPORT, 0)?; + // Give uinput some time to update the mouse location, + // otherwise it fails to move the mouse on release mode + // A delay of 1 milliseconds seems to be enough for it + thread::sleep(Duration::from_millis(1)); + Ok(()) + } + + /// Move the mouse relative to the current position + fn move_relative_(&self, x: i32, y: i32) -> Result<()> { + // uinput does not move the mouse in pixels but uses `units`. I couldn't + // find information regarding to this uinput `unit`, but according to + // my findings 1 unit corresponds to exactly 2 pixels. + // + // To achieve the expected behavior; divide the parameters by 2 + // + // This seems like there is a bug in this crate, but the + // behavior is the same on other projects that make use of + // uinput. e.g. `ydotool`. When you try to move your mouse, + // it will move 2x further pixels + self.emit(EV_REL, REL_X as c_int, (x as f32 / 2.).ceil() as c_int)?; + self.emit(EV_REL, REL_Y as c_int, (y as f32 / 2.).ceil() as c_int)?; + self.syncronize() + } + + fn map_btn(button: &MouseButton) -> c_int { + match button { + MouseButton::Left => BTN_LEFT, + MouseButton::Right => BTN_RIGHT, + MouseButton::Middle => BTN_MIDDLE, + MouseButton::Side => BTN_SIDE, + MouseButton::Extra => BTN_EXTRA, + MouseButton::Forward => BTN_FORWARD, + MouseButton::Back => BTN_BACK, + MouseButton::Task => BTN_TASK, + } + } + + pub fn move_to(&self, x: usize, y: usize) -> Result<()> { + // // For some reason, absolute mouse move events are not working on uinput + // // (as I understand those events are intended for touch events) + // // + // // As a work around solution; first set the mouse to top left, then + // // call relative move function to simulate an absolute move event + //self.move_relative(i32::MIN, i32::MIN)?; + //self.move_relative(x as i32, y as i32) + + self.emit(EV_ABS, ABS_X as c_int, x as c_int)?; + self.emit(EV_ABS, ABS_Y as c_int, y as c_int)?; + self.syncronize() + } + + pub fn move_relative(&self, x_offset: i32, y_offset: i32) -> Result<()> { + self.move_relative_(x_offset, y_offset) + } + + pub fn press_button(&self, button: &MouseButton) -> Result<()> { + self.emit(EV_KEY, Self::map_btn(button), 1)?; + self.syncronize() + } + + pub fn release_button(&self, button: &MouseButton) -> Result<()> { + self.emit(EV_KEY, Self::map_btn(button), 0)?; + self.syncronize() + } + + pub fn click_button(&self, button: &MouseButton) -> Result<()> { + self.press_button(button)?; + self.release_button(button) + } + + pub fn scroll_wheel(&self, direction: &ScrollDirection) -> Result<()> { + let (code, scroll_value) = match direction { + ScrollDirection::Up => (REL_WHEEL, 1), + ScrollDirection::Down => (REL_WHEEL, -1), + ScrollDirection::Left => (REL_HWHEEL, -1), + ScrollDirection::Right => (REL_HWHEEL, 1), + }; + self.emit(EV_REL, code as c_int, scroll_value)?; + self.syncronize() + } + } + + impl Drop for UInputMouseManager { + fn drop(&mut self) { + let fd = self.uinput_file.as_raw_fd(); + unsafe { + // Destroy the device, the file is closed automatically by the File module + ioctl(fd, UI_DEV_DESTROY as c_ulong); + } + } + } +} diff --git a/vendor/rustdesk/src/server/video_qos.rs b/vendor/rustdesk/src/server/video_qos.rs new file mode 100644 index 0000000..b02f6ad --- /dev/null +++ b/vendor/rustdesk/src/server/video_qos.rs @@ -0,0 +1,595 @@ +use super::*; +use scrap::codec::{Quality, BR_BALANCED, BR_BEST, BR_SPEED}; +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; + +/* +FPS adjust: +a. new user connected =>set to INIT_FPS +b. TestDelay receive => update user's fps according to network delay + When network delay < DELAY_THRESHOLD_150MS, set minimum fps according to image quality, and increase fps; + When network delay >= DELAY_THRESHOLD_150MS, set minimum fps according to image quality, and decrease fps; +c. second timeout / TestDelay receive => update real fps to the minimum fps from all users + +ratio adjust: +a. user set image quality => update to the maximum ratio of the latest quality +b. 3 seconds timeout => update ratio according to network delay + When network delay < DELAY_THRESHOLD_150MS, increase ratio, max 150kbps; + When network delay >= DELAY_THRESHOLD_150MS, decrease ratio; + +adjust between FPS and ratio: + When network delay < DELAY_THRESHOLD_150MS, fps is always higher than the minimum fps, and ratio is increasing; + When network delay >= DELAY_THRESHOLD_150MS, fps is always lower than the minimum fps, and ratio is decreasing; + +delay: + use delay minus RTT as the actual network delay +*/ + +// Constants +pub const FPS: u32 = 30; +pub const MIN_FPS: u32 = 1; +pub const MAX_FPS: u32 = 120; +pub const INIT_FPS: u32 = 15; + +// Bitrate ratio constants for different quality levels +const BR_MAX: f32 = 40.0; // 2000 * 2 / 100 +const BR_MIN: f32 = 0.2; +const BR_MIN_HIGH_RESOLUTION: f32 = 0.1; // For high resolution, BR_MIN is still too high, so we set a lower limit +const MAX_BR_MULTIPLE: f32 = 1.0; + +const HISTORY_DELAY_LEN: usize = 2; +const ADJUST_RATIO_INTERVAL: usize = 3; // Adjust quality ratio every 3 seconds +const DYNAMIC_SCREEN_THRESHOLD: usize = 2; // Allow increase quality ratio if encode more than 2 times in one second +const DELAY_THRESHOLD_150MS: u32 = 150; // 150ms is the threshold for good network condition + +#[derive(Default, Debug, Clone)] +struct UserDelay { + response_delayed: bool, + delay_history: VecDeque, + fps: Option, + rtt_calculator: RttCalculator, + quick_increase_fps_count: usize, + increase_fps_count: usize, +} + +impl UserDelay { + fn add_delay(&mut self, delay: u32) { + self.rtt_calculator.update(delay); + if self.delay_history.len() > HISTORY_DELAY_LEN { + self.delay_history.pop_front(); + } + self.delay_history.push_back(delay); + } + + // Average delay minus RTT + fn avg_delay(&self) -> u32 { + let len = self.delay_history.len(); + if len > 0 { + let avg_delay = self.delay_history.iter().sum::() / len as u32; + + // If RTT is available, subtract it from average delay to get actual network latency + if let Some(rtt) = self.rtt_calculator.get_rtt() { + if avg_delay > rtt { + avg_delay - rtt + } else { + avg_delay + } + } else { + avg_delay + } + } else { + DELAY_THRESHOLD_150MS + } + } +} + +// User session data structure +#[derive(Default, Debug, Clone)] +struct UserData { + auto_adjust_fps: Option, // reserve for compatibility + custom_fps: Option, + quality: Option<(i64, Quality)>, // (time, quality) + delay: UserDelay, + record: bool, +} + +#[derive(Default, Debug, Clone)] +struct DisplayData { + send_counter: usize, // Number of times encode during period + support_changing_quality: bool, +} + +// Main QoS controller structure +pub struct VideoQoS { + fps: u32, + ratio: f32, + users: HashMap, + displays: HashMap, + bitrate_store: u32, + adjust_ratio_instant: Instant, + abr_config: bool, + new_user_instant: Instant, +} + +impl Default for VideoQoS { + fn default() -> Self { + VideoQoS { + fps: FPS, + ratio: BR_BALANCED, + users: Default::default(), + displays: Default::default(), + bitrate_store: 0, + adjust_ratio_instant: Instant::now(), + abr_config: true, + new_user_instant: Instant::now(), + } + } +} + +// Basic functionality +impl VideoQoS { + // Calculate seconds per frame based on current FPS + pub fn spf(&self) -> Duration { + Duration::from_secs_f32(1. / (self.fps() as f32)) + } + + // Get current FPS within valid range + pub fn fps(&self) -> u32 { + let fps = self.fps; + if fps >= MIN_FPS && fps <= MAX_FPS { + fps + } else { + FPS + } + } + + // Store bitrate for later use + pub fn store_bitrate(&mut self, bitrate: u32) { + self.bitrate_store = bitrate; + } + + // Get stored bitrate + pub fn bitrate(&self) -> u32 { + self.bitrate_store + } + + // Get current bitrate ratio with bounds checking + pub fn ratio(&mut self) -> f32 { + if self.ratio < BR_MIN_HIGH_RESOLUTION || self.ratio > BR_MAX { + self.ratio = BR_BALANCED; + } + self.ratio + } + + // Check if any user is in recording mode + pub fn record(&self) -> bool { + self.users.iter().any(|u| u.1.record) + } + + pub fn set_support_changing_quality(&mut self, video_service_name: &str, support: bool) { + if let Some(display) = self.displays.get_mut(video_service_name) { + display.support_changing_quality = support; + } + } + + // Check if variable bitrate encoding is supported and enabled + pub fn in_vbr_state(&self) -> bool { + self.abr_config && self.displays.iter().all(|e| e.1.support_changing_quality) + } +} + +// User session management +impl VideoQoS { + // Initialize new user session + pub fn on_connection_open(&mut self, id: i32) { + self.users.insert(id, UserData::default()); + self.abr_config = Config::get_option("enable-abr") != "N"; + self.new_user_instant = Instant::now(); + } + + // Clean up user session + pub fn on_connection_close(&mut self, id: i32) { + self.users.remove(&id); + if self.users.is_empty() { + *self = Default::default(); + } + } + + pub fn user_custom_fps(&mut self, id: i32, fps: u32) { + if fps < MIN_FPS || fps > MAX_FPS { + return; + } + if let Some(user) = self.users.get_mut(&id) { + user.custom_fps = Some(fps); + } + } + + pub fn user_auto_adjust_fps(&mut self, id: i32, fps: u32) { + if fps < MIN_FPS || fps > MAX_FPS { + return; + } + if let Some(user) = self.users.get_mut(&id) { + user.auto_adjust_fps = Some(fps); + } + } + + pub fn user_image_quality(&mut self, id: i32, image_quality: i32) { + let convert_quality = |q: i32| -> Quality { + if q == ImageQuality::Balanced.value() { + Quality::Balanced + } else if q == ImageQuality::Low.value() { + Quality::Low + } else if q == ImageQuality::Best.value() { + Quality::Best + } else { + let b = ((q >> 8 & 0xFFF) * 2) as f32 / 100.0; + Quality::Custom(b.clamp(BR_MIN, BR_MAX)) + } + }; + + let quality = Some((hbb_common::get_time(), convert_quality(image_quality))); + if let Some(user) = self.users.get_mut(&id) { + user.quality = quality; + // update ratio directly + self.ratio = self.latest_quality().ratio(); + } + } + + pub fn user_record(&mut self, id: i32, v: bool) { + if let Some(user) = self.users.get_mut(&id) { + user.record = v; + } + } + + pub fn user_network_delay(&mut self, id: i32, delay: u32) { + let highest_fps = self.highest_fps(); + let target_ratio = self.latest_quality().ratio(); + + // For bad network, small fps means quick reaction and high quality + let (min_fps, normal_fps) = if target_ratio >= BR_BEST { + (8, 16) + } else if target_ratio >= BR_BALANCED { + (10, 20) + } else { + (12, 24) + }; + + // Calculate minimum acceptable delay-fps product + let dividend_ms = DELAY_THRESHOLD_150MS * min_fps; + + let mut adjust_ratio = false; + if let Some(user) = self.users.get_mut(&id) { + let delay = delay.max(10); + let old_avg_delay = user.delay.avg_delay(); + user.delay.add_delay(delay); + let mut avg_delay = user.delay.avg_delay(); + avg_delay = avg_delay.max(10); + let mut fps = self.fps; + + // Adaptive FPS adjustment based on network delay: + if avg_delay < 50 { + user.delay.quick_increase_fps_count += 1; + let mut step = if fps < normal_fps { 1 } else { 0 }; + if user.delay.quick_increase_fps_count >= 3 { + // After 3 consecutive good samples, increase more aggressively + user.delay.quick_increase_fps_count = 0; + step = 5; + } + fps = min_fps.max(fps + step); + } else if avg_delay < 100 { + let step = if avg_delay < old_avg_delay { + if fps < normal_fps { + 1 + } else { + 0 + } + } else { + 0 + }; + fps = min_fps.max(fps + step); + } else if avg_delay < DELAY_THRESHOLD_150MS { + fps = min_fps.max(fps); + } else { + let devide_fps = ((fps as f32) / (avg_delay as f32 / DELAY_THRESHOLD_150MS as f32)) + .ceil() as u32; + if avg_delay < 200 { + fps = min_fps.max(devide_fps); + } else if avg_delay < 300 { + fps = min_fps.min(devide_fps); + } else if avg_delay < 600 { + fps = dividend_ms / avg_delay; + } else { + fps = (dividend_ms / avg_delay).min(devide_fps); + } + } + + if avg_delay < DELAY_THRESHOLD_150MS { + user.delay.increase_fps_count += 1; + } else { + user.delay.increase_fps_count = 0; + } + if user.delay.increase_fps_count >= 3 { + // After 3 stable samples, try increasing FPS + user.delay.increase_fps_count = 0; + fps += 1; + } + + // Reset quick increase counter if network condition worsens + if avg_delay > 50 { + user.delay.quick_increase_fps_count = 0; + } + + fps = fps.clamp(MIN_FPS, highest_fps); + // first network delay message + adjust_ratio = user.delay.fps.is_none(); + user.delay.fps = Some(fps); + } + self.adjust_fps(); + if adjust_ratio && !cfg!(target_os = "linux") { + //Reduce the possibility of vaapi being created twice + self.adjust_ratio(false); + } + } + + pub fn user_delay_response_elapsed(&mut self, id: i32, elapsed: u128) { + if let Some(user) = self.users.get_mut(&id) { + user.delay.response_delayed = elapsed > 2000; + if user.delay.response_delayed { + user.delay.add_delay(elapsed as u32); + self.adjust_fps(); + } + } + } +} + +// Common adjust functions +impl VideoQoS { + pub fn new_display(&mut self, video_service_name: String) { + self.displays + .insert(video_service_name, DisplayData::default()); + } + + pub fn remove_display(&mut self, video_service_name: &str) { + self.displays.remove(video_service_name); + } + + pub fn update_display_data(&mut self, video_service_name: &str, send_counter: usize) { + if let Some(display) = self.displays.get_mut(video_service_name) { + display.send_counter += send_counter; + } + self.adjust_fps(); + let abr_enabled = self.in_vbr_state(); + if abr_enabled { + if self.adjust_ratio_instant.elapsed().as_secs() >= ADJUST_RATIO_INTERVAL as u64 { + let dynamic_screen = self + .displays + .iter() + .any(|d| d.1.send_counter >= ADJUST_RATIO_INTERVAL * DYNAMIC_SCREEN_THRESHOLD); + self.displays.iter_mut().for_each(|d| { + d.1.send_counter = 0; + }); + self.adjust_ratio(dynamic_screen); + } + } else { + self.ratio = self.latest_quality().ratio(); + } + } + + #[inline] + fn highest_fps(&self) -> u32 { + let user_fps = |u: &UserData| { + let mut fps = u.custom_fps.unwrap_or(FPS); + if let Some(auto_adjust_fps) = u.auto_adjust_fps { + if fps == 0 || auto_adjust_fps < fps { + fps = auto_adjust_fps; + } + } + fps + }; + + let fps = self + .users + .iter() + .map(|(_, u)| user_fps(u)) + .filter(|u| *u >= MIN_FPS) + .min() + .unwrap_or(FPS); + + fps.clamp(MIN_FPS, MAX_FPS) + } + + // Get latest quality settings from all users + pub fn latest_quality(&self) -> Quality { + self.users + .iter() + .map(|(_, u)| u.quality) + .filter(|q| *q != None) + .max_by(|a, b| a.unwrap_or_default().0.cmp(&b.unwrap_or_default().0)) + .flatten() + .unwrap_or((0, Quality::Balanced)) + .1 + } + + // Adjust quality ratio based on network delay and screen changes + fn adjust_ratio(&mut self, dynamic_screen: bool) { + if !self.in_vbr_state() { + return; + } + // Get maximum delay from all users + let max_delay = self.users.iter().map(|u| u.1.delay.avg_delay()).max(); + let Some(max_delay) = max_delay else { + return; + }; + + let target_quality = self.latest_quality(); + let target_ratio = self.latest_quality().ratio(); + let current_ratio = self.ratio; + let current_bitrate = self.bitrate(); + + // Calculate minimum ratio for high resolution (1Mbps baseline) + let ratio_1mbps = if current_bitrate > 0 { + Some((current_ratio * 1000.0 / current_bitrate as f32).max(BR_MIN_HIGH_RESOLUTION)) + } else { + None + }; + + // Calculate ratio for adding 150kbps bandwidth + let ratio_add_150kbps = if current_bitrate > 0 { + Some((current_bitrate + 150) as f32 * current_ratio / current_bitrate as f32) + } else { + None + }; + + // Set minimum ratio based on quality mode + let min = match target_quality { + Quality::Best => { + // For Best quality, ensure minimum 1Mbps for high resolution + let mut min = BR_BEST / 2.5; + if let Some(ratio_1mbps) = ratio_1mbps { + if min > ratio_1mbps { + min = ratio_1mbps; + } + } + min.max(BR_MIN) + } + Quality::Balanced => { + let mut min = (BR_BALANCED / 2.0).min(0.4); + if let Some(ratio_1mbps) = ratio_1mbps { + if min > ratio_1mbps { + min = ratio_1mbps; + } + } + min.max(BR_MIN_HIGH_RESOLUTION) + } + Quality::Low => BR_MIN_HIGH_RESOLUTION, + Quality::Custom(_) => BR_MIN_HIGH_RESOLUTION, + }; + let max = target_ratio * MAX_BR_MULTIPLE; + + let mut v = current_ratio; + + // Adjust ratio based on network delay thresholds + if max_delay < 50 { + if dynamic_screen { + v = current_ratio * 1.15; + } + } else if max_delay < 100 { + if dynamic_screen { + v = current_ratio * 1.1; + } + } else if max_delay < DELAY_THRESHOLD_150MS { + if dynamic_screen { + v = current_ratio * 1.05; + } + } else if max_delay < 200 { + v = current_ratio * 0.95; + } else if max_delay < 300 { + v = current_ratio * 0.9; + } else if max_delay < 500 { + v = current_ratio * 0.85; + } else { + v = current_ratio * 0.8; + } + + // Limit quality increase rate for better stability + if let Some(ratio_add_150kbps) = ratio_add_150kbps { + if v > ratio_add_150kbps + && ratio_add_150kbps > current_ratio + && current_ratio >= BR_SPEED + { + v = ratio_add_150kbps; + } + } + + self.ratio = v.clamp(min, max); + self.adjust_ratio_instant = Instant::now(); + } + + // Adjust fps based on network delay and user response time + fn adjust_fps(&mut self) { + let highest_fps = self.highest_fps(); + // Get minimum fps from all users + let mut fps = self + .users + .iter() + .map(|u| u.1.delay.fps.unwrap_or(INIT_FPS)) + .min() + .unwrap_or(INIT_FPS); + + if self.users.iter().any(|u| u.1.delay.response_delayed) { + if fps > MIN_FPS + 1 { + fps = MIN_FPS + 1; + } + } + + // For new connections (within 1 second), cap fps to INIT_FPS to ensure stability + if self.new_user_instant.elapsed().as_secs() < 1 { + if fps > INIT_FPS { + fps = INIT_FPS; + } + } + + // Ensure fps stays within valid range + self.fps = fps.clamp(MIN_FPS, highest_fps); + } +} + +#[derive(Default, Debug, Clone)] +struct RttCalculator { + min_rtt: Option, // Historical minimum RTT ever observed + window_min_rtt: Option, // Minimum RTT within last 60 samples + smoothed_rtt: Option, // Smoothed RTT estimation + samples: VecDeque, // Last 60 RTT samples +} + +impl RttCalculator { + const WINDOW_SAMPLES: usize = 60; // Keep last 60 samples + const MIN_SAMPLES: usize = 10; // Require at least 10 samples + const ALPHA: f32 = 0.5; // Smoothing factor for weighted average + + /// Update RTT estimates with a new sample + pub fn update(&mut self, delay: u32) { + // 1. Update historical minimum RTT + match self.min_rtt { + Some(min_rtt) if delay < min_rtt => self.min_rtt = Some(delay), + None => self.min_rtt = Some(delay), + _ => {} + } + + // 2. Update sample window + if self.samples.len() >= Self::WINDOW_SAMPLES { + self.samples.pop_front(); + } + self.samples.push_back(delay); + + // 3. Calculate minimum RTT within the window + self.window_min_rtt = self.samples.iter().min().copied(); + + // 4. Calculate smoothed RTT + // Use weighted average if we have enough samples + if self.samples.len() >= Self::WINDOW_SAMPLES { + if let (Some(min), Some(window_min)) = (self.min_rtt, self.window_min_rtt) { + // Weighted average of historical minimum and window minimum + let new_srtt = + ((1.0 - Self::ALPHA) * min as f32 + Self::ALPHA * window_min as f32) as u32; + self.smoothed_rtt = Some(new_srtt); + } + } + } + + /// Get current RTT estimate + /// Returns None if no valid estimation is available + pub fn get_rtt(&self) -> Option { + if let Some(rtt) = self.smoothed_rtt { + return Some(rtt); + } + if self.samples.len() >= Self::MIN_SAMPLES { + if let Some(rtt) = self.min_rtt { + return Some(rtt); + } + } + None + } +} diff --git a/vendor/rustdesk/src/server/video_service.rs b/vendor/rustdesk/src/server/video_service.rs new file mode 100644 index 0000000..13a781c --- /dev/null +++ b/vendor/rustdesk/src/server/video_service.rs @@ -0,0 +1,1419 @@ +// 24FPS (actually 23.976FPS) is what video professionals ages ago determined to be the +// slowest playback rate that still looks smooth enough to feel real. +// Our eyes can see a slight difference and even though 30FPS actually shows +// more information and is more realistic. +// 60FPS is commonly used in game, teamviewer 12 support this for video editing user. + +// how to capture with mouse cursor: +// https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/desktop-dup-api?redirectedfrom=MSDN + +// RECORD: The following Project has implemented audio capture, hardware codec and mouse cursor drawn. +// https://github.com/PHZ76/DesktopSharing + +// dxgi memory leak issue +// https://stackoverflow.com/questions/47801238/memory-leak-in-creating-direct2d-device +// but per my test, it is more related to AcquireNextFrame, +// https://forums.developer.nvidia.com/t/dxgi-outputduplication-memory-leak-when-using-nv-but-not-amd-drivers/108582 + +// to-do: +// https://slhck.info/video/2017/03/01/rate-control.html + +use super::{display_service::check_display_changed, service::ServiceTmpl, video_qos::VideoQoS, *}; +#[cfg(target_os = "linux")] +use crate::common::SimpleCallOnReturn; +#[cfg(target_os = "linux")] +use crate::platform::linux::is_x11; +use crate::privacy_mode::{get_privacy_mode_conn_id, INVALID_PRIVACY_MODE_CONN_ID}; +#[cfg(windows)] +use crate::{ + platform::windows::is_process_consent_running, + privacy_mode::{is_current_privacy_mode_impl, PRIVACY_MODE_IMPL_WIN_MAG}, + ui_interface::is_installed, +}; +use hbb_common::{ + anyhow::anyhow, + config, + tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex as TokioMutex, + }, +}; +#[cfg(feature = "hwcodec")] +use scrap::hwcodec::{HwRamEncoder, HwRamEncoderConfig}; +#[cfg(feature = "vram")] +use scrap::vram::{VRamEncoder, VRamEncoderConfig}; +#[cfg(not(windows))] +use scrap::Capturer; +use scrap::{ + aom::AomEncoderConfig, + codec::{Encoder, EncoderCfg}, + record::{Recorder, RecorderContext}, + vpxcodec::{VpxEncoderConfig, VpxVideoCodecId}, + CodecFormat, Display, EncodeInput, TraitCapturer, TraitPixelBuffer, +}; +#[cfg(windows)] +use std::sync::Once; +use std::{ + collections::HashSet, + io::ErrorKind::WouldBlock, + ops::{Deref, DerefMut}, + time::{self, Duration, Instant}, +}; + +pub const OPTION_REFRESH: &'static str = "refresh"; + +type FrameFetchedNotifierSender = UnboundedSender<(i32, Option)>; +type FrameFetchedNotifierReceiver = Arc)>>>; + +lazy_static::lazy_static! { + static ref FRAME_FETCHED_NOTIFIERS: Mutex> = Mutex::new(HashMap::default()); + + // display_idx -> set of conn id. + // Used to record which connections need to be notified when + // 1. A new frame is received from a web client. + // Because web client does not send the display index in message `VideoReceived`. + // 2. The client is closing. + static ref DISPLAY_CONN_IDS: Arc>>> = Default::default(); + pub static ref VIDEO_QOS: Arc> = Default::default(); + pub static ref IS_UAC_RUNNING: Arc> = Default::default(); + pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc> = Default::default(); + static ref SCREENSHOTS: Mutex> = Default::default(); +} + +struct Screenshot { + sid: String, + tx: Sender, + restore_vram: bool, +} + +#[inline] +pub fn notify_video_frame_fetched(display_idx: usize, conn_id: i32, frame_tm: Option) { + if let Some(notifier) = FRAME_FETCHED_NOTIFIERS.lock().unwrap().get(&display_idx) { + notifier.0.send((conn_id, frame_tm)).ok(); + } +} + +#[inline] +pub fn notify_video_frame_fetched_by_conn_id(conn_id: i32, frame_tm: Option) { + let vec_display_idx: Vec = { + let display_conn_ids = DISPLAY_CONN_IDS.lock().unwrap(); + display_conn_ids + .iter() + .filter_map(|(display_idx, conn_ids)| { + if conn_ids.contains(&conn_id) { + Some(*display_idx) + } else { + None + } + }) + .collect() + }; + let notifiers = FRAME_FETCHED_NOTIFIERS.lock().unwrap(); + for display_idx in vec_display_idx { + if let Some(notifier) = notifiers.get(&display_idx) { + notifier.0.send((conn_id, frame_tm)).ok(); + } + } +} + +struct VideoFrameController { + display_idx: usize, + cur: Instant, + send_conn_ids: HashSet, +} + +impl VideoFrameController { + fn new(display_idx: usize) -> Self { + Self { + display_idx, + cur: Instant::now(), + send_conn_ids: HashSet::new(), + } + } + + fn reset(&mut self) { + self.send_conn_ids.clear(); + } + + fn set_send(&mut self, tm: Instant, conn_ids: HashSet) { + if !conn_ids.is_empty() { + self.cur = tm; + self.send_conn_ids = conn_ids; + DISPLAY_CONN_IDS + .lock() + .unwrap() + .insert(self.display_idx, self.send_conn_ids.clone()); + } + } + + #[tokio::main(flavor = "current_thread")] + async fn try_wait_next(&mut self, fetched_conn_ids: &mut HashSet, timeout_millis: u64) { + if self.send_conn_ids.is_empty() { + return; + } + + let timeout_dur = Duration::from_millis(timeout_millis as u64); + let receiver = { + match FRAME_FETCHED_NOTIFIERS + .lock() + .unwrap() + .get(&self.display_idx) + { + Some(notifier) => notifier.1.clone(), + None => { + return; + } + } + }; + let mut receiver_guard = receiver.lock().await; + match tokio::time::timeout(timeout_dur, receiver_guard.recv()).await { + Err(_) => { + // break if timeout + // log::error!("blocking wait frame receiving timeout {}", timeout_millis); + } + Ok(Some((id, instant))) => { + if let Some(tm) = instant { + log::trace!("Channel recv latency: {}", tm.elapsed().as_secs_f32()); + } + fetched_conn_ids.insert(id); + } + Ok(None) => { + // this branch would never be reached + } + } + while !receiver_guard.is_empty() { + if let Some((id, instant)) = receiver_guard.recv().await { + if let Some(tm) = instant { + log::trace!("Channel recv latency: {}", tm.elapsed().as_secs_f32()); + } + fetched_conn_ids.insert(id); + } + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum VideoSource { + Monitor, + Camera, +} + +impl VideoSource { + pub fn service_name_prefix(&self) -> &'static str { + match self { + VideoSource::Monitor => "monitor", + VideoSource::Camera => "camera", + } + } + + pub fn is_monitor(&self) -> bool { + matches!(self, VideoSource::Monitor) + } + + pub fn is_camera(&self) -> bool { + matches!(self, VideoSource::Camera) + } +} + +#[derive(Clone)] +pub struct VideoService { + sp: GenericService, + idx: usize, + source: VideoSource, +} + +impl Deref for VideoService { + type Target = ServiceTmpl; + + fn deref(&self) -> &Self::Target { + &self.sp + } +} + +impl DerefMut for VideoService { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sp + } +} + +pub fn get_service_name(source: VideoSource, idx: usize) -> String { + format!("{}{}", source.service_name_prefix(), idx) +} + +pub fn new(source: VideoSource, idx: usize) -> GenericService { + let _ = FRAME_FETCHED_NOTIFIERS + .lock() + .unwrap() + .entry(idx) + .or_insert_with(|| { + let (tx, rx) = unbounded_channel(); + (tx, Arc::new(TokioMutex::new(rx))) + }); + let vs = VideoService { + sp: GenericService::new(get_service_name(source, idx), true), + idx, + source, + }; + GenericService::run(&vs, run); + vs.sp +} + +// Capturer object is expensive, avoiding to create it frequently. +fn create_capturer( + privacy_mode_id: i32, + display: Display, + _current: usize, + _portable_service_running: bool, +) -> ResultType> { + #[cfg(not(windows))] + let c: Option> = None; + #[cfg(windows)] + let mut c: Option> = None; + if privacy_mode_id > 0 { + #[cfg(windows)] + { + if let Some(c1) = crate::privacy_mode::win_mag::create_capturer( + privacy_mode_id, + display.origin(), + display.width(), + display.height(), + )? { + c = Some(Box::new(c1)); + } + } + } + + match c { + Some(c1) => return Ok(c1), + None => { + #[cfg(windows)] + { + log::debug!("Create capturer dxgi|gdi"); + return crate::portable_service::client::create_capturer( + _current, + display, + _portable_service_running, + ); + } + #[cfg(not(windows))] + { + log::debug!("Create capturer from scrap"); + return Ok(Box::new( + Capturer::new(display).with_context(|| "Failed to create capturer")?, + )); + } + } + }; +} + +// This function works on privacy mode. Windows only for now. +pub fn test_create_capturer( + privacy_mode_id: i32, + display_idx: usize, + timeout_millis: u64, +) -> String { + let test_begin = Instant::now(); + loop { + let err = match Display::all() { + Ok(mut displays) => { + if displays.len() <= display_idx { + anyhow!( + "Failed to get display {}, the displays' count is {}", + display_idx, + displays.len() + ) + } else { + let display = displays.remove(display_idx); + match create_capturer(privacy_mode_id, display, display_idx, false) { + Ok(_) => return "".to_owned(), + Err(e) => e, + } + } + } + Err(e) => e.into(), + }; + if test_begin.elapsed().as_millis() >= timeout_millis as _ { + return err.to_string(); + } + std::thread::sleep(Duration::from_millis(300)); + } +} + +// Note: This function is extremely expensive, do not call it frequently. +#[cfg(windows)] +fn check_uac_switch(privacy_mode_id: i32, capturer_privacy_mode_id: i32) -> ResultType<()> { + if capturer_privacy_mode_id != INVALID_PRIVACY_MODE_CONN_ID + && is_current_privacy_mode_impl(PRIVACY_MODE_IMPL_WIN_MAG) + { + if !is_installed() { + if privacy_mode_id != capturer_privacy_mode_id { + if !is_process_consent_running()? { + bail!("consent.exe is not running"); + } + } + if is_process_consent_running()? { + bail!("consent.exe is running"); + } + } + } + Ok(()) +} + +pub(super) struct CapturerInfo { + pub origin: (i32, i32), + pub width: usize, + pub height: usize, + pub ndisplay: usize, + pub current: usize, + pub privacy_mode_id: i32, + pub _capturer_privacy_mode_id: i32, + pub capturer: Box, +} + +impl Deref for CapturerInfo { + type Target = Box; + + fn deref(&self) -> &Self::Target { + &self.capturer + } +} + +impl DerefMut for CapturerInfo { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.capturer + } +} + +fn get_capturer_monitor( + current: usize, + portable_service_running: bool, +) -> ResultType { + #[cfg(target_os = "linux")] + { + if !is_x11() { + return super::wayland::get_capturer_for_display(current); + } + } + + let mut displays = Display::all()?; + let ndisplay = displays.len(); + if ndisplay <= current { + bail!( + "Failed to get display {}, displays len: {}", + current, + ndisplay + ); + } + + let display = displays.remove(current); + + #[cfg(target_os = "linux")] + if let Display::X11(inner) = &display { + if let Err(err) = inner.get_shm_status() { + log::warn!( + "MIT-SHM extension not working properly on select X11 server: {:?}", + err + ); + } + } + + let (origin, width, height) = (display.origin(), display.width(), display.height()); + let name = display.name(); + log::debug!( + "#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}, name:{}", + ndisplay, + current, + &origin, + width, + height, + num_cpus::get_physical(), + num_cpus::get(), + &name, + ); + + let privacy_mode_id = get_privacy_mode_conn_id().unwrap_or(INVALID_PRIVACY_MODE_CONN_ID); + #[cfg(not(windows))] + let capturer_privacy_mode_id = privacy_mode_id; + #[cfg(windows)] + let mut capturer_privacy_mode_id = privacy_mode_id; + #[cfg(windows)] + { + if capturer_privacy_mode_id != INVALID_PRIVACY_MODE_CONN_ID + && is_current_privacy_mode_impl(PRIVACY_MODE_IMPL_WIN_MAG) + { + if !is_installed() { + if is_process_consent_running()? { + capturer_privacy_mode_id = INVALID_PRIVACY_MODE_CONN_ID; + } + } + } + } + log::debug!( + "Try create capturer with capturer privacy mode id {}", + capturer_privacy_mode_id, + ); + + if privacy_mode_id != INVALID_PRIVACY_MODE_CONN_ID { + if privacy_mode_id != capturer_privacy_mode_id { + log::info!("In privacy mode, but show UAC prompt window for now"); + } else { + log::info!("In privacy mode, the peer side cannot watch the screen"); + } + } + let capturer = create_capturer( + capturer_privacy_mode_id, + display, + current, + portable_service_running, + )?; + Ok(CapturerInfo { + origin, + width, + height, + ndisplay, + current, + privacy_mode_id, + _capturer_privacy_mode_id: capturer_privacy_mode_id, + capturer, + }) +} + +fn get_capturer_camera(current: usize) -> ResultType { + let cameras = camera::Cameras::get_sync_cameras(); + let ncamera = cameras.len(); + if ncamera <= current { + bail!("Failed to get camera {}, cameras len: {}", current, ncamera,); + } + let Some(camera) = cameras.get(current) else { + bail!( + "Camera of index {} doesn't exist or platform not supported", + current + ); + }; + let capturer = camera::Cameras::get_capturer(current)?; + let (width, height) = (camera.width as usize, camera.height as usize); + let origin = (camera.x as i32, camera.y as i32); + let name = &camera.name; + let privacy_mode_id = get_privacy_mode_conn_id().unwrap_or(INVALID_PRIVACY_MODE_CONN_ID); + let _capturer_privacy_mode_id = privacy_mode_id; + log::debug!( + "#cameras={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}, name:{}", + ncamera, + current, + &origin, + width, + height, + num_cpus::get_physical(), + num_cpus::get(), + name, + ); + return Ok(CapturerInfo { + origin, + width, + height, + ndisplay: ncamera, + current, + privacy_mode_id, + _capturer_privacy_mode_id: privacy_mode_id, + capturer, + }); +} +fn get_capturer( + source: VideoSource, + current: usize, + portable_service_running: bool, +) -> ResultType { + match source { + VideoSource::Monitor => get_capturer_monitor(current, portable_service_running), + VideoSource::Camera => get_capturer_camera(current), + } +} + +fn run(vs: VideoService) -> ResultType<()> { + let mut _raii = Raii::new(vs.idx, vs.sp.name()); + // Wayland only support one video capturer for now. It is ok to call ensure_inited() here. + // + // ensure_inited() is needed because clear() may be called. + // to-do: wayland ensure_inited should pass current display index. + // But for now, we do not support multi-screen capture on wayland. + #[cfg(target_os = "linux")] + super::wayland::ensure_inited()?; + #[cfg(target_os = "linux")] + let _wayland_call_on_ret = { + // Increment active display count when starting + let _display_count = super::wayland::increment_active_display_count(); + + SimpleCallOnReturn { + b: true, + f: Box::new(|| { + // Decrement active display count and only clear if this was the last display + let remaining_count = super::wayland::decrement_active_display_count(); + if remaining_count == 0 { + super::wayland::clear(); + } + }), + } + }; + + #[cfg(windows)] + let last_portable_service_running = crate::portable_service::client::running(); + #[cfg(not(windows))] + let last_portable_service_running = false; + + let display_idx = vs.idx; + let sp = vs.sp; + let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?; + #[cfg(windows)] + if !scrap::codec::enable_directx_capture() && !c.is_gdi() { + log::info!("disable dxgi with option, fall back to gdi"); + c.set_gdi(); + } + let mut video_qos = VIDEO_QOS.lock().unwrap(); + let mut spf = video_qos.spf(); + let mut quality = video_qos.ratio(); + let record_incoming = config::option2bool( + "allow-auto-record-incoming", + &Config::get_option("allow-auto-record-incoming"), + ); + let client_record = video_qos.record(); + drop(video_qos); + let (mut encoder, encoder_cfg, codec_format, use_i444, recorder) = match setup_encoder( + &c, + sp.name(), + quality, + client_record, + record_incoming, + last_portable_service_running, + vs.source, + display_idx, + ) { + Ok(result) => result, + Err(err) => { + log::error!("Failed to create encoder: {err:?}, fallback to VP9"); + Encoder::set_fallback(&EncoderCfg::VPX(VpxEncoderConfig { + width: c.width as _, + height: c.height as _, + quality, + codec: VpxVideoCodecId::VP9, + keyframe_interval: None, + })); + setup_encoder( + &c, + sp.name(), + quality, + client_record, + record_incoming, + last_portable_service_running, + vs.source, + display_idx, + )? + } + }; + #[cfg(feature = "vram")] + c.set_output_texture(encoder.input_texture()); + #[cfg(target_os = "android")] + if vs.source.is_monitor() { + if let Err(e) = check_change_scale(encoder.is_hardware()) { + try_broadcast_display_changed(&sp, display_idx, &c, true).ok(); + bail!(e); + } + } + VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate()); + VIDEO_QOS + .lock() + .unwrap() + .set_support_changing_quality(&sp.name(), encoder.support_changing_quality()); + log::info!("initial quality: {quality:?}"); + + if sp.is_option_true(OPTION_REFRESH) { + sp.set_option_bool(OPTION_REFRESH, false); + } + + let mut frame_controller = VideoFrameController::new(display_idx); + + let start = time::Instant::now(); + let mut last_check_displays = time::Instant::now(); + #[cfg(windows)] + let mut try_gdi = 1; + #[cfg(windows)] + log::info!("gdi: {}", c.is_gdi()); + #[cfg(windows)] + start_uac_elevation_check(); + + #[cfg(target_os = "linux")] + let mut would_block_count = 0u32; + let mut yuv = Vec::new(); + let mut mid_data = Vec::new(); + let mut repeat_encode_counter = 0; + let repeat_encode_max = 10; + let mut encode_fail_counter = 0; + let mut first_frame = true; + let capture_width = c.width; + let capture_height = c.height; + let (mut second_instant, mut send_counter) = (Instant::now(), 0); + + while sp.ok() { + #[cfg(windows)] + check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?; + check_qos( + &mut encoder, + &mut quality, + &mut spf, + client_record, + &mut send_counter, + &mut second_instant, + &sp.name(), + )?; + if sp.is_option_true(OPTION_REFRESH) { + if vs.source.is_monitor() { + let _ = try_broadcast_display_changed(&sp, display_idx, &c, true); + } + log::info!("switch to refresh"); + bail!("SWITCH"); + } + if codec_format != Encoder::negotiated_codec() { + log::info!( + "switch due to codec changed, {:?} -> {:?}", + codec_format, + Encoder::negotiated_codec() + ); + bail!("SWITCH"); + } + #[cfg(windows)] + if last_portable_service_running != crate::portable_service::client::running() { + log::info!("switch due to portable service running changed"); + bail!("SWITCH"); + } + if Encoder::use_i444(&encoder_cfg) != use_i444 { + log::info!("switch due to i444 changed"); + bail!("SWITCH"); + } + #[cfg(all(windows, feature = "vram"))] + if c.is_gdi() && encoder.input_texture() { + log::info!("changed to gdi when using vram"); + VRamEncoder::set_fallback_gdi(sp.name(), true); + bail!("SWITCH"); + } + if vs.source.is_monitor() { + check_privacy_mode_changed(&sp, display_idx, &c)?; + } + #[cfg(windows)] + { + if crate::platform::windows::desktop_changed() + && !crate::portable_service::client::running() + { + bail!("Desktop changed"); + } + } + let now = time::Instant::now(); + if vs.source.is_monitor() && last_check_displays.elapsed().as_millis() > 1000 { + last_check_displays = now; + // This check may be redundant, but it is better to be safe. + // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. + try_broadcast_display_changed(&sp, display_idx, &c, false)?; + } + + frame_controller.reset(); + + let time = now - start; + let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; + let res = match c.frame(spf) { + Ok(frame) => { + repeat_encode_counter = 0; + if frame.valid() { + let screenshot = SCREENSHOTS.lock().unwrap().remove(&display_idx); + if let Some(mut screenshot) = screenshot { + let restore_vram = screenshot.restore_vram; + let (msg, w, h, data) = match &frame { + scrap::Frame::PixelBuffer(f) => match get_rgba_from_pixelbuf(f) { + Ok(rgba) => ("".to_owned(), f.width(), f.height(), rgba), + Err(e) => { + let serr = e.to_string(); + log::error!( + "Failed to convert the pix format into rgba, {}", + &serr + ); + (format!("Convert pixfmt: {}", serr), 0, 0, vec![]) + } + }, + scrap::Frame::Texture(_) => { + if restore_vram { + // Already set one time, just ignore to break infinite loop. + // Though it's unreachable, this branch is kept to avoid infinite loop. + ( + "Please change codec and try again.".to_owned(), + 0, + 0, + vec![], + ) + } else { + #[cfg(all(windows, feature = "vram"))] + VRamEncoder::set_not_use(sp.name(), true); + screenshot.restore_vram = true; + SCREENSHOTS.lock().unwrap().insert(display_idx, screenshot); + _raii.try_vram = false; + bail!("SWITCH"); + } + } + }; + std::thread::spawn(move || { + handle_screenshot(screenshot, msg, w, h, data); + }); + if restore_vram { + bail!("SWITCH"); + } + } + + let frame = frame.to(encoder.yuvfmt(), &mut yuv, &mut mid_data)?; + let send_conn_ids = handle_one_frame( + display_idx, + &sp, + frame, + ms, + &mut encoder, + recorder.clone(), + &mut encode_fail_counter, + &mut first_frame, + capture_width, + capture_height, + )?; + frame_controller.set_send(now, send_conn_ids); + send_counter += 1; + } + #[cfg(windows)] + { + #[cfg(feature = "vram")] + if try_gdi == 1 && !c.is_gdi() { + VRamEncoder::set_fallback_gdi(sp.name(), false); + } + try_gdi = 0; + } + Ok(()) + } + Err(err) => Err(err), + }; + + match res { + Err(ref e) if e.kind() == WouldBlock => { + #[cfg(windows)] + if try_gdi > 0 && !c.is_gdi() { + if try_gdi > 3 { + c.set_gdi(); + try_gdi = 0; + log::info!("No image, fall back to gdi"); + } + try_gdi += 1; + } + #[cfg(target_os = "linux")] + { + would_block_count += 1; + if !is_x11() { + if would_block_count >= 100 { + // to-do: Unknown reason for WouldBlock 100 times (seconds = 100 * 1 / fps) + // https://github.com/rustdesk/rustdesk/blob/63e6b2f8ab51743e77a151e2b7ff18816f5fa2fb/libs/scrap/src/common/wayland.rs#L81 + // + // Do not reset the capturer for now, as it will cause the prompt to show every few minutes. + // https://github.com/rustdesk/rustdesk/issues/4276 + // + // super::wayland::clear(); + // bail!("Wayland capturer none 100 times, try restart capture"); + } + } + } + if !encoder.latency_free() && yuv.len() > 0 { + // yun.len() > 0 means the frame is not texture. + if repeat_encode_counter < repeat_encode_max { + repeat_encode_counter += 1; + let send_conn_ids = handle_one_frame( + display_idx, + &sp, + EncodeInput::YUV(&yuv), + ms, + &mut encoder, + recorder.clone(), + &mut encode_fail_counter, + &mut first_frame, + capture_width, + capture_height, + )?; + frame_controller.set_send(now, send_conn_ids); + send_counter += 1; + } + } + } + Err(err) => { + // This check may be redundant, but it is better to be safe. + // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. + if vs.source.is_monitor() { + try_broadcast_display_changed(&sp, display_idx, &c, true)?; + } + + #[cfg(windows)] + if !c.is_gdi() { + c.set_gdi(); + log::info!("dxgi error, fall back to gdi: {:?}", err); + continue; + } + return Err(err.into()); + } + _ => { + #[cfg(target_os = "linux")] + { + would_block_count = 0; + } + } + } + + let mut fetched_conn_ids = HashSet::new(); + let timeout_millis = 3_000u64; + let wait_begin = Instant::now(); + while wait_begin.elapsed().as_millis() < timeout_millis as _ { + if vs.source.is_monitor() { + check_privacy_mode_changed(&sp, display_idx, &c)?; + } + frame_controller.try_wait_next(&mut fetched_conn_ids, 300); + // break if all connections have received current frame + if fetched_conn_ids.len() >= frame_controller.send_conn_ids.len() { + break; + } + } + DISPLAY_CONN_IDS.lock().unwrap().remove(&display_idx); + + let elapsed = now.elapsed(); + // may need to enable frame(timeout) + log::trace!("{:?} {:?}", time::Instant::now(), elapsed); + if elapsed < spf { + std::thread::sleep(spf - elapsed); + } + } + + Ok(()) +} + +struct Raii { + display_idx: usize, + name: String, + try_vram: bool, +} + +impl Raii { + fn new(display_idx: usize, name: String) -> Self { + log::info!("new video service: {}", name); + VIDEO_QOS.lock().unwrap().new_display(name.clone()); + Raii { + display_idx, + name, + try_vram: true, + } + } +} + +impl Drop for Raii { + fn drop(&mut self) { + log::info!("stop video service: {}", self.name); + #[cfg(feature = "vram")] + if self.try_vram { + VRamEncoder::set_not_use(self.name.clone(), false); + } + #[cfg(feature = "vram")] + Encoder::update(scrap::codec::EncodingUpdate::Check); + VIDEO_QOS.lock().unwrap().remove_display(&self.name); + DISPLAY_CONN_IDS.lock().unwrap().remove(&self.display_idx); + } +} + +fn setup_encoder( + c: &CapturerInfo, + name: String, + quality: f32, + client_record: bool, + record_incoming: bool, + last_portable_service_running: bool, + source: VideoSource, + display_idx: usize, +) -> ResultType<( + Encoder, + EncoderCfg, + CodecFormat, + bool, + Arc>>, +)> { + let encoder_cfg = get_encoder_config( + &c, + name.to_string(), + quality, + client_record || record_incoming, + last_portable_service_running, + source, + ); + Encoder::set_fallback(&encoder_cfg); + let codec_format = Encoder::negotiated_codec(); + let recorder = get_recorder(record_incoming, display_idx, source == VideoSource::Camera); + let use_i444 = Encoder::use_i444(&encoder_cfg); + let encoder = Encoder::new(encoder_cfg.clone(), use_i444)?; + Ok((encoder, encoder_cfg, codec_format, use_i444, recorder)) +} + +fn get_encoder_config( + c: &CapturerInfo, + _name: String, + quality: f32, + record: bool, + _portable_service: bool, + _source: VideoSource, +) -> EncoderCfg { + #[cfg(all(windows, feature = "vram"))] + if _portable_service || c.is_gdi() || _source == VideoSource::Camera { + log::info!("gdi:{}, portable:{}", c.is_gdi(), _portable_service); + VRamEncoder::set_not_use(_name, true); + } + #[cfg(feature = "vram")] + Encoder::update(scrap::codec::EncodingUpdate::Check); + // https://www.wowza.com/community/t/the-correct-keyframe-interval-in-obs-studio/95162 + let keyframe_interval = if record { Some(240) } else { None }; + let negotiated_codec = Encoder::negotiated_codec(); + match negotiated_codec { + CodecFormat::H264 | CodecFormat::H265 => { + #[cfg(feature = "vram")] + if let Some(feature) = VRamEncoder::try_get(&c.device(), negotiated_codec) { + return EncoderCfg::VRAM(VRamEncoderConfig { + device: c.device(), + width: c.width, + height: c.height, + quality, + feature, + keyframe_interval, + }); + } + #[cfg(feature = "hwcodec")] + if let Some(hw) = HwRamEncoder::try_get(negotiated_codec) { + return EncoderCfg::HWRAM(HwRamEncoderConfig { + name: hw.name, + mc_name: hw.mc_name, + width: c.width, + height: c.height, + quality, + keyframe_interval, + }); + } + EncoderCfg::VPX(VpxEncoderConfig { + width: c.width as _, + height: c.height as _, + quality, + codec: VpxVideoCodecId::VP9, + keyframe_interval, + }) + } + format @ (CodecFormat::VP8 | CodecFormat::VP9) => EncoderCfg::VPX(VpxEncoderConfig { + width: c.width as _, + height: c.height as _, + quality, + codec: if format == CodecFormat::VP8 { + VpxVideoCodecId::VP8 + } else { + VpxVideoCodecId::VP9 + }, + keyframe_interval, + }), + CodecFormat::AV1 => EncoderCfg::AOM(AomEncoderConfig { + width: c.width as _, + height: c.height as _, + quality, + keyframe_interval, + }), + _ => EncoderCfg::VPX(VpxEncoderConfig { + width: c.width as _, + height: c.height as _, + quality, + codec: VpxVideoCodecId::VP9, + keyframe_interval, + }), + } +} + +fn get_recorder( + record_incoming: bool, + display_idx: usize, + camera: bool, +) -> Arc>> { + #[cfg(windows)] + let root = crate::platform::is_root(); + #[cfg(not(windows))] + let root = false; + let recorder = if record_incoming { + use crate::hbbs_http::record_upload; + + let tx = if record_upload::is_enable() { + let (tx, rx) = std::sync::mpsc::channel(); + record_upload::run(rx); + Some(tx) + } else { + None + }; + Recorder::new(RecorderContext { + server: true, + id: Config::get_id(), + dir: crate::ui_interface::video_save_directory(root), + display_idx, + camera, + tx, + }) + .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) + } else { + Default::default() + }; + + 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; + + // isStart flag is set at the end of startCapture() in Android, wait it to be set. + let n = 60; // 3s + for i in 0..n { + if scrap::is_start() == Some(true) { + log::info!("start flag is set"); + break; + } + log::info!("wait for start, {i}"); + std::thread::sleep(Duration::from_millis(50)); + if i == n - 1 { + log::error!("wait for start timeout"); + } + } + let screen_size = scrap::screen_size(); + let scale_soft = hbb_common::config::option2bool(SCALE_SOFT, &Config::get_option(SCALE_SOFT)); + let half_scale = !hardware && scale_soft; + log::info!("hardware: {hardware}, scale_soft: {scale_soft}, screen_size: {screen_size:?}",); + scrap::android::call_main_service_set_by_name( + "half_scale", + Some(half_scale.to_string().as_str()), + None, + ) + .ok(); + let old_scale = screen_size.2; + let new_scale = scrap::screen_size().2; + log::info!("old_scale: {old_scale}, new_scale: {new_scale}"); + if old_scale != new_scale { + log::info!("switch due to scale changed, {old_scale} -> {new_scale}"); + // switch is not a must, but it is better to do so. + bail!("SWITCH"); + } + Ok(()) +} + +fn check_privacy_mode_changed( + sp: &GenericService, + display_idx: usize, + ci: &CapturerInfo, +) -> ResultType<()> { + let privacy_mode_id_2 = get_privacy_mode_conn_id().unwrap_or(INVALID_PRIVACY_MODE_CONN_ID); + if ci.privacy_mode_id != privacy_mode_id_2 { + if privacy_mode_id_2 != INVALID_PRIVACY_MODE_CONN_ID { + let msg_out = crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOnByOther, + "".to_owned(), + ); + sp.send_to_others(msg_out, privacy_mode_id_2); + } + log::info!("switch due to privacy mode changed"); + try_broadcast_display_changed(&sp, display_idx, ci, true).ok(); + bail!("SWITCH"); + } + Ok(()) +} + +#[inline] +fn handle_one_frame( + display: usize, + sp: &GenericService, + frame: EncodeInput, + ms: i64, + encoder: &mut Encoder, + recorder: Arc>>, + encode_fail_counter: &mut usize, + first_frame: &mut bool, + width: usize, + height: usize, +) -> ResultType> { + sp.snapshot(|sps| { + // so that new sub and old sub share the same encoder after switch + if sps.has_subscribes() { + log::info!("switch due to new subscriber"); + bail!("SWITCH"); + } + Ok(()) + })?; + + let mut send_conn_ids: HashSet = Default::default(); + let first = *first_frame; + *first_frame = false; + match encoder.encode_to_message(frame, ms) { + Ok(mut vf) => { + *encode_fail_counter = 0; + vf.display = display as _; + let mut msg = Message::new(); + msg.set_video_frame(vf); + recorder + .lock() + .unwrap() + .as_mut() + .map(|r| r.write_message(&msg, width, height)); + send_conn_ids = sp.send_video_frame(msg); + } + Err(e) => { + *encode_fail_counter += 1; + // Encoding errors are not frequent except on Android + if !cfg!(target_os = "android") { + log::error!("encode fail: {e:?}, times: {}", *encode_fail_counter,); + } + let max_fail_times = if cfg!(target_os = "android") && encoder.is_hardware() { + 9 + } else { + 3 + }; + let repeat = !encoder.latency_free(); + // repeat encoders can reach max_fail_times on the first frame + if (first && !repeat) || *encode_fail_counter >= max_fail_times { + *encode_fail_counter = 0; + if encoder.is_hardware() { + encoder.disable(); + log::error!("switch due to encoding fails, first frame: {first}, error: {e:?}"); + bail!("SWITCH"); + } + } + match e.to_string().as_str() { + scrap::codec::ENCODE_NEED_SWITCH => { + encoder.disable(); + log::error!("switch due to encoder need switch"); + bail!("SWITCH"); + } + _ => {} + } + } + } + Ok(send_conn_ids) +} + +#[inline] +pub fn refresh() { + #[cfg(target_os = "android")] + Display::refresh_size(); +} + +#[cfg(windows)] +fn start_uac_elevation_check() { + static START: Once = Once::new(); + START.call_once(|| { + if !crate::platform::is_installed() && !crate::platform::is_root() { + std::thread::spawn(|| loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + if let Ok(uac) = is_process_consent_running() { + *IS_UAC_RUNNING.lock().unwrap() = uac; + } + if !crate::platform::is_elevated(None).unwrap_or(false) { + if let Ok(elevated) = crate::platform::is_foreground_window_elevated() { + *IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap() = elevated; + } + } + }); + } + }); +} + +#[inline] +fn try_broadcast_display_changed( + sp: &GenericService, + display_idx: usize, + cap: &CapturerInfo, + refresh: bool, +) -> ResultType<()> { + if refresh { + // Get display information immediately. + crate::display_service::check_displays_changed().ok(); + } + if let Some(display) = check_display_changed( + cap.ndisplay, + cap.current, + (cap.origin.0, cap.origin.1, cap.width, cap.height), + ) { + log::info!("Display {} changed", display); + if let Some(msg_out) = + make_display_changed_msg(display_idx, Some(display), VideoSource::Monitor) + { + let msg_out = Arc::new(msg_out); + sp.send_shared(msg_out.clone()); + // switch display may occur before the first video frame, add snapshot to send to new subscribers + sp.snapshot(move |sps| { + sps.send_shared(msg_out.clone()); + Ok(()) + })?; + bail!("SWITCH"); + } + } + Ok(()) +} + +pub fn make_display_changed_msg( + display_idx: usize, + opt_display: Option, + source: VideoSource, +) -> Option { + let display = match opt_display { + Some(d) => d, + None => match source { + VideoSource::Monitor => display_service::get_display_info(display_idx)?, + VideoSource::Camera => camera::Cameras::get_sync_cameras() + .get(display_idx)? + .clone(), + }, + }; + let mut misc = Misc::new(); + misc.set_switch_display(SwitchDisplay { + display: display_idx as _, + x: display.x, + y: display.y, + width: display.width, + height: display.height, + cursor_embedded: match source { + VideoSource::Monitor => display_service::capture_cursor_embedded(), + VideoSource::Camera => false, + }, + #[cfg(not(target_os = "android"))] + resolutions: Some(SupportedResolutions { + resolutions: match source { + VideoSource::Monitor => { + if display.name.is_empty() { + vec![] + } else { + crate::platform::resolutions(&display.name) + } + } + VideoSource::Camera => camera::Cameras::get_camera_resolution(display_idx) + .ok() + .into_iter() + .collect(), + }, + ..SupportedResolutions::default() + }) + .into(), + original_resolution: display.original_resolution, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + Some(msg_out) +} + +fn check_qos( + encoder: &mut Encoder, + ratio: &mut f32, + spf: &mut Duration, + client_record: bool, + send_counter: &mut usize, + second_instant: &mut Instant, + name: &str, +) -> ResultType<()> { + let mut video_qos = VIDEO_QOS.lock().unwrap(); + *spf = video_qos.spf(); + if *ratio != video_qos.ratio() { + *ratio = video_qos.ratio(); + if encoder.support_changing_quality() { + allow_err!(encoder.set_quality(*ratio)); + video_qos.store_bitrate(encoder.bitrate()); + } else { + // Now only vaapi doesn't support changing quality + if !video_qos.in_vbr_state() && !video_qos.latest_quality().is_custom() { + log::info!("switch to change quality"); + bail!("SWITCH"); + } + } + } + if client_record != video_qos.record() { + log::info!("switch due to record changed"); + bail!("SWITCH"); + } + if second_instant.elapsed() > Duration::from_secs(1) { + *second_instant = Instant::now(); + video_qos.update_display_data(&name, *send_counter); + *send_counter = 0; + } + drop(video_qos); + Ok(()) +} + +pub fn set_take_screenshot(display_idx: usize, sid: String, tx: Sender) { + SCREENSHOTS.lock().unwrap().insert( + display_idx, + Screenshot { + sid, + tx, + restore_vram: false, + }, + ); +} + +// We need to this function, because the `stride` may be larger than `width * 4`. +fn get_rgba_from_pixelbuf<'a>(pixbuf: &scrap::PixelBuffer<'a>) -> ResultType> { + let w = pixbuf.width(); + let h = pixbuf.height(); + let stride = pixbuf.stride(); + let Some(s) = stride.get(0) else { + bail!("Invalid pixel buf stride.") + }; + + if *s == w * 4 { + let mut rgba = vec![]; + scrap::convert(pixbuf, scrap::Pixfmt::RGBA, &mut rgba)?; + Ok(rgba) + } else { + let bgra = pixbuf.data(); + let mut bit_flipped = Vec::with_capacity(w * h * 4); + for y in 0..h { + for x in 0..w { + let i = s * y + 4 * x; + bit_flipped.extend_from_slice(&[bgra[i + 2], bgra[i + 1], bgra[i], bgra[i + 3]]); + } + } + Ok(bit_flipped) + } +} + +fn handle_screenshot(screenshot: Screenshot, msg: String, w: usize, h: usize, data: Vec) { + let mut response = ScreenshotResponse::new(); + response.sid = screenshot.sid; + if msg.is_empty() { + if data.is_empty() { + response.msg = "Failed to take screenshot, please try again later.".to_owned(); + } else { + fn encode_png(width: usize, height: usize, rgba: Vec) -> ResultType> { + let mut png = Vec::new(); + let mut encoder = + repng::Options::smallest(width as _, height as _).build(&mut png)?; + encoder.write(&rgba)?; + encoder.finish()?; + Ok(png) + } + match encode_png(w as _, h as _, data) { + Ok(png) => { + response.data = png.into(); + } + Err(e) => { + response.msg = format!("Error encoding png: {}", e); + } + } + } + } else { + response.msg = msg; + } + let mut msg_out = Message::new(); + msg_out.set_screenshot_response(response); + if let Err(e) = screenshot + .tx + .send((hbb_common::tokio::time::Instant::now(), Arc::new(msg_out))) + { + log::error!("Failed to send screenshot, {}", e); + } +} diff --git a/vendor/rustdesk/src/server/wayland.rs b/vendor/rustdesk/src/server/wayland.rs new file mode 100644 index 0000000..1e0efc0 --- /dev/null +++ b/vendor/rustdesk/src/server/wayland.rs @@ -0,0 +1,308 @@ +use super::*; +use hbb_common::{allow_err, anyhow, platform::linux::DISTRO}; +use scrap::{ + is_cursor_embedded, set_map_err, + wayland::pipewire::{fill_displays, try_fix_logical_size}, + Capturer, Display, Frame, TraitCapturer, +}; +use std::collections::HashMap; +use std::io; + +use crate::{ + client::{ + SCRAP_OTHER_VERSION_OR_X11_REQUIRED, SCRAP_UBUNTU_HIGHER_REQUIRED, + SCRAP_X11_REQUIRED, SCRAP_XDP_PORTAL_UNAVAILABLE, + }, + platform::linux::is_x11, +}; + +lazy_static::lazy_static! { + static ref CAP_DISPLAY_INFO: RwLock> = RwLock::new(HashMap::new()); + static ref PIPEWIRE_INITIALIZED: RwLock = RwLock::new(false); + static ref LOG_SCRAP_COUNT: Mutex = Mutex::new(0); + static ref ACTIVE_DISPLAY_COUNT: RwLock = RwLock::new(0); +} + +pub fn init() { + set_map_err(map_err_scrap); +} + +pub(super) fn increment_active_display_count() -> usize { + let mut count = ACTIVE_DISPLAY_COUNT.write().unwrap(); + *count += 1; + *count +} + +pub(super) fn decrement_active_display_count() -> usize { + let mut count = ACTIVE_DISPLAY_COUNT.write().unwrap(); + if *count > 0 { + *count -= 1; + } + *count +} + +fn map_err_scrap(err: String) -> io::Error { + // to-do: Handle error better, do not restart server + if err.starts_with("Did not receive a reply") { + log::error!("Fatal pipewire error, {}", &err); + std::process::exit(-1); + } + + if DISTRO.name.to_uppercase() == "Ubuntu".to_uppercase() { + if DISTRO.version_id < "21".to_owned() { + io::Error::new(io::ErrorKind::Other, SCRAP_UBUNTU_HIGHER_REQUIRED) + } else { + try_log(&err); + io::Error::new(io::ErrorKind::Other, err) + } + } else { + try_log(&err); + let err_lower = err.to_ascii_lowercase(); + if err_lower.contains("org.freedesktop.portal") + || err_lower.contains("dbus") + || err_lower.contains("d-bus") + { + // The portal D-Bus interface is unreachable. This typically means + // xdg-desktop-portal has crashed... for more info, see: Issue #12897 + io::Error::new(io::ErrorKind::Other, SCRAP_XDP_PORTAL_UNAVAILABLE) + } else if err_lower.contains("pipewire") { + io::Error::new(io::ErrorKind::Other, SCRAP_OTHER_VERSION_OR_X11_REQUIRED) + } else { + io::Error::new(io::ErrorKind::Other, SCRAP_X11_REQUIRED) + } + } +} + +fn try_log(err: &String) { + let mut lock_count = LOG_SCRAP_COUNT.lock().unwrap(); + if *lock_count >= 1000000 { + return; + } + if *lock_count % 10000 == 0 { + log::error!("Failed scrap {}", err); + } + *lock_count += 1; +} + +struct CapturerPtr(*mut Capturer); + +impl Clone for CapturerPtr { + fn clone(&self) -> Self { + Self(self.0) + } +} + +impl TraitCapturer for CapturerPtr { + fn frame<'a>(&'a mut self, timeout: std::time::Duration) -> std::io::Result> { + unsafe { (*self.0).frame(timeout) } + } +} + +struct CapDisplayInfo { + rects: Vec<((i32, i32), usize, usize)>, + displays: Vec, + num: usize, + primary: usize, + current: usize, + capturer: CapturerPtr, +} + +#[tokio::main(flavor = "current_thread")] +pub(super) async fn ensure_inited() -> ResultType<()> { + check_init().await +} + +pub(super) fn is_inited() -> Option { + if is_x11() { + None + } else { + if CAP_DISPLAY_INFO.read().unwrap().is_empty() { + let mut msg_out = Message::new(); + let res = MessageBox { + msgtype: "nook-nocancel-hasclose".to_owned(), + title: "Wayland".to_owned(), + text: "Please Select the screen to be shared(Operate on the peer side).".to_owned(), + link: "".to_owned(), + ..Default::default() + }; + msg_out.set_message_box(res); + Some(msg_out) + } else { + None + } + } +} + +pub(super) async fn check_init() -> ResultType<()> { + if !is_x11() { + if CAP_DISPLAY_INFO.read().unwrap().is_empty() { + if crate::input_service::wayland_use_uinput() { + if let Some((minx, maxx, miny, maxy)) = + scrap::wayland::display::get_desktop_rect_for_uinput() + { + log::info!( + "update mouse resolution: ({}, {}), ({}, {})", + minx, + maxx, + miny, + maxy + ); + allow_err!( + input_service::update_mouse_resolution(minx, maxx, miny, maxy).await + ); + } else { + log::warn!("Failed to get desktop rect for uinput"); + } + } + + let mut lock = CAP_DISPLAY_INFO.write().unwrap(); + if lock.is_empty() { + // Check if PipeWire is already initialized to prevent duplicate recorder creation + if *PIPEWIRE_INITIALIZED.read().unwrap() { + log::warn!("wayland_diag: Preventing duplicate PipeWire initialization"); + return Ok(()); + } + + let mut all = Display::all()?; + log::debug!("Initializing displays with fill_displays()"); + { + let temp_mouse_move_handle = input_service::TemporaryMouseMoveHandle::new(); + let move_mouse_to = |x, y| temp_mouse_move_handle.move_mouse_to(x, y); + fill_displays(move_mouse_to, crate::get_cursor_pos, &mut all)?; + } + log::debug!("Attempting to fix logical size with try_fix_logical_size()"); + try_fix_logical_size(&mut all); + *PIPEWIRE_INITIALIZED.write().unwrap() = true; + let num = all.len(); + let primary = super::display_service::get_primary_2(&all); + super::display_service::check_update_displays(&all); + let mut displays = super::display_service::get_sync_displays(); + for display in displays.iter_mut() { + display.cursor_embedded = is_cursor_embedded(); + } + + let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); + for d in &all { + rects.push((d.origin(), d.width(), d.height())); + } + + log::debug!( + "#displays={}, primary={}, rects: {:?}, cpus={}/{}", + num, + primary, + rects, + num_cpus::get_physical(), + num_cpus::get() + ); + + // Create individual CapDisplayInfo for each display with its own capturer + for (idx, display) in all.into_iter().enumerate() { + let capturer = + Box::into_raw(Box::new(Capturer::new(display).with_context(|| { + format!("Failed to create capturer for display {}", idx) + })?)); + let capturer = CapturerPtr(capturer); + + let cap_display_info = Box::into_raw(Box::new(CapDisplayInfo { + rects: rects.clone(), + displays: displays.clone(), + num, + primary, + current: idx, + capturer, + })); + + lock.insert(idx, cap_display_info as u64); + } + } + } + } + Ok(()) +} + +pub(super) async fn get_displays() -> ResultType> { + check_init().await?; + let cap_map = CAP_DISPLAY_INFO.read().unwrap(); + if let Some(addr) = cap_map.values().next() { + let cap_display_info: *const CapDisplayInfo = *addr as _; + unsafe { + let cap_display_info = &*cap_display_info; + Ok(cap_display_info.displays.clone()) + } + } else { + bail!("Failed to get capturer display info"); + } +} + +pub(super) fn get_primary() -> ResultType { + let cap_map = CAP_DISPLAY_INFO.read().unwrap(); + if let Some(addr) = cap_map.values().next() { + let cap_display_info: *const CapDisplayInfo = *addr as _; + unsafe { + let cap_display_info = &*cap_display_info; + Ok(cap_display_info.primary) + } + } else { + bail!("Failed to get capturer display info"); + } +} + +pub fn clear() { + if is_x11() { + return; + } + let mut write_lock = CAP_DISPLAY_INFO.write().unwrap(); + for (_, addr) in write_lock.iter() { + let cap_display_info: *mut CapDisplayInfo = *addr as _; + unsafe { + let _box_capturer = Box::from_raw((*cap_display_info).capturer.0); + let _box_cap_display_info = Box::from_raw(cap_display_info); + } + } + write_lock.clear(); + + // Reset PipeWire initialization flag to allow recreation on next init + *PIPEWIRE_INITIALIZED.write().unwrap() = false; +} + +pub(super) fn get_capturer_for_display( + display_idx: usize, +) -> ResultType { + if is_x11() { + bail!("Do not call this function if not wayland"); + } + let cap_map = CAP_DISPLAY_INFO.read().unwrap(); + if let Some(addr) = cap_map.get(&display_idx) { + let cap_display_info: *const CapDisplayInfo = *addr as _; + unsafe { + let cap_display_info = &*cap_display_info; + let rect = cap_display_info.rects[cap_display_info.current]; + Ok(super::video_service::CapturerInfo { + origin: rect.0, + width: rect.1, + height: rect.2, + ndisplay: cap_display_info.num, + current: cap_display_info.current, + privacy_mode_id: 0, + _capturer_privacy_mode_id: 0, + capturer: Box::new(cap_display_info.capturer.clone()), + }) + } + } else { + bail!( + "Failed to get capturer display info for display {}", + display_idx + ); + } +} + +pub fn common_get_error() -> String { + if DISTRO.name.to_uppercase() == "Ubuntu".to_uppercase() { + if DISTRO.version_id < "21".to_owned() { + return "".to_owned(); + } + } else { + // to-do: check other distros + } + return "".to_owned(); +} diff --git a/vendor/rustdesk/src/service.rs b/vendor/rustdesk/src/service.rs new file mode 100644 index 0000000..ce1855b --- /dev/null +++ b/vendor/rustdesk/src/service.rs @@ -0,0 +1,11 @@ +use librustdesk::*; + +#[cfg(not(target_os = "macos"))] +fn main() {} + +#[cfg(target_os = "macos")] +fn main() { + crate::common::load_custom_client(); + hbb_common::init_log(false, "service"); + crate::start_os_service(); +} diff --git a/vendor/rustdesk/src/tray.rs b/vendor/rustdesk/src/tray.rs new file mode 100644 index 0000000..e8db0ef --- /dev/null +++ b/vendor/rustdesk/src/tray.rs @@ -0,0 +1,281 @@ +use crate::client::translate; +#[cfg(windows)] +use crate::ipc::Data; +#[cfg(windows)] +use hbb_common::tokio; +use hbb_common::{allow_err, log}; +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" { + #[cfg(not(target_os = "macos"))] + { + return; + } + } + + #[cfg(target_os = "linux")] + crate::server::check_zombie(); + + allow_err!(make_tray()); +} + +fn make_tray() -> hbb_common::ResultType<()> { + // https://github.com/tauri-apps/tray-icon/blob/dev/examples/tao.rs + use hbb_common::anyhow::Context; + use tao::event_loop::{ControlFlow, EventLoopBuilder}; + use tray_icon::{ + menu::{Menu, MenuEvent, MenuItem}, + TrayIcon, TrayIconBuilder, TrayIconEvent as TrayEvent, + }; + let icon; + #[cfg(target_os = "macos")] + { + icon = include_bytes!("../res/mac-tray-dark-x2.png"); // use as template, so color is not important + } + #[cfg(not(target_os = "macos"))] + { + icon = include_bytes!("../res/tray-icon.ico"); + } + + let (icon_rgba, icon_width, icon_height) = { + let image = load_icon_from_asset() + .unwrap_or(image::load_from_memory(icon).context("Failed to open icon path")?) + .into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + let icon = tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height) + .context("Failed to open icon")?; + + let mut event_loop = EventLoopBuilder::new().build(); + + let tray_menu = Menu::new(); + let hide_stop_service = crate::ui_interface::get_builtin_option( + hbb_common::config::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. + let quit_i = if !hide_stop_service { + Some(MenuItem::new(translate("Stop service".to_owned()), true, None)) + } else { + None + }; + let open_i = MenuItem::new(translate("Open".to_owned()), true, None); + if let Some(quit_i) = &quit_i { + tray_menu.append_items(&[&open_i, quit_i]).ok(); + } else { + tray_menu.append_items(&[&open_i]).ok(); + } + let tooltip = |count: usize| { + if count == 0 { + format!( + "{} {}", + crate::get_app_name(), + translate("Service is running".to_owned()), + ) + } else { + format!( + "{} - {}\n{}", + crate::get_app_name(), + translate("Ready".to_owned()), + translate("{".to_string() + &format!("{count}") + "} sessions"), + ) + } + }; + let mut _tray_icon: Arc>> = Default::default(); + + let menu_channel = MenuEvent::receiver(); + let tray_channel = TrayEvent::receiver(); + #[cfg(windows)] + let (ipc_sender, ipc_receiver) = std::sync::mpsc::channel::(); + + let open_func = move || { + if cfg!(not(feature = "flutter")) { + crate::run_me::<&str>(vec![]).ok(); + return; + } + #[cfg(target_os = "macos")] + crate::platform::macos::handle_application_should_open_untitled_file(); + #[cfg(target_os = "windows")] + { + // Do not use "start uni link" way, it may not work on some Windows, and pop out error + // dialog, I found on one user's desktop, but no idea why, Windows is shit. + // Use `run_me` instead. + // `allow_multiple_instances` in `flutter/windows/runner/main.cpp` allows only one instance without args. + crate::run_me::<&str>(vec![]).ok(); + } + #[cfg(target_os = "linux")] + { + // Do not use "xdg-open", it won't read the config. + if crate::dbus::invoke_new_connection(crate::get_uri_prefix()).is_err() { + if let Ok(task) = crate::run_me::<&str>(vec![]) { + crate::server::CHILD_PROCESS.lock().unwrap().push(task); + } + } + } + }; + + #[cfg(windows)] + std::thread::spawn(move || { + start_query_session_count(ipc_sender.clone()); + }); + #[cfg(windows)] + let mut last_click = std::time::Instant::now(); + #[cfg(target_os = "macos")] + { + use tao::platform::macos::EventLoopExtMacOS; + event_loop.set_activation_policy(tao::platform::macos::ActivationPolicy::Accessory); + } + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::WaitUntil( + std::time::Instant::now() + std::time::Duration::from_millis(100), + ); + + 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" { + return; + } + // We create the icon once the event loop is actually running + // to prevent issues like https://github.com/tauri-apps/tray-icon/issues/90 + let tray = TrayIconBuilder::new() + .with_menu(Box::new(tray_menu.clone())) + .with_tooltip(tooltip(0)) + .with_icon(icon.clone()) + .with_icon_as_template(true) // mac only + .build(); + match tray { + Ok(tray) => _tray_icon = Arc::new(Mutex::new(Some(tray))), + Err(err) => { + log::error!("Failed to create tray icon: {}", err); + } + }; + + // We have to request a redraw here to have the icon actually show up. + // Tao only exposes a redraw method on the Window so we use core-foundation directly. + #[cfg(target_os = "macos")] + unsafe { + use core_foundation::runloop::{CFRunLoopGetMain, CFRunLoopWakeUp}; + + let rl = CFRunLoopGetMain(); + CFRunLoopWakeUp(rl); + } + } + + if let Ok(event) = menu_channel.try_recv() { + if let Some(quit_i) = &quit_i { + if event.id == quit_i.id() { + /* failed in windows, seems no permission to check system process + if !crate::check_process("--server", false) { + *control_flow = ControlFlow::Exit; + return; + } + */ + if !crate::platform::uninstall_service(false, false) { + *control_flow = ControlFlow::Exit; + } + } else if event.id == open_i.id() { + open_func(); + } + } else if event.id == open_i.id() { + open_func(); + } + } + + if let Ok(_event) = tray_channel.try_recv() { + #[cfg(target_os = "windows")] + match _event { + TrayEvent::Click { + button, + button_state, + .. + } => { + if button == tray_icon::MouseButton::Left + && button_state == tray_icon::MouseButtonState::Up + { + if last_click.elapsed() < std::time::Duration::from_secs(1) { + return; + } + open_func(); + last_click = std::time::Instant::now(); + } + } + _ => {} + } + } + + #[cfg(windows)] + if let Ok(data) = ipc_receiver.try_recv() { + match data { + Data::ControlledSessionCount(count) => { + _tray_icon + .lock() + .unwrap() + .as_mut() + .map(|t| t.set_tooltip(Some(tooltip(count)))); + } + _ => {} + } + } + }); +} + +#[cfg(windows)] +#[tokio::main(flavor = "current_thread")] +async fn start_query_session_count(sender: std::sync::mpsc::Sender) { + let mut last_count = 0; + loop { + if let Ok(mut c) = crate::ipc::connect(1000, "").await { + let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1))); + loop { + tokio::select! { + res = c.next() => { + match res { + Err(err) => { + log::error!("ipc connection closed: {}", err); + break; + } + + Ok(Some(Data::ControlledSessionCount(count))) => { + if count != last_count { + last_count = count; + sender.send(Data::ControlledSessionCount(count)).ok(); + } + } + _ => {} + } + } + + _ = timer.tick() => { + c.send(&Data::ControlledSessionCount(0)).await.ok(); + } + } + } + } + hbb_common::sleep(1.).await; + } +} + +fn load_icon_from_asset() -> Option { + let Some(path) = std::env::current_exe().map_or(None, |x| x.parent().map(|x| x.to_path_buf())) + else { + return None; + }; + #[cfg(target_os = "macos")] + let path = path.join("../Frameworks/App.framework/Resources/flutter_assets/assets/icon.png"); + #[cfg(windows)] + let path = path.join(r"data\flutter_assets\assets\icon.png"); + #[cfg(target_os = "linux")] + let path = path.join(r"data/flutter_assets/assets/icon.png"); + if path.exists() { + if let Ok(image) = image::open(path) { + return Some(image); + } + } + None +} diff --git a/vendor/rustdesk/src/ui.rs b/vendor/rustdesk/src/ui.rs new file mode 100644 index 0000000..6d0d092 --- /dev/null +++ b/vendor/rustdesk/src/ui.rs @@ -0,0 +1,878 @@ +use std::{ + collections::HashMap, + iter::FromIterator, + sync::{Arc, Mutex}, +}; + +use sciter::Value; + +use hbb_common::{ + allow_err, + config::{LocalConfig, PeerConfig}, + log, +}; + +#[cfg(not(any(feature = "flutter", feature = "cli")))] +use crate::ui_session_interface::Session; +use crate::{common::get_app_name, ipc, ui_interface::*}; + +mod cm; +#[cfg(feature = "inline")] +pub mod inline; +pub mod remote; + +#[allow(dead_code)] +type Status = (i32, bool, i64, String); + +lazy_static::lazy_static! { + // stupid workaround for https://sciter.com/forums/topic/crash-on-latest-tis-mac-sdk-sometimes/ + static ref STUPID_VALUES: Mutex>>> = Default::default(); +} + +#[cfg(not(any(feature = "flutter", feature = "cli")))] +lazy_static::lazy_static! { + pub static ref CUR_SESSION: Arc>>> = Default::default(); +} + +struct UIHostHandler; + +pub fn start(args: &mut [String]) { + #[cfg(target_os = "macos")] + crate::platform::delegate::show_dock(); + #[cfg(all(target_os = "linux", feature = "inline"))] + { + let app_dir = std::env::var("APPDIR").unwrap_or("".to_string()); + let mut so_path = "/usr/share/rustdesk/libsciter-gtk.so".to_owned(); + for (prefix, dir) in [ + ("", "/usr"), + ("", "/app"), + (&app_dir, "/usr"), + (&app_dir, "/app"), + ] + .iter() + { + let path = format!("{prefix}{dir}/share/rustdesk/libsciter-gtk.so"); + if std::path::Path::new(&path).exists() { + so_path = path; + break; + } + } + sciter::set_library(&so_path).ok(); + } + #[cfg(windows)] + // Check if there is a sciter.dll nearby. + if let Ok(exe) = std::env::current_exe() { + if let Some(parent) = exe.parent() { + let sciter_dll_path = parent.join("sciter.dll"); + if sciter_dll_path.exists() { + // Try to set the sciter dll. + let p = sciter_dll_path.to_string_lossy().to_string(); + log::debug!("Found dll:{}, \n {:?}", p, sciter::set_library(&p)); + } + } + } + // https://github.com/c-smile/sciter-sdk/blob/master/include/sciter-x-types.h + // https://github.com/rustdesk/rustdesk/issues/132#issuecomment-886069737 + #[cfg(windows)] + allow_err!(sciter::set_options(sciter::RuntimeOptions::GfxLayer( + sciter::GFX_LAYER::WARP + ))); + use sciter::SCRIPT_RUNTIME_FEATURES::*; + allow_err!(sciter::set_options(sciter::RuntimeOptions::ScriptFeatures( + ALLOW_FILE_IO as u8 | ALLOW_SOCKET_IO as u8 | ALLOW_EVAL as u8 | ALLOW_SYSINFO as u8 + ))); + let mut frame = sciter::WindowBuilder::main_window().create(); + #[cfg(windows)] + allow_err!(sciter::set_options(sciter::RuntimeOptions::UxTheming(true))); + frame.set_title(&crate::get_app_name()); + #[cfg(target_os = "macos")] + crate::platform::delegate::make_menubar(frame.get_host(), args.is_empty()); + #[cfg(windows)] + crate::platform::try_set_window_foreground(frame.get_hwnd() as _); + let page; + if args.len() > 1 && args[0] == "--play" { + args[0] = "--connect".to_owned(); + let path: std::path::PathBuf = (&args[1]).into(); + let id = path + .file_stem() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("") + .to_owned(); + args[1] = id; + } + if args.is_empty() { + std::thread::spawn(move || check_zombie()); + crate::common::check_software_update(); + frame.event_handler(UI {}); + frame.sciter_handler(UIHostHandler {}); + page = "index.html"; + // Start pulse audio local server. + #[cfg(target_os = "linux")] + std::thread::spawn(crate::ipc::start_pa); + } else if args[0] == "--install" { + frame.event_handler(UI {}); + frame.sciter_handler(UIHostHandler {}); + page = "install.html"; + } else if args[0] == "--cm" { + frame.register_behavior("connection-manager", move || { + Box::new(cm::SciterConnectionManager::new()) + }); + page = "cm.html"; + *cm::HIDE_CM.lock().unwrap() = crate::ipc::get_config("hide_cm") + .ok() + .flatten() + .unwrap_or_default() + == "true"; + } else if (args[0] == "--connect" + || args[0] == "--file-transfer" + || args[0] == "--port-forward" + || args[0] == "--rdp") + && args.len() > 1 + { + #[cfg(windows)] + { + let hw = frame.get_host().get_hwnd(); + crate::platform::windows::enable_lowlevel_keyboard(hw as _); + } + let mut iter = args.iter(); + let Some(cmd) = iter.next() else { + log::error!("Failed to get cmd arg"); + return; + }; + let cmd = cmd.to_owned(); + let Some(id) = iter.next() else { + log::error!("Failed to get id arg"); + return; + }; + let id = id.to_owned(); + let pass = iter.next().unwrap_or(&"".to_owned()).clone(); + let args: Vec = iter.map(|x| x.clone()).collect(); + frame.set_title(&id); + frame.register_behavior("native-remote", move || { + let handler = + remote::SciterSession::new(cmd.clone(), id.clone(), pass.clone(), args.clone()); + #[cfg(not(any(feature = "flutter", feature = "cli")))] + { + *CUR_SESSION.lock().unwrap() = Some(handler.inner()); + } + Box::new(handler) + }); + page = "remote.html"; + } else { + log::error!("Wrong command: {:?}", args); + return; + } + #[cfg(feature = "inline")] + { + let html = if page == "index.html" { + inline::get_index() + } else if page == "cm.html" { + inline::get_cm() + } else if page == "install.html" { + inline::get_install() + } else { + inline::get_remote() + }; + frame.load_html(html.as_bytes(), Some(page)); + } + #[cfg(not(feature = "inline"))] + frame.load_file(&format!( + "file://{}/src/ui/{}", + std::env::current_dir() + .map(|c| c.display().to_string()) + .unwrap_or("".to_owned()), + page + )); + let hide_cm = *cm::HIDE_CM.lock().unwrap(); + if !args.is_empty() && args[0] == "--cm" && hide_cm { + // run_app calls expand(show) + run_loop, we use collapse(hide) + run_loop instead to create a hidden window + frame.collapse(true); + frame.run_loop(); + return; + } + frame.run_app(); +} + +struct UI {} + +impl UI { + fn recent_sessions_updated(&self) -> bool { + recent_sessions_updated() + } + + fn get_id(&self) -> String { + ipc::get_id() + } + + fn temporary_password(&mut self) -> String { + temporary_password() + } + + fn update_temporary_password(&self) { + update_temporary_password() + } + + fn set_permanent_password(&self, password: String) { + let _ = set_permanent_password_with_result(password); + } + + fn is_local_permanent_password_set(&self) -> bool { + is_local_permanent_password_set() + } + + fn is_permanent_password_set(&self) -> bool { + is_permanent_password_set() + } + + fn get_remote_id(&mut self) -> String { + LocalConfig::get_remote_id() + } + + fn set_remote_id(&mut self, id: String) { + LocalConfig::set_remote_id(&id); + } + + fn goto_install(&mut self) { + goto_install(); + } + + fn install_me(&mut self, _options: String, _path: String) { + install_me(_options, _path, false, false); + } + + fn update_me(&self, _path: String) { + update_me(_path); + } + + fn run_without_install(&self) { + run_without_install(); + } + + fn show_run_without_install(&self) -> bool { + show_run_without_install() + } + + fn get_license(&self) -> String { + get_license() + } + + fn get_option(&self, key: String) -> String { + get_option(key) + } + + fn get_local_option(&self, key: String) -> String { + get_local_option(key) + } + + fn set_local_option(&self, key: String, value: String) { + set_local_option(key, value); + } + + fn peer_has_password(&self, id: String) -> bool { + peer_has_password(id) + } + + fn forget_password(&self, id: String) { + forget_password(id) + } + + fn get_peer_option(&self, id: String, name: String) -> String { + get_peer_option(id, name) + } + + fn set_peer_option(&self, id: String, name: String, value: String) { + set_peer_option(id, name, value) + } + + fn using_public_server(&self) -> bool { + crate::using_public_server() + } + + fn is_incoming_only(&self) -> bool { + hbb_common::config::is_incoming_only() + } + + pub fn is_outgoing_only(&self) -> bool { + hbb_common::config::is_outgoing_only() + } + + pub fn is_custom_client(&self) -> bool { + crate::common::is_custom_client() + } + + pub fn is_disable_settings(&self) -> bool { + hbb_common::config::is_disable_settings() + } + + pub fn is_disable_account(&self) -> bool { + hbb_common::config::is_disable_account() + } + + pub fn is_disable_installation(&self) -> bool { + hbb_common::config::is_disable_installation() + } + + pub fn is_disable_ab(&self) -> bool { + hbb_common::config::is_disable_ab() + } + + fn get_options(&self) -> Value { + let hashmap: HashMap = + serde_json::from_str(&get_options()).unwrap_or_default(); + let mut m = Value::map(); + for (k, v) in hashmap { + m.set_item(k, v); + } + m + } + + fn test_if_valid_server(&self, host: String, test_with_proxy: bool) -> String { + test_if_valid_server(host, test_with_proxy) + } + + fn get_sound_inputs(&self) -> Value { + Value::from_iter(get_sound_inputs()) + } + + fn set_options(&self, v: Value) { + let mut m = HashMap::new(); + for (k, v) in v.items() { + if let Some(k) = k.as_string() { + if let Some(v) = v.as_string() { + if !v.is_empty() { + m.insert(k, v); + } + } + } + } + set_options(m); + } + + fn set_option(&self, key: String, value: String) { + set_option(key, value); + } + + fn install_path(&mut self) -> String { + install_path() + } + + fn install_options(&self) -> String { + install_options() + } + + fn get_socks(&self) -> Value { + Value::from_iter(get_socks()) + } + + fn set_socks(&self, proxy: String, username: String, password: String) { + set_socks(proxy, username, password) + } + + fn is_installed(&self) -> bool { + is_installed() + } + + fn get_supported_privacy_mode_impls(&self) -> String { + serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl()) + .unwrap_or_default() + } + + fn is_root(&self) -> bool { + is_root() + } + + fn is_release(&self) -> bool { + #[cfg(not(debug_assertions))] + return true; + #[cfg(debug_assertions)] + return false; + } + + fn is_share_rdp(&self) -> bool { + is_share_rdp() + } + + fn set_share_rdp(&self, _enable: bool) { + set_share_rdp(_enable); + } + + fn is_installed_lower_version(&self) -> bool { + is_installed_lower_version() + } + + fn closing(&mut self, x: i32, y: i32, w: i32, h: i32) { + crate::server::input_service::fix_key_down_timeout_at_exit(); + LocalConfig::set_size(x, y, w, h); + } + + fn get_size(&mut self) -> Value { + let s = LocalConfig::get_size(); + let mut v = Vec::new(); + v.push(s.0); + v.push(s.1); + v.push(s.2); + v.push(s.3); + Value::from_iter(v) + } + + fn get_mouse_time(&self) -> f64 { + get_mouse_time() + } + + fn check_mouse_time(&self) { + check_mouse_time() + } + + fn get_connect_status(&mut self) -> Value { + let mut v = Value::array(0); + let x = get_connect_status(); + v.push(x.status_num); + v.push(x.key_confirmed); + v.push(x.id); + v + } + + #[inline] + fn get_peer_value(id: String, p: PeerConfig) -> Value { + let values = vec![ + id, + p.info.username.clone(), + p.info.hostname.clone(), + p.info.platform.clone(), + p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), + ]; + Value::from_iter(values) + } + + fn get_peer(&self, id: String) -> Value { + let c = get_peer(id.clone()); + Self::get_peer_value(id, c) + } + + fn get_fav(&self) -> Value { + Value::from_iter(get_fav()) + } + + fn store_fav(&self, fav: Value) { + let mut tmp = vec![]; + fav.values().for_each(|v| { + if let Some(v) = v.as_string() { + if !v.is_empty() { + tmp.push(v); + } + } + }); + store_fav(tmp); + } + + fn get_recent_sessions(&mut self) -> Value { + // to-do: limit number of recent sessions, and remove old peer file + let peers: Vec = PeerConfig::peers(None) + .drain(..) + .map(|p| Self::get_peer_value(p.0, p.2)) + .collect(); + Value::from_iter(peers) + } + + fn get_icon(&mut self) -> String { + get_icon() + } + + fn remove_peer(&mut self, id: String) { + PeerConfig::remove(&id); + } + + fn remove_discovered(&mut self, id: String) { + remove_discovered(id); + } + + fn send_wol(&mut self, id: String) { + crate::lan::send_wol(id) + } + + fn new_remote(&mut self, id: String, remote_type: String, force_relay: bool) { + new_remote(id, remote_type, force_relay) + } + + fn is_process_trusted(&mut self, _prompt: bool) -> bool { + is_process_trusted(_prompt) + } + + fn is_can_screen_recording(&mut self, _prompt: bool) -> bool { + is_can_screen_recording(_prompt) + } + + fn is_installed_daemon(&mut self, _prompt: bool) -> bool { + is_installed_daemon(_prompt) + } + + fn get_error(&mut self) -> String { + get_error() + } + + fn is_login_wayland(&mut self) -> bool { + is_login_wayland() + } + + fn current_is_wayland(&mut self) -> bool { + current_is_wayland() + } + + fn get_software_update_url(&self) -> String { + crate::SOFTWARE_UPDATE_URL.lock().unwrap().clone() + } + + fn get_new_version(&self) -> String { + get_new_version() + } + + fn get_version(&self) -> String { + get_version() + } + + fn get_fingerprint(&self) -> String { + get_fingerprint() + } + + fn get_app_name(&self) -> String { + get_app_name() + } + + fn get_software_ext(&self) -> String { + #[cfg(windows)] + let p = "exe"; + #[cfg(target_os = "macos")] + let p = "dmg"; + #[cfg(target_os = "linux")] + let p = "deb"; + p.to_owned() + } + + fn get_software_store_path(&self) -> String { + let mut p = std::env::temp_dir(); + let name = crate::SOFTWARE_UPDATE_URL + .lock() + .unwrap() + .split("/") + .last() + .map(|x| x.to_owned()) + .unwrap_or(crate::get_app_name()); + p.push(name); + format!("{}.{}", p.to_string_lossy(), self.get_software_ext()) + } + + fn create_shortcut(&self, _id: String) { + #[cfg(windows)] + create_shortcut(_id) + } + + fn discover(&self) { + std::thread::spawn(move || { + allow_err!(crate::lan::discover()); + }); + } + + fn get_lan_peers(&self) -> String { + // let peers = get_lan_peers() + // .into_iter() + // .map(|mut peer| { + // ( + // peer.remove("id").unwrap_or_default(), + // peer.remove("username").unwrap_or_default(), + // peer.remove("hostname").unwrap_or_default(), + // peer.remove("platform").unwrap_or_default(), + // ) + // }) + // .collect::>(); + serde_json::to_string(&get_lan_peers()).unwrap_or_default() + } + + fn get_uuid(&self) -> String { + get_uuid() + } + + fn open_url(&self, url: String) { + #[cfg(windows)] + let p = "explorer"; + #[cfg(target_os = "macos")] + let p = "open"; + #[cfg(target_os = "linux")] + let p = if std::path::Path::new("/usr/bin/firefox").exists() { + "firefox" + } else { + "xdg-open" + }; + allow_err!(std::process::Command::new(p).arg(url).spawn()); + } + + fn change_id(&self, id: String) { + reset_async_job_status(); + let old_id = self.get_id(); + change_id_shared(id, old_id); + } + + fn http_request(&self, url: String, method: String, body: Option, header: String) { + http_request(url, method, body, header) + } + + fn post_request(&self, url: String, body: String, header: String) { + post_request(url, body, header) + } + + fn is_ok_change_id(&self) -> bool { + hbb_common::machine_uid::get().is_ok() + } + + fn get_async_job_status(&self) -> String { + get_async_job_status() + } + + fn get_http_status(&self, url: String) -> Option { + get_async_http_status(url) + } + + fn t(&self, name: String) -> String { + crate::client::translate(name) + } + + fn is_xfce(&self) -> bool { + crate::platform::is_xfce() + } + + fn get_api_server(&self) -> String { + get_api_server() + } + + fn has_hwcodec(&self) -> bool { + has_hwcodec() + } + + fn has_vram(&self) -> bool { + has_vram() + } + + fn get_langs(&self) -> String { + get_langs() + } + + fn video_save_directory(&self, root: bool) -> String { + video_save_directory(root) + } + + fn handle_relay_id(&self, id: String) -> String { + handle_relay_id(&id).to_owned() + } + + fn get_login_device_info(&self) -> String { + get_login_device_info_json() + } + + fn support_remove_wallpaper(&self) -> bool { + support_remove_wallpaper() + } + + fn has_valid_2fa(&self) -> bool { + has_valid_2fa() + } + + fn generate2fa(&self) -> String { + generate2fa() + } + + pub fn verify2fa(&self, code: String) -> bool { + verify2fa(code) + } + + fn verify_login(&self, raw: String, id: String) -> bool { + crate::verify_login(&raw, &id) + } + + fn generate_2fa_img_src(&self, data: String) -> String { + let v = qrcode_generator::to_png_to_vec(data, qrcode_generator::QrCodeEcc::Low, 128) + .unwrap_or_default(); + let s = hbb_common::sodiumoxide::base64::encode( + v, + hbb_common::sodiumoxide::base64::Variant::Original, + ); + format!("data:image/png;base64,{s}") + } + + pub fn check_hwcodec(&self) { + check_hwcodec() + } + + fn is_option_fixed(&self, key: String) -> bool { + crate::ui_interface::is_option_fixed(&key) + } + + fn get_builtin_option(&self, key: String) -> String { + crate::ui_interface::get_builtin_option(&key) + } + + fn is_remote_modify_enabled_by_control_permissions(&self) -> String { + match crate::ui_interface::is_remote_modify_enabled_by_control_permissions() { + Some(true) => "true", + Some(false) => "false", + None => "", + } + .to_string() + } +} + +impl sciter::EventHandler for UI { + sciter::dispatch_script_call! { + fn t(String); + fn get_api_server(); + fn is_xfce(); + fn using_public_server(); + fn is_custom_client(); + fn is_outgoing_only(); + fn is_incoming_only(); + fn is_disable_settings(); + fn is_disable_account(); + fn is_disable_installation(); + fn is_disable_ab(); + fn get_id(); + fn temporary_password(); + fn update_temporary_password(); + fn set_permanent_password(String); + fn is_local_permanent_password_set(); + fn is_permanent_password_set(); + fn get_remote_id(); + fn set_remote_id(String); + fn closing(i32, i32, i32, i32); + fn get_size(); + fn new_remote(String, String, bool); + fn send_wol(String); + fn remove_peer(String); + fn remove_discovered(String); + fn get_connect_status(); + fn get_mouse_time(); + fn check_mouse_time(); + fn get_recent_sessions(); + fn get_peer(String); + fn get_fav(); + fn store_fav(Value); + fn recent_sessions_updated(); + fn get_icon(); + fn install_me(String, String); + fn is_installed(); + fn get_supported_privacy_mode_impls(); + fn is_root(); + fn is_release(); + fn set_socks(String, String, String); + fn get_socks(); + fn is_share_rdp(); + fn set_share_rdp(bool); + fn is_installed_lower_version(); + fn install_path(); + fn install_options(); + fn goto_install(); + fn is_process_trusted(bool); + fn is_can_screen_recording(bool); + fn is_installed_daemon(bool); + fn get_error(); + fn is_login_wayland(); + fn current_is_wayland(); + fn get_options(); + fn get_option(String); + fn get_local_option(String); + fn set_local_option(String, String); + fn get_peer_option(String, String); + fn peer_has_password(String); + fn forget_password(String); + fn set_peer_option(String, String, String); + fn get_license(); + fn test_if_valid_server(String, bool); + fn get_sound_inputs(); + fn set_options(Value); + fn set_option(String, String); + fn get_software_update_url(); + fn get_new_version(); + fn get_version(); + fn get_fingerprint(); + fn update_me(String); + fn show_run_without_install(); + fn run_without_install(); + fn get_app_name(); + fn get_software_store_path(); + fn get_software_ext(); + fn open_url(String); + fn change_id(String); + fn get_async_job_status(); + fn post_request(String, String, String); + fn is_ok_change_id(); + fn create_shortcut(String); + fn discover(); + fn get_lan_peers(); + fn get_uuid(); + fn has_hwcodec(); + fn has_vram(); + fn get_langs(); + fn video_save_directory(bool); + fn handle_relay_id(String); + fn get_login_device_info(); + fn support_remove_wallpaper(); + fn has_valid_2fa(); + fn generate2fa(); + fn generate_2fa_img_src(String); + fn verify2fa(String); + fn check_hwcodec(); + fn verify_login(String, String); + fn is_option_fixed(String); + fn get_builtin_option(String); + fn is_remote_modify_enabled_by_control_permissions(); + } +} + +impl sciter::host::HostHandler for UIHostHandler { + fn on_graphics_critical_failure(&mut self) { + log::error!("Critical rendering error: e.g. DirectX gfx driver error. Most probably bad gfx drivers."); + } +} + +#[cfg(not(target_os = "linux"))] +fn get_sound_inputs() -> Vec { + let mut out = Vec::new(); + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + if let Ok(devices) = host.devices() { + for device in devices { + if device.default_input_config().is_err() { + continue; + } + if let Ok(name) = device.name() { + out.push(name); + } + } + } + out +} + +#[cfg(target_os = "linux")] +fn get_sound_inputs() -> Vec { + crate::platform::linux::get_pa_sources() + .drain(..) + .map(|x| x.1) + .collect() +} + +// sacrifice some memory +pub fn value_crash_workaround(values: &[Value]) -> Arc> { + let persist = Arc::new(values.to_vec()); + STUPID_VALUES.lock().unwrap().push(persist.clone()); + persist +} + +pub fn get_icon() -> String { + // 128x128 + #[cfg(target_os = "macos")] + // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding + { + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAABhGlDQ1BJQ0MgcHJvZmlsZQAAeJx9kT1Iw0AYht+mSkUqHewg4pChOlkQFXHUVihChVArtOpgcukfNGlIUlwcBdeCgz+LVQcXZ10dXAVB8AfE1cVJ0UVK/C4ptIjxjuMe3vvel7vvAKFZZZrVMwFoum1mUgkxl18VQ68QEEKYZkRmljEvSWn4jq97BPh+F+dZ/nV/jgG1YDEgIBLPMcO0iTeIZzZtg/M+cZSVZZX4nHjcpAsSP3Jd8fiNc8llgWdGzWwmSRwlFktdrHQxK5sa8TRxTNV0yhdyHquctzhr1Tpr35O/MFzQV5a5TmsEKSxiCRJEKKijgipsxGnXSbGQofOEj3/Y9UvkUshVASPHAmrQILt+8D/43VurODXpJYUTQO+L43yMAqFdoNVwnO9jx2mdAMFn4Erv+GtNYPaT9EZHix0BkW3g4rqjKXvA5Q4w9GTIpuxKQVpCsQi8n9E35YHBW6B/zetb+xynD0CWepW+AQ4OgbESZa/7vLuvu2//1rT79wPpl3Jwc6WkiQAAE5pJREFUeAHtXQt0VNW5/s5kkskkEyCEZwgQSIAEg6CgYBGKiFolwQDRlWW5BatiqiIWiYV6l4uq10fN9fq4rahYwAILXNAlGlAUgV5oSXiqDRggQIBAgJAEwmQeycycu//JDAwQyJzHPpPTmW+tk8yc2fucs//v23v/+3mMiCCsYQz1A0QQWkQEEOaICCDMERFAmCMigDBHRABhjogAwhwRAYQ5IgIIc0QEEOaICCDMobkAhg8f3m/cuHHjR40adXtGRkZmampqX4vFksR+MrPDoPXzhAgedtitVmttVVXVibKysn0lJSU7tm3btrm0tPSIlg+iiQDS0tK6FBQUzMjPz/+PlJSUIeyUoMV92zFI6PFM+PEsE/Rhx+i8vLyZ7JzIBFG2cuXKZQsXLlx8+PDhGt4PwlUAjPjuRUVFL2ZnZz9uNBrNPO/1bwKBMsjcuXPfZMeCzz///BP2/1UmhDO8bshFACaTybBgwYJZ7OFfZsR34HGPMIA5Nzf3GZZ5fsUy0UvMnu87nU6P2jdRXQCDBg3quXr16hVZWVnj1L52OIIy0Lx5895hQshl1cQjBw4cqFb1+mpe7L777hvOyP+C1W3Jal43AoAy1C4GJoJJGzZs2K3WdVUTwNSpU8cw56U4UuTzA2Ws4uLiTcyZzl6zZs1WNa6pigAo50fI1wZkY7I1qxLGq1ESKBaAr87/IkK+diBbk81HMCj1CRQJgLx9cvj0Uue7RRFnmSNd3+xBg0tEk0f0no82CLAYBSRGG9A9xuD93t5BNifbMw3craR1oEgA1NRrj96+yIiuaHRje10z9l5oRlmDCxU2N6ocLriIcy+/Yst/P9dCy3eBHT1MBgyIN2KwxYhhCdEY1SkGWZZoRAntSxhke+Jg/vz578q9hmwBUCcPtfPlxlcbF1mu/vpME76sdmLj2SZUOzw+glty+RVke78LpJTLv4nePyQLb9xqZxP+r9556ffEaAHjk2IxsUssctjRJSZKq6TdEMTBokWLVsrtLJItAOrhC3W972EEfnu6GUsqHVh7ygG7vyD05WYvm95sLbbyGdcVQWtx65tFrDljZ4cNRgNwLxPDjJ7xyO1qDmmVQRwQF5MnT35WVnw5kahvn7p35cRVA42sHF98xIF3Dtpw2OoJKMbRJpFKROAP72K+w/pzDqyvdaAnqy5+08uCp1Ms6BwdmlKBuGCcvMxKgXNS48oSQEFBwa9D0bfvcIv480EH3txvY86ceLl4J0giUrkI/OGrmf/10pEG/PH4RTzb24LCPh3QyajtoCZxwTh5tLCw8C3JceXcMD8//5dy4skFOXWrjzfhhT02VDLn7nJdroRI9URAP1lZqfRaZQM+PGXFK/064slkCwwaOo2Mk2maCGDkyJH9fEO6muCY1Y0nSxqx4VSzj3hpxGgpAgpf2+TBUwfr8c8LTnyamcSCaCMC4oS4KS0tPSolnmQB0GQOaDCeT2ZdesiJ2TttaGgOLOohixgtRUA/LmPO4rQe8bivs2Y1pUDcMAF8IiWSZAGMGDHidqlxpKKREV7wTxuWHbncDFOLGC1F8E2dQ0sBEDe3sX98BZCRkTFYahwpOMa8+ge/teKHOneLYTkQo5UIojSe+CSHG8kCSE1N7SM1TrDYe86FBzY04rTdoxKpwYQHt3tNTIpVxzBBguZXSo0jWQC+CZyqY9tpFyZ+3eir79XM2W2F53Mv6hf4eaK2ApDDjZxmoOqV2ncnXZjEyLe5fIblSEzr4dW91xOM/PcGdVLTRMFCMjdyBKBqL0fJGRce/IrIB+c6vq3w6tzriV7xWJjZSdM+gABI5iakC0MqLniQs97OvP6AkzoWwRO9GfmDQ0a+LIRMAA1NInLW2XDO7qvz/d263q/6E8HMPnH4QGfkE0IiAOrafXSjA+V1/iFbXGt4HYlgJsv5H9zUUXfkE0IigA/KmvG3w662SVOJVBqkG5FkxPDORmR2jELfeAO6mgyIMwreYDa36O3CPW7z4IDVhT3nm7Gjvtl7vq17eXN+lj7JJ2gugEPnPSjc2hR8zpUpAjNL2eQ+MXiorwkTekTDEi2NICcjf2ttE9accuKzk3bUNQVUVb57FaTG409DOsgin0rB4loHNtU7QI+W08WMMZ20bTYSNBUAJXrmRids5PRdIhCqiqCbWcCcwWY8MdCEzib5DRZTlIAJ3Uze4+0hCVhVZcefjtrwk9WN9PgoPJcWh+m9zbIGe5weEY+U1eJvNXZfmkS8deIi5vROwH+nJ8p+ZjnQVAB//cmFLVVu3zeJdXgbv8cywl64ORaFWbGSc3tbMLNrz+gb5z2UgsjP+6EWxefs1/g/bzMRjOloQm5X5fcJFpoJwNosYv62Zh+ZkOfIXef3O7pHYcnYeAzs2D7m6V0PNKFlKiOfZhNdLy3PV5zH/UlmmDSaZqaZAN7b04xT1gD2VRLB80Ni8fptse1+KjeRP+X7WnxF5PvRSlqP2F1YeNKK2aw60AKaCIDa/EU7XQG5X7kIWKmMD8fG4rFBJi2SoAhE/uQ9tfj6nBPBjHC+cawBM5PjWdXDf2qZJgL46AcX6gOEr1QERP6K8WY8nBajxeMrgp3I312HDV7yEVRaTzs9WFzdiKdS+JcC3AXgZk7P+7tdrRbfckXw0Vj9kP/grjp8S+RLrPreOWFFQS/+8wq5C2DdEQ+ONwScUCiCwmEm/Dqj/ZNPxf6kHXXY6M/5EtN6yObCxjqnd/0BT3AXwJJ/tZb75YlgdM8ovDay/df5hJcPWrGxpkmR4JewakDXAjjvELGuwnOd3CzNMGbWtl9ytxnGdu7tE6jD66NKW/BO7XVEsLbGDqvbAwtHZ5CrAIj8JteNivTgDTP/1hikd9THLnK0LLHWGZgOyBIBTZD5mjUb87rz6xjiLAB3EPV624bpGS/g+Vvaf73vB/UcDk4wYv9Fl7TmbSt2+lKvAvAu3DzqS4lCETx/azTiVO7e5Y1Z/ePwm+/J+5XYx3FV+G+ZAKhK4bXAhJsAys+JONeIAA8YkCOCeJbxH78pmtdjcsO03rF4oewiLvo3JJApAlp7WGF3YUAcHxtwE0DJSX/ul9LMu9YwU9ON6GjSV+4nWIwGTEmOxdLjdskdXVeH336+SX8C2Hval1jJbf0rDfPwgPY9wHMjTOlpwtJjdskdXVeH39vQjF9x2oSHmwD2nQ1MKGSJIJZxP76PfgUwvlsMjLSfgBhsutGqncqsLm7PyE0Ah2p92V92r5+A23sYYDbqr/j3g6qBYR2N2FVPBMoXwaFGnQmAdtCovggo7f8f3l0f7f4b4ZZO0S0CUDD4VWV3e3c447FJFRcBnG2kQaCAEzJFkJmkfwEMshhl+kKXw9McqpomD3qY1K8OuQigjqa6icravxS+bwf9Fv9+9DYbrkqrPBHUNetIAFanKClx1zNGV7P+BZAU4yvFFIqgpT9BfXARQJN/3qdCEXBq+moKasm0XgVIE4F/V1O1wakVIAQk2vddhgj0n/8pmcINmsPBi4AP/ZwE4N1EU4WlXLZm6B5Wf1ewwmVoMXoaC0jwD9wpFEHLwlF9o8bpCaI53LadLJz6Q7gIIJG2KVDY9KHPJy7oXwCVVneQgr+xnWgncx7gIoBuFoAm7ngUiqC8Vv8C2H/B5xErEAFR3z1GRwKgaVsprA1//Lz0zp/A8Lur9S+AnbW+XkAFS9OTYw3cpsJxGwtI7wwmAGnt/qsNU3pSZE1K5gBF6bM9cKLRjcMXL21hLlsE6fH8Jm5xu3JWdwGbDouSO38Cw1ubgH+cEHFXqj4FsO6kkrWQlz/flKBDAQzrGZg4+SJYU+5mAtDnmMCqSqfCllDLZxpR5AVuV77Dv52kxM6fq8Ov3OdB0QQRsTobFj7U4Mbfz/iGcRWK4I7O/CbEchPAoK4CulsEnLFK6/y52jC1jSJWMRFMH6qviSHv/uSASNW/AEUtoSSTgMwEfmnnJgBKz4R0YPleKWr3nbwq/J936UsAVY0efHLQtx5Q4VrIu7uauK4P5LouICdTwPI9Pi9IgQjKzuqrOfife+xweDe+hCL/h37K7sl3KRxXAdw/CKzuRosxFIigfyf91P9bqpvxaUVTyxeF/g91/mX35LsghqsAOsQKmDQY+OxHMegirzXDzB6pj1bA+SYRj261+ZKkvOp7oEcMEjn1APrBfXXwjBFMAD9ApgcMFNwWhcduaf8CoJVQM/5uQ2XDVZtfKhDB9FT+28ZxF8C9AwX07wwcqZPuAT/Fcv7/TjRwWxalJn5X6sDayubW0yJDBL3MBuQk818PyV0AtLJ59p3sWCvN+Xmakf++Tsh/ebcDRT86L59QQQSzBmizFF6TPYIeGwm8+h1QYw1OBLPuEPCuDsinYr9wuwNv/+jbCKItkoMUQcdoAU+ma7NrqCYCiI8R8LtxIuYWo816b/ZoA/7HS74WTyYf9U4R07+z48tjzdKqtiB2RZ+TYUYnzs6fH5rtE/jUaOD9bcCx87iuCJ4bLeBtHZC/8YQLj2224ziHfQ97xBrw2wzt3jSmmQBoi5e3ckQ8/ClaNcScMQKKFJBPxTGNHiaw0oaXgI4xD//3251YcShgqZeMzp0bieDVYXFI0HAvBE33Cs67WcC88SLe3OyzjUhkiXjxbgEv3yuPOIdLxB+2uPHhHo93L8L+icAztxswY2gUEmPVMeT+Wg/e+b4JS8td3vkJavTwtSaC0V2j8GiatptgaSoAssHrEwXk3yLim4Mtaf9FhoCsHvKIsjWLmLTCje+O+iZdsMscqWelyQY3XtzsRs5AA6YMMmBCfwOSJCwyIZ4qznuw/qgbqw66sP20+9L1LxMMVUVA6wc+/pm27xsmhOSFEUOTBXYouwaRn7PcjU1HxFY9cHuTiM/2efDZfo/358FdgVuY0AYlGZCSICApDt53ChAfVubH1dhFbxG/v1bEzjMenGz1tfS+LxzeVPL6rXHel1lojZC+NEoubPS+oeUeH/lo09D0d99ZdtQQqZdLi0se+TWfA26mRvHe1oBPSgyezQzN/oe6E4CX/GU+8pV64FeE55Oz2wqf3sGAT8fGheyVM7oSgJf8v3p8cw3BgRhtRZBoMuCLeyze/6GCbgTQyMiftJRyPjgTo40IzKy6//yeeGR2Cu1EFzkCoEpUU8kS+TlLRGw+EnBSxyKgae6rJ8RhbE/V85+n7SBXQs4T0PYP8TLiyQJtN5O7lJFfgVa9fb2JgFoeq++NwwN9uKx9t0uNIFkAVqu11mKxaCaAFXuAjQfBzQPXUgSJMQLW3h+HMcl8al7iRmocyU9SWVl5PCsrq0/bIdXBxkPg5oEHF16dew3oyBy+iWZkJPKr8xk3x6TGkSyA8vLy/UwAd0qNJxdGv7ehYxHk9DNi6T1m5u0LqtmlNRA3UuNIFsCuXbt25OXlzZQaTy5yBgOLd4ADqVLDS49rZtX86z+LwbNDozWZ21BSUrJDahzJAtiyZcsmtCSRf4oYcrMETB8hYuku6EoEdyYb8PGEWFbka9ZgErdt27ZJaiTJAigtLT1aVVX1r5SUlJulxpUDsvHifAETBoqYtw44STuwt2MR9Igz4LU7ozF9sFHT3j3ihHFTKTWeLHd05cqVy+bOnftHOXHlgOw4bbiAKUNEvLcNeGsLUGdrXyLoZALmjDDit7dGwxKjHfF+ECdy4skSwMKFCxc/99xzfzAajdpNXWGIi6H5BMDTo0V8XAK89w8Bx+pDK4LeCQJm3WrEzKGh29be5XLZiBM5cWUJ4PDhw+eKi4sX5ebmzpITXykSmKHn/ByYPUbEV+UCFjP/YF25CKfCFUjBho8xinggzYAZQ4yYmMZv945gwbj4hDiRE1d2jwSrAv4rOzt7OisFOsi9hlJEMcNns1YCHQ0OZohyYP1PIr6pEFDTqK4I6IXe4/sJyEmPwgPpBtVmGykFy/0NxIXc+LIFwBR3pqio6KV58+a9I/caaoKWoT0yDOwQvNyV14goOQ58Xy16F5dW1ArMgRTh9rdfrrchE/vXqwNtcWPATd0E7ySSkb0EZHYRQjZkeyMQB8SF3PiK+iQXLFjwPisFcrOyssYpuY7aIJ4yGXmZ3bzfLp2ncYWzVnjnDl50tmxpS3MSaREmVSu0vV23eIS8SA8WZWVlW4gDJddQJACn0+nJy8t7ZBeDxWLh9FIT9UDEJrPcnXxFpaUPsq+G1Wo9RbYnDpRcR/GoxIEDB6rZg+QwR2RzKP2BcALV+8zmk8j2Sq+lyrDUhg0b9uTn52eztmhxRAR8QeSTrZnNd6txPdXGJdesWbOV+QN3rV69+ks9VAd6hK/Yn6QW+QRVB6apJBjBwESwnDmGd6l57XAHOXxU56tR7AdC9ZkJ9IBMAxOYd/oMa5++EqkSlIGKfGrqkbev1OFrDVymptCDzp8//71FixateuONN36fm5v7OBMCvzcg/xuCEW+n3lbq5FHSzm8LXGcF04M/9NBDs9PS0l4pKCiYwZyXab5RRH22vfhDrKqqKqOBHerbZ/ar4X1DTaaFUz91YWFhER3Dhw9PHTdu3PhRo0bdnpGRMTg1NbUvcxqTWDAaWGr/mwGpAyrK7TSHj6bYlZeX7yspKdlJ4/k03K7lg2i+LmD37t2V7PgL+/gXre8dwbXQzcKQCPggIoAwR0QAYY6IAMIcEQGEOSICCHNEBBDmiAggzBERQJgjIoAwR0QAYY7/B1LDyJ6QBLUVAAAAAElFTkSuQmCC".into() + } + #[cfg(not(target_os = "macos"))] // 128x128 no padding + { + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAEiuAABIrgHwmhA7AAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAEx9JREFUeJztnXmYHMV5h9+vZnZ0rHYRum8J4/AErQlgAQbMsRIWBEFCjK2AgwTisGILMBFCIMug1QLiPgIYE/QY2QQwiMVYjoSlODxEAgLEHMY8YuUEbEsOp3Z1X7vanf7yR8/MztEz0zPTPTO7M78/tnurvqn6uuqdr6q7a7pFVelrkpaPhhAMTEaYjJHDUWsEARkODANGAfWgINEPxLb7QNtBPkdoR7Ud0T8iphUTbtXp4z8pyQH5KOntAEhL2yCCnALW6aAnIDQAI+3MqFHkGJM73BkCO93JXnQnsAl4C8MGuoIv69mj2rw9ouKq1wEgzRiO2noSlp6DoRHleISgnQkJnRpLw0sI4v9X4H2E9Yj172zf+2udOflgYUdYXPUaAOTpzxoImJkIsxG+YCfG+Z7cecWDIN5+J8hqjNXCIW3rdMqULvdHWBqVNQDS8tlwNPCPKJcjOslOjGZGt2UHQTStHZGnMPxQG8d9mOk4S6myBEBWbj0aZR7ILISBPRlZOiMlr+QQgGAhvITqg0ybsEZjhZWHygoA+VnbaSBLEaY6dgb0Vgii+h2GO2gcv7JcQCgLAOSp7ZNBlyI6sycR+igEILoRdJFOnfgCJVZJAZCf7pxETfhmlIsQjHNH9VkIAF0H1iKdetjvKJFKAoC0EODA9msQvQUYmL2j8uwMJ/uygwAL0dvZMHGJNmFRZBUdAHlix5dQfQw4IbeO6tMQgOgybZx4I0VW0QCQ5dQQ2v4DhO8Dofw6qk9DEIZwg0497H8ookwxKpEV7WOo2fES0IQSAnrmwBrXEhq/lcR5cnJasm1KWq5lx9knl5NvvW7877EPIMFZFFm+AyA/2Xk6EngbOCVtA1chsO1V/4oiyzcABERW7FiI6osoo2IZVQicy7HtwxRZQT8KlWaCjNm5AiOzY+Oe0jPuqdjjXjQttpWe8TMhT0Djxs/ktGRbCi07g4/kWW/C8afxX/htAc2elzyPAPIQ/Ri7cyXCbBfjXjUS9Nh2IeEnKLI8BUB+1DaI/jvXoJwfS6xC4FxOcr2i12vjpM0UWZ6dBsry/aOh61fAMfmfCyfllfoU0Y2P+dab6P/d+rVx11MCeQKALN8zDA1vAJlc+AWRpLw+D4Hcp9PHLqBEKngIkBXtdVjWWlQmA4XMgBPTymU4cONj3vXKvaXsfCgQAGkhRGfoOZDjgHwnP3F5FQXBvTp97HWUWHkDIM0Y2nY/C5zpwQw4Lq8SINC79azSdz4UEgGG7l4CnOfJDDglr09DcK/+dWkmfE7KaxIoD++aDmYtaMCDGbBtXxETQ7lXzx5dFt/8qHIGQB7eORENvI0w1E4pZAacZN+XIUDu1XPKq/MhRwDkp/Rn7+7XQY6xE6I5ZQ/BbrB+j8gWkC2g7cBeAtJFdA2GyqGIDkUYA0xAtAEYkrFstxAY7tIZY26gDJXbvYDd+5qRuM7XyBbBt+vjONgnl0NKvZtRXYewAfRtvjX8Q00cwV1JWraNRbqPRbURkTOAoxGRnHzE3KUzRpVl50MOEUAe2H88Yr0GBEu/esapHPkjWE+CPKOzh25ydVA5Sp5vHw3hbwIXInoSEvEgnY/C7Xru6MV++AIgL245FmMuQmhArQ7EvInK4zpt3Meuy3ADgDQT4tC9b6EclbbzSgOBgq5B9T7mDNuQz7c8X8kv2o9Auq8C5gB1ST5uQ/VKPW/MSl/qbmkNMbTun1G+69A2BxDma+OER12V5QqA+/c2Y1jSk5BQYSkgUGAlAb3Zr2+7W8na7fV0dH0To18G3YOwkfrOn2vjpA5f6mtpDTGk7jmUv8n4BYFLdOqEf81aXjYA5L49R2DMRtCa1A6iFBC8glgLdM7QNzM63gclaz/sR03/51DOdREld9PV9Rd65uFbM5WZ/UKQBG5DqbEnenHp6S7yuL8gkrmceHs7bT8Wi/jzoY0V2fktrSHMgGdRzgXcXKSqpya0hCzKGAHkngNfwVivJ052nM6z8TsSvALM1ssHb8l2QH1Rsn5zfzprnkf0bDshPhMyRIIuAqZBTxv3QbqyM0eAgHUbINkvu+JjJNDlhAefUbGd39Ia4kBNC3B2HpfUa+i2bstYfroIIPftn4HyQgnX1nchXKFXDM46kemrkvWb+9MRWgV6lp0Qzchp0qyY8MnaOOkNpzrSRwAL+1cqpVlC1YnFhRXd+Ws/7Mf+fs+hkc6HXOZL8XmCFfxB2nqcIoDcc+AroG9EPh61jDOI33oeCQ6gOkO/M3h9Oqf7uqTlowHUml8C03Nq49h+ShtbqDlSzxj7v8l1OUcAteanHZsT0iI1eBcJurBkZkV3/ppPBzLQ/BvKdCC3Nnayt7cGY33Psb7kCCD3HRhPN39AtIZIWYlb3yKBAhfrd+ufdHK0EiRrPh0IuhqYljZK5h8J9hHS8XrKhB3xdaZGgG6uBGq8WZRBLpHg/oru/OXUoKwCmZYxSuYfCWrpNN9OrjcBAGnGoPT8QLFoEOgGttaX7R2zomjUpw8C010NlflCIFyaXG1iBAh1nAqMdbiq5CcEuyA8W5voTnauUiS/+PgIYG5O86V8IFD9S/mPj4+Jrzt5CLggzQUFByfwBgJlgc4b8n9UsgKBuajYfeE3BAG9IL7qGADSTBD4RoarSg5OUCgEL3FV3QoqXSpHRbaR/0ncegmBpRdI3HSxJwLUdE4FRqQ5jXAuuDAILLrNAk20qEypdvbs+w7BYfz6oxOiSSYu88wkQ58h4An9p9p3qQqEl121sVcQBJgR/bcHAGFaltOI7A66hyBMWG+lKlsHeRyho2gQWDRGdw2ANDMY5egUQ/8geF7n15ft83OLLZ05qo0wz9j/xGf4BsGJ9kWnaAQIHjwdCBTtFzzGuo+qkqQP5dTGhUEQop91EkQBsLTR9WmEWwfTQaDSqlfXO96arGTp+aPfAXm/aBCIPQxE5wDHpjVMKMQTCCr2cm9WKc/k3Mb5QmDpCdADQEPazvMaAhN4mqqcFQ635NXG+UHQYFss2zuScM1nsdyUu1BJ6bF9dbjD52CfWM4mvbZ2MlWllTz/+WZgYl5t7GSfXE58XqBzsKEr0BCjJWKbuPUwEgjrqCqzVP7T3oLvkaCr35EG4h/t4jMEYdlAVZkl1oa0nec1BCINBmRiiqFTwV5AYOQdqsqscMC+OloMCNDDDcoIR0OngguDYKteO6Cy7/q5UlsrYL9tzHcIdIQhdgPIwdCp4HwhsPT3VJVVOnPyQZQ/9CTEb72GQIYbkBEZDZ0KzgcCkc0pR1tVGsnHRXlmkTLcoDIiq6FTwTlDwBaqcifFfkex/xAMN6B1rmhxKjgnCGQ7VblVW0obgx8QDDEoxoUhBUMgupeq3EnFfraA/xCY3NehOdm7gSAs+6jKpbQjbRsnpEGhEBhUxI1hQoVO9tkgMFKU9xP1DUWaqggQGGwIshoWDEGY/lTlTsqgrG2ckpcfBAaNrMf3GwKRAVTlUjrIVRun5OUMgRqQbWk7z0sILB1BVe6UcHXWVwh2GFTbHQv2GgLDWKpyKZ2QUxun5LmGoN0A7amF+ACBMp6q3Ellgr2N/g8+QdBuEGlPnbSlGHoBQQNVZZU8/ekwkFF5tbGTfSYILN1qCOvWrOvHvIFgjDTvGUZVmaWBKWk7z3sI2g1iPkgxdCrYCwhqQsdSVRbJ8UD6zvMSAsyfDJa1ydEwXp5BoI0OpVcVL5VpPfvgKwQW7xtM8H1XtHgDwdeoKq3kic9rUU5OjcQ+QdBNq9Hb2AZsLQ4EMkVu3zucqpwlwekg/QCH4dhzCNp05qi26PX51gyGXkIQoLvmG1SVThcBqW0c2/cUglaI3nVQeSODoYMzBUAgXEhVKZKWHYegnJN28h3b9woC3oTYbSdrfVGWINn7p8qtnYdTVaIOWBcD9v2SYkCAvUTfBmBA8L+AriJBYFCuoqqYpIUAcE1qR+MXBGGk36sQAUCb2Av6joNh5gqdHHQHwWVyF3VUZWvf9vNROdz1tZjYfp4QiLyrfzd4J8Q/IcSSDWloyVyhk4PZIains6M6GYTow7mWAqltHEvDWwgsa320iB4AjFntWKFTwV5AoIHjqArG77gCmJy2jWNpeAcBsja61wPAAF5D+cixQqeCC4cg/pMVKfnZrkMRWercbr5B8Dk6cn30ozEAtAkLaHF/GlEgBEL1d4Kd4ftBRwJp2s0HCJSf60zC0Y8lLtRUszL1w/gAgbZRV/MMFSz58Y4ZqFySvd08hgBJeJdhIgD38BuI/ITLLwhEFORanc8BKlTy4+3jMPIT9+3mGQSfsGn4q/G+JACgimLJY/6uQ5Ol2hSq2OcESQshCLRg4fybTPAPAovHI0N9TKlr9UM8itLhCwSit2pT8OaUOitEAsKOnf8CeiKQz5enEAi6CQd+lOxTCgB6G22gT2U8jcgHAtE7dWnopuT6KkrLd92JcKmrbyt4C4HynF405KNkl9L8Wsc8mFBAihPkCkGzNocWOddVGZLluxYDCz150ko+EIg+5OSXIwB6N++hvJRQQIoTuIWgSW8JLnWqpxIkIPLIrrtRluU1bjvZ5w7BW3rhiNec/AtmcL0ZVfvlRQpIZEftunu2QuyxZQl5ApbepLcFK/ah0PIQ/ajZ/SjCJWnbLfo/9LSbaqItDvbJtmQoW0g778r87uDrdDVE31QddUbj9uO3ceXYTizR280taQvv45KHto8jGGwBTnTVbhL/4Yh9sq2TfbJtctnKqzpr2Knp/Mz8i11LFgHhlNAT2yc19Nj7iyu68x/ecx6B4DsoibP92D6p7ebbcGBlfBlXxggAIAusxxC5jLhjyEw0N+rtZlnGQvuo5JFdh2KZO4C5jt/g4keCVTpr6Ncz+Zz9N/tB04RiP9whWyQQrq/EzpdmQvLD3dcQNh+gzI2kOnzbI+kpafgRCboQSfvO4Jjv2SIAgCxgDugKJOK9E9GGhXqHuSdrYXlKbjnYgCWXYfQIIIRar6Os0Kb+f/arzqw+NRNi8L4LMXoT6BftxGhm1KpEkcDoLTpr2JKsx+AGAABZwCzQBxCGJFW4Hax5eldgZfpP5y9pJoR2PoDId5LqBTQMrAJ9iJv6v6yJ3xHfJA/sG4lYl6DyPWBs2s4rFQTQyu7tX9arv9hJFrkGAEAWcQjd/C1qNSAEEfMu+1mlD+PLA6BkIbXUdq0BGjM2ov3/FuBZxDxLd807yde8C/bl3j3DCJizUP4B4UzQYNqZd4qPCX76DYGFcIpePOR1V8eVCwDFlCykloFdLwCnu2rEhMaQbaDrgZdB36W74z1tstfAua7/no7DEJ0CHI9YU4EpgHF9+pXiYxb/nezzgUB5UC8dco2bY7Q/UoYARDr/Vyin5dSImTvjE+Aj0M8w8jkW3QR0N4ogMhi0FiPDUGsCMAmJLNFOd53Dfb3u/XeyzwUC5T26O07SuaP341JlB4A0M5Cu7jUIUz17MUIujeimM/Kt118I9iDWCTpnaE7PZC6rR7cldD6kOdUBcDg1ynpBBIe8DOU41evm3ke8ivH0NY38F5Y5uXY+lBEA0sxADnavAaZmP9+FsoagUP8z1evs/x16xeDnyUNlAYA0M4jO8DqQqZ41YqVAYPEC9Yfmvc6i5ADIQmrpCK8GTvW8Efs8BPIG/TsviF/lm6tKOgmUhdQSDEfO80k/sUo+1UmxTWNfLhPDQv13tt9IwJyul9cX9BT2kgEgC6kloGtAG4vSiH0Lgj9BzVd17sBPKVAlGQKkmUGY8LrYM4OKEU77znCwGZjuRedDCQAQQdinT6JyClDcRuz9EGykq+urOveQnncKFaiiDwFyPeeCri5pOO2dw8F/Y8k5emXdNjxU8YcAy5pV8m9Sb4sEsIbAvmledz6UZA4gRwKlD6e9AwIFvYut9V/P5fp+LsqwKtg3daHYbaeQ12pj16tmsf8k2yeXg0O9CWWnqddf/3cizNF5h/yykMbOphIMAfo2UD4Tq3KMBOi7qHWcXlnna+dDKQBQ8yjRh0NUIUiuw0LlAbrqT9arvZvpZ1JJLgTJtSxDdHGZzK7L5exgI8b6tl5d3/PMxiKoNPcC7udGVK5HsdesVXYk6ASa2DloSrE7H0oUAWKVX8dE1FqGyLdwWm4V2yeXb1JviQSK6CosXawL6kr2Yu2yWBEk19KA0TuBcyoDAl5Dwot0ft0rlFhlAUBUch1ngd5AdEVQX4NA+A1Gm3R+7TrKRGUFQFSygKMJWPNQuRihfy+HoAt0FaLL9braFx0PuIQqSwCikvmMpsaaBzILdJKdGM2MbssWgo8RXUE3j+hib+7c+aGyBiBesogGwtZsDBcDo+3EaGaZQKC0Y1iLWC10DFyrTZG3spaxeg0AUcnfE+Cw7tNQcyZGp4JMAYIlgqAb0d+isoGgrqaj/6te/yLJb/U6AJIlN1CHhE9DZSpGjwUagJE+QdCG8D6qbxCQlwn2e1WvZ4/Xx1RM9XoAnCSLGQrdX0LNkYh1GCIjEB2GMhzRUYjU9xgnQLAdQztoO8o2hK0gH2BkE8Fgq34fz2/Hllr/D1DoAB9bI40ZAAAAAElFTkSuQmCC".into() + } +} diff --git a/vendor/rustdesk/src/ui/ab.tis b/vendor/rustdesk/src/ui/ab.tis new file mode 100644 index 0000000..d0c2e9e --- /dev/null +++ b/vendor/rustdesk/src/ui/ab.tis @@ -0,0 +1,772 @@ +var selectTags = []; +var ab = { tags: [], peers: [] }; +var abLoading; +var abError; +var current_menu_peer_id = ''; +var current_menu_tag = ''; + +class AddressBook: Reactor.Component +{ + this var style; + this var selectedTags = function() { + var tags = handler.get_local_option("selected-tags"); + if (tags) return tags.split(","); + return []; + }(); + + function render() { + if (!handler.get_local_option("access_token")) { + return
{translate("Login")}
; + } + if (abLoading) { + return
; + } else if (abError) { + return
{abError} +
{translate("Retry")}
+
; + } + var peers = this.getPeers(); + var me = this; + return
+ + +
  • {translate('Add ID')}
  • +
  • {translate('Add Tag')}
  • +
  • {translate('Unselect all tags')}
  • + + +
  • {translate('Remove')}
  • + +
    +
    +
    {translate('Tags')}{svg_menu}
    +
    + {ab.tags.map(function(t) { + return = 0 ? "active" : "inactive"}>{t}; + })} +
    +
    +
    +
    + +
    +
    +
    ; + } + + event mouseup $(#tags span) (evt, me) { + if(evt.propButton) { + current_menu_tag = me.text; + me.popup($(#tag-context)); + return true; + } + } + + event click $(#retry) (_, __) { + refreshCurrentUser(); + } + + event click $(#login-link) (_, __) { + login(); + } + + event click $(#tags-label svg#menu) (_, me) { + me.popup($(#ab-context)); + } + + event click $(#add-id) (_, __) { + var me = this; + msgbox( + "custom-add-id", + translate("Add ID"), +
    +
    {translate("whitelist_sep")}
    + +
    , + "", + function(res=null) { + if (!res) return; + var value = (res.text || "").trim(); + var values = value.split(/[\s,;\n]+/g); + if (values.length == 0) return; + for (var v in values) { + var found; + for (var i = 0; i < ab.peers.length; ++i) { + if (ab.peers[i].id == v) { + found = true; + break; + } + } + if (found) continue; + ab.peers.push({ id: v }); + } + updateAb(); + me.update(); + }, + 300); + } + + event click $(#add-tag) (_, __) { + var me = this; + msgbox("custom-add-tag", translate("Add Tag"),
    +
    {translate("whitelist_sep")}
    + +
    , "", function(res=null) { + if (!res) return; + var value = (res.text || "").trim(); + var values = value.split(/[\s,;\n]+/g); + if (values.length == 0) return; + for (var v in values) { + if (ab.tags.indexOf(v) < 0) { + ab.tags.push(v); + } + } + updateAb(); + me.update(); + }, 300); + } + + event click $(#remove-tag) (_, me) { + var tag = current_menu_tag; + var i = ab.tags.indexOf(tag); + ab.tags.splice(i, 1); + for (var p in ab.peers) { + if (p.tags) { + i = p.tags.indexOf(tag); + if (i >= 0) p.tags.splice(i, 1); + } + } + updateAb(); + this.update(); + } + + event click $(#unselect-tags) (_, me) { + this.selectedTags = []; + handler.set_local_option("selected-tags", ""); + this.update(); + } + + event click $(#tags span) (_, me) { + me.attributes.toggleClass('active'); + if (me.attributes.hasClass('active')) { + this.selectedTags.push(me.text); + } else { + this.selectedTags.splice(this.selectedTags.indexOf(me.text), 1); + } + handler.set_local_option("selected-tags", this.selectedTags.join(',')); + this.update(); + } + + function getPeers() { + var tags = []; + for (var t in this.selectedTags) { + if (ab.tags.indexOf(t) >= 0) tags.push(t); + } + if (tags.length != this.selectedTags.length) { + this.selectedTags = tags; + handler.set_local_option("selected-tags", tags.join(",")); + stdout.println("updated selected tags"); + } + if (tags.length == 0) return ab.peers; + var peers = []; + if (tags.length > 0) { + for (var p in ab.peers) { + for (var t in (p.tags || [])) { + if (tags.indexOf(t) >= 0) { + peers.push(p); + break; + } + } + } + } else { + peers = ab.peers; + } + return peers; + } +} + +class SelectTags: Reactor.Component { + function this(params) { + selectTags = this; + this.tags = params.tags; + } + + function render() { + var me = this; + return
    + {ab.tags.map(function(t) { + return = 0 ? "active" : "inactive"}>{t}; + })} +
    ; + } + + event click $(#tags span) (_, me) { + me.attributes.toggleClass('active'); + var i = this.tags.indexOf(me.text); + if (i < 0) { + this.tags.push(me.text); + } else { + this.tags.splice(i, 1); + } + } +} + +var svg_tile = ; +var svg_list = ; +var search_icon = ; +var clear_icon = ; + +function getSessionsStyleOption(type) { + return (type || "recent") + "-sessions-style"; +} + +function getSessionsStyle(type) { + var v = handler.get_local_option(getSessionsStyleOption(type)); + if (!v) v = type == "ab" ? "list" : "tile"; + return v; +} + +var searchPatterns = {}; + +class SearchBar: Reactor.Component { + this var type = ""; + + function this(params) { + this.type = (params || {}).type || ""; + } + + function render() { + var value = searchPatterns[this.type] || ""; + var me = this; + self.timer(1ms, function() { (me.search_id || {}).value = value; }); + return
    + {search_icon} + + {value && {clear_icon}} +
    ; + } + + event click $(span.clear-input) { + this.onChange(''); + } + + event change $(input) (_, el) { + this.onChange(el.value.trim().toLowerCase()); + } + + function onChange(v) { + searchPatterns[this.type] = v; + app.multipleSessions.update(); + } +} + +class SessionStyle: Reactor.Component { + this var type = ""; + + function this(params) { + this.type = (params || {}).type || ""; + } + + function render() { + var sessionsStyle = getSessionsStyle(this.type); + return
    + {svg_tile} + {svg_list} +
    ; + } + + event click $(span.inactive) { + var option = getSessionsStyleOption(this.type); + var sessionsStyle = getSessionsStyle(this.type); + handler.set_local_option(option, sessionsStyle == "tile" ? "list" : "tile"); + if (is_linux) { + app.multipleSessions.stupidUpdate(); + } else { + app.multipleSessions.update(); + } + } +} + +class SessionList: Reactor.Component { + this var sessions = []; + this var type = ""; + this var style; + + function this(params) { + this.sessions = params.sessions; + this.type = params.type || ""; + this.style = getSessionsStyle(this.type); + } + + function getSessions() { + var p = searchPatterns[this.type]; + if (!p) return this.sessions; + var tmp = []; + this.sessions.map(function(s) { + var name = (s[4] || s.alias || "").toLowerCase(); + var id = (s[0] || s.id || "").toLowerCase(); + var user = (s[1] || "").toLowerCase(); + var hostname = (s[2] || "").toLowerCase(); + if (name.indexOf(p) >= 0 || id.indexOf(p) >= 0 || user.indexOf(p) >= 0 || hostname.indexOf(p) >= 0) { + tmp.push(s); + } + }); + return tmp; + } + + function render() { + var sessions = this.getSessions(); + if (sessions.length == 0) { + return
    {translate("Empty")}
    ; + } + var me = this; + sessions = sessions.map(function(x) { return me.getSession(x); }); + return
    + + +
  • {translate('Connect')}
  • +
  • {translate('Transfer file')}
  • +
  • {translate('TCP tunneling')}
  • +
  • {svg_checkmark}{translate('Always connect via relay')}
  • +
  • RDP
  • +
  • {translate('WOL')}
  • +
    + {this.type != "lan" &&
  • {translate('Rename')}
  • } + {this.type != "fav" &&
  • {translate('Remove')}
  • } + {is_win &&
  • {translate('Create desktop shortcut')}
  • } +
  • {translate('Forget Password')}
  • + {(!this.type || this.type == "fav") &&
  • {translate('Add to Favorites')}
  • } + {(!this.type || this.type == "fav") &&
  • {translate('Remove from Favorites')}
  • } + {this.type == "ab" &&
  • {translate('Edit Tag')}
  • } + + + {sessions} +
    ; + } + + function getSession(s) { + var id = s[0] || s.id || ""; + var username = s[1] || s.username || ""; + var hostname = s[2] || s.hostname || ""; + var platform = s[3] || s.platform || ""; + var alias = s[4] || s.alias || ""; + if (this.style == "list") { + return
    +
    + {platform && platformSvg(platform, "white")} +
    +
    +
    +
    {alias ? alias : formatId(id)}
    +
    {username}@{hostname}
    +
    +
    +
    + {svg_menu} +
    +
    ; + } + return
    +
    + {platform && platformSvg(platform, "white")} +
    {username}@{hostname}
    +
    +
    +
    {alias ? alias : formatId(id)}
    + {svg_menu} +
    +
    ; + } + + event dblclick $(div.remote-session-link) (evt, me) { + createNewConnect(me.id, "connect"); + } + + event click $(#menu) (_, me) { + var id = me.parent.parent.id; + var platform = me.parent.parent.attributes["platform"]; + this.$(#rdp).style.set{ + display: (platform == "Windows" && is_win) ? "block" : "none", + }; + this.$(#forget-password).style.set{ + display: handler.peer_has_password(id) ? "block" : "none", + }; + if (!this.type || this.type == "fav") { + var in_fav = handler.get_fav().indexOf(id) >= 0; + this.$(#add-fav).style.set{ + display: in_fav ? "none" : "block", + }; + this.$(#remove-fav).style.set{ + display: in_fav ? "block" : "none", + }; + } + // https://sciter.com/forums/topic/replacecustomize-context-menu/ + var menu = this.$(menu#remote-context); + current_menu_peer_id = id; + var el = this.$(li#force-always-relay); + if (el) { + var force = handler.get_peer_option(id, "force-always-relay"); + el.attributes.toggleClass("selected", force == "Y"); + } + var conn = this.$(menu #connect); + if (conn) { + var alias = me.parent.parent.$(#alias); + if (alias) { + alias = alias.text.replace(' ', ''); + if (alias != id) { + conn.text = translate('Connect') + ' ' + id; + } else { + conn.text = translate('Connect'); + } + } + } + me.popup(menu); + } + + event click $(menu#remote-context li) (evt, me) { + var action = me.id; + var id = current_menu_peer_id; + if (action == "connect") { + createNewConnect(id, "connect"); + } else if (action == "transfer") { + createNewConnect(id, "file-transfer"); + } else if (action == "wol") { + handler.send_wol(id); + } else if (action == "remove") { + if (this.type == "ab") { + for (var i = 0; i < ab.peers.length; ++i) { + if (ab.peers[i].id == id) { + ab.peers.splice(i, 1); + app.update(); + updateAb(); + break; + } + } + } else if (this.type == "lan") { + handler.remove_discovered(id); + app.update(); + } else { + handler.remove_peer(id); + app.update(); + } + } else if (action == "forget-password") { + handler.forget_password(id); + } else if (action == "shortcut") { + handler.create_shortcut(id); + } else if (action == "rdp") { + if (is_edit_rdp_port) { + is_edit_rdp_port = false; + return; + } + createNewConnect(id, "rdp"); + } else if (action == "add-fav") { + var favs = handler.get_fav(); + if (favs.indexOf(id) < 0) { + favs = [id].concat(favs); + handler.store_fav(favs); + } + app.multipleSessions.update(); + app.update(); + } else if (action == "remove-fav") { + var favs = handler.get_fav(); + var i = favs.indexOf(id); + favs.splice(i, 1); + handler.store_fav(favs); + app.multipleSessions.update(); + } else if (action == "tunnel") { + createNewConnect(id, "port-forward"); + } else if (action == "rename") { + var old_name = handler.get_peer_option(id, "alias"); + var abPeer; + if (this.type == "ab") { + for (var v in ab.peers) { + if (v.id == id) { + abPeer = v; + old_name = v.alias || ""; + } + } + } + msgbox("custom-rename", "Rename", "
    \ +
    \ +
    \ + ", "", function(res=null) { + if (!res) return; + var name = (res.name || "").trim(); + if (name != old_name) { + if (abPeer) { + abPeer.alias = name; + updateAb(); + } + handler.set_peer_option(id, "alias", name); + } + app.update(); + }); + } else if (action == "force-always-relay") { + var force = handler.get_peer_option(id, "force-always-relay"); + handler.set_peer_option(id, "force-always-relay", force == "Y" ? "" : "Y"); + } else if (action == "edit-tag") { + var peer; + for (var v in ab.peers) { + if (v.id == id) { + peer = v; + } + } + if (!peer) return; + msgbox("custom-edit-tag", "Edit Tag", , "", function(res=null) { + if (!res) return; + peer.tags = selectTags.tags; + updateAb(); + }, 260, 500, 0, "size: *; margin: 2em 0;"); + } + } +} + +function getSessionsType() { + return handler.get_local_option("show-sessions-type"); +} + +class Favorites: Reactor.Component { + function render() { + var sessions = handler.get_fav().map(function(f) { + return handler.get_peer(f); + }); + return ; + } +} + +class MultipleSessions: Reactor.Component { + function render() { + var type = getSessionsType(); + return
    +
    +
    + {translate('Recent sessions')} + {translate('Favorites')} + {handler.is_installed() && {translate('Discovered')}} + {!disable_account && !disable_ab && {translate('Address book')}} +
    + {!this.hidden && !(disable_account && type == "ab") && } + {!this.hidden && !(disable_account && type == "ab") && } +
    + {!this.hidden && + ((type == "fav" && ) || + (type == "lan" && handler.is_installed() && ) || + (type == "ab" && !disable_account && !disable_ab && ) || + )} +
    ; + } + + function stupidUpdate() { + /* hidden is workaround of stupid sciter bug */ + this.hidden = true; + this.update(); + var me = this; + self.timer(60ms, function() { + me.hidden = false; + me.update(); + self.timer(30ms, function() { me.onSize(); }); + }); + } + + event click $(div#sessions-type span.inactive) (_, el) { + if (el.id == "lan") { + discover(); + } + handler.set_local_option('show-sessions-type', el.id || ""); + this.stupidUpdate(); + } + + function onSize() { + var w = this.$(.sessions-bar .sessions-tab).box(#width); + var len = translate('Recent sessions').length; + var totalChars = 0; + var nEle = 0; + for (var el in this.$$(#sessions-type span)) { + nEle += 1; + totalChars += el.text.length; + } + for (var el in this.$$(#sessions-type span)) { + var maxWidth = (w - nEle * 2 * 8) * el.text.length / totalChars; + if (maxWidth < 0) maxWidth = 36; + el.style.set{ + "max-width": maxWidth + "px", + }; + } + } +} + +function discover() { + handler.discover(); + var tries = 15; + function update() { + self.timer(300ms, function() { + tries -= 1; + if (tries == 0) return; + update(); + var p = (app || {}).multipleSessions; + if (p) { + p.update(); + } + }); + } + update(); +} + +if (getSessionsType() == "lan" && handler.is_installed()) { + discover(); +} + +class LanPeers: Reactor.Component { + function render() { + var sessions = []; + try { + sessions = JSON.parse(handler.get_lan_peers()); + } catch (_) {} + return ; + } +} + +view.on("size", function() { if (app && app.multipleSessions) app.multipleSessions.onSize(); }); + +/* +{ + peers: [{id: "abcd", username: "", hostname: "", platform: "", alias: "", tags: ["", "", ...]}, ...], + tags: [], +} +*/ + +function handleAbError(err) { + abLoading = false; + err = translate(err); + stderr.println(err); + abError = err; + app.update(); +} + +function getAb() { + abLoading = true; + abError = ""; + app.update(); + httpRequest(handler.get_api_server() + "/api/ab/get", #post, {}, function(data) { + if (data) { + if (data.error) { + handleAbError(data.error); + return; + } + var tm = data.updated_at; + ab = JSON.parse(data.data); + if (!ab.tags) ab.tags = []; + if (!ab.peers) ab.peers = []; + } + abLoading = false; + app.update(); + }, function(err, status) { + handleAbError(err); + }, getHttpHeaders()); +} + +function updateAb() { + httpRequest(handler.get_api_server() + "/api/ab", #post, { data: JSON.stringify(ab) }, function(data) { + }, function(err, status) { + }, getHttpHeaders()); +} + +function resetAb() { + ab = { tags: [], peers: [] }; + app.update(); +} + +function updateAbPeer() { + if (ab.peers.length == 0) return; + // to-do: inefficient + var sessions = handler.get_recent_sessions(); + if (sessions.length == 0) return; + var s = sessions[0]; + var id = s[0] || ""; + var p; + for (var tmp in ab.peers) { + if (tmp.id == id) p = tmp; + } + if (!p) return; + var username = s[1] || ""; + var hostname = s[2] || ""; + var platform = s[3] || ""; + var alias = s[4] || ""; + var updated; + if (username != (p.username || "")) { + p.username = username; + updated = true; + } + if (hostname != (p.hostname || "")) { + p.hostname = hostname; + updated = true; + } + if (platform != (p.platform || "")) { + p.platform = platform; + updated = true; + } + if (alias != (p.alias || "")) { + if (alias) { + p.alias = alias; + } else if (p.alias) { + handler.set_peer_option(id, "alias", p.alias); + } + updated = true; + } + if (updated) { + updateAb(); + stdout.println("Ab peer updated"); + } +} + +var is_edit_rdp_port; +class EditRdpPort: Reactor.Component { + function render() { + return {svg_edit}; + } + + function onMouse(evt) { + if (evt.type == Event.MOUSE_DOWN) { + is_edit_rdp_port = true; + editRdpPort(); + } + } +} + +function editRdpPort() { + var id = current_menu_peer_id; + var p0 = handler.get_peer_option(id, "rdp_port"); + var port = p0 ? : + ; + var name0 = handler.get_peer_option(id, "rdp_username"); + var pass0 = handler.get_peer_option(id, "rdp_password"); + msgbox("custom-rdp-port", 'RDP ' + translate('Settings'),
    +
    {translate('Port')}:{port}
    +
    {translate('Username')}:
    +
    {translate('Password')}:
    +
    , "", function(res=null) { + if (!res) return; + var p = (res.port || '').trim(); + if (p != p0) { + if (!p) p = '0'; + p = p.toNumber(); + if (p < 0 || p != p.toInteger()) { + return translate("Invalid port"); + } + if (p == 0) p = ""; + else p = p.toInteger() + ''; + handler.set_peer_option(id, "rdp_port", p); + } + + var name = (res.username || '').trim(); + if (name != name0) { + handler.set_peer_option(id, "rdp_username", name); + } + + var pass = (res.password || '').trim(); + if (pass != pass0) { + handler.set_peer_option(id, "rdp_password", pass); + } + }, 240); +} + diff --git a/vendor/rustdesk/src/ui/chatbox.html b/vendor/rustdesk/src/ui/chatbox.html new file mode 100644 index 0000000..87d6162 --- /dev/null +++ b/vendor/rustdesk/src/ui/chatbox.html @@ -0,0 +1,35 @@ + + + + + + + diff --git a/vendor/rustdesk/src/ui/cm.css b/vendor/rustdesk/src/ui/cm.css new file mode 100644 index 0000000..3ac6c7b --- /dev/null +++ b/vendor/rustdesk/src/ui/cm.css @@ -0,0 +1,290 @@ +body { + behavior: connection-manager; +} + +div.content { + flow: horizontal; + size: *; +} + +div.left-panel { + size: *; + padding: 1em; + border-spacing: 1em; + overflow-x: scroll-indicator; + position: relative; +} + +div.chaticon svg { + size: 24px; + margin: 4px; + opacity: 0.66; +} + +div.chaticon { + position: absolute; + right: 0; + top: 0; + size: 32px; + background-color: color(gray-bg); +} + +div.chaticon:hover svg { + opacity: 1; +} + +div.chaticon:active { + background: white; +} + +div.right-panel { + background: white; + border-left: color(border) 1px solid; + size: *; +} + +div.icon-and-id { + flow: horizontal; + border-spacing: 1em; +} + +div.icon { + size: 96px; + text-align: center; + font-size: 76px; + line-height: 96px; + color: white; + font-weight: bold; +} + +img.icon { + size: 96px; + border-radius: 8px; +} + +div.id { + @ELLIPSIS; + color: color(green-blue); +} + +div.permissions { + flow: horizontal; + border-spacing: 0.5em; +} + +div.permissions > div { + size: 42px; + background: color(accent); +} + +div.permissions icon { + margin: *; + size: 32px; + background-size: cover; + background-repeat: no-repeat; + display: block; +} + +div.permissions > div.disabled { + background: #ddd; +} + +div.permissions > div:active { + opacity: 0.5; +} + +div.permissions.locked, +div.permissions.locked *, +div.permissions.locked > div:active { + cursor: default !important; + opacity: 1; +} + +icon.keyboard { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII='); +} + +icon.clipboard { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII='); +} + +icon.audio { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg=='); +} + +icon.file { + background:url('data: image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg=='); +} + +icon.restart { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'); +} + +icon.recording { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANpJREFUWEftltENAiEMhtsJ1NcynG6gI+gGugEOR591gppeQoIYSDBILxEeydH/57u2FMF4obE+TAOTwLoIhBDOAHBExG2n6rgR0akW640AM0sn4SWMiDycc7s8JjN7Ijro/k8NqAAR5RoeAPZxv2ggP9hCJiWZxtGbq3hqbJiBVHy4gVx8qAER8Yi4JFy6huVAKXemgb8icI+1b5KEitq0DOO/Nm1EEX1TK27p/bVvv36MOhl4EtHHbFF7jq8AoG1z08OAiFycczrkFNe6RrIet26NMQlMAuYEXiayryF/QQktAAAAAElFTkSuQmCC'); +} + +icon.block_input { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAjdJREFUWEe1V8tNAzEQfXOHAx2QG0UgQSqBFIIgHdABoQqOhBq4cCMlcMh90FvZq/HEXtvJxlKUZNceP783no+gY6jqNYBHAHcA+JufXTDBb37eRWTbalZqE82mz7W55v0ABMBGRCLA7PJJAKr6AiC3sT11NHyf2SEyQjvtAMKp3wBYo9VTGbYegjxxU65d5tg4YEBVbwF8ALgw2lLX4in80QqyZUEkAMLCb7P5n4hcdWifTA32Pg0bByA8AE4+oL3n9A1s7ERkEeeNAJzD/QC4OVaCAgjrU7wdK86zAHREJSKqyvvORRxVb67JFOT4NfYGpxwAqCo34oYcKxHZhOdzg7D2BhYigHj6RJ+5QbjrPezlqR61sZTOKYfztSUBWPoXpdA5FwjnC2sCGK+eiNRC8yw+oap0RiayLQHEPwf65zx7DibMoXcEEB0wq/85QJQAbEVkWbvP8f0pTFi/65ZgjtuRyJ7QYWL0OZnwTmiLDobH5nLqGDlUlcmON49jQwnsg/Wxma/VJ1zcGQIR7+OYJGyqbJWhhwlDPxh3JpNRL4Ba7nAsJckoYaFUv7UCyslBvQ3TNDWEfVsPJGH2FCkKTPAxD8ox+poFwJfZqqX15H6eYyK+TgJeriidLCJ7wAQHZ4Udy7u9iFxaG7mynEx4EF1leZDANzV7AE8i8joJICz2cvBxbExIYTZYTTQmxTxTzP+VnvC8rZlLOLEj7m5OW6JqtTs2US6247Hvy7XnX0OV05FP/gHde5fLZaGS8AAAAABJRU5ErkJggg=='); +} + +icon.privacy_mode { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAB7UlEQVR4AdyTrVYDMRCFuyjqiiuuOJA46sCVR6jDgQTXN+CgQIJCgkOCA0cduOLAgaOOuuW7czYhyWY5FcXQc28n85O5m9nsUuuPf/9IoCzLLnxd9MTCET3SvNckQnwL7lfcpnYueIGiKNbY8QYjERo+wZK4HuAcK94rVvGSWCO8gCqKjAixTXLPsAl7ldBxriASqAo6lfUnqUTaWAP5FajTYjxGCNXeYSRAwSflToBlKxSZKSCiMoUa6Uh+QNW/B37LC9D8lkTYHNegTf7JqNP8b5RB5AT7AkPoNqqXxUyATT28AUzhRuFFaLpDUYc9V1ihr7+EA/JdxUyAxQTWQDM3CuVSEWugGiUztJ5OIJPPhlKRbFEVXJZ1Anph8iNyTCsieA0dvIgCQY3ckBtyTIBjfuDcwRR2TPJDElkRcrpd6XcyJm7X2ATY3CKwi1UxxkNPeyiP/BAa8LVZObtdBMOPcYbvX7wXYJNE2lidBuNxyhgm0I1LCdcgFXmguXqoxhgJKELBKvYMhljH+ULEwDr8mEIRXWHSP6gJKIXIESxYh3PHzWJK1IuwjpAVcBWIhHPX0x2QE/vkHGofIzUevwr4KhZ003wvsOKYkAcxXfPoxbvk3AJuQ5MNRNwFsNKFCaibRGB0CxcqIJGU3wAAAP//8GtoDAAAAAZJREFUAwCJJuAxFVNbWwAAAABJRU5ErkJggg=='); +} + +div.outer_buttons { + flow:vertical; + border-spacing:8; +} + +div.inner_buttons { + flow:horizontal; + border-spacing:8; +} + +button.control { + width: *; +} + +button.elevate { + background:green; +} + +button.elevate:active { + background: rgb(2, 104, 2); + border-color: color(hover-border); +} + +button.elevate>span { + flow:horizontal; + width: *; +} + +button.elevate>span>span { + margin-left:*; + margin-right:*; +} + +button.elevate>span>span>span { + vertical-align: middle; +} + +button#disconnect { + background: color(blood-red); + border: none; +} + +button#disconnect:active { + opacity: 0.5; +} + +@media platform != "OSX" { +header .window-toolbar { + left: 40px; + top: 8px; +} +} + +@media platform == "OSX" { +header .tabs-wrapper { + margin-left: 80px; + margin-top: 8px; +} +} + +div.tabs-wrapper { + size: *; + position: relative; + overflow: hidden; +} + +div.tabs { + size: *; + flow: horizontal; + white-space: nowrap; + overflow: hidden; +} + +header { + height: 32px; + border-bottom: none; +} + +div.border-bottom { + position: absolute; + bottom: 0; + left: 0; + width: *; + height: 1px; + background: color(border) 1px solid; +} + +header div.window-icon { + size: 32px; +} + +div.tabs > div { + display: inline-block; + height: 24px; + line-height: 24px; +} + +div.tab { + width: 70px; + @ELLIPSIS; + text-align: center; + position: relative; + padding: 0 5px; + color: black +} + +div.active-tab { + background: color(gray-bg); + border: color(border) 1px solid; + border-bottom: none; + font-weight: bold; +} + +span.unreaded { + position: absolute; + font-size: 11px; + size: 15px; + border-radius: 15px; + line-height: 15px; + background: color(blood-red); + display: inline-block; + color: white; +} + +div.left-panel { + background: color(gray-bg); +} + +button.window#minimize { + right: 0px!important; +} + +div.tab-arrows { + position: absolute; + right: 2px; + font-weight: bold; + background: white; +} + +div.tab-arrows span { + display: inline-block; + height: *; + margin: 0; + padding: 6px 2px; + line-height: 20px; + opacity: 0.66; +} + +div.tab-arrows span:hover { + opacity: 1; +} + +div.tab-arrows span:active { + opacity: 1; + background-color: #ddd; +} diff --git a/vendor/rustdesk/src/ui/cm.html b/vendor/rustdesk/src/ui/cm.html new file mode 100644 index 0000000..8664190 --- /dev/null +++ b/vendor/rustdesk/src/ui/cm.html @@ -0,0 +1,21 @@ + + + + + +
    +
    + + + + + + + + diff --git a/vendor/rustdesk/src/ui/cm.rs b/vendor/rustdesk/src/ui/cm.rs new file mode 100644 index 0000000..4a68a57 --- /dev/null +++ b/vendor/rustdesk/src/ui/cm.rs @@ -0,0 +1,198 @@ +#[cfg(target_os = "linux")] +use crate::ipc::start_pa; +use crate::ui_cm_interface::{start_ipc, ConnectionManager, InvokeUiCM}; + +use hbb_common::{allow_err, log}; +use sciter::{make_args, Element, Value, HELEMENT}; +use std::sync::Mutex; +use std::{ops::Deref, sync::Arc}; + +lazy_static::lazy_static! { + pub static ref HIDE_CM: Arc> = Arc::new(Mutex::new(false)); +} + +#[derive(Clone, Default)] +pub struct SciterHandler { + pub element: Arc>>, +} + +impl InvokeUiCM for SciterHandler { + fn add_connection(&self, client: &crate::ui_cm_interface::Client) { + self.call( + "addConnection", + &make_args!( + client.id, + client.is_file_transfer, + client.is_view_camera, + client.is_terminal, + client.port_forward.clone(), + client.peer_id.clone(), + client.name.clone(), + client.avatar.clone(), + client.authorized, + client.keyboard, + client.clipboard, + client.audio, + client.file, + client.restart, + client.recording, + client.block_input, + client.privacy_mode + ), + ); + } + + fn remove_connection(&self, id: i32, close: bool) { + self.call("removeConnection", &make_args!(id, close)); + if crate::ui_cm_interface::get_clients_length().eq(&0) { + crate::platform::quit_gui(); + } + } + + fn new_message(&self, id: i32, text: String) { + self.call("newMessage", &make_args!(id, text)); + } + + fn change_theme(&self, dark: String) { + self.call("changeTheme", &make_args!(dark)); + } + + fn change_language(&self) { + self.call("changeLanguage", &make_args!()); + } + + fn show_elevation(&self, show: bool) { + self.call("showElevation", &make_args!(show)); + } + + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + self.call( + "updateVoiceCallState", + &make_args!(client.id, client.in_voice_call, client.incoming_voice_call), + ); + } + + fn file_transfer_log(&self, _action: &str, _log: &str) {} +} + +impl SciterHandler { + #[inline] + fn call(&self, func: &str, args: &[Value]) { + if let Some(e) = self.element.lock().unwrap().as_ref() { + allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); + } + } +} + +pub struct SciterConnectionManager(ConnectionManager); + +impl Deref for SciterConnectionManager { + type Target = ConnectionManager; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl SciterConnectionManager { + pub fn new() -> Self { + #[cfg(target_os = "linux")] + std::thread::spawn(start_pa); + let cm = ConnectionManager { + ui_handler: SciterHandler::default(), + }; + let cloned = cm.clone(); + std::thread::spawn(move || start_ipc(cloned)); + SciterConnectionManager(cm) + } + + fn get_icon(&mut self) -> String { + super::get_icon() + } + + fn check_click_time(&mut self, id: i32) { + crate::ui_cm_interface::check_click_time(id); + } + + fn get_click_time(&self) -> f64 { + crate::ui_cm_interface::get_click_time() as _ + } + + fn switch_permission(&self, id: i32, name: String, enabled: bool) { + crate::ui_cm_interface::switch_permission(id, name, enabled); + } + + fn close(&self, id: i32) { + crate::ui_cm_interface::close(id); + } + + fn remove_disconnected_connection(&self, id: i32) { + crate::ui_cm_interface::remove(id); + } + + fn quit(&self) { + crate::platform::quit_gui(); + } + + fn authorize(&self, id: i32) { + crate::ui_cm_interface::authorize(id); + } + + fn send_msg(&self, id: i32, text: String) { + crate::ui_cm_interface::send_chat(id, text); + } + + fn t(&self, name: String) -> String { + crate::client::translate(name) + } + + fn can_elevate(&self) -> bool { + crate::ui_cm_interface::can_elevate() + } + + fn elevate_portable(&self, id: i32) { + crate::ui_cm_interface::elevate_portable(id); + } + + fn get_option(&self, key: String) -> String { + crate::ui_interface::get_option(key) + } + + fn get_builtin_option(&self, key: String) -> String { + crate::ui_interface::get_builtin_option(&key) + } + + fn hide_cm(&self) -> bool { + *crate::ui::cm::HIDE_CM.lock().unwrap() + } + + fn get_supported_privacy_mode_impls(&self) -> String { + serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl()) + .unwrap_or_default() + } +} + +impl sciter::EventHandler for SciterConnectionManager { + fn attached(&mut self, root: HELEMENT) { + *self.ui_handler.element.lock().unwrap() = Some(Element::from(root)); + } + + sciter::dispatch_script_call! { + fn t(String); + fn check_click_time(i32); + fn get_click_time(); + fn get_icon(); + fn close(i32); + fn remove_disconnected_connection(i32); + fn quit(); + fn authorize(i32); + fn switch_permission(i32, String, bool); + fn send_msg(i32, String); + fn can_elevate(); + fn elevate_portable(i32); + fn get_option(String); + fn get_builtin_option(String); + fn hide_cm(); + fn get_supported_privacy_mode_impls(); + } +} diff --git a/vendor/rustdesk/src/ui/cm.tis b/vendor/rustdesk/src/ui/cm.tis new file mode 100644 index 0000000..f306e90 --- /dev/null +++ b/vendor/rustdesk/src/ui/cm.tis @@ -0,0 +1,598 @@ +view.windowFrame = is_osx ? #extended : #solid; + +var body; +var connections = []; +var show_chat = false; +var show_elevation = true; +var is_privacy_mode_supported = handler.get_supported_privacy_mode_impls() != '[]'; +var allow_perm_change_in_accept_window = + handler.get_builtin_option('enable-perm-change-in-accept-window') != 'N'; +var svg_elevate = ; + +var hide_cm = undefined; +function setWindowState(state) { + if (hide_cm == undefined) hide_cm = handler.hide_cm(); + if (hide_cm) return; + view.windowState = state; +} + +class Body: Reactor.Component +{ + this var cur = 0; + + function this() { + body = this; + } + + function render() { + if (connections.length == 0) + return
    + Waiting for new connection ... +
    ; + var c = connections[this.cur]; + this.connection = c; + this.cid = c.id; + var auth = c.authorized; + var me = this; + var callback = function(msg) { + me.sendMsg(msg); + }; + var right_style = show_chat ? "" : "display: none"; + var permissions_locked = !allow_perm_change_in_accept_window; + var disconnected = c.disconnected; + var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && !c.is_view_camera && !c.is_terminal && c.port_forward.length == 0; + var show_accept_btn = handler.get_option('approve-mode') != 'password'; + // below size:* is a workaround for Linux, it already set in css, but not work, shit sciter + return
    +
    +
    + {c.avatar ? + : +
    + {c.name[0].toUpperCase()} +
    } +
    +
    {c.name}
    +
    ({c.peer_id})
    +
    {auth + ? {disconnected ? translate('Disconnected') : translate('Connected')}{" "}{getElapsed(c.time, c.now)} + : {translate('Request access to your device')}{"..."}} +
    +
    +
    +
    + {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
    {translate('Permissions')}
    } + {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + } + {c.is_file_transfer ?
    {translate('Transfer file')}
    : ""} + {c.is_view_camera ?
    {translate('View camera')}
    : ""} + {c.is_terminal ?
    {translate('Terminal')}
    : ""} + {c.port_forward ?
    Port Forwarding: {c.port_forward}
    : ""} +
    +
    + {!auth && !disconnected && show_elevation_btn && show_accept_btn ? : "" } + {auth && !disconnected && show_elevation_btn ? : "" } +
    + {!auth && show_accept_btn ? : "" } + {!auth ? : "" } +
    + {auth && !disconnected ? : "" } + {auth && disconnected ? : "" } +
    + {c.is_file_transfer || c.is_terminal || c.port_forward ? "" :
    {svg_chat}
    } +
    +
    + {c.is_file_transfer || c.is_terminal || c.port_forward ? "" : } +
    +
    ; + } + + function sendMsg(text) { + if (!text) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.msgs.push({ name: "me", text: text, time: getNowStr()}); + handler.send_msg(cid, text); + body.update(); + }); + } + + event click $(icon.keyboard) (e) { + if (!allow_perm_change_in_accept_window) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.keyboard = !connection.keyboard; + body.update(); + handler.switch_permission(cid, "keyboard", connection.keyboard); + }); + } + + event click $(icon.clipboard) { + if (!allow_perm_change_in_accept_window) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.clipboard = !connection.clipboard; + body.update(); + handler.switch_permission(cid, "clipboard", connection.clipboard); + }); + } + + event click $(icon.audio) { + if (!allow_perm_change_in_accept_window) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.audio = !connection.audio; + body.update(); + handler.switch_permission(cid, "audio", connection.audio); + }); + } + + event click $(icon.file) { + if (!allow_perm_change_in_accept_window) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.file = !connection.file; + body.update(); + handler.switch_permission(cid, "file", connection.file); + }); + } + + event click $(icon.restart) { + if (!allow_perm_change_in_accept_window) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.restart = !connection.restart; + body.update(); + handler.switch_permission(cid, "restart", connection.restart); + }); + } + + event click $(icon.recording) { + if (!allow_perm_change_in_accept_window) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.recording = !connection.recording; + body.update(); + handler.switch_permission(cid, "recording", connection.recording); + }); + } + + event click $(icon.block_input) { + if (!allow_perm_change_in_accept_window) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.block_input = !connection.block_input; + body.update(); + handler.switch_permission(cid, "block_input", connection.block_input); + }); + } + + event click $(icon.privacy_mode) { + if (!allow_perm_change_in_accept_window) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.privacy_mode = !connection.privacy_mode; + body.update(); + handler.switch_permission(cid, "privacy_mode", connection.privacy_mode); + }); + } + + event click $(button#accept) { + var { cid, connection } = this; + checkClickTime(function() { + connection.authorized = true; + body.update(); + handler.authorize(cid); + self.timer(30ms, function() { + setWindowState(View.WINDOW_MINIMIZED); + }); + }); + } + + event click $(button#elevate_accept) { + var { cid, connection } = this; + checkClickTime(function() { + connection.authorized = true; + show_elevation = false; + body.update(); + handler.elevate_portable(cid); + handler.authorize(cid); + self.timer(30ms, function() { + setWindowState(View.WINDOW_MINIMIZED); + }); + }); + } + + event click $(button#elevate) { + var { cid, connection } = this; + checkClickTime(function() { + show_elevation = false; + body.update(); + handler.elevate_portable(cid); + self.timer(30ms, function() { + setWindowState(View.WINDOW_MINIMIZED); + }); + }); + } + + event click $(button#dismiss) { + var cid = this.cid; + checkClickTime(function() { + handler.close(cid); + }); + } + + event click $(button#disconnect) { + var cid = this.cid; + checkClickTime(function() { + handler.close(cid); + }); + } + + event click $(button#close) { + var cid = this.cid; + if (this.cur >= 0 && this.cur < connections.length){ + handler.remove_disconnected_connection(cid); + connections.splice(this.cur, 1); + if (connections.length > 0) { + if (this.cur > 0) + this.cur -= 1; + else + this.cur = connections.length - 1; + header.update(); + body.update(); + } else { + handler.quit(); + } + } + + } +} + +$(body).content(); + +var header; + +class Header: Reactor.Component +{ + function this() { + header = this; + } + + function render() { + var me = this; + var conn = connections[body.cur]; + if (conn && conn.unreaded > 0) {; + var el = me.select("#unreaded" + conn.id); + if (el) el.style.set { + display: "inline-block", + }; + self.timer(300ms, function() { + conn.unreaded = 0; + var el = me.select("#unreaded" + conn.id); + if (el) el.style.set { + display: "none", + }; + }); + } + var tabs = connections.map(function(c, i) { return me.renderTab(c, i) }); + return
    + {tabs} +
    +
    + < + > +
    +
    ; + } + + function renderTab(c, i) { + var cur = body.cur; + return
    + {c.name} + {c.unreaded > 0 ? {c.unreaded} : ""} +
    ; + } + + function update_cur(idx) { + checkClickTime(function() { + body.cur = idx; + update(); + self.timer(1ms, adjustHeader); + }); + } + + event click $(div.tab) (_, me) { + var idx = me.index; + if (idx == body.cur) return; + this.update_cur(idx); + } + + event click $(span#left-arrow) { + var cur = body.cur; + if (cur == 0) return; + this.update_cur(cur - 1); + } + + event click $(span#right-arrow) { + var cur = body.cur; + if (cur == connections.length - 1) return; + this.update_cur(cur + 1); + } +} + +if (is_osx) { + $(header).content(
    ); + $(header).attributes["role"] = "window-caption"; +} else { + $(div.window-toolbar).content(
    ); + setWindowButontsAndIcon(true); +} + +event click $(div.chaticon) { + checkClickTime(function() { + show_chat = !show_chat; + adaptSizeForChat(); + if (show_chat) { + view.focus = $(.outline-focus); + } + }); +} + +function checkClickTime(callback) { + var click_callback_time = getTime(); + handler.check_click_time(body.cid); + self.timer(120ms, function() { + var d = click_callback_time - handler.get_click_time(); + if (d > 120) + callback(); + }); +} + +function adaptSizeForChat() { + $(div.right-panel).style.set { + display: show_chat ? "block" : "none", + }; + var (x, y, w, h) = view.box(#rectw, #border, #screen); + if (show_chat && w < scaleIt(600)) { + view.move(x - (scaleIt(600) - w), y, scaleIt(600), h); + } else if (!show_chat && w > scaleIt(450)) { + view.move(x + (w - scaleIt(300)), y, scaleIt(300), h); + } +} + +function update() { + header.update(); + body.update(); +} + +function bring_to_top(idx=-1) { + if (view.windowState == View.WINDOW_HIDDEN || view.windowState == View.WINDOW_MINIMIZED) { + if (is_linux) { + view.focus = self; + } else { + setWindowState(View.WINDOW_SHOWN); + } + if (idx >= 0) body.cur = idx; + } else { + view.windowTopmost = true; + view.windowTopmost = false; + } +} + +handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, privacy_mode) { + stdout.println("new connection #" + id + ": " + peer_id); + var conn; + connections.map(function(c) { + if (c.id == id) conn = c; + }); + if (conn) { + conn.authorized = authorized; + conn.privacy_mode = privacy_mode; + update(); + return; + } + var idx = -1; + connections.map(function(c, i) { + if (c.disconnected && c.peer_id == peer_id) idx = i; + }); + if (!name) name = "NA"; + conn = { + id: id, is_file_transfer: is_file_transfer, is_view_camera: is_view_camera, is_terminal: is_terminal, peer_id: peer_id, + port_forward: port_forward, + avatar: avatar, + name: name, authorized: authorized, time: new Date(), now: new Date(), + keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, + audio: audio, file: file, restart: restart, recording: recording, + block_input:block_input, privacy_mode:privacy_mode, + disconnected: false + }; + if (idx < 0) { + connections.push(conn); + body.cur = connections.length - 1; + } else { + connections[idx] = conn; + body.cur = idx; + } + bring_to_top(); + update(); + self.timer(1ms, adjustHeader); + if (authorized) { + self.timer(3s, function() { + setWindowState(View.WINDOW_MINIMIZED); + }); + } +} + +handler.removeConnection = function(id, close) { + var i = -1; + connections.map(function(c, idx) { + if (c.id == id) i = idx; + }); + if (i < 0) return; + if (close) { + connections.splice(i, 1); + } else { + var conn = connections[i]; + conn.disconnected = true; + } + if (connections.length > 0) { + if (body.cur >= i && body.cur > 0 && close) body.cur -= 1; + update(); + } +} + +handler.newMessage = function(id, text) { + var idx = -1; + connections.map(function(c, i) { + if (c.id == id) idx = i; + }); + var conn = connections[idx]; + if (!conn) return; + conn.msgs.push({name: conn.name, text: text, time: getNowStr()}); + bring_to_top(idx); + if (idx == body.cur) { + var shouldAdapt = !show_chat; + show_chat = true; + if (shouldAdapt) adaptSizeForChat(); + } + conn.unreaded += 1; + update(); +} + +handler.showElevation = function(show) { + if (show != show_elevation) { + show_elevation = show; + update(); + } +} + +view << event statechange { + adjustBorder(); +} + +function self.ready() { + adjustBorder(); + var (sw, sh) = view.screenBox(#workarea, #dimension); + var w = scaleIt(300); + var h = scaleIt(400); + view.move(sw - w, 0, w, h); +} + +function getElapsed(time, now) { + var seconds = Date.diff(time, now, #seconds); + var hours = seconds / 3600; + var days = hours / 24; + hours = hours % 24; + var minutes = seconds % 3600 / 60; + seconds = seconds % 60; + var out = String.printf("%02d:%02d:%02d", hours, minutes, seconds); + if (days > 0) { + out = String.printf("%d day%s %s", days, days > 1 ? "s" : "", out); + } + return out; +} + +var ui_status_cache = ["", ""]; +function check_update_ui() { + self.timer(1s, function() { + var approve_mode = handler.get_option('approve-mode'); + var allow_perm_change = handler.get_builtin_option('enable-perm-change-in-accept-window'); + var changed = false; + if (ui_status_cache[0] != approve_mode) { + ui_status_cache[0] = approve_mode; + changed = true; + } + if (ui_status_cache[1] != allow_perm_change) { + ui_status_cache[1] = allow_perm_change; + allow_perm_change_in_accept_window = allow_perm_change != 'N'; + changed = true; + } + if (changed) update(); + check_update_ui(); + }); +} +check_update_ui(); + +function updateTime() { + self.timer(1s, function() { + var now = new Date(); + connections.map(function(c) { + if (!c.authorized) c.time = now; + if (!c.disconnected) c.now = now; + }); + var el = $(#time); + if (el) { + var c = connections[body.cur]; + if (c && c.authorized && !c.disconnected) { + el.text = getElapsed(c.time, c.now); + } + } + updateTime(); + }); +} + +updateTime(); + +var tm0 = getTime(); + +function self.closing() { + if (connections.length == 0 && getTime() - tm0 > 30000) return true; + setWindowState(View.WINDOW_HIDDEN); + return false; +} + + +function adjustHeader() { + var hw = $(header).box(#width) / scaleFactor; + var tabswrapper = $(div.tabs-wrapper); + var tabs = $(div.tabs); + var arrows = $(div.tab-arrows); + if (!arrows) return; + var n = connections.length; + var wtab = 80; + var max = hw - 98; + var need_width = n * wtab + scaleIt(2); // include border of active tab + if (need_width < max) { + arrows.style.set { + display: "none", + }; + tabs.style.set { + width: need_width, + margin-left: 0, + }; + tabswrapper.style.set { + width: need_width, + }; + } else { + var margin = (body.cur + 1) * wtab - max + 30; + if (margin < 0) margin = 0; + arrows.style.set { + display: "block", + }; + tabs.style.set { + width: (max - 20 + margin) + 'px', + margin-left: -margin + 'px' + }; + tabswrapper.style.set { + width: (max + 10) + 'px', + }; + } +} + +view.on("size", adjustHeader); + +// handler.addConnection(0, false, false, 0, "", "test1", true, false, false, true, true); +// handler.addConnection(1, false, false, 0, "", "test2--------", true, false, false, false, false); +// handler.addConnection(2, false, false, 0, "", "test3", true, false, false, false, false); +// handler.newMessage(0, 'h'); diff --git a/vendor/rustdesk/src/ui/common.css b/vendor/rustdesk/src/ui/common.css new file mode 100644 index 0000000..16dd6ca --- /dev/null +++ b/vendor/rustdesk/src/ui/common.css @@ -0,0 +1,492 @@ +html { + var(accent): #0071ff; + var(button): #2C8CFF; + var(gray-bg): #eee; + var(bg): white; + var(border): #ccc; + var(hover-border): #999; + var(text): #222; + var(placeholder): #aaa; + var(lighter-text): #888; + var(light-text): #666; + var(menu-hover): #D7E4F2; + var(dark-red): #A72145; + var(dark-yellow): #FBC732; + var(dark-blue): #2E2459; + var(green-blue): #197260; + var(gray-blue): #2B3439; + var(blue-green): #4299bf; + var(light-green): #D4EAB7; + var(dark-green): #5CB85C; + var(blood-red): #F82600; + var(gray-bg-osx): rgba(238, 238, 238, 0.75); +} + +html.darktheme { + var(bg): #252525; + var(gray-bg): #141414; + var(menu-hover): #2D3033; + var(border): #555; + + var(text): white; + var(light-text): #999; + var(lighter-text): #777; + var(placeholder): #555; + var(gray-bg-osx): rgba(37, 37, 37, 0.75); +} + +body { + margin: 0; + color: color(text); +} + +button.button { + height: 2em; + border-radius: 0.5em; + background: color(button); + color: color(bg); + border-color: color(button); + min-width: 40px; +} + +button[type=checkbox], button[type=checkbox]:active { + background: none; + border: none; + color: unset; + height: 1.4em; +} + +button.outline { + border: color(border) solid 1px; + background: transparent; + color: color(text); +} + +button.button:active, button.active { + background: color(accent); + color: color(bg); + border-color: color(accent); +} + +button.button:hover, button.outline:hover { + border-color: color(hover-border); +} + +button:disabled, +button:disabled:hover { + opacity: 0.3; +} + +button.link { + background: none !important; + border: none; + padding: 0 !important; + color: color(button); + text-decoration: underline; + cursor: pointer; +} + +input[type=text], input[type=password], input[type=number] { + width: *; + font-size: 1.5em; + border-color: color(border); + border-radius: 0; + color: color(text); + padding-left: 0.5em; + background: color(bg); +} + +input:empty { + color: color(placeholder); +} + +input.outline-focus:focus { + outline: color(button) solid 3px; +} + +textarea { + background: color(bg); + color: color(text); +} + +textarea:empty { + color: color(placeholder); +} + +@set my-scrollbar +{ + .prev { display:none; } + .next { display:none; } + .base, .next-page, .prev-page { background: white;} + .slider { background: #bbb; border: white solid 4px; } + .base:disabled { background: transparent; } + .slider:hover { background: grey; } + .slider:active { background: grey; } + .base { size: 16px; } + .corner { background: white; } +} + +@mixin ELLIPSIS { + text-overflow: ellipsis; + white-space: nowrap; + overflow-x: hidden; +} + +.ellipsis { + @ELLIPSIS; +} + +div.password svg:not(.checkmark) { + padding-left: 1em; + size: 16px; + color: #ddd; + background: none; + padding-top: 4px!important; +} + +div.password input { + font-family: Consolas, Menlo, Monaco, 'Courier New'; + font-size: 1.2em; +} + +div.username input { + font-size: 1.2em; +} + +svg { + background: none; +} + +header { + border-bottom: color(border) solid 1px; + height: 22px; + flow: horizontal; + overflow-x: hidden; + position: relative; +} + +@media platform == "OSX" { + header { + background: linear-gradient(top,#E4E4E4,#D1D1D1); + } +} + +header div.window-icon { + size: 22px; +} + +@media platform != "OSX" { +header { + background: white; + height: 30px; +} + +header div.window-icon { + size: 30px; +} +} + +header div.window-icon icon { + display: block; + margin: *; + size: 16px; + background-size: cover; + background-repeat: no-repeat; +} + +header caption { + size: *; +} + +@media platform != "OSX" { + button.window { + top: 0; + padding: 0 10px; + width: 22px; + position: absolute; + color: black; + border: none; + background: none; + border-radius: 0; + } + button.window div { + size: 10px; + margin: *; + background-size: cover; + background-repeat: no-repeat; + } + button.window:hover { + background: color(gray-bg); + } + button.window#minimize { + right: 84px; + } + button.window#maximize { + right: 42px; + } + button.window#close { + right: 0px; + } + button.window#minimize div { + height: 3px; + border-bottom: black solid 1px; + width: 12px; + } + button.window#maximize div { + border: black solid 1px; + } + button.window#close:hover { + background: #F82600; + } + button.window#close:hover div { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAD///////////////+PQt5oAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC'); + } + button.window#close div { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMBAMAAACkW0HUAAAAD1BMVEUAAAAAAAAAAAAAAAAAAABPDueNAAAABXRSTlMAO+hBqp3RzLsAAAAuSURBVAjXY3BkAAIRBiEDBgZGZRACMkEYxAJyQRwgV5EBSsEEoUqgGqDaoYYBALKmBEEnAGy8AAAAAElFTkSuQmCC'); + size: 12px; + } + button.window#maximize.restore div { + border: none; + size: 12px; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMAQMAAABsu86kAAAABlBMVEUAAAAAAAClZ7nPAAAAAXRSTlMAQObYZgAAAB1JREFUCNdjsP/AoCDA8P8CQ0MABipgaHBg+H8AAMfSC36WAZteAAAAAElFTkSuQmCC'); +} +} + +div.chatbox { + size: *; +} + +div.chatbox div.send svg { + size: 16px; +} + +div.chatbox div.send span:active { + opacity: 0.5; +} + +div.chatbox div.send span { + display: inline-block; + padding: 6px; +} + +div.chatbox .msgs { + border: none; + size: *; + border-bottom: color(border) 1px solid; + overflow-x: hidden; + overflow-y: scroll-indicator; + border-spacing: 1em; + padding: 1em; +} + +div.chatbox div.send { + flow: horizontal; + height: 30px; + padding: 5px; +} + +div.chatbox div.send input { + height: 20px !important; +} + +div.chatbox div.name { + color: color(dark-green); +} + +div.chatbox div.right-side div { + text-align: right; +} + +div.chatbox div.text { + margin-top: 0.5em; +} + +@media platform != "OSX" { +header .window-toolbar { + width: max-content; + background: transparent; + position: absolute; + bottom: 4px; + height: 24px; +} +} + +header svg, menu svg { + size: 14px; +} + +header span, menu span { + padding: 4px 8px; + margin: * 0.5em; + color: color(light-text); +} + +progress { + display: inline-block; + aspect: Progress; + border: none; + margin-right: 1em; + height: 0.25em; + background: transparent; +} + +menu { + background: color(bg); +} + +menu div.separator { + height: 1px; + width: *; + margin: 5px 0; + background: color(gray-bg); + border: none; +} + +menu li { + color: color(text); + position: relative; +} + +menu li span { + display: none; +} + +menu li.selected span:nth-child(1) { + display: inline-block; + position: absolute; + left: -10px; + top: 2px; +} + +.link { + cursor: pointer; + text-decoration: underline; +} + +.link:active { + opacity: 0.5; +} + +menu li:hover { + background: color(menu-hover); + color: color(text); +} + +menu li.line-through, menu li.line-through :hover { + text-decoration-line: line-through; + color: red; +} + +#tags { + size: *; + padding: 0.5em; + overflow-y: scroll-indicator; + border-spacing: 0.5em; + flow: horizontal-flow; +} + +#tags span { + background: color(gray-bg); + display: inline-block; + border-radius: 6px; + padding: 3px 0.5em; + word-wrap: normal; +} + +#tags span.active { + background: color(button); + border-color: color(button); + color: white; +} + +#tags span:hover { + border-color: color(hover-border); +} + +div#msgbox .msgbox-icon svg { + size: 80px; + background: white; + +} + +div#msgbox .form { + border-spacing: 0.5em; +} + +div#msgbox .caption { + @ELLIPSIS; + height: 2em; + line-height: 2em; + text-align: center; + color: color(bg); + font-weight: bold; +} + +div#msgbox .form .text { + @ELLIPSIS; +} + +div#msgbox button.button { + margin-left: 1.6em; +} + +div#msgbox div.password { + position: relative; +} + +div#msgbox div.password svg { + position: absolute; + right: 0.25em; + top: 0.25em; + padding: 0.5em; + color: color(text); +} + +div#msgbox div.set-password > div { + flow: horizontal; +} + +div#msgbox div.set-password > div > span { + width: 30%; + line-height: 2em; +} + +div#msgbox div.set-password div.password { + width: *; +} + +div#msgbox div.set-password div > input { + width: *; +} + +div#msgbox div.set-password input { + font-size: 1em; +} + +.wrap-text { + width: *; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; + height: auto; + overflow: hidden; +} + +div#msgbox #error { + color: red; +} + +div.user-session .title { + font-size: 1.2em; + margin-bottom: 2em; +} + +div.user-session select { + width: 98%; + height: 2em; + border-radius: 0.5em; + border: color(border) solid 1px; + background: color(bg); + color: color(text); + padding-left: 0.5em; +} diff --git a/vendor/rustdesk/src/ui/common.tis b/vendor/rustdesk/src/ui/common.tis new file mode 100644 index 0000000..2407990 --- /dev/null +++ b/vendor/rustdesk/src/ui/common.tis @@ -0,0 +1,482 @@ +include "sciter:reactor.tis"; + +var handler = $(#handler) || view; +try { view.windowIcon = self.url(handler.get_icon()); } catch(e) {} +var OS = view.mediaVar("platform"); +var is_osx = OS == "OSX"; +var is_win = OS == "Windows"; +var is_linux = OS == "Linux"; +var is_file_transfer; +var is_xfce = false; +try { is_xfce = handler.is_xfce(); } catch(e) {} + +function isEnterKey(evt) { + return (evt.keyCode == Event.VK_ENTER || + (is_osx && evt.keyCode == 0x4C) || + (is_linux && evt.keyCode == 65421)); +} + +function getScaleFactor() { + if (!is_win) return 1; + var s = self.toPixels(10000dip) / 10000.; + return s < 0.000001 ? 1 : s; +} +var scaleFactor = getScaleFactor(); +view << event resolutionchange { + scaleFactor = getScaleFactor(); +} +function scaleIt(x) { + return (x * scaleFactor).toInteger(); +} +stdout.println("scaleFactor", scaleFactor); + +function translate(name) { + try { + return handler.t(name); + } catch(_) { + return name; + } +} + +function hashCode(str) { + var hash = 160 << 16 + 114 << 8 + 91; + for (var i = 0; i < str.length; i += 1) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash % 16777216; +} + +function intToRGB(i, a = 1) { + return 'rgba(' + ((i >> 16) & 0xFF) + ', ' + ((i >> 8) & 0x7F) + + ',' + (i & 0xFF) + ',' + a + ')'; +} + +function string2RGB(s, a = 1) { + return intToRGB(hashCode(s), a); +} + +function getTime() { + var now = new Date(); + return now.valueOf(); +} + +function platformSvg(platform, color) { + platform = (platform || "").toLowerCase(); + if (platform == "linux") { + return + + + + + ; + } + if (platform == "mac os") { + return + + ; + } + if (platform == "android") { + return ; + } + return + + ; +} + +function centerize(w, h) { + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + if (w > sw) w = sw; + if (h > sh) h = sh; + var x = (sx + sw - w) / 2; + var y = (sy + sh - h) / 2; + view.move(x, y, w, h); +} + +function setWindowButontsAndIcon(only_min=false) { + if (only_min) { + $(div.window-buttons).content(
    +
    +
    ); + } else { + $(div.window-buttons).content(
    +
    +
    +
    +
    ); + } + $(div.window-icon>icon).style.set { + "background-image": "url('" + handler.get_icon() + "')", + }; +} + +function adjustBorder() { + if (is_osx) { + if (view.windowState == View.WINDOW_FULL_SCREEN) { + $(header).style.set { + display: "none", + }; + } else { + $(header).style.set { + display: "block", + padding: "0", + }; + } + return; + } + if (view.windowState == view.WINDOW_MAXIMIZED) { + self.style.set { + border: "window-frame-width solid transparent", + }; + } else if (view.windowState == view.WINDOW_FULL_SCREEN) { + self.style.set { + border: "none", + }; + } else { + self.style.set { + border: "black solid 1px", + }; + } + var el = $(button#maximize); + if (el) el.attributes.toggleClass("restore", view.windowState == View.WINDOW_MAXIMIZED); + el = $(span#fullscreen); + if (el) el.attributes.toggleClass("active", view.windowState == View.WINDOW_FULL_SCREEN); +} + +var svg_checkmark = ; +var svg_edit = + +; +var svg_eye = + + +; +var svg_send = + +; +var svg_chat = + +; +var svg_keyboard = ; + +function scrollToBottom(el) { + var y = el.box(#height, #content) - el.box(#height, #client); + el.scrollTo(0, y); +} + +function getNowStr() { + var now = new Date(); + return String.printf("%02d:%02d:%02d", now.hour, now.minute, now.second); +} + +/******************** start of chatbox ****************************************/ +class ChatBox: Reactor.Component { + this var msgs = []; + this var callback; + + function this(params) { + if (params) { + this.msgs = params.msgs || []; + this.callback = params.callback; + } + } + + function renderMsg(msg) { + var cls = msg.name == "me" ? "right-side msg" : "left-side msg"; + return
    + {msg.name == "me" ? +
    {msg.time + " "} me
    : +
    {msg.name} {" " + msg.time}
    + } +
    {msg.text}
    +
    ; + } + + function render() { + var me = this; + var msgs = this.msgs.map(function(msg) { return me.renderMsg(msg); }); + self.timer(1ms, function() { + scrollToBottom(me.msgs); + }); + return
    + + {msgs} + +
    + + {svg_send} +
    +
    ; + } + + function send() { + var el = this.$(input); + var value = (el.value || "").trim(); + el.value = ""; + if (!value) return; + if (this.callback) this.callback(value); + } + + event keydown $(input) (evt) { + if (!evt.shortcutKey) { + if (isEnterKey(evt)) { + this.send(); + } + } + } + + event click $(div.send span) { + this.send(); + view.focus = $(input); + } +} +/******************** end of chatbox ****************************************/ + +/******************** start of msgbox ****************************************/ +var remember_password = false; +var last_msgbox_tag = ""; +function msgbox(type, title, content, link="", callback=null, height=180, width=500, hasRetry=false, contentStyle="") { + $(body).scrollTo(0, 0); + if (!type) { + closeMsgbox(); + return; + } + var remember = false; + try { remember = handler.get_remember(); } catch(e) {} + var autoLogin = false; + try { autoLogin = handler.get_option("auto-login") != ''; } catch(e) {} + width += is_xfce ? 50 : 0; + height += is_xfce ? 50 : 0; + + if (type.indexOf("input-password") >= 0) { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.login("", "", res.password, res.remember); + if (!is_port_forward) { + // Specially handling file transfer for no permission hanging issue (including 60ms + // timer in setPermission. + // For wrong password input hanging issue, we can not use handler.msgbox. + // But how about wrong password for file transfer? + if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in..."); + else msgbox("connecting", "Connecting...", "Logging in..."); + } + }; + } else if (type.indexOf("input-2fa") >= 0) { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.send2fa(res.code, res.trust_this_device || false); + msgbox("connecting", "Connecting...", "Logging in..."); + }; + } else if (type == "session-login" || type == "session-re-login") { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.login(res.osusername, res.ospassword, "", false); + if (!is_port_forward) { + if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in..."); + else msgbox("connecting", "Connecting...", "Logging in..."); + } + }; + } else if (type.indexOf("session-login") >= 0) { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.login(res.osusername, res.ospassword, res.password, res.remember); + if (!is_port_forward) { + if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in..."); + else msgbox("connecting", "Connecting...", "Logging in..."); + } + }; + } else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) { + callback = function() { view.close(); } + } else if (type == 'wait-remote-accept-nook') { + callback = function (res) { + if (!res) { + view.close(); + return; + } + }; + } + last_msgbox_tag = type + "-" + title + "-" + content + "-" + link; + $(#msgbox).content(); +} + +function connecting() { + handler.msgbox("connecting", "Connecting...", "Connection in progress. Please wait."); +} + +handler.msgbox = function(type, title, text, link = "", hasRetry=false) { + // crash somehow (when input wrong password), even with small time, for example, 1ms + self.timer(60ms, function() { msgbox(type, title, text, link, null, 180, 500, hasRetry); }); +} + +handler.cancel_msgbox = function(tag) { + if (last_msgbox_tag == tag) { + closeMsgbox(); + } +} + +var reconnectTimeout = 1000; +handler.msgbox_retry = function(type, title, text, link, hasRetry) { + handler.msgbox(type, title, text, link, hasRetry); + if (hasRetry) { + self.timer(0, retryConnect); + self.timer(reconnectTimeout, retryConnect); + reconnectTimeout *= 2; + } else { + reconnectTimeout = 1000; + } +} + +function retryConnect(cancelTimer=false) { + if (cancelTimer) self.timer(0, retryConnect); + if (!is_port_forward) connecting(); + handler.reconnect(false); +} +/******************** end of msgbox ****************************************/ + +function Progress() +{ + var _val; + var pos = -0.25; + + function step() { + if( _val !== undefined ) { this.refresh(); return false; } + pos += 0.02; + if( pos > 1.25) + pos = -0.25; + this.refresh(); + return true; + } + + function paintNoValue(gfx) + { + var (w,h) = this.box(#dimension,#inner); + var x = pos * w; + w = w * 0.25; + gfx.fillColor( this.style#color ) + .pushLayer(#inner-box) + .rectangle(x,0,w,h) + .popLayer(); + return true; + } + + this[#value] = property(v) { + get return _val; + set { + _val = undefined; + pos = -0.25; + this.paintContent = paintNoValue; + this.animate(step); + this.refresh(); + } + } + + this.value = ""; +} + +var svg_eye_cross = + + +; + +class PasswordComponent: Reactor.Component { + this var visible = false; + this var value = ''; + this var name = 'password'; + + function this(params) { + if (params && params.value) { + this.value = params.value; + } + if (params && params.name) { + this.name = params.name; + } + } + + function render() { + return
    + + {this.visible ? svg_eye_cross : svg_eye} +
    ; + } + + event click $(svg) { + var el = this.$(input); + var value = el.value; + var start = el.xcall(#selectionStart) || 0; + var end = el.xcall(#selectionEnd); + this.update({ visible: !this.visible }); + var me = this; + self.timer(30ms, function() { + var el = me.$(input); + view.focus = el; + el.value = value; + el.xcall(#setSelection, start, end); + }); + } +} + +// type: #post, #get, #delete, #put +function httpRequest(url, type, params, _onSuccess, _onError, headers="") { + if (type != #post) { + stderr.println("only post ok"); + } + handler.post_request(url, JSON.stringify(params), headers); + function check_status() { + var status = handler.get_async_job_status(); + if (status == " ") self.timer(0.1s, check_status); + else { + try { + var data = JSON.parse(status || "{}"); + _onSuccess(data); + } catch (e) { + _onError(status, 0); + } + } + } + check_status(); +} + +function isReasonableSize(r) { + var x = r[0]; + var y = r[1]; + var n = scaleIt(3200); + return !(x < -n || x > n || y < -n || y > n); +} + +function awake() { + view.windowState = View.WINDOW_SHOWN; + view.focus = self; +} + +class MultipleSessionComponent extends Reactor.Component { + this var sessions = []; + this var messageText = translate("Please select the session you want to connect to"); + + function this(params) { + if (params && params.sessions) { + this.sessions = params.sessions; + } + } + + function render() { + return
    +
    {this.messageText}
    + +
    ; + } +} \ No newline at end of file diff --git a/vendor/rustdesk/src/ui/file_transfer.css b/vendor/rustdesk/src/ui/file_transfer.css new file mode 100644 index 0000000..7fd4ac7 --- /dev/null +++ b/vendor/rustdesk/src/ui/file_transfer.css @@ -0,0 +1,269 @@ +div#file-transfer-wrapper { + size:*; + display: none; +} + +div#file-transfer { + size: *; + margin: 0; + flow: horizontal; + background: color(gray-bg); + padding: 0.5em; +} + +table +{ + font: system; + border: 1px solid color(border); + flow: table-fixed; + prototype: Grid; + size: *; + padding:0; + border-spacing: 0; + overflow-x: auto; + overflow-y: hidden; +} + +table > thead { + behavior: column-resizer; + border-bottom: color(border) solid 1px; +} + +table > tbody { + behavior: select-multiple; + overflow-y: scroll-indicator; + size: *; + background: white; +} + +table th { + background-color: color(gray-bg); +} + +table th +{ + padding: 4px; + foreground-repeat: no-repeat; + foreground-position: 50% 3px auto auto; + border-left: color(border) solid 1px; +} + +table th.sortable[sort=asc] +{ + foreground-image: url(stock:arrow-down); +} + +table th.sortable[sort=desc] +{ + foreground-image: url(stock:arrow-up); +} + +table th:nth-child(1) { + width: 32px; +} + +table th:nth-child(2) { + width: *; +} + +table th:nth-child(3) { + width: *; +} + +table th:nth-child(4) { + width: 45px; +} + +table.has_current thead th:current { + font-weight: bold; +} + +table tr:nth-child(odd) { background-color: white; } /* each odd row */ +table tr:nth-child(even) { background-color: #F4F5F6; } /* each even row */ + +table.has_current tr:current /* current row */ +{ + background-color: color(accent); +} + +table.has_current tbody tr:checked +{ + background-color: color(accent); +} + +table.has_current tbody tr:checked td { + color: highlighttext; +} + +table td +{ + padding: 4px; + text-align: left; + font-size: 1em; + height: 1.4em; + @ELLIPSIS; +} + +table.folder-view td:nth-child(1) { + behavior:shell-icon; +} + +table td:nth-child(3), table td:nth-child(4) { + color: color(lighter-text); + font-size: 0.9em; +} + +table.has_current tr:current td { + color: white; +} + +table td:nth-child(4) { + text-align: right; +} + +section { + size: *; + margin: 1em; + border-spacing: 0.5em; +} + +table td:nth-child(1) { + foreground-repeat: no-repeat; + foreground-position: 50% 50% +} + +div.toolbar { + flow: horizontal; +} + +div.toolbar svg { + size: 16px; +} + +div.toolbar .spacer { + width: *; +} + +div.toolbar > div.button { + padding: 4px 8px; + opacity: 0.66; +} + +div.toolbar > div.button:active { + opacity: 1; + background-color: #ddd; +} + +div.toolbar > div.button:hover { + opacity: 1; +} + +div.toolbar > div.send { + flow: horizontal; + border-spacing: 0.5em; +} + +div.remote > div.send svg { + transform: scale(-1, 1); +} + +div.navbar { + border: color(border) solid 1px; + padding: 4px 0; +} + +select.select-dir { + width: *; + padding: 0 4px; +} + +div.title { + flow: horizontal; + border-spacing: 1em; + position: relative; +} + +div.title svg.computer { + size: 48px; +} + +div.title div { + margin: * 0; + color: color(light-text); +} + +div.title div.platform { + position: absolute; + left: 12px; + top: 7px; +} + +div.title div.platform svg { + size: 24px; +} + +table.job-table tr td { + width: *; + padding: 0.5em 1em; + border-bottom: color(border) 1px solid; + flow: horizontal; + border-spacing: 1em; + height: 3em; + overflow-x: hidden; +} + +table.job-table tr svg { + size: 16px; +} + +table.job-table tr.is_remote svg { + transform: scale(-1, 1); +} + +table.job-table tr.is_remote div.svg_continue svg { + transform: scale(1, 1); +} + +table.job-table tr td div.text { + width: *; + overflow-x: hidden; +} + +table.job-table tr td div.path { + width: *; + color: color(light-text); + @ELLIPSIS; +} + +table.job-table tr:current td div.path { + color: white; +} + +table#port-forward thead tr th { + padding-left: 1em; + size: *; +} + +table#port-forward tr td { + height: 3em; + text-align: left; +} + +table#port-forward input[type=text], table#port-forward input[type=number] { + font-size: 1.2em; +} + +table#port-forward td.right-arrow svg { + size: 1.2em; + transform: rotate(180deg); +} + +table#port-forward td.remove svg { + size: 0.8em; +} + +table#port-forward tr.value td { + padding-left: 1em; + font-size: 1.5em; + color: black; +} diff --git a/vendor/rustdesk/src/ui/file_transfer.tis b/vendor/rustdesk/src/ui/file_transfer.tis new file mode 100644 index 0000000..1090c01 --- /dev/null +++ b/vendor/rustdesk/src/ui/file_transfer.tis @@ -0,0 +1,819 @@ +var remote_home_dir; + +var svg_add_folder = + + +; +var svg_trash = + + + +; +var svg_arrow = + +; +var svg_home = + +; +var svg_refresh = + +; +var svg_cancel = ; +var svg_continue = ; +var svg_computer = + + + +; + +function getSize(type, size) { + if (!size) { + if (type <= 3) return ""; + return "0B"; + } + size = size.toFloat(); + var toFixed = function(size) { + size = (size * 100).toInteger(); + var a = (size / 100).toInteger(); + if (size % 100 == 0) return a; + if (size % 10 == 0) return a + '.' + (size % 10); + var b = size % 100; + if (b < 10) b = '0' + b; + return a + '.' + b; + } + if (size < 1024) return size.toInteger() + "B"; + if (size < 1024 * 1024) return toFixed(size / 1024) + "K"; + if (size < 1024 * 1024 * 1024) return toFixed(size / (1024 * 1024)) + "M"; + return toFixed(size / (1024 * 1024 * 1024)) + "G"; +} + +function getParentPath(is_remote, path) { + var sep = handler.get_path_sep(is_remote); + var res = path.lastIndexOf(sep); + if (res <= 0) return "/"; + return path.substr(0, res); +} + +function getFileName(is_remote, path) { + var sep = handler.get_path_sep(is_remote); + var res = path.lastIndexOf(sep); + return path.substr(res + 1); +} + +function getExt(name) { + if (name.indexOf(".") == 0) { + return ""; + } + var i = name.lastIndexOf("."); + if (i > 0) return name.substr(i + 1); + return ""; +} + +class JobTable: Reactor.Component { + this var jobs = []; + this var job_map = {}; + + function render() { + var me = this; + var rows = this.jobs.map(function(job, i) { return me.renderRow(job, i); }); + return
    + + {rows} + +
    ; + } + + event click $(svg.cancel) (_, me) { + var job = this.jobs[me.parent.parent.index]; + var id = job.id; + handler.cancel_job(id); + delete this.job_map[id]; + var i = -1; + this.jobs.map(function(job, idx) { + if (job.id == id) i = idx; + }); + this.jobs.splice(i, 1); + this.update(); + var is_remote = job.is_remote; + if (job.type != "del-dir") is_remote = !is_remote; + refreshDir(is_remote); + } + + event click $(svg.continue) (_, me) { + var job = this.jobs[me.parent.parent.parent.index]; + var id = job.id; + this.continueJob(id); + this.update(); + } + + function clearAllJobs() { + this.jobs = []; + this.job_map = {}; + this.update(); + } + + function send(path, is_remote) { + var to; + var show_hidden; + if (is_remote) { + to = file_transfer.local_folder_view.fd.path; + show_hidden = file_transfer.remote_folder_view.show_hidden; + } else { + to = file_transfer.remote_folder_view.fd.path; + show_hidden = file_transfer.local_folder_view.show_hidden; + } + if (!to) return; + to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path); + var id = handler.get_next_job_id(); + this.jobs.push({ type: "transfer", + id: id, path: path, to: to, + include_hidden: show_hidden, + is_remote: is_remote, + is_last: false + }); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + handler.send_files(id, 0, path, to, 0, show_hidden, is_remote); + var self = this; + self.timer(30ms, function() { self.update(); }); + } + + function addJob(id, path, to, file_num, show_hidden, is_remote, auto_start) { + var job = { type: "transfer", + id: id, path: path, to: to, + include_hidden: show_hidden, + is_remote: is_remote, is_last: true, file_num: file_num }; + this.jobs.push(job); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + handler.update_next_job_id(id + 1); + handler.add_job(id, 0, path, to, file_num, show_hidden, is_remote); + if (auto_start) { + this.continueJob(id); + this.update(); + } + stdout.println(JSON.stringify(job)); + } + + function continueJob(id) { + var job = this.job_map[id]; + if (job == null || !job.is_last){ + return; + } + job.is_last = false; + handler.resume_job(job.id, job.is_remote); + } + + function addDelDir(path, is_remote) { + var id = handler.get_next_job_id(); + this.jobs.push({ type: "del-dir", id: id, path: path, is_remote: is_remote }); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + this.update(); + } + + function addDelFile(path, is_remote) { + var id = handler.get_next_job_id(); + this.jobs.push({ type: "del-file", id: id, path: path, is_remote: is_remote }); + this.job_map[id] = this.jobs[this.jobs.length - 1]; + this.update(); + } + + function confirmDeletePolling(is_remote) { + for(var i=0;i n) i = n; + var res = i + ' / ' + n + " " + translate("files"); + if (job.total_size > 0) { + var s = getSize(0, job.finished_size); + if (s) s += " / "; + res += ", " + s + getSize(0, job.total_size); + } + // below has problem if some file skipped + var percent = job.total_size == 0 ? 100 : (100. * job.finished_size / job.total_size).toInteger(); // (100. * i / (n || 1)).toInteger(); + if (job.finished) percent = '100'; + if (percent) res += ", " + percent + "%"; + if (job.finished) { + if (job.err == "skipped") { + res = translate("Skipped") + " " + res; + } else { + res = translate("Finished") + " " + res; + } + } + if (job.speed) res += ", " + getSize(0, job.speed) + "/s"; + return res; + } + + function updateJob(job) { + var el = this.select("div[id=s" + job.id + "]"); + if (el) el.text = this.getStatus(job); + } + + function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) { + var job = this.job_map[id]; + if (job.type == "del-file"){ + job.finished = true; + job.err = err; + refreshDir(job.is_remote); + this.updateJob(job); + return; + } + if (!job) return; + if (file_num < job.file_num) return; + job.file_num = file_num; + var n = job.num_entries || job.entries.length; + job.finished = job.file_num >= n - 1 || err == "cancel" || err == "skipped"; + job.finished_size = finished_size; + job.speed = speed || 0; + job.err = err; + this.updateJob(job); + if (job.type == "del-dir") { + if (job.finished) { + if (!err) { + handler.remove_dir(job.id, job.path, job.is_remote); + refreshDir(job.is_remote); + // Use the job's is_remote; local variable `is_remote` is undefined in this scope. + if (job.is_remote) file_transfer.remote_folder_view.table.resetCurrent(); + else file_transfer.local_folder_view.table.resetCurrent(); + } + } else if (!job.no_confirm) { + handler.confirm_delete_files(id, job.file_num + 1); + } + } else if (job.finished || file_num == -1) { + refreshDir(!job.is_remote); + } + } + + function renderRow(job, i) { + var svg = this.getSvg(job); + return + {svg} +
    +
    {job.path}
    +
    {this.getStatus(job)}
    +
    +
    + {svg_continue} +
    + {svg_cancel} + ; + } +} + +class FolderView : Reactor.Component { + this var fd = {}; + this var history = []; + this var show_hidden = false; + + function sep() { + return handler.get_path_sep(this.is_remote); + } + + function this(params) { + this.is_remote = params.is_remote; + if (this.is_remote) { + this.show_hidden = !!handler.get_option("remote_show_hidden"); + } else { + this.show_hidden = !!handler.get_option("local_show_hidden"); + } + if (!this.is_remote) { + var dir = handler.get_option("local_dir"); + if (dir) { + this.fd = handler.read_dir(dir, this.show_hidden); + if (this.fd) return; + } + this.fd = handler.read_dir(handler.get_home_dir(), this.show_hidden); + } + } + + // sort predicate + function foldersFirst(a, b) { + if (a.type <= 3 && b.type > 3) return -1; + if (a.type > 3 && b.type <= 3) return +1; + if (a.name == b.name) return 0; + return a.name.toLowerCase().lexicalCompare(b.name.toLowerCase()); + } + + function render() + { + return
    + {this.renderTitle()} + {this.renderNavBar()} + {this.renderOpBar()} + {this.renderTable()} +
    ; + } + + function renderTitle() { + return
    + {svg_computer} +
    {platformSvg(handler.get_platform(this.is_remote), "white")}
    +
    {translate(this.is_remote ? "Remote Computer" : "Local Computer")}
    +
    + } + + function renderNavBar() { + return
    +
    {svg_home}
    +
    {svg_arrow}
    +
    {svg_arrow}
    + {this.renderSelect()} +
    {svg_refresh}
    +
    ; + } + + function renderSelect() { + return ; + } + + function renderOpBar() { + if (this.is_remote) { + return
    +
    {svg_send}{translate('Receive')}
    +
    +
    {svg_add_folder}
    +
    {svg_trash}
    +
    ; + } + return
    +
    {svg_add_folder}
    +
    {svg_trash}
    +
    +
    {translate('Send')}{svg_send}
    +
    ; + } + + function get_updated() { + this.table.sortRows(false); + if (this.fd && this.fd.path) this.select_dir.value = this.fd.path; + } + + function renderTable() { + var fd = this.fd; + var entries = fd.entries || []; + var table = this.table; + if (!table || !table.sortBy) { + entries.sort(this.foldersFirst); + } + var me = this; + var path = fd.path; + if (path != "/" && path) { + entries = [{ name: "..", type: 1 }].concat(entries); + } + var rows = entries.map(function(e) { return me.renderRow(e); }); + var id = (this.is_remote ? "remote" : "local") + "-folder-view"; + return + + + + + {rows} + + + +
  • {svg_checkmark}{translate('Show Hidden Files')}
  • + +
    +
    {translate('Name')}{translate('Modified')}{translate('Size')}
    ; + } + + function joinPath(name) { + var path = this.fd.path; + if (path == "/") { + if (this.sep() == "/") return this.sep() + name; + else return name; + } + return path + (path[path.length - 1] == this.sep() ? "" : this.sep()) + name; + } + + function attached() { + var me = this; + this.table.onRowDoubleClick = function (row) { + var type = row[0].attributes["type"]; + if (type > 3) return; + var name = row[1].text; + var path = name == ".." ? getParentPath(me.is_remote, me.fd.path) : me.joinPath(name); + me.table.resetCurrent(); + me.goto(path, true); + } + this.get_updated(); + } + + function goto(path, push) { + if (!path) return; + if (this.sep() == "\\" && path.length == 2) { // windows drive + path += "\\"; + } + if (push) this.pushHistory(); + if (this.is_remote) { + handler.read_remote_dir(path, this.show_hidden); + } else { + var fd = handler.read_dir(path, this.show_hidden); + this.refresh({ fd: fd }); + } + } + + function refresh(data) { + if (!data.fd || !data.fd.path) return; + if (this.is_remote && !remote_home_dir) { + remote_home_dir = data.fd.path; + } + this.update(data); + var me = this; + self.timer(1ms, function() { me.get_updated(); }); + } + + function renderRow(entry) { + var path; + if (this.is_remote) { + path = handler.get_icon_path(entry.type, getExt(entry.name)); + } else { + path = this.joinPath(entry.name); + } + var tm = entry.time ? new Date(entry.time.toFloat() * 1000.).toLocaleString() : 0; + return + + {entry.name} + {tm || ""} + {getSize(entry.type, entry.size)} + ; + } + + event click $(#switch-hidden) { + this.show_hidden = !this.show_hidden; + this.refreshDir(); + } + + event click $(.goup) () { + var path = this.fd.path; + if (!path || path == "/") return; + path = getParentPath(this.is_remote, path); + this.goto(path, true); + } + + event click $(.goback) () { + var path = this.history.pop(); + if (!path) return; + this.goto(path, false); + } + + event click $(.trash) () { + var rows = this.getCurrentRows(); + if (!rows || rows.length == 0) return; + + var delete_dirs = new Array(); + + for (var i = 0; i < rows.length; ++i) { + var row = rows[i]; + + var path = row[0]; + var type = row[1]; + + var new_history = []; + for (var j = 0; j < this.history.length; ++j) { + var h = this.history[j]; + if ((h + this.sep()).indexOf(path + this.sep()) == -1) new_history.push(h); + } + this.history = new_history; + if (type == 1) { + file_transfer.job_table.addDelDir(path, this.is_remote); + } else { + file_transfer.job_table.addDelFile(path, this.is_remote); + } + } + file_transfer.job_table.confirmDeletePolling(this.is_remote); + } + + event click $(.add-folder) () { + var me = this; + msgbox("custom", translate("Create Folder"), "
    \ +
    " + translate("Please enter the folder name") + ":
    \ +
    \ +
    ", "", function(res=null) { + if (!res) return; + if (!res.name) return; + var name = res.name.trim(); + if (!name) return; + if (name.indexOf(me.sep()) >= 0) { + handler.msgbox("custom-error", "Create Folder", "Invalid folder name"); + return; + } + var path = me.joinPath(name); + var id = handler.get_next_job_id(); + handler.create_dir(id, path, me.is_remote); + create_dir_jobs[id] = { is_remote: me.is_remote, path: path }; + }); + } + + function refreshDir() { + this.goto(this.fd.path, false); + } + + event click $(.refresh) () { + this.refreshDir(); + } + + event click $(.home) () { + var path = this.is_remote ? remote_home_dir : handler.get_home_dir(); + if (!path) return; + if (path == this.fd.path) return; + this.goto(path, true); + } + + function getCurrentRow() { + var row = this.table.getCurrentRow(); + if (!row) return; + var name = row[1].text; + if (!name || name == "..") return; + var type = row[0].attributes["type"]; + return [this.joinPath(name), type]; + } + + function getCurrentRows() { + var rows = this.table.getCurrentRows(); + if (!rows || rows.length== 0) return; + + var records = new Array(); + + for (var i = 0; i < rows.length; ++i) { + var name = rows[i][1].text; + if (!name || name == "..") continue; + + var type = rows[i][0].attributes["type"]; + records.push([this.joinPath(name), type]); + } + return records; + } + + event click $(.send) () { + var rows = this.getCurrentRows(); + if (!rows || rows.length == 0) return; + for (var i = 0; i < rows.length; ++i) { + file_transfer.job_table.send(rows[i][0], this.is_remote); + } + } + + event change $(.select-dir) (_, el) { + var x = getTime() - last_key_time; + if (x < 1000) return; + if (this.fd.path != el.value) { + this.goto(el.value, true); + } + } + + event keydown $(.select-dir) (evt, me) { + if (isEnterKey(evt)) { + this.goto(me.value, true); + } + } + + function pushHistory() { + var path = this.fd.path; + if (!path) return; + if (path != this.history[this.history.length - 1]) this.history.push(path); + } +} + +var file_transfer; + +class FileTransfer: Reactor.Component { + function this() { + file_transfer = this; + } + + function render() { + return
    + + + +
    ; + } +} + +function initializeFileTransfer() +{ + $(#file-transfer-wrapper).content(); + $(#video-wrapper).style.set { visibility: "hidden", position: "absolute" }; + $(#file-transfer-wrapper).style.set { display: "block" }; +} + +handler.updateFolderFiles = function(fd) { + // stdout.println("update folder files: " + JSON.stringify(fd)); + fd.entries = fd.entries || []; + if (fd.id > 0) { + var jt = file_transfer.job_table; + var job = jt.job_map[fd.id]; + if (job) { + job.file_num = -1; + job.total_size = fd.total_size; + job.entries = fd.entries; + job.num_entries = fd.num_entries; + file_transfer.job_table.updateJobStatus(job.id); + } + } else { + file_transfer.remote_folder_view.refresh({ fd: fd }); + } +} + +handler.jobProgress = function(id, file_num, speed, finished_size) { + file_transfer.job_table.updateJobStatus(id, file_num, null, speed, finished_size); +} + +handler.jobDone = function(id, file_num = -1) { + var job = create_dir_jobs[id]; + if (job) { + refreshDir(job.is_remote); + return; + } + file_transfer.job_table.updateJobStatus(id, file_num); +} + +handler.jobError = function(id, err, file_num = -1) { + var job = deleting_single_file_jobs[id]; + if (job) { + msgbox("custom-error", "Delete File", err); + return; + } + job = create_dir_jobs[id]; + if (job) { + msgbox("custom-error", "Create Folder", err); + return; + } + if (file_num < 0) { + handler.msgbox("custom-error", "Failed", err); + } + file_transfer.job_table.updateJobStatus(id, file_num, err); +} + +handler.clearAllJobs = function() { + file_transfer.job_table.clearAllJobs(); +} + +handler.addJob = function (id, path, to, file_num, show_hidden, is_remote, auto_start) { // load last job + // stdout.println("restore job: " + is_remote); + file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote,auto_start); +} + +handler.updateTransferList = function () { + file_transfer.job_table.update(); +} + +function refreshDir(is_remote) { + if (is_remote) file_transfer.remote_folder_view.refreshDir(); + else file_transfer.local_folder_view.refreshDir(); +} + +var deleting_single_file_jobs = {}; +var create_dir_jobs = {} + +function confirmDelete(id ,path, is_remote) { + msgbox("custom-skip", "Confirm Delete", "
    \ +
    " + translate('Are you sure you want to delete this file?') + "
    \ + " + path + "
    \ +
    ", "", function(res=null) { + if (!res) { + file_transfer.job_table.updateJobStatus(id, -1, "cancel"); + file_transfer.job_table.cancelDeletePolling(); + } else if (res.skip) { + file_transfer.job_table.updateJobStatus(id, -1, "cancel"); + file_transfer.job_table.confirmDeletePolling(is_remote); + } else { + handler.remove_file(id, path, 0, is_remote); + if (is_remote) file_transfer.remote_folder_view.table.resetCurrent(); + else file_transfer.local_folder_view.table.resetCurrent(); + deleting_single_file_jobs[id] = { is_remote: is_remote, path: path }; + file_transfer.job_table.confirmDeletePolling(is_remote); + } + }); +} + +handler.confirmDeleteFiles = function(id, i, name) { + var jt = file_transfer.job_table; + var job = jt.job_map[id]; + if (!job) return; + var n = job.num_entries; + if (i >= n) return; + var file_path = job.path; + if (name) file_path += handler.get_path_sep(job.is_remote) + name; + msgbox("custom-skip", "Confirm Delete", "
    \ +
    " + translate('Deleting') + " #" + (i + 1) + " / " + n + " " + translate('files') + ".
    \ +
    " + translate('Are you sure you want to delete this file?') + "
    \ + " + file_path + "
    \ +
    " + translate('Do this for all conflicts') + "
    \ +
    ", "", function(res=null) { + if (!res) { + jt.updateJobStatus(id, i - 1, "cancel"); + file_transfer.job_table.cancelDeletePolling(); + } else if (res.skip) { + if (res.remember){ + jt.updateJobStatus(id, i, "cancel"); + } else{ + handler.jobDone(id, i); + } + file_transfer.job_table.confirmDeletePolling(job.is_remote); + } else { + job.no_confirm = res.remember; + if (job.no_confirm){ + handler.set_no_confirm(id); + file_transfer.job_table.confirmDeletePolling(job.is_remote); + } + handler.remove_file(id, file_path, i, job.is_remote); + } + if(i+1 >= n){ + file_transfer.job_table.confirmDeletePolling(job.is_remote); + } + }); +} + +handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical) { + var jt = file_transfer.job_table; + var identical_msg = is_identical ? translate("identical_file_tip"): ""; + msgbox("custom-skip", "Confirm Write Strategy", "
    \ +
    " + translate('Overwrite') + " " + translate('files') + ".
    \ +
    " + translate('This file exists, skip or overwrite this file?') + "
    \ + " + to + "
    \ +
    " + identical_msg + "
    \ +
    " + translate('Do this for all conflicts') + "
    \ +
    ", "", function(res=null) { + if (!res) { + jt.updateJobStatus(id, -1, "cancel"); + handler.cancel_job(id); + } else if (res.skip) { + if (res.remember){ + handler.set_write_override(id,file_num,false,true, is_upload); // + } else { + handler.set_write_override(id,file_num,false,false,is_upload); // + } + } else { + if (res.remember){ + handler.set_write_override(id,file_num,true,true,is_upload); // + } else { + handler.set_write_override(id,file_num,true,false,is_upload); // + } + } + }); +} + +function save_file_transfer_close_state() { + var local_dir = file_transfer.local_folder_view.fd.path || ""; + var local_show_hidden = file_transfer.local_folder_view.show_hidden ? "Y" : ""; + var remote_dir = file_transfer.remote_folder_view.fd.path || ""; + var remote_show_hidden = file_transfer.remote_folder_view.show_hidden ? "Y" : ""; + handler.save_close_state("local_dir", local_dir); + handler.save_close_state("local_show_hidden", local_show_hidden); + handler.save_close_state("remote_dir", remote_dir); + handler.save_close_state("remote_show_hidden", remote_show_hidden); +} diff --git a/vendor/rustdesk/src/ui/grid.tis b/vendor/rustdesk/src/ui/grid.tis new file mode 100644 index 0000000..6560521 --- /dev/null +++ b/vendor/rustdesk/src/ui/grid.tis @@ -0,0 +1,258 @@ +var last_key_time = 0; +var keymap = {}; +for (var (k, v) in Event) { + k = k + "" + if (k[0] == "V" && k[1] == "K") { + keymap[v] = k; + } +} + +class Grid: Behavior { + const TABLE_HEADER_CLICK = 0x81; + const TABLE_ROW_CLICK = 0x82; + const TABLE_ROW_DBL_CLICK = 0x83; + function onHeaderClick(headerCell) + { + this.postEvent(TABLE_HEADER_CLICK, headerCell.index, headerCell); + return true; + } + + function onRowClick(row , reason) + { + this.postEvent(TABLE_ROW_CLICK, row.index, row); + return true; + } + + function onRowDoubleClick(row) + { + this.postEvent(TABLE_ROW_DBL_CLICK, row.index, row); + return true; + } + + function getCurrentRow() + { + return this.$(tbody>tr:current); + } + + function getCurrentRows() + { + return this.$$(tbody>tr:checked); + } + + function getCurrentColumn() + { + return this.$(thead>:current); // return current cell in header row + } + + function resetCurrent() { + var rows = this.getCurrentRows(); + for (var i = 0; i < rows.length; ++i) { + var row = rows[i]; + row.state.current = false; + row.state.checked = false; + } + } + + function setCurrentRow(row, reason = #by_code, doubleClick = false) + { + if (!row) return; + // get previously selected row: + var prev = this.getCurrentRow(); + if (prev) + { + if (prev === row && !doubleClick) return; // already here, nothing to do. + prev.state.current = false; // drop state flag + prev.state.checked = false; // drop state flag + } + row.state.current = true; + row.state.checked = true; + row.scrollToView(); + + if (doubleClick) + this.onRowDoubleClick(row,reason); + else + this.onRowClick(row,reason); + } + + function setCurrentColumn(col) + { + // get previously selected column: + var prev = this.getCurrentColumn(); + if (prev) + { + if (prev === col) return; // already here, nothing to do. + prev.state.current = false; // drop state flag + } + col.state.current = true; // set state flag + col.scrollToView(); + this.onHeaderClick(col); + } + + function sortRows(sortClicked) + { + var col = this.sortBy; + if (!col) return; + var byColumn = col.index; + var nowDesc = (col.attributes["sort"] || "desc") == "desc"; + if (sortClicked) (this.$(thead [sort]) || col).attributes["sort"] = undefined; // drop any other sort order. + var getValue = function(x) { + var value = x.attributes["value"]; + if (value == undefined) return x.text.toLowerCase(); + return value.toFloat(); + } + var sort = function(r1, r2, asc) { + if (r1[1].text == "..") { + return -1; + } + if (r2[1].text == "..") { + return 1; + } + if (!asc) + return getValue(r1[byColumn]) < getValue(r2[byColumn]) ? -1 : 1; + else + return getValue(r1[byColumn]) > getValue(r2[byColumn]) ? -1 : 1; + } + if (nowDesc) + { + if (sortClicked) col.attributes["sort"] = "asc"; + this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? true : false)); + } else { + if (sortClicked) col.attributes["sort"] = "desc"; + this.body.sort(:r1, r2: sort(r1, r2, sortClicked ? false : true)); + } + } + + function attached() + { + assert this.tag == "table" : "wrong element type for grid, table expected"; + this.body = this.$(:root>tbody); + assert this.body : "Grid require element"; + } + + function onMouse(evt) + { + if ((evt.type != Event.MOUSE_DOWN) && (evt.type != Event.MOUSE_DCLICK)) + return false; + + if (!evt.mainButton) + return false; + + // auxiliary function, returns row this target element belongs to + function targetRow(target) { return target.$p(tbody>tr); } + + // auxiliary function, returns row this target element belongs to + function targetHeaderCell(target) { return target.$p(thead>tr>th); } + + if (var row = targetRow(evt.target)) // click on the row + this.setCurrentRow(row, #by_mouse, evt.type == Event.MOUSE_DCLICK); + else if (var headerCell = targetHeaderCell(evt.target)) + { + this.setCurrentColumn(headerCell); // click on the header cell + if (evt.type != Event.MOUSE_DCLICK && headerCell.$is(.sortable)) { + this.sortBy = headerCell; + this.sortRows(true); + } + } + + //return true; // as it is always ours then stop event bubbling + } + + function onFocus(evt) + { + return (evt.type == Event.GOT_FOCUS || evt.type == Event.LOST_FOCUS); + } + + function onKey(evt) + { + last_key_time = getTime(); + if (evt.type != Event.KEY_DOWN) + return false; + + switch(evt.keyCode) + { + case Event.VK_DOWN: + { + var crow = this.getCurrentRow(); + var idx = crow? crow.index + 1 : 0; + if (idx < this.body.length) this.setCurrentRow(this.body[idx],#by_key); + } + return true; + + case Event.VK_UP: + { + var crow = this.getCurrentRow(); + var idx = crow? crow.index - 1 : this.length - 1; + if (idx >= 0) this.setCurrentRow(this.body[idx],#by_key); + } + return true; + + case Event.VK_PRIOR: + { + var y = this.body.scroll(#top) - this.body.scroll(#height); + var r; + for(var i = this.body.length - 1; i >= 0; --i) + { + var pr = r; r = this.body[i]; + if (r.box(#top, #inner, #content) < y) + { + // this row is further than scroll pos - height of scroll area + this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible + return true; + } + } + this.setCurrentRow(r,#by_key); // just in case + } + return true; + case Event.VK_NEXT: + { + var y = this.body.scroll(#top) + 2 * this.body.scroll(#height); + var lastScrollable = this.body.length - 1; + var r; + for(var i = 0; i <= lastScrollable; ++i) + { + var pr = r; r = this.body[i]; + if (r.box(#bottom, #inner, #content) > y) + { + // this row is further than scroll pos - height of scroll area + this.setCurrentRow(pr? pr: r,#by_key); // to last fully visible + return true; + } + } + this.setCurrentRow(r,#by_key); // just in case + } + return true; + + case Event.VK_HOME: + { + if (this.body.length) + this.setCurrentRow(this.body.first,#by_key); + } + return true; + + case Event.VK_END: + { + if (this.body.length) + this.setCurrentRow(this.body.last,#by_key); + } + return true; + } + var char = handler.get_char(keymap[evt.keyCode] || "", evt.keyCode); + if (char) { + var crow = this.getCurrentRow(); + var idx = crow? crow.index + 1 : 0; + while (idx < this.body.length) { + var el = this.body[idx]; + var text = el[1].text; + if (text && text[0].toLowerCase() == char) { + this.setCurrentRow(el, #by_key); + return true; + } + idx += 1; + } + } + if (isEnterKey(evt)) { + this.onRowDoubleClick(this.getCurrentRow()); + } + return false; + } +} diff --git a/vendor/rustdesk/src/ui/header.css b/vendor/rustdesk/src/ui/header.css new file mode 100644 index 0000000..8fe4086 --- /dev/null +++ b/vendor/rustdesk/src/ui/header.css @@ -0,0 +1,97 @@ +header div { + word-wrap: normal; +} + +header #screens { + background: white; + border: #A9A9A9 1px solid; + height: 22px; + border-radius: 4px; + flow: horizontal; + border-spacing: 0.5em; + padding-right: 1em; + position: relative; +} + +header #screen { + text-align: center; + margin: 3px 0; + width: 18px; + height: 14px; + border: color(border) solid 1px; + font-size: 11px; + color: color(light-text); +} + +@media platform == "OSX" { + header #screen { + line-height: 11px; + } +} + +header #secure { + position: absolute; + left: -10px; + top: -2px; +} + +header #secure svg { + size: 18px; +} + +header .remote-id { + width: 80px; + @ELLIPSIS; + padding-left: 30px; + padding-right: 4em; + margin: * 0; +} + +header span:hover { + background: #f7f7f7; +} + +@media platform != "OSX" { +header span:hover { + background: #d9d9d9; +} +} + +header #screen:hover { + background: #d9d9d9; +} + +header #secure:hover { + background: unset; +} + +header span:active, header #screen:active { + color: black; + background: color(gray-bg); +} + +div#global-screens { + position: relative; + margin: 2px 0; +} + +div#global-screens > div { + position: absolute; + border: color(border) solid 1px; + text-align: center; + color: color(light-text); +} + +header #screen.current, div#global-screens > div.current { + background: #666; + color: white; +} + +span#fullscreen.active { + border: color(border) solid 1px; +} + +button:disabled { + opacity: 0.3; +} + diff --git a/vendor/rustdesk/src/ui/header.tis b/vendor/rustdesk/src/ui/header.tis new file mode 100644 index 0000000..40ccbcb --- /dev/null +++ b/vendor/rustdesk/src/ui/header.tis @@ -0,0 +1,722 @@ +var pi = handler.get_default_pi(); // peer information +var chat_msgs = []; + +var svg_fullscreen = + +; +var svg_action = ; +var svg_display = + +; +var svg_secure = + +; +var svg_insecure = ; +var svg_insecure_relay = ; +var svg_secure_relay = ; +var svg_recording_off = ; +var svg_recording_on = ; + +var cur_window_state = view.windowState; +function check_state_change() { + if (view.windowState != cur_window_state) { + stateChanged(); + } + self.timer(30ms, check_state_change); +} + +if (is_linux) { + check_state_change(); +} else { + view << event statechange { + stateChanged(); + } +} + +function get_id() { + return handler.get_option('alias') || handler.get_id() +} + +function stateChanged() { + stdout.println('state changed from ' + cur_window_state + ' -> ' + view.windowState); + cur_window_state = view.windowState; + adjustBorder(); + adaptDisplay(); + if (cur_window_state != View.WINDOW_MINIMIZED) { + view.focus = handler; // to make focus away from restore/maximize button, so that enter key work + } + var fs = view.windowState == View.WINDOW_FULL_SCREEN; + var el = $(#fullscreen); + if (el) el.attributes.toggleClass("active", fs); + el = $(#maximize); + if (el) { + el.state.disabled = fs; + } + if (fs) { + $(header).style.set { + display: "none", + }; + } +} + +var header; +var old_window_state = View.WINDOW_SHOWN; + +var is_edit_os_password; +class EditOsPassword: Reactor.Component { + function render() { + return {svg_edit}; + } + + function onMouse(evt) { + if (evt.type == Event.MOUSE_DOWN) { + is_edit_os_password = true; + editOSPassword(); + } + } +} + +function editOSPassword(login=false) { + var p0 = handler.get_option('os-password'); + msgbox("custom-os-password", 'OS Password', p0, "", function(res=null) { + if (!res) return; + var a0 = handler.get_option('auto-login') != ''; + var p = (res.password || '').trim(); + var a = res.autoLogin || false; + if (p == p0 && a == a0) return; + if (p != p0) handler.set_option('os-password', p); + if (a != a0) handler.set_option('auto-login', a ? 'Y' : ''); + if (p && login) { + handler.input_os_password(p, true); + } + }); +} + +var recording = false; + +class Header: Reactor.Component { + this var conn_note = ""; + + function this() { + header = this; + } + + function render() { + var icon_conn; + var title_conn; + if (this.secure_connection && this.direct_connection) { + icon_conn = svg_secure; + title_conn = translate("Direct and encrypted connection"); + } else if (this.secure_connection && !this.direct_connection) { + icon_conn = svg_secure_relay; + title_conn = translate("Relayed and encrypted connection"); + } else if (!this.secure_connection && this.direct_connection) { + icon_conn = svg_insecure; + title_conn = translate("Direct and unencrypted connection"); + } else { + icon_conn = svg_insecure_relay; + title_conn = translate("Relayed and unencrypted connection"); + } + var stream_type = this.stream_type; + if (stream_type == "Relay") { + stream_type = "TCP"; + } + if (stream_type) { + title_conn += " (" + stream_type + ")"; + } + var title = get_id(); + if (pi.hostname) title += "(" + pi.username + "@" + pi.hostname + ")"; + if ((pi.displays || []).length == 0) { + return
    {title}
    ; + } + var screens = pi.displays.map(function(d, i) { + return
    + {i+1} +
    ; + }); + updateWindowToolbarPosition(); + var style = "flow:horizontal;"; + if (is_osx) style += "margin:*"; + self.timer(1ms, updatePrivacyMode); + self.timer(1ms, toggleMenuState); + return
    + {is_osx || is_xfce ? "" : {svg_fullscreen}} +
    + {icon_conn} +
    {get_id()}
    +
    {screens}
    + {this.renderGlobalScreens()} +
    + {svg_chat} + {svg_action} + {svg_display} + {svg_keyboard} + {recording_enabled ? {recording ? svg_recording_on : svg_recording_off} : ""} + {this.renderKeyboardPop()} + {this.renderDisplayPop()} + {this.renderActionPop()} +
    ; + } + + function renderKeyboardPop(){ + const is_map_mode_supported = handler.is_keyboard_mode_supported("map"); + const is_translate_mode_supported = handler.is_keyboard_mode_supported("translate"); + return + +
  • {svg_checkmark}{translate('Legacy mode')}
  • + { is_map_mode_supported &&
  • {svg_checkmark}{translate('Map mode')}
  • } + { is_translate_mode_supported &&
  • {svg_checkmark}{translate('Translate mode')}
  • } + +
    ; + } + + function renderDisplayPop() { + var codecs = handler.alternative_codecs(); + var show_codec = codecs[0] || codecs[1] || codecs[2] || codecs[3]; + + var cursor_embedded = false; + if ((pi.displays || []).length > 0) { + if (pi.displays.length > pi.current_display) { + cursor_embedded = pi.displays[pi.current_display].cursor_embedded; + } + } + + var is_file_copy_paste_supported = false; + if (handler.version_cmp(pi.version, '1.2.4') < 0) { + is_file_copy_paste_supported = is_win && pi.platform == "Windows"; + } else { + is_file_copy_paste_supported = handler.has_file_clipboard() && pi.platform_additions?.has_file_clipboard; + } + + return + +
  • {translate('Adjust Window')}
  • +
    +
  • {svg_checkmark}{translate('Original')}
  • +
  • {svg_checkmark}{translate('Shrink')}
  • +
  • {svg_checkmark}{translate('Stretch')}
  • +
    +
  • {svg_checkmark}{translate('Good image quality')}
  • +
  • {svg_checkmark}{translate('Balanced')}
  • +
  • {svg_checkmark}{translate('Optimize reaction time')}
  • +
  • {svg_checkmark}{translate('Custom')}
  • + {show_codec ?
    +
    +
  • {svg_checkmark}Auto
  • + {codecs[0] ?
  • {svg_checkmark}VP8
  • : ""} +
  • {svg_checkmark}VP9
  • + {codecs[1] ?
  • {svg_checkmark}AV1
  • : ""} + {codecs[2] ?
  • {svg_checkmark}H264
  • : ""} + {codecs[3] ?
  • {svg_checkmark}H265
  • : ""} +
    : ""} +
    + {!cursor_embedded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • } + {
  • {svg_checkmark}{translate('Follow remote cursor')}
  • } + {
  • {svg_checkmark}{translate('Follow remote window focus')}
  • } +
  • {svg_checkmark}{translate('Show quality monitor')}
  • + {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} + {is_file_copy_paste_supported && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} + {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} + {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} + {(pi.platform == "Windows" || pi.platform == "Mac OS") && (handler.get_toggle_option("privacy-mode") || (keyboard_enabled && privacy_mode_enabled)) ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} + {keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ?
  • {svg_checkmark}{translate('Swap control-command key')}
  • : ""} + {handler.version_cmp(pi.version, '1.2.4') >= 0 ?
  • {svg_checkmark}{translate('True color (4:4:4)')}
  • : ""} + + ; + } + + function renderActionPop() { + return + + {keyboard_enabled ?
  • {translate('OS Password')}
  • : ""} +
  • {translate('Transfer file')}
  • +
  • {translate('TCP tunneling')}
  • + {handler.get_audit_server("conn") &&
  • {translate('Note')}
  • } +
    + {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • {translate('Insert')} Ctrl + Alt + Del
  • : ""} + {restart_enabled && (pi.platform == "Linux" || pi.platform == "Windows" || pi.platform == "Mac OS") ?
  • {translate('Restart remote device')}
  • : ""} + {keyboard_enabled ?
  • {translate('Insert Lock')}
  • : ""} + {keyboard_enabled && pi.platform == "Windows" && pi.sas_enabled ?
  • {translate("Block user input")}
  • : ""} + {handler.is_screenshot_supported() ?
  • {translate('Take screenshot')}
  • : "" } +
  • {translate('Refresh')}
  • + + ; + } + + function renderGlobalScreens() { + if (pi.displays.length < 3) return ""; + var x0 = 9999999; + var y0 = 9999999; + var x = -9999999; + var y = -9999999; + pi.displays.map(function(d, i) { + if (d.x < x0) x0 = d.x; + if (d.y < y0) y0 = d.y; + var dx = d.x + d.width; + if (dx > x) x = dx; + var dy = d.y + d.height; + if (dy > y) y = dy; + }); + var w = x - x0; + var h = y - y0; + var scale = 16. / h; + var screens = pi.displays.map(function(d, i) { + var min_wh = d.width > d.height ? d.height : d.width; + var fs = min_wh * 0.9 * scale; + var style = "width:" + (d.width * scale) + "px;" + + "height:" + (d.height * scale) + "px;" + + "left:" + ((d.x - x0) * scale) + "px;" + + "top:" + ((d.y - y0) * scale) + "px;" + + "font-size:" + fs + "px;"; + if (is_osx) { + style += "line-height:" + fs + "px;"; + } + return
    {i+1}
    ; + }); + + var style = "width:" + (w * scale) + "px; height:" + (h * scale) + "px;"; + return
    + {screens} +
    ; + } + + event click $(#fullscreen) (_, el) { + if (view.windowState == View.WINDOW_FULL_SCREEN) { + if (old_window_state == View.WINDOW_MAXIMIZED) { + view.windowState = View.WINDOW_SHOWN; + } + view.windowState = old_window_state; + } else { + old_window_state = view.windowState; + if (view.windowState == View.WINDOW_MAXIMIZED) { + view.windowState = View.WINDOW_SHOWN; + } + view.windowState = View.WINDOW_FULL_SCREEN; + if (is_linux) { self.timer(150ms, function() { view.windowState = View.WINDOW_FULL_SCREEN; }); } + } + } + + event click $(#chat) { + startChat(); + } + + event click $(#action) (_, me) { + var menu = $(menu#action-options); + me.popup(menu); + } + + event click $(#display) (_, me) { + var menu = $(menu#display-options); + me.popup(menu); + } + + event click $(#keyboard) (_, me) { + var menu = $(menu#keyboard-options); + me.popup(menu); + } + + event click $(span#recording) (_, me) { + header.update(); + handler.record_screen(!recording) + } + + event click $(#screen) (_, me) { + if (pi.current_display == me.index) return; + handler.switch_display(me.index); + } + + event keyup (evt) { + if((pi.displays || []).length > 0 && evt.keyCode == 220) + { + if (pi.displays.length > pi.current_display) + handler.switch_display(pi.current_display + 1); + else + handler.switch_display(1); + } + } + + event click $(#transfer-file) { + handler.transfer_file(); + } + + event click $(#os-password) (evt) { + if (is_edit_os_password) { + is_edit_os_password = false; + return; + } + var p = handler.get_option('os-password'); + if (!p) editOSPassword(true); + else handler.input_os_password(p, true); + } + + event click $(#tunnel) { + handler.tunnel(); + } + + event click $(#note) { + var self = this; + msgbox("custom", "Note",
    + +
    , "", function(res=null) { + if (!res) return; + if (res.text == null || res.text == undefined) return; + self.conn_note = res.text ?? ""; + handler.send_note(res.text); + }, 280); + } + + event click $(#ctrl-alt-del) { + handler.ctrl_alt_del(); + } + + event click $(#restart_remote_device) { + msgbox( + "restart-confirmation", + translate("Restart remote device"), + translate("Are you sure you want to restart") + " " + pi.username + "@" + pi.hostname + "(" + get_id() + ") ?", + "", + function(res=null) { + if (res != null) handler.restart_remote_device(); + } + ); + } + + event click $(#lock-screen) { + handler.lock_screen(); + } + + event click $(#take-screenshot) { + handler.take_screenshot(pi.current_display, ""); + } + + event click $(#refresh) { + // 0 is just a dummy value. It will be ignored by the handler. + handler.refresh_video(0); + } + + event click $(#block-input) { + if (!input_blocked) { + handler.toggle_option("block-input"); + input_blocked = true; + $(#block-input).text = translate("Unblock user input"); + } else { + handler.toggle_option("unblock-input"); + input_blocked = false; + $(#block-input).text = translate("Block user input"); + } + } + + event click $(menu#display-options li) (_, me) { + if (me.id == "custom") { + handle_custom_image_quality(); + } else if (me.id == "privacy-mode") { + togglePrivacyMode(me.id); + } else if (me.id == "show-quality-monitor") { + toggleQualityMonitor(me.id); + } else if (me.id == "i444") { + toggleI444(me.id); + } else if (me.attributes.hasClass("toggle-option")) { + handler.toggle_option(me.id); + toggleMenuState(); + } else if (!me.attributes.hasClass("selected")) { + var type = me.attributes["type"]; + if (type == "image-quality") { + handler.save_image_quality(me.id); + } else if (type == "view-style") { + handler.save_view_style(me.id); + adaptDisplay(); + } else if (type == "codec-preference") { + handler.set_option("codec-preference", me.id); + handler.update_supported_decodings(); + } + toggleMenuState(); + } + } + + event click $(menu#keyboard-options>li) (_, me) { + if (me.id == "legacy") { + handler.save_keyboard_mode("legacy"); + } else if (me.id == "map") { + handler.save_keyboard_mode("map"); + } else if (me.id == "translate") { + handler.save_keyboard_mode("translate"); + } + toggleMenuState() + } +} + +function handle_custom_image_quality() { + var tmp = handler.get_custom_image_quality(); + var bitrate = (tmp[0] || 50); + var extendedBitrate = bitrate > 100; + var maxRate = extendedBitrate ? 2000 : 100; + msgbox("custom-image-quality", "Custom Image Quality", "
    \ +
    x% Bitrate More
    \ +
    ", "", function(res=null) { + if (!res) return; + if (res.id === "extended-slider") { + var slider = res.parent.$(#bitrate-slider) + slider.slider.max = res.checked ? 2000 : 100; + if (slider.value > slider.slider.max) { + slider.value = slider.slider.max; + } + var buddy = res.parent.$(#bitrate-buddy); + buddy.value = slider.value; + return; + } + if (!res.bitrate) return; + handler.save_custom_image_quality(res.bitrate); + toggleMenuState(); + }); +} + +function toggleMenuState() { + var values = []; + var q = handler.get_image_quality(); + if (!q) q = "balanced"; + values.push(q); + var s = handler.get_view_style(); + if (!s) s = "original"; + values.push(s); + var k = handler.get_keyboard_mode(); + values.push(k); + var c = handler.get_option("codec-preference"); + if (!c) c = "auto"; + values.push(c); + for (var el in $$(menu#display-options li)) { + el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); + } + for (var el in $$(menu#keyboard-options>li)) { + el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); + } + for (var id in ["show-remote-cursor", "follow-remote-cursor", "follow-remote-window", "show-quality-monitor", "disable-audio", "enable-file-copy-paste", "disable-clipboard", "lock-after-session-end", "allow_swap_key", "i444"]) { + var el = self.select('#' + id); + if (el) { + var value = handler.get_toggle_option(id); + el.attributes.toggleClass("selected", value); + } + } +} + +if (is_osx) { + $(header).content(
    ); + $(header).attributes["role"] = "window-caption"; +} else { + if (is_file_transfer || is_port_forward) { + $(caption).content(
    ); + } else { + $(div.window-toolbar).content(
    ); + } + setWindowButontsAndIcon(); +} + +if (!(is_file_transfer || is_port_forward)) { + $(header).style.set { + height: "32px", + }; + if (!is_osx) { + $(div.window-icon).style.set { + size: "32px", + }; + } +} + +handler.updatePi = function(v) { + pi = v; + recording = handler.is_recording(); + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + +handler.updateDisplays = function(v) { + pi.displays = v; + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + +handler.setMultipleWindowsSession = function(sessions) { + // It will be covered by other message box if the timer is not used, + self.timer(1000ms, function() { + msgbox("multiple-sessions-nocancel", translate("Multiple Windows sessions found"), , "", function(res) { + if (res && res.sid) { + handler.set_selected_windows_session_id("" + res.sid); + } + }, 230); + }); +} + +handler.setCurrentDisplay = function(v) { + pi.current_display = v; + handler.switch_display(v); + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + +handler.screenshot = function(msg) { + if (msg) { + msgbox( + "custom-nocancel-nook-hasclose-error", + translate("Take screenshot"), + msg, + "", + function() {} + ); + } else { + msgbox( + "custom-take-screenshot-nocancel-nook", + translate("Take screenshot"), + translate("screenshot-action-tip"), + "", + function() {} + ); + } +} + +function updatePrivacyMode() { + var el = $(li#privacy-mode); + if (el) { + var supported = handler.is_privacy_mode_supported(); + if (!supported) { + // el.attributes.toggleClass("line-through", true); + el.style["display"]="none"; + } else { + var value = handler.get_toggle_option("privacy-mode"); + el.attributes.toggleClass("selected", value); + var el = $(li#block-input); + if (el) { + el.state.disabled = value; + } + } + } +} +handler.updatePrivacyMode = updatePrivacyMode; + +function togglePrivacyMode(privacy_id) { + var supported = handler.is_privacy_mode_supported(); + if (!supported) { + msgbox("nocancel", translate("Privacy mode"), translate("Unsupported"), "", function() { }); + } else { + var privacy_mode_impls = pi.platform_additions?.supported_privacy_mode_impl; + if (privacy_mode_impls == null || privacy_mode_impls == undefined) { + handler.toggle_option(privacy_id); + return; + } + var is_on = handler.get_toggle_option("privacy-mode"); + handler.toggle_privacy_mode("", !is_on); + } +} + +function toggleQualityMonitor(name) { + var show = handler.get_toggle_option(name); + if (show) { + $(#quality-monitor).style.set{ display: "none" }; + } else { + $(#quality-monitor).style.set{ display: "block" }; + } + handler.toggle_option(name); + toggleMenuState(); +} + +function toggleI444(name) { + handler.toggle_option(name); + handler.update_supported_decodings(); + toggleMenuState(); +} + +handler.updateBlockInputState = function(input_blocked) { + if (!input_blocked) { + handler.toggle_option("block-input"); + input_blocked = true; + $(#block-input).text = translate("Unblock user input"); + } else { + handler.toggle_option("unblock-input"); + input_blocked = false; + $(#block-input).text = translate("Block user input"); + } +} + +handler.switchDisplay = function(i) { + pi.current_display = i; + header.update(); +} + +function updateWindowToolbarPosition() { + if (is_osx) return; + self.timer(1ms, function() { + var el = $(div.window-toolbar); + var w1 = el.box(#width, #border); + var w2 = $(header).box(#width, #border); + var x = (w2 - w1) / 2 / scaleFactor; + el.style.set { + left: x + "px", + display: "block", + }; + }); +} + +view.on("size", function() { + // ensure size is done, so add timer + self.timer(1ms, function() { + updateWindowToolbarPosition(); + adaptDisplay(); + }); +}); + +handler.newMessage = function(text) { + chat_msgs.push({text: text, name: pi.username || "", time: getNowStr()}); + startChat(); +} + +function sendMsg(text) { + chat_msgs.push({text: text, name: "me", time: getNowStr()}); + handler.send_chat(text); + if (chatbox) chatbox.refresh(); +} + +var chatbox; +function startChat() { + if (chatbox) { + chatbox.windowState = View.WINDOW_SHOWN; + chatbox.refresh(); + return; + } + var icon = handler.get_icon(); + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + var w = scaleIt(300); + var h = scaleIt(400); + var x = (sx + sw - w) / 2; + var y = sy + scaleIt(80); + var params = { + type: View.FRAME_WINDOW, + x: x, + y: y, + width: w, + height: h, + client: true, + parameters: { msgs: chat_msgs, callback: sendMsg, icon: icon }, + caption: get_id(), + }; + var html = handler.get_chatbox(); + if (html) params.html = html; + else params.url = self.url("chatbox.html"); + chatbox = view.window(params); +} + +handler.setConnectionType = function(secured, direct, stream_type) { + header.update({ + secure_connection: secured, + direct_connection: direct, + stream_type: stream_type, + }); +} + +handler.updateRecordStatus = function(status) { + recording = status; + header.update(); +} diff --git a/vendor/rustdesk/src/ui/index.css b/vendor/rustdesk/src/ui/index.css new file mode 100644 index 0000000..d23e4f0 --- /dev/null +++ b/vendor/rustdesk/src/ui/index.css @@ -0,0 +1,441 @@ +html { + background-color: transparent; +} + +body { + overflow: hidden; +} + +@media platform != "OSX" { + body { + border-top: color(border) solid 1px; + } +} + +.title { + font-size: 1.4em; +} + +.app { + flow: horizontal; + size: *; +} + +.lighter-text { + color: color(lighter-text); + font-size: 0.9em; +} + +.left-pane { + width: 200px; + height: *; + background: color(bg); + border-right: color(border) 1px solid; + position: relative; +} + +#ab .left-pane { + border-radius: 1em; + padding: 1em; +} + +#ab .right-pane { + background: none; +} + +#ab .right-content { + overflow: unset; +} + +.left-pane > div:nth-child(1) { + border-spacing: 1em; + padding: 20px; + padding-bottom: 60px; /* reserve space for bottom connect-status */ +} + +.left-pane > div.connect-status { + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + +.left-pane div { + word-wrap: break-word; +} + +div.sessions-bar { + color: color(light-text); + padding-top: 0.5em; + border-top: color(border) solid 1px; + margin-bottom: 1em; + position: relative; + flow: horizontal; +} + +div.sessions-tab span { + display: inline-block; + padding: 6px 8px; + cursor: pointer; + @ELLIPSIS; +} + +div.sessions-tab svg { + size: 14px; +} + +div.sessions-tab span.active { + cursor: default; + border-radius: 3px; + background: color(bg); + color: color(text); +} + +div.search-id { + width: 120px; + padding: 0; + position: relative; + display: inline-block; +} + +div.search-id input { + font-size: 1em; + height: 20px; + border: none; + padding-left: 26px; +} + +div.search-id span { + position: absolute; + top: 0px; + padding: 6px; + color: color(border); +} + +div.search-id svg { + size: 14px; +} + +span.search-icon { + left: 0px; +} + +span.clear-input { + display: none; + right: 0px; +} + +div.search-id:hover span.clear-input { + display: inline-block; +} + +span.clear-input:hover { + color: black; +} + +.your-desktop { + border-spacing: 0.5em; + border-left: color(accent) solid 2px; + padding-left: 0.5em; +} + +.your-desktop input[type=text] { + padding: 0; + border: none; + height: 1.5em; +} + +.your-desktop > div { + color: color(light-text); +} + +.right-pane { + size: *; + background: color(gray-bg); +} + +.right-content { + overflow: scroll-indicator; + padding: 1.6em; + border-spacing: 1.6em; + size: *; + flow: vertical; +} + +@media platform == "OSX" { + .right-pane { + background: color(gray-bg-osx); + } +} + +@mixin CARD { + padding: 1.6em; + border-spacing: 1em; + background: color(bg); + border-radius: 1em; +} + +.card-connect { + @CARD; + width: 320px; +} + +.right-buttons { + text-align: right; +} + +.right-buttons>button { + margin-left: 1.6em; +} + +div.connect-status { + left: 240px; + border-top: color(border) solid 1px; + width: 100%; + padding: 1em; +} + +div.connect-status > span.connect-status-icon { + border-radius: 4px; + width: 8px; + height: 8px; + display: inline-block; + margin-right: 1em; +} + +div.connect-status > span.link { + margin-left: 1em; + display: inline-block; +} + +span.connect-status-1 { + background: #e04f5f; +} + +span.connect-status1 { + background: #32bea6; +} + +span.connect-status0 { + background: #F5853B; +} + +div.recent-sessions-content { + border-spacing: 1em; + flow: horizontal-flow; +} + +div.remote-session { + border-radius: 1em; + height: 140px; + width: 220px; + padding: 0; + position: relative; + border: none; +} + +div.remote-session:hover, div.remote-session-list:hover { + outline: color(button) solid 2px -2px; +} + +div.remote-session .platform { + width: *; + height: 120px; + padding: *; + position: relative; +} + +div.remote-session .platform .username{ + left: 0; + color: #eee; + position: absolute; + bottom: 38px; + font-size: 0.8em; + width: 220px; + text-align: center; +} + +div.remote-session .platform svg { + width: 60px; + height: 60px; + background: none; +} + +div.remote-session-list { + background: color(bg); + width: 220px; + flow: horizontal; +} + +div.remote-session-list .platform { + size: 42px; +} + +div.remote-session-list .platform svg { + width: 30px; + height: 30px; + background: none; + padding: 6px; +} + +div.remote-session-list .name { + size: *; + padding-left: 1em; +} + +div.remote-session-list .name >div { + margin-top: *; + margin-bottom: *; + width: *; +} + +div.remote-session-list .name .username { + margin-top: 3px; + font-size: 0.8em; + color: color(lighter-text); +} + +div.remote-session .text { + background: color(bg); + position: absolute; + height: 3em; + width: 100%; + border-radius: 0 0 1em 1em; + bottom: 0; + flow: horizontal; +} + +div.remote-session .text > div { + padding-top: 1em; + padding-left: 1em; + width: *; +} + +svg#menu { + size: 1em; + background: none; + padding: 0.5em; + margin: 0.5em; + color: color(light-text); +} + +.remote-session-list svg#menu { + margin-right: 0; +} + +svg#menu:hover { + color: color(text); + border-radius: 1em; + background: color(gray-bg); +} + +svg#edit:hover { + color: color(text); +} + +svg#edit { + display: inline-block; +} + +div.install-me, div.trust-me { + margin-top: 0.5em; + padding: 20px; + color: white; + background: linear-gradient(left,#e242bc,#f4727c); +} + +div.trust-me > div:nth-child(1), +div.install-me > div:nth-child(1) { + font-size: 1.2em; + font-weight: bold; + text-align: center; + margin-bottom: 0.5em; +} + +div.install-me > div:nth-child(2) { + line-height: 1.4em; +} + +#install-me.link { + margin-top: 0.5em; +} + +div.trust-me > div:nth-child(2) { + font-size: 0.9em; + margin-bottom: 1em; +} + +div.install-me > div:nth-child(3), +div.trust-me > div:nth-child(3) { + text-align: center; + font-size: 1.5em; + font-weight: bold; +} + +div.trust-me > div:nth-child(4), +div.trust-me > div:nth-child(5) { + margin-top: 0.5em; + text-align: center; +} + +div#myid, div#tags-label { + position: relative; +} + +div#myid svg#menu, div#tags-label svg#menu { + position: absolute; + right: -1em; +} + +div#tags-label svg#menu:hover { + background-color: #ddd; +} + +div.remote-session svg#menu { + position: absolute; + right: 0; + top: 0; +} + +.install-me .button { + height: 2em; + line-height: 2em; + text-align: center; + font-weight: bold; + font-size: 1em; + margin-top: 1em; + border-color: white; + border: 1px; + background: none; + color: white; +} + +svg#refresh-password { + display: inline-block; + stroke:#ddd; +} + +svg#refresh-password:hover { + stroke:color(text); +} + +li:disabled, li:disabled:hover { + color: color(lighter-text); + background: color(menu); + opacity: 0.8; +} + +.grey-text { + color: #888 !important; +} + +input.grey-text, +textarea.grey-text { + color: #888 !important; +} + +@media platform == "OSX" { + div.eye-area > input { + font-size: 1em; + } +} \ No newline at end of file diff --git a/vendor/rustdesk/src/ui/index.html b/vendor/rustdesk/src/ui/index.html new file mode 100644 index 0000000..88c1722 --- /dev/null +++ b/vendor/rustdesk/src/ui/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/vendor/rustdesk/src/ui/index.tis b/vendor/rustdesk/src/ui/index.tis new file mode 100644 index 0000000..f3840c4 --- /dev/null +++ b/vendor/rustdesk/src/ui/index.tis @@ -0,0 +1,1681 @@ +if (is_osx) view.windowBlurbehind = #light; +stdout.println("current platform:", OS); +stdout.println("is_xfce: ", is_xfce); + +// See default height in common.tis `msgbox()`. +const msgbox_default_height = 180; +const incoming_only_width = 180; + +const outgoing_only = handler.is_outgoing_only(); +const incoming_only = handler.is_incoming_only(); +const disable_installation = handler.is_disable_installation(); +const disable_account = handler.is_disable_account(); +const disable_settings = handler.is_disable_settings(); +const is_custom_client = handler.is_custom_client(); +const disable_ab = handler.is_disable_ab(); +const hide_server_settings = handler.get_builtin_option("hide-server-settings") == "Y"; +const hide_proxy_settings = handler.get_builtin_option("hide-proxy-settings") == "Y"; +const hide_websocket_settings = handler.get_builtin_option("hide-websocket-settings") == "Y"; +const hide_stop_service = handler.get_builtin_option("hide-stop-service") == "Y"; +const disable_change_permanent_password = handler.get_builtin_option("disable-change-permanent-password") == "Y"; +const disable_change_id = handler.get_builtin_option("disable-change-id") == "Y"; + +// html min-width, min-height not working on mac, below works for all +if (incoming_only) { + view.windowMinSize = (scaleIt(incoming_only_width), scaleIt((handler.is_installed() || disable_installation) ? 300 : 390)); +} else { + view.windowMinSize = (scaleIt(560), scaleIt(300)); +} + +var app; +var tmp = handler.get_connect_status(); +var connect_status = tmp[0]; +var service_stopped = handler.get_option("stop-service") == "Y"; +var disable_udp = handler.get_option("disable-udp") == "Y"; +var using_public_server = handler.using_public_server(); +var software_update_url = ""; +var key_confirmed = tmp[1]; +var system_error = ""; + +const default_option_lang = is_custom_client ? 'default' : ''; +const default_option_yes = is_custom_client ? 'Y' : ''; +const default_option_no = is_custom_client ? 'N' : ''; +const default_option_whitelist = is_custom_client ? ',' : ''; +const default_option_approve_mode = is_custom_client ? 'password-click' : ''; + +const grey_text_style = "color:#888;"; + +var svg_menu = + + + +; +var svg_refresh_password = ; + +var my_id = handler.get_id(); +function get_id() { + my_id = handler.get_id(); + return my_id; +} + +function get_msgbox_width(width=500) { + if (incoming_only) { + var maxw = scaleIt(incoming_only_width); + if (width > maxw) width = maxw; + } + return width; +} + +class ConnectStatus: Reactor.Component { + function render() { + return +
    + + {this.getConnectStatusStr()} + {service_stopped ? {translate('Start service')} : ""} +
    ; + } + + function getConnectStatusStr() { + if (service_stopped) { + return translate("Service is not running"); + } else if (connect_status == -1) { + return translate('not_ready_status'); + } else if (connect_status == 0) { + return translate('connecting_status'); + } + if (!handler.using_public_server()) return translate('Ready'); + return {translate("Ready")}, {translate("setup_server_tip")}; + } + + event click $(#start-service) () { + handler.set_option("stop-service", ""); + } + + event click $(#setup-server) () { + handler.open_url("https://cstudio.ch/hello-agent/blog/id-relay-set/"); + } +} + +function createNewConnect(id, type) { + id = id.replace(/\s/g, ""); + app.remote_id.value = formatId(id); + if (!id) return; + var old_id = id; + id = handler.handle_relay_id(id); + var force_relay = old_id != id; + if (id == my_id) { + msgbox("custom-error", "Error", "You cannot connect to your own computer"); + return; + } + handler.set_remote_id(id); + handler.new_remote(id, type, force_relay); +} + +class ShareRdp: Reactor.Component { + function render() { + var rdp_shared_string = translate("Enable RDP session sharing"); + var cls = handler.is_share_rdp() ? "selected" : "line-through"; + return
  • {svg_checkmark}{rdp_shared_string}
  • ; + } + + function onClick() { + handler.set_share_rdp(!handler.is_share_rdp()); + this.update(); + } +} + +var direct_server; +class DirectServer: Reactor.Component { + function this() { + direct_server = this; + } + + function render() { + var text = translate("Enable direct IP access"); + var enabled = handler.get_option("direct-server") == "Y"; + var cls = enabled ? "selected" : "line-through"; + return
  • {svg_checkmark}{text}{enabled && }
  • ; + } + + function onClick() { + if (is_edit_rdp_port) { + is_edit_rdp_port = false; + return; + } + handler.set_option("direct-server", handler.get_option("direct-server") == "Y" ? default_option_no : "Y"); + this.update(); + } +} + +var myIdMenu; +var audioInputMenu; +class AudioInputs: Reactor.Component { + function this() { + audioInputMenu = this; + } + + function render() { + if (!this.show) return
  • ; + var inputs = handler.get_sound_inputs(); + if (is_win) inputs = ["System Sound"].concat(inputs); + if (!inputs.length) return
  • ; + var me = this; + self.timer(1ms, function() { me.toggleMenuState() }); + return
  • {translate('Audio Input')} + +
  • {svg_checkmark}{translate("Mute")}
  • +
    + {inputs.map(function(name) { + return
  • {svg_checkmark}{translate(name)}
  • ; + })} +
    +
  • ; + } + + function get_default() { + if (is_win) return "System Sound"; + return ""; + } + + function get_value() { + return handler.get_option("audio-input") || this.get_default(); + } + + function toggleMenuState() { + var el = this.$(li#enable-audio); + var enabled = handler.get_option(el.id) != "N"; + el.attributes.toggleClass("selected", !enabled); + var is_opt_fixed = handler.is_option_fixed("enable-audio"); + if (disable_settings || is_opt_fixed) { + el.state.disabled = true; + } + var v = this.get_value(); + for (var el in this.$$(menu#audio-input>li)) { + if (el.id == 'enable-audio') continue; + var selected = el.id == v; + el.attributes.toggleClass("selected", selected); + } + } + + event click $(menu#audio-input>li) (_, me) { + if (me.state.disabled) return; + var v = me.id; + if (v == 'enable-audio') { + handler.set_option(v, handler.get_option(v) != 'N' ? 'N' : default_option_yes); + } else { + if (v == this.get_value()) return; + if (v == this.get_default()) v = ""; + handler.set_option("audio-input", v); + } + this.toggleMenuState(); + } +}; + +class Languages: Reactor.Component { + function render() { + var langs = JSON.parse(handler.get_langs()); + var me = this; + self.timer(1ms, function() { me.toggleMenuState() }); + return
  • {translate('Language')} + +
  • {svg_checkmark}Default
  • +
    + {langs.map(function(lang) { + return
  • {svg_checkmark}{lang[1]}
  • ; + })} +
    +
  • ; + } + + + function toggleMenuState() { + var cur = handler.get_local_option("lang") || "default"; + var is_opt_fixed = handler.is_option_fixed("lang"); + for (var el in this.$$(menu#languages>li)) { + var selected = cur == el.id; + el.attributes.toggleClass("selected", selected); + if (is_opt_fixed) { + el.state.disabled = true; + } + } + } + + event click $(menu#languages>li) (_, me) { + if (me.state.disabled) return; + var v = me.id; + if (v == "default") v = default_option_lang; + handler.set_local_option("lang", v); + app.update(); + this.toggleMenuState(); + } +} + +var enhancementsMenu; +class Enhancements: Reactor.Component { + function this() { + enhancementsMenu = this; + } + + function render() { + var has_hwcodec = handler.has_hwcodec(); + var has_vram = handler.has_vram(); + var support_remove_wallpaper = handler.support_remove_wallpaper(); + var me = this; + self.timer(1ms, function() { me.toggleMenuState() }); + return
  • {translate('Enhancements')} + + {(has_hwcodec || has_vram) ?
  • {svg_checkmark}{translate("Enable hardware codec")}
  • : ""} +
  • {svg_checkmark}{translate("Adaptive bitrate")} (beta)
  • +
  • {translate("Recording")}
  • + {support_remove_wallpaper ?
  • {svg_checkmark}{translate("Remove wallpaper during incoming sessions")}
  • : ""} +
  • {svg_checkmark}{translate("keep-awake-during-incoming-sessions-label")}
  • +
    +
  • ; + } + + function toggleMenuState() { + for (var el in $$(menu#enhancements-menu>li)) { + if (el.id && el.id.indexOf("enable-") == 0) { + var enabled = handler.get_option(el.id) != "N"; + el.attributes.toggleClass("selected", enabled); + var is_opt_fixed = handler.is_option_fixed(el.id); + if (is_opt_fixed) { + el.state.disabled = true; + } + } else if (el.id && el.id.indexOf("allow-") == 0) { + var enabled = handler.get_option(el.id) == "Y"; + el.attributes.toggleClass("selected", enabled); + var is_opt_fixed = handler.is_option_fixed(el.id); + if (is_opt_fixed) { + el.state.disabled = true; + } + } else if (el.id == "keep-awake-during-incoming-sessions") { + var enabled = handler.get_option(el.id) != "N"; + el.attributes.toggleClass("selected", enabled); + var is_opt_fixed = handler.is_option_fixed(el.id); + if (is_opt_fixed) { + el.state.disabled = true; + } + } + } + + } + + event click $(menu#enhancements-menu>li) (_, me) { + if (me.state.disabled) return; + var v = me.id; + if (v.indexOf("enable-") == 0) { + var set_value = handler.get_option(v) != 'N' ? 'N' : default_option_yes; + handler.set_option(v, set_value); + if (v == "enable-hwcodec" && set_value != 'N') { + handler.check_hwcodec(); + } + } else if (v.indexOf("allow-") == 0) { + handler.set_option(v, handler.get_option(v) == 'Y' ? default_option_no : 'Y'); + } else if (v == 'keep-awake-during-incoming-sessions') { + handler.set_option(v, handler.get_option(v) != 'N' ? 'N' : default_option_yes); + } else if (v == 'screen-recording') { + var show_root_dir = is_win && handler.is_installed(); + var user_dir = handler.video_save_directory(false); + var root_dir = show_root_dir ? handler.video_save_directory(true) : ""; + var ts0 = handler.get_option("enable-record-session") != 'N' ? { checked: true } : {}; + var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {}; + var ts2 = handler.get_local_option("allow-auto-record-outgoing") == 'Y' ? { checked: true } : {}; + var is_opt_fixed_enable_record = handler.is_option_fixed("enable-record-session"); + var is_opt_fixed_auto_incoming = handler.is_option_fixed("allow-auto-record-incoming"); + var is_opt_fixed_auto_outgoing = handler.is_option_fixed("allow-auto-record-outgoing"); + var is_opt_fixed_video_dir = handler.is_option_fixed("video-save-directory"); + if (is_opt_fixed_enable_record) { ts0.disabled = true; ts0.style = grey_text_style; } + if (is_opt_fixed_auto_incoming) { ts1.disabled = true; ts1.style = grey_text_style; } + if (is_opt_fixed_auto_outgoing) { ts2.disabled = true; ts2.style = grey_text_style; } + msgbox("custom-recording", translate('Recording'), +
    +
    {translate('Enable recording session')}
    +
    {translate('Automatically record incoming sessions')}
    +
    {translate('Automatically record outgoing sessions')}
    +
    + {show_root_dir ?
    {translate("Incoming")}:  {root_dir}
    : ""} +
    {translate(show_root_dir ? "Outgoing" : "Directory")}:  {user_dir}
    + {is_opt_fixed_video_dir ? "" :
    } +
    +
    + , "", function(res=null) { + if (!res) return; + if (!is_opt_fixed_enable_record) handler.set_option("enable-record-session", res.enable_record_session ? default_option_yes : 'N'); + if (!is_opt_fixed_auto_incoming) handler.set_option("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : default_option_no); + if (!is_opt_fixed_auto_outgoing) handler.set_local_option("allow-auto-record-outgoing", res.auto_record_outgoing ? 'Y' : default_option_no); + if (!is_opt_fixed_video_dir) handler.set_local_option("video-save-directory", $(#folderPath).text); + }, msgbox_default_height, get_msgbox_width()); + } + this.toggleMenuState(); + } +} + +function getUserName() { + try { + return JSON.parse(handler.get_local_option("user_info")).name; + } catch(e) {} + return ''; +} + +function getAccountLabelWithHandle() { + try { + var user = JSON.parse(handler.get_local_option("user_info")); + var username = (user.name || '').trim(); + if (!username) { + return ''; + } + var displayName = (user.display_name || '').trim(); + if (!displayName || displayName == username) { + return username; + } + return displayName + " (@" + username + ")"; + } catch(e) {} + return ''; +} + +// Shared dialog functions +function open_custom_server_dialog() { + var configOptions = handler.get_options(); + var old_relay = configOptions["relay-server"] || ""; + var old_api = configOptions["api-server"] || ""; + var old_id = configOptions["custom-rendezvous-server"] || ""; + var old_key = configOptions["key"] || ""; + msgbox("custom-server", "ID/Relay Server", "
    \ +
    " + translate("ID Server") + ":
    \ +
    " + translate("Relay Server") + ":
    \ +
    " + translate("API Server") + ":
    \ +
    " + translate("Key") + ":
    \ +
    \ + ", "", function(res=null, show_progress) { + if (!res) return; + if (typeof show_progress === 'function') show_progress(); + var id = (res.id || "").trim(); + var relay = (res.relay || "").trim(); + var api = (res.api || "").trim().toLowerCase(); + var key = (res.key || "").trim(); + if (id == old_id && relay == old_relay && key == old_key && api == old_api) return; + if (id) { + var err = handler.test_if_valid_server(id, true); + if (err) { if (typeof show_progress === 'function') show_progress(false, translate("ID Server") + ": " + err); return; } + } + if (relay) { + var err = handler.test_if_valid_server(relay, true); + if (err) { if (typeof show_progress === 'function') show_progress(false, translate("Relay Server") + ": " + err); return; } + } + if (api) { + if (0 != api.indexOf("https://") && 0 != api.indexOf("http://")) { + if (typeof show_progress === 'function') show_progress(false, translate("API Server") + ": " + translate("invalid_http")); + return; + } + } + configOptions["custom-rendezvous-server"] = id; + configOptions["relay-server"] = relay; + configOptions["api-server"] = api; + configOptions["key"] = key; + handler.set_options(configOptions); + if (typeof show_progress === 'function') show_progress(-1); + }, 260, get_msgbox_width()); +} + +function open_whitelist_dialog() { + var is_opt_fixed = handler.is_option_fixed("whitelist"); + var v = handler.get_option("whitelist"); + var old_value = v == default_option_whitelist ? '' : v.split(",").join("\n"); + var type_str = is_opt_fixed ? "custom-whitelist-nook" : "custom-whitelist"; + var readonly_attr = is_opt_fixed ? " readonly=\"readonly\"" : ""; + var grey_class = is_opt_fixed ? " class=\"grey-text\"" : ""; + msgbox(type_str, translate("IP Whitelisting"), "
    \ + " + translate("whitelist_sep") + "
    \ + \ +
    \ + ", "", function(res=null, show_progress) { + if (!res) return; + if (typeof show_progress === 'function') show_progress(); + var value = (res.text || "").trim(); + if (value) { + var values = value.split(/[\s,;\n]+/g); + for (var ip in values) { + if (!ip.match(/^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$/) + && !ip.match(/^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$/)) { + if (typeof show_progress === 'function') show_progress(false, translate("Invalid IP") + ": " + ip); + return; + } + } + value = values.join("\n"); + } + if (value == old_value) return; + if (!value) value = default_option_whitelist; + handler.set_option("whitelist", value.replace("\n", ",")); + if (typeof show_progress === 'function') show_progress(-1); + }, 300, get_msgbox_width()); +} + +function open_proxy_dialog() { + var is_opt_fixed = handler.is_option_fixed("proxy-url"); + var socks5 = handler.get_socks() || {}; + var old_proxy = socks5[0] || ""; + var old_username = socks5[1] || ""; + var old_password = socks5[2] || ""; + var type_str = is_opt_fixed ? "custom-server-nook" : "custom-server"; + var greyStyle = is_opt_fixed ? grey_text_style : ""; + msgbox(type_str, "Socks5/Http(s) Proxy",
    +
    {translate("Server")}:
    +
    {translate("Username")}:
    +
    {translate("Password")}:{ is_opt_fixed ? : }
    +
    + , "", function(res=null, show_progress) { + if (!res) return; + if (typeof show_progress === 'function') show_progress(); + var proxy = (res.proxy || "").trim(); + var username = (res.username || "").trim(); + var password = (res.password || "").trim(); + if (proxy == old_proxy && username == old_username && password == old_password) return; + if (proxy) { + var domain_port = proxy; + var protocol_index = domain_port.indexOf('://'); + if (protocol_index !== -1) { + domain_port = domain_port.substring(protocol_index + 3); + } + var err = handler.test_if_valid_server(domain_port, false); + if (err) { if (typeof show_progress === 'function') show_progress(false, translate("Server") + ": " + err); return; } + } + handler.set_socks(proxy, username, password); + if (typeof show_progress === 'function') show_progress(-1); + }, 240, get_msgbox_width()); +} + +function updateTheme() { + var root_element = self; + if (handler.get_option("allow-darktheme") == "Y") { + // enable dark theme + root_element.attributes.toggleClass("darktheme", true); + } else { + // disable dark theme + root_element.attributes.toggleClass("darktheme", false); + } +} + +class MyIdMenu: Reactor.Component { + function this() { + myIdMenu = this; + } + + function render() { + return
    + {this.renderPop()} + ID{svg_menu} +
    ; + } + + function renderPop() { + var accountLabel = handler.get_local_option("access_token") ? getAccountLabelWithHandle() : ''; + return + + {!disable_settings &&
  • {svg_checkmark}{translate('Enable keyboard/mouse')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable clipboard')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable file transfer')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable camera')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable terminal')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable remote restart')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable TCP tunneling')}
  • } + {!disable_settings && is_win ?
  • {svg_checkmark}{translate('Enable blocking user input')}
  • : ""} + {!disable_settings && (handler.get_supported_privacy_mode_impls() != '[]') &&
  • {svg_checkmark}{translate('Enable privacy mode')}
  • } + {!disable_settings &&
  • {svg_checkmark}{translate('Enable LAN discovery')}
  • } + + + {!disable_settings &&
  • {svg_checkmark}{translate('Enable remote configuration modification')}
  • } + {!disable_settings &&
    } + {!disable_settings && !hide_server_settings &&
  • {translate('ID/Relay Server')}
  • } + {!disable_settings &&
  • {translate('IP Whitelisting')}
  • } + {!disable_settings && !hide_proxy_settings &&
  • {translate('Socks5/Http(s) Proxy')}
  • } + {!disable_settings && !hide_websocket_settings &&
  • {svg_checkmark}{translate('Use WebSocket')}
  • } + {!disable_settings && !using_public_server && !outgoing_only &&
  • {svg_checkmark}{translate('Disable UDP')}
  • } + {!disable_settings && !using_public_server &&
  • {svg_checkmark}{translate('Allow insecure TLS fallback')}
  • } +
    + {(!hide_stop_service || service_stopped) &&
  • {svg_checkmark}{translate("Enable service")}
  • } + {!disable_settings && is_win && handler.is_installed() ? : ""} + {!disable_settings && } + {!disable_settings && false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } + {!disable_change_id && handler.is_ok_change_id() ?
    : ""} + {!disable_account && (accountLabel ? +
  • {translate('Logout')} ({accountLabel})
  • : +
  • {translate('Login')}
  • )} + {!disable_change_id && !disable_settings && handler.is_ok_change_id() && key_confirmed && connect_status > 0 ?
  • {translate('Change ID')}
  • : ""} +
    +
  • {svg_checkmark}{translate('Dark Theme')}
  • + + {disable_installation ? "" :
  • {svg_checkmark}{translate('Auto update')}
  • } +
  • {translate('About')} {" "}{handler.get_app_name()}
  • + + ; + } + + event click $(svg#menu) (_, me) { + this.showSettingMenu(); + } + + function showSettingMenu() { + audioInputMenu.update({ show: true }); + this.toggleMenuState(); + if (direct_server) direct_server.update(); + var menu = this.$(menu#config-options); + this.$(svg#menu).popup(menu); + } + + event click $(li#login) () { + login(); + } + + event click $(li#logout) () { + logout(); + } + + function toggleMenuState() { + for (var el in $$(menu#config-options>li)) { + var id = el.id; + if (!id) continue; + var is_opt_fixed = handler.is_option_fixed(id); + if (id.indexOf("enable-") == 0) { + var enabled = handler.get_option(id) != "N"; + el.attributes.toggleClass("selected", enabled); + el.attributes.toggleClass("line-through", !enabled); + } else if (id.indexOf("allow-") == 0) { + var enabled = handler.get_option(id) == "Y"; + el.attributes.toggleClass("selected", enabled); + el.attributes.toggleClass("line-through", !enabled); + } else if (id == "whitelist") { + // whitelist should be clickable even when fixed (to view the content) + // The dialog will show readonly textarea and no OK button when fixed + continue; + } + if (is_opt_fixed) { + el.state.disabled = true; + } + } + } + + function showAbout() { + var name = handler.get_app_name(); + msgbox("custom-nocancel-nook-hasclose", translate("About") + " " + name, "
    \ +
    Version: " + handler.get_version() + " \ +
    Fingerprint: " + handler.get_fingerprint() + " \ +
    " + translate("Privacy Statement") + "
    \ +
    " + translate("Website") + "
    \ +
    Copyright © 2025 cStudio GmbH.\ +
    " + handler.get_license() + " \ +

    " + translate("Slogan_tip") + "

    \ +
    \ +
    ", "", function(el) { + if (el && el.attributes) { + handler.open_url(el.attributes['url']); + }; + }, 400, get_msgbox_width()); + } + + event click $(menu#config-options>li) (_, me) { + if (me.state.disabled) return; + if (me.id && me.id.indexOf("enable-") == 0) { + handler.set_option(me.id, handler.get_option(me.id) == "N" ? default_option_yes : "N"); + } + if (me.id && me.id.indexOf("allow-") == 0) { + handler.set_option(me.id, handler.get_option(me.id) == "Y" ? default_option_no : "Y"); + } + if (me.id == "whitelist") { + open_whitelist_dialog(); + } else if (me.id == "custom-server") { + open_custom_server_dialog(); + } else if (me.id == "socks5-server") { + open_proxy_dialog(); + } else if (me.id == "disable-udp") { + handler.set_option("disable-udp", handler.get_option("disable-udp") == "Y" ? "N" : "Y"); + } else if (me.id == "stop-service") { + handler.set_option("stop-service", service_stopped ? default_option_no : "Y"); + } else if (me.id == "change-id") { + var id_label_width = incoming_only ? "50px" : "100px"; + var input_width = incoming_only ? (incoming_only_width - 20) + "px" : "250px"; + msgbox("custom-id", translate("Change ID"), "
    \ +
    " + translate('id_change_tip') + "
    \ +
    ID:
    \ +
    \ + ", "", function(res=null, show_progress) { + if (!res) return; + show_progress(); + var id = (res.id || "").trim(); + if (!id) return; + if (id == my_id) return; + handler.change_id(id); + function check_status() { + var status = handler.get_async_job_status(); + if (status == " ") self.timer(0.1s, check_status); + else { + if (status) show_progress(false, translate(status)); + else show_progress(-1); + } + } + check_status(); + return " "; + }, msgbox_default_height, get_msgbox_width()); + } else if (me.id == "allow-darktheme") { + updateTheme(); + } else if (me.id == "about") { + this.showAbout() + } + } +} + +var is_edit_direct_access_port; +class EditDirectAccessPort: Reactor.Component { + function render() { + return {svg_edit}; + } + + function onMouse(evt) { + if (evt.type == Event.MOUSE_DOWN) { + is_edit_direct_access_port = true; + editDirectAccessPort(); + } + } +} + +function editDirectAccessPort() { + var is_opt_fixed = handler.is_option_fixed("direct-access-port"); + var p0 = handler.get_option('direct-access-port'); + var greyStyle = is_opt_fixed ? grey_text_style : ""; + var port = p0 ? : + ; + var type_str = is_opt_fixed ? "custom-direct-access-port-nook" : "custom-direct-access-port"; + msgbox(type_str, translate('Direct IP Access Settings'),
    +
    {translate('Port')}:{port}
    +
    , "", function(res=null) { + if (!res) return; + var p = (res.port || '').trim(); + if (p) { + p = p.toInteger(); + if (!(p > 0)) { + return translate("Invalid port"); + } + p = p + ''; + } + if (p != p0) handler.set_option('direct-access-port', p); + }, msgbox_default_height, get_msgbox_width()); +} + +class App: Reactor.Component +{ + function this() { + app = this; + } + + function render() { + var is_can_screen_recording = handler.is_can_screen_recording(false); + return +
    +
    +
    + {is_custom_client && handler.get_builtin_option("hide-powered-by-me") != "Y" ?
    {translate('powered_by_me')}
    : ""} +
    + {translate('Your Desktop')} + {outgoing_only ? {svg_menu} : ""} +
    +
    {outgoing_only ? translate('outgoing_only_desk_tip') : translate('desk_tip')}
    + {outgoing_only ?
    : ""} + {!outgoing_only &&
    + + {key_confirmed ? : translate("Generating ...")} +
    } + {!outgoing_only && } +
    + {(!is_win || handler.is_installed() || disable_installation) ? "" : } + {software_update_url && !disable_installation ? : ""} + {is_win && handler.is_installed() && !software_update_url && handler.is_installed_lower_version() && !disable_installation ? : ""} + {is_can_screen_recording ? "": } + {is_can_screen_recording && !handler.is_process_trusted(false) ? : ""} + {!service_stopped && is_can_screen_recording && handler.is_process_trusted(false) && handler.is_installed() && !handler.is_installed_daemon(false) ? : ""} + {system_error ? : ""} + {!system_error && handler.is_login_wayland() && !handler.current_is_wayland() ? : ""} + {!system_error && handler.current_is_wayland() ? : ""} + {incoming_only ? : ""} +
    + {!incoming_only &&
    +
    +
    +
    {translate('Control Remote Desktop')}
    + +
    + + +
    +
    + +
    + {!outgoing_only ? : ""} +
    } +
    +
    ; + } + + event click $(button#connect) { + this.newRemote("connect"); + } + + event click $(button#file-transfer) { + this.newRemote("file-transfer"); + } + + function newRemote(type) { + createNewConnect(this.remote_id.value, type); + } +} + +class InstallMe: Reactor.Component { + function render() { + return
    + +
    {translate('install_tip')}
    +
    +
    ; + } + + event click $(#install-me) { + handler.goto_install(); + } +} + +function download(from, to, args..) { + var rqp = { type:#get, url: from, toFile: to }; + var fn = 0; + var on = 0; + for( var p in args ) { + if( p instanceof Function ) { + switch(++fn) { + case 1: rqp.success = p; break; + case 2: rqp.error = p; break; + case 3: rqp.progress = p; break; + } + } else if( p instanceof Object ) { + switch(++on) { + case 1: rqp.params = p; break; + case 2: rqp.headers = p; break; + } + } + } + view.request(rqp); +} + +// current running version is higher than installed +class UpgradeMe: Reactor.Component { + function render() { + var update_or_download = is_osx ? "download" : "update"; + return
    +
    {translate('Status')}
    +
    {translate('Your installation is lower version.')}
    +
    {translate('Click to upgrade')}
    +
    ; + } + + event click $(#install-me) { + handler.update_me(""); + } +} + +class UpdateMe: Reactor.Component { + function render() { + var update_or_download = "download"; // !is_win ? "download" : "update"; + return
    +
    {translate('Status')}
    +
    There is a newer version of {handler.get_app_name()} ({handler.get_new_version()}) available.
    + {is_custom_client + ?
    {translate('Enable \"Auto update\" or contact your administrator for the latest version.')}
    + :
    {translate('Click to ' + update_or_download)}
    } +
    +
    ; + } + + event click $(#install-me) { + handler.open_url("https://cstudio.ch/hello-agent/download"); + return; + if (!is_win) { + handler.open_url("https://cstudio.ch/hello-agent"); + return; + } + var url = software_update_url + '.' + handler.get_software_ext(); + var path = handler.get_software_store_path(); + var onsuccess = function(md5) { + $(#download-percent).content(translate("Installing ...")); + handler.update_me(path); + }; + var onerror = function(err) { + msgbox("custom-error", "Download Error", "Failed to download"); + }; + var onprogress = function(loaded, total) { + if (!total) total = 5 * 1024 * 1024; + var el = $(#download-percent); + el.style.set{display: "block"}; + el.content("Downloading %" + (loaded * 100 / total)); + }; + stdout.println("Downloading " + url + " to " + path); + download( + url, + self.url(path), + onsuccess, onerror, onprogress); + } +} + +class SystemError: Reactor.Component { + function render() { + return
    +
    {system_error}
    +
    ; + } +} + +class TrustMe: Reactor.Component { + function render() { + return
    +
    {translate('Permissions')}
    +
    {translate('config_acc')}
    +
    {translate('Configure')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#trust-me) { + handler.is_process_trusted(true); + watch_trust(); + } + + event click $(#help-me) { + handler.open_url(translate("doc_mac_permission")); + } +} + +class CanScreenRecording: Reactor.Component { + function render() { + return
    +
    {translate('Permissions')}
    +
    {translate('config_screen')}
    +
    {translate('Configure')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#screen-recording) { + handler.is_can_screen_recording(true); + watch_screen_recording(); + } + + event click $(#help-me) { + handler.open_url(translate("doc_mac_permission")); + } +} + +class InstallDaemon: Reactor.Component { + function render() { + return
    + +
    {translate('install_daemon_tip')}
    +
    {translate('Install')}
    +
    ; + } + + event click $(#install-me) { + handler.is_installed_daemon(true); + } +} + +class FixWayland: Reactor.Component { + function render() { + return
    +
    {translate('Warning')}
    +
    {translate('Login screen using Wayland is not supported')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#help-me) { + handler.open_url(translate("doc_fix_wayland")); + } +} + +class ModifyDefaultLogin: Reactor.Component { + function render() { + return
    +
    {translate('Warning')}
    +
    {translate('wayland_experiment_tip')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#help-me) { + handler.open_url(translate("doc_fix_wayland")); + } +} + +function watch_trust() { + // not use TrustMe::update, because it is buggy + var trusted = handler.is_process_trusted(false); + var el = $(div#trust-me-box); + if (el) { + el.style.set { + display: trusted ? "none" : "block", + }; + } + if (trusted) { + app.update(); + return; + } + self.timer(1s, watch_trust); +} + +function watch_screen_recording() { + var trusted = handler.is_can_screen_recording(false); + var el = $(div#screen-recording-box); + if (el) { + el.style.set { + display: trusted ? "none" : "block", + }; + } + if (trusted) { + app.update(); + return; + } + self.timer(1s, watch_screen_recording); +} + +class PasswordEyeArea : Reactor.Component { + render() { + var method = handler.get_option('verification-method'); + var mode= handler.get_option('approve-mode'); + var hide_one_time = mode == 'click' || method == 'use-permanent-password'; + var value = hide_one_time ? "-" : password_cache[0]; + return +
    + + {hide_one_time ? "" : svg_refresh_password} +
    ; + } + + event click $(svg#refresh-password) (_, me) { + handler.update_temporary_password(); + this.update(); + } +} + +var temporaryPasswordLengthMenu; +class TemporaryPasswordLengthMenu: Reactor.Component { + function this() { + temporaryPasswordLengthMenu = this; + } + + function render() { + if (!this.show) return
  • ; + var me = this; + var method = handler.get_option('verification-method'); + self.timer(1ms, function() { me.toggleMenuState() }); + return
  • {translate("One-time password length")} + +
  • {svg_checkmark}6
  • +
  • {svg_checkmark}8
  • +
  • {svg_checkmark}10
  • +
    +
  • ; + } + + function toggleMenuState() { + var is_opt_fixed = handler.is_option_fixed('temporary-password-length'); + var length = handler.get_option("temporary-password-length"); + var index = ['6', '8', '10'].indexOf(length); + if (index < 0) index = 0; + for (var (i, el) in this.$$(menu#temporary-password-length>li)) { + el.attributes.toggleClass("selected", i == index); + if (is_opt_fixed) { + el.state.disabled = true; + } + } + } + + event click $(menu#temporary-password-length>li) (_, me) { + if (me.state.disabled) return; + var length = me.id.substring('temporary-password-length-'.length); + var old_length = handler.get_option('temporary-password-length'); + if (length != old_length) { + handler.set_option('temporary-password-length', length); + handler.update_temporary_password(); + this.toggleMenuState(); + passwordArea.update(); + } + } +} + +var passwordArea; +class PasswordArea: Reactor.Component { + function this() { + passwordArea = this; + } + + function render() { + var me = this; + self.timer(1ms, function() { me.toggleMenuState() }); + return +
    +
    {translate('One-time Password')}
    +
    + {this.renderPop()} + + {!disable_settings && svg_edit} +
    +
    ; + } + + function renderPop() { + var method = handler.get_option('verification-method'); + var approve_mode= handler.get_option('approve-mode'); + var show_password = approve_mode != 'click'; + var has_local_password = handler.is_local_permanent_password_set(); + return +
  • {svg_checkmark}{translate('Accept sessions via password')}
  • +
  • {svg_checkmark}{translate('Accept sessions via click')}
  • +
  • {svg_checkmark}{translate('Accept sessions via both')}
  • + { !show_password ? '' :
    } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use one-time password')}
  • } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use permanent password')}
  • } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use both passwords')}
  • } + { !show_password ? '' :
    } + { !show_password || disable_change_permanent_password ? '' :
  • {translate('Set permanent password')}
  • } + { !show_password || disable_change_permanent_password ? '' :
  • {translate('Clear permanent password')}
  • } + { !show_password ? '' : } +
    +
  • {svg_checkmark}{translate('enable-2fa-title')}
  • + ; + } + + function toggleMenuState() { + var mode= handler.get_option('approve-mode'); + var mode_id; + if (mode == 'password') + mode_id = 'approve-mode-password'; + else if (mode == 'click') + mode_id = 'approve-mode-click'; + else + mode_id = 'approve-mode-both'; + var pwd_id = handler.get_option('verification-method'); + if (pwd_id != 'use-temporary-password' && pwd_id != 'use-permanent-password') + pwd_id = 'use-both-passwords'; + var has_valid_2fa = handler.has_valid_2fa(); + for (var el in this.$$(menu#edit-password-context>li)) { + if (el.id.indexOf("approve-mode-") == 0) { + el.attributes.toggleClass("selected", el.id == mode_id); + if (handler.is_option_fixed('approve-mode')) { + el.state.disabled = true; + } + } + if (el.id.indexOf("use-") == 0) { + el.attributes.toggleClass("selected", el.id == pwd_id); + if (handler.is_option_fixed('verification-method')) { + el.state.disabled = true; + } + } + if (el.id == "clear-password") { + var has_local_password = handler.is_local_permanent_password_set(); + el.state.disabled = !has_local_password; + } + if (el.id == "tfa") + el.attributes.toggleClass("selected", has_valid_2fa); + } + } + + event click $(svg#edit) (_, me) { + var approve_mode= handler.get_option('approve-mode'); + var show_password = approve_mode != 'click'; + if(show_password && temporaryPasswordLengthMenu) temporaryPasswordLengthMenu.update({show: true }); + var menu = $(menu#edit-password-context); + me.popup(menu); + } + + event click $(li#set-password) { + var me = this; + var has_local_password = handler.is_local_permanent_password_set(); + var permanent_password_set = handler.is_permanent_password_set(); + var password_hidden_tip = translate('password-hidden-tip'); + var preset_password_tip = translate('preset-password-in-use-tip'); + var password_tip = ""; + if (has_local_password) { + password_tip = "
    [!] " + password_hidden_tip + "
    "; + } else if (permanent_password_set) { + password_tip = "
    [!] " + preset_password_tip + "
    "; + } + msgbox("custom-password", translate("Set Password"), "
    \ +
    " + translate('Password') + ":
    \ +
    " + translate('Confirmation') + ":
    \ + " + password_tip + " \ +
    \ + ", "", function(res=null) { + if (!res) return; + var p0 = (res.password || "").trim(); + var p1 = (res.confirmation || "").trim(); + if (p0.length == 0 && p1.length == 0) { + return " "; + } + if (p0.length < 6 && p0.length != 0) { + return translate("Too short, at least 6 characters."); + } + if (p0 != p1) { + return translate("The confirmation is not identical."); + } + handler.set_permanent_password(p0); + me.update(); + }, msgbox_default_height, get_msgbox_width()); + self.timer(30ms, function() { + updateSetPasswordSubmitState(); + }); + } + + event click $(li#clear-password) { + if (this.$(li#clear-password).state.disabled) return; + handler.set_permanent_password(""); + this.update(); + } + + event click $(menu#edit-password-context>li) (_, me) { + if (me.state.disabled) return; + if (me.id.indexOf('use-') == 0) { + handler.set_option('verification-method', me.id); + this.toggleMenuState(); + passwordArea.update(); + } else if (me.id.indexOf('approve-mode') == 0) { + var approve_mode; + if (me.id == 'approve-mode-password') + approve_mode = 'password'; + else if (me.id == 'approve-mode-click') + approve_mode = 'click'; + else + approve_mode = default_option_approve_mode; + handler.set_option('approve-mode', approve_mode); + this.toggleMenuState(); + passwordArea.update(); + } + } + + event click $(li#tfa) { + var me = this; + var has_valid_2fa = handler.has_valid_2fa(); + if (has_valid_2fa) { + handler.set_option('2fa', ''); + me.update(); + } else { + var new2fa = handler.generate2fa(); + var src = handler.generate_2fa_img_src(new2fa); + msgbox("custom-2fa-setting", translate('enable-2fa-title'), +
    +
    {translate('enable-2fa-desc')}
    + +
    +
    + , "", function(res=null) { + if (!res) return; + if (!res.code) return; + if (!handler.verify2fa(res.code)) { + return translate('wrong-2fa-code'); + } + me.update(); + }, 400, get_msgbox_width()); + } + } +} + +var password_cache = ["","","",""]; +function updatePasswordArea() { + self.timer(1s, function() { + var temporary_password = handler.temporary_password(); + var verification_method = handler.get_option('verification-method'); + var temporary_password_length = handler.get_option('temporary-password-length'); + var approve_mode = handler.get_option('approve-mode'); + var update = false; + if (password_cache[0] != temporary_password) { + password_cache[0] = temporary_password; + update = true; + } + if (password_cache[1] != verification_method) { + password_cache[1] = verification_method; + update = true; + } + if (password_cache[2] != temporary_password_length) { + password_cache[2] = temporary_password_length; + update = true; + } + if (password_cache[3] != approve_mode) { + password_cache[3] = approve_mode; + update = true; + } + if (update && passwordArea) passwordArea.update(); + updatePasswordArea(); + }); +} +if (!outgoing_only) updatePasswordArea(); + +function updateSetPasswordSubmitState() { + var dialog = $(#msgbox); + if (!dialog) return; + var password = dialog.$(input[name='password']); + var confirmation = dialog.$(input[name='confirmation']); + var submit = dialog.$(button#submit); + if (!password || !confirmation || !submit) return; + var can_submit = (password.value || "").trim().length > 0 || + (confirmation.value || "").trim().length > 0; + submit.state.disabled = !can_submit; +} + +class ID: Reactor.Component { + function render() { + return ; + } + + // https://github.com/c-smile/sciter-sdk/blob/master/doc/content/sciter/Event.htm + event change { + var fid = formatId(this.value); + var d = this.value.length - (this.old_value || "").length; + this.old_value = this.value; + var start = this.xcall(#selectionStart) || 0; + var end = this.xcall(#selectionEnd); + if (fid == this.value || d <= 0 || start != end) { + return; + } + // fix Caret position + this.value = fid; + var text_after_caret = this.old_value.substr(start); + var n = fid.length - formatId(text_after_caret).length; + this.xcall(#setSelection, n, n); + } +} + +var reg = /^\d+$/; +function formatId(id) { + id = id.replace(/\s/g, ""); + if (reg.test(id) && id.length > 3) { + var n = id.length; + var a = n % 3 || 3; + var new_id = id.substr(0, a); + for (var i = a; i < n; i += 3) { + new_id += " " + id.substr(i, 3); + } + return new_id; + } + return id; +} + +event keydown (evt) { + if (view.focus && view.focus.id != 'remote_id') { + return; + } + if (!evt.shortcutKey) { + if (isEnterKey(evt)) { + var el = $(button#connect); + view.focus = el; + el.sendEvent("click"); + // simulate button click effect, windows does not have this issue + el.attributes.toggleClass("active", true); + self.timer(0.3s, function() { + el.attributes.toggleClass("active", false); + }); + } + } +} + +event keyup $(#msgbox input[name='password']) { + updateSetPasswordSubmitState(); +} + +event keyup $(#msgbox input[name='confirmation']) { + updateSetPasswordSubmitState(); +} + +event change $(#msgbox input[name='password']) { + updateSetPasswordSubmitState(); +} + +event change $(#msgbox input[name='confirmation']) { + updateSetPasswordSubmitState(); +} + +$(body).content(
    ); + +event click $(#powered-by) { + handler.open_url("https://cstudio.ch/hello-agent"); +} + +event click $(#open-settings) (_, me) { + showSettings(); +} + +// Event handlers for outgoing_only mode (when menu items are in main UI, not in MyIdMenu) +event click $(li#custom-server) (_, me) { + if (!outgoing_only) return; + open_custom_server_dialog(); +} + +event click $(li#whitelist) (_, me) { + if (!outgoing_only) return; + open_whitelist_dialog(); +} + +event click $(li#socks5-server) (_, me) { + if (!outgoing_only) return; + open_proxy_dialog(); +} + +event click $(li#login) (_, me) { + if (!outgoing_only) return; + login(); +} + +function self.closing() { + var (x, y, w, h) = view.box(#rectw, #border, #screen); + handler.closing(x, y, w, h); + return true; +} + +function self.ready() { + var r = handler.get_size(); + if (isReasonableSize(r) && r[2] > 0) { + var (sx, sy, sw, sh) = view.screenBox(#workarea, #rectw); + if (r[2] >= sw && r[3] >= sh) { + self.timer(1ms, function() { view.windowState = View.WINDOW_MAXIMIZED; }); + } else { + view.move(r[0], r[1], incoming_only ? scaleIt(incoming_only_width) : r[2], r[3]); + } + } else { + centerize(scaleIt(incoming_only ? incoming_only_width : 800), scaleIt(incoming_only ? 390 : 600)); + } + if (!handler.get_remote_id()) { + view.focus = $(#remote_id); + } + refreshCurrentUser(); + updateTheme(); +} + +function showAbout() { + myIdMenu.showAbout(); +} + +function showSettings() { + if ($(#overlay).style#display == 'block') return; + var menu = myIdMenu.$(menu#config-options); + var anchor = $(#open-settings); + if (!anchor) anchor = myIdMenu.$(svg#menu); + // show immediately at button, then update menu state asynchronously + anchor.popup(menu); + self.timer(1ms, function() { + audioInputMenu.update({ show: true }); + myIdMenu.toggleMenuState(); + if (direct_server) direct_server.update(); + }); +} + +function checkConnectStatus() { + handler.check_mouse_time(); // trigger connection status updater + self.timer(1s, function() { + var tmp = handler.get_option("stop-service") == "Y"; + if (tmp != service_stopped) { + service_stopped = tmp; + app.update(); + } + tmp = handler.using_public_server(); + if (tmp != using_public_server) { + using_public_server = tmp; + app.connect_status.update(); + } + tmp = handler.get_connect_status(); + if (tmp[0] != connect_status) { + connect_status = tmp[0]; + app.connect_status.update(); + myIdMenu.update(); + } + if (tmp[1] != key_confirmed) { + key_confirmed = tmp[1]; + app.update(); + } + if (tmp[2] && tmp[2] != my_id) { + stdout.println("id updated"); + app.update(); + } + tmp = handler.get_error(); + if (system_error != tmp) { + system_error = tmp; + app.update(); + } + tmp = handler.get_software_update_url(); + if (tmp != software_update_url) { + software_update_url = tmp; + app.update(); + } + if (handler.recent_sessions_updated()) { + stdout.println("recent sessions updated"); + updateAbPeer(); + app.update(); + } + tmp = handler.get_option("disable-udp") == "Y"; + if (tmp != disable_udp) { + disable_udp = tmp; + app.update(); + } + check_if_overlay(); + checkConnectStatus(); + }); +} + +var enter = false; +function self.onMouse(evt) { + switch(evt.type) { + case Event.MOUSE_ENTER: + enter = true; + check_if_overlay(); + break; + case Event.MOUSE_LEAVE: + $(#overlay).style#display = 'none'; + enter = false; + break; + } +} + +function check_if_overlay() { + var enabled; + var is_enabled_by_control_permissions = handler.is_remote_modify_enabled_by_control_permissions(); + if (is_enabled_by_control_permissions == "true") { + enabled = true; + } else if (is_enabled_by_control_permissions == "false") { + enabled = false; + } else { + enabled = handler.get_option('allow-remote-config-modification') == 'Y'; + } + if (!enabled) { + var time0 = getTime(); + handler.check_mouse_time(); + self.timer(120ms, function() { + if (!enter) return; + var d = time0 - handler.get_mouse_time(); + if (d < 120) $(#overlay).style#display = 'block'; + }); + } +} + +checkConnectStatus(); + +function set_local_user_info(user) { + var user_info = {name: user.name}; + if (user.display_name) { + user_info.display_name = user.display_name; + } + if (user.avatar) { + user_info.avatar = user.avatar; + } + if (user.status) { + user_info.status = user.status; + } + handler.set_local_option("user_info", JSON.stringify(user_info)); +} + +function login() { + var name0 = getUserName(); + var pass0 = ''; + msgbox("custom-login", translate('Login'),
    +
    {translate('Username')}:
    +
    {translate('Password')}:
    +
    , "", function(res=null, show_progress) { + if (!res) return; + show_progress(); + var name = (res.username || '').trim(); + if (!name) { + show_progress(false, translate("Username missed")); + return " "; + } + var pass = (res.password || '').trim(); + if (!pass) { + show_progress(false, translate("Password missed")); + return " "; + } + abLoading = true; + var url = handler.get_api_server(); + httpRequest(url + "/api/login", #post, {username: name, password: pass, id: my_id, uuid: handler.get_uuid(), type: 'account', deviceInfo: getDeviceInfo()}, function(data) { + if (data.error) { + abLoading = false; + var err = translate(data.error); + show_progress(false, err); + return; + } + if (data.type == 'email_check') { + abLoading = false; + show_progress(-1); + on_2fa_check(data); + return; + } + handler.set_local_option("access_token", data.access_token); + set_local_user_info(data.user); + show_progress(-1); + myIdMenu.update(); + getAb(); + }, function(err, status) { + abLoading = false; + err = translate(err); + if (url.indexOf('rustdesk') < 0) err = url + ', ' + err; + show_progress(false, err); + }); + return " "; + }, msgbox_default_height, get_msgbox_width()); +} + +function on_2fa_check(last_msg) { + const isEmailCheck = !last_msg.tfa_type || last_msg.tfa_type == 'email_check'; + const secret = last_msg.secret; + const emailHint = last_msg.user.email; + + msgbox("custom-2fa-verification-code", translate('Verification code'),
    + { isEmailCheck &&
    {translate('Email')}:{emailHint}
    } +
    {translate(isEmailCheck ? 'Verification code' : '2FA code')}:
    + { isEmailCheck &&
    {translate('verification_tip')}
    } +
    , "", + function(res=null, show_progress) { + if (!res) return; + show_progress(); + var code = (res.verification_code || '').trim(); + if (!code || code.length < 6) { + show_progress(false, translate("Too short, at least 6 characters.")); + return " "; + } + abLoading = true; + var url = handler.get_api_server(); + const loginData = { + username: last_msg.user.name, + id: my_id, + uuid: handler.get_uuid(), + type: 'email_code', + verificationCode: code, + tfaCode: isEmailCheck ? '' : code, + secret: secret, + deviceInfo: getDeviceInfo() + }; + httpRequest(url + "/api/login", #post, loginData, + function(data) { + if (data.error) { + abLoading = false; + show_progress(false, data.error); + return; + } + handler.set_local_option("access_token", data.access_token); + set_local_user_info(data.user); + show_progress(-1); + myIdMenu.update(); + getAb(); + }, + function(err, status) { + abLoading = false; + err = translate(err); + if (url.indexOf('rustdesk') < 0) err = url + ', ' + err; + show_progress(false, err); + } + ); + return " "; + }, + msgbox_default_height, + get_msgbox_width() + ); +} + +function reset_token() { + handler.set_local_option("access_token", ""); + handler.set_local_option("user_info", ""); + handler.set_local_option("selected-tags", ""); + myIdMenu.update(); + resetAb(); + if (abComponent) { + abComponent.update(); + } +} + +function logout() { + var url = handler.get_api_server(); + httpRequest(url + "/api/logout", #post, {id: my_id, uuid: handler.get_uuid()}, function(data) { + }, function(err, status) { + msgbox("custom-error", translate('Error'), err); + }, getHttpHeaders()); + reset_token(); +} + +function refreshCurrentUser() { + var token = handler.get_local_option("access_token"); + if (!token) { return; } + abLoading = true; + abError = ""; + app.update(); + httpRequest(handler.get_api_server() + "/api/currentUser", #post, {id: my_id, uuid: handler.get_uuid()}, function(data) { + if (data.error) { + if (data.error == 'Invalid token') { + reset_token(); + } + handleAbError(data.error); + return; + } + if (!handler.verify_login(data.verifier, token)) { + handleAbError("Please update your self-hosting server Pro to latest version"); + return; + } + set_local_user_info(data); + myIdMenu.update(); + getAb(); + }, function(err, status) { + if (status == 401 || status == 400) { + reset_token(); + } + handleAbError(err); + }, getHttpHeaders()); +} + +function getHttpHeaders() { + return "Authorization: Bearer " + handler.get_local_option("access_token"); +} + +function getDeviceInfo() { + return JSON.parse(handler.get_login_device_info()); +} diff --git a/vendor/rustdesk/src/ui/install.html b/vendor/rustdesk/src/ui/install.html new file mode 100644 index 0000000..bd9653e --- /dev/null +++ b/vendor/rustdesk/src/ui/install.html @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/vendor/rustdesk/src/ui/install.tis b/vendor/rustdesk/src/ui/install.tis new file mode 100644 index 0000000..f1e2108 --- /dev/null +++ b/vendor/rustdesk/src/ui/install.tis @@ -0,0 +1,70 @@ +function self.ready() { + centerize(scaleIt(800), scaleIt(600)); +} + +var install_path = ""; + +class Install: Reactor.Component { + function render() { + const install_options = JSON.parse(view.install_options()); + const desktop_icon = { checked: install_options?.DESKTOPSHORTCUTS == '0' ? false : true }; + const startmenu_shortcuts = { checked: install_options?.STARTMENUSHORTCUTS == '0' ? false : true }; + return
    +
    {translate('Installation')}
    +
    {translate('Installation Path')} {": "} + +
    +
    {translate('Create start menu shortcuts')}
    +
    {translate('Create desktop icon')}
    +
    {translate('End-user license agreement')}
    +
    {translate('agreement_tip')}
    +
    +
    + + + + {handler.show_run_without_install() && } +
    +
    ; + } + + event click $(#cancel) { + view.close(); + } + + event click $(#run-without-install) { + handler.run_without_install(); + } + + event click $(#path) { + install_path = view.selectFolder() || ""; + if (install_path) { + install_path = install_path.urlUnescape(); + install_path = install_path.replace("file://", "").replace("/", "\\"); + if (install_path[install_path.length - 1] != "\\") install_path += "\\"; + install_path += handler.get_app_name(); + $(#path_input).value = install_path; + } + } + + event click $(#agreement) { + view.open_url("https://cstudio.ch/hello-agent/privacy"); + } + + event click $(#submit) { + for (var el in $$(button)) el.state.disabled = true; + $(progress).style.set{ display: "inline-block" }; + var args = ""; + if ($(#startmenu).value) { + args += "startmenu "; + } + if ($(#desktopicon).value) { + args += "desktopicon "; + } + view.install_me(args, install_path); + } +} + +$(body).content(); diff --git a/vendor/rustdesk/src/ui/msgbox.tis b/vendor/rustdesk/src/ui/msgbox.tis new file mode 100644 index 0000000..6e6b6a6 --- /dev/null +++ b/vendor/rustdesk/src/ui/msgbox.tis @@ -0,0 +1,390 @@ +function translate_text(text) { + if (text.indexOf('Failed') == 0 && text.indexOf(': ') > 0) { + var fds = text.split(': '); + for (var i = 0; i < fds.length; ++i) { + fds[i] = translate(fds[i]); + } + text = fds.join(': '); + } else { + var fds = text.split(' '); + if (fds.length > 1 && fds[0].slice(-4) === '_tip') { + fds[0] = translate(fds[0]); + var rest = text.substring(fds[0].length + 1); + text = fds[0] + ' ' + translate(rest); + } else { + text = translate(text); + } + } + return text; +} + +var msgboxTimerFunc = function() {} +function closeMsgbox() { + self.timer(0, msgboxTimerFunc); + $(#msgbox).content(); +} + +class MsgboxComponent: Reactor.Component { + function this(params) { + this.width = params.width; + this.height = params.height; + this.type = params.type; + this.title = params.title; + this.content = params.content; + this.link = params.link; + this.remember = params.remember; + this.callback = params.callback; + this.hasRetry = params.hasRetry; + this.autoLogin = params.autoLogin; + this.contentStyle = params.contentStyle; + try { this.content = translate_text(this.content); } catch (e) {} + } + + function getIcon(color) { + if (this.type == "input-password" || this.type == "session-login" || this.type == "session-login-password" || this.type == "input-2fa") { + return ; + } + if (this.type == "connecting") { + return ; + } + if (this.type == "success") { + return ; + } + if (this.type.indexOf("error") >= 0 || this.type == "re-input-password" || this.type == "input-2fa" || this.type == "session-re-login" || this.type == "session-login-re-password") { + return ; + } + return null; + } + + function getInputPasswordContent() { + var ts = this.remember ? { checked: true } : {}; + return
    +
    {translate('Please enter your password')}
    + +
    {translate('Remember password')}
    +
    ; + } + + function get2faContent() { + var enable_trusted_devices = handler.get_enable_trusted_devices(); + return
    +
    {translate('enter-2fa-title')}
    +
    + {enable_trusted_devices ?
    {translate('Trust this device')}
    : ""} +
    ; + } + + function getInputUserPasswordContent() { + return
    +
    {translate("OS Username")}
    +
    +
    {translate("OS Password")}
    + +
    +
    ; + } + + function getXsessionPasswordContent() { + return
    +
    {translate("OS Username")}
    +
    +
    {translate("OS Password")}
    + +
    {translate('Please enter your password')}
    + +
    {translate('Remember password')}
    +
    ; + } + + function getContent() { + if (this.type == "input-password") { + return this.getInputPasswordContent(); + } else if (this.type == "input-2fa") { + return this.get2faContent(); + } else if (this.type == "session-login") { + return this.getInputUserPasswordContent(); + } else if (this.type == "session-login-password") { + return this.getXsessionPasswordContent(); + } else if (this.type == "custom-os-password") { + var ts = this.autoLogin ? { checked: true } : {}; + return
    + +
    {translate('Auto Login')}
    +
    ; + } + return this.content; + } + + function getColor() { + if (this.type == "input-password" || this.type == "input-2fa" || this.type == "custom-os-password" || this.type == "session-login" || this.type == "session-login-password") { + return "#AD448E"; + } + if (this.type == "success") { + return "#32bea6"; + } + if (this.type.indexOf("error") >= 0 || this.type == "re-input-password" || this.type == "session-re-login" || this.type == "session-login-re-password") { + return "#e04f5f"; + } + return "#2C8CFF"; + } + + function hasSkip() { + return this.type.indexOf("skip") >= 0; + } + + function getScreenshotButtons() { + var isScreenshot = this.type.indexOf("take-screenshot") >= 0; + return isScreenshot + ?
    + + + +
    + : ""; + } + + function render() { + this.set_outline_focus(); + var color = this.getColor(); + var icon = this.getIcon(color); + var content = this.getContent(); + var hasCancel = this.type.indexOf("error") < 0 && this.type.indexOf("nocancel") < 0 && this.type != "restarting"; + var hasOk = this.type != "connecting" && this.type != "success" && this.type.indexOf("nook") < 0; + var hasLink = this.link != ""; + var hasClose = this.type.indexOf("hasclose") >= 0; + var show_progress = this.type == "connecting"; + var me = this; + self.timer(0, msgboxTimerFunc); + msgboxTimerFunc = function() { + if (typeof content == "string") + me.$(#content).html = translate(content); + else + me.$(#content).content(content); + }; + self.timer(3ms, msgboxTimerFunc); + return (
    +
    +
    +
    + {translate(this.title)} +
    +
    +
    + {icon &&
    {icon}
    } +
    +
    +
    + + + {hasCancel || this.hasRetry ? : ""} + {this.hasSkip() ? : ""} + {hasOk || this.hasRetry ? : ""} + {hasLink ? : ""} + {hasClose ? : ""} + {this.getScreenshotButtons()} +
    +
    +
    +
    ); + } + + event click $(.custom-event) (_, me) { + if (this.callback) this.callback(me); + } + + function submit() { + var submit_btn = this.$(button#submit); + if (submit_btn) { + if (submit_btn.state.disabled) return; + submit_btn.sendEvent("click"); + } + } + + function cancel() { + if (this.$(button#cancel)) { + this.$(button#cancel).sendEvent("click"); + } + } + + event click $(button#cancel) { + this.close(); + if (this.callback) this.callback(null); + } + + event click $(button#skip) { + var values = this.getValues(); + values.skip = true; + if (this.callback) this.callback(values); + if (this.close) this.close(); + } + + event click $(button#jumplink) { + if (this.link.indexOf("http") == 0) { + Sciter.launch(this.link); + } + } + + event click $(button#submit) { + if (this.type == "error") { + if (this.hasRetry) { + retryConnect(true); + return; + } + } + if (this.type == "re-input-password") { + this.type = "input-password"; + this.update(); + return; + } + if (this.type == "session-re-login") { + this.type = "session-login"; + this.update(); + return; + } + if (this.type == "session-login-re-password") { + this.type = "session-login-password"; + this.update(); + return; + } + var values = this.getValues(); + if (this.callback) { + var self = this; + var err = this.callback(values, function(a=1, b='') { self.show_progress(a, b); }); + if (!err) { + if (this.close) this.close(); + return; + } + if (err && err.trim()) this.show_progress(false, err); + } else { + this.close(); + } + } + + event click $(button#screenshotSaveAs) { + this.close(); + + handler.leave(handler.get_keyboard_mode()); + const filter = "Png file (*.png)"; + const defaultExt = "png"; + const initialPath = System.path(#USER_DOCUMENTS, "screenshot"); + const caption = "Save as"; + var url = view.selectFile(#save, filter, defaultExt, initialPath, caption); + handler.enter(handler.get_keyboard_mode()); + if(url) { + var res = handler.handle_screenshot("0:" + URL.toPath(url)); + if (res) { + msgbox("custom-error-nocancel-nook-hasclose", "Take screenshot", res, "", function() {}); + } + } else { + handler.handle_screenshot("2"); + } + } + + event click $(button#screenshotCopyToClip) { + this.close(); + var res = handler.handle_screenshot("1"); + if (res) { + msgbox("custom-error-nocancel-nook-hasclose", "Take screenshot", res, "", function() {}); + } + } + + event click $(button#screenshotCancel) { + this.close(); + handler.handle_screenshot("2"); + } + + event keydown (evt) { + if (!evt.shortcutKey) { + if (isEnterKey(evt)) { + this.submit(); + } + if (evt.keyCode == Event.VK_ESCAPE) { + this.cancel(); + } + } + } + + event click $(button#select_directory) { + var folder = view.selectFolder(translate("Change"), $(#folderPath).text); + if (folder) { + if (folder.indexOf("file://") == 0) folder = folder.substring(7); + $(#folderPath).text = folder; + } + } + + function show_progress(show=1, err="") { + if (show == -1) { + this.close() + return; + } + this.$(#progress).style.set { + display: show ? "inline-block" : "none" + }; + this.$(#error).text = err; + } + + function getValues() { + var values = { type: this.type }; + for (var el in this.$$(.form input)) { + values[el.attributes["name"]] = el.value; + } + for (var el in this.$$(.form textarea)) { + values[el.attributes["name"]] = el.value; + } + for (var el in this.$$(.form button)) { + values[el.attributes["name"]] = el.value; + } + if (this.type == "input-password") { + values.password = (values.password || "").trim(); + if (!values.password) { + return; + } + } + if (this.type == "input-2fa") { + values.code = (values.code || "").trim(); + if (!values.code) { + return; + } + } + if (this.type == "session-login") { + values.osusername = (values.osusername || "").trim(); + values.ospassword = (values.ospassword || "").trim(); + if (!values.osusername || !values.ospassword) { + return; + } + } + if (this.type == "session-login-password") { + values.password = (values.password || "").trim(); + values.osusername = (values.osusername || "").trim(); + values.ospassword = (values.ospassword || "").trim(); + if (!values.osusername || !values.ospassword || !values.password) { + return; + } + } + if (this.type == "multiple-sessions-nocancel") { + values.sid = (this.$$(select))[0].value; + } + if (this.type == "remote-printer-selector") { + values.name = (this.$$(select))[0].value; + } + return values; + } + + function set_outline_focus() { + var me = this; + self.timer(30ms, function() { + var el = me.$(.outline-focus); + if (el) view.focus = el; + else { + el = me.$(#submit); + if (el) { + view.focus = el; + } + } + }); + } + + function close() { + closeMsgbox(); + } +} diff --git a/vendor/rustdesk/src/ui/port_forward.tis b/vendor/rustdesk/src/ui/port_forward.tis new file mode 100644 index 0000000..a30f698 --- /dev/null +++ b/vendor/rustdesk/src/ui/port_forward.tis @@ -0,0 +1,77 @@ +class PortForward: Reactor.Component { + function render() { + var args = handler.get_args(); + var is_rdp = handler.is_rdp(); + if (is_rdp) { + this.pfs = [["", "", "RDP"]]; + args = ["rdp"]; + } else if (args.length) { + this.pfs = [args]; + } else { + this.pfs = handler.get_port_forwards(); + } + var pfs = this.pfs.map(function(pf, i) { + return + {is_rdp ? : pf[0]} + {args.length ? svg_arrow : ""} + {pf[1] || "localhost"} + {pf[2]} + {args.length ? "" : {svg_cancel}} + ; + }); + return
    + {pfs.length ?
    + {translate('Listening ...')}
    + {translate('not_close_tcp_tip')} +
    : ""} + + + + + + + {args.length ? "" : } + + + + {args.length ? "" : + + + + + + + + } + {pfs} + +
    {translate('Local Port')} + {translate('Remote Host')}{translate('Remote Port')}{translate('Action')}
    {svg_arrow}
    ; + } + + event click $(#add) () { + var port = ($(#port).value || "").toInteger() || 0; + var remote_host = $(#remote-host).value || ""; + var remote_port = ($(#remote-port).value || "").toInteger() || 0; + if (port <= 0 || remote_port <= 0) return; + handler.add_port_forward(port, remote_host, remote_port); + this.update(); + } + + event click $(#new-rdp) { + handler.new_rdp(); + } + + event click $(.remove svg) (_, me) { + var pf = this.pfs[me.parent.parent.index - 1]; + handler.remove_port_forward(pf[0]); + this.update(); + } +} + +function initializePortForward() +{ + $(#file-transfer-wrapper).content(); + $(#video-wrapper).style.set { visibility: "hidden", position: "absolute" }; + $(#file-transfer-wrapper).style.set { display: "block" }; +} diff --git a/vendor/rustdesk/src/ui/printer.tis b/vendor/rustdesk/src/ui/printer.tis new file mode 100644 index 0000000..c284826 --- /dev/null +++ b/vendor/rustdesk/src/ui/printer.tis @@ -0,0 +1,41 @@ +include "sciter:reactor.tis"; + +handler.printerRequest = function(id, path) { + show_printer_selector(id, path); +}; + +function show_printer_selector(id, path) +{ + var names = handler.get_printer_names(); + msgbox("remote-printer-selector", "Incoming Print Job", , "", function(res=null) { + if (res && res.name) { + handler.on_printer_selected(id, path, res.name); + } + }, 180); +} + +class PrinterComponent extends Reactor.Component { + this var names = []; + this var jobTip = translate("print-incoming-job-confirm-tip"); + + function this(params) { + if (params && params.names) { + this.names = params.names; + } + } + + function render() { + return
    +
    {translate("print-incoming-job-confirm-tip")}
    +
    +
    + +
    +
    +
    ; + } +} diff --git a/vendor/rustdesk/src/ui/remote.css b/vendor/rustdesk/src/ui/remote.css new file mode 100644 index 0000000..71b2c16 --- /dev/null +++ b/vendor/rustdesk/src/ui/remote.css @@ -0,0 +1,46 @@ +body { + margin: 0; + color: black; + overflow: scroll-indicator; +} + +div#video-wrapper { + size: *; + background: #212121; +} + +div#quality-monitor { + top: 20px; + right: 20px; + background: #7571719c; + padding: 5px; + min-width: 150px; + color: azure; + border: 0.5px solid azure; +} + +video#handler { + behavior: native-remote video; + size: *; + margin: *; + foreground-size: contain; +} + +img#cursor { + position: absolute; + display: none; + //opacity: 0.66, + //transform: scale(0.8); +} + +.goup { + transform: rotate(90deg); +} + +table#remote-folder-view { + context-menu: selector(menu#remote-folder-view); +} + +table#local-folder-view { + context-menu: selector(menu#local-folder-view); +} \ No newline at end of file diff --git a/vendor/rustdesk/src/ui/remote.html b/vendor/rustdesk/src/ui/remote.html new file mode 100644 index 0000000..70e909d --- /dev/null +++ b/vendor/rustdesk/src/ui/remote.html @@ -0,0 +1,44 @@ + + + + + + +
    + + +
    + + + +
    + + +
    +

    (filename: P) -> io::Result>> +where + P: AsRef, +{ + let file = File::open(filename)?; + Ok(io::BufReader::new(file).lines()) +} + +pub fn is_valid_custom_id(id: &str) -> bool { + regex::Regex::new(r"^[a-zA-Z][\w-]{5,15}$") + .unwrap() + .is_match(id) +} + +// Support 1.1.10-1, the number after - is a patch version. +pub fn get_version_number(v: &str) -> i64 { + let mut versions = v.split('-'); + + let mut n = 0; + + // The first part is the version number. + // 1.1.10 -> 1001100, 1.2.3 -> 1001030, multiple the last number by 10 + // to leave space for patch version. + if let Some(v) = versions.next() { + let mut last = 0; + for x in v.split('.') { + last = x.parse::().unwrap_or(0); + n = n * 1000 + last; + } + n -= last; + n += last * 10; + } + + if let Some(v) = versions.next() { + n += v.parse::().unwrap_or(0); + } + + // Ignore the rest + + n +} + +pub fn get_modified_time(path: &std::path::Path) -> SystemTime { + std::fs::metadata(path) + .map(|m| m.modified().unwrap_or(UNIX_EPOCH)) + .unwrap_or(UNIX_EPOCH) +} + +pub fn get_created_time(path: &std::path::Path) -> SystemTime { + std::fs::metadata(path) + .map(|m| m.created().unwrap_or(UNIX_EPOCH)) + .unwrap_or(UNIX_EPOCH) +} + +pub fn get_exe_time() -> SystemTime { + std::env::current_exe().map_or(UNIX_EPOCH, |path| { + let m = get_modified_time(&path); + let c = get_created_time(&path); + if m > c { + m + } else { + c + } + }) +} + +/// Known cases where machine_uid::get() may fail: +/// - Windows shutdown: "The media is write protected. (os error 19)" +/// - macOS (hard to reproduce, reproduced at login screen): "No matching IOPlatformUUID in `ioreg -rd1 -c IOPlatformExpertDevice` command" +pub fn get_uuid() -> Vec { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + use std::sync::atomic::{AtomicUsize, Ordering}; + + static CACHED_MACHINE_UID: std::sync::OnceLock> = std::sync::OnceLock::new(); + // Throttle only applies to the fallback machine_uid::get() log below, not the Once::call_once retry logs. + static LOG_COUNT: AtomicUsize = AtomicUsize::new(0); + + // Only macOS needs retry logic here because: + // - macOS: in testing, only one failure occurred when reading at 50ms intervals, so retry helps + // - Windows: failures during shutdown are persistent, retrying is pointless + #[cfg(target_os = "macos")] + { + static INIT: std::sync::Once = std::sync::Once::new(); + INIT.call_once(|| { + // Keep in sync with upstream handling: + // https://github.com/rustdesk/rustdesk/blob/85db6779828349b23ca3eba91cc7cd36c5337797/src/common.rs#L822 + let username = whoami::username().trim_end_matches('\0').to_owned(); + let max_retries = if username == "root" { 16 } else { 8 }; + for i in 0..max_retries { + match machine_uid::get() { + Ok(id) => { + let _ = CACHED_MACHINE_UID.set(id.into()); + return; + } + Err(e) => { + log::error!("Failed to get machine uid in macOS retry #{i}: {e}"); + } + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + }); + } + + if let Some(uid) = CACHED_MACHINE_UID.get() { + return uid.clone(); + } + + match machine_uid::get() { + Ok(id) => { + let uid: Vec = id.into(); + let _ = CACHED_MACHINE_UID.set(uid.clone()); + return uid; + } + Err(e) => { + if LOG_COUNT + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { + (count < 30).then_some(count + 1) + }) + .is_ok() + { + log::error!("Failed to get machine uid: {e}"); + } + } + } + } + Config::get_key_pair().1 +} + +#[inline] +pub fn get_time() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0) as _ +} + +#[inline] +pub fn is_ipv4_str(id: &str) -> bool { + if let Ok(reg) = regex::Regex::new( + r"^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:\d+)?$", + ) { + reg.is_match(id) + } else { + false + } +} + +#[inline] +pub fn is_ipv6_str(id: &str) -> bool { + if let Ok(reg) = regex::Regex::new( + r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$", + ) { + reg.is_match(id) + } else { + false + } +} + +#[inline] +pub fn is_ip_str(id: &str) -> bool { + is_ipv4_str(id) || is_ipv6_str(id) +} + +#[inline] +pub fn is_domain_port_str(id: &str) -> bool { + // modified regex for RFC1123 hostname. check https://stackoverflow.com/a/106223 for original version for hostname. + // according to [TLD List](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) version 2023011700, + // there is no digits in TLD, and length is 2~63. + if let Ok(reg) = regex::Regex::new( + r"(?i)^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z-]{0,61}[a-z]:\d{1,5}$", + ) { + reg.is_match(id) + } else { + false + } +} + +pub fn init_log(_is_async: bool, _name: &str) -> Option { + static INIT: std::sync::Once = std::sync::Once::new(); + #[allow(unused_mut)] + let mut logger_holder: Option = None; + INIT.call_once(|| { + #[cfg(debug_assertions)] + { + use env_logger::*; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info,reqwest=warn,rustls=warn,webrtc-sctp=warn,webrtc=warn")); + } + #[cfg(not(debug_assertions))] + { + // https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write + // though async logger more efficient, but it also causes more problems, disable it for now + let mut path = config::Config::log_path(); + #[cfg(target_os = "android")] + if !config::Config::get_home().exists() { + return; + } + if !_name.is_empty() { + path.push(_name); + } + use flexi_logger::*; + if let Ok(x) = Logger::try_with_env_or_str("debug,reqwest=warn,rustls=warn,webrtc-sctp=warn,webrtc=warn") { + logger_holder = x + .log_to_file(FileSpec::default().directory(path)) + .write_mode(if _is_async { + WriteMode::Async + } else { + WriteMode::Direct + }) + .format(opt_format) + .rotate( + Criterion::Age(Age::Day), + Naming::Timestamps, + Cleanup::KeepLogFiles(31), + ) + .start() + .ok(); + } + } + }); + logger_holder +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct VersionCheckRequest { + #[serde(default)] + pub os: String, + #[serde(default)] + pub os_version: String, + #[serde(default)] + pub arch: String, + #[serde(default)] + pub device_id: Vec, + #[serde(default)] + pub typ: String, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct VersionCheckResponse { + #[serde(default)] + pub url: String, +} + +pub const VER_TYPE_RUSTDESK_CLIENT: &str = "rustdesk-client"; +pub const VER_TYPE_RUSTDESK_SERVER: &str = "rustdesk-server"; + +pub fn version_check_request(typ: String) -> (VersionCheckRequest, String) { + const URL: &str = "https://api.rustdesk.com/version/latest"; + + use sysinfo::System; + let system = System::new(); + let os = system.distribution_id(); + let os_version = system.os_version().unwrap_or_default(); + let arch = std::env::consts::ARCH.to_string(); + #[allow(deprecated)] + let device_id = fingerprint::get_fingerprint(None, None); + ( + VersionCheckRequest { + os, + os_version, + arch, + device_id, + typ, + }, + URL.to_string(), + ) +} + +pub fn time_based_rand() -> u32 { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + + let mut x = nanos as u64; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + + (x % 32768) as u32 +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_mangle() { + let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + + let addr = "[2001:db8::1]:8080".parse::().unwrap(); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + + let addr = "[2001:db8:ff::1111]:80".parse::().unwrap(); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + } + + #[test] + fn test_allow_err() { + allow_err!(Err("test err") as Result<(), &str>); + allow_err!( + Err("test err with msg") as Result<(), &str>, + "prompt {}", + "failed" + ); + } + + #[test] + fn test_ipv6() { + assert!(is_ipv6_str("1:2:3")); + assert!(is_ipv6_str("[ab:2:3]:12")); + assert!(is_ipv6_str("[ABEF:2a:3]:12")); + assert!(!is_ipv6_str("[ABEG:2a:3]:12")); + assert!(!is_ipv6_str("1[ab:2:3]:12")); + assert!(!is_ipv6_str("1.1.1.1")); + assert!(is_ip_str("1.1.1.1")); + assert!(!is_ipv6_str("1:2:")); + assert!(is_ipv6_str("1:2::0")); + assert!(is_ipv6_str("[1:2::0]:1")); + assert!(!is_ipv6_str("[1:2::0]:")); + assert!(!is_ipv6_str("1:2::0]:1")); + } + + #[test] + fn test_ipv4() { + assert!(is_ipv4_str("1.2.3.4")); + assert!(is_ipv4_str("1.2.3.4:90")); + assert!(is_ipv4_str("192.168.0.1")); + assert!(is_ipv4_str("0.0.0.0")); + assert!(is_ipv4_str("255.255.255.255")); + assert!(!is_ipv4_str("256.0.0.0")); + assert!(!is_ipv4_str("256.256.256.256")); + assert!(!is_ipv4_str("1:2:")); + assert!(!is_ipv4_str("192.168.0.256")); + assert!(!is_ipv4_str("192.168.0.1/24")); + assert!(!is_ipv4_str("192.168.0.")); + assert!(!is_ipv4_str("192.168..1")); + } + + #[test] + fn test_hostname_port() { + assert!(!is_domain_port_str("a:12")); + assert!(!is_domain_port_str("a.b.c:12")); + assert!(is_domain_port_str("test.com:12")); + assert!(is_domain_port_str("test-UPPER.com:12")); + assert!(is_domain_port_str("some-other.domain.com:12")); + assert!(!is_domain_port_str("under_score:12")); + assert!(!is_domain_port_str("a@bc:12")); + assert!(!is_domain_port_str("1.1.1.1:12")); + assert!(!is_domain_port_str("1.2.3:12")); + assert!(!is_domain_port_str("1.2.3.45:12")); + assert!(!is_domain_port_str("a.b.c:123456")); + assert!(!is_domain_port_str("---:12")); + assert!(!is_domain_port_str(".:12")); + // todo: should we also check for these edge cases? + // out-of-range port + assert!(is_domain_port_str("test.com:0")); + assert!(is_domain_port_str("test.com:98989")); + } + + #[test] + fn test_mangle2() { + let addr = "[::ffff:127.0.0.1]:8080".parse().unwrap(); + let addr_v4 = "127.0.0.1:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr)), addr_v4); + assert_eq!( + AddrMangle::decode(&AddrMangle::encode("[::127.0.0.1]:8080".parse().unwrap())), + addr_v4 + ); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v4)), addr_v4); + let addr_v6 = "[ef::fe]:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); + let addr_v6 = "[::1]:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); + } + + #[test] + fn test_get_version_number() { + assert_eq!(get_version_number("1.1.10"), 1001100); + assert_eq!(get_version_number("1.1.10-1"), 1001101); + assert_eq!(get_version_number("1.1.11-1"), 1001111); + assert_eq!(get_version_number("1.2.3"), 1002030); + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/mem.rs b/vendor/rustdesk/libs/hbb_common/src/mem.rs new file mode 100644 index 0000000..90a5d6d --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/mem.rs @@ -0,0 +1,14 @@ +/// SAFETY: the returned Vec must not be resized or reserverd +pub unsafe fn aligned_u8_vec(cap: usize, align: usize) -> Vec { + use std::alloc::*; + + let layout = + Layout::from_size_align(cap, align).expect("invalid aligned value, must be power of 2"); + unsafe { + let ptr = alloc(layout); + if ptr.is_null() { + panic!("failed to allocate {} bytes", cap); + } + Vec::from_raw_parts(ptr, 0, cap) + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/password_security.rs b/vendor/rustdesk/libs/hbb_common/src/password_security.rs new file mode 100644 index 0000000..6471d71 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/password_security.rs @@ -0,0 +1,474 @@ +use crate::config::Config; +use sodiumoxide::base64; +use std::sync::{Arc, RwLock}; + +lazy_static::lazy_static! { + pub static ref TEMPORARY_PASSWORD:Arc> = Arc::new(RwLock::new(get_auto_password())); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum VerificationMethod { + OnlyUseTemporaryPassword, + OnlyUsePermanentPassword, + UseBothPasswords, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ApproveMode { + Both, + Password, + Click, +} + +fn get_auto_password() -> String { + let len = temporary_password_length(); + if Config::get_bool_option(crate::config::keys::OPTION_ALLOW_NUMERNIC_ONE_TIME_PASSWORD) { + Config::get_auto_numeric_password(len) + } else { + Config::get_auto_password(len) + } +} + +// Should only be called in server +pub fn update_temporary_password() { + *TEMPORARY_PASSWORD.write().unwrap() = get_auto_password(); +} + +// Should only be called in server +pub fn temporary_password() -> String { + TEMPORARY_PASSWORD.read().unwrap().clone() +} + +fn verification_method() -> VerificationMethod { + let method = Config::get_option("verification-method"); + if method == "use-temporary-password" { + VerificationMethod::OnlyUseTemporaryPassword + } else if method == "use-permanent-password" { + VerificationMethod::OnlyUsePermanentPassword + } else { + VerificationMethod::UseBothPasswords // default + } +} + +pub fn temporary_password_length() -> usize { + let length = Config::get_option("temporary-password-length"); + if length == "8" { + 8 + } else if length == "10" { + 10 + } else { + 6 // default + } +} + +pub fn temporary_enabled() -> bool { + verification_method() != VerificationMethod::OnlyUsePermanentPassword +} + +pub fn permanent_enabled() -> bool { + verification_method() != VerificationMethod::OnlyUseTemporaryPassword +} + +pub fn has_valid_password() -> bool { + temporary_enabled() && !temporary_password().is_empty() + || permanent_enabled() && Config::has_permanent_password() +} + +pub fn approve_mode() -> ApproveMode { + let mode = Config::get_option("approve-mode"); + if mode == "password" { + ApproveMode::Password + } else if mode == "click" { + ApproveMode::Click + } else { + ApproveMode::Both + } +} + +pub fn hide_cm() -> bool { + approve_mode() == ApproveMode::Password + && verification_method() == VerificationMethod::OnlyUsePermanentPassword + && crate::config::option2bool("allow-hide-cm", &Config::get_option("allow-hide-cm")) +} + +const VERSION_LEN: usize = 2; + +// Check if data is already encrypted by verifying: +// 1) version prefix "00" +// 2) valid base64 payload +// 3) decoded payload length >= secretbox::MACBYTES +// +// We intentionally avoid trying to decrypt here because key mismatch would cause +// false negatives. +// Reference: secretbox::seal returns ciphertext length = plaintext length + MACBYTES +// https://github.com/sodiumoxide/sodiumoxide/blob/3057acb1a030ad86ed8892a223d64036ab5e8523/src/crypto/secretbox/xsalsa20poly1305.rs#L67 +fn is_encrypted(v: &[u8]) -> bool { + if v.len() <= VERSION_LEN || !v.starts_with(b"00") { + return false; + } + match base64::decode(&v[VERSION_LEN..], base64::Variant::Original) { + Ok(decoded) => decoded.len() >= sodiumoxide::crypto::secretbox::MACBYTES, + Err(_) => false, + } +} + +pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String { + if is_encrypted(s.as_bytes()) { + log::error!("Duplicate encryption!"); + return s.to_owned(); + } + if s.chars().count() > max_len { + return String::default(); + } + if version == "00" { + if let Ok(s) = encrypt(s.as_bytes()) { + return version.to_owned() + &s; + } + } + s.to_owned() +} + +// String: password +// bool: whether decryption is successful +// bool: whether should store to re-encrypt when load +// note: s.len() return length in bytes, s.chars().count() return char count +// &[..2] return the left 2 bytes, s.chars().take(2) return the left 2 chars +pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) { + if s.len() > VERSION_LEN { + if s.starts_with("00") { + if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) { + return ( + String::from_utf8_lossy(&v).to_string(), + true, + "00" != current_version, + ); + } + } + } + + // For values that already look encrypted (version prefix + base64), avoid + // repeated store on each load when decryption fails. + ( + s.to_owned(), + false, + !s.is_empty() && !is_encrypted(s.as_bytes()), + ) +} + +pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec { + if is_encrypted(v) { + log::error!("Duplicate encryption!"); + return v.to_owned(); + } + if v.len() > max_len { + return vec![]; + } + if version == "00" { + if let Ok(s) = encrypt(v) { + let mut version = version.to_owned().into_bytes(); + version.append(&mut s.into_bytes()); + return version; + } + } + v.to_owned() +} + +// Vec: password +// bool: whether decryption is successful +// bool: whether should store to re-encrypt when load +pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec, bool, bool) { + if v.len() > VERSION_LEN { + let version = String::from_utf8_lossy(&v[..VERSION_LEN]); + if version == "00" { + if let Ok(v) = decrypt(&v[VERSION_LEN..]) { + return (v, true, version != current_version); + } + } + } + + // For values that already look encrypted (version prefix + base64), avoid + // repeated store on each load when decryption fails. + (v.to_owned(), false, !v.is_empty() && !is_encrypted(v)) +} + +fn encrypt(v: &[u8]) -> Result { + if !v.is_empty() { + symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original)) + } else { + Err(()) + } +} + +fn decrypt(v: &[u8]) -> Result, ()> { + if !v.is_empty() { + base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false)) + } else { + Err(()) + } +} + +pub fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result, ()> { + use sodiumoxide::crypto::secretbox; + use std::convert::TryInto; + + let uuid = crate::get_uuid(); + let mut keybuf = uuid.clone(); + keybuf.resize(secretbox::KEYBYTES, 0); + let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?); + let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]); + + if encrypt { + Ok(secretbox::seal(data, &nonce, &key)) + } else { + let res = secretbox::open(data, &nonce, &key); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if res.is_err() { + // Fallback: try pk if uuid decryption failed (in case encryption used pk due to machine_uid failure) + if let Some(key_pair) = Config::get_existing_key_pair() { + let pk = key_pair.1; + if pk != uuid { + let mut keybuf = pk; + keybuf.resize(secretbox::KEYBYTES, 0); + let pk_key = secretbox::Key(keybuf.try_into().map_err(|_| ())?); + return secretbox::open(data, &nonce, &pk_key); + } + } + } + res + } +} + +mod test { + + #[test] + fn test() { + use super::*; + use rand::{thread_rng, Rng}; + use std::time::Instant; + + let version = "00"; + let max_len = 128; + + println!("test str"); + let data = "1ü1111"; + let encrypted = encrypt_str_or_original(data, version, max_len); + let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version); + println!("data: {data}"); + println!("encrypted: {encrypted}"); + println!("decrypted: {decrypted}"); + assert_eq!(data, decrypted); + assert_eq!(version, &encrypted[..2]); + assert!(succ); + assert!(!store); + let (_, _, store) = decrypt_str_or_original(&encrypted, "99"); + assert!(store); + assert!(!decrypt_str_or_original(&decrypted, version).1); + assert_eq!( + encrypt_str_or_original(&encrypted, version, max_len), + encrypted + ); + + println!("test vec"); + let data: Vec = "1ü1111".as_bytes().to_vec(); + let encrypted = encrypt_vec_or_original(&data, version, max_len); + let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version); + println!("data: {data:?}"); + println!("encrypted: {encrypted:?}"); + println!("decrypted: {decrypted:?}"); + assert_eq!(data, decrypted); + assert_eq!(version.as_bytes(), &encrypted[..2]); + assert!(!store); + assert!(succ); + let (_, _, store) = decrypt_vec_or_original(&encrypted, "99"); + assert!(store); + assert!(!decrypt_vec_or_original(&decrypted, version).1); + assert_eq!( + encrypt_vec_or_original(&encrypted, version, max_len), + encrypted + ); + + println!("test original"); + let data = version.to_string() + "Hello World"; + let (decrypted, succ, store) = decrypt_str_or_original(&data, version); + assert_eq!(data, decrypted); + assert!(store); + assert!(!succ); + let verbytes = version.as_bytes(); + let data: Vec = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6]; + let (decrypted, succ, store) = decrypt_vec_or_original(&data, version); + assert_eq!(data, decrypted); + assert!(store); + assert!(!succ); + let (_, succ, store) = decrypt_str_or_original("", version); + assert!(!store); + assert!(!succ); + let (_, succ, store) = decrypt_vec_or_original(&[], version); + assert!(!store); + assert!(!succ); + let data = "1ü1111"; + assert_eq!(decrypt_str_or_original(data, version).0, data); + let data: Vec = "1ü1111".as_bytes().to_vec(); + assert_eq!(decrypt_vec_or_original(&data, version).0, data); + + // Base64-shaped "00" prefixed values shorter than MACBYTES are treated + // as original/plain values and should be stored. + let data = "00YWJjZA=="; + let (decrypted, succ, store) = decrypt_str_or_original(data, version); + assert_eq!(decrypted, data); + assert!(!succ); + assert!(store); + let data = b"00YWJjZA==".to_vec(); + let (decrypted, succ, store) = decrypt_vec_or_original(&data, version); + assert_eq!(decrypted, data); + assert!(!succ); + assert!(store); + + // When decoded length reaches MACBYTES, it is treated as encrypted-like + // and should not trigger repeated store. + let exact_mac = vec![0u8; sodiumoxide::crypto::secretbox::MACBYTES]; + let exact_mac_b64 = + sodiumoxide::base64::encode(&exact_mac, sodiumoxide::base64::Variant::Original); + let data = format!("00{exact_mac_b64}"); + let (_, succ, store) = decrypt_str_or_original(&data, version); + assert!(!succ); + assert!(!store); + let data = data.into_bytes(); + let (_, succ, store) = decrypt_vec_or_original(&data, version); + assert!(!succ); + assert!(!store); + + println!("test speed"); + let test_speed = |len: usize, name: &str| { + let mut data: Vec = vec![]; + let mut rng = thread_rng(); + for _ in 0..len { + data.push(rng.gen_range(0..255)); + } + let start: Instant = Instant::now(); + let encrypted = encrypt_vec_or_original(&data, version, len); + assert_ne!(data, decrypted); + let t1 = start.elapsed(); + let start = Instant::now(); + let (decrypted, _, _) = decrypt_vec_or_original(&encrypted, version); + let t2 = start.elapsed(); + assert_eq!(data, decrypted); + println!("{name}"); + println!("encrypt:{:?}, decrypt:{:?}", t1, t2); + + let start: Instant = Instant::now(); + let encrypted = base64::encode(&data, base64::Variant::Original); + let t1 = start.elapsed(); + let start = Instant::now(); + let decrypted = base64::decode(&encrypted, base64::Variant::Original).unwrap(); + let t2 = start.elapsed(); + assert_eq!(data, decrypted); + println!("base64, encrypt:{:?}, decrypt:{:?}", t1, t2,); + }; + test_speed(128, "128"); + test_speed(1024, "1k"); + test_speed(1024 * 1024, "1M"); + test_speed(10 * 1024 * 1024, "10M"); + test_speed(100 * 1024 * 1024, "100M"); + } + + #[test] + fn test_is_encrypted() { + use super::*; + use sodiumoxide::base64::{encode, Variant}; + use sodiumoxide::crypto::secretbox; + + // Empty data should not be considered encrypted + assert!(!is_encrypted(b"")); + assert!(!is_encrypted(b"0")); + assert!(!is_encrypted(b"00")); + + // Data without "00" prefix should not be considered encrypted + assert!(!is_encrypted(b"01abcd")); + assert!(!is_encrypted(b"99abcd")); + assert!(!is_encrypted(b"hello world")); + + // Data with "00" prefix but invalid base64 should not be considered encrypted + assert!(!is_encrypted(b"00!!!invalid base64!!!")); + assert!(!is_encrypted(b"00@#$%")); + + // Data with "00" prefix and valid base64 but shorter than MACBYTES is not encrypted + assert!(!is_encrypted(b"00YWJjZA==")); // "abcd" in base64 + assert!(!is_encrypted(b"00SGVsbG8gV29ybGQ=")); // "Hello World" in base64 + + // Data with "00" prefix and valid base64 with decoded len == MACBYTES is considered encrypted + let exact_mac = vec![0u8; secretbox::MACBYTES]; + let exact_mac_b64 = encode(&exact_mac, Variant::Original); + let exact_mac_candidate = format!("00{exact_mac_b64}"); + assert!(is_encrypted(exact_mac_candidate.as_bytes())); + + // Real encrypted data should be detected + let version = "00"; + let max_len = 128; + let encrypted_str = encrypt_str_or_original("1", version, max_len); + assert!(is_encrypted(encrypted_str.as_bytes())); + let encrypted_vec = encrypt_vec_or_original(b"1", version, max_len); + assert!(is_encrypted(&encrypted_vec)); + + // Original unencrypted data should not be detected as encrypted + assert!(!is_encrypted(b"1")); + assert!(!is_encrypted("1".as_bytes())); + } + + #[test] + fn test_encrypted_payload_min_len_macbytes() { + use super::*; + use sodiumoxide::base64::{decode, Variant}; + use sodiumoxide::crypto::secretbox; + + let version = "00"; + let max_len = 128; + + let encrypted_str = encrypt_str_or_original("1", version, max_len); + let decoded = decode(&encrypted_str.as_bytes()[VERSION_LEN..], Variant::Original).unwrap(); + assert!( + decoded.len() >= secretbox::MACBYTES, + "decoded encrypted payload must be at least MACBYTES" + ); + + let encrypted_vec = encrypt_vec_or_original(b"1", version, max_len); + let decoded = decode(&encrypted_vec[VERSION_LEN..], Variant::Original).unwrap(); + assert!( + decoded.len() >= secretbox::MACBYTES, + "decoded encrypted payload must be at least MACBYTES" + ); + } + + // Test decryption fallback when data was encrypted with key_pair but decryption tries machine_uid first + #[test] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn test_decrypt_with_pk_fallback() { + use sodiumoxide::crypto::secretbox; + use std::convert::TryInto; + + let uuid = crate::get_uuid(); + let pk = crate::config::Config::get_key_pair().1; + + // Ensure uuid != pk, otherwise fallback branch won't be tested + if uuid == pk { + eprintln!("skip: uuid == pk, fallback branch won't be tested"); + return; + } + + let data = b"test password 123"; + let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]); + + // Encrypt with pk (simulating machine_uid failure during encryption) + let mut pk_keybuf = pk; + pk_keybuf.resize(secretbox::KEYBYTES, 0); + let pk_key = secretbox::Key(pk_keybuf.try_into().unwrap()); + let encrypted = secretbox::seal(data, &nonce, &pk_key); + + // Decrypt using symmetric_crypt (should fallback to pk since uuid differs) + let decrypted = super::symmetric_crypt(&encrypted, false); + assert!( + decrypted.is_ok(), + "Decryption with pk fallback should succeed" + ); + assert_eq!(decrypted.unwrap(), data); + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/platform/linux.rs b/vendor/rustdesk/libs/hbb_common/src/platform/linux.rs new file mode 100644 index 0000000..d4b29bb --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/platform/linux.rs @@ -0,0 +1,572 @@ +use crate::ResultType; +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(); +} + +// to-do: There seems to be some runtime issue that causes the audit logs to be generated. +// We may need to fix this and remove this workaround in the future. +// +// We use the pre-search method to find the command path to avoid the audit logs on some systems. +// No idea why the audit logs happen. +// Though the audit logs may disappear after rebooting. +// +// See https://github.com/rustdesk/rustdesk/discussions/11959 +// +// `ausearch -x /usr/share/rustdesk/rustdesk` will return +// ... +// time->Tue Jun 24 10:40:43 2025 +// type=PROCTITLE msg=audit(1750776043.446:192757): proctitle=2F7573722F62696E2F727573746465736B002D2D73657276696365 +// type=PATH msg=audit(1750776043.446:192757): item=0 name="/usr/local/bin/sh" nametype=UNKNOWN cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0 +// type=CWD msg=audit(1750776043.446:192757): cwd="/" +// type=SYSCALL msg=audit(1750776043.446:192757): arch=c000003e syscall=59 success=no exit=-2 a0=7fb7dbd22da0 a1=1d65f2c0 a2=7ffc25193360 a3=7ffc25194ec0 items=1 ppid=172208 pid=267565 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="rustdesk" exe="/usr/share/rustdesk/rustdesk" subj=unconfined key="processos_criados" +// ---- +// time->Tue Jun 24 10:40:43 2025 +// type=PROCTITLE msg=audit(1750776043.446:192758): proctitle=2F7573722F62696E2F727573746465736B002D2D73657276696365 +// type=PATH msg=audit(1750776043.446:192758): item=0 name="/usr/sbin/sh" nametype=UNKNOWN cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0 +// ... +lazy_static::lazy_static! { + pub static ref CMD_LOGINCTL: String = find_cmd_path("loginctl"); + pub static ref CMD_PS: String = find_cmd_path("ps"); + pub static ref CMD_SH: String = find_cmd_path("sh"); +} + +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 } + } +} + +fn find_cmd_path(cmd: &'static str) -> String { + let test_cmd = format!("/bin/{}", cmd); + if std::path::Path::new(&test_cmd).exists() { + return test_cmd; + } + let test_cmd = format!("/usr/bin/{}", cmd); + if std::path::Path::new(&test_cmd).exists() { + return test_cmd; + } + if let Ok(output) = Command::new("which").arg(cmd).output() { + if output.status.success() { + return String::from_utf8_lossy(&output.stdout).trim().to_string(); + } + } + cmd.to_string() +} + +// Deprecated. Use `hbb_common::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 `hbb_common::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()) +} + +pub fn run_cmds_trim_newline(cmds: &str) -> ResultType { + let output = std::process::Command::new(CMD_SH.as_str()) + .args(vec!["-c", cmds]) + .output()?; + let out = String::from_utf8_lossy(&output.stdout); + Ok(if out.ends_with('\n') { + out[..out.len() - 1].to_string() + } else { + out.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(()); + } + } + crate::bail!("failed to post system message"); +} + +#[derive(Debug, Clone)] +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, +} + +// Retrieves information about all connected displays via the Wayland protocol. +pub fn get_wayland_displays() -> 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!(); + } + + sctk::delegate_output!(WaylandEnv); + sctk::delegate_registry!(WaylandEnv); + + let conn = Connection::connect_to_env()?; + 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) { + let (x, y) = 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; + display_infos.push(WaylandDisplayInfo { + name, + x, + y, + width, + height, + logical_size, + refresh_rate, + }); + } + }); + } + } + + 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_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/vendor/rustdesk/libs/hbb_common/src/platform/macos.rs b/vendor/rustdesk/libs/hbb_common/src/platform/macos.rs new file mode 100644 index 0000000..dd83a87 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/platform/macos.rs @@ -0,0 +1,55 @@ +use crate::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/vendor/rustdesk/libs/hbb_common/src/platform/mod.rs b/vendor/rustdesk/libs/hbb_common/src/platform/mod.rs new file mode 100644 index 0000000..6818add --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/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 crate::{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/vendor/rustdesk/libs/hbb_common/src/platform/windows.rs b/vendor/rustdesk/libs/hbb_common/src/platform/windows.rs new file mode 100644 index 0000000..7481631 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/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/vendor/rustdesk/libs/hbb_common/src/protos/mod.rs b/vendor/rustdesk/libs/hbb_common/src/protos/mod.rs new file mode 100644 index 0000000..57d9b68 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/protos/mod.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); diff --git a/vendor/rustdesk/libs/hbb_common/src/proxy.rs b/vendor/rustdesk/libs/hbb_common/src/proxy.rs new file mode 100644 index 0000000..d3b8a76 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/proxy.rs @@ -0,0 +1,716 @@ +use std::{ + io::Error as IoError, + net::{SocketAddr, ToSocketAddrs}, +}; + +use anyhow::bail; +use async_recursion::async_recursion; +use base64::{engine::general_purpose, Engine}; +use httparse::{Error as HttpParseError, Response, EMPTY_HEADER}; +use thiserror::Error as ThisError; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufStream}; +use tokio_native_tls::{native_tls, TlsConnector, TlsStream}; +use tokio_rustls::{client::TlsStream as RustlsTlsStream, TlsConnector as RustlsTlsConnector}; +use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr, TargetAddr}; +use tokio_util::codec::Framed; +use url::Url; + +use crate::{ + bytes_codec::BytesCodec, + config::Socks5Server, + tcp::{DynTcpStream, FramedStream}, + tls::{get_cached_tls_accept_invalid_cert, get_cached_tls_type, upsert_tls_cache, TlsType}, + ResultType, +}; + +#[derive(Debug, ThisError)] +pub enum ProxyError { + #[error("IO Error: {0}")] + IoError(#[from] IoError), + #[error("Target parse error: {0}")] + TargetParseError(String), + #[error("HTTP parse error: {0}")] + HttpParseError(#[from] HttpParseError), + #[error("The maximum response header length is exceeded: {0}")] + MaximumResponseHeaderLengthExceeded(usize), + #[error("The end of file is reached")] + EndOfFile, + #[error("The url is error: {0}")] + UrlBadScheme(String), + #[error("The url parse error: {0}")] + UrlParseScheme(#[from] url::ParseError), + #[error("No HTTP code was found in the response")] + NoHttpCode, + #[error("The HTTP code is not equal 200: {0}")] + HttpCode200(u16), + #[error("The proxy address resolution failed: {0}")] + AddressResolutionFailed(String), + #[error("The native tls error: {0}")] + NativeTlsError(#[from] tokio_native_tls::native_tls::Error), +} + +const MAXIMUM_RESPONSE_HEADER_LENGTH: usize = 4096; +/// The maximum HTTP Headers, which can be parsed. +const MAXIMUM_RESPONSE_HEADERS: usize = 16; +const DEFINE_TIME_OUT: u64 = 600; + +pub trait IntoUrl { + // Besides parsing as a valid `Url`, the `Url` must be a valid + // `http::Uri`, in that it makes sense to use in a network request. + fn into_url(self) -> Result; + + fn as_str(&self) -> &str; +} + +impl IntoUrl for Url { + fn into_url(self) -> Result { + if self.has_host() { + Ok(self) + } else { + Err(ProxyError::UrlBadScheme(self.to_string())) + } + } + + fn as_str(&self) -> &str { + self.as_ref() + } +} + +impl<'a> IntoUrl for &'a str { + fn into_url(self) -> Result { + Url::parse(self) + .map_err(ProxyError::UrlParseScheme)? + .into_url() + } + + fn as_str(&self) -> &str { + self + } +} + +impl<'a> IntoUrl for &'a String { + fn into_url(self) -> Result { + (&**self).into_url() + } + + fn as_str(&self) -> &str { + self.as_ref() + } +} + +impl<'a> IntoUrl for String { + fn into_url(self) -> Result { + (&*self).into_url() + } + + fn as_str(&self) -> &str { + self.as_ref() + } +} + +#[derive(Clone)] +pub struct Auth { + user_name: String, + password: String, +} + +impl Auth { + fn get_proxy_authorization(&self) -> String { + format!( + "Proxy-Authorization: Basic {}\r\n", + self.get_basic_authorization() + ) + } + + pub fn get_basic_authorization(&self) -> String { + let authorization = format!("{}:{}", &self.user_name, &self.password); + general_purpose::STANDARD.encode(authorization.as_bytes()) + } + + pub fn username(&self) -> &str { + &self.user_name + } + + pub fn password(&self) -> &str { + &self.password + } +} + +#[derive(Clone)] +pub enum ProxyScheme { + Http { + auth: Option, + host: String, + }, + Https { + auth: Option, + host: String, + }, + Socks5 { + addr: SocketAddr, + auth: Option, + remote_dns: bool, + }, +} + +impl ProxyScheme { + pub fn maybe_auth(&self) -> Option<&Auth> { + match self { + ProxyScheme::Http { auth, .. } + | ProxyScheme::Https { auth, .. } + | ProxyScheme::Socks5 { auth, .. } => auth.as_ref(), + } + } + + fn socks5(addr: SocketAddr) -> Result { + Ok(ProxyScheme::Socks5 { + addr, + auth: None, + remote_dns: false, + }) + } + + fn http(host: &str) -> Result { + Ok(ProxyScheme::Http { + auth: None, + host: host.to_string(), + }) + } + fn https(host: &str) -> Result { + Ok(ProxyScheme::Https { + auth: None, + host: host.to_string(), + }) + } + + fn set_basic_auth, U: Into>(&mut self, username: T, password: U) { + let auth = Auth { + user_name: username.into(), + password: password.into(), + }; + match self { + ProxyScheme::Http { auth: a, .. } => *a = Some(auth), + ProxyScheme::Https { auth: a, .. } => *a = Some(auth), + ProxyScheme::Socks5 { auth: a, .. } => *a = Some(auth), + } + } + + fn parse(url: Url) -> Result { + use url::Position; + + // Resolve URL to a host and port + let to_addr = || { + let addrs = url.socket_addrs(|| match url.scheme() { + "socks5" => Some(1080), + _ => None, + })?; + addrs + .into_iter() + .next() + .ok_or_else(|| ProxyError::UrlParseScheme(url::ParseError::EmptyHost)) + }; + + let mut scheme: Self = match url.scheme() { + "http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?, + "https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?, + "socks5" => Self::socks5(to_addr()?)?, + e => return Err(ProxyError::UrlBadScheme(e.to_string())), + }; + + if let Some(pwd) = url.password() { + let username = url.username(); + scheme.set_basic_auth(username, pwd); + } + + Ok(scheme) + } + pub async fn socket_addrs(&self) -> Result { + log::trace!("Resolving socket address"); + match self { + ProxyScheme::Http { host, .. } => self.resolve_host(host, 80).await, + ProxyScheme::Https { host, .. } => self.resolve_host(host, 443).await, + ProxyScheme::Socks5 { addr, .. } => Ok(addr.clone()), + } + } + + async fn resolve_host(&self, host: &str, default_port: u16) -> Result { + let (host_str, port) = match host.split_once(':') { + Some((h, p)) => (h, p.parse::().ok()), + None => (host, None), + }; + let addr = (host_str, port.unwrap_or(default_port)) + .to_socket_addrs()? + .next() + .ok_or_else(|| ProxyError::AddressResolutionFailed(host.to_string()))?; + Ok(addr) + } + + pub fn get_domain(&self) -> Result { + match self { + ProxyScheme::Http { host, .. } | ProxyScheme::Https { host, .. } => { + let domain = host + .split(':') + .next() + .ok_or_else(|| ProxyError::AddressResolutionFailed(host.clone()))?; + Ok(domain.to_string()) + } + ProxyScheme::Socks5 { addr, .. } => match addr { + SocketAddr::V4(addr_v4) => Ok(addr_v4.ip().to_string()), + SocketAddr::V6(addr_v6) => Ok(addr_v6.ip().to_string()), + }, + } + } + pub fn get_host_and_port(&self) -> Result { + match self { + ProxyScheme::Http { host, .. } => Ok(self.append_default_port(host, 80)), + ProxyScheme::Https { host, .. } => Ok(self.append_default_port(host, 443)), + ProxyScheme::Socks5 { addr, .. } => Ok(format!("{}", addr)), + } + } + fn append_default_port(&self, host: &str, default_port: u16) -> String { + if host.contains(':') { + host.to_string() + } else { + format!("{}:{}", host, default_port) + } + } +} + +pub trait IntoProxyScheme { + fn into_proxy_scheme(self) -> Result; +} + +impl IntoProxyScheme for S { + fn into_proxy_scheme(self) -> Result { + // validate the URL + let url = match self.as_str().into_url() { + Ok(ok) => ok, + Err(e) => { + match e { + // If the string does not contain protocol headers, try to parse it using the socks5 protocol + ProxyError::UrlParseScheme(_source) => { + let try_this = format!("socks5://{}", self.as_str()); + try_this.into_url()? + } + _ => { + return Err(e); + } + } + } + }; + ProxyScheme::parse(url) + } +} + +impl IntoProxyScheme for ProxyScheme { + fn into_proxy_scheme(self) -> Result { + Ok(self) + } +} + +#[derive(Clone)] +pub struct Proxy { + pub intercept: ProxyScheme, + ms_timeout: u64, +} + +impl Proxy { + pub fn new(proxy_scheme: U, ms_timeout: u64) -> Result { + Ok(Self { + intercept: proxy_scheme.into_proxy_scheme()?, + ms_timeout, + }) + } + + pub fn is_http_or_https(&self) -> bool { + return match self.intercept { + ProxyScheme::Socks5 { .. } => false, + _ => true, + }; + } + + pub fn from_conf(conf: &Socks5Server, ms_timeout: Option) -> Result { + let mut proxy; + match ms_timeout { + None => { + proxy = Self::new(&conf.proxy, DEFINE_TIME_OUT)?; + } + Some(time_out) => { + proxy = Self::new(&conf.proxy, time_out)?; + } + } + + if !conf.password.is_empty() && !conf.username.is_empty() { + proxy = proxy.basic_auth(&conf.username, &conf.password); + } + Ok(proxy) + } + + pub async fn proxy_addrs(&self) -> Result { + self.intercept.socket_addrs().await + } + + fn basic_auth(mut self, username: &str, password: &str) -> Proxy { + self.intercept.set_basic_auth(username, password); + self + } + + async fn new_stream( + &self, + local: SocketAddr, + proxy: SocketAddr, + ) -> ResultType { + let stream = super::timeout( + self.ms_timeout, + crate::tcp::new_socket(local, true)?.connect(proxy), + ) + .await??; + stream.set_nodelay(true).ok(); + Ok(stream) + } + + pub async fn connect<'t, T>( + &self, + target: T, + local_addr: Option, + ) -> ResultType + where + T: IntoTargetAddr<'t>, + { + log::trace!("Connect to proxy server"); + let proxy = self.proxy_addrs().await?; + + let target_addr = target + .into_target_addr() + .map_err(|e| ProxyError::TargetParseError(e.to_string()))?; + + let local = if let Some(addr) = local_addr { + addr + } else { + crate::config::Config::get_any_listen_addr(proxy.is_ipv4()) + }; + + let stream = self.new_stream(local, proxy).await?; + let addr = stream.local_addr()?; + + return match self.intercept { + ProxyScheme::Http { .. } => { + log::trace!("Connect to remote http proxy server: {}", proxy); + let stream = + super::timeout(self.ms_timeout, self.http_connect(stream, &target_addr)) + .await??; + Ok(FramedStream( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + )) + } + ProxyScheme::Https { .. } => { + log::trace!("Connect to remote https proxy server: {}", proxy); + let url = format!("https://{}", self.intercept.get_host_and_port()?); + let tls_type = get_cached_tls_type(&url); + let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(&url); + let stream = match tls_type.unwrap_or(TlsType::Rustls) { + TlsType::Rustls => { + self.https_connect_rustls_wrap_danger( + &url, + local, + proxy, + Some(stream), + &target_addr, + tls_type.is_some(), + danger_accept_invalid_cert, + danger_accept_invalid_cert, + ) + .await? + } + TlsType::NativeTls => { + self.https_connect_nativetls_wrap_danger( + &url, + local, + proxy, + &target_addr, + danger_accept_invalid_cert, + ) + .await? + } + _ => { + // Unreachable + crate::bail!("Unreachable, TlsType::Plain in HTTPS proxy"); + } + }; + Ok(FramedStream( + Framed::new(stream, BytesCodec::new()), + addr, + None, + 0, + )) + } + ProxyScheme::Socks5 { .. } => { + log::trace!("Connect to remote socket5 proxy server: {}", proxy); + let stream = if let Some(auth) = self.intercept.maybe_auth() { + super::timeout( + self.ms_timeout, + Socks5Stream::connect_with_password_and_socket( + stream, + target_addr, + &auth.user_name, + &auth.password, + ), + ) + .await?? + } else { + super::timeout( + self.ms_timeout, + Socks5Stream::connect_with_socket(stream, target_addr), + ) + .await?? + }; + Ok(FramedStream( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + )) + } + }; + } + + async fn https_connect_nativetls_wrap_danger<'a>( + &self, + url: &str, + local: SocketAddr, + proxy: SocketAddr, + target_addr: &TargetAddr<'a>, + danger_accept_invalid_cert: Option, + ) -> ResultType { + let stream = self.new_stream(local, proxy).await?; + let s = super::timeout( + self.ms_timeout, + self.https_connect_nativetls( + stream, + &target_addr, + danger_accept_invalid_cert.unwrap_or(false), + ), + ) + .await??; + upsert_tls_cache( + url, + TlsType::NativeTls, + danger_accept_invalid_cert.unwrap_or(false), + ); + Ok(DynTcpStream(Box::new(s))) + } + + pub async fn https_connect_nativetls<'a, Input>( + &self, + io: Input, + target_addr: &TargetAddr<'a>, + danger_accept_invalid_cert: bool, + ) -> Result>, ProxyError> + where + Input: AsyncRead + AsyncWrite + Unpin, + { + let mut tls_connector_builder = native_tls::TlsConnector::builder(); + if danger_accept_invalid_cert { + tls_connector_builder.danger_accept_invalid_certs(true); + } + let tls_connector = TlsConnector::from(tls_connector_builder.build()?); + let stream = tls_connector + .connect(&self.intercept.get_domain()?, io) + .await?; + self.http_connect(stream, target_addr).await + } + + #[async_recursion] + async fn https_connect_rustls_wrap_danger<'a>( + &self, + url: &str, + local: SocketAddr, + proxy: SocketAddr, + stream: Option, + target_addr: &TargetAddr<'a>, + is_tls_type_cached: bool, + danger_accept_invalid_cert: Option, + origin_danger_accept_invalid_cert: Option, + ) -> ResultType { + let stream = stream.unwrap_or(self.new_stream(local, proxy).await?); + match super::timeout( + self.ms_timeout, + self.https_connect_rustls( + stream, + target_addr, + danger_accept_invalid_cert.unwrap_or(false), + ), + ) + .await? + { + Ok(s) => { + upsert_tls_cache( + &url, + TlsType::Rustls, + danger_accept_invalid_cert.unwrap_or(false), + ); + Ok(DynTcpStream(Box::new(s))) + } + Err(e) => { + // NOTE: Maybe it's better to check if the error is related to TLS here. (ProxyError::IoError(e), or ProxyError::NativeTlsError(e)) + // But we can only get the error when the TLS protocol is TLSv1.1. + // The error message of the following is unclear: + // https://github.com/rustdesk/rustdesk-server-pro/issues/189#issuecomment-1895701480 + // So we just try to fallback unconditionally here. + // + // If the protocol is TLS 1.1, the error is: + // 1. "IO Error: received fatal alert: ProtocolVersion" + // 2. "IO Error: An existing connection was forcibly closed by the remote host. (os error 10054)" on Windows sometimes. + // + // If the cert verification fails, the error is: + // "IO Error: invalid peer certificate: UnknownIssuer" + + let s = if danger_accept_invalid_cert.is_none() { + log::warn!( + "Falling back to rustls-tls (accept invalid cert) for HTTPS proxy server." + ); + self.https_connect_rustls_wrap_danger( + &url, + local, + proxy, + None, + target_addr, + is_tls_type_cached, + Some(true), + origin_danger_accept_invalid_cert, + ) + .await? + } else if !is_tls_type_cached { + log::warn!("Falling back to native-tls for HTTPS proxy server."); + self.https_connect_nativetls_wrap_danger( + &url, + local, + proxy, + &target_addr, + origin_danger_accept_invalid_cert, + ) + .await? + } else { + log::error!( + "Failed to connect to HTTPS proxy server with native-tls: {:?}.", + e + ); + bail!(e) + }; + Ok(s) + } + } + } + + pub async fn https_connect_rustls<'a, Input>( + &self, + io: Input, + target_addr: &TargetAddr<'a>, + danger_accept_invalid_cert: bool, + ) -> Result>, ProxyError> + where + Input: AsyncRead + AsyncWrite + Unpin, + { + use std::convert::TryFrom; + + let url_domain = self.intercept.get_domain()?; + let domain = rustls_pki_types::ServerName::try_from(url_domain.as_str()) + .map_err(|e| ProxyError::AddressResolutionFailed(e.to_string()))? + .to_owned(); + let client_config = crate::verifier::client_config(danger_accept_invalid_cert) + .map_err(|e| ProxyError::IoError(std::io::Error::other(e)))?; + let tls_connector = RustlsTlsConnector::from(std::sync::Arc::new(client_config)); + let stream = tls_connector.connect(domain, io).await?; + self.http_connect(stream, target_addr).await + } + + pub async fn http_connect<'a, Input>( + &self, + io: Input, + target_addr: &TargetAddr<'a>, + ) -> Result, ProxyError> + where + Input: AsyncRead + AsyncWrite + Unpin, + { + let mut stream = BufStream::new(io); + let (domain, port) = get_domain_and_port(target_addr)?; + + let request = self.make_request(&domain, port); + stream.write_all(request.as_bytes()).await?; + stream.flush().await?; + recv_and_check_response(&mut stream).await?; + Ok(stream) + } + + fn make_request(&self, host: &str, port: u16) -> String { + let mut request = format!( + "CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n", + host = host, + port = port + ); + + if let Some(auth) = self.intercept.maybe_auth() { + request = format!("{}{}", request, auth.get_proxy_authorization()); + } + + request.push_str("\r\n"); + request + } +} + +fn get_domain_and_port<'a>(target_addr: &TargetAddr<'a>) -> Result<(String, u16), ProxyError> { + match target_addr { + tokio_socks::TargetAddr::Ip(addr) => Ok((addr.ip().to_string(), addr.port())), + tokio_socks::TargetAddr::Domain(name, port) => Ok((name.to_string(), *port)), + } +} + +async fn get_response(stream: &mut BufStream) -> Result +where + IO: AsyncRead + AsyncWrite + Unpin, +{ + use tokio::io::AsyncBufReadExt; + let mut response = String::new(); + + loop { + if stream.read_line(&mut response).await? == 0 { + return Err(ProxyError::EndOfFile); + } + + if MAXIMUM_RESPONSE_HEADER_LENGTH < response.len() { + return Err(ProxyError::MaximumResponseHeaderLengthExceeded( + response.len(), + )); + } + + if response.ends_with("\r\n\r\n") { + return Ok(response); + } + } +} + +async fn recv_and_check_response(stream: &mut BufStream) -> Result<(), ProxyError> +where + IO: AsyncRead + AsyncWrite + Unpin, +{ + let response_string = get_response(stream).await?; + + let mut response_headers = [EMPTY_HEADER; MAXIMUM_RESPONSE_HEADERS]; + let mut response = Response::new(&mut response_headers); + let response_bytes = response_string.into_bytes(); + response.parse(&response_bytes)?; + + return match response.code { + Some(code) => { + if code == 200 { + Ok(()) + } else { + Err(ProxyError::HttpCode200(code)) + } + } + None => Err(ProxyError::NoHttpCode), + }; +} diff --git a/vendor/rustdesk/libs/hbb_common/src/socket_client.rs b/vendor/rustdesk/libs/hbb_common/src/socket_client.rs new file mode 100644 index 0000000..9178b74 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/socket_client.rs @@ -0,0 +1,348 @@ +#[cfg(feature = "webrtc")] +use crate::webrtc::{self, is_webrtc_endpoint}; +use crate::{ + config::{Config, NetworkType}, + tcp::FramedStream, + udp::FramedSocket, + websocket::{self, check_ws, is_ws_endpoint}, + ResultType, Stream, +}; +use anyhow::Context; +use std::{net::SocketAddr, sync::Arc}; +use tokio::net::{ToSocketAddrs, UdpSocket}; +use tokio_socks::{IntoTargetAddr, TargetAddr}; + +#[inline] +pub fn check_port(host: T, port: i32) -> String { + let host = host.to_string(); + if crate::is_ipv6_str(&host) { + if host.starts_with('[') { + return host; + } + return format!("[{host}]:{port}"); + } + if !host.contains(':') { + return format!("{host}:{port}"); + } + host +} + +#[inline] +pub fn increase_port(host: T, offset: i32) -> String { + let host = host.to_string(); + if crate::is_ipv6_str(&host) { + if host.starts_with('[') { + let tmp: Vec<&str> = host.split("]:").collect(); + if tmp.len() == 2 { + let port: i32 = tmp[1].parse().unwrap_or(0); + if port > 0 { + return format!("{}]:{}", tmp[0], port + offset); + } + } + } + } else if host.contains(':') { + let tmp: Vec<&str> = host.split(':').collect(); + if tmp.len() == 2 { + let port: i32 = tmp[1].parse().unwrap_or(0); + if port > 0 { + return format!("{}:{}", tmp[0], port + offset); + } + } + } + host +} + +pub fn split_host_port(host: T) -> Option<(String, i32)> { + let host = host.to_string(); + if crate::is_ipv6_str(&host) { + if host.starts_with('[') { + let tmp: Vec<&str> = host.split("]:").collect(); + if tmp.len() == 2 { + let port: i32 = tmp[1].parse().unwrap_or(0); + if port > 0 { + return Some((format!("{}]", tmp[0]), port)); + } + } + } + } else if host.contains(':') { + let tmp: Vec<&str> = host.split(':').collect(); + if tmp.len() == 2 { + let port: i32 = tmp[1].parse().unwrap_or(0); + if port > 0 { + return Some((tmp[0].to_string(), port)); + } + } + } + None +} + +pub fn test_if_valid_server(host: &str, test_with_proxy: bool) -> String { + let host = check_port(host, 0); + use std::net::ToSocketAddrs; + + if test_with_proxy && NetworkType::ProxySocks == Config::get_network_type() { + test_if_valid_server_for_proxy_(&host) + } else { + match host.to_socket_addrs() { + Err(err) => err.to_string(), + Ok(_) => "".to_owned(), + } + } +} + +#[inline] +pub fn test_if_valid_server_for_proxy_(host: &str) -> String { + // `&host.into_target_addr()` is defined in `tokio-socs`, but is a common pattern for testing, + // it can be used for both `socks` and `http` proxy. + match &host.into_target_addr() { + Err(err) => err.to_string(), + Ok(_) => "".to_owned(), + } +} + +pub trait IsResolvedSocketAddr { + fn resolve(&self) -> Option<&SocketAddr>; +} + +impl IsResolvedSocketAddr for SocketAddr { + fn resolve(&self) -> Option<&SocketAddr> { + Some(self) + } +} + +impl IsResolvedSocketAddr for String { + fn resolve(&self) -> Option<&SocketAddr> { + None + } +} + +impl IsResolvedSocketAddr for &str { + fn resolve(&self) -> Option<&SocketAddr> { + None + } +} + +// This function checks if the target is a websocket endpoint and connects accordingly. +#[inline] +pub async fn connect_tcp< + 't, + T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display, +>( + target: T, + ms_timeout: u64, +) -> ResultType { + #[cfg(feature = "webrtc")] + if is_webrtc_endpoint(&target.to_string()) { + return Ok(Stream::WebRTC( + webrtc::WebRTCStream::new(&target.to_string(), false, ms_timeout).await?, + )); + } + let target_str = check_ws(&target.to_string()); + if is_ws_endpoint(&target_str) { + return Ok(Stream::WebSocket( + websocket::WsFramedStream::new(target_str, None, None, ms_timeout).await?, + )); + } + connect_tcp_local(target, None, ms_timeout).await +} + +// This function connects directly to the target without checking for websocket endpoints. +pub async fn connect_tcp_local< + 't, + T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display, +>( + target: T, + local: Option, + ms_timeout: u64, +) -> ResultType { + if let Some(conf) = Config::get_socks() { + return Ok(Stream::Tcp( + FramedStream::connect(target, local, &conf, ms_timeout).await?, + )); + } + + if let Some(target_addr) = target.resolve() { + if let Some(local_addr) = local { + if local_addr.is_ipv6() && target_addr.is_ipv4() { + let resolved_target = query_nip_io(target_addr).await?; + return Ok(Stream::Tcp( + FramedStream::new(resolved_target, Some(local_addr), ms_timeout).await?, + )); + } + } + } + + Ok(Stream::Tcp( + FramedStream::new(target, local, ms_timeout).await?, + )) +} + +#[inline] +pub fn is_ipv4(target: &TargetAddr<'_>) -> bool { + match target { + TargetAddr::Ip(addr) => addr.is_ipv4(), + _ => true, + } +} + +#[inline] +pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { + tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port())) + .await? + .find(|x| x.is_ipv6()) + .context("Failed to get ipv6 from nip.io") +} + +#[inline] +pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { + if !ipv4 && crate::is_ipv4_str(&addr) { + if let Some(ip) = addr.split(':').next() { + return addr.replace(ip, &format!("{ip}.nip.io")); + } + } + addr +} + +async fn test_target(target: &str) -> ResultType { + if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await { + if let Ok(addr) = s.peer_addr() { + return Ok(addr); + } + } + tokio::net::lookup_host(target) + .await? + .next() + .context(format!("Failed to look up host for {target}")) +} + +#[inline] +pub async fn new_direct_udp_for(target: &str) -> ResultType<(Arc, SocketAddr)> { + let peer_addr = test_target(target).await?; + let local_addr = Config::get_any_listen_addr(peer_addr.is_ipv4()); + let socket = UdpSocket::bind(local_addr).await?; + Ok((Arc::new(socket), peer_addr)) +} + +#[inline] +pub async fn new_udp_for( + target: &str, + ms_timeout: u64, +) -> ResultType<(FramedSocket, TargetAddr<'static>)> { + let (ipv4, target) = if NetworkType::Direct == Config::get_network_type() { + let addr = test_target(target).await?; + (addr.is_ipv4(), addr.into_target_addr()?) + } else { + (true, target.into_target_addr()?) + }; + Ok(( + new_udp(Config::get_any_listen_addr(ipv4), ms_timeout).await?, + target.to_owned(), + )) +} + +async fn new_udp(local: T, ms_timeout: u64) -> ResultType { + match Config::get_socks() { + None => Ok(FramedSocket::new(local).await?), + Some(conf) => { + let socket = FramedSocket::new_proxy( + conf.proxy.as_str(), + local, + conf.username.as_str(), + conf.password.as_str(), + ms_timeout, + ) + .await?; + Ok(socket) + } + } +} + +pub async fn rebind_udp_for( + target: &str, +) -> ResultType)>> { + if Config::get_network_type() != NetworkType::Direct { + return Ok(None); + } + let addr = test_target(target).await?; + let v4 = addr.is_ipv4(); + Ok(Some(( + FramedSocket::new(Config::get_any_listen_addr(v4)).await?, + addr.into_target_addr()?.to_owned(), + ))) +} + +#[cfg(test)] +mod tests { + use std::net::ToSocketAddrs; + + use super::*; + + #[test] + fn test_nat64() { + test_nat64_async(); + } + + #[tokio::main(flavor = "current_thread")] + async fn test_nat64_async() { + assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), true), "1.1.1.1"); + assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), false), "1.1.1.1.nip.io"); + assert_eq!( + ipv4_to_ipv6("1.1.1.1:8080".to_owned(), false), + "1.1.1.1.nip.io:8080" + ); + assert_eq!( + ipv4_to_ipv6("rustdesk.com".to_owned(), false), + "rustdesk.com" + ); + if ("rustdesk.com:80") + .to_socket_addrs() + .unwrap() + .next() + .unwrap() + .is_ipv6() + { + assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()) + .await + .unwrap() + .is_ipv6()); + return; + } + assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()).await.is_err()); + } + + #[test] + fn test_test_if_valid_server() { + assert!(!test_if_valid_server("a", false).is_empty()); + // on Linux, "1" is resolved to "0.0.0.1" + assert!(test_if_valid_server("1.1.1.1", false).is_empty()); + assert!(test_if_valid_server("1.1.1.1:1", false).is_empty()); + assert!(test_if_valid_server("microsoft.com", false).is_empty()); + assert!(test_if_valid_server("microsoft.com:1", false).is_empty()); + + // with proxy + // `:0` indicates `let host = check_port(host, 0);` is called. + assert!(test_if_valid_server_for_proxy_("a:0").is_empty()); + assert!(test_if_valid_server_for_proxy_("1.1.1.1:0").is_empty()); + assert!(test_if_valid_server_for_proxy_("1.1.1.1:1").is_empty()); + assert!(test_if_valid_server_for_proxy_("abc.com:0").is_empty()); + assert!(test_if_valid_server_for_proxy_("abcd.com:1").is_empty()); + } + + #[test] + fn test_check_port() { + assert_eq!(check_port("[1:2]:12", 32), "[1:2]:12"); + assert_eq!(check_port("1:2", 32), "[1:2]:32"); + assert_eq!(check_port("z1:2", 32), "z1:2"); + assert_eq!(check_port("1.1.1.1", 32), "1.1.1.1:32"); + assert_eq!(check_port("1.1.1.1:32", 32), "1.1.1.1:32"); + assert_eq!(check_port("test.com:32", 0), "test.com:32"); + assert_eq!(increase_port("[1:2]:12", 1), "[1:2]:13"); + assert_eq!(increase_port("1.2.2.4:12", 1), "1.2.2.4:13"); + assert_eq!(increase_port("1.2.2.4", 1), "1.2.2.4"); + assert_eq!(increase_port("test.com", 1), "test.com"); + assert_eq!(increase_port("test.com:13", 4), "test.com:17"); + assert_eq!(increase_port("1:13", 4), "1:13"); + assert_eq!(increase_port("22:1:13", 4), "22:1:13"); + assert_eq!(increase_port("z1:2", 1), "z1:3"); + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/stream.rs b/vendor/rustdesk/libs/hbb_common/src/stream.rs new file mode 100644 index 0000000..a8e6b6c --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/stream.rs @@ -0,0 +1,149 @@ +use crate::{config, tcp, websocket, ResultType}; +#[cfg(feature = "webrtc")] +use crate::webrtc; +use sodiumoxide::crypto::secretbox::Key; +use std::net::SocketAddr; +use tokio::net::TcpStream; + +// support Websocket and tcp. +pub enum Stream { + #[cfg(feature = "webrtc")] + WebRTC(webrtc::WebRTCStream), + WebSocket(websocket::WsFramedStream), + Tcp(tcp::FramedStream), +} + +impl Stream { + #[inline] + pub fn set_send_timeout(&mut self, ms: u64) { + match self { + #[cfg(feature = "webrtc")] + Stream::WebRTC(s) => s.set_send_timeout(ms), + Stream::WebSocket(s) => s.set_send_timeout(ms), + Stream::Tcp(s) => s.set_send_timeout(ms), + } + } + + #[inline] + pub fn set_raw(&mut self) { + match self { + #[cfg(feature = "webrtc")] + Stream::WebRTC(s) => s.set_raw(), + Stream::WebSocket(s) => s.set_raw(), + Stream::Tcp(s) => s.set_raw(), + } + } + + #[inline] + pub async fn send_bytes(&mut self, bytes: bytes::Bytes) -> ResultType<()> { + match self { + #[cfg(feature = "webrtc")] + Stream::WebRTC(s) => s.send_bytes(bytes).await, + Stream::WebSocket(s) => s.send_bytes(bytes).await, + Stream::Tcp(s) => s.send_bytes(bytes).await, + } + } + + #[inline] + pub async fn send_raw(&mut self, bytes: Vec) -> ResultType<()> { + match self { + #[cfg(feature = "webrtc")] + Stream::WebRTC(s) => s.send_raw(bytes).await, + Stream::WebSocket(s) => s.send_raw(bytes).await, + Stream::Tcp(s) => s.send_raw(bytes).await, + } + } + + #[inline] + pub fn set_key(&mut self, key: Key) { + match self { + #[cfg(feature = "webrtc")] + Stream::WebRTC(s) => s.set_key(key), + Stream::WebSocket(s) => s.set_key(key), + Stream::Tcp(s) => s.set_key(key), + } + } + + #[inline] + pub fn is_secured(&self) -> bool { + match self { + #[cfg(feature = "webrtc")] + Stream::WebRTC(s) => s.is_secured(), + Stream::WebSocket(s) => s.is_secured(), + Stream::Tcp(s) => s.is_secured(), + } + } + + #[inline] + pub async fn next_timeout( + &mut self, + timeout: u64, + ) -> Option> { + match self { + #[cfg(feature = "webrtc")] + Stream::WebRTC(s) => s.next_timeout(timeout).await, + Stream::WebSocket(s) => s.next_timeout(timeout).await, + Stream::Tcp(s) => s.next_timeout(timeout).await, + } + } + + /// establish connect from websocket + #[inline] + pub async fn connect_websocket( + url: impl AsRef, + local_addr: Option, + proxy_conf: Option<&config::Socks5Server>, + timeout_ms: u64, + ) -> ResultType { + let ws_stream = + websocket::WsFramedStream::new(url, local_addr, proxy_conf, timeout_ms).await?; + log::debug!("WebSocket connection established"); + Ok(Self::WebSocket(ws_stream)) + } + + /// send message + #[inline] + pub async fn send(&mut self, msg: &impl protobuf::Message) -> ResultType<()> { + match self { + #[cfg(feature = "webrtc")] + Self::WebRTC(s) => s.send(msg).await, + Self::WebSocket(ws) => ws.send(msg).await, + Self::Tcp(tcp) => tcp.send(msg).await, + } + } + + /// receive message + #[inline] + pub async fn next(&mut self) -> Option> { + match self { + #[cfg(feature = "webrtc")] + Self::WebRTC(s) => s.next().await, + Self::WebSocket(ws) => ws.next().await, + Self::Tcp(tcp) => tcp.next().await, + } + } + + #[inline] + pub fn local_addr(&self) -> SocketAddr { + match self { + #[cfg(feature = "webrtc")] + Self::WebRTC(s) => s.local_addr(), + Self::WebSocket(ws) => ws.local_addr(), + Self::Tcp(tcp) => tcp.local_addr(), + } + } + + #[inline] + pub fn from(stream: TcpStream, stream_addr: SocketAddr) -> Self { + Self::Tcp(tcp::FramedStream::from(stream, stream_addr)) + } + + #[inline] + #[cfg(feature = "webrtc")] + pub fn get_webrtc_stream(&self) -> Option { + match self { + Self::WebRTC(s) => Some(s.clone()), + _ => None, + } + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/tcp.rs b/vendor/rustdesk/libs/hbb_common/src/tcp.rs new file mode 100644 index 0000000..2296edb --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/tcp.rs @@ -0,0 +1,344 @@ +use crate::{bail, bytes_codec::BytesCodec, ResultType, config::Socks5Server, proxy::Proxy}; +use anyhow::Context as AnyhowCtx; +use bytes::{BufMut, Bytes, BytesMut}; +use futures::{SinkExt, StreamExt}; +use protobuf::Message; +use sodiumoxide::crypto::{ + box_, + secretbox::{self, Key, Nonce}, +}; +use std::{ + io::{self, Error, ErrorKind}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + ops::{Deref, DerefMut}, + pin::Pin, + task::{Context, Poll}, +}; +use tokio::{ + io::{AsyncRead, AsyncWrite, ReadBuf}, + net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs}, +}; +use tokio_socks::IntoTargetAddr; +use tokio_util::codec::Framed; + +pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {} +pub struct DynTcpStream(pub Box); + +#[derive(Clone)] +pub struct Encrypt(pub Key, pub u64, pub u64); + +pub struct FramedStream( + pub Framed, + pub SocketAddr, + pub Option, + pub u64, +); + +impl Deref for FramedStream { + type Target = Framed; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for FramedStream { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Deref for DynTcpStream { + type Target = Box; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for DynTcpStream { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub(crate) fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result { + let socket = match addr { + std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?, + std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?, + }; + if reuse { + // windows has no reuse_port, but its reuse_address + // almost equals to unix's reuse_port + reuse_address, + // though may introduce nondeterministic behavior + // illumos has no support for SO_REUSEPORT + #[cfg(all(unix, not(target_os = "illumos")))] + socket.set_reuseport(true).ok(); + socket.set_reuseaddr(true).ok(); + } + socket.bind(addr)?; + Ok(socket) +} + +impl FramedStream { + pub async fn new( + remote_addr: T, + local_addr: Option, + ms_timeout: u64, + ) -> ResultType { + for remote_addr in lookup_host(&remote_addr).await? { + let local = if let Some(addr) = local_addr { + addr + } else { + crate::config::Config::get_any_listen_addr(remote_addr.is_ipv4()) + }; + if let Ok(socket) = new_socket(local, true) { + if let Ok(Ok(stream)) = + super::timeout(ms_timeout, socket.connect(remote_addr)).await + { + stream.set_nodelay(true).ok(); + let addr = stream.local_addr()?; + return Ok(Self( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + )); + } + } + } + bail!(format!("Failed to connect to {remote_addr}")); + } + + pub async fn connect<'t, T>( + target: T, + local_addr: Option, + proxy_conf: &Socks5Server, + ms_timeout: u64, + ) -> ResultType + where + T: IntoTargetAddr<'t>, + { + let proxy = Proxy::from_conf(proxy_conf, Some(ms_timeout))?; + proxy.connect::(target, local_addr).await + } + + pub fn local_addr(&self) -> SocketAddr { + self.1 + } + + pub fn set_send_timeout(&mut self, ms: u64) { + self.3 = ms; + } + + pub fn from(stream: impl TcpStreamTrait + Send + Sync + 'static, addr: SocketAddr) -> Self { + Self( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + ) + } + + pub fn set_raw(&mut self) { + self.0.codec_mut().set_raw(); + self.2 = None; + } + + pub fn is_secured(&self) -> bool { + self.2.is_some() + } + + #[inline] + pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> { + self.send_raw(msg.write_to_bytes()?).await + } + + #[inline] + pub async fn send_raw(&mut self, msg: Vec) -> ResultType<()> { + let mut msg = msg; + if let Some(key) = self.2.as_mut() { + msg = key.enc(&msg); + } + self.send_bytes(bytes::Bytes::from(msg)).await?; + Ok(()) + } + + #[inline] + pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> { + if self.3 > 0 { + super::timeout(self.3, self.0.send(bytes)).await??; + } else { + self.0.send(bytes).await?; + } + Ok(()) + } + + #[inline] + pub async fn next(&mut self) -> Option> { + let mut res = self.0.next().await; + if let Some(Ok(bytes)) = res.as_mut() { + if let Some(key) = self.2.as_mut() { + if let Err(err) = key.dec(bytes) { + return Some(Err(err)); + } + } + } + res + } + + #[inline] + pub async fn next_timeout(&mut self, ms: u64) -> Option> { + if let Ok(res) = super::timeout(ms, self.next()).await { + res + } else { + None + } + } + + pub fn set_key(&mut self, key: Key) { + self.2 = Some(Encrypt::new(key)); + } + + fn get_nonce(seqnum: u64) -> Nonce { + let mut nonce = Nonce([0u8; secretbox::NONCEBYTES]); + nonce.0[..std::mem::size_of_val(&seqnum)].copy_from_slice(&seqnum.to_le_bytes()); + nonce + } +} + +const DEFAULT_BACKLOG: u32 = 128; + +pub async fn new_listener(addr: T, reuse: bool) -> ResultType { + if !reuse { + Ok(TcpListener::bind(addr).await?) + } else { + let addr = lookup_host(&addr) + .await? + .next() + .context("could not resolve to any address")?; + new_socket(addr, true)? + .listen(DEFAULT_BACKLOG) + .map_err(anyhow::Error::msg) + } +} + +pub async fn listen_any(port: u16) -> ResultType { + if let Ok(mut socket) = TcpSocket::new_v6() { + #[cfg(unix)] + { + // illumos has no support for SO_REUSEPORT + #[cfg(not(target_os = "illumos"))] + socket.set_reuseport(true).ok(); + socket.set_reuseaddr(true).ok(); + use std::os::unix::io::{FromRawFd, IntoRawFd}; + let raw_fd = socket.into_raw_fd(); + let sock2 = unsafe { socket2::Socket::from_raw_fd(raw_fd) }; + sock2.set_only_v6(false).ok(); + socket = unsafe { TcpSocket::from_raw_fd(sock2.into_raw_fd()) }; + } + #[cfg(windows)] + { + use std::os::windows::prelude::{FromRawSocket, IntoRawSocket}; + let raw_socket = socket.into_raw_socket(); + let sock2 = unsafe { socket2::Socket::from_raw_socket(raw_socket) }; + sock2.set_only_v6(false).ok(); + socket = unsafe { TcpSocket::from_raw_socket(sock2.into_raw_socket()) }; + } + if socket + .bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)) + .is_ok() + { + if let Ok(l) = socket.listen(DEFAULT_BACKLOG) { + return Ok(l); + } + } + } + Ok(new_socket( + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port), + true, + )? + .listen(DEFAULT_BACKLOG)?) +} + +impl Unpin for DynTcpStream {} + +impl AsyncRead for DynTcpStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf) + } +} + +impl AsyncWrite for DynTcpStream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_flush(Pin::new(&mut self.0), cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx) + } +} + +impl TcpStreamTrait for R {} + +impl Encrypt { + pub fn new(key: Key) -> Self { + Self(key, 0, 0) + } + + pub fn dec(&mut self, bytes: &mut BytesMut) -> Result<(), Error> { + if bytes.len() <= 1 { + return Ok(()); + } + self.2 += 1; + let nonce = FramedStream::get_nonce(self.2); + match secretbox::open(bytes, &nonce, &self.0) { + Ok(res) => { + bytes.clear(); + bytes.put_slice(&res); + Ok(()) + } + Err(()) => Err(Error::new(ErrorKind::Other, "decryption error")), + } + } + + pub fn enc(&mut self, data: &[u8]) -> Vec { + self.1 += 1; + let nonce = FramedStream::get_nonce(self.1); + secretbox::seal(&data, &nonce, &self.0) + } + + pub fn decode( + symmetric_data: &[u8], + their_pk_b: &[u8], + our_sk_b: &box_::SecretKey, + ) -> ResultType { + if their_pk_b.len() != box_::PUBLICKEYBYTES { + anyhow::bail!("Handshake failed: pk length {}", their_pk_b.len()); + } + let nonce = box_::Nonce([0u8; box_::NONCEBYTES]); + let mut pk_ = [0u8; box_::PUBLICKEYBYTES]; + pk_[..].copy_from_slice(their_pk_b); + let their_pk_b = box_::PublicKey(pk_); + let symmetric_key = box_::open(symmetric_data, &nonce, &their_pk_b, &our_sk_b) + .map_err(|_| anyhow::anyhow!("Handshake failed: box decryption failure"))?; + if symmetric_key.len() != secretbox::KEYBYTES { + anyhow::bail!("Handshake failed: invalid secret key length from peer"); + } + let mut key = [0u8; secretbox::KEYBYTES]; + key[..].copy_from_slice(&symmetric_key); + Ok(Key(key)) + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/tls.rs b/vendor/rustdesk/libs/hbb_common/src/tls.rs new file mode 100644 index 0000000..b086236 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/tls.rs @@ -0,0 +1,121 @@ +use std::{collections::HashMap, sync::RwLock}; + +use crate::config::allow_insecure_tls_fallback; + +#[derive(Debug, Clone, Copy)] +pub enum TlsType { + Plain, + NativeTls, + Rustls, +} + +lazy_static::lazy_static! { + static ref URL_TLS_TYPE: RwLock> = RwLock::new(HashMap::new()); + static ref URL_TLS_DANGER_ACCEPT_INVALID_CERTS: RwLock> = RwLock::new(HashMap::new()); +} + +#[inline] +pub fn is_plain(url: &str) -> bool { + url.starts_with("ws://") || url.starts_with("http://") +} + +// Extract domain from URL. +// e.g., "https://example.com/path" -> "example.com" +// "https://example.com:8080/path" -> "example.com:8080" +// See the tests for more examples. +#[inline] +fn get_domain_and_port_from_url(url: &str) -> &str { + // Remove scheme (e.g., http://, https://, ws://, wss://) + let scheme_end = url.find("://").map(|pos| pos + 3).unwrap_or(0); + let url2 = &url[scheme_end..]; + // If userinfo is present, domain is after last '@' + let after_at = match url2.rfind('@') { + Some(pos) => &url2[pos + 1..], + None => url2, + }; + // Find the end of domain (before '/' or '?') + let domain_end = after_at.find(&['/', '?'][..]).unwrap_or(after_at.len()); + &after_at[..domain_end] +} + +#[inline] +pub fn upsert_tls_cache(url: &str, tls_type: TlsType, danger_accept_invalid_cert: bool) { + if is_plain(url) { + return; + } + + let domain_port = get_domain_and_port_from_url(url); + // Use curly braces to ensure the lock is released immediately. + { + URL_TLS_TYPE + .write() + .unwrap() + .insert(domain_port.to_string(), tls_type); + } + { + URL_TLS_DANGER_ACCEPT_INVALID_CERTS + .write() + .unwrap() + .insert(domain_port.to_string(), danger_accept_invalid_cert); + } +} + +#[inline] +pub fn reset_tls_cache() { + // Use curly braces to ensure the lock is released immediately. + { + URL_TLS_TYPE.write().unwrap().clear(); + } + { + URL_TLS_DANGER_ACCEPT_INVALID_CERTS.write().unwrap().clear(); + } +} + +#[inline] +pub fn get_cached_tls_type(url: &str) -> Option { + if is_plain(url) { + return Some(TlsType::Plain); + } + let domain_port = get_domain_and_port_from_url(url); + URL_TLS_TYPE.read().unwrap().get(domain_port).cloned() +} + +#[inline] +pub fn get_cached_tls_accept_invalid_cert(url: &str) -> Option { + if !allow_insecure_tls_fallback() { + return Some(false); + } + + if is_plain(url) { + return Some(false); + } + let domain_port = get_domain_and_port_from_url(url); + URL_TLS_DANGER_ACCEPT_INVALID_CERTS + .read() + .unwrap() + .get(domain_port) + .cloned() +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_domain_and_port_from_url() { + for (url, expected_domain_port) in vec![ + ("http://example.com", "example.com"), + ("https://example.com", "example.com"), + ("ws://example.com/path", "example.com"), + ("wss://example.com:8080/path", "example.com:8080"), + ("https://user:pass@example.com", "example.com"), + ("https://example.com?query=param", "example.com"), + ("https://example.com:8443?query=param", "example.com:8443"), + ("ftp://example.com/resource", "example.com"), // ftp scheme + ("example.com/path", "example.com"), // no scheme + ("example.com:8080/path", "example.com:8080"), + ] { + let domain_port = get_domain_and_port_from_url(url); + assert_eq!(domain_port, expected_domain_port); + } + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/udp.rs b/vendor/rustdesk/libs/hbb_common/src/udp.rs new file mode 100644 index 0000000..fbdf332 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/udp.rs @@ -0,0 +1,171 @@ +use crate::ResultType; +use anyhow::{anyhow, Context}; +use bytes::{Bytes, BytesMut}; +use futures::{SinkExt, StreamExt}; +use protobuf::Message; +use socket2::{Domain, Socket, Type}; +use std::net::SocketAddr; +use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket}; +use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs}; +use tokio_util::{codec::BytesCodec, udp::UdpFramed}; + +pub enum FramedSocket { + Direct(UdpFramed), + ProxySocks(Socks5UdpFramed), +} + +fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result { + let socket = match addr { + SocketAddr::V4(..) => Socket::new(Domain::ipv4(), Type::dgram(), None), + SocketAddr::V6(..) => Socket::new(Domain::ipv6(), Type::dgram(), None), + }?; + if reuse { + // windows has no reuse_port, but its reuse_address + // almost equals to unix's reuse_port + reuse_address, + // though may introduce nondeterministic behavior + // illumos has no support for SO_REUSEPORT + #[cfg(all(unix, not(target_os = "illumos")))] + socket.set_reuse_port(true).ok(); + socket.set_reuse_address(true).ok(); + } + // only nonblocking work with tokio, https://stackoverflow.com/questions/64649405/receiver-on-tokiompscchannel-only-receives-messages-when-buffer-is-full + socket.set_nonblocking(true)?; + if buf_size > 0 { + socket.set_recv_buffer_size(buf_size).ok(); + } + log::debug!( + "Receive buf size of udp {}: {:?}", + addr, + socket.recv_buffer_size() + ); + if addr.is_ipv6() && addr.ip().is_unspecified() && addr.port() > 0 { + socket.set_only_v6(false).ok(); + } + socket.bind(&addr.into())?; + Ok(socket) +} + +impl FramedSocket { + pub async fn new(addr: T) -> ResultType { + Self::new_reuse(addr, false, 0).await + } + + pub async fn new_reuse( + addr: T, + reuse: bool, + buf_size: usize, + ) -> ResultType { + let addr = lookup_host(&addr) + .await? + .next() + .context("could not resolve to any address")?; + Ok(Self::Direct(UdpFramed::new( + UdpSocket::from_std(new_socket(addr, reuse, buf_size)?.into_udp_socket())?, + BytesCodec::new(), + ))) + } + + pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>( + proxy: P, + local: T, + username: &'a str, + password: &'a str, + ms_timeout: u64, + ) -> ResultType { + let framed = if username.trim().is_empty() { + super::timeout(ms_timeout, Socks5UdpFramed::connect(proxy, Some(local))).await?? + } else { + super::timeout( + ms_timeout, + Socks5UdpFramed::connect_with_password(proxy, Some(local), username, password), + ) + .await?? + }; + log::trace!( + "Socks5 udp connected, local addr: {:?}, target addr: {}", + framed.local_addr(), + framed.socks_addr() + ); + Ok(Self::ProxySocks(framed)) + } + + #[inline] + pub async fn send( + &mut self, + msg: &impl Message, + addr: impl IntoTargetAddr<'_>, + ) -> ResultType<()> { + let addr = addr.into_target_addr()?.to_owned(); + let send_data = Bytes::from(msg.write_to_bytes()?); + match self { + Self::Direct(f) => { + if let TargetAddr::Ip(addr) = addr { + f.send((send_data, addr)).await? + } + } + Self::ProxySocks(f) => f.send((send_data, addr)).await?, + }; + Ok(()) + } + + // https://stackoverflow.com/a/68733302/1926020 + #[inline] + pub async fn send_raw( + &mut self, + msg: &'static [u8], + addr: impl IntoTargetAddr<'static>, + ) -> ResultType<()> { + let addr = addr.into_target_addr()?.to_owned(); + + match self { + Self::Direct(f) => { + if let TargetAddr::Ip(addr) = addr { + f.send((Bytes::from(msg), addr)).await? + } + } + Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?, + }; + Ok(()) + } + + #[inline] + pub async fn next(&mut self) -> Option)>> { + match self { + Self::Direct(f) => match f.next().await { + Some(Ok((data, addr))) => { + Some(Ok((data, addr.into_target_addr().ok()?.to_owned()))) + } + Some(Err(e)) => Some(Err(anyhow!(e))), + None => None, + }, + Self::ProxySocks(f) => match f.next().await { + Some(Ok((data, _))) => Some(Ok((data.data, data.dst_addr))), + Some(Err(e)) => Some(Err(anyhow!(e))), + None => None, + }, + } + } + + #[inline] + pub async fn next_timeout( + &mut self, + ms: u64, + ) -> Option)>> { + if let Ok(res) = + tokio::time::timeout(std::time::Duration::from_millis(ms), self.next()).await + { + res + } else { + None + } + } + + pub fn local_addr(&self) -> Option { + if let FramedSocket::Direct(x) = self { + if let Ok(v) = x.get_ref().local_addr() { + return Some(v); + } + } + None + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/verifier.rs b/vendor/rustdesk/libs/hbb_common/src/verifier.rs new file mode 100644 index 0000000..9f62854 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/verifier.rs @@ -0,0 +1,257 @@ +use crate::ResultType; +use rustls_pki_types::{ServerName, UnixTime}; +use std::sync::Arc; +use tokio_rustls::rustls::{self, client::WebPkiServerVerifier, ClientConfig}; +use tokio_rustls::rustls::{ + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + DigitallySignedStruct, Error as TLSError, SignatureScheme, +}; + +// https://github.com/seanmonstar/reqwest/blob/fd61bc93e6f936454ce0b978c6f282f06eee9287/src/tls.rs#L608 +#[derive(Debug)] +pub(crate) struct NoVerifier; + +impl ServerCertVerifier for NoVerifier { + fn verify_server_cert( + &self, + _end_entity: &rustls_pki_types::CertificateDer, + _intermediates: &[rustls_pki_types::CertificateDer], + _server_name: &ServerName, + _ocsp_response: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &rustls_pki_types::CertificateDer, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &rustls_pki_types::CertificateDer, + _dss: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + SignatureScheme::RSA_PKCS1_SHA1, + SignatureScheme::ECDSA_SHA1_Legacy, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + ] + } +} + +/// A certificate verifier that tries a primary verifier first, +/// and falls back to a platform verifier if the primary fails. +#[cfg(any(target_os = "android", target_os = "ios"))] +#[derive(Debug)] +struct FallbackPlatformVerifier { + primary: Arc, + fallback: Arc, +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +impl FallbackPlatformVerifier { + fn with_platform_fallback( + primary: Arc, + provider: Arc, + ) -> Result { + #[cfg(target_os = "android")] + if !crate::config::ANDROID_RUSTLS_PLATFORM_VERIFIER_INITIALIZED + .load(std::sync::atomic::Ordering::Relaxed) + { + return Err(TLSError::General( + "rustls-platform-verifier not initialized".to_string(), + )); + } + let fallback = Arc::new(rustls_platform_verifier::Verifier::new(provider)?); + Ok(Self { primary, fallback }) + } +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +impl ServerCertVerifier for FallbackPlatformVerifier { + fn verify_server_cert( + &self, + end_entity: &rustls_pki_types::CertificateDer<'_>, + intermediates: &[rustls_pki_types::CertificateDer<'_>], + server_name: &ServerName<'_>, + ocsp_response: &[u8], + now: UnixTime, + ) -> Result { + match self.primary.verify_server_cert( + end_entity, + intermediates, + server_name, + ocsp_response, + now, + ) { + Ok(verified) => Ok(verified), + Err(primary_err) => { + match self.fallback.verify_server_cert( + end_entity, + intermediates, + server_name, + ocsp_response, + now, + ) { + Ok(verified) => Ok(verified), + Err(fallback_err) => { + log::error!( + "Both primary and fallback verifiers failed to verify server certificate, primary error: {:?}, fallback error: {:?}", + primary_err, + fallback_err + ); + Err(primary_err) + } + } + } + } + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &rustls_pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + // Both WebPkiServerVerifier and rustls_platform_verifier use the same signature verification implementation. + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L278 + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/crypto/mod.rs#L17 + // https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/android.rs#L9 + // https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/apple.rs#L6 + self.primary.verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &rustls_pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + // Same implementation as verify_tls12_signature. + self.primary.verify_tls13_signature(message, cert, dss) + } + + fn supported_verify_schemes(&self) -> Vec { + // Both WebPkiServerVerifier and rustls_platform_verifier use the same crypto provider, + // so their supported signature schemes are identical. + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L172C52-L172C85 + // https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/android.rs#L327 + // https://github.com/rustls/rustls-platform-verifier/blob/1099f161bfc5e3ac7f90aad88b1bf788e72906cb/rustls-platform-verifier/src/verification/apple.rs#L304 + self.primary.supported_verify_schemes() + } +} + +fn webpki_server_verifier( + provider: Arc, +) -> ResultType> { + // Load root certificates from both bundled webpki_roots and system-native certificate stores. + // This approach is consistent with how reqwest and tokio-tungstenite handle root certificates. + // https://github.com/snapview/tokio-tungstenite/blob/35d110c24c9d030d1608ec964d70c789dfb27452/src/tls.rs#L95 + // https://github.com/seanmonstar/reqwest/blob/b126ca49da7897e5d676639cdbf67a0f6838b586/src/async_impl/client.rs#L643 + let mut root_cert_store = rustls::RootCertStore::empty(); + root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + let rustls_native_certs::CertificateResult { certs, errors, .. } = + rustls_native_certs::load_native_certs(); + if !errors.is_empty() { + log::warn!("native root CA certificate loading errors: {errors:?}"); + } + root_cert_store.add_parsable_certificates(certs); + + // Build verifier using with_root_certificates behavior (WebPkiServerVerifier without CRLs). + // Both reqwest and tokio-tungstenite use this approach. + // https://github.com/seanmonstar/reqwest/blob/b126ca49da7897e5d676639cdbf67a0f6838b586/src/async_impl/client.rs#L749 + // https://github.com/snapview/tokio-tungstenite/blob/35d110c24c9d030d1608ec964d70c789dfb27452/src/tls.rs#L127 + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/client/builder.rs#L47 + // with_root_certificates creates a WebPkiServerVerifier without revocation checking: + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L177 + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L168 + // Since no CRL is provided (as is the case here), we must explicitly set allow_unknown_revocation_status() + // to match the behavior of with_root_certificates, which allows unknown revocation status by default. + // https://github.com/rustls/rustls/blob/1ee126adb3352a2dcd72420dcd6040351a6ddc1e/rustls/src/webpki/server_verifier.rs#L37 + // Note: build() only returns an error if the root certificate store is empty, which won't happen here. + let verifier = rustls::client::WebPkiServerVerifier::builder_with_provider( + Arc::new(root_cert_store), + provider.clone(), + ) + .allow_unknown_revocation_status() + .build() + .map_err(|e| anyhow::anyhow!(e))?; + Ok(verifier) +} + +pub fn client_config(danger_accept_invalid_cert: bool) -> ResultType { + if danger_accept_invalid_cert { + client_config_danger() + } else { + client_config_safe() + } +} + +pub fn client_config_safe() -> ResultType { + // Use the default builder which uses the default protocol versions and crypto provider. + // The with_protocol_versions API has been removed in rustls master branch: + // https://github.com/rustls/rustls/pull/2599 + // This approach is consistent with tokio-tungstenite's usage: + // https://github.com/snapview/tokio-tungstenite/blob/35d110c24c9d030d1608ec964d70c789dfb27452/src/tls.rs#L126 + let config_builder = rustls::ClientConfig::builder(); + let provider = config_builder.crypto_provider().clone(); + let webpki_verifier = webpki_server_verifier(provider.clone())?; + #[cfg(any(target_os = "android", target_os = "ios"))] + { + match FallbackPlatformVerifier::with_platform_fallback(webpki_verifier.clone(), provider) { + Ok(fallback_verifier) => { + let config = config_builder + .dangerous() + .with_custom_certificate_verifier(Arc::new(fallback_verifier)) + .with_no_client_auth(); + Ok(config) + } + Err(e) => { + log::error!( + "Failed to create fallback verifier: {:?}, use webpki verifier instead", + e + ); + let config = config_builder + .with_webpki_verifier(webpki_verifier) + .with_no_client_auth(); + Ok(config) + } + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let config = config_builder + .with_webpki_verifier(webpki_verifier) + .with_no_client_auth(); + Ok(config) + } +} + +pub fn client_config_danger() -> ResultType { + let config = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoVerifier)) + .with_no_client_auth(); + Ok(config) +} diff --git a/vendor/rustdesk/libs/hbb_common/src/webrtc.rs b/vendor/rustdesk/libs/hbb_common/src/webrtc.rs new file mode 100644 index 0000000..8f3c410 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/webrtc.rs @@ -0,0 +1,770 @@ +use std::collections::HashMap; +use std::io::{Error, ErrorKind}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::Arc; +use std::time::Duration; + +use webrtc::api::setting_engine::SettingEngine; +use webrtc::api::APIBuilder; +use webrtc::data_channel::RTCDataChannel; +use webrtc::ice::mdns::MulticastDnsMode; +use webrtc::ice_transport::ice_server::RTCIceServer; +use webrtc::peer_connection::configuration::RTCConfiguration; +use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; +use webrtc::peer_connection::policy::ice_transport_policy::RTCIceTransportPolicy; +use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; +use webrtc::peer_connection::RTCPeerConnection; + +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use base64::Engine; +use bytes::{Bytes, BytesMut}; +use tokio::sync::watch; +use tokio::sync::Mutex; +use tokio::time::timeout; +use url::Url; + +use crate::config; +use crate::protobuf::Message; +use crate::sodiumoxide::crypto::secretbox::Key; +use crate::ResultType; + +pub struct WebRTCStream { + pc: Arc, + stream: Arc>>, + state_notify: watch::Receiver, + send_timeout: u64, +} + +/// Standard maximum message size for WebRTC data channels (RFC 8831, 65535 bytes). +/// Most browsers, including Chromium, enforce this protocol limit. +const DATA_CHANNEL_BUFFER_SIZE: u16 = u16::MAX; + +// use 3 public STUN servers to find out the NAT type, 2 must be the same address but different ports +// https://stackoverflow.com/questions/72805316/determine-nat-mapping-behaviour-using-two-stun-servers +// luckily nextcloud supports two ports for STUN +// unluckily webrtc-rs does not use the same port to do the STUN request +static DEFAULT_ICE_SERVERS: [&str; 3] = [ + "stun:stun.cloudflare.com:3478", + "stun:stun.nextcloud.com:3478", + "stun:stun.nextcloud.com:443", +]; + +lazy_static::lazy_static! { + static ref SESSIONS: Arc::>> = Default::default(); +} + +impl Clone for WebRTCStream { + fn clone(&self) -> Self { + WebRTCStream { + pc: self.pc.clone(), + stream: self.stream.clone(), + state_notify: self.state_notify.clone(), + send_timeout: self.send_timeout, + } + } +} + +impl WebRTCStream { + #[inline] + fn get_remote_offer(endpoint: &str) -> ResultType { + // Ensure the endpoint starts with the "webrtc://" prefix + if !endpoint.starts_with("webrtc://") { + return Err( + Error::new(ErrorKind::InvalidInput, "Invalid WebRTC endpoint format").into(), + ); + } + + // Extract the Base64-encoded SDP part + let encoded_sdp = &endpoint["webrtc://".len()..]; + // Decode the Base64 string + let decoded_bytes = BASE64_STANDARD + .decode(encoded_sdp) + .map_err(|_| Error::new(ErrorKind::InvalidInput, "Failed to decode Base64 SDP"))?; + Ok(String::from_utf8(decoded_bytes).map_err(|_| { + Error::new( + ErrorKind::InvalidInput, + "Failed to convert decoded bytes to UTF-8", + ) + })?) + } + + #[inline] + fn sdp_to_endpoint(sdp: &str) -> String { + let encoded_sdp = BASE64_STANDARD.encode(sdp); + format!("webrtc://{}", encoded_sdp) + } + + #[inline] + fn get_key_for_sdp(sdp: &RTCSessionDescription) -> ResultType { + let binding = sdp.unmarshal()?; + let Some(fingerprint) = binding.attribute("fingerprint") else { + // find fingerprint attribute in media descriptions + for media in &binding.media_descriptions { + if media.media_name.media != "application" { + continue; + } + if let Some(fp) = media + .attributes + .iter() + .find(|x| x.key == "fingerprint") + .and_then(|x| x.value.clone()) + { + return Ok(fp); + } + } + return Err(anyhow::anyhow!("SDP fingerprint attribute not found")); + }; + Ok(fingerprint.to_string()) + } + + #[inline] + fn get_key_for_sdp_json(sdp_json: &str) -> ResultType { + if sdp_json.is_empty() { + return Ok("".to_string()); + } + let sdp = serde_json::from_str::(&sdp_json)?; + Self::get_key_for_sdp(&sdp) + } + + #[inline] + async fn get_key_for_peer(pc: &Arc, is_local: bool) -> ResultType { + let Some(desc) = (match is_local { + true => pc.local_description().await, + false => pc.remote_description().await, + }) else { + return Err(anyhow::anyhow!("PeerConnection description is not set")); + }; + Self::get_key_for_sdp(&desc) + } + + #[inline] + fn get_ice_server_from_url(url: &str) -> Option { + // standard url format with turn scheme: turn://user:pass@host:port + match Url::parse(url) { + Ok(u) => { + if u.scheme() == "turn" + || u.scheme() == "turns" + || u.scheme() == "stun" + || u.scheme() == "stuns" + { + Some(RTCIceServer { + urls: vec![format!( + "{}:{}:{}", + u.scheme(), + u.host_str().unwrap_or_default(), + u.port().unwrap_or(3478) + )], + username: u.username().to_string(), + credential: u.password().unwrap_or_default().to_string(), + ..Default::default() + }) + } else { + None + } + } + Err(_) => None, + } + } + + #[inline] + fn get_ice_servers() -> Vec { + let mut ice_servers = Vec::new(); + let cfg = config::Config::get_option(config::keys::OPTION_ICE_SERVERS); + + let mut has_stun = false; + + for url in cfg.split(',').map(str::trim) { + if let Some(ice_server) = Self::get_ice_server_from_url(url) { + // Detect STUN in user config + if ice_server + .urls + .iter() + .any(|u| u.starts_with("stun:") || u.starts_with("stuns:")) + { + has_stun = true; + } + + ice_servers.push(ice_server); + } + } + + // If there is no STUN (either TURN-only or empty config) → prepend defaults + if !has_stun { + ice_servers.insert( + 0, + RTCIceServer { + urls: DEFAULT_ICE_SERVERS.iter().map(|s| s.to_string()).collect(), + ..Default::default() + }, + ); + } + ice_servers + } + + pub async fn new( + remote_endpoint: &str, + force_relay: bool, + ms_timeout: u64, + ) -> ResultType { + log::debug!("New webrtc stream to endpoint: {}", remote_endpoint); + let remote_offer = if remote_endpoint.is_empty() { + "".into() + } else { + Self::get_remote_offer(remote_endpoint)? + }; + + let mut key = Self::get_key_for_sdp_json(&remote_offer)?; + let sessions_lock = SESSIONS.lock().await; + if let Some(cached_stream) = sessions_lock.get(&key) { + if !key.is_empty() { + log::debug!("Start webrtc with cached peer"); + return Ok(cached_stream.clone()); + } + } + drop(sessions_lock); + + let start_local_offer = remote_offer.is_empty(); + // Create a SettingEngine and enable Detach + let mut s = SettingEngine::default(); + s.detach_data_channels(); + s.set_ice_multicast_dns_mode(MulticastDnsMode::Disabled); + + // Create the API object + let api = APIBuilder::new().with_setting_engine(s).build(); + + // Prepare the configuration, get ICE servers from config + let config = RTCConfiguration { + ice_servers: Self::get_ice_servers(), + ice_transport_policy: if force_relay { + RTCIceTransportPolicy::Relay + } else { + RTCIceTransportPolicy::All + }, + ..Default::default() + }; + + let (notify_tx, notify_rx) = watch::channel(false); + // Create a new RTCPeerConnection + let pc = Arc::new(api.new_peer_connection(config).await?); + let bootstrap_dc = if start_local_offer { + let dc_open_notify = notify_tx.clone(); + // Create a data channel with label "bootstrap" + let dc = pc.create_data_channel("bootstrap", None).await?; + dc.on_open(Box::new(move || { + log::debug!("Local data channel bootstrap open."); + let _ = dc_open_notify.send(true); + Box::pin(async {}) + })); + dc + } else { + // Wait for the data channel to be created by the remote peer + // Here we create a dummy data channel to satisfy the type system + Arc::new(RTCDataChannel::default()) + }; + + let stream = Arc::new(Mutex::new(bootstrap_dc)); + if !start_local_offer { + // Register data channel creation handling + let dc_open_notify = notify_tx.clone(); + let stream_for_dc = stream.clone(); + pc.on_data_channel(Box::new(move |dc: Arc| { + let d_label = dc.label().to_owned(); + let dc_open_notify2 = dc_open_notify.clone(); + let stream_for_dc_clone = stream_for_dc.clone(); + log::debug!("Remote data channel {} ready", d_label); + Box::pin(async move { + let mut stream_lock = stream_for_dc_clone.lock().await; + *stream_lock = dc.clone(); + drop(stream_lock); + dc.on_open(Box::new(move || { + let _ = dc_open_notify2.send(true); + Box::pin(async {}) + })); + }) + })); + } + + // This will notify you when the peer has connected/disconnected + let stream_for_close = stream.clone(); + let pc_for_close = pc.clone(); + pc.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| { + let stream_for_close2 = stream_for_close.clone(); + let on_connection_notify = notify_tx.clone(); + let pc_for_close2 = pc_for_close.clone(); + Box::pin(async move { + log::debug!("WebRTC session peer connection state: {}", s); + match s { + RTCPeerConnectionState::Disconnected + | RTCPeerConnectionState::Failed + | RTCPeerConnectionState::Closed => { + let _ = on_connection_notify.send(true); + log::debug!("WebRTC session closing due to disconnected"); + let _ = stream_for_close2.lock().await.close().await; + log::debug!("WebRTC session stream closed"); + + let mut sessions_lock = SESSIONS.lock().await; + match Self::get_key_for_peer(&pc_for_close2, start_local_offer).await { + Ok(k) => { + sessions_lock.remove(&k); + log::debug!("WebRTC session removed key: {}", k); + } + Err(e) => { + log::error!( + "Failed to extract key for peer during session cleanup: {:?}", + e + ); + // Fallback: try to remove any session associated with this peer connection + let keys_to_remove: Vec = sessions_lock + .iter() + .filter_map(|(key, session)| { + if Arc::ptr_eq(&session.pc, &pc_for_close2) { + Some(key.clone()) + } else { + None + } + }) + .collect(); + for k in keys_to_remove { + sessions_lock.remove(&k); + log::debug!("WebRTC session removed by fallback key: {}", k); + } + } + } + } + _ => {} + } + }) + })); + + // process offer/answer + if start_local_offer { + let sdp = pc.create_offer(None).await?; + let mut gather_complete = pc.gathering_complete_promise().await; + pc.set_local_description(sdp.clone()).await?; + let _ = gather_complete.recv().await; + + log::debug!("local offer:\n{}", sdp.sdp); + // get local sdp key + key = Self::get_key_for_sdp(&sdp)?; + log::debug!("Start webrtc with local key: {}", key); + } else { + let sdp = serde_json::from_str::(&remote_offer)?; + pc.set_remote_description(sdp.clone()).await?; + let answer = pc.create_answer(None).await?; + let mut gather_complete = pc.gathering_complete_promise().await; + pc.set_local_description(answer).await?; + let _ = gather_complete.recv().await; + + log::debug!("remote offer:\n{}", sdp.sdp); + // get remote sdp key + key = Self::get_key_for_sdp(&sdp)?; + log::debug!("Start webrtc with remote key: {}", key); + } + + let mut final_lock = SESSIONS.lock().await; + if let Some(session) = final_lock.get(&key) { + pc.close().await.ok(); + return Ok(session.clone()); + } + + let webrtc_stream = Self { + pc, + stream, + state_notify: notify_rx, + send_timeout: ms_timeout, + }; + final_lock.insert(key, webrtc_stream.clone()); + Ok(webrtc_stream) + } + + #[inline] + pub async fn get_local_endpoint(&self) -> ResultType { + if let Some(local_desc) = self.pc.local_description().await { + let sdp = serde_json::to_string(&local_desc)?; + let endpoint = Self::sdp_to_endpoint(&sdp); + Ok(endpoint) + } else { + Err(anyhow::anyhow!("Local desc is not set")) + } + } + + #[inline] + pub async fn set_remote_endpoint(&self, endpoint: &str) -> ResultType<()> { + let offer = Self::get_remote_offer(endpoint)?; + log::debug!("WebRTC set remote sdp: {}", offer); + let sdp = serde_json::from_str::(&offer)?; + self.pc.set_remote_description(sdp).await?; + Ok(()) + } + + #[inline] + pub fn set_raw(&mut self) { + // not-supported + } + + #[inline] + pub fn local_addr(&self) -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) + } + + #[inline] + pub fn set_send_timeout(&mut self, ms: u64) { + self.send_timeout = ms; + } + + #[inline] + pub fn set_key(&mut self, _key: Key) { + // not-supported + // WebRTC uses built-in DTLS encryption for secure communication. + // DTLS handles key exchange and encryption automatically, so explicit key management is not required. + } + + #[inline] + pub fn is_secured(&self) -> bool { + true + } + + #[inline] + pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> { + self.send_raw(msg.write_to_bytes()?).await + } + + #[inline] + pub async fn send_raw(&mut self, msg: Vec) -> ResultType<()> { + self.send_bytes(Bytes::from(msg)).await + } + + #[inline] + async fn wait_for_connect_result(&mut self) { + if *self.state_notify.borrow() { + return; + } + let _ = self.state_notify.changed().await; + } + + pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> { + if self.send_timeout > 0 { + match timeout( + Duration::from_millis(self.send_timeout), + self.wait_for_connect_result(), + ) + .await + { + Ok(_) => {} + Err(_) => { + self.pc.close().await.ok(); + return Err(Error::new( + ErrorKind::TimedOut, + "WebRTC send wait for connect timeout", + ) + .into()); + } + } + } else { + self.wait_for_connect_result().await; + } + let stream = self.stream.lock().await.clone(); + stream.send(&bytes).await?; + Ok(()) + } + + #[inline] + pub async fn next(&mut self) -> Option> { + self.wait_for_connect_result().await; + let stream = self.stream.lock().await.clone(); + + // TODO reuse buffer? + let mut buffer = BytesMut::zeroed(DATA_CHANNEL_BUFFER_SIZE as usize); + let dc = stream.detach().await.ok()?; + let n = match dc.read(&mut buffer).await { + Ok(n) => n, + Err(err) => { + self.pc.close().await.ok(); + return Some(Err(Error::new( + ErrorKind::Other, + format!("data channel read error: {}", err), + ))); + } + }; + if n == 0 { + self.pc.close().await.ok(); + return Some(Err(Error::new( + ErrorKind::Other, + "data channel read exited with 0 bytes", + ))); + } + buffer.truncate(n); + Some(Ok(buffer)) + } + + #[inline] + pub async fn next_timeout(&mut self, ms: u64) -> Option> { + match timeout(Duration::from_millis(ms), self.next()).await { + Ok(res) => res, + Err(_) => None, + } + } +} + +pub fn is_webrtc_endpoint(endpoint: &str) -> bool { + // use sdp base64 json string as endpoint, or prefix webrtc: + endpoint.starts_with("webrtc://") +} + +#[cfg(test)] +mod tests { + use crate::config; + use crate::webrtc::WebRTCStream; + use crate::webrtc::DEFAULT_ICE_SERVERS; + use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; + + #[test] + fn test_webrtc_ice_url() { + assert_eq!( + WebRTCStream::get_ice_server_from_url("turn://example.com:3478") + .unwrap_or_default() + .urls[0], + "turn:example.com:3478" + ); + + assert_eq!( + WebRTCStream::get_ice_server_from_url("turn://example.com") + .unwrap_or_default() + .urls[0], + "turn:example.com:3478" + ); + + assert_eq!( + WebRTCStream::get_ice_server_from_url("turn://123@example.com") + .unwrap_or_default() + .username, + "123" + ); + + assert_eq!( + WebRTCStream::get_ice_server_from_url("turn://123@example.com") + .unwrap_or_default() + .credential, + "" + ); + + assert_eq!( + WebRTCStream::get_ice_server_from_url("turn://123:321@example.com") + .unwrap_or_default() + .credential, + "321" + ); + + assert_eq!( + WebRTCStream::get_ice_server_from_url("stun://example.com:3478") + .unwrap_or_default() + .urls[0], + "stun:example.com:3478" + ); + + assert_eq!( + WebRTCStream::get_ice_server_from_url("http://123:123@example.com:3478"), + None + ); + + config::Config::set_option("ice-servers".to_string(), "".to_string()); + assert_eq!( + WebRTCStream::get_ice_servers()[0].urls[0], + DEFAULT_ICE_SERVERS[0].to_string() + ); + + config::Config::set_option( + "ice-servers".to_string(), + ",stun://example.com,turn://example.com,sdf".to_string(), + ); + assert_eq!( + WebRTCStream::get_ice_servers()[0].urls[0], + "stun:example.com:3478" + ); + assert_eq!( + WebRTCStream::get_ice_servers()[1].urls[0], + "turn:example.com:3478" + ); + assert_eq!(WebRTCStream::get_ice_servers().len(), 2); + config::Config::set_option( + "ice-servers".to_string(), + "".to_string(), + ); + } + + #[test] + fn test_webrtc_session_key() { + let mut sdp_str = "".to_owned(); + assert_eq!( + WebRTCStream::get_key_for_sdp( + &RTCSessionDescription::offer(sdp_str).unwrap_or_default() + ) + .unwrap_or_default(), + "" + ); + + sdp_str = "\ +v=0 +o=- 7400546379179479477 208696200 IN IP4 0.0.0.0 +s=- +t=0 0 +a=fingerprint:sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88 +a=group:BUNDLE 0 +a=extmap-allow-mixed +m=application 9 UDP/DTLS/SCTP webrtc-datachannel +c=IN IP4 0.0.0.0 +a=setup:actpass +a=mid:0 +a=sendrecv +a=sctp-port:5000 +a=ice-ufrag:RMWjjpXfpXbDPdMz +a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT".to_owned(); + assert_eq!( + WebRTCStream::get_key_for_sdp( + &RTCSessionDescription::offer(sdp_str).unwrap_or_default() + ).unwrap_or_default(), + "sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88" + ); + + sdp_str = "\ +v=0 +o=- 7400546379179479477 208696200 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE 0 +a=extmap-allow-mixed +m=application 9 UDP/DTLS/SCTP webrtc-datachannel +c=IN IP4 0.0.0.0 +a=fingerprint:sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88 +a=setup:actpass +a=mid:0 +a=sendrecv +a=sctp-port:5000 +a=ice-ufrag:RMWjjpXfpXbDPdMz +a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT".to_owned(); + assert_eq!( + WebRTCStream::get_key_for_sdp( + &RTCSessionDescription::offer(sdp_str).unwrap_or_default() + ).unwrap_or_default(), + "sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88" + ); + + sdp_str = "\ +v=0 +o=- 7400546379179479477 208696200 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE 0 +a=extmap-allow-mixed +m=application 9 UDP/DTLS/SCTP webrtc-datachannel +c=IN IP4 0.0.0.0 +a=setup:actpass +a=mid:0 +a=sendrecv +a=sctp-port:5000 +a=ice-ufrag:RMWjjpXfpXbDPdMz +a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT" + .to_owned(); + assert!( + WebRTCStream::get_key_for_sdp( + &RTCSessionDescription::offer(sdp_str).unwrap_or_default() + ) + .is_err(), + "can not find fingerprint attribute" + ); + + sdp_str = "\ +v=0 +o=- 7400546379179479477 208696200 IN IP4 0.0.0.0 +s=- +t=0 0 +a=group:BUNDLE 0 +a=extmap-allow-mixed +m=audio 9 UDP/DTLS/SCTP webrtc-datachannel +c=IN IP4 0.0.0.0 +a=fingerprint:sha-256 97:52:D6:1F:1E:87:6C:DA:B8:21:95:64:A5:85:89:FA:02:71:C7:4D:B3:FD:25:92:40:FB:6B:65:24:3C:79:88 +a=setup:actpass +a=mid:0 +a=sendrecv +a=sctp-port:5000 +a=ice-ufrag:RMWjjpXfpXbDPdMz +a=ice-pwd:BtIqlWHfwhsJdFiBROeLuEbNmYfHxRfT".to_owned(); + assert!( + WebRTCStream::get_key_for_sdp( + &RTCSessionDescription::offer(sdp_str).unwrap_or_default() + ) + .is_err(), + "can not find datachannel fingerprint attribute" + ); + + assert!( + WebRTCStream::get_key_for_sdp( + &RTCSessionDescription::offer("".to_owned()).unwrap_or_default() + ) + .is_err(), + "invalid sdp should error" + ); + + assert!( + WebRTCStream::get_key_for_sdp_json("{}").is_err(), + "empty sdp json should error" + ); + + assert!( + WebRTCStream::get_key_for_sdp_json("{ss}").is_err(), + "invalid sdp json should error" + ); + + let endpoint = "webrtc://eyJ0eXBlIjoiYW5zd2VyIiwic2RwIjoidj0wXHJcbm89LSA0MTA1NDk3NTY2NDgyMTQzODEwIDYwMzk1NzQw\ +MCBJTiBJUDQgMC4wLjAuMFxyXG5zPS1cclxudD0wIDBcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IDYxOjYwOjc0OjQwOjI4OkNFOjBCOjBDOjc1OjRCOj\ +EwOjlBOkVFOjc3OkY1OjQ0OjU3Ojg0OjUxOkRCOjA0OjkyOjRBOjEwOjFDOjRFOjVGOjdFOkYxOkIzOjcxOjIyXHJcbmE9Z3JvdXA6QlVORExFIDBcclxu\ +YT1leHRtYXAtYWxsb3ctbWl4ZWRcclxubT1hcHBsaWNhdGlvbiA5IFVEUC9EVExTL1NDVFAgd2VicnRjLWRhdGFjaGFubmVsXHJcbmM9SU4gSVA0IDAuMC\ +4wLjBcclxuYT1zZXR1cDphY3RpdmVcclxuYT1taWQ6MFxyXG5hPXNlbmRyZWN2XHJcbmE9c2N0cC1wb3J0OjUwMDBcclxuYT1pY2UtdWZyYWc6SHlnU1Rr\ +V2RsRlpHRG1XWlxyXG5hPWljZS1wd2Q6SkJneFZWaGZveVhHdHZha1VWcnBQeHVOSVpMU3llS1pcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDEgdWRwID\ +IxMzA3MDY0MzEgMTkyLjE2OC4xLjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDIgdWRwIDIxMzA3MDY0MzEgMTkyLjE2OC4x\ +LjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6MTg2MTA0NTE5MCAxIHVkcCAxNjk0NDk4ODE1IDE0LjIxMi42OC4xMiAyNzAwNCB0eXAgc3JmbH\ +ggcmFkZHIgMC4wLjAuMCBycG9ydCA2NDAwOFxyXG5hPWNhbmRpZGF0ZToxODYxMDQ1MTkwIDIgdWRwIDE2OTQ0OTg4MTUgMTQuMjEyLjY4LjEyIDI3MDA0\ +IHR5cCBzcmZseCByYWRkciAwLjAuMC4wIHJwb3J0IDY0MDA4XHJcbmE9ZW5kLW9mLWNhbmRpZGF0ZXNcclxuIn0=".to_owned(); + assert_eq!( + WebRTCStream::get_key_for_sdp_json( + &WebRTCStream::get_remote_offer(&endpoint).unwrap_or_default() + ).unwrap_or_default(), + "sha-256 61:60:74:40:28:CE:0B:0C:75:4B:10:9A:EE:77:F5:44:57:84:51:DB:04:92:4A:10:1C:4E:5F:7E:F1:B3:71:22" + ); + } + + #[tokio::test] + async fn test_webrtc_new_stream() { + let mut endpoint = "webrtc://sdfsdf".to_owned(); + assert!( + WebRTCStream::new(&endpoint, false, 10000).await.is_err(), + "invalid webrtc endpoint should error" + ); + + endpoint = "wss://sdfsdf".to_owned(); + assert!( + WebRTCStream::new(&endpoint, false, 10000).await.is_err(), + "invalid webrtc endpoint should error" + ); + + assert!( + WebRTCStream::new("", false, 10000).await.is_ok(), + "local webrtc endpoint should ok" + ); + + endpoint = "webrtc://eyJ0eXBlIjoiYW5zd2VyIiwic2RwIjoidj0wXHJcbm89LSA0MTA1NDk3NTY2NDgyMTQzODEwIDYwMzk1NzQw\ +MCBJTiBJUDQgMC4wLjAuMFxyXG5zPS1cclxudD0wIDBcclxuYT1maW5nZXJwcmludDpzaGEtMjU2IDYxOjYwOjc0OjQwOjI4OkNFOjBCOjBDOjc1OjRCOj\ +EwOjlBOkVFOjc3OkY1OjQ0OjU3Ojg0OjUxOkRCOjA0OjkyOjRBOjEwOjFDOjRFOjVGOjdFOkYxOkIzOjcxOjIyXHJcbmE9Z3JvdXA6QlVORExFIDBcclxu\ +YT1leHRtYXAtYWxsb3ctbWl4ZWRcclxubT1hcHBsaWNhdGlvbiA5IFVEUC9EVExTL1NDVFAgd2VicnRjLWRhdGFjaGFubmVsXHJcbmM9SU4gSVA0IDAuMC\ +4wLjBcclxuYT1zZXR1cDphY3RpdmVcclxuYT1taWQ6MFxyXG5hPXNlbmRyZWN2XHJcbmE9c2N0cC1wb3J0OjUwMDBcclxuYT1pY2UtdWZyYWc6SHlnU1Rr\ +V2RsRlpHRG1XWlxyXG5hPWljZS1wd2Q6SkJneFZWaGZveVhHdHZha1VWcnBQeHVOSVpMU3llS1pcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDEgdWRwID\ +IxMzA3MDY0MzEgMTkyLjE2OC4xLjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6OTYzOTg4MzQ4IDIgdWRwIDIxMzA3MDY0MzEgMTkyLjE2OC4x\ +LjIgNjQwMDcgdHlwIGhvc3RcclxuYT1jYW5kaWRhdGU6MTg2MTA0NTE5MCAxIHVkcCAxNjk0NDk4ODE1IDE0LjIxMi42OC4xMiAyNzAwNCB0eXAgc3JmbH\ +ggcmFkZHIgMC4wLjAuMCBycG9ydCA2NDAwOFxyXG5hPWNhbmRpZGF0ZToxODYxMDQ1MTkwIDIgdWRwIDE2OTQ0OTg4MTUgMTQuMjEyLjY4LjEyIDI3MDA0\ +IHR5cCBzcmZseCByYWRkciAwLjAuMC4wIHJwb3J0IDY0MDA4XHJcbmE9ZW5kLW9mLWNhbmRpZGF0ZXNcclxuIn0=".to_owned(); + assert!( + WebRTCStream::new(&endpoint, false, 10000).await.is_err(), + "connect to an 'answer' webrtc endpoint should error" + ); + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/websocket.rs b/vendor/rustdesk/libs/hbb_common/src/websocket.rs new file mode 100644 index 0000000..9be3e89 --- /dev/null +++ b/vendor/rustdesk/libs/hbb_common/src/websocket.rs @@ -0,0 +1,531 @@ +use crate::{ + config::{ + keys::OPTION_RELAY_SERVER, use_ws, Config, Socks5Server, RELAY_PORT, RENDEZVOUS_PORT, + }, + protobuf::Message, + socket_client::split_host_port, + sodiumoxide::crypto::secretbox::Key, + tcp::Encrypt, + tls::{get_cached_tls_accept_invalid_cert, get_cached_tls_type, upsert_tls_cache, TlsType}, + ResultType, +}; +use anyhow::bail; +use async_recursion::async_recursion; +use bytes::{Bytes, BytesMut}; +use futures::{SinkExt, StreamExt}; +use std::{ + io::{Error, ErrorKind}, + net::SocketAddr, + sync::Arc, + time::Duration, +}; +use tokio::{net::TcpStream, time::timeout}; +use tokio_native_tls::native_tls::TlsConnector; +use tokio_tungstenite::{ + connect_async_tls_with_config, tungstenite::protocol::Message as WsMessage, Connector, + MaybeTlsStream, WebSocketStream, +}; +use tungstenite::client::IntoClientRequest; +use tungstenite::protocol::Role; + +pub struct WsFramedStream { + stream: WebSocketStream>, + addr: SocketAddr, + encrypt: Option, + send_timeout: u64, +} + +impl WsFramedStream { + #[inline] + fn get_connector( + tls_type: &TlsType, + danger_accept_invalid_certs: bool, + ) -> ResultType> { + match tls_type { + TlsType::Plain => Ok(Some(Connector::Plain)), + TlsType::NativeTls => { + let connector = TlsConnector::builder() + .danger_accept_invalid_certs(danger_accept_invalid_certs) + .build()?; + Ok(Some(Connector::NativeTls(connector))) + } + TlsType::Rustls => { + let connector = match crate::verifier::client_config(danger_accept_invalid_certs) { + Ok(client_config) => Some(Connector::Rustls(Arc::new(client_config))), + Err(e) => { + log::warn!( + "Failed to get client config: {:?}, fallback to default connector", + e + ); + None + } + }; + Ok(connector) + } + } + } + + async fn connect( + url: &str, + ms_timeout: u64, + ) -> ResultType>> { + // to-do: websocket proxy. + + let tls_type = get_cached_tls_type(url); + let is_tls_type_cached = tls_type.is_some(); + let tls_type = tls_type.unwrap_or(TlsType::Rustls); + let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(&url); + Self::try_connect( + url, + ms_timeout, + tls_type, + is_tls_type_cached, + danger_accept_invalid_cert, + danger_accept_invalid_cert, + ) + .await + } + + #[async_recursion] + async fn try_connect( + url: &str, + ms_timeout: u64, + tls_type: TlsType, + is_tls_type_cached: bool, + danger_accept_invalid_cert: Option, + original_danger_accept_invalid_certs: Option, + ) -> ResultType>> { + let ws_config = None; + let disable_nagle = false; + let request = url + .into_client_request() + .map_err(|e| Error::new(ErrorKind::Other, e))?; + let connector = + Self::get_connector(&tls_type, danger_accept_invalid_cert.unwrap_or(false))?; + match timeout( + Duration::from_millis(ms_timeout), + connect_async_tls_with_config(request, ws_config, disable_nagle, connector), + ) + .await? + { + Ok((ws_stream, _)) => { + upsert_tls_cache(url, tls_type, danger_accept_invalid_cert.unwrap_or(false)); + Ok(ws_stream) + } + Err(e) => match (tls_type, is_tls_type_cached, danger_accept_invalid_cert) { + (TlsType::Rustls, _, None) => { + log::warn!( + "WebSocket connection with rustls-tls failed, try accept invalid certs: {}, {:?}", + url, + e + ); + Self::try_connect( + url, + ms_timeout, + tls_type, + is_tls_type_cached, + Some(true), + original_danger_accept_invalid_certs, + ) + .await + } + (TlsType::Rustls, false, Some(_)) => { + log::warn!( + "WebSocket connection with rustls-tls failed, try native-tls: {}, {:?}", + url, + e + ); + Self::try_connect( + url, + ms_timeout, + TlsType::NativeTls, + is_tls_type_cached, + original_danger_accept_invalid_certs, + original_danger_accept_invalid_certs, + ) + .await + } + (TlsType::NativeTls, _, None) => { + log::warn!( + "WebSocket connection with native-tls failed, try accept invalid certs: {}, {:?}", + url, + e + ); + Self::try_connect( + url, + ms_timeout, + tls_type, + is_tls_type_cached, + Some(true), + original_danger_accept_invalid_certs, + ) + .await + } + _ => { + log::error!( + "WebSocket connection failed with tls_type {:?}: {}, {:?}", + tls_type, + url, + e + ); + bail!(e) + } + }, + } + } + + pub async fn new>( + url: T, + _local_addr: Option, + _proxy_conf: Option<&Socks5Server>, + ms_timeout: u64, + ) -> ResultType { + let stream = Self::connect(url.as_ref(), ms_timeout).await?; + let addr = match stream.get_ref() { + MaybeTlsStream::Plain(tcp) => tcp.peer_addr()?, + MaybeTlsStream::NativeTls(tls) => tls.get_ref().get_ref().get_ref().peer_addr()?, + MaybeTlsStream::Rustls(tls) => tls.get_ref().0.peer_addr()?, + _ => return Err(Error::new(ErrorKind::Other, "Unsupported stream type").into()), + }; + + let ws = Self { + stream, + addr, + encrypt: None, + send_timeout: ms_timeout, + }; + + Ok(ws) + } + + #[inline] + pub fn set_raw(&mut self) { + self.encrypt = None; + } + + #[inline] + pub async fn from_tcp_stream(stream: TcpStream, addr: SocketAddr) -> ResultType { + let ws_stream = + WebSocketStream::from_raw_socket(MaybeTlsStream::Plain(stream), Role::Client, None) + .await; + + Ok(Self { + stream: ws_stream, + addr, + encrypt: None, + send_timeout: 0, + }) + } + + #[inline] + pub fn local_addr(&self) -> SocketAddr { + self.addr + } + + #[inline] + pub fn set_send_timeout(&mut self, ms: u64) { + self.send_timeout = ms; + } + + #[inline] + pub fn set_key(&mut self, key: Key) { + self.encrypt = Some(Encrypt::new(key)); + } + + #[inline] + pub fn is_secured(&self) -> bool { + self.encrypt.is_some() + } + + #[inline] + pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> { + self.send_raw(msg.write_to_bytes()?).await + } + + #[inline] + pub async fn send_raw(&mut self, msg: Vec) -> ResultType<()> { + let mut msg = msg; + if let Some(key) = self.encrypt.as_mut() { + msg = key.enc(&msg); + } + self.send_bytes(Bytes::from(msg)).await + } + + pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> { + let msg = WsMessage::Binary(bytes); + if self.send_timeout > 0 { + timeout( + Duration::from_millis(self.send_timeout), + self.stream.send(msg), + ) + .await?? + } else { + self.stream.send(msg).await? + }; + Ok(()) + } + + #[inline] + pub async fn next(&mut self) -> Option> { + while let Some(msg) = self.stream.next().await { + let msg = match msg { + Ok(msg) => msg, + Err(e) => { + log::error!("{}", e); + return Some(Err(Error::new( + ErrorKind::Other, + format!("WebSocket protocol error: {}", e), + ))); + } + }; + + match msg { + WsMessage::Binary(data) => { + let mut bytes = BytesMut::from(&data[..]); + if let Some(key) = self.encrypt.as_mut() { + if let Err(err) = key.dec(&mut bytes) { + return Some(Err(err)); + } + } + return Some(Ok(bytes)); + } + WsMessage::Text(text) => { + let bytes = BytesMut::from(text.as_bytes()); + return Some(Ok(bytes)); + } + WsMessage::Close(_) => { + return None; + } + _ => { + continue; + } + } + } + + None + } + + #[inline] + pub async fn next_timeout(&mut self, ms: u64) -> Option> { + match timeout(Duration::from_millis(ms), self.next()).await { + Ok(res) => res, + Err(_) => None, + } + } +} + +pub fn is_ws_endpoint(endpoint: &str) -> bool { + endpoint.starts_with("ws://") || endpoint.starts_with("wss://") +} + +/** + * Core function to convert an endpoint to WebSocket format + * + * Converts between different address formats: + * 1. IPv4 address with/without port -> ws://ipv4:port + * 2. IPv6 address with/without port -> ws://[ipv6]:port + * 3. Domain with/without port -> ws(s)://domain/ws/path + * + * @param endpoint The endpoint to convert + * @return The converted WebSocket endpoint + */ +pub fn check_ws(endpoint: &str) -> String { + if !use_ws() { + return endpoint.to_string(); + } + + if endpoint.is_empty() { + return endpoint.to_string(); + } + + if is_ws_endpoint(endpoint) { + return endpoint.to_string(); + } + + let Some((endpoint_host, endpoint_port)) = split_host_port(endpoint) else { + debug_assert!(false, "endpoint doesn't have port"); + return endpoint.to_string(); + }; + + let custom_rendezvous_server = Config::get_rendezvous_server(); + let relay_server = Config::get_option(OPTION_RELAY_SERVER); + let rendezvous_port = split_host_port(&custom_rendezvous_server) + .map(|(_, p)| p) + .unwrap_or(RENDEZVOUS_PORT); + let relay_port = split_host_port(&relay_server) + .map(|(_, p)| p) + .unwrap_or(RELAY_PORT); + + let (relay, dst_port) = if endpoint_port == rendezvous_port { + // rendezvous + (false, endpoint_port + 2) + } else if endpoint_port == rendezvous_port - 1 { + // online + (false, endpoint_port + 3) + } else if endpoint_port == relay_port || endpoint_port == rendezvous_port + 1 { + // relay + // https://github.com/rustdesk/rustdesk/blob/6ffbcd1375771f2482ec4810680623a269be70f1/src/rendezvous_mediator.rs#L615 + // https://github.com/rustdesk/rustdesk-server/blob/235a3c326ceb665e941edb50ab79faa1208f7507/src/relay_server.rs#L83, based on relay port. + (true, endpoint_port + 2) + } else { + // fallback relay + // for controlling side, relay server is passed from the controlled side, not related to local config. + (true, endpoint_port + 2) + }; + + let (address, is_domain) = if crate::is_ip_str(endpoint) { + (format!("{}:{}", endpoint_host, dst_port), false) + } else { + let domain_path = if relay { "/ws/relay" } else { "/ws/id" }; + (format!("{}{}", endpoint_host, domain_path), true) + }; + let protocol = if is_domain { + let api_server = Config::get_option("api-server"); + if api_server.starts_with("https") { + "wss" + } else { + "ws" + } + } else { + "ws" + }; + format!("{}://{}", protocol, address) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{keys, Config}; + + #[test] + fn test_check_ws() { + // enable websocket + Config::set_option(keys::OPTION_ALLOW_WEBSOCKET.to_string(), "Y".to_string()); + + // not set custom-rendezvous-server + Config::set_option("custom-rendezvous-server".to_string(), "".to_string()); + Config::set_option("relay-server".to_string(), "".to_string()); + Config::set_option("api-server".to_string(), "".to_string()); + assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119"); + assert_eq!(check_ws("rustdesk.com:21115"), "ws://rustdesk.com/ws/id"); + assert_eq!(check_ws("rustdesk.com:21116"), "ws://rustdesk.com/ws/id"); + assert_eq!(check_ws("rustdesk.com:21117"), "ws://rustdesk.com/ws/relay"); + // set relay-server without port + Config::set_option("relay-server".to_string(), "127.0.0.1".to_string()); + Config::set_option( + "api-server".to_string(), + "https://api.rustdesk.com".to_string(), + ); + assert_eq!( + check_ws("[0:0:0:0:0:0:0:1]:21115"), + "ws://[0:0:0:0:0:0:0:1]:21118" + ); + assert_eq!( + check_ws("[0:0:0:0:0:0:0:1]:21116"), + "ws://[0:0:0:0:0:0:0:1]:21118" + ); + assert_eq!( + check_ws("[0:0:0:0:0:0:0:1]:21117"), + "ws://[0:0:0:0:0:0:0:1]:21119" + ); + assert_eq!(check_ws("rustdesk.com:21115"), "wss://rustdesk.com/ws/id"); + assert_eq!(check_ws("rustdesk.com:21116"), "wss://rustdesk.com/ws/id"); + assert_eq!( + check_ws("rustdesk.com:21117"), + "wss://rustdesk.com/ws/relay" + ); + // set relay-server with default port + Config::set_option("relay-server".to_string(), "127.0.0.1:21117".to_string()); + assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119"); + // set relay-server with custom port + Config::set_option("relay-server".to_string(), "127.0.0.1:34567".to_string()); + assert_eq!(check_ws("rustdesk.com:21115"), "wss://rustdesk.com/ws/id"); + assert_eq!(check_ws("rustdesk.com:21116"), "wss://rustdesk.com/ws/id"); + assert_eq!( + check_ws("rustdesk.com:34567"), + "wss://rustdesk.com/ws/relay" + ); + + // set custom-rendezvous-server without port + Config::set_option( + "custom-rendezvous-server".to_string(), + "127.0.0.1".to_string(), + ); + Config::set_option("relay-server".to_string(), "".to_string()); + Config::set_option("api-server".to_string(), "".to_string()); + assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119"); + // set relay-server without port + Config::set_option("relay-server".to_string(), "127.0.0.1".to_string()); + assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119"); + // set relay-server with default port + Config::set_option("relay-server".to_string(), "127.0.0.1:21117".to_string()); + assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119"); + // set relay-server with custom port + Config::set_option("relay-server".to_string(), "127.0.0.1:34567".to_string()); + assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:34567"), "ws://127.0.0.1:34569"); + + // set custom-rendezvous-server without default port + Config::set_option( + "custom-rendezvous-server".to_string(), + "127.0.0.1".to_string(), + ); + Config::set_option("relay-server".to_string(), "".to_string()); + Config::set_option("api-server".to_string(), "".to_string()); + assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119"); + // set relay-server without port + Config::set_option("relay-server".to_string(), "127.0.0.1".to_string()); + assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119"); + // set relay-server with default port + Config::set_option("relay-server".to_string(), "127.0.0.1:21117".to_string()); + assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119"); + // set relay-server with custom port + Config::set_option("relay-server".to_string(), "127.0.0.1:34567".to_string()); + assert_eq!(check_ws("127.0.0.1:21115"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:21116"), "ws://127.0.0.1:21118"); + assert_eq!(check_ws("127.0.0.1:34567"), "ws://127.0.0.1:34569"); + + // set custom-rendezvous-server with custom port + Config::set_option( + "custom-rendezvous-server".to_string(), + "127.0.0.1:23456".to_string(), + ); + Config::set_option("relay-server".to_string(), "".to_string()); + Config::set_option("api-server".to_string(), "".to_string()); + assert_eq!(check_ws("127.0.0.1:23455"), "ws://127.0.0.1:23458"); + assert_eq!(check_ws("127.0.0.1:23456"), "ws://127.0.0.1:23458"); + assert_eq!(check_ws("127.0.0.1:23457"), "ws://127.0.0.1:23459"); + // set relay-server without port + Config::set_option("relay-server".to_string(), "127.0.0.1".to_string()); + assert_eq!(check_ws("127.0.0.1:23455"), "ws://127.0.0.1:23458"); + assert_eq!(check_ws("127.0.0.1:23456"), "ws://127.0.0.1:23458"); + assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119"); + // set relay-server with default port + Config::set_option("relay-server".to_string(), "127.0.0.1:21117".to_string()); + assert_eq!(check_ws("127.0.0.1:23455"), "ws://127.0.0.1:23458"); + assert_eq!(check_ws("127.0.0.1:23456"), "ws://127.0.0.1:23458"); + assert_eq!(check_ws("127.0.0.1:21117"), "ws://127.0.0.1:21119"); + // set relay-server with custom port + Config::set_option("relay-server".to_string(), "127.0.0.1:34567".to_string()); + assert_eq!(check_ws("127.0.0.1:23455"), "ws://127.0.0.1:23458"); + assert_eq!(check_ws("127.0.0.1:23456"), "ws://127.0.0.1:23458"); + assert_eq!(check_ws("127.0.0.1:34567"), "ws://127.0.0.1:34569"); + } +} diff --git a/vendor/rustdesk/libs/libxdo-sys-stub/Cargo.toml b/vendor/rustdesk/libs/libxdo-sys-stub/Cargo.toml new file mode 100644 index 0000000..0b52cfb --- /dev/null +++ b/vendor/rustdesk/libs/libxdo-sys-stub/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "libxdo-sys" +version = "0.11.0" +edition = "2021" +publish = false +description = "Dynamic loading wrapper for libxdo-sys that doesn't require libxdo at compile/link time" + +[dependencies] +hbb_common = { path = "../hbb_common" } diff --git a/vendor/rustdesk/libs/libxdo-sys-stub/src/lib.rs b/vendor/rustdesk/libs/libxdo-sys-stub/src/lib.rs new file mode 100644 index 0000000..53d0e09 --- /dev/null +++ b/vendor/rustdesk/libs/libxdo-sys-stub/src/lib.rs @@ -0,0 +1,505 @@ +//! Dynamic loading wrapper for libxdo. +//! +//! Provides the same API as libxdo-sys but loads libxdo at runtime, +//! allowing the program to run on systems without libxdo installed +//! (e.g., Wayland-only environments). + +use hbb_common::{ + libc::{c_char, c_int, c_uint}, + libloading::{Library, Symbol}, + log, +}; +use std::sync::OnceLock; + +pub use hbb_common::x11::xlib::{Display, Screen, Window}; + +#[repr(C)] +pub struct xdo_t { + _private: [u8; 0], +} + +#[repr(C)] +pub struct charcodemap_t { + _private: [u8; 0], +} + +#[repr(C)] +pub struct xdo_search_t { + _private: [u8; 0], +} + +pub type useconds_t = c_uint; + +pub const CURRENTWINDOW: Window = 0; + +type FnXdoNew = unsafe extern "C" fn(*const c_char) -> *mut xdo_t; +type FnXdoNewWithOpenedDisplay = + unsafe extern "C" fn(*mut Display, *const c_char, c_int) -> *mut xdo_t; +type FnXdoFree = unsafe extern "C" fn(*mut xdo_t); +type FnXdoSendKeysequenceWindow = + unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int; +type FnXdoSendKeysequenceWindowDown = + unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int; +type FnXdoSendKeysequenceWindowUp = + unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int; +type FnXdoEnterTextWindow = + unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int; +type FnXdoClickWindow = unsafe extern "C" fn(*const xdo_t, Window, c_int) -> c_int; +type FnXdoMouseDown = unsafe extern "C" fn(*const xdo_t, Window, c_int) -> c_int; +type FnXdoMouseUp = unsafe extern "C" fn(*const xdo_t, Window, c_int) -> c_int; +type FnXdoMoveMouse = unsafe extern "C" fn(*const xdo_t, c_int, c_int, c_int) -> c_int; +type FnXdoMoveMouseRelative = unsafe extern "C" fn(*const xdo_t, c_int, c_int) -> c_int; +type FnXdoMoveMouseRelativeToWindow = + unsafe extern "C" fn(*const xdo_t, Window, c_int, c_int) -> c_int; +type FnXdoGetMouseLocation = + unsafe extern "C" fn(*const xdo_t, *mut c_int, *mut c_int, *mut c_int) -> c_int; +type FnXdoGetMouseLocation2 = + unsafe extern "C" fn(*const xdo_t, *mut c_int, *mut c_int, *mut c_int, *mut Window) -> c_int; +type FnXdoGetActiveWindow = unsafe extern "C" fn(*const xdo_t, *mut Window) -> c_int; +type FnXdoGetFocusedWindow = unsafe extern "C" fn(*const xdo_t, *mut Window) -> c_int; +type FnXdoGetFocusedWindowSane = unsafe extern "C" fn(*const xdo_t, *mut Window) -> c_int; +type FnXdoGetWindowLocation = + unsafe extern "C" fn(*const xdo_t, Window, *mut c_int, *mut c_int, *mut *mut Screen) -> c_int; +type FnXdoGetWindowSize = + unsafe extern "C" fn(*const xdo_t, Window, *mut c_uint, *mut c_uint) -> c_int; +type FnXdoGetInputState = unsafe extern "C" fn(*const xdo_t) -> c_uint; +type FnXdoActivateWindow = unsafe extern "C" fn(*const xdo_t, Window) -> c_int; +type FnXdoWaitForMouseMoveFrom = unsafe extern "C" fn(*const xdo_t, c_int, c_int) -> c_int; +type FnXdoWaitForMouseMoveTo = unsafe extern "C" fn(*const xdo_t, c_int, c_int) -> c_int; +type FnXdoSetWindowClass = + unsafe extern "C" fn(*const xdo_t, Window, *const c_char, *const c_char) -> c_int; +type FnXdoSearchWindows = + unsafe extern "C" fn(*const xdo_t, *const xdo_search_t, *mut *mut Window, *mut c_uint) -> c_int; + +struct XdoLib { + _lib: Library, + xdo_new: FnXdoNew, + xdo_new_with_opened_display: Option, + xdo_free: FnXdoFree, + xdo_send_keysequence_window: FnXdoSendKeysequenceWindow, + xdo_send_keysequence_window_down: Option, + xdo_send_keysequence_window_up: Option, + xdo_enter_text_window: Option, + xdo_click_window: Option, + xdo_mouse_down: Option, + xdo_mouse_up: Option, + xdo_move_mouse: Option, + xdo_move_mouse_relative: Option, + xdo_move_mouse_relative_to_window: Option, + xdo_get_mouse_location: Option, + xdo_get_mouse_location2: Option, + xdo_get_active_window: Option, + xdo_get_focused_window: Option, + xdo_get_focused_window_sane: Option, + xdo_get_window_location: Option, + xdo_get_window_size: Option, + xdo_get_input_state: Option, + xdo_activate_window: Option, + xdo_wait_for_mouse_move_from: Option, + xdo_wait_for_mouse_move_to: Option, + xdo_set_window_class: Option, + xdo_search_windows: Option, +} + +impl XdoLib { + fn load() -> Option { + // https://github.com/rustdesk/rustdesk/issues/13711 + const LIB_NAMES: [&str; 3] = ["libxdo.so.4", "libxdo.so.3", "libxdo.so"]; + + unsafe { + let (lib, lib_name) = LIB_NAMES + .iter() + .find_map(|name| Library::new(name).ok().map(|lib| (lib, *name)))?; + + log::info!("libxdo-sys Loaded {}", lib_name); + + let xdo_new: FnXdoNew = *lib.get(b"xdo_new").ok()?; + let xdo_free: FnXdoFree = *lib.get(b"xdo_free").ok()?; + let xdo_send_keysequence_window: FnXdoSendKeysequenceWindow = + *lib.get(b"xdo_send_keysequence_window").ok()?; + + let xdo_new_with_opened_display = lib + .get(b"xdo_new_with_opened_display") + .ok() + .map(|s: Symbol| *s); + let xdo_send_keysequence_window_down = lib + .get(b"xdo_send_keysequence_window_down") + .ok() + .map(|s: Symbol| *s); + let xdo_send_keysequence_window_up = lib + .get(b"xdo_send_keysequence_window_up") + .ok() + .map(|s: Symbol| *s); + let xdo_enter_text_window = lib + .get(b"xdo_enter_text_window") + .ok() + .map(|s: Symbol| *s); + let xdo_click_window = lib + .get(b"xdo_click_window") + .ok() + .map(|s: Symbol| *s); + let xdo_mouse_down = lib + .get(b"xdo_mouse_down") + .ok() + .map(|s: Symbol| *s); + let xdo_mouse_up = lib + .get(b"xdo_mouse_up") + .ok() + .map(|s: Symbol| *s); + let xdo_move_mouse = lib + .get(b"xdo_move_mouse") + .ok() + .map(|s: Symbol| *s); + let xdo_move_mouse_relative = lib + .get(b"xdo_move_mouse_relative") + .ok() + .map(|s: Symbol| *s); + let xdo_move_mouse_relative_to_window = lib + .get(b"xdo_move_mouse_relative_to_window") + .ok() + .map(|s: Symbol| *s); + let xdo_get_mouse_location = lib + .get(b"xdo_get_mouse_location") + .ok() + .map(|s: Symbol| *s); + let xdo_get_mouse_location2 = lib + .get(b"xdo_get_mouse_location2") + .ok() + .map(|s: Symbol| *s); + let xdo_get_active_window = lib + .get(b"xdo_get_active_window") + .ok() + .map(|s: Symbol| *s); + let xdo_get_focused_window = lib + .get(b"xdo_get_focused_window") + .ok() + .map(|s: Symbol| *s); + let xdo_get_focused_window_sane = lib + .get(b"xdo_get_focused_window_sane") + .ok() + .map(|s: Symbol| *s); + let xdo_get_window_location = lib + .get(b"xdo_get_window_location") + .ok() + .map(|s: Symbol| *s); + let xdo_get_window_size = lib + .get(b"xdo_get_window_size") + .ok() + .map(|s: Symbol| *s); + let xdo_get_input_state = lib + .get(b"xdo_get_input_state") + .ok() + .map(|s: Symbol| *s); + let xdo_activate_window = lib + .get(b"xdo_activate_window") + .ok() + .map(|s: Symbol| *s); + let xdo_wait_for_mouse_move_from = lib + .get(b"xdo_wait_for_mouse_move_from") + .ok() + .map(|s: Symbol| *s); + let xdo_wait_for_mouse_move_to = lib + .get(b"xdo_wait_for_mouse_move_to") + .ok() + .map(|s: Symbol| *s); + let xdo_set_window_class = lib + .get(b"xdo_set_window_class") + .ok() + .map(|s: Symbol| *s); + let xdo_search_windows = lib + .get(b"xdo_search_windows") + .ok() + .map(|s: Symbol| *s); + + Some(Self { + _lib: lib, + xdo_new, + xdo_new_with_opened_display, + xdo_free, + xdo_send_keysequence_window, + xdo_send_keysequence_window_down, + xdo_send_keysequence_window_up, + xdo_enter_text_window, + xdo_click_window, + xdo_mouse_down, + xdo_mouse_up, + xdo_move_mouse, + xdo_move_mouse_relative, + xdo_move_mouse_relative_to_window, + xdo_get_mouse_location, + xdo_get_mouse_location2, + xdo_get_active_window, + xdo_get_focused_window, + xdo_get_focused_window_sane, + xdo_get_window_location, + xdo_get_window_size, + xdo_get_input_state, + xdo_activate_window, + xdo_wait_for_mouse_move_from, + xdo_wait_for_mouse_move_to, + xdo_set_window_class, + xdo_search_windows, + }) + } + } +} + +static XDO_LIB: OnceLock> = OnceLock::new(); + +fn get_lib() -> Option<&'static XdoLib> { + XDO_LIB + .get_or_init(|| { + let lib = XdoLib::load(); + if lib.is_none() { + log::info!("libxdo-sys libxdo not found, xdo functions will be disabled"); + } + lib + }) + .as_ref() +} + +pub unsafe extern "C" fn xdo_new(display: *const c_char) -> *mut xdo_t { + get_lib().map_or(std::ptr::null_mut(), |lib| (lib.xdo_new)(display)) +} + +pub unsafe extern "C" fn xdo_new_with_opened_display( + xdpy: *mut Display, + display: *const c_char, + close_display_when_freed: c_int, +) -> *mut xdo_t { + get_lib() + .and_then(|lib| lib.xdo_new_with_opened_display) + .map_or(std::ptr::null_mut(), |f| { + f(xdpy, display, close_display_when_freed) + }) +} + +pub unsafe extern "C" fn xdo_free(xdo: *mut xdo_t) { + if xdo.is_null() { + return; + } + if let Some(lib) = get_lib() { + (lib.xdo_free)(xdo); + } +} + +pub unsafe extern "C" fn xdo_send_keysequence_window( + xdo: *const xdo_t, + window: Window, + keysequence: *const c_char, + delay: useconds_t, +) -> c_int { + get_lib().map_or(1, |lib| { + (lib.xdo_send_keysequence_window)(xdo, window, keysequence, delay) + }) +} + +pub unsafe extern "C" fn xdo_send_keysequence_window_down( + xdo: *const xdo_t, + window: Window, + keysequence: *const c_char, + delay: useconds_t, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_send_keysequence_window_down) + .map_or(1, |f| f(xdo, window, keysequence, delay)) +} + +pub unsafe extern "C" fn xdo_send_keysequence_window_up( + xdo: *const xdo_t, + window: Window, + keysequence: *const c_char, + delay: useconds_t, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_send_keysequence_window_up) + .map_or(1, |f| f(xdo, window, keysequence, delay)) +} + +pub unsafe extern "C" fn xdo_enter_text_window( + xdo: *const xdo_t, + window: Window, + string: *const c_char, + delay: useconds_t, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_enter_text_window) + .map_or(1, |f| f(xdo, window, string, delay)) +} + +pub unsafe extern "C" fn xdo_click_window( + xdo: *const xdo_t, + window: Window, + button: c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_click_window) + .map_or(1, |f| f(xdo, window, button)) +} + +pub unsafe extern "C" fn xdo_mouse_down(xdo: *const xdo_t, window: Window, button: c_int) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_mouse_down) + .map_or(1, |f| f(xdo, window, button)) +} + +pub unsafe extern "C" fn xdo_mouse_up(xdo: *const xdo_t, window: Window, button: c_int) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_mouse_up) + .map_or(1, |f| f(xdo, window, button)) +} + +pub unsafe extern "C" fn xdo_move_mouse( + xdo: *const xdo_t, + x: c_int, + y: c_int, + screen: c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_move_mouse) + .map_or(1, |f| f(xdo, x, y, screen)) +} + +pub unsafe extern "C" fn xdo_move_mouse_relative(xdo: *const xdo_t, x: c_int, y: c_int) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_move_mouse_relative) + .map_or(1, |f| f(xdo, x, y)) +} + +pub unsafe extern "C" fn xdo_move_mouse_relative_to_window( + xdo: *const xdo_t, + window: Window, + x: c_int, + y: c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_move_mouse_relative_to_window) + .map_or(1, |f| f(xdo, window, x, y)) +} + +pub unsafe extern "C" fn xdo_get_mouse_location( + xdo: *const xdo_t, + x: *mut c_int, + y: *mut c_int, + screen_num: *mut c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_mouse_location) + .map_or(1, |f| f(xdo, x, y, screen_num)) +} + +pub unsafe extern "C" fn xdo_get_mouse_location2( + xdo: *const xdo_t, + x: *mut c_int, + y: *mut c_int, + screen_num: *mut c_int, + window: *mut Window, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_mouse_location2) + .map_or(1, |f| f(xdo, x, y, screen_num, window)) +} + +pub unsafe extern "C" fn xdo_get_active_window( + xdo: *const xdo_t, + window_ret: *mut Window, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_active_window) + .map_or(1, |f| f(xdo, window_ret)) +} + +pub unsafe extern "C" fn xdo_get_focused_window( + xdo: *const xdo_t, + window_ret: *mut Window, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_focused_window) + .map_or(1, |f| f(xdo, window_ret)) +} + +pub unsafe extern "C" fn xdo_get_focused_window_sane( + xdo: *const xdo_t, + window_ret: *mut Window, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_focused_window_sane) + .map_or(1, |f| f(xdo, window_ret)) +} + +pub unsafe extern "C" fn xdo_get_window_location( + xdo: *const xdo_t, + window: Window, + x: *mut c_int, + y: *mut c_int, + screen_ret: *mut *mut Screen, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_window_location) + .map_or(1, |f| f(xdo, window, x, y, screen_ret)) +} + +pub unsafe extern "C" fn xdo_get_window_size( + xdo: *const xdo_t, + window: Window, + width: *mut c_uint, + height: *mut c_uint, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_window_size) + .map_or(1, |f| f(xdo, window, width, height)) +} + +pub unsafe extern "C" fn xdo_get_input_state(xdo: *const xdo_t) -> c_uint { + get_lib() + .and_then(|lib| lib.xdo_get_input_state) + .map_or(0, |f| f(xdo)) +} + +pub unsafe extern "C" fn xdo_activate_window(xdo: *const xdo_t, wid: Window) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_activate_window) + .map_or(1, |f| f(xdo, wid)) +} + +pub unsafe extern "C" fn xdo_wait_for_mouse_move_from( + xdo: *const xdo_t, + origin_x: c_int, + origin_y: c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_wait_for_mouse_move_from) + .map_or(1, |f| f(xdo, origin_x, origin_y)) +} + +pub unsafe extern "C" fn xdo_wait_for_mouse_move_to( + xdo: *const xdo_t, + dest_x: c_int, + dest_y: c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_wait_for_mouse_move_to) + .map_or(1, |f| f(xdo, dest_x, dest_y)) +} + +pub unsafe extern "C" fn xdo_set_window_class( + xdo: *const xdo_t, + wid: Window, + name: *const c_char, + class: *const c_char, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_set_window_class) + .map_or(1, |f| f(xdo, wid, name, class)) +} + +pub unsafe extern "C" fn xdo_search_windows( + xdo: *const xdo_t, + search: *const xdo_search_t, + windowlist_ret: *mut *mut Window, + nwindows_ret: *mut c_uint, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_search_windows) + .map_or(1, |f| f(xdo, search, windowlist_ret, nwindows_ret)) +} diff --git a/vendor/rustdesk/libs/portable/.gitignore b/vendor/rustdesk/libs/portable/.gitignore new file mode 100644 index 0000000..8dfaeb7 --- /dev/null +++ b/vendor/rustdesk/libs/portable/.gitignore @@ -0,0 +1,3 @@ +/target +*.exe +*.bin \ No newline at end of file diff --git a/vendor/rustdesk/libs/portable/Cargo.toml b/vendor/rustdesk/libs/portable/Cargo.toml new file mode 100644 index 0000000..bbbb558 --- /dev/null +++ b/vendor/rustdesk/libs/portable/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "rustdesk-portable-packer" +version = "1.4.6" +edition = "2021" +description = "RustDesk Remote Desktop" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +build = "build.rs" + +[dependencies] +brotli = "3.4" +dirs = "5.0" +md5 = "0.7" +winapi = { version = "0.3", features = ["winbase"] } + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.61", features = [ + "Wdk", + "Wdk_System", + "Wdk_System_SystemServices", + "Win32", + "Win32_System", + "Win32_System_SystemInformation", +] } +native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]} + +[package.metadata.winres] +LegalCopyright = "Copyright © 2025 cStudio GmbH. All rights reserved." +ProductName = "RustDesk" +OriginalFilename = "rustdesk.exe" +FileDescription = "RustDesk Remote Desktop" +#ProductVersion = "" + +[target.'cfg(target_os="windows")'.build-dependencies] +winres = "0.1" +winapi = { version = "0.3", features = [ "winnt", "pdh", "synchapi" ] } + + diff --git a/vendor/rustdesk/libs/portable/build.rs b/vendor/rustdesk/libs/portable/build.rs new file mode 100644 index 0000000..74e7cc7 --- /dev/null +++ b/vendor/rustdesk/libs/portable/build.rs @@ -0,0 +1,20 @@ +fn main() { + #[cfg(windows)] + { + use std::io::Write; + let mut res = winres::WindowsResource::new(); + res.set_icon("../../res/icon.ico") + .set_language(winapi::um::winnt::MAKELANGID( + winapi::um::winnt::LANG_ENGLISH, + winapi::um::winnt::SUBLANG_ENGLISH_US, + )) + .set_manifest_file("../../res/manifest.xml"); + match res.compile() { + Err(e) => { + write!(std::io::stderr(), "{}", e).unwrap(); + std::process::exit(1); + } + Ok(_) => {} + } + } +} diff --git a/vendor/rustdesk/libs/portable/generate.py b/vendor/rustdesk/libs/portable/generate.py new file mode 100755 index 0000000..0116c2c --- /dev/null +++ b/vendor/rustdesk/libs/portable/generate.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +import os +import optparse +from hashlib import md5 +import brotli +import datetime + +# 4GB maximum +length_count = 4 +# encoding +encoding = 'utf-8' + +# output: {path: (compressed_data, file_md5)} + + +def generate_md5_table(folder: str, level) -> dict: + res: dict = dict() + curdir = os.curdir + os.chdir(folder) + for root, _, files in os.walk('.'): + # remove ./ + for f in files: + md5_generator = md5() + full_path = os.path.join(root, f) + print(f"Processing {full_path}...") + f = open(full_path, "rb") + content = f.read() + content_compressed = brotli.compress( + content, quality=level) + md5_generator.update(content) + md5_code = md5_generator.hexdigest().encode(encoding=encoding) + res[full_path] = (content_compressed, md5_code) + os.chdir(curdir) + return res + + +def write_package_metadata(md5_table: dict, output_folder: str, exe: str): + output_path = os.path.join(output_folder, "data.bin") + with open(output_path, "wb") as f: + f.write("rustdesk".encode(encoding=encoding)) + for path in md5_table.keys(): + (compressed_data, md5_code) = md5_table[path] + data_length = len(compressed_data) + path = path.encode(encoding=encoding) + # path length & path + f.write((len(path)).to_bytes(length=length_count, byteorder='big')) + f.write(path) + # data length & compressed data + f.write(data_length.to_bytes( + length=length_count, byteorder='big')) + f.write(compressed_data) + # md5 code + f.write(md5_code) + # end + f.write("rustdesk".encode(encoding=encoding)) + # executable + f.write(exe.encode(encoding='utf-8')) + print(f"Metadata has been written to {output_path}") + +def write_app_metadata(output_folder: str): + output_path = os.path.join(output_folder, "app_metadata.toml") + with open(output_path, "w") as f: + f.write(f"timestamp = {int(datetime.datetime.now().timestamp() * 1000)}\n") + print(f"App metadata has been written to {output_path}") + +def build_portable(output_folder: str, target: str): + os.chdir(output_folder) + if target: + os.system("cargo build --release --target " + target) + else: + os.system("cargo build --release") + +# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py +# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe + + +if __name__ == '__main__': + parser = optparse.OptionParser() + parser.add_option("-f", "--folder", dest="folder", + help="folder to compress") + parser.add_option("-o", "--output", dest="output_folder", + help="the root of portable packer project, default is './'") + parser.add_option("-e", "--executable", dest="executable", + help="specify startup file in --folder, default is rustdesk.exe") + parser.add_option("-t", "--target", dest="target", + help="the target used by cargo") + parser.add_option("-l", "--level", dest="level", type="int", + help="compression level, default is 11, highest", default=11) + (options, args) = parser.parse_args() + folder = options.folder or './rustdesk' + output_folder = os.path.abspath(options.output_folder or './') + + if not options.executable: + options.executable = 'rustdesk.exe' + if not options.executable.startswith(folder): + options.executable = folder + '/' + options.executable + exe: str = os.path.abspath(options.executable) + if not exe.startswith(os.path.abspath(folder)): + print("The executable must locate in source folder") + exit(-1) + exe = '.' + exe[len(os.path.abspath(folder)):] + print("Executable path: " + exe) + print("Compression level: " + str(options.level)) + md5_table = generate_md5_table(folder, options.level) + write_package_metadata(md5_table, output_folder, exe) + write_app_metadata(output_folder) + build_portable(output_folder, options.target) diff --git a/vendor/rustdesk/libs/portable/requirements.txt b/vendor/rustdesk/libs/portable/requirements.txt new file mode 100644 index 0000000..ac6cebc --- /dev/null +++ b/vendor/rustdesk/libs/portable/requirements.txt @@ -0,0 +1 @@ +brotli \ No newline at end of file diff --git a/vendor/rustdesk/libs/portable/src/bin_reader.rs b/vendor/rustdesk/libs/portable/src/bin_reader.rs new file mode 100644 index 0000000..9effbc5 --- /dev/null +++ b/vendor/rustdesk/libs/portable/src/bin_reader.rs @@ -0,0 +1,139 @@ +use std::{ + fs::{self}, + io::{Cursor, Read}, + path::Path, +}; + +#[cfg(windows)] +const BIN_DATA: &[u8] = include_bytes!("../data.bin"); +#[cfg(not(windows))] +const BIN_DATA: &[u8] = &[]; +// 4bytes +const LENGTH: usize = 4; +const IDENTIFIER_LENGTH: usize = 8; +const MD5_LENGTH: usize = 32; +const BUF_SIZE: usize = 4096; + +pub(crate) struct BinaryData { + pub md5_code: &'static [u8], + // compressed gzip data + pub raw: &'static [u8], + pub path: String, +} + +pub(crate) struct BinaryReader { + pub files: Vec, + pub exe: String, +} + +impl Default for BinaryReader { + fn default() -> Self { + let (files, exe) = BinaryReader::read(); + Self { files, exe } + } +} + +impl BinaryData { + fn decompress(&self) -> Vec { + let cursor = Cursor::new(self.raw); + let mut decoder = brotli::Decompressor::new(cursor, BUF_SIZE); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).ok(); + buf + } + + pub fn write_to_file(&self, prefix: &Path) { + let p = prefix.join(&self.path); + if let Some(parent) = p.parent() { + if !parent.exists() { + let _ = fs::create_dir_all(parent); + } + } + if p.exists() { + // check md5 + let f = fs::read(p.clone()).unwrap_or_default(); + let digest = format!("{:x}", md5::compute(&f)); + let md5_record = String::from_utf8_lossy(self.md5_code); + if digest == md5_record { + // same, skip this file + println!("skip {}", &self.path); + return; + } else { + println!("writing {}", p.display()); + println!("{} -> {}", md5_record, digest) + } + } + let _ = fs::write(p, self.decompress()); + } +} + +impl BinaryReader { + fn read() -> (Vec, String) { + let mut base: usize = 0; + let mut parsed = vec![]; + assert!(BIN_DATA.len() > IDENTIFIER_LENGTH, "bin data invalid!"); + let mut iden = String::from_utf8_lossy(&BIN_DATA[base..base + IDENTIFIER_LENGTH]); + if iden != "rustdesk" { + panic!("bin file is not valid!"); + } + base += IDENTIFIER_LENGTH; + loop { + iden = String::from_utf8_lossy(&BIN_DATA[base..base + IDENTIFIER_LENGTH]); + if iden == "rustdesk" { + base += IDENTIFIER_LENGTH; + break; + } + // start reading + let mut offset = 0; + let path_length = u32::from_be_bytes([ + BIN_DATA[base + offset], + BIN_DATA[base + offset + 1], + BIN_DATA[base + offset + 2], + BIN_DATA[base + offset + 3], + ]) as usize; + offset += LENGTH; + let path = + String::from_utf8_lossy(&BIN_DATA[base + offset..base + offset + path_length]) + .to_string(); + offset += path_length; + // file sz + let file_length = u32::from_be_bytes([ + BIN_DATA[base + offset], + BIN_DATA[base + offset + 1], + BIN_DATA[base + offset + 2], + BIN_DATA[base + offset + 3], + ]) as usize; + offset += LENGTH; + let raw = &BIN_DATA[base + offset..base + offset + file_length]; + offset += file_length; + // md5 + let md5 = &BIN_DATA[base + offset..base + offset + MD5_LENGTH]; + offset += MD5_LENGTH; + parsed.push(BinaryData { + md5_code: md5, + raw: raw, + path: path, + }); + base += offset; + } + // executable + let executable = String::from_utf8_lossy(&BIN_DATA[base..]).to_string(); + (parsed, executable) + } + + #[cfg(linux)] + pub fn configure_permission(&self, prefix: &Path) { + use std::os::unix::prelude::PermissionsExt; + + let exe_path = prefix.join(&self.exe); + if exe_path.exists() { + if let Ok(f) = File::open(exe_path) { + if let Ok(meta) = f.metadata() { + let mut permissions = meta.permissions(); + permissions.set_mode(0o755); + f.set_permissions(permissions).ok(); + } + } + } + } +} diff --git a/vendor/rustdesk/libs/portable/src/main.rs b/vendor/rustdesk/libs/portable/src/main.rs new file mode 100644 index 0000000..b7ff44e --- /dev/null +++ b/vendor/rustdesk/libs/portable/src/main.rs @@ -0,0 +1,248 @@ +#![windows_subsystem = "windows"] + +use std::{ + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use bin_reader::BinaryReader; + +pub mod bin_reader; +#[cfg(windows)] +mod ui; + +#[cfg(windows)] +const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml"); +#[cfg(not(windows))] +const APP_METADATA: &[u8] = &[]; +const APP_METADATA_CONFIG: &str = "meta.toml"; +const META_LINE_PREFIX_TIMESTAMP: &str = "timestamp = "; +const APP_PREFIX: &str = "rustdesk"; +const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME"; +#[cfg(windows)] +const SET_FOREGROUND_WINDOW_ENV_KEY: &str = "SET_FOREGROUND_WINDOW"; + +fn is_timestamp_matches(dir: &Path, ts: &mut u64) -> bool { + let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else { + return true; + }; + for line in app_metadata.lines() { + if line.starts_with(META_LINE_PREFIX_TIMESTAMP) { + if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::() { + *ts = stored_ts; + break; + } + } + } + if *ts == 0 { + return true; + } + + if let Ok(content) = std::fs::read_to_string(dir.join(APP_METADATA_CONFIG)) { + for line in content.lines() { + if line.starts_with(META_LINE_PREFIX_TIMESTAMP) { + if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::() { + return *ts == stored_ts; + } + } + } + } + false +} + +fn write_meta(dir: &Path, ts: u64) { + let meta_file = dir.join(APP_METADATA_CONFIG); + if ts != 0 { + let content = format!("{}{}", META_LINE_PREFIX_TIMESTAMP, ts); + // Ignore is ok here + let _ = std::fs::write(meta_file, content); + } +} + +fn setup( + reader: BinaryReader, + dir: Option, + clear: bool, + _args: &Vec, + _ui: &mut bool, +) -> Option { + let dir = if let Some(dir) = dir { + dir + } else { + // home dir + if let Some(dir) = dirs::data_local_dir() { + dir.join(APP_PREFIX) + } else { + eprintln!("not found data local dir"); + return None; + } + }; + + let mut ts = 0; + if clear || !is_timestamp_matches(&dir, &mut ts) { + #[cfg(windows)] + if _args.is_empty() { + *_ui = true; + ui::setup(); + } + std::fs::remove_dir_all(&dir).ok(); + } + for file in reader.files.iter() { + file.write_to_file(&dir); + } + write_meta(&dir, ts); + #[cfg(windows)] + win::copy_runtime_broker(&dir); + #[cfg(linux)] + reader.configure_permission(&dir); + Some(dir.join(&reader.exe)) +} + +fn use_null_stdio() -> bool { + #[cfg(windows)] + { + // When running in CMD on Windows 7, using Stdio::inherit() with spawn returns an "invalid handle" error. + // Since using Stdio::null() didn’t cause any issues, and determining whether the program is launched from CMD or by double-clicking would require calling more APIs during startup, we also use Stdio::null() when launched by double-clicking on Windows 7. + let is_windows_7 = is_windows_7(); + println!("is windows7: {}", is_windows_7); + return is_windows_7; + } + #[cfg(not(windows))] + false +} + +#[cfg(windows)] +fn is_windows_7() -> bool { + use windows::Wdk::System::SystemServices::RtlGetVersion; + use windows::Win32::System::SystemInformation::OSVERSIONINFOW; + + unsafe { + let mut version_info = OSVERSIONINFOW::default(); + version_info.dwOSVersionInfoSize = std::mem::size_of::() as u32; + + if RtlGetVersion(&mut version_info).is_ok() { + // Windows 7 is version 6.1 + println!( + "Windows version: {}.{}", + version_info.dwMajorVersion, version_info.dwMinorVersion + ); + return version_info.dwMajorVersion == 6 && version_info.dwMinorVersion == 1; + } + } + false +} + +fn execute(path: PathBuf, args: Vec, _ui: bool) { + println!("executing {}", path.display()); + // setup env + let exe = std::env::current_exe().unwrap_or_default(); + let exe_name = exe.file_name().unwrap_or_default(); + // run executable + let mut cmd = Command::new(path); + cmd.args(args); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW); + if _ui { + cmd.env(SET_FOREGROUND_WINDOW_ENV_KEY, "1"); + } + } + + cmd.env(APPNAME_RUNTIME_ENV_KEY, exe_name); + if use_null_stdio() { + cmd.stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + } else { + cmd.stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + } + let _child = cmd.spawn(); + + #[cfg(windows)] + if _ui { + match _child { + Ok(child) => unsafe { + winapi::um::winuser::AllowSetForegroundWindow(child.id() as u32); + }, + Err(e) => { + eprintln!("{:?}", e); + } + } + } +} + +fn main() { + let mut args = Vec::new(); + let mut arg_exe = Default::default(); + let mut i = 0; + for arg in std::env::args() { + if i == 0 { + arg_exe = arg.clone(); + } else { + args.push(arg); + } + i += 1; + } + let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe"); + #[cfg(windows)] + let quick_support = args.is_empty() && win::is_quick_support_exe(&arg_exe); + #[cfg(not(windows))] + let quick_support = false; + + let mut ui = false; + let reader = BinaryReader::default(); + if let Some(exe) = setup( + reader, + None, + click_setup || args.contains(&"--silent-install".to_owned()), + &args, + &mut ui, + ) { + if click_setup { + args = vec!["--install".to_owned()]; + } else if quick_support { + args = vec!["--quick_support".to_owned()]; + } + execute(exe, args, ui); + } +} + +#[cfg(windows)] +mod win { + use std::{fs, os::windows::process::CommandExt, path::Path, process::Command}; + + // Used for privacy mode(magnifier impl). + pub const RUNTIME_BROKER_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe"; + pub const WIN_TOPMOST_INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe"; + + pub(super) fn copy_runtime_broker(dir: &Path) { + let src = RUNTIME_BROKER_EXE; + let tgt = WIN_TOPMOST_INJECTED_PROCESS_EXE; + let target_file = dir.join(tgt); + if target_file.exists() { + if let (Ok(src_file), Ok(tgt_file)) = (fs::read(src), fs::read(&target_file)) { + let src_md5 = format!("{:x}", md5::compute(&src_file)); + let tgt_md5 = format!("{:x}", md5::compute(&tgt_file)); + if src_md5 == tgt_md5 { + return; + } + } + } + let _allow_err = Command::new("taskkill") + .args(&["/F", "/IM", "RuntimeBroker_rustdesk.exe"]) + .creation_flags(winapi::um::winbase::CREATE_NO_WINDOW) + .output(); + let _allow_err = std::fs::copy(src, &format!("{}\\{}", dir.to_string_lossy(), tgt)); + } + + /// Check if the executable is a Quick Support version. + /// Note: This function must be kept in sync with `src/core_main.rs`. + #[inline] + pub(super) fn is_quick_support_exe(exe: &str) -> bool { + let exe = exe.to_lowercase(); + exe.contains("-qs-") || exe.contains("-qs.exe") || exe.contains("_qs.exe") + } +} diff --git a/vendor/rustdesk/libs/portable/src/res/label.png b/vendor/rustdesk/libs/portable/src/res/label.png new file mode 100644 index 0000000000000000000000000000000000000000..6876c7935afc33e4f62d0069d7fab30dabd121d9 GIT binary patch literal 1234 zcmeAS@N?(olHy`uVBq!ia0vp^2|%pC!3-pC$@QiIDaPU;cPGZ1Cw1z99L@rd$YKTt zZeb8+WSBKa0;u3nfKP}kQ1bsM7|J1V^=YUzFcznk1o;Is@H=P;Yuf)`nf9^QXIbVf zmb~SgTE&!CAJ{&_n<;Vbp7Sf5qx5UH7;xk)*gI2|@yMO`ikJQO|2k3e;fJii{m+L= zB3P3bC2`sEt2*ZlpxB=<}J_ajpYU2QX!@JWGE>}h&VZVU`e?4B-;Ar*6y zQx+H|$ZSox(iF(e*38<|(^IdxrQxak=lnZbHzXwM3yh5FmL6c5cRy$T$LIs!CqGv9 z)m6A$z{AZV;rPI`Sz<=rsRIhki{^XZ4gdA+?c`74a&E`;_n1pcm^3uyyGrO!&S{i; z%$v6LbyUyuDJ=7iJmN0fET1*WI!I`O*aq~|6l7^K-`9j-Vv=i}qj)66Q- z4=Q?YC(QpWc-vN??^1B%^mXk^Ldti_H$)ydk@)e&y31wj_v-aT*OX68Y`pK+-^=6Y zZfkB}z+z;S7r67(|HI|H7JLd~Kh~yk+VDOCBs5MpC9{(7`< z!Gd_LA4-*;Z)Dh7_#4D52?@ zFOps=8N@G}v5{f5-8=i(pF0k-Zf8BxU@`e@_2K*%oZl~=YC3uTm&B)g9AAZ&&zkcq q!7ENZ+-zePyJM?y*1-we85k;iZYXA&dFuibErX}4pUXO@geCx`f))+{ literal 0 HcmV?d00001 diff --git a/vendor/rustdesk/libs/portable/src/res/spin.gif b/vendor/rustdesk/libs/portable/src/res/spin.gif new file mode 100644 index 0000000000000000000000000000000000000000..44b2e2e62cf04fd7f19ea414c885a9e7369bc432 GIT binary patch literal 59332 zcmeFaWmMMd*7ki7(se1FiiC6u3Mvf(Qc6h+NOw0V(nv{ncXxO9MR#|1gYO?*YwdOK zz4ra=_3rl_V?9gx#Q5{+JjdYtjbk3ioKr|x@X6D6?VxthDF`$@Jv}osGdnvwH#avw zKfk=Zys@#dv$Jz>aBzBhdUkepetv#&adCNh`Ss_|&l~vL-N1vd$l?OhZ}@m51(@j> zVW2=D(1UYWL>Lenh!*%A_$O%xfe>y$Lx!^hf*$Pg3TwQJA zwfpXT8<8^1-smj$;a4W2Y(+C{B<_ zW~rnL=gHZU{}Yi2j1WeJah%VOkusKfloHocRM+2SVCW)Y;l*g`=Hf2#7?3Wdy7}=` zP`pQCGD*`h^)9E>G_3TBH2X;LY5bEw?E2eqZ{}2;q>7HG=>~92UU6SSDDc;LIXl~< zxMN=@XV@nCQDY=ty@gH6yn%ndM(rBisDP1$A<`V5gWSVTlUdCcoMq1hr@vLuPLZ`$ z_yRn)Rn!aZR$n|Mt4{~5%agTTIyGRiT{fqHj9s=AT4g9YpPIE(tPrueQ`MJPyi>iQ za%Z<@CzOA;_E_9)w{F?5dAEN40(q~YAL-RzNZg>iin?rl_FiOCuT6dfIo< zM)Cyqn@m-Dfi#A#-Kp6JowQ|J2VHbn_syYjiPR3E82g@?_pr}a7xr=-P8IcZL!%b= z^WFQL4dHnh!`j=x>}Vk9*toDyn$7hC3Im?NX4a z*4ex#W$xL6FF|R=ax53w(;?^2;up(kF=+D37`8X(<92B~HvEO6!B185+vqR1iracP zr|(^(Iqd0dT2}G5zQMdu?qd0BFVHtC;2_ZRMz!i#T_d}{s&>nSPW6k+a;{y+H$*78x4`)>m>opW_iXC#EaZMbBb=Ki zee}FuD&GdXdJcv%ooCiG-}UY@Vg&F(19+mxkm4vF;f+l|S;3wgqi+I=_RA*xi9Pox z-3e&c?M=iNdmd!mD2W&zFPlm6_dQ>}O1vA^-b}%|x{u&}bSvbnjrySsaMc=(g4e>;f$JkTH9002$AzZf)OkcnTF=g&8J-lI|J zE-zSY3wS6R`>LYweG>@8DB}`u{;>j@*=NKc*nD*mVaZSB`4H>IV5Xcu8i=)FbJPr` zU8`sC*l?`C+9x&U@YL{*xm#DQ?&K4r%1+xpgPp4*?KOyyMBYuck<~V1#Z6u9&h@29 z*icJat3GkH$ZaIlSRI=7b8swf2LYcHutwx(B4zI-6tO(zWaX5KODQ^o${V109Tz4(OOzZJlqHm$? z-NXQ1J(;9ndWF}?;lv^`DbYwAuT$eMp30;p&z$R~r&Dm~WhARaQfJ=C6^Q7H3qLQqSaFkJmTz%Cczg?|U*ZrYu+Ks2x%q$yyMx&X zSfbf-R0ZdA*x=pIVvtd753#@r{V3(INn>+E@TA29UvSuzHIBh(9p3(QS+#jxRp8ua zVU%?x;e%M4IqFmcGx3EN3Yyhe3*B2(9OPkb=WV&aD6l+XMBA;rk!_v zGG)DceLCmCaC5d4W_)wLmX>#Ou~oKvbGg?hVbA>wsE~dKs_)`&adB~Vb#-fNYj1Dw z=;-LD?E8Z!?dPYz;|&0y!ur1nRAMf3F4~n5eEMt?<3+l#W&&Bn3KPYfLzx2bmSUGI zTVo0KrMZT7^hU!_76-X8^{2bTY}V*yhF3O5k)2^laNA0y-D_RDvSl}BPj}ZX%Wizo zW(GKSs~0NLSXJ$fFS?hc3e!-DcQ3m)SL;^noh^j7X0C@-g%Q`R@zr3wrQ<)q53S05 zywB9Gq=sQdHV@@?$7EIrhgdd2@GeE_ygi{?Ij$Ez)RSn9N9vmKu9V(kf!;^Lp|L)P zSnLtL*pRWw_mgi~XWzrfP$pY5LZ43t(%iM}55ju9uF2ajlROnHua&VJs$`WB1X6QH zT#H5km-Bv2?Xw?ec)fmG<*40>t*a)mx7dZSCVMlC*beWG7NIJsq zY@L)c-hy_kFa2aWR;7cqIw?{@tu`oABegoy(_&Px^b)B8C)Q-s#52<}Z^AwfQ%C1J zRjx$6c)PKjTYa*znO6^|2mv*txky2^lTPXvbQ4Dz6sj;!ZVB|gs7fgw;83J1exuGw zS28JUA5k{0g_&C(9C&HS3tKE&&%2sP5K*~Fy0lZZ*V?tMafIE~U$f1eyj$ygENZ|9 zTFmONho#ioYd|I^G^s^lILR;gDl&!9)JbYpz;~CXnz8K;;R#bazVR!wcQ_i+rtN5f zN`-24T$l&V)aHcpJ#4cmf!!`KQ!L$Fc~fEid}&G+Ee;PpAM}v6Wgn`bOTa$v6-CdM z8x<11(#Wlyhcf4F@t z=-lU)Bjk9GK5r7^;fr!1)Hk8!`NH?aZ22`+>7`a8`I>E4C1_AN8ejMm+cl=j?HFxj zYV=g{hSs4~trZp_n`|e~`q=O2?;08J*1|Jf?KR^VU+uS3{)eD~!N&N7P6)rF6Hu32 zT3T9LTif2=-rwIpIXU@Rj{G~m!avXS$2S1b3H*N%omd~&pM$2m;~x_|udiU$u1I)u z?{N>lKdsT8imTElzgM)OTPqpU9`Sq_#Wv~RSWCg_i5 z7}DObo1Ai3g-$BoEFRwwy|kyK&tt~#R-WZJ=s;x10fRL}h`D)MQd^va3A~fI( zSB(>IM+(-tuXgR2d|wN}E}6vK6LAdu!TW7G*xg4U9P+7xE}a0tuK9%6r@@fhLYBO zAHszde@mwdas@ees+DjSTYyelPLEe24ZJ(h8SME*{a-7l2-(1_{C*O=M+P!gXck)r#X~=i`7yX@lE{IkO8}NXKal zJ9x9U-Vbs_1(uAy4y7wWM7_TQLN=W=)SUh3(_g+6s&7>6pOQtxx8>c{U$w_fVVsLP zJekXTwh}c~dquyLSF;0GjoVN)U~j?)_0=1<361EAp&oqC1h)y5b1RPzPyZF}t$1Q; z1)~d^bzpO0nyJwgUFCP-!3Z+(Q^YwK_c%`b7Ioa+uMX=&U-o6>=Uo-p;}_gSG3^xD z6_pyqL2o(lQ!aX`rJ%1;D5fk^F`u_}}5^yGORZ zzW&`K190@y8vEmi>*v?M;|%~fLjS)6N6TGYsJQvap!(N6@$7xuuO`eP{pmtsJFhS; z^@g*=E-kL{tqjJW>Q?32R~3s-SJ|FYINq}{tZZ@|WOX96H5*}$@EExyybxU0{?uO*_bPPm%;fxMRoZr)B?URe7$LH}2=FU`H>+avk@7W2i6o?y+N>kzvrtVVUOH$j=fLCnm$*+U~90e;s)m;ve&13~| zMB#-Ms^*TjB&;0zHdRJF6;VrAtF(h$Mz7U&`K8f_aEi3qN(Wht)rl=xwB1TaYMf&t zUV5bF-HlXPkKT%)MCFgekAeg2Dw2~p&({}Iqpdd9)8ehD>(imt`0F#$E?#bArY3rC z_}yd`+6876kKpQO=L}qAdX;r@WaXA9jL_ubK@n_eHeE#agSyf30}9%BL$eDJ9&c_H z_3vDa6(=)m(iNe+&!W?q@|)-{8>J@ODfbG|-{D>R=(AnvQ=ynyw$D~cRLyT-xF*zgGtfDjaN^{yHk7s?(VGQ4a-%ene9MztGWSkvZtrzyF1Qa;+i z4*P_w{`LdjW;2x?aafj)3-MRx6`0en0(-580kMPyrHs>ub*{u(BP8JyI3TL6A2uj) zFD9$Ng1ibmRHy!YG@sDau_Wv5J%Z9rd(dC3C5IVZ ztf!~tUTkES?Ober_rCrZM-F08P`^kZL~Qq z29dg4%{5&H2x34}H%GZ6D7W`}+*d$I@W2bjRd+!a_6+w%E-07OVr;b`SMNMtT9;qbw>AZQ&5 z(262V@%l;>2bGIUgvE$1Wemb2?vW^mlXc2hh*MN+@gFJjL*hIsymds(?{2ImX{n^F zB?ni&4T28PLdr;ZBeSudOr}ennH1xsw~_eZbLK|;>zEDdxEs9>UnQ4mA1pQuge>0|>K|xjJdz5`%LecL2w)<@q%$2AiQw6+_(hMd#X}1p7Ux>(3PW(58LOdwCCES#)v3#}unSzq~4fpsgzzt4VQb7I(;SZOe@) z7^4ObyU-D_4QtrvgpCUr_gIcHowis_8VT{AsIW>bos>=LCRLwwVs1Mg@tH1$SkKwZ zNtY|T$dQ!``1Fx83;OfjFfF2MOWQWU4Te+*Mw#d4t-_dNUeqOt=H{<^nZueWKeV^F z5XA9e=d6I4_Pd0x+*LW@Ax@`=YZxD{@Y*pxBKEEZSciXrzi)B|8SOW^M=YJ-hWsx4 zfg2LV(!9qV!}2*@GE&V%UJT~YL|&{|gmE86cO48Z){(V%UY3>ghjv7jYm2;`-niod z{Lo%Yg4=W*5F2qTR^?#g6rtr`K-L-zf<_?3NDWsf4d7OSNzPb_dYcW(6ct9kA*km{%Kv3^>QZSo>%xxs%bxJfnhfHcb zHudYYWN2!c^mJ1E*BRNJ=en6U=|&UOSz(Ql>&Zn{ks&#WsYBYizG#(LdG@uiTXEWW z6D!%4R<`|N@BHv`B6?-@w@b!ezvm8xwGagf!hIsx37QGD4=i6Ziqfc9iH*uFrz-5C zFW+s9N~qqhRLrXxpXW3dyiAyYg}AeF=;>+aA4%a zltYCy<8dR`S5i~Y?Wavsr*Y&ySqoT9zAu|KRm40~a#TGko5w^JHxcyNtYT31*T<|_ z+I{WEAQUzj^>pE@naUHP;}N$4p&0#OdY24s8p+MvCgRKC+=st0yw!CH!4?KY9*x$Ji`q>vdK-(v*UvGPE!$I7H`;j!v1K;9ce|pvNTL%j zj<+al`+A(7HJ=}CQaXEbIbUCvGlc~>Z9g4~bN-5F6X}8sE}!MCVoDEiMHP57?{Xni zKIe8%3t`@!P(3`JH{5M3z=Q0y)`HhVQt1V6l!P@>4MvWk1)hN6Gf4-k+*A$Uh3hkj zEiamva^QV256zcrbia5|!NNpp^&#@ajA0-ptMz4ZS#`jJ($o?T4bw@@pa^3O0R)(l zS;+% z!B%?fDG}bsYIg-YuNq-kf4An07l`t8GA}XIfZ%Bv^?&#J@9$P+!1Pf#ng_=$qBX$JNUoHBjdoSm#Bu&E#fJJFYU98R)N5_nyf zHW5?J7}a*aHKOH-G^UqajddmF=#6bsvRFS1^0TYU%ya7R%Pk@{-^z;9_2X== zBw)4Qt&%`>*lYSz35nY%pYY3pt+;~|W zE`gKrdE3L0G7=)^i_@!V$~Y$woK1`~`nL9c=(C>DfRD&*;Us)zbmdB}XfiqpE?e)P zOt|kqC=YPIf5Lv>Eg-}*z!OO-b-|mmGKf2L8OqiJXuc@wu8&mmT|&XpcMW{;k_#D&c?C&(pyl3BFRs-lRP;{OCeP{)QTFE z8Yv=@p5_LPpPBeJFmgpC4Q(PM^Cqr#NH0q3_&g<5hQ3($dxq~NmLzu&3??T?u#tju z%cqsiKCrMqbS%3Fn}=Zgrc6f7-SZb@edvJvlEV%ARO z;>gNQ6+%nYPW2HIK&TexI)v5fu&AR9bXyv<^i?;gPZf@Mnd&#((S=31RSXp|A zw=tRn90}Q(@7K7qKM+HM*i6zznROJ|;L?}lWIBEr?Bp{en%53%6hRT5x<^_&{ zlfyu6q5==1%29KwTjbS+@?1uj82v3hfNHb zVPj5J%4Z`qy&m$jjwV-YV#RTM*XO4sF1=Ei1dew|({MiS3m4DvZ9QI7Q^62KfP6xI z+di#=M^XO35Yui=QT2iM8iofFs@j6rBljl@hD5ap^4{nLp8Y(Xrf41u z*I$5}4@TAA5jAY3Zi1`T#D*81dae;oE5>NTD{uhH2AWZSeg8()rKRe^1R%%BgU zTAAy&d5MfyaG$6*(i5EQz!~YO-kBLF85l^3&oBhDNlAa)BNuzP|&7W20va<$=*0x0<*KHf4h z#1Q)KUF}yDzJEr~M@W<14^49F6WRWdLE5YhAtk#|o7PA4po@`&%)D$*L6)V0y{a0_<;b^aOCpcuh54p8HZIpjfMjj6A3c^6YMgqkpA(ctLy{AA$0 z{dU-dKJqpOggH&woL@oWx%K3*{WY!B3R2zN^=T}dA#o+bX(q@q3S5Wjvt$%1R9&hlD}6|4 zzl>+u!?3~T9BeNbD`ZzKnCj}cdN3irVH!- ze+9c81dz)#R6WmzN=`wU>8$w&8j?BFCvA)UUl`>n+muh`V16}0{nn=#&K9A%+B`N2$*ZyO1*ghRF4U|u@pD)k7`9Fn znK~lSFR0g(gJps0AR{;)fSs9lhCqxvwO)E0GwVi1x|M<+Pe$p+M&?a!>jiac;fUQv zTIotAbx!38l73!ct_Vhc22vD8K_otbRCXuZZ6r{TCPxyHP;DCufmN}-bW#PFlL1dA zNnN&NhrCl39;BEnu=+7-J9#5h->7P|R#B&VU*!|1W(V3kuNoOot)BPlgkZG}L`jxk zkF>35Qd9n*I=2y-CAz<9cETsJd5T%OpatQnFH>o@DG<*fVaV=p$MDcHD}zdVw%19k zxH;Z+hn4KG8_}z~@con3t%7cpVS}934|j47d!MNqunss;)g1Mr;)yYS2fmxOg zn=G2h&*tHRgX9^aIwzxawa<*F|kWL@9bCg zuQ{H0*z9-;?}+Ziv*0fw*|{Q@XLd1((|dEiStn8OzXmn_9qc6T{wn4We<$XEPT}vp zSU{UF(2E85U_aZ0f4==c&J6&3yZ4t9bEp{qHQ$u~2j6i1cfQ#R?9G?>Ydb-z`H#k& z^U_oxz#ORcmcBGDNG;#hE#K^~A#7lm@#V+&t?#yAgFhlJl-zg4czQ2^s!r0qu${zy<> z0Rf>oXNx+l>bQ|X$Wo~qpJT=3Nj1fI&mu#l)bWzP6q>%()KJy)n*1my?ggkf)NpXP z0b22Flz!{m{%GuB-Bq4VM~~T9q|d9AHZE+e)A3N)3a=Br*?=HofFSj{D2z0~w=i)% zFd*hSGg47(0WlZ;Q2_%Gb9#E2*_j-=SvUFKnY!7?QkvT0a4DI%JaJmaJb znQIR&-l?t9uHK@T=7vHZuA+lBU5GN~z1+zPsGIboiK<_@tCZh>_#k>mA4RpRzX{_Z ze_k`B5J(0@7bg#d^la{jBv?CM2^H8Z|rEHD#gcY*(&CrBR8%U zhZ;DjpY#keE}f>#JYd=JysT^zoDp-{h6Q;J>9blF3!J-Wy1hG#qWbBHfNbCJ6Cj94 z&opC%SS_`TB}{8uO=voEzF^id8MwAuDT>ohoM05V5Wcv=A-F)zQC0N2{04He<(`_) zD~Ekt5@*w~PLFPfLo@>Pn!^DJe~iHq)i{R}h<~@^>1-C^pARC=3I7&E{HH(%FzN80 zy10JYWk1eK{)3jWQQA=<`CAoL&x6A5$>8i-LC#YpOTOB1`i~+k$9~Km!ySoVe4DR%0we5aId!V@z zAk@q&Au0OFv5*VP#c}@|?Ij~fm3h^Ci6WT<JVUkz=!q1`hhz>TDzK?%e(YAJ_3rN z1P`Z(@OOMv#KjBm=wGlZ@UhU#KJ*Qj8cTT3NpG>p%cZA9>K|hIQe8)gIhZU^Gzc#7 z%jfyAc!M|8N6FrDBcbnky{O*aH%8!YPYO8+d>RljeV(Sp168^{69w1ub|%__G?-Y> zW+g-0%0XdOn-6kFZ$93WA`=KAvTf+d!0-b??aK=ha7wtAcSZ`5F3`mlY{r3+o*b&D zn~@&bsgs%gu~H8JnbCz_c0sI&er6()MOIb?ezAUX&5KWZK%nE>KtshZP~G?#v{3!L z_EJSd9G5!9W8#W*CBEvLIi-_qS?Oi73Ya^kPC=sdf~&HkX_d>=xA-dAq>tEZ_35uZ zq_%LWnOX=nE8^*@e=xE zjkm$Fk`=Zhg-q>hK1w45MjL+FV(xyF^@@d$UXez=8x5j#*h=^{+Pt5WeTYSkn~Us- zPxN${bwGlK=vYOFlkTXWZr1m>?XDpZp(o!AdCadaWKp85CJ|gZ@qTu=bW-sRCcCov zpz?7tzrg7GeF5W1I;I&IZOrmH&^@#~K@T}t#uhWWTB${>>&YFZO)LJ2idAZ~ftBc4 zQOdQg$Y*xbog3BG8@QA`cAGSHJNcW1l*M+x47(SGp9`-eR~K&`)89~ev<5x33%J8G3|43~(` zw8lgM)T-h`!jK#-GleF0wI=M1KAT!>drCb!7X4It4f0sLO@7+WbgPx+#wc&Brb2md zoX#&!eg>e{JWa00*bG3eI;vi4`B{Kk7t?}L&NK6Fs&-%qHuyu+zNMN2Vq`0^Ywz~4 zniB?9JBgch~`sqaY zA@7$5_(xW>2LPim)*}LW*hT_^5||}R%|0?eQI)OrY=b7L*_v0$qbi3ofF6{5DG$`uv!!#_GkrLg?8HMA_D$S~#pvzYIK#-BX7(0=X z_e8l%M<}|MX1Dri1c-J(k7l|9 zdT~y-4tcpr7wB!@TnJ$jk z_ySy#P3p8Hv|5ru`sm#T$C4?>0XnIwApwc!X*2e&+htMIip^z1w&uRzG5N30O$B{D zwx2W*5ak}uX^?gYF0*LFDz*`fE3?jk8OS*Llfz;q$6sajVr+60ZCAD_^@&?-mr}x7 z?U0@J*z=~9MPKZ~G#C|bH6u$~93as3Rr5rvZaqD09lUzBO{eNCxd)GucMU!6>|e9A zz{lnIKPC$P4{H5uAJ}bqk^Abm#NzK^_J>~C&)K*??Bo8U9sDQE{)RBCSNaCCVC;Y| zNdU~&Q~g3^fI%7}NN+&Vp?VvsP$pO#nc0Gw0~x?vCvPg*oRQ8<;ldSa8ky1)yR4U$ zaL8HGwb%sI`Hd7+R0XYUoK%IKf|rkqdht1fiU(w64D-s>X=rmMiKBwbs;sbbQy2n8 za#J(>mN!$znLDM@4sKy~rl-YSOM*9-S9fJ}v|E1(X8>-1A#h-9{7U@{+yr||G_-rL zrj3Z?CiSg=?FMA(A2u_!p}9_(@FMtD7qme|q2%*Yf9cNaVtANr+6})XYTiS-SiN7v ziGxzq_jHngm5(2XZWr?Mo-)JWT}rY2L`jC=qdsrhTbS(vFcTrkJRT9I;QS zV->Y9AD~5^DCMV8eQp_NK8SjfJ#D^{Q#NfDWgs>EdJ&abP)%d{v;~ix*eVjknEwgH zfzOXo=*y+jdH4g3TI&|{8ZnEhc!uDUVg0ZfjIm5kAh9SGhRQiE-uI$-tMp-TMQ7Bi zIL1yb39Q539aM=c?p6|pfWFRCLi0m-uUh7##;jL0hyJP({mm2qNG$&Ic-%jNncDSl z1;n3D*pK!`|2*%%yMe!=6V`^sE&oqaYy|J^fo60Fooa4`Ez3%0tg#iE7)z1H0JE6B z;Ph_=#Hf^w!4jDnl^Df2msw!YItoVo&h8@2Go;<$3W&LoithzPKcIlP^{s%Y0=cx_ zoByCn#`Ud$_(e_={aXQXb(Y?^&UWT(LYQCmd#rImj9>AW0-_)gYy4I~Jm6t{@B!<> zEx``muw2ru&-N_AJ#|<}U5!MmJ-`zoP@Ytc&K)6=kNJhH2450W@S-Ls{m`N(k&)Gc z5BuHm`?^9%$UhzX(9SkjtCs-(Ipwl!dBEo-^dw-_$N z-@i?Ih;WPhe~UG~5D86* zcUPc#6%(pQou=#sNU@+yi(4tCbt}bk|3Hd)-%7DWe3^o7zi(2E!=R}9yA zQ-^YLl|FD%Wh@#g$mWHsPd*Zg{Vv6--a?%?N!Qn2u2g~RK#QAs0#Go|7#r>!+VAlr z!dLf0!PCByY=Q%pm$bmWj0Uw}Jp`l}vU#=%FFYW{n$Nwb_IW5=4e~l4QVt!oFf`FJ zcCZk*6jrmH>YMiB9Zs?Eal>El^z*~ev38?|pd9sjlg3y;d_|s@3`i-~NJ3aCCk}g1 zO%0C{6jWo1M%hHk*b;R`Ke>z{1mrLah~uD^M7KysYY)*Jcq-5(sykYDiER#G9lM|LVsQPZ!`*+O*V->5q+mgR zXm|lrbb-}gKd0#ayNB)P_rK*0{3Q*q|Bra(io#v}4_<*PZw;?oUadj=jg!Cg>cfr3 z(sy2c`IT36g&u1+z`za6RnBbVuTE!IWm3(m=5{GzL@r6ftZJtAc(~L2#gMW&bBCvo zl=;Cs>TKbPyiyFAy}64Djk{ya64(bV?BNN+@}V+qBJlV#H2n)OX^2;C-)a6BjlwfESw z16t5WqY7FPOda;CNM<+l+rd?e2i25G4%}5VMb%85M0o_}wJd`M=G~|eC6*+1&L#6s z?mXI~ett@Pi@ui(m8^q&G#9KxFGLMl`Cn2_Q}%)1e||Ko%qiygsbYQsi1}aG{J`^{EE-4Q|5Q40w+GWc7x}U%9Jb@q&K^t@Og4MNiGH9 z@Y^oKDEmEyjx;7dUO{w3yIAt^S20~ry+>xZk(t!Zu})ftcG+I2*lW67wD@PH=-#TW zyx-icf7clXv@ZiIvc7Mg1Xg7I>vEqhytefFks#VX#6iP)V(hcL9a|q@?%-*+kru~K__-9C~ zfP{kjhNReEk>vDELfs-s{F{UVki_s!LeUjE|AHj;??@^RX;U?|O$nRkzo25h|G^Fq zm*`_|$*)K%03?(fmO?raKORRhfF!&0SiT3$tmVyql3-%CRAPlhGQvA)E^gc2AY<$fU_6pWe{Vk=~}> zN_%*%XOIl%Y*yXzX>Gz1q(=m&Wxlo189*$hZzB0*L6o=}mAC2@7hW z`o|etZm3;$TcEQlnA(ojVGElpdqfNCa1l+++6!~8@;ewdFC)8eX0oJuE|4VgduffE zSz2SEl+A?rCZ9z>j_kIi20&-k;hlo6u#X!)N}(3NltX4??nm(*I#yL(%q|*}{Mupx zEgr^tqM~1>JODCYOnKZ(eJ7waUPZr1+FIGMEZKU%dRnY-iWc(xNLldV&fR$nJn{W; zW;$`4{@^}MTc!7$TTjGXHupAP z0lj7Qe=^wpKPUi>V*MI_|5wWj$X@`9vVLpt`e|AH{!#n+```Em{*smzId}MfNvwC@ zh(-8~Sa-e=>-@Wj`nSYtk-H_>mP>jg=RT76 z>3B@`j*skxpoVA2=SN9?bQ5C_{V?sY$@t>wjuy3qYR7N`Q3`)8`cVSJ@`l|AP2p9f z#(Qt9`Z{8gN8_Zl-VNU}LyHH#RChKCzGZwm+TsG6Sa4^#qCeL9HZ_Q#ur&J*V#&aL zBbLa;7tsGktjt?t;cxzXV!c7lqD}ujv5+&03vY>~_|9HWbdvNkq%5=-lU||eAH-_( zsfRiu4){i_`YObs&0VN(#PaLCG_HX{&t^!vCf+h`L}KtwZ$NvfB-IQ9AE4cYHR=PZ z#YMiNXdxQ(k!puI1y+%e#Q7FB=jA9B3NrX~M|KmHMN9QC+RGL;5$uaMxqD zk*BDB2TFpoU4vb2RV`+cac#o|Yv~%aVckJ6DU8ZDD!J0N`zwD|ME#qMjt;-hEBt*Q z@^2d*fwfsbTebeiFY?c${oxG&p!&;gbZqh^MLX{Dd+p@C}7*ihbp~jDS#yi?OIM#Np6@@ER(U1VPwT-Tn^-6SwZv zZMCn~%>x`}L;cO28prsEeNOqSJH=)f^1a#@ctdtu?aREa_I0km`c~~zyRG&G6@IJs zW!_f%8ts0o_SxzEYqgK_Hd_%z1yuXGvWj{Z@N;sDUT@x3`?4}ivz&&v3z#p*)!-Ulgl>LuFu zH3o%Uzhda&L4JN`4Q%2`J2O0t)tpLNv*jS3uF9OOcK~U~QtKSt!O{5y1TLN4{gwjc~wqeF37F;=A z2e|b1A6ycV{e?>xm;jfwBL8bHWdWJL-*ZV{b~G{be{hL-^H&!NU$ed%uDJ?ab2g$6 z23nvLLKE)Q!rTLjhalwWy?RgPsr)ANLz?tv_%})gb>`~6yHNOsC{FpE^|Mf+1q<`+_AF)X)D_ zDX5?CGNo8p5{B(q_?752OFPy3p3!0Dhc!53)V7W|<1}8M<$9HxlC6}VjvkdNF^BmM zc3Jgg-_xD3DCoY|X))ZxC`sD4yf2AG@WMt4oyO=uik&kzuO?W|=zKX`JNJAg+Ir{D zFVFgwOKrrz*6#k$ZTerVrk|S}f512S2RZWJ(`~w(`!Ea7eQ+ZmZ=*a{{~T>q{(rhn z-_dhiF`jf|oTfZ&4|M#tC@#H#Tv>0ee-G?kF77W&B=3If_`Op`V!M5=t)v3A0S|UY z#=yCOcdnNtBHmB;IY-<~O{;_0nP8lc;rJ@CXcLQrrfhQnUqBV*QFc*nqo zne+`kd^Buh0D7p0G~BpUu*rD&NCEUv(FZ5e01MF_+5b}`{fA9ER6GGWafA1A#p)I?3C>UaeiGXx50{W{yfr=4Ngp zMZiE#Jsk3uOzwf+R&J3u*;Zk9rU({Hn_w1=aF1-&mUO&G)^>WWyXJOTgGk7B>5>)l z-SV{&YOLa=Qc=UowHK2+r5$$2c_q!g`q|Lb(*!%REgOo|FpTG-BWZ^elEz|&{0-;K*(G>d%yKP>fAnv(E4bH-L$i;h~M7?q0#CA4WR2VYW(v9`)dh*|b2scr|1 zC|XY(k7>GI>5R%dqn0Ka*!M_I=og`ar$4t6fs1E+Hfq?%eJ`%92R=+b2hVvU(SsL0 zQrAA2_uz1{8MYDZu>#>ry+~VD)`*2n+KA*Hbbco21+P1L8x>AuL=l_wW!5=KZWZG2 z+ix>h`El}=?e;z$jNQ%cA8%yfci2ZH^1s?rXYlXex%9;X3wE1oy2^4`I>x&Lv4r+AJB6~7GU z6C!i5f;|vBpEm{@o=Li4pByJRp?QsAc=bV9C4S5u7P_Taaw0G73)v+1kdCo=fMT!{ zyiWlE<&B8ywdfyp$15NRp(lL8;~B*W$(J|0^6%ftVW$(gD_X6q8l$*W1Q?*$32Q|l z=q0a2LhHVbaWuKWo)gd*(bcvww@XXoU7rWGksy0frn`P(gH^wsTLBDUGAjlsCLIZ^ z;gXMZfkx)PonY~uVwD-cP>kw3#WL%^Qw-L|Qa8K$L{Cqo9FE2yNB&$dJC6c2YpYO( zXp=UNjE-hI|IPNrR!I`Y{ej{sIGT`>G50LPm(!u5+0yf=KHSACp`syK8!slQsZ7bdXE&u@_4)Kz%j(W^{bA){6L0!)8OiMFHOzuo`1-=c~dzYoGSFWzq@ z(!Qmb;w{DASTJ``A;Z>pfoK7gkvE|fb+fn<7WObKMi;db9Xb^8a%~zE^xy7(Y=OQ{ zR5T#^X3JuL9|!fQ`3)DUWv{%%cEGTlP~`EbnmLN@h_rFfagvUHkL0*^5-NDguapQp z?K|Pfo-_7&qsAKI*!3Jd>vcf~o_EG~dNTKk`i0GqC0suMFdnqDC7n0)2i;!CwUEvj z|7l1^f<5Z%F-wjaGv3TRCt!gV4y^qaOpiDxOzEIs#bD%YZ{>RJ*By&JBrox+&3Y2m z{+(wu3=W6A4;d_u5X2;Ej`&oK1A6{s(^NbN2KQGH_P_X2z^r`m0+GKzhc8s=Z}>Vpzr(I-fO$R|tpe4#is6ii z2jB}4=?uda{;AHC3OExUqPP8aP0sx;Tk||0w#pceCqbp4y4Qz<@_295kh5qtdS=!I zUS@i@L?=>NSrQ-r?J*5aCg`UT0mvWRlRRRF!vlg)neigOAb=~9U5nm_YieA-vsL`W zhCPzaf>hIrEmCJbN)nu`76GR zKGi3~OWT2<5$|KH3n0h?11#e0GBdN_7F*vyrjbIQM!1!2b5pv6nr1t#(rIa{q*?@PyLd`=6I?oXLyc9oL|kuB zK1ZsUTiBeqNt?epQyEk>ZiZ!?#Q@>ll%EvY%n_^UbXza|a#OVq3|j(5z@r$G*Ed14 zc7vNSM5Dnin2HAAR$Tp8;5IyaS_}{wOg=_Ck-m~y2W65{K?}{7slqOFQ9_n3B*@i4 zBYPJxIuPa5;GmD2lD4?->827(qY%1San~!7?SKJMp^2j*X(1Hd!Ixa9$4N?@J(44e z=BVItKX)SVgl}dI+wkYg>(XA+sOR7*uTDB}Q!lpl6I9fw2;V=MRfzi_Dl^!}V zbvjPgfZGFAr@?I@qNuNjEs^NWcw?QNfGs6Bz?Kr27{5yB%siv>4Wt@0PTs;kBgyU3 zd!OugGETcW*J{7Mu-j{#B{ttLCh@=8Q)bw`I_wwv6J1j~|IVudX59iK0)KW){ZMc8 z&%=Jdfxnq-J`1%-D@x?|g*+x;DA!vS{f>vn%hgxW5XsMjcmM z@KkWEKSctV;9e=PHNJXdH0uYsx&J(b6BhXAu~-B25>NIacC%X7QM zJ%?5IbjoO7cu${*8}3W)ZAI$-dA>Zr54A=fm~~rH9uN=#_6i7uKR}r9L}5CQ|B}pe zzNqn4$}2JiQ7n}FW3?FW@;g=NGXiLJY~htawOFqe7kr+z6>elL&6Q}_23tss4R-Nt z?Amcezk`c91*Gpy1?1bOpoIZ&~FYnIR5+%c_(}a-d@> z7wI05p=d?Ymy0O{Se2AWwwYU<=#7 z=5M_-QlqeeO^*Pp^5vme3F{jPpKdWW;|bF;wP3$BU~0wGbzo`(+g3BRgFyWO?Idpi zR-q(B7dF!50jxsh16ai}SiRRnv6{`&_-wHnqvsw8p+(<|Dg%ptbg;5{gAn`nQByE6 zoyCBNgx=!;N{O73;Rl@8mP7CE(XEf%vqhzv(2crY8wxMuJ4KvF4acPz>QFF4}Y2DW;+UQ0m&4^hncy;WxgOx|x4(hEpra*6%Kcvg>a!%zM_43WhpC>bx!uJ&91F6YeHZh}_9W!d zc88?HP52eTS#d`;p{=o!8)N_6EA?Zaxuz~<#@MQsL(IV}oLOSqbFV$}&NzlqYsV!d z$>3SO%YC6)=ej$OATFp_I%>`c(B<*g_nz7)tAGV@z^+JdZ62tgXnGRz6Ot~?o7(dqXZ-zQ)2L~B^V@MYS;TJWtOeuU5eX3(gK9uks?hHxG&Bf?^)-}oO|Ye znl%?bW%2FZAO63!_p|r&BwvY-eWbLVp76nLh%?Nl%_<`RJtSwGSZun$4O@O3oT(DF zvhd(3^(j`2Y$sQ@ZoA?joue0^iS)KM$vk%Eo z@yag?juox4*=(Dqq??ON$f_^abbyDgc5VTf1(*fa@@UfnVIyI-#eG+EY|w*(l{O_K z((ZPpzEqH-F%A)`@JW5Rp@2weN<5>@@?? zOZRh%M9Q1E2WB>A7(I>4X3<^h=mwSrU~mX`wSBgh0jYVqUJEeBjRt`~{^jeNNe5@#oK}aonFZMYt;Qvb!GlXE2~YGx$0KBp&)~NLw@d&NXztH^{=Noslk^Fi5(MX9D&G7s&{^$rm40`r8-;GbL)m6}sAnVA; zJZwOeqTgZl;Os%P}WaV_Wg9u>AyrRQ!~6BdNS zT1D$I(e2zRq^ORa$wbLE1+W4uRt9_^(XH;kEs3R?QE+HiXS>hRqavhu@$)s!Ky;0v z=!)LwJGd~k>dmkYhXEhY2&Z9dc!!gTeGTU;B~l82#-InZ6gg*X73Hy#Z3{fDMyem$RwTjd`w2+n9w+n@GBaK|(I_l$Hk6K|}rjB@HXv#pO+Ho@<=Bi2}Nu)BY%t315X)#!iFJ-w%)=D7vAALXe! z4CC7F9~0{H+$E3DJydZ{;o8D4hIM)E&(uIJ@7?TLRodNV?sGX@=jyzCa0E^Tk!jTu zvab1%>-^{V15^FB{PC~q+0Ov36ZP!pTX^#0|3d`;;{DaGy7XDaej}dJuMW9*i2J6`n?~$L5CLhbBMNqM_oPKd6-tZ{kI@6o9 z=U}`3=!m%cuF7vxE-r9e>ifyX#WKCSV-7XEDK~2^M|lMs2adbT_G&sVgbnI68#w2r zee;c83g$d5Ppst^O?DNQHkKRyVXT{cZx$F&3qZ+@-_NI0s9PaC5+(YQGiZQ_N!N@F zKt0PXS+veMByJE~$}Ot4rUM$j(40J~1)$+8-Y}#E8a^RgR153Abz%FSwM`M0v=V6e zIJNBxKhir07Yy+h+vSdGJhv^Ieq-~ceEtq;Z3XDP4PE70iA^_XlLl*tSu_4+wHi6e zhOFN2$7R|K@(S@gydLk1Nof!dLzZ-L~&Ji=jGw% zWwQ$#E#TKW;?@MIiG@(1B48}J{et&vyVL+iyi?A(qzI!pkSpG*3|5p<)xdd3b}ou^ zIP^jYI2<0RGUOF@-@bSMZa>XM;b#L^j!2va%>Zrfv;LB!^st%cw4=$*%Rqd|qssPy zHr$h2!rdb-pQ*b?Prc?SYiAf;kx~mBKufBJP6^BOIdRjJPtwDU)q#n-VLA?rLR~bkw?-Q7f*?E*ot- z9gbhASsUb;2tu72GEkL^BW`@zq_ZM4i)Mw)VcWnDMYu=t@;Wf){xH;D&rI{vO z=wKktvX}MzrR{VO3@n{7+ zd`JjT+d4<~cS0xlBIk_9Fi$s&h4cCFV+XL^?8Yt&6MVHV5kvYM7U<^YxP0g+T4;TH zW@hk?v;rS~T!xLi(_9Ir4fwR+IN?fVbr-PK__8$tBM&@{_c^!cIO$0})65g(&9TPw zL_AsgY=H03u32Q>MKvDBfYIPIsKoU_vxgcYuciaFHJl!Xl9Obei%1%spMDHBV~Kx5 zVL)*{D(BlS_lqdU)(3XxO2M;cU{?cN2yD_RIS11!;8GTn zNeUQd_J=Ozmj|{jAqzt(mkaVKwU<#9oS$s+hy~<@iYVl?1&ircuCAa5&2v^tUK5N9 zgGLNDSIWi>&()R_DrZ!wk{t}pHHT1dbyK;m|;~^7^r1?Iwn_}g8zh=XIsF@*0H|r zM6{%bJ#sEHs|WmaZ{s8PyDDmu+*hePXufxpK=4xSP0T9}8_oAR6dEy?brm*xRCPVK z$XFt(wnAYrJ^a|8U(Y93)A~7;{9RG*AJ5GGNs&7_GyD1Mo&5NlRRG}FU&_?b1Vusr zT{s)$Yf4N>3Rd*ED~=@&r{;4Sk$m>BO`iF#GzL`E*%70NC$_sQb-jmN2iK=lQM4Er zeFf|#%;0D>EN++CM14zUH755)LnaLu?bI@`1zo|R;}Ynj4Q1cDcc7ICaHcj!=}GR zbcIKr2Z2E3*bmU?J3vBmEHwKi0l=}W%*Q0r3&5EfD@Z3LBilOL`1K-iX7)O}crmxw zE0i&}?4$L~Jj}M??flw`OTgC1o*=L_VlD`5jr6Pi!V-?9W3c30vJ+shIOkwKYT!eyOb`X`v9S zC1lW9driibSn!dEZRkloM3wz%7=wHwqLK1mEFj9kM^J8DHo3F}{0z0xEoZg$3tK6G zsU-A5sm{nJa?x{=Z9)Sc>oD0P9JW)kcNt!XArS_mO=KK4yRrp1#Cp}3^RT^I3A||4 z+xI-TdJTmR^af~5lh6-u>MA;XhQwVn7=Gxy0xa!ANS#L>l}4aDN6l2v?Tlz}9Sro- z9SC=idC(b`jWBTM>*MKJ#>&Q{$gXwc6?DJAjiWUqUDX|N0bkW)iSnI0oFlI9%`w{< z4}4;I%~{xj^u{2TSy4LrO&k-+^39e0?TA%210#2>26*w4kQY1JdS5%i#GaeI@+O`& z178!Wwrw>{^v1>@#9q79;ksVE)0b=DdyAjiz4kLwxQ_NW2Vh4ByK~5+!{3kSswKU& z=1Uu%O3bHKM`^g`ce*{5RFVF-4gP;)fe1)$fL-s&_1WL-6+ii$KdJ!0x4%>br3s3h z7U0{s&jisw^6km;559r#X@nchnb-TtPkpz&yJ^{zVP}1qD7&T8@}95q&6T|k+gP+V zzA4Fl=hNr+_x6n`Dod6p1jCv&s_9j}5WS5{b4cEo)g&6E2KZLxv>85Be+U!ZHIms~ z8){~_;61Lex4B4C*u2Z>>G5qwxWClfTgABtuU^dMy>qZ*-~*xNH}(ee@5DJ$2nKWb z6UvpbJg2@w&gEZ=B1HtU-P|1sI&)o%+Y5~#p9~?2nAZy#Mg{X&v%KM-QYO_199Iz= zlhq4Cd@g$zCEW8tFG`K++YnT&DR!C=Bw;o6@+zt8bo4KGT4P_)n;S$2oTh)C9%EP% zgN=Q3r*+21is#f|{65+M;rDEj*&^xGXvkb5_=_td#EVpLKJ~dE2?!J*;_xFldH$^& zNS5-_Bh{Aj(N_bO zkWrxxO9k)tD3?*ufojWzuUW$`6jVr5Ubx+LD40`RTw$1(m1TBs`A(cY?n9=itGj?f zmqx{sQ7i8v;3MB81hXdW69Q=vPirg3Zx4qdtM`Qzx-8kwZRf$kH(+*jP|?|Puu zFCi~gVrHHaA+2U}MB=DsX`=}5dS*g>#a9|pL=Y4V#6z}!}z>0Jw42?9JhLPBMt}$U`UU_G4H5n zyOZ8JUf<>*ublRlOIHv7)C}^0_s=Y^f7R81HIiT55>IrsKYIL5{{OeEz+X*Q`@yt} zZmDa&b*j{C8dRrzCqMeJ?08`AO6ax)2(LPX%N5Qh#0q(ruw43T*^{n2FwH((WF4EX zUgO1r+q4}FGu?DsVXph*jR-jhefh3%jhiG1zvc!Ng@%V9mse016n< zlN|Dn`}HttMf+1iWs{ySHr{y~K>8ro)SBk0%iADIy}{_rseKr{%;;k~$ZkaFT0R9;>{f zrkHp`wmv`O^ML2n-Iw4u26S=OHyg|o7(SEDsULKd&qg`TX3Zsg9FxpbWOFX{Ig|B# ziX3C=G1HDo#tv-y2a>M60Fo$&E%jdkv&U4yVv<)3 z#sxd(R%S+f1TG2|j+ka9F^-vuk>o6>O%TP<%~GG$iJ4Dx&qvf}O19H2u=$R;X%)a7 zkk8SXVa^rAE1i|f)kmBPtMyC9+pASX5gsbKqA4CdokCcT&03Rdo?DzIQJ#&%F#LhC ziT9tj1|gyRZ9C+j6ttiBY9|WX-}0e8`KW(Wfxnu9W`ZK4`F_zlwcx)gw&^qZG2C0p zm_6=zAJP%nhy0+if*Oaqtyn4kmQ?N zmi_Ete*Nns>6bsWJ5z2knb4loSX>xW}exl)UP8xy` zC#YY~y7vO*`NDB2Ijxg%DY5U44WOXCVX7@5cpz0Zg#xpWf8%@LU-^E+F!!Q!oyw3a2+622s3_2inz)ip#uOX(h;tSD z4P)0yc-0bOmc5ltt~-V6kgj#zH#tw)9;pOagPa9bE)^k--AhWRXyD5v3mx)Hl}9uR zogY8hJFM3;hkI=JimtA05MP8GZ1(BH4z>m@kO$i%|3RF}5n#zE4@{9y{!S`zQh}2S UoK)bX0w)zXslZ7E{;w7IFBB{E@c;k- literal 0 HcmV?d00001 diff --git a/vendor/rustdesk/libs/portable/src/ui.rs b/vendor/rustdesk/libs/portable/src/ui.rs new file mode 100644 index 0000000..0a015a2 --- /dev/null +++ b/vendor/rustdesk/libs/portable/src/ui.rs @@ -0,0 +1,232 @@ +use native_windows_gui as nwg; +use nwg::NativeUi; +use std::cell::RefCell; + +const GIF_DATA: &[u8] = include_bytes!("./res/spin.gif"); +const LABEL_DATA: &[u8] = include_bytes!("./res/label.png"); +const GIF_SIZE: i32 = 32; +const BG_COLOR: [u8; 3] = [90, 90, 120]; +const BORDER_COLOR: [u8; 3] = [40, 40, 40]; +const GIF_DELAY: u64 = 30; + +#[derive(Default)] +pub struct BasicApp { + window: nwg::Window, + + border_image: nwg::ImageFrame, + bg_image: nwg::ImageFrame, + gif_image: nwg::ImageFrame, + label_image: nwg::ImageFrame, + + border_layout: nwg::GridLayout, + bg_layout: nwg::GridLayout, + inner_layout: nwg::GridLayout, + + timer: nwg::AnimationTimer, + decoder: nwg::ImageDecoder, + gif_index: RefCell, + gif_images: RefCell>, +} + +impl BasicApp { + fn exit(&self) { + self.timer.stop(); + nwg::stop_thread_dispatch(); + } + + fn load_gif(&self) -> Result<(), nwg::NwgError> { + let image_source = self.decoder.from_stream(GIF_DATA)?; + for frame_index in 0..image_source.frame_count() { + let image_data = image_source.frame(frame_index)?; + let image_data = self + .decoder + .resize_image(&image_data, [GIF_SIZE as u32, GIF_SIZE as u32])?; + let bmp = image_data.as_bitmap()?; + self.gif_images.borrow_mut().push(bmp); + } + Ok(()) + } + + fn update_gif(&self) -> Result<(), nwg::NwgError> { + let images = self.gif_images.borrow(); + if images.len() == 0 { + return Err(nwg::NwgError::ImageDecoderError( + -1, + "no gif images".to_string(), + )); + } + let image_index = *self.gif_index.borrow() % images.len(); + self.gif_image.set_bitmap(Some(&images[image_index])); + *self.gif_index.borrow_mut() = (image_index + 1) % images.len(); + Ok(()) + } + + fn start_timer(&self) { + self.timer.start(); + } +} + +mod basic_app_ui { + use super::*; + use native_windows_gui::{self as nwg, Bitmap}; + use nwg::{Event, GridLayoutItem}; + use std::cell::RefCell; + use std::ops::Deref; + use std::rc::Rc; + + pub struct BasicAppUi { + inner: Rc, + default_handler: RefCell>, + } + + impl nwg::NativeUi for BasicApp { + fn build_ui(mut data: BasicApp) -> Result { + data.decoder = nwg::ImageDecoder::new()?; + let col_cnt: i32 = 7; + let row_cnt: i32 = 3; + let border_width: i32 = 1; + let window_size = ( + GIF_SIZE * col_cnt + 2 * border_width, + GIF_SIZE * row_cnt + 2 * border_width, + ); + + // Controls + nwg::Window::builder() + .flags(nwg::WindowFlags::POPUP | nwg::WindowFlags::VISIBLE) + .size(window_size) + .center(true) + .build(&mut data.window)?; + + nwg::ImageFrame::builder() + .parent(&data.window) + .size(window_size) + .background_color(Some(BORDER_COLOR)) + .build(&mut data.border_image)?; + + nwg::ImageFrame::builder() + .parent(&data.border_image) + .size((row_cnt * GIF_SIZE, col_cnt * GIF_SIZE)) + .background_color(Some(BG_COLOR)) + .build(&mut data.bg_image)?; + + nwg::ImageFrame::builder() + .parent(&data.bg_image) + .size((GIF_SIZE, GIF_SIZE)) + .background_color(Some(BG_COLOR)) + .build(&mut data.gif_image)?; + + nwg::ImageFrame::builder() + .parent(&data.bg_image) + .background_color(Some(BG_COLOR)) + .bitmap(Some(&Bitmap::from_bin(LABEL_DATA)?)) + .build(&mut data.label_image)?; + + nwg::AnimationTimer::builder() + .parent(&data.window) + .interval(std::time::Duration::from_millis(GIF_DELAY)) + .build(&mut data.timer)?; + + // Wrap-up + let ui = BasicAppUi { + inner: Rc::new(data), + default_handler: Default::default(), + }; + + // Layouts + nwg::GridLayout::builder() + .parent(&ui.window) + .spacing(0) + .margin([0, 0, 0, 0]) + .max_column(Some(1)) + .max_row(Some(1)) + .child_item(GridLayoutItem::new(&ui.border_image, 0, 0, 1, 1)) + .build(&ui.border_layout)?; + + nwg::GridLayout::builder() + .parent(&ui.border_image) + .spacing(0) + .margin([ + border_width as _, + border_width as _, + border_width as _, + border_width as _, + ]) + .max_column(Some(1)) + .max_row(Some(1)) + .child_item(GridLayoutItem::new(&ui.bg_image, 0, 0, 1, 1)) + .build(&ui.bg_layout)?; + + nwg::GridLayout::builder() + .parent(&ui.bg_image) + .spacing(0) + .margin([0, 0, 0, 0]) + .max_column(Some(col_cnt as _)) + .max_row(Some(row_cnt as _)) + .child_item(GridLayoutItem::new(&ui.gif_image, 2, 1, 1, 1)) + .child_item(GridLayoutItem::new(&ui.label_image, 3, 1, 3, 1)) + .build(&ui.inner_layout)?; + + // Events + let evt_ui = Rc::downgrade(&ui.inner); + let handle_events = move |evt, _evt_data, _handle| { + if let Some(evt_ui) = evt_ui.upgrade().as_mut() { + match evt { + Event::OnWindowClose => { + evt_ui.exit(); + } + Event::OnTimerTick => { + if let Err(e) = evt_ui.update_gif() { + eprintln!("{:?}", e); + } + } + _ => {} + } + } + }; + + ui.default_handler + .borrow_mut() + .push(nwg::full_bind_event_handler( + &ui.window.handle, + handle_events, + )); + + return Ok(ui); + } + } + + impl Drop for BasicAppUi { + /// To make sure that everything is freed without issues, the default handler must be unbound. + fn drop(&mut self) { + let mut handlers = self.default_handler.borrow_mut(); + for handler in handlers.drain(0..) { + nwg::unbind_event_handler(&handler); + } + } + } + + impl Deref for BasicAppUi { + type Target = BasicApp; + + fn deref(&self) -> &BasicApp { + &self.inner + } + } +} + +fn ui() -> Result<(), nwg::NwgError> { + nwg::init()?; + let app = BasicApp::build_ui(Default::default())?; + app.load_gif()?; + app.start_timer(); + nwg::dispatch_thread_events(); + Ok(()) +} + +pub fn setup() { + std::thread::spawn(move || { + if let Err(e) = ui() { + eprintln!("{:?}", e); + } + }); +} diff --git a/vendor/rustdesk/libs/remote_printer/Cargo.toml b/vendor/rustdesk/libs/remote_printer/Cargo.toml new file mode 100644 index 0000000..30f8aff --- /dev/null +++ b/vendor/rustdesk/libs/remote_printer/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "remote_printer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[target.'cfg(target_os = "windows")'.dependencies] +hbb_common = { version = "0.1.0", path = "../hbb_common" } +winapi = { version = "0.3" } +windows-strings = "0.3.1" diff --git a/vendor/rustdesk/libs/remote_printer/src/lib.rs b/vendor/rustdesk/libs/remote_printer/src/lib.rs new file mode 100644 index 0000000..51ee372 --- /dev/null +++ b/vendor/rustdesk/libs/remote_printer/src/lib.rs @@ -0,0 +1,34 @@ +#[cfg(target_os = "windows")] +mod setup; +#[cfg(target_os = "windows")] +pub use setup::{ + is_rd_printer_installed, + setup::{install_update_printer, uninstall_printer}, +}; + +#[cfg(target_os = "windows")] +const RD_DRIVER_INF_PATH: &str = "drivers/RustDeskPrinterDriver/RustDeskPrinterDriver.inf"; + +#[cfg(target_os = "windows")] +fn get_printer_name(app_name: &str) -> Vec { + format!("{} Printer", app_name) + .encode_utf16() + .chain(Some(0)) + .collect() +} + +#[cfg(target_os = "windows")] +fn get_driver_name() -> Vec { + "RustDesk v4 Printer Driver" + .encode_utf16() + .chain(Some(0)) + .collect() +} + +#[cfg(target_os = "windows")] +fn get_port_name(app_name: &str) -> Vec { + format!("{} Printer", app_name) + .encode_utf16() + .chain(Some(0)) + .collect() +} diff --git a/vendor/rustdesk/libs/remote_printer/src/setup/driver.rs b/vendor/rustdesk/libs/remote_printer/src/setup/driver.rs new file mode 100644 index 0000000..81226c5 --- /dev/null +++ b/vendor/rustdesk/libs/remote_printer/src/setup/driver.rs @@ -0,0 +1,202 @@ +use super::{common_enum, get_wstr_bytes, is_name_equal}; +use hbb_common::{bail, log, ResultType}; +use std::{io, ptr::null_mut, time::Duration}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD, MAX_PATH}, + ntdef::{DWORDLONG, LPCWSTR}, + winerror::{ERROR_UNKNOWN_PRINTER_DRIVER, S_OK}, + }, + um::{ + winspool::{ + DeletePrinterDriverExW, DeletePrinterDriverPackageW, EnumPrinterDriversW, + InstallPrinterDriverFromPackageW, UploadPrinterDriverPackageW, DPD_DELETE_ALL_FILES, + DRIVER_INFO_6W, DRIVER_INFO_8W, IPDFP_COPY_ALL_FILES, UPDP_SILENT_UPLOAD, + UPDP_UPLOAD_ALWAYS, + }, + winuser::GetForegroundWindow, + }, +}; +use windows_strings::PCWSTR; + +const HRESULT_ERR_ELEMENT_NOT_FOUND: u32 = 0x80070490; + +fn enum_printer_driver( + level: DWORD, + p_driver_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinterdrivers + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPrinterDriversW( + null_mut(), + null_mut(), + level, + p_driver_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +pub fn get_installed_driver_version(name: &PCWSTR) -> ResultType> { + common_enum( + "EnumPrinterDriversW", + enum_printer_driver, + 6, + |info: &DRIVER_INFO_6W| { + if is_name_equal(name, info.pName) { + Some(info.dwlDriverVersion) + } else { + None + } + }, + || None, + ) +} + +fn find_inf(name: &PCWSTR) -> ResultType> { + let r = common_enum( + "EnumPrinterDriversW", + enum_printer_driver, + 8, + |info: &DRIVER_INFO_8W| { + if is_name_equal(name, info.pName) { + Some(get_wstr_bytes(info.pszInfPath)) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(vec![])) +} + +fn delete_printer_driver(name: &PCWSTR) -> ResultType<()> { + unsafe { + // If the printer is used after the spooler service is started. E.g., printing a document through RustDesk Printer. + // `DeletePrinterDriverExW()` may fail with `ERROR_PRINTER_DRIVER_IN_USE`(3001, 0xBB9). + // We can only ignore this error for now. + // Though restarting the spooler service is a solution, it's not a good idea to restart the service. + // + // Deleting the printer driver after deleting the printer is a common practice. + // No idea why `DeletePrinterDriverExW()` fails with `ERROR_UNKNOWN_PRINTER_DRIVER` after using the printer once. + // https://github.com/ChromiumWebApps/chromium/blob/c7361d39be8abd1574e6ce8957c8dbddd4c6ccf7/cloud_print/virtual_driver/win/install/setup.cc#L422 + // AnyDesk printer driver and the simplest printer driver also have the same issue. + if FALSE + == DeletePrinterDriverExW( + null_mut(), + null_mut(), + name.as_ptr() as _, + DPD_DELETE_ALL_FILES, + 0, + ) + { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(ERROR_UNKNOWN_PRINTER_DRIVER as _) { + return Ok(()); + } else { + bail!("Failed to delete the printer driver, {}", err) + } + } + } + Ok(()) +} + +// https://github.com/dvalter/chromium-android-ext-dev/blob/dab74f7d5bc5a8adf303090ee25c611b4d54e2db/cloud_print/virtual_driver/win/install/setup.cc#L190 +fn delete_printer_driver_package(inf: Vec) -> ResultType<()> { + if inf.is_empty() { + return Ok(()); + } + let slen = if inf[inf.len() - 1] == 0 { + inf.len() - 1 + } else { + inf.len() + }; + let inf_path = String::from_utf16_lossy(&inf[..slen]); + if !std::path::Path::new(&inf_path).exists() { + return Ok(()); + } + + let mut retries = 3; + loop { + unsafe { + let res = DeletePrinterDriverPackageW(null_mut(), inf.as_ptr(), null_mut()); + if res == S_OK || res == HRESULT_ERR_ELEMENT_NOT_FOUND as i32 { + return Ok(()); + } + log::error!("Failed to delete the printer driver, result: {}", res); + } + retries -= 1; + if retries <= 0 { + bail!("Failed to delete the printer driver"); + } + std::thread::sleep(Duration::from_secs(2)); + } +} + +pub fn uninstall_driver(name: &PCWSTR) -> ResultType<()> { + // Note: inf must be found before `delete_printer_driver()`. + let inf = find_inf(name)?; + delete_printer_driver(name)?; + delete_printer_driver_package(inf) +} + +pub fn install_driver(name: &PCWSTR, inf: LPCWSTR) -> ResultType<()> { + let mut size = (MAX_PATH * 10) as u32; + let mut package_path = [0u16; MAX_PATH * 10]; + unsafe { + let mut res = UploadPrinterDriverPackageW( + null_mut(), + inf, + null_mut(), + UPDP_SILENT_UPLOAD | UPDP_UPLOAD_ALWAYS, + null_mut(), + package_path.as_mut_ptr(), + &mut size as _, + ); + if res != S_OK { + log::error!( + "Failed to upload the printer driver package to the driver cache silently, {}. Will try with user UI.", + res + ); + + res = UploadPrinterDriverPackageW( + null_mut(), + inf, + null_mut(), + UPDP_UPLOAD_ALWAYS, + GetForegroundWindow(), + package_path.as_mut_ptr(), + &mut size as _, + ); + if res != S_OK { + bail!( + "Failed to upload the printer driver package to the driver cache with UI, {}", + res + ); + } + } + + // https://learn.microsoft.com/en-us/windows/win32/printdocs/installprinterdriverfrompackage + res = InstallPrinterDriverFromPackageW( + null_mut(), + package_path.as_ptr(), + name.as_ptr(), + null_mut(), + IPDFP_COPY_ALL_FILES, + ); + if res != S_OK { + bail!("Failed to install the printer driver from package, {}", res); + } + } + + Ok(()) +} diff --git a/vendor/rustdesk/libs/remote_printer/src/setup/mod.rs b/vendor/rustdesk/libs/remote_printer/src/setup/mod.rs new file mode 100644 index 0000000..562a730 --- /dev/null +++ b/vendor/rustdesk/libs/remote_printer/src/setup/mod.rs @@ -0,0 +1,101 @@ +#![allow(non_snake_case)] + +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + ntdef::{LPCWSTR, LPWSTR}, + }, + um::winbase::{lstrcmpiW, lstrlenW}, +}; +use windows_strings::PCWSTR; + +mod driver; +mod port; +pub(crate) mod printer; +pub(crate) mod setup; + +#[inline] +pub fn is_rd_printer_installed(app_name: &str) -> ResultType { + let printer_name = crate::get_printer_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + printer::is_printer_added(&rd_printer_name) +} + +fn get_wstr_bytes(p: LPWSTR) -> Vec { + let mut vec_bytes = vec![]; + unsafe { + let len: isize = lstrlenW(p) as _; + if len > 0 { + for i in 0..len + 1 { + vec_bytes.push(*p.offset(i)); + } + } + } + vec_bytes +} + +fn is_name_equal(name: &PCWSTR, name_from_api: LPCWSTR) -> bool { + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrcmpiw + // For some locales, the lstrcmpi function may be insufficient. + // If this occurs, use `CompareStringEx` to ensure proper comparison. + // For example, in Japan call with the NORM_IGNORECASE, NORM_IGNOREKANATYPE, and NORM_IGNOREWIDTH values to achieve the most appropriate non-exact string comparison. + // Note that specifying these values slows performance, so use them only when necessary. + // + // No need to consider `CompareStringEx` for now. + unsafe { lstrcmpiW(name.as_ptr(), name_from_api) == 0 } +} + +fn common_enum( + enum_name: &str, + enum_fn: fn( + Level: DWORD, + pDriverInfo: LPBYTE, + cbBuf: DWORD, + pcbNeeded: LPDWORD, + pcReturned: LPDWORD, + ) -> BOOL, + level: DWORD, + on_data: impl Fn(&T) -> Option, + on_no_data: impl Fn() -> Option, +) -> ResultType> { + let mut needed = 0; + let mut returned = 0; + enum_fn(level, null_mut(), 0, &mut needed, &mut returned); + if needed == 0 { + return Ok(on_no_data()); + } + + let mut buffer = vec![0u8; needed as usize]; + if FALSE + == enum_fn( + level, + buffer.as_mut_ptr(), + needed, + &mut needed, + &mut returned, + ) + { + bail!( + "Failed to call {}, error: {}", + enum_name, + io::Error::last_os_error() + ) + } + + // to-do: how to free the buffers in *const T? + + let p_enum_info = buffer.as_ptr() as *const T; + unsafe { + for i in 0..returned { + let enum_info = p_enum_info.offset(i as isize); + let r = on_data(&*enum_info); + if r.is_some() { + return Ok(r); + } + } + } + + Ok(on_no_data()) +} diff --git a/vendor/rustdesk/libs/remote_printer/src/setup/port.rs b/vendor/rustdesk/libs/remote_printer/src/setup/port.rs new file mode 100644 index 0000000..d5ab0ba --- /dev/null +++ b/vendor/rustdesk/libs/remote_printer/src/setup/port.rs @@ -0,0 +1,128 @@ +use super::{common_enum, is_name_equal, printer::get_printer_installed_on_port}; +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + um::{ + winnt::HANDLE, + winspool::{ + ClosePrinter, EnumPortsW, OpenPrinterW, XcvDataW, PORT_INFO_2W, PRINTER_DEFAULTSW, + SERVER_WRITE, + }, + }, +}; +use windows_strings::{w, PCWSTR}; + +const XCV_MONITOR_LOCAL_PORT: PCWSTR = w!(",XcvMonitor Local Port"); + +fn enum_printer_port( + level: DWORD, + p_port_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumports + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPortsW( + null_mut(), + level, + p_port_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +fn is_port_exists(name: &PCWSTR) -> ResultType { + let r = common_enum( + "EnumPortsW", + enum_printer_port, + 2, + |info: &PORT_INFO_2W| { + if is_name_equal(name, info.pPortName) { + Some(true) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(false)) +} + +unsafe fn execute_on_local_port(port: &PCWSTR, command: &PCWSTR) -> ResultType<()> { + let mut dft = PRINTER_DEFAULTSW { + pDataType: null_mut(), + pDevMode: null_mut(), + DesiredAccess: SERVER_WRITE, + }; + let mut h_monitor: HANDLE = null_mut(); + if FALSE + == OpenPrinterW( + XCV_MONITOR_LOCAL_PORT.as_ptr() as _, + &mut h_monitor, + &mut dft as *mut PRINTER_DEFAULTSW as _, + ) + { + bail!(format!( + "Failed to open Local Port monitor. Error: {}", + io::Error::last_os_error() + )) + } + + let mut output_needed: u32 = 0; + let mut status: u32 = 0; + if FALSE + == XcvDataW( + h_monitor, + command.as_ptr(), + port.as_ptr() as *mut u8, + (port.len() + 1) as u32 * 2, + null_mut(), + 0, + &mut output_needed, + &mut status, + ) + { + ClosePrinter(h_monitor); + bail!(format!( + "Failed to execute the command on the printer port, Error: {}", + io::Error::last_os_error() + )) + } + + ClosePrinter(h_monitor); + + Ok(()) +} + +fn add_local_port(port: &PCWSTR) -> ResultType<()> { + unsafe { execute_on_local_port(port, &w!("AddPort")) } +} + +fn delete_local_port(port: &PCWSTR) -> ResultType<()> { + unsafe { execute_on_local_port(port, &w!("DeletePort")) } +} + +pub fn check_add_local_port(port: &PCWSTR) -> ResultType<()> { + if !is_port_exists(port)? { + return add_local_port(port); + } + Ok(()) +} + +pub fn check_delete_local_port(port: &PCWSTR) -> ResultType<()> { + if is_port_exists(port)? { + if get_printer_installed_on_port(port)?.is_some() { + bail!("The printer is installed on the port. Please remove the printer first."); + } + return delete_local_port(port); + } + Ok(()) +} diff --git a/vendor/rustdesk/libs/remote_printer/src/setup/printer.rs b/vendor/rustdesk/libs/remote_printer/src/setup/printer.rs new file mode 100644 index 0000000..9882b8f --- /dev/null +++ b/vendor/rustdesk/libs/remote_printer/src/setup/printer.rs @@ -0,0 +1,161 @@ +use super::{common_enum, get_wstr_bytes, is_name_equal}; +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + ntdef::HANDLE, + winerror::ERROR_INVALID_PRINTER_NAME, + }, + um::winspool::{ + AddPrinterW, ClosePrinter, DeletePrinter, EnumPrintersW, OpenPrinterW, SetPrinterW, + PRINTER_ALL_ACCESS, PRINTER_ATTRIBUTE_LOCAL, PRINTER_CONTROL_PURGE, PRINTER_DEFAULTSW, + PRINTER_ENUM_LOCAL, PRINTER_INFO_1W, PRINTER_INFO_2W, + }, +}; +use windows_strings::{w, PCWSTR}; + +fn enum_local_printer( + level: DWORD, + p_printer_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinters + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPrintersW( + PRINTER_ENUM_LOCAL, + null_mut(), + level, + p_printer_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +#[inline] +pub fn is_printer_added(name: &PCWSTR) -> ResultType { + let r = common_enum( + "EnumPrintersW", + enum_local_printer, + 1, + |info: &PRINTER_INFO_1W| { + if is_name_equal(name, info.pName) { + Some(true) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(false)) +} + +// Only return the first matched printer +pub fn get_printer_installed_on_port(port: &PCWSTR) -> ResultType>> { + common_enum( + "EnumPrintersW", + enum_local_printer, + 2, + |info: &PRINTER_INFO_2W| { + if is_name_equal(port, info.pPortName) { + Some(get_wstr_bytes(info.pPrinterName)) + } else { + None + } + }, + || None, + ) +} + +pub fn add_printer(name: &PCWSTR, driver: &PCWSTR, port: &PCWSTR) -> ResultType<()> { + let mut printer_info = PRINTER_INFO_2W { + pServerName: null_mut(), + pPrinterName: name.as_ptr() as _, + pShareName: null_mut(), + pPortName: port.as_ptr() as _, + pDriverName: driver.as_ptr() as _, + pComment: null_mut(), + pLocation: null_mut(), + pDevMode: null_mut(), + pSepFile: null_mut(), + pPrintProcessor: w!("WinPrint").as_ptr() as _, + pDatatype: w!("RAW").as_ptr() as _, + pParameters: null_mut(), + pSecurityDescriptor: null_mut(), + Attributes: PRINTER_ATTRIBUTE_LOCAL, + Priority: 0, + DefaultPriority: 0, + StartTime: 0, + UntilTime: 0, + Status: 0, + cJobs: 0, + AveragePPM: 0, + }; + unsafe { + let h_printer = AddPrinterW( + null_mut(), + 2, + &mut printer_info as *mut PRINTER_INFO_2W as _, + ); + if h_printer.is_null() { + bail!(format!( + "Failed to add printer. Error: {}", + io::Error::last_os_error() + )) + } + } + Ok(()) +} + +pub fn delete_printer(name: &PCWSTR) -> ResultType<()> { + let mut dft = PRINTER_DEFAULTSW { + pDataType: null_mut(), + pDevMode: null_mut(), + DesiredAccess: PRINTER_ALL_ACCESS, + }; + let mut h_printer: HANDLE = null_mut(); + unsafe { + if FALSE + == OpenPrinterW( + name.as_ptr() as _, + &mut h_printer, + &mut dft as *mut PRINTER_DEFAULTSW as _, + ) + { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(ERROR_INVALID_PRINTER_NAME as _) { + return Ok(()); + } else { + bail!(format!("Failed to open printer. Error: {}", err)) + } + } + + if FALSE == SetPrinterW(h_printer, 0, null_mut(), PRINTER_CONTROL_PURGE) { + ClosePrinter(h_printer); + bail!(format!( + "Failed to purge printer queue. Error: {}", + io::Error::last_os_error() + )) + } + + if FALSE == DeletePrinter(h_printer) { + ClosePrinter(h_printer); + bail!(format!( + "Failed to delete printer. Error: {}", + io::Error::last_os_error() + )) + } + + ClosePrinter(h_printer); + } + + Ok(()) +} diff --git a/vendor/rustdesk/libs/remote_printer/src/setup/setup.rs b/vendor/rustdesk/libs/remote_printer/src/setup/setup.rs new file mode 100644 index 0000000..f461ab7 --- /dev/null +++ b/vendor/rustdesk/libs/remote_printer/src/setup/setup.rs @@ -0,0 +1,94 @@ +use super::{ + driver::{get_installed_driver_version, install_driver, uninstall_driver}, + port::{check_add_local_port, check_delete_local_port}, + printer::{add_printer, delete_printer}, +}; +use hbb_common::{allow_err, bail, lazy_static, log, ResultType}; +use std::{path::PathBuf, sync::Mutex}; +use windows_strings::PCWSTR; + +lazy_static::lazy_static!( + static ref SETUP_MTX: Mutex<()> = Mutex::new(()); +); + +fn get_driver_inf_abs_path() -> ResultType { + use crate::RD_DRIVER_INF_PATH; + + let exe_file = std::env::current_exe()?; + let abs_path = match exe_file.parent() { + Some(parent) => parent.join(RD_DRIVER_INF_PATH), + None => bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ), + }; + if !abs_path.exists() { + bail!( + "The driver inf file \"{}\" does not exists", + RD_DRIVER_INF_PATH + ) + } + Ok(abs_path) +} + +// Note: This function must be called in a separate thread. +// Because many functions in this module are blocking or synchronous. +// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. +// Steps: +// 1. Add the local port. +// 2. Check if the driver is installed. +// Uninstall the existing driver if it is installed. +// We should not check the driver version because the driver is deployed with the application. +// It's better to uninstall the existing driver and install the driver from the application. +// 3. Add the printer. +pub fn install_update_printer(app_name: &str) -> ResultType<()> { + let printer_name = crate::get_printer_name(app_name); + let driver_name = crate::get_driver_name(); + let port = crate::get_port_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + let rd_printer_driver_name = PCWSTR::from_raw(driver_name.as_ptr()); + let rd_printer_port = PCWSTR::from_raw(port.as_ptr()); + + let inf_file = get_driver_inf_abs_path()?; + let inf_file: Vec = inf_file + .to_string_lossy() + .as_ref() + .encode_utf16() + .chain(Some(0).into_iter()) + .collect(); + let _lock = SETUP_MTX.lock().unwrap(); + + check_add_local_port(&rd_printer_port)?; + + let should_install_driver = match get_installed_driver_version(&rd_printer_driver_name)? { + Some(_version) => { + delete_printer(&rd_printer_name)?; + allow_err!(uninstall_driver(&rd_printer_driver_name)); + true + } + None => true, + }; + + if should_install_driver { + allow_err!(install_driver(&rd_printer_driver_name, inf_file.as_ptr())); + } + + add_printer(&rd_printer_name, &rd_printer_driver_name, &rd_printer_port)?; + + Ok(()) +} + +pub fn uninstall_printer(app_name: &str) { + let printer_name = crate::get_printer_name(app_name); + let driver_name = crate::get_driver_name(); + let port = crate::get_port_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + let rd_printer_driver_name = PCWSTR::from_raw(driver_name.as_ptr()); + let rd_printer_port = PCWSTR::from_raw(port.as_ptr()); + + let _lock = SETUP_MTX.lock().unwrap(); + + allow_err!(delete_printer(&rd_printer_name)); + allow_err!(uninstall_driver(&rd_printer_driver_name)); + allow_err!(check_delete_local_port(&rd_printer_port)); +} diff --git a/vendor/rustdesk/libs/scrap/.gitignore b/vendor/rustdesk/libs/scrap/.gitignore new file mode 100644 index 0000000..5e10219 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/.gitignore @@ -0,0 +1,4 @@ +/target/ +**/*.rs.bk +Cargo.lock +generated/ diff --git a/vendor/rustdesk/libs/scrap/Cargo.toml b/vendor/rustdesk/libs/scrap/Cargo.toml new file mode 100644 index 0000000..505eca2 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "scrap" +description = "Screen capture made easy." +version = "0.5.0" +repository = "https://github.com/quadrupleslap/scrap" +documentation = "https://docs.rs/scrap" +keywords = ["screen", "capture", "record"] +license = "MIT" +authors = ["Ram "] +edition = "2018" + +[features] +wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"] +mediacodec = ["ndk"] +linux-pkg-config = ["dep:pkg-config"] +hwcodec = ["dep:hwcodec"] +vram = ["hwcodec/vram"] + +[dependencies] +cfg-if = "1.0" +num_cpus = "1.15" +lazy_static = "1.4" +hbb_common = { path = "../hbb_common" } +webm = { git = "https://github.com/rustdesk-org/rust-webm" } +serde = {version="1.0", features=["derive"]} + +[dependencies.winapi] +version = "0.3" +default-features = true +features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11", "winuser", "winerror", "errhandlingapi", "libloaderapi"] + +[target.'cfg(target_os = "macos")'.dependencies] +block = "0.1" + +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.13" +jni = "0.21" +lazy_static = "1.4" +log = "0.4" +serde_json = "1.0" +ndk = { version = "0.7", features = ["media"], optional = true} +ndk-context = "0.1" + +[target.'cfg(not(target_os = "android"))'.dev-dependencies] +repng = "0.2" +docopt = "1.1" +quest = "0.3" + +[build-dependencies] +target_build_utils = "0.3" +bindgen = "0.65" +pkg-config = { version = "0.3.27", optional = true } + +[target.'cfg(target_os = "linux")'.dependencies] +dbus = { version = "0.9", optional = true } +tracing = { version = "0.1", optional = true } +gstreamer = { version = "0.16", optional = true } +gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } +gstreamer-video = { version = "0.16", optional = true } +zbus = { version = "3.15", optional = true } + +[dependencies.hwcodec] +git = "https://github.com/rustdesk-org/hwcodec" +optional = true + +[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] +nokhwa = { git = "https://github.com/rustdesk-org/nokhwa.git", branch = "fix_from_raw_parts", features = ["input-native"] } + diff --git a/vendor/rustdesk/libs/scrap/build.rs b/vendor/rustdesk/libs/scrap/build.rs new file mode 100644 index 0000000..7376505 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/build.rs @@ -0,0 +1,267 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, + println, +}; + +#[cfg(all(target_os = "linux", feature = "linux-pkg-config"))] +fn link_pkg_config(name: &str) -> Vec { + // sometimes an override is needed + let pc_name = match name { + "libvpx" => "vpx", + _ => name, + }; + let lib = pkg_config::probe_library(pc_name) + .expect(format!( + "unable to find '{pc_name}' development headers with pkg-config (feature linux-pkg-config is enabled). + try installing '{pc_name}-dev' from your system package manager.").as_str()); + + lib.include_paths +} +#[cfg(not(all(target_os = "linux", feature = "linux-pkg-config")))] +fn link_pkg_config(_name: &str) -> Vec { + unimplemented!() +} + +/// Link vcpkg package. +fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if target_arch == "x86_64" { + target_arch = "x64".to_owned(); + } else if target_arch == "x86" { + target_arch = "x86".to_owned(); + } else if target_arch == "loongarch64" { + target_arch = "loongarch64".to_owned(); + } else if target_arch == "aarch64" { + target_arch = "arm64".to_owned(); + } else { + target_arch = "arm".to_owned(); + } + let mut target = if target_os == "macos" { + if target_arch == "x64" { + "x64-osx".to_owned() + } else if target_arch == "arm64" { + "arm64-osx".to_owned() + } else { + format!("{}-{}", target_arch, target_os) + } + } else if target_os == "windows" { + "x64-windows-static".to_owned() + } else { + format!("{}-{}", target_arch, target_os) + }; + if target_arch == "x86" { + target = target.replace("x64", "x86"); + } + println!("cargo:info={}", target); + if let Ok(vcpkg_root) = std::env::var("VCPKG_INSTALLED_ROOT") { + path = vcpkg_root.into(); + } else { + path.push("installed"); + } + path.push(target); + println!( + "cargo:rustc-link-lib=static={}", + name.trim_start_matches("lib") + ); + println!( + "cargo:rustc-link-search={}", + path.join("lib").to_str().unwrap() + ); + let include = path.join("include"); + println!("cargo:include={}", include.to_str().unwrap()); + include +} + +/// Link homebrew package(for Mac M1). +fn link_homebrew_m1(name: &str) -> PathBuf { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if target_os != "macos" || target_arch != "aarch64" { + panic!("Couldn't find VCPKG_ROOT, also can't fallback to homebrew because it's only for macos aarch64."); + } + let mut path = PathBuf::from("/opt/homebrew/Cellar"); + path.push(name); + let entries = if let Ok(dir) = std::fs::read_dir(&path) { + dir + } else { + panic!("Could not find package in {}. Make sure your homebrew and package {} are all installed.", path.to_str().unwrap(),&name); + }; + let mut directories = entries + .into_iter() + .filter(|x| x.is_ok()) + .map(|x| x.unwrap().path()) + .filter(|x| x.is_dir()) + .collect::>(); + // Find the newest version. + directories.sort_unstable(); + if directories.is_empty() { + panic!( + "There's no installed version of {} in /opt/homebrew/Cellar", + name + ); + } + path.push(directories.pop().unwrap()); + // Link the library. + println!( + "cargo:rustc-link-lib=static={}", + name.trim_start_matches("lib") + ); + // Add the library path. + println!( + "cargo:rustc-link-search={}", + path.join("lib").to_str().unwrap() + ); + // Add the include path. + let include = path.join("include"); + println!("cargo:include={}", include.to_str().unwrap()); + include +} + +/// Find package. By default, it will try to find vcpkg first, then homebrew(currently only for Mac M1). +/// If building for linux and feature "linux-pkg-config" is enabled, will try to use pkg-config +/// unless check fails (e.g. NO_PKG_CONFIG_libyuv=1) +fn find_package(name: &str) -> Vec { + let no_pkg_config_var_name = format!("NO_PKG_CONFIG_{name}"); + println!("cargo:rerun-if-env-changed={no_pkg_config_var_name}"); + if cfg!(all(target_os = "linux", feature = "linux-pkg-config")) + && std::env::var(no_pkg_config_var_name).as_deref() != Ok("1") + { + link_pkg_config(name) + } else if let Ok(vcpkg_root) = std::env::var("VCPKG_ROOT") { + vec![link_vcpkg(vcpkg_root.into(), name)] + } else { + // Try using homebrew + vec![link_homebrew_m1(name)] + } +} + +fn generate_bindings( + ffi_header: &Path, + include_paths: &[PathBuf], + ffi_rs: &Path, + exact_file: &Path, + regex: &str, +) { + let mut b = bindgen::builder() + .header(ffi_header.to_str().unwrap()) + .allowlist_type(regex) + .allowlist_var(regex) + .allowlist_function(regex) + .rustified_enum(regex) + .trust_clang_mangling(false) + .layout_tests(false) // breaks 32/64-bit compat + .generate_comments(false); // comments have prefix /*!\ + + for dir in include_paths { + b = b.clang_arg(format!("-I{}", dir.display())); + } + + b.generate().unwrap().write_to_file(ffi_rs).unwrap(); + fs::copy(ffi_rs, exact_file).ok(); // ignore failure +} + +fn gen_vcpkg_package(package: &str, ffi_header: &str, generated: &str, regex: &str) { + let includes = find_package(package); + let src_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap(); + let src_dir = Path::new(&src_dir); + let out_dir = env::var_os("OUT_DIR").unwrap(); + let out_dir = Path::new(&out_dir); + + let ffi_header = src_dir.join("src").join("bindings").join(ffi_header); + println!("rerun-if-changed={}", ffi_header.display()); + for dir in &includes { + println!("rerun-if-changed={}", dir.display()); + } + + let ffi_rs = out_dir.join(generated); + let exact_file = src_dir.join("generated").join(generated); + generate_bindings(&ffi_header, &includes, &ffi_rs, &exact_file, regex); +} + +// If you have problems installing ffmpeg, you can download $VCPKG_ROOT/installed from ci +// Linux require link in hwcodec +/* +fn ffmpeg() { + // ffmpeg + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + let static_libs = vec!["avcodec", "avutil", "avformat"]; + static_libs.iter().for_each(|lib| { + find_package(lib); + }); + if target_os == "windows" { + println!("cargo:rustc-link-lib=static=libmfx"); + } + + // os + let dyn_libs: Vec<&str> = if target_os == "windows" { + ["User32", "bcrypt", "ole32", "advapi32"].to_vec() + } else if target_os == "linux" { + let mut v = ["va", "va-drm", "va-x11", "vdpau", "X11", "stdc++"].to_vec(); + if target_arch == "x86_64" { + v.push("z"); + } + v + } else if target_os == "macos" || target_os == "ios" { + ["c++", "m"].to_vec() + } else if target_os == "android" { + ["z", "m", "android", "atomic"].to_vec() + } else { + panic!("unsupported os"); + }; + dyn_libs + .iter() + .map(|lib| println!("cargo:rustc-link-lib={}", lib)) + .count(); + + if target_os == "macos" || target_os == "ios" { + println!("cargo:rustc-link-lib=framework=CoreFoundation"); + println!("cargo:rustc-link-lib=framework=CoreVideo"); + println!("cargo:rustc-link-lib=framework=CoreMedia"); + println!("cargo:rustc-link-lib=framework=VideoToolbox"); + println!("cargo:rustc-link-lib=framework=AVFoundation"); + } +} +*/ + +fn main() { + // in this crate, these are also valid configurations + println!("cargo:rustc-check-cfg=cfg(dxgi,quartz,x11)"); + + // there is problem with cfg(target_os) in build.rs, so use our workaround + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + + // note: all link symbol names in x86 (32-bit) are prefixed wth "_". + // run "rustup show" to show current default toolchain, if it is stable-x86-pc-windows-msvc, + // please install x64 toolchain by "rustup toolchain install stable-x86_64-pc-windows-msvc", + // then set x64 to default by "rustup default stable-x86_64-pc-windows-msvc" + let target = target_build_utils::TargetInfo::new(); + if target.unwrap().target_pointer_width() != "64" { + // panic!("Only support 64bit system"); + } + env::remove_var("CARGO_CFG_TARGET_FEATURE"); + env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static"); + + find_package("libyuv"); + gen_vcpkg_package("libvpx", "vpx_ffi.h", "vpx_ffi.rs", "^[vV].*"); + gen_vcpkg_package("aom", "aom_ffi.h", "aom_ffi.rs", "^(aom|AOM|OBU|AV1).*"); + gen_vcpkg_package("libyuv", "yuv_ffi.h", "yuv_ffi.rs", ".*"); + // ffmpeg(); + + if target_os == "ios" { + // nothing + } else if target_os == "android" { + println!("cargo:rustc-cfg=android"); + } else if cfg!(windows) { + // The first choice is Windows because DXGI is amazing. + println!("cargo:rustc-cfg=dxgi"); + } else if cfg!(target_os = "macos") { + // Quartz is second because macOS is the (annoying) exception. + println!("cargo:rustc-cfg=quartz"); + } else if cfg!(unix) { + // On UNIX we pray that X11 (with XCB) is available. + println!("cargo:rustc-cfg=x11"); + } +} diff --git a/vendor/rustdesk/libs/scrap/src/android/ffi.rs b/vendor/rustdesk/libs/scrap/src/android/ffi.rs new file mode 100644 index 0000000..2c891a9 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/android/ffi.rs @@ -0,0 +1,511 @@ +use jni::objects::JByteBuffer; +use jni::objects::JString; +use jni::objects::JValue; +use jni::sys::jboolean; +use jni::JNIEnv; +use jni::{ + objects::{GlobalRef, JClass, JObject}, + strings::JNIString, + JavaVM, +}; + +use hbb_common::{message_proto::MultiClipboards, protobuf::Message}; +use jni::errors::{Error as JniError, Result as JniResult}; +use lazy_static::lazy_static; +use serde::Deserialize; +use std::ops::Not; +use std::os::raw::c_void; +use std::sync::atomic::{AtomicPtr, Ordering::SeqCst}; +use std::sync::{Mutex, RwLock}; +use std::time::{Duration, Instant}; + +lazy_static! { + static ref JVM: RwLock> = RwLock::new(None); + static ref MAIN_SERVICE_CTX: RwLock> = RwLock::new(None); // MainService -> video service / audio service / info + static ref APPLICATION_CONTEXT: RwLock> = RwLock::new(None); + static ref VIDEO_RAW: Mutex = Mutex::new(FrameRaw::new("video", MAX_VIDEO_FRAME_TIMEOUT)); + static ref AUDIO_RAW: Mutex = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT)); + static ref NDK_CONTEXT_INITED: Mutex = Default::default(); + static ref MEDIA_CODEC_INFOS: RwLock> = RwLock::new(None); + static ref CLIPBOARD_MANAGER: RwLock> = RwLock::new(None); + static ref CLIPBOARDS_HOST: Mutex> = Mutex::new(None); + static ref CLIPBOARDS_CLIENT: Mutex> = Mutex::new(None); +} + +const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100); +const MAX_AUDIO_FRAME_TIMEOUT: Duration = Duration::from_millis(1000); + +struct FrameRaw { + name: &'static str, + ptr: AtomicPtr, + len: usize, + last_update: Instant, + timeout: Duration, + enable: bool, +} + +impl FrameRaw { + fn new(name: &'static str, timeout: Duration) -> Self { + FrameRaw { + name, + ptr: AtomicPtr::default(), + len: 0, + last_update: Instant::now(), + timeout, + enable: false, + } + } + + fn set_enable(&mut self, value: bool) { + self.enable = value; + self.ptr.store(std::ptr::null_mut(), SeqCst); + self.len = 0; + } + + fn update(&mut self, data: *mut u8, len: usize) { + if self.enable.not() { + return; + } + self.len = len; + self.ptr.store(data, SeqCst); + self.last_update = Instant::now(); + } + + // take inner data as slice + // release when success + fn take<'a>(&mut self, dst: &mut Vec, last: &mut Vec) -> Option<()> { + if self.enable.not() { + return None; + } + let ptr = self.ptr.load(SeqCst); + if ptr.is_null() || self.len == 0 { + None + } else { + if self.last_update.elapsed() > self.timeout { + log::trace!("Failed to take {} raw,timeout!", self.name); + return None; + } + let slice = unsafe { std::slice::from_raw_parts(ptr, self.len) }; + self.release(); + if last.len() == slice.len() && crate::would_block_if_equal(last, slice).is_err() { + return None; + } + dst.resize(slice.len(), 0); + unsafe { + std::ptr::copy_nonoverlapping(slice.as_ptr(), dst.as_mut_ptr(), slice.len()); + } + Some(()) + } + } + + fn release(&mut self) { + self.len = 0; + self.ptr.store(std::ptr::null_mut(), SeqCst); + } +} + +pub fn get_video_raw<'a>(dst: &mut Vec, last: &mut Vec) -> Option<()> { + VIDEO_RAW.lock().ok()?.take(dst, last) +} + +pub fn get_audio_raw<'a>(dst: &mut Vec, last: &mut Vec) -> Option<()> { + AUDIO_RAW.lock().ok()?.take(dst, last) +} + +pub fn get_clipboards(client: bool) -> Option { + if client { + CLIPBOARDS_CLIENT.lock().ok()?.take() + } else { + CLIPBOARDS_HOST.lock().ok()?.take() + } +} + +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_onVideoFrameUpdate( + env: JNIEnv, + _class: JClass, + buffer: JObject, +) { + let jb = JByteBuffer::from(buffer); + if let Ok(data) = env.get_direct_buffer_address(&jb) { + if let Ok(len) = env.get_direct_buffer_capacity(&jb) { + VIDEO_RAW.lock().unwrap().update(data, len); + } + } +} + +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_onAudioFrameUpdate( + env: JNIEnv, + _class: JClass, + buffer: JObject, +) { + let jb = JByteBuffer::from(buffer); + if let Ok(data) = env.get_direct_buffer_address(&jb) { + if let Ok(len) = env.get_direct_buffer_capacity(&jb) { + AUDIO_RAW.lock().unwrap().update(data, len); + } + } +} + +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_onClipboardUpdate( + env: JNIEnv, + _class: JClass, + buffer: JByteBuffer, +) { + if let Ok(data) = env.get_direct_buffer_address(&buffer) { + if let Ok(len) = env.get_direct_buffer_capacity(&buffer) { + let data = unsafe { std::slice::from_raw_parts(data, len) }; + if let Ok(clips) = MultiClipboards::parse_from_bytes(&data[1..]) { + let is_client = data[0] == 1; + if is_client { + *CLIPBOARDS_CLIENT.lock().unwrap() = Some(clips); + } else { + *CLIPBOARDS_HOST.lock().unwrap() = Some(clips); + } + } + } + } +} + +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_setFrameRawEnable( + env: JNIEnv, + _class: JClass, + name: JString, + value: jboolean, +) { + let mut env = env; + if let Ok(name) = env.get_string(&name) { + let name: String = name.into(); + let value = value.eq(&1); + if name.eq("video") { + VIDEO_RAW.lock().unwrap().set_enable(value); + } else if name.eq("audio") { + AUDIO_RAW.lock().unwrap().set_enable(value); + } + }; +} + +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObject) { + log::debug!("MainService init from java"); + if let Ok(jvm) = env.get_java_vm() { + let java_vm = jvm.get_java_vm_pointer() as *mut c_void; + let mut jvm_lock = JVM.write().unwrap(); + if jvm_lock.is_none() { + *jvm_lock = Some(jvm); + } + drop(jvm_lock); + if let Ok(context) = env.new_global_ref(ctx) { + let context_jobject = context.as_obj().as_raw() as *mut c_void; + *MAIN_SERVICE_CTX.write().unwrap() = Some(context); + init_ndk_context(java_vm, context_jobject); + } + } +} + +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_setClipboardManager( + env: JNIEnv, + _class: JClass, + clipboard_manager: JObject, +) { + log::debug!("ClipboardManager init from java"); + if let Ok(jvm) = env.get_java_vm() { + let java_vm = jvm.get_java_vm_pointer() as *mut c_void; + let mut jvm_lock = JVM.write().unwrap(); + if jvm_lock.is_none() { + *jvm_lock = Some(jvm); + } + drop(jvm_lock); + if let Ok(manager) = env.new_global_ref(clipboard_manager) { + *CLIPBOARD_MANAGER.write().unwrap() = Some(manager); + } + } +} + +#[derive(Debug, Deserialize, Clone)] +pub struct MediaCodecInfo { + pub name: String, + pub is_encoder: bool, + #[serde(default)] + pub hw: Option, // api 29+ + pub mime_type: String, + pub surface: bool, + pub nv12: bool, + #[serde(default)] + pub low_latency: Option, // api 30+, decoder + pub min_bitrate: u32, + pub max_bitrate: u32, + pub min_width: usize, + pub max_width: usize, + pub min_height: usize, + pub max_height: usize, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct MediaCodecInfos { + pub version: usize, + pub w: usize, // aligned + pub h: usize, // aligned + pub codecs: Vec, +} + +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_setCodecInfo(env: JNIEnv, _class: JClass, info: JString) { + let mut env = env; + if let Ok(info) = env.get_string(&info) { + let info: String = info.into(); + if let Ok(infos) = serde_json::from_str::(&info) { + *MEDIA_CODEC_INFOS.write().unwrap() = Some(infos); + } + } +} + +pub fn get_codec_info() -> Option { + MEDIA_CODEC_INFOS.read().unwrap().as_ref().cloned() +} + +pub fn clear_codec_info() { + *MEDIA_CODEC_INFOS.write().unwrap() = None; +} + +// another way to fix "reference table overflow" error caused by new_string and call_main_service_pointer_input frequently calld +// is below, but here I change kind from string to int for performance +/* + env.with_local_frame(10, || { + let kind = env.new_string(kind)?; + env.call_method( + ctx, + "rustPointerInput", + "(Ljava/lang/String;III)V", + &[ + JValue::Object(&JObject::from(kind)), + JValue::Int(mask), + JValue::Int(x), + JValue::Int(y), + ], + )?; + Ok(JObject::null()) + })?; +*/ +pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> { + if let (Some(jvm), Some(ctx)) = ( + JVM.read().unwrap().as_ref(), + MAIN_SERVICE_CTX.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread_as_daemon()?; + let kind = if kind == "touch" { 0 } else { 1 }; + env.call_method( + ctx, + "rustPointerInput", + "(IIII)V", + &[ + JValue::Int(kind), + JValue::Int(mask), + JValue::Int(x), + JValue::Int(y), + ], + )?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_main_service_key_event(data: &[u8]) -> JniResult<()> { + if let (Some(jvm), Some(ctx)) = ( + JVM.read().unwrap().as_ref(), + MAIN_SERVICE_CTX.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread_as_daemon()?; + let data = env.byte_array_from_slice(data)?; + + env.call_method( + ctx, + "rustKeyEventInput", + "([B)V", + &[JValue::Object(&JObject::from(data))], + )?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +fn _call_clipboard_manager(name: S, sig: T, args: &[JValue]) -> JniResult<()> +where + S: Into, + T: Into + AsRef, +{ + if let (Some(jvm), Some(cm)) = ( + JVM.read().unwrap().as_ref(), + CLIPBOARD_MANAGER.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread()?; + env.call_method(cm, name, sig, args)?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_clipboard_manager_update_clipboard(data: &[u8]) -> JniResult<()> { + if let (Some(jvm), Some(cm)) = ( + JVM.read().unwrap().as_ref(), + CLIPBOARD_MANAGER.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread()?; + let data = env.byte_array_from_slice(data)?; + + env.call_method( + cm, + "rustUpdateClipboard", + "([B)V", + &[JValue::Object(&JObject::from(data))], + )?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_clipboard_manager_enable_client_clipboard(enable: bool) -> JniResult<()> { + _call_clipboard_manager( + "rustEnableClientClipboard", + "(Z)V", + &[JValue::Bool(jboolean::from(enable))], + ) +} + +pub fn call_main_service_get_by_name(name: &str) -> JniResult { + if let (Some(jvm), Some(ctx)) = ( + JVM.read().unwrap().as_ref(), + MAIN_SERVICE_CTX.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread_as_daemon()?; + let res = env.with_local_frame(10, |env| -> JniResult { + let name = env.new_string(name)?; + let res = env + .call_method( + ctx, + "rustGetByName", + "(Ljava/lang/String;)Ljava/lang/String;", + &[JValue::Object(&JObject::from(name))], + )? + .l()?; + let res = JString::from(res); + let res = env.get_string(&res)?; + let res = res.to_string_lossy().to_string(); + Ok(res) + })?; + Ok(res) + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +pub fn call_main_service_set_by_name( + name: &str, + arg1: Option<&str>, + arg2: Option<&str>, +) -> JniResult<()> { + if let (Some(jvm), Some(ctx)) = ( + JVM.read().unwrap().as_ref(), + MAIN_SERVICE_CTX.read().unwrap().as_ref(), + ) { + let mut env = jvm.attach_current_thread_as_daemon()?; + env.with_local_frame(10, |env| -> JniResult<()> { + let name = env.new_string(name)?; + let arg1 = env.new_string(arg1.unwrap_or(""))?; + let arg2 = env.new_string(arg2.unwrap_or(""))?; + + env.call_method( + ctx, + "rustSetByName", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + &[ + JValue::Object(&JObject::from(name)), + JValue::Object(&JObject::from(arg1)), + JValue::Object(&JObject::from(arg2)), + ], + )?; + Ok(()) + })?; + return Ok(()); + } else { + return Err(JniError::ThrowFailed(-1)); + } +} + +// Difference between MainService, MainActivity, JNI_OnLoad: +// jvm is the same, ctx is differen and ctx of JNI_OnLoad is null. +// cpal: all three works +// Service(GetByName, ...): only ctx from MainService works, so use 2 init context functions +// On app start: JNI_OnLoad or MainActivity init context +// On service start first time: MainService replace the context + +fn init_ndk_context(java_vm: *mut c_void, context_jobject: *mut c_void) { + let mut lock = NDK_CONTEXT_INITED.lock().unwrap(); + if *lock { + unsafe { + ndk_context::release_android_context(); + } + *lock = false; + } + unsafe { + ndk_context::initialize_android_context(java_vm, context_jobject); + #[cfg(feature = "hwcodec")] + hwcodec::android::ffmpeg_set_java_vm(java_vm); + } + *lock = true; +} + +fn try_init_rustls_platform_verifier(env: &mut JNIEnv, context_jobject: *mut c_void) { + use hbb_common::config::ANDROID_RUSTLS_PLATFORM_VERIFIER_INITIALIZED as INITIALIZED; + use std::sync::atomic::Ordering; + let initialized = INITIALIZED.load(Ordering::Relaxed); + if !initialized { + let ctx_for_rustls = unsafe { JObject::from_raw(context_jobject as jni::sys::jobject) }; + if let Err(e) = + hbb_common::rustls_platform_verifier::android::init_hosted(env, ctx_for_rustls) + { + log::error!("Failed to initialize rustls-platform-verifier: {:?}", e); + } else { + INITIALIZED.store(true, Ordering::Relaxed); + log::info!("rustls-platform-verifier initialized successfully"); + } + } +} + +// https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init +#[no_mangle] +pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint { + if let Ok(env) = vm.get_env() { + let vm = vm.get_java_vm_pointer() as *mut std::os::raw::c_void; + init_ndk_context(vm, res); + } + jni::JNIVersion::V6.into() +} + +#[no_mangle] +pub extern "system" fn Java_ffi_FFI_onAppStart(mut env: JNIEnv, _class: JClass, ctx: JObject) { + if ctx.is_null() { + log::error!("application context is null"); + return; + } + if APPLICATION_CONTEXT.read().unwrap().is_some() { + log::info!("application context already initialized"); + return; + } + if let Ok(jvm) = env.get_java_vm() { + if let Ok(context) = env.new_global_ref(ctx) { + let java_vm = jvm.get_java_vm_pointer() as *mut c_void; + let context_jobject = context.as_obj().as_raw() as *mut c_void; + *APPLICATION_CONTEXT.write().unwrap() = Some(context); + try_init_rustls_platform_verifier(&mut env, context_jobject); + } + } +} diff --git a/vendor/rustdesk/libs/scrap/src/android/mod.rs b/vendor/rustdesk/libs/scrap/src/android/mod.rs new file mode 100644 index 0000000..9040395 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/android/mod.rs @@ -0,0 +1,3 @@ +pub mod ffi; + +pub use ffi::*; diff --git a/vendor/rustdesk/libs/scrap/src/bindings/aom_ffi.h b/vendor/rustdesk/libs/scrap/src/bindings/aom_ffi.h new file mode 100644 index 0000000..bc4077b --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/bindings/aom_ffi.h @@ -0,0 +1,10 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/vendor/rustdesk/libs/scrap/src/bindings/vpx_ffi.h b/vendor/rustdesk/libs/scrap/src/bindings/vpx_ffi.h new file mode 100644 index 0000000..cd44f98 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/bindings/vpx_ffi.h @@ -0,0 +1,9 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/vendor/rustdesk/libs/scrap/src/bindings/yuv_ffi.h b/vendor/rustdesk/libs/scrap/src/bindings/yuv_ffi.h new file mode 100644 index 0000000..1ea24ef --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/bindings/yuv_ffi.h @@ -0,0 +1,6 @@ +#include +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/vendor/rustdesk/libs/scrap/src/common/android.rs b/vendor/rustdesk/libs/scrap/src/common/android.rs new file mode 100644 index 0000000..49956fc --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/android.rs @@ -0,0 +1,189 @@ +use crate::android::ffi::*; +use crate::{Frame, Pixfmt}; +use lazy_static::lazy_static; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::Mutex; +use std::{io, time::Duration}; + +lazy_static! { + pub(crate) static ref SCREEN_SIZE: Mutex<(u16, u16, u16)> = Mutex::new((0, 0, 0)); // (width, height, scale) +} + +pub struct Capturer { + display: Display, + rgba: Vec, + saved_raw_data: Vec, // for faster compare and copy +} + +impl Capturer { + pub fn new(display: Display) -> io::Result { + Ok(Capturer { + display, + rgba: Vec::new(), + saved_raw_data: Vec::new(), + }) + } + + pub fn width(&self) -> usize { + self.display.width() as usize + } + + pub fn height(&self) -> usize { + self.display.height() as usize + } +} + +impl crate::TraitCapturer for Capturer { + fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result> { + if get_video_raw(&mut self.rgba, &mut self.saved_raw_data).is_some() { + Ok(Frame::PixelBuffer(PixelBuffer::new( + &self.rgba, + self.width(), + self.height(), + ))) + } else { + return Err(io::ErrorKind::WouldBlock.into()); + } + } +} + +pub struct PixelBuffer<'a> { + data: &'a [u8], + width: usize, + height: usize, + stride: Vec, +} + +impl<'a> PixelBuffer<'a> { + pub fn new(data: &'a [u8], width: usize, height: usize) -> Self { + let stride0 = data.len() / height; + let mut stride = Vec::new(); + stride.push(stride0); + PixelBuffer { + data, + width, + height, + stride, + } + } +} + +impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { + fn data(&self) -> &[u8] { + self.data + } + + fn width(&self) -> usize { + self.width + } + + fn height(&self) -> usize { + self.height + } + + fn stride(&self) -> Vec { + self.stride.clone() + } + + fn pixfmt(&self) -> Pixfmt { + Pixfmt::RGBA + } +} + +pub struct Display { + default: bool, + rect: Rect, +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +struct Rect { + pub x: i16, + pub y: i16, + pub w: u16, + pub h: u16, +} + +impl Display { + pub fn primary() -> io::Result { + let mut size = SCREEN_SIZE.lock().unwrap(); + if size.0 == 0 || size.1 == 0 { + *size = get_size().unwrap_or_default(); + } + Ok(Display { + default: true, + rect: Rect { + x: 0, + y: 0, + w: size.0, + h: size.1, + }, + }) + } + + pub fn all() -> io::Result> { + Ok(vec![Display::primary()?]) + } + + pub fn width(&self) -> usize { + self.rect.w as usize + } + + pub fn height(&self) -> usize { + self.rect.h as usize + } + + pub fn origin(&self) -> (i32, i32) { + let r = self.rect; + (r.x as _, r.y as _) + } + + pub fn is_online(&self) -> bool { + true + } + + pub fn is_primary(&self) -> bool { + self.default + } + + pub fn name(&self) -> String { + "Android".into() + } + + pub fn refresh_size() { + let mut size = SCREEN_SIZE.lock().unwrap(); + *size = get_size().unwrap_or_default(); + } + + // Big android screen size will be shrinked, to improve performance when screen-capturing and encoding + // e.g 2280x1080 size will be set to 1140x540, and `scale` is 2 + // need to multiply by `4` (2*2) when compute the bitrate + pub fn fix_quality() -> u16 { + let scale = SCREEN_SIZE.lock().unwrap().2; + if scale <= 0 { + 1 + } else { + scale * scale + } + } +} + +fn get_size() -> Option<(u16, u16, u16)> { + let res = call_main_service_get_by_name("screen_size").ok()?; + if let Ok(json) = serde_json::from_str::>(&res) { + if let (Some(Value::Number(w)), Some(Value::Number(h)), Some(Value::Number(scale))) = + (json.get("width"), json.get("height"), json.get("scale")) + { + let w = w.as_i64()? as _; + let h = h.as_i64()? as _; + let scale = scale.as_i64()? as _; + return Some((w, h, scale)); + } + } + None +} + +pub fn is_start() -> Option { + let res = call_main_service_get_by_name("is_start").ok()?; + Some(res == "true") +} diff --git a/vendor/rustdesk/libs/scrap/src/common/aom.rs b/vendor/rustdesk/libs/scrap/src/common/aom.rs new file mode 100644 index 0000000..e5093e5 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/aom.rs @@ -0,0 +1,581 @@ +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(improper_ctypes)] +#![allow(dead_code)] + +include!(concat!(env!("OUT_DIR"), "/aom_ffi.rs")); + +use crate::codec::{base_bitrate, codec_thread_num}; +use crate::{codec::EncoderApi, EncodeFrame, STRIDE_ALIGN}; +use crate::{common::GoogleImage, generate_call_macro, generate_call_ptr_macro, Error, Result}; +use crate::{EncodeInput, EncodeYuvFormat, Pixfmt}; +use hbb_common::{ + anyhow::{anyhow, Context}, + bytes::Bytes, + log, + message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame}, + ResultType, +}; +use std::{ptr, slice}; + +generate_call_macro!(call_aom, false); +generate_call_macro!(call_aom_allow_err, true); +generate_call_ptr_macro!(call_aom_ptr); + +impl Default for aom_codec_enc_cfg_t { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +impl Default for aom_codec_ctx_t { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +impl Default for aom_image_t { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct AomEncoderConfig { + pub width: u32, + pub height: u32, + pub quality: f32, + pub keyframe_interval: Option, +} + +pub struct AomEncoder { + ctx: aom_codec_ctx_t, + width: usize, + height: usize, + i444: bool, + yuvfmt: EncodeYuvFormat, +} + +// https://webrtc.googlesource.com/src/+/refs/heads/main/modules/video_coding/codecs/av1/libaom_av1_encoder.cc +mod webrtc { + use super::*; + + const kUsageProfile: u32 = AOM_USAGE_REALTIME; + const kBitDepth: u32 = 8; + const kLagInFrames: u32 = 0; // No look ahead. + pub(super) const kTimeBaseDen: i64 = 1000; + + // Only positive speeds, range for real-time coding currently is: 6 - 8. + // Lower means slower/better quality, higher means fastest/lower quality. + fn get_cpu_speed(width: u32, height: u32) -> u32 { + // aux_config_ = nullptr, kComplexityHigh + if width * height <= 320 * 180 { + 8 + } else if width * height <= 640 * 360 { + 9 + } else { + 10 + } + } + + fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t { + use aom_superblock_size::*; + let resolution = width * height; + if threads >= 4 && resolution >= 960 * 540 && resolution < 1920 * 1080 { + AOM_SUPERBLOCK_SIZE_64X64 + } else { + AOM_SUPERBLOCK_SIZE_DYNAMIC + } + } + + pub fn enc_cfg( + i: *const aom_codec_iface, + cfg: AomEncoderConfig, + i444: bool, + ) -> ResultType { + let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; + call_aom!(aom_codec_enc_config_default(i, &mut c, kUsageProfile)); + + // Overwrite default config with input encoder settings & RTC-relevant values. + c.g_w = cfg.width; + c.g_h = cfg.height; + c.g_threads = codec_thread_num(64) as _; + c.g_timebase.num = 1; + c.g_timebase.den = kTimeBaseDen as _; + c.g_input_bit_depth = kBitDepth; + if let Some(keyframe_interval) = cfg.keyframe_interval { + c.kf_min_dist = 0; + c.kf_max_dist = keyframe_interval as _; + } else { + c.kf_mode = aom_kf_mode::AOM_KF_DISABLED; + } + let (q_min, q_max) = AomEncoder::calc_q_values(cfg.quality); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = AomEncoder::bitrate(cfg.width as _, cfg.height as _, cfg.quality); + c.rc_undershoot_pct = 50; + c.rc_overshoot_pct = 50; + c.rc_buf_initial_sz = 600; + c.rc_buf_optimal_sz = 600; + c.rc_buf_sz = 1000; + c.g_usage = kUsageProfile; + c.g_error_resilient = 0; + // Low-latency settings. + c.rc_end_usage = aom_rc_mode::AOM_CBR; // Constant Bit Rate (CBR) mode + c.g_pass = aom_enc_pass::AOM_RC_ONE_PASS; // One-pass rate control + c.g_lag_in_frames = kLagInFrames; // No look ahead when lag equals 0. + + // https://aomedia.googlesource.com/aom/+/refs/tags/v3.6.0/av1/common/enums.h#82 + c.g_profile = if i444 { 1 } else { 0 }; + + Ok(c) + } + + pub fn set_controls(ctx: *mut aom_codec_ctx_t, cfg: &aom_codec_enc_cfg) -> ResultType<()> { + use aom_tune_content::*; + use aome_enc_control_id::*; + macro_rules! call_ctl { + ($ctx:expr, $av1e:expr, $arg:expr) => {{ + call_aom_allow_err!(aom_codec_control($ctx, $av1e as i32, $arg)); + }}; + } + + call_ctl!(ctx, AOME_SET_CPUUSED, get_cpu_speed(cfg.g_w, cfg.g_h)); + call_ctl!(ctx, AV1E_SET_ENABLE_CDEF, 1); + call_ctl!(ctx, AV1E_SET_ENABLE_TPL_MODEL, 0); + call_ctl!(ctx, AV1E_SET_DELTAQ_MODE, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_ORDER_HINT, 0); + call_ctl!(ctx, AV1E_SET_AQ_MODE, 3); + call_ctl!(ctx, AOME_SET_MAX_INTRA_BITRATE_PCT, 300); + call_ctl!(ctx, AV1E_SET_COEFF_COST_UPD_FREQ, 3); + call_ctl!(ctx, AV1E_SET_MODE_COST_UPD_FREQ, 3); + call_ctl!(ctx, AV1E_SET_MV_COST_UPD_FREQ, 3); + // kScreensharing + call_ctl!(ctx, AV1E_SET_TUNE_CONTENT, AOM_CONTENT_SCREEN); + call_ctl!(ctx, AV1E_SET_ENABLE_PALETTE, 1); + let tile_set = if cfg.g_threads == 4 && cfg.g_w == 640 && (cfg.g_h == 360 || cfg.g_h == 480) + { + AV1E_SET_TILE_ROWS + } else { + AV1E_SET_TILE_COLUMNS + }; + // Failed on android + call_ctl!(ctx, tile_set, (cfg.g_threads as f64 * 1.0f64).log2().ceil()); + call_ctl!(ctx, AV1E_SET_ROW_MT, 1); + call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0); + call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_WARPED_MOTION, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_GLOBAL_MOTION, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_REF_FRAME_MVS, 0); + call_ctl!( + ctx, + AV1E_SET_SUPERBLOCK_SIZE, + get_super_block_size(cfg.g_w, cfg.g_h, cfg.g_threads) + ); + call_ctl!(ctx, AV1E_SET_ENABLE_CFL_INTRA, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_SMOOTH_INTRA, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_ANGLE_DELTA, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_FILTER_INTRA, 0); + call_ctl!(ctx, AV1E_SET_INTRA_DEFAULT_TX_ONLY, 1); + call_ctl!(ctx, AV1E_SET_DISABLE_TRELLIS_QUANT, 1); + call_ctl!(ctx, AV1E_SET_ENABLE_DIST_WTD_COMP, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_DIFF_WTD_COMP, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_DUAL_FILTER, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_INTERINTRA_COMP, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_INTERINTRA_WEDGE, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_INTRA_EDGE_FILTER, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_INTRABC, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_MASKED_COMP, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_PAETH_INTRA, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_QM, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_RECT_PARTITIONS, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_RESTORATION, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_SMOOTH_INTERINTRA, 0); + call_ctl!(ctx, AV1E_SET_ENABLE_TX64, 0); + call_ctl!(ctx, AV1E_SET_MAX_REFERENCE_FRAMES, 3); + + Ok(()) + } +} + +impl EncoderApi for AomEncoder { + fn new(cfg: crate::codec::EncoderCfg, i444: bool) -> ResultType + where + Self: Sized, + { + match cfg { + crate::codec::EncoderCfg::AOM(config) => { + let i = call_aom_ptr!(aom_codec_av1_cx()); + let c = webrtc::enc_cfg(i, config, i444)?; + + let mut ctx = Default::default(); + // Flag options: AOM_CODEC_USE_PSNR and AOM_CODEC_USE_HIGHBITDEPTH + let flags: aom_codec_flags_t = 0; + call_aom!(aom_codec_enc_init_ver( + &mut ctx, + i, + &c, + flags, + AOM_ENCODER_ABI_VERSION as _ + )); + webrtc::set_controls(&mut ctx, &c)?; + Ok(Self { + ctx, + width: config.width as _, + height: config.height as _, + i444, + yuvfmt: Self::get_yuvfmt(config.width, config.height, i444), + }) + } + _ => Err(anyhow!("encoder type mismatch")), + } + } + + fn encode_to_message(&mut self, input: EncodeInput, ms: i64) -> ResultType { + let mut frames = Vec::new(); + for ref frame in self + .encode(ms, input.yuv()?, STRIDE_ALIGN) + .with_context(|| "Failed to encode")? + { + frames.push(Self::create_frame(frame)); + } + if frames.len() > 0 { + Ok(Self::create_video_frame(frames)) + } else { + Err(anyhow!("no valid frame")) + } + } + + fn yuvfmt(&self) -> crate::EncodeYuvFormat { + self.yuvfmt.clone() + } + + #[cfg(feature = "vram")] + fn input_texture(&self) -> bool { + false + } + + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { + let mut c = unsafe { *self.ctx.config.enc.to_owned() }; + let (q_min, q_max) = Self::calc_q_values(ratio); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = Self::bitrate(self.width as _, self.height as _, ratio); + call_aom!(aom_codec_enc_config_set(&mut self.ctx, &c)); + Ok(()) + } + + fn bitrate(&self) -> u32 { + let c = unsafe { *self.ctx.config.enc.to_owned() }; + c.rc_target_bitrate + } + + fn support_changing_quality(&self) -> bool { + true + } + + fn latency_free(&self) -> bool { + true + } + + fn is_hardware(&self) -> bool { + false + } + + fn disable(&self) {} +} + +impl AomEncoder { + pub fn encode<'a>(&'a mut self, ms: i64, data: &[u8], stride_align: usize) -> Result> { + let bpp = if self.i444 { 24 } else { 12 }; + if data.len() < self.width * self.height * bpp / 8 { + return Err(Error::FailedCall("len not enough".to_string())); + } + let fmt = if self.i444 { + aom_img_fmt::AOM_IMG_FMT_I444 + } else { + aom_img_fmt::AOM_IMG_FMT_I420 + }; + + let mut image = Default::default(); + call_aom_ptr!(aom_img_wrap( + &mut image, + fmt, + self.width as _, + self.height as _, + stride_align as _, + data.as_ptr() as _, + )); + let pts = webrtc::kTimeBaseDen / 1000 * ms; + let duration = webrtc::kTimeBaseDen / 1000; + call_aom!(aom_codec_encode( + &mut self.ctx, + &image, + pts as _, + duration as _, // Duration + 0, // Flags + )); + + Ok(EncodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } + + #[inline] + pub fn create_video_frame(frames: Vec) -> VideoFrame { + let mut vf = VideoFrame::new(); + let av1s = EncodedVideoFrames { + frames: frames.into(), + ..Default::default() + }; + vf.set_av1s(av1s); + vf + } + + #[inline] + fn create_frame(frame: &EncodeFrame) -> EncodedVideoFrame { + EncodedVideoFrame { + data: Bytes::from(frame.data.to_vec()), + key: frame.key, + pts: frame.pts, + ..Default::default() + } + } + + fn bitrate(width: u32, height: u32, ratio: f32) -> u32 { + let bitrate = base_bitrate(width, height) as f32; + (bitrate * ratio) as u32 + } + + #[inline] + fn calc_q_values(ratio: f32) -> (u32, u32) { + let b = (ratio * 100.0) as u32; + let b = std::cmp::min(b, 200); + let q_min1 = 24; + let q_min2 = 5; + let q_max1 = 45; + let q_max2 = 25; + + let t = b as f32 / 200.0; + + let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32; + let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32; + + q_min = q_min.clamp(q_min2, q_min1); + q_max = q_max.clamp(q_max2, q_max1); + + (q_min, q_max) + } + + fn get_yuvfmt(width: u32, height: u32, i444: bool) -> EncodeYuvFormat { + let mut img = Default::default(); + let fmt = if i444 { + aom_img_fmt::AOM_IMG_FMT_I444 + } else { + aom_img_fmt::AOM_IMG_FMT_I420 + }; + unsafe { + aom_img_wrap( + &mut img, + fmt, + width as _, + height as _, + crate::STRIDE_ALIGN as _, + 0x1 as _, + ); + } + let pixfmt = if i444 { Pixfmt::I444 } else { Pixfmt::I420 }; + EncodeYuvFormat { + pixfmt, + w: img.w as _, + h: img.h as _, + stride: img.stride.map(|s| s as usize).to_vec(), + u: img.planes[1] as usize - img.planes[0] as usize, + v: img.planes[2] as usize - img.planes[0] as usize, + } + } +} + +impl Drop for AomEncoder { + fn drop(&mut self) { + unsafe { + let result = aom_codec_destroy(&mut self.ctx); + if result != aom_codec_err_t::AOM_CODEC_OK { + panic!("failed to destroy aom codec"); + } + } + } +} + +pub struct EncodeFrames<'a> { + ctx: &'a mut aom_codec_ctx_t, + iter: aom_codec_iter_t, +} + +impl<'a> Iterator for EncodeFrames<'a> { + type Item = EncodeFrame<'a>; + fn next(&mut self) -> Option { + loop { + unsafe { + let pkt = aom_codec_get_cx_data(self.ctx, &mut self.iter); + if pkt.is_null() { + return None; + } else if (*pkt).kind == aom_codec_cx_pkt_kind::AOM_CODEC_CX_FRAME_PKT { + let f = &(*pkt).data.frame; + return Some(Self::Item { + data: slice::from_raw_parts(f.buf as _, f.sz as _), + key: (f.flags & AOM_FRAME_IS_KEY) != 0, + pts: f.pts, + }); + } else { + // Ignore the packet. + } + } + } + } +} + +pub struct AomDecoder { + ctx: aom_codec_ctx_t, +} + +impl AomDecoder { + pub fn new() -> Result { + let i = call_aom_ptr!(aom_codec_av1_dx()); + let mut ctx = Default::default(); + let cfg = aom_codec_dec_cfg_t { + threads: codec_thread_num(64) as _, + w: 0, + h: 0, + allow_lowbitdepth: 1, + }; + call_aom!(aom_codec_dec_init_ver( + &mut ctx, + i, + &cfg, + 0, + AOM_DECODER_ABI_VERSION as _, + )); + Ok(Self { ctx }) + } + + pub fn decode<'a>(&'a mut self, data: &[u8]) -> Result> { + call_aom!(aom_codec_decode( + &mut self.ctx, + data.as_ptr(), + data.len() as _, + ptr::null_mut(), + )); + + Ok(DecodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } + + /// Notify the decoder to return any pending frame + pub fn flush<'a>(&'a mut self) -> Result> { + call_aom!(aom_codec_decode( + &mut self.ctx, + ptr::null(), + 0, + ptr::null_mut(), + )); + Ok(DecodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } +} + +impl Drop for AomDecoder { + fn drop(&mut self) { + unsafe { + let result = aom_codec_destroy(&mut self.ctx); + if result != aom_codec_err_t::AOM_CODEC_OK { + panic!("failed to destroy aom codec"); + } + } + } +} + +pub struct DecodeFrames<'a> { + ctx: &'a mut aom_codec_ctx_t, + iter: aom_codec_iter_t, +} + +impl<'a> Iterator for DecodeFrames<'a> { + type Item = Image; + fn next(&mut self) -> Option { + let img = unsafe { aom_codec_get_frame(self.ctx, &mut self.iter) }; + if img.is_null() { + return None; + } else { + return Some(Image(img)); + } + } +} + +pub struct Image(*mut aom_image_t); +impl Image { + #[inline] + pub fn new() -> Self { + Self(std::ptr::null_mut()) + } + + #[inline] + pub fn is_null(&self) -> bool { + self.0.is_null() + } + + #[inline] + pub fn format(&self) -> aom_img_fmt_t { + self.inner().fmt + } + + #[inline] + pub fn inner(&self) -> &aom_image_t { + unsafe { &*self.0 } + } +} + +impl GoogleImage for Image { + #[inline] + fn width(&self) -> usize { + self.inner().d_w as _ + } + + #[inline] + fn height(&self) -> usize { + self.inner().d_h as _ + } + + #[inline] + fn stride(&self) -> Vec { + self.inner().stride.iter().map(|x| *x as i32).collect() + } + + #[inline] + fn planes(&self) -> Vec<*mut u8> { + self.inner().planes.iter().map(|p| *p as *mut u8).collect() + } + + fn chroma(&self) -> Chroma { + match self.inner().fmt { + aom_img_fmt::AOM_IMG_FMT_I444 => Chroma::I444, + _ => Chroma::I420, + } + } +} + +impl Drop for Image { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { aom_img_free(self.0) }; + } + } +} + +unsafe impl Send for aom_codec_ctx_t {} diff --git a/vendor/rustdesk/libs/scrap/src/common/camera.rs b/vendor/rustdesk/libs/scrap/src/common/camera.rs new file mode 100644 index 0000000..ea259bd --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/camera.rs @@ -0,0 +1,286 @@ +use std::{ + io, + sync::{Arc, Mutex}, +}; + +#[cfg(any(target_os = "windows", target_os = "linux"))] +use nokhwa::{ + pixel_format::RgbAFormat, + query, + utils::{ApiBackend, CameraIndex, RequestedFormat, RequestedFormatType}, + Camera, +}; + +use hbb_common::message_proto::{DisplayInfo, Resolution}; + +#[cfg(feature = "vram")] +use crate::AdapterDevice; + +use crate::common::{bail, ResultType}; +use crate::{Frame, TraitCapturer}; +#[cfg(any(target_os = "windows", target_os = "linux"))] +use crate::{PixelBuffer, Pixfmt}; + +pub const PRIMARY_CAMERA_IDX: usize = 0; +lazy_static::lazy_static! { + static ref SYNC_CAMERA_DISPLAYS: Arc>> = Arc::new(Mutex::new(Vec::new())); +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +const CAMERA_NOT_SUPPORTED: &str = "This platform doesn't support camera yet"; + +pub struct Cameras; + +// pre-condition +pub fn primary_camera_exists() -> bool { + Cameras::exists(PRIMARY_CAMERA_IDX) +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +impl Cameras { + pub fn all_info() -> ResultType> { + match query(ApiBackend::Auto) { + Ok(cameras) => { + let mut camera_displays = SYNC_CAMERA_DISPLAYS.lock().unwrap(); + camera_displays.clear(); + // FIXME: nokhwa returns duplicate info for one physical camera on linux for now. + // issue: https://github.com/l1npengtul/nokhwa/issues/171 + // Use only one camera as a temporary hack. + cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + let Some(info) = cameras.first() else { + bail!("No camera found") + }; + // Use index (0) camera as main camera, fallback to the first camera if index (0) is not available. + // But maybe we also need to check index (1) or the lowest index camera. + // + // https://askubuntu.com/questions/234362/how-to-fix-this-problem-where-sometimes-dev-video0-becomes-automatically-dev + // https://github.com/rustdesk/rustdesk/pull/12010#issue-3125329069 + let mut camera_index = info.index().clone(); + if !matches!(camera_index, CameraIndex::Index(0)) { + if cameras.iter().any(|cam| matches!(cam.index(), CameraIndex::Index(0))) { + camera_index = CameraIndex::Index(0); + } + } + let camera = Self::create_camera(&camera_index)?; + let resolution = camera.resolution(); + let (width, height) = (resolution.width() as i32, resolution.height() as i32); + camera_displays.push(DisplayInfo { + x: 0, + y: 0, + name: info.human_name().clone(), + width, + height, + online: true, + cursor_embedded: false, + scale:1.0, + original_resolution: Some(Resolution { + width, + height, + ..Default::default() + }).into(), + ..Default::default() + }); + } else { + let mut x = 0; + for info in &cameras { + let camera = Self::create_camera(info.index())?; + let resolution = camera.resolution(); + let (width, height) = (resolution.width() as i32, resolution.height() as i32); + camera_displays.push(DisplayInfo { + x, + y: 0, + name: info.human_name().clone(), + width, + height, + online: true, + cursor_embedded: false, + scale:1.0, + original_resolution: Some(Resolution { + width, + height, + ..Default::default() + }).into(), + ..Default::default() + }); + x += width; + } + } + } + Ok(camera_displays.clone()) + } + Err(e) => { + bail!("Query cameras error: {}", e) + } + } + } + + pub fn exists(index: usize) -> bool { + match query(ApiBackend::Auto) { + Ok(cameras) => index < cameras.len(), + _ => return false, + } + } + + fn create_camera(index: &CameraIndex) -> ResultType { + let format_type = if cfg!(target_os = "linux") { + RequestedFormatType::None + } else { + RequestedFormatType::AbsoluteHighestResolution + }; + let result = Camera::new( + index.clone(), + RequestedFormat::new::(format_type), + ); + match result { + Ok(camera) => Ok(camera), + Err(e) => bail!("create camera{} error: {}", index, e), + } + } + + pub fn get_camera_resolution(index: usize) -> ResultType { + let index = CameraIndex::Index(index as u32); + let camera = Self::create_camera(&index)?; + let resolution = camera.resolution(); + Ok(Resolution { + width: resolution.width() as i32, + height: resolution.height() as i32, + ..Default::default() + }) + } + + pub fn get_sync_cameras() -> Vec { + SYNC_CAMERA_DISPLAYS.lock().unwrap().clone() + } + + pub fn get_capturer(current: usize) -> ResultType> { + Ok(Box::new(CameraCapturer::new(current)?)) + } +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +impl Cameras { + pub fn all_info() -> ResultType> { + return Ok(Vec::new()); + } + + pub fn exists(_index: usize) -> bool { + false + } + + pub fn get_camera_resolution(_index: usize) -> ResultType { + bail!(CAMERA_NOT_SUPPORTED); + } + + pub fn get_sync_cameras() -> Vec { + vec![] + } + + pub fn get_capturer(_current: usize) -> ResultType> { + bail!(CAMERA_NOT_SUPPORTED); + } +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +pub struct CameraCapturer { + camera: Camera, + data: Vec, + last_data: Vec, // for faster compare and copy +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +pub struct CameraCapturer; + +impl CameraCapturer { + #[cfg(any(target_os = "windows", target_os = "linux"))] + fn new(current: usize) -> ResultType { + let index = CameraIndex::Index(current as u32); + let camera = Cameras::create_camera(&index)?; + Ok(CameraCapturer { + camera, + data: Vec::new(), + last_data: Vec::new(), + }) + } + + #[allow(dead_code)] + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + fn new(_current: usize) -> ResultType { + bail!(CAMERA_NOT_SUPPORTED); + } +} + +impl TraitCapturer for CameraCapturer { + #[cfg(any(target_os = "windows", target_os = "linux"))] + fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result> { + // TODO: move this check outside `frame`. + if !self.camera.is_stream_open() { + if let Err(e) = self.camera.open_stream() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera open stream error: {}", e), + )); + } + } + match self.camera.frame() { + Ok(buffer) => { + match buffer.decode_image::() { + Ok(decoded) => { + self.data = decoded.as_raw().to_vec(); + crate::would_block_if_equal(&mut self.last_data, &self.data)?; + // FIXME: macos's PixelBuffer cannot be directly created from bytes slice. + cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "windows"))] { + Ok(Frame::PixelBuffer(PixelBuffer::new( + &self.data, + Pixfmt::RGBA, + decoded.width() as usize, + decoded.height() as usize, + ))) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera is not supported on this platform yet"), + )) + } + } + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera frame decode error: {}", e), + )), + } + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera frame error: {}", e), + )), + } + } + + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result> { + Err(io::Error::new( + io::ErrorKind::Other, + CAMERA_NOT_SUPPORTED.to_string(), + )) + } + + #[cfg(windows)] + fn is_gdi(&self) -> bool { + true + } + + #[cfg(windows)] + fn set_gdi(&mut self) -> bool { + true + } + + #[cfg(feature = "vram")] + fn device(&self) -> AdapterDevice { + AdapterDevice::default() + } + + #[cfg(feature = "vram")] + fn set_output_texture(&mut self, _texture: bool) {} +} diff --git a/vendor/rustdesk/libs/scrap/src/common/codec.rs b/vendor/rustdesk/libs/scrap/src/common/codec.rs new file mode 100644 index 0000000..9b072e1 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/codec.rs @@ -0,0 +1,1157 @@ +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, + sync::{Arc, Mutex}, + time::Instant, +}; + +#[cfg(feature = "hwcodec")] +use crate::hwcodec::*; +#[cfg(feature = "mediacodec")] +use crate::mediacodec::{MediaCodecDecoder, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT}; +#[cfg(feature = "vram")] +use crate::vram::*; +use crate::{ + aom::{self, AomDecoder, AomEncoder, AomEncoderConfig}, + common::GoogleImage, + vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId}, + CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb, ImageTexture, +}; + +#[cfg(any( + feature = "hwcodec", + feature = "mediacodec", + feature = "vram", + target_os = "windows" +))] +use hbb_common::config::option2bool; +use hbb_common::{ + anyhow::anyhow, + bail, + config::{Config, PeerConfig}, + lazy_static, log, + message_proto::{ + supported_decoding::PreferCodec, video_frame, Chroma, CodecAbility, EncodedVideoFrames, + SupportedDecoding, SupportedEncoding, VideoFrame, + }, + sysinfo::System, + ResultType, +}; + +lazy_static::lazy_static! { + static ref PEER_DECODINGS: Arc>> = Default::default(); + static ref ENCODE_CODEC_FORMAT: Arc> = Arc::new(Mutex::new(CodecFormat::VP9)); + static ref THREAD_LOG_TIME: Arc>> = Arc::new(Mutex::new(None)); + static ref USABLE_ENCODING: Arc>> = Arc::new(Mutex::new(None)); +} + +pub const ENCODE_NEED_SWITCH: &'static str = "ENCODE_NEED_SWITCH"; + +#[derive(Debug, Clone)] +pub enum EncoderCfg { + VPX(VpxEncoderConfig), + AOM(AomEncoderConfig), + #[cfg(feature = "hwcodec")] + HWRAM(HwRamEncoderConfig), + #[cfg(feature = "vram")] + VRAM(VRamEncoderConfig), +} + +pub trait EncoderApi { + fn new(cfg: EncoderCfg, i444: bool) -> ResultType + where + Self: Sized; + + fn encode_to_message(&mut self, frame: EncodeInput, ms: i64) -> ResultType; + + fn yuvfmt(&self) -> EncodeYuvFormat; + + #[cfg(feature = "vram")] + fn input_texture(&self) -> bool; + + fn set_quality(&mut self, ratio: f32) -> ResultType<()>; + + fn bitrate(&self) -> u32; + + fn support_changing_quality(&self) -> bool; + + fn latency_free(&self) -> bool; + + fn is_hardware(&self) -> bool; + + fn disable(&self); +} + +pub struct Encoder { + pub codec: Box, +} + +impl Deref for Encoder { + type Target = Box; + + fn deref(&self) -> &Self::Target { + &self.codec + } +} + +impl DerefMut for Encoder { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.codec + } +} + +pub struct Decoder { + vp8: Option, + vp9: Option, + av1: Option, + #[cfg(feature = "hwcodec")] + h264_ram: Option, + #[cfg(feature = "hwcodec")] + h265_ram: Option, + #[cfg(feature = "vram")] + h264_vram: Option, + #[cfg(feature = "vram")] + h265_vram: Option, + #[cfg(feature = "mediacodec")] + h264_media_codec: MediaCodecDecoder, + #[cfg(feature = "mediacodec")] + h265_media_codec: MediaCodecDecoder, + format: CodecFormat, + valid: bool, + #[cfg(feature = "hwcodec")] + i420: Vec, +} + +#[derive(Debug, Clone)] +pub enum EncodingUpdate { + Update(i32, SupportedDecoding), + Remove(i32), + NewOnlyVP9(i32), + Check, +} + +impl Encoder { + pub fn new(config: EncoderCfg, i444: bool) -> ResultType { + log::info!("new encoder: {config:?}, i444: {i444}"); + match config { + EncoderCfg::VPX(_) => Ok(Encoder { + codec: Box::new(VpxEncoder::new(config, i444)?), + }), + EncoderCfg::AOM(_) => Ok(Encoder { + codec: Box::new(AomEncoder::new(config, i444)?), + }), + + #[cfg(feature = "hwcodec")] + EncoderCfg::HWRAM(_) => match HwRamEncoder::new(config, i444) { + Ok(hw) => Ok(Encoder { + codec: Box::new(hw), + }), + Err(e) => { + log::error!("new hw encoder failed: {e:?}, clear config"); + HwCodecConfig::clear(false, true); + *ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9; + Err(e) + } + }, + #[cfg(feature = "vram")] + EncoderCfg::VRAM(_) => match VRamEncoder::new(config, i444) { + Ok(tex) => Ok(Encoder { + codec: Box::new(tex), + }), + Err(e) => { + log::error!("new vram encoder failed: {e:?}, clear config"); + HwCodecConfig::clear(true, true); + *ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9; + Err(e) + } + }, + } + } + + pub fn update(update: EncodingUpdate) { + log::info!("update:{:?}", update); + let mut decodings = PEER_DECODINGS.lock().unwrap(); + match update { + EncodingUpdate::Update(id, decoding) => { + decodings.insert(id, decoding); + } + EncodingUpdate::Remove(id) => { + decodings.remove(&id); + } + EncodingUpdate::NewOnlyVP9(id) => { + decodings.insert( + id, + SupportedDecoding { + ability_vp9: 1, + ..Default::default() + }, + ); + } + EncodingUpdate::Check => {} + } + + let vp8_useable = decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_vp8 > 0); + let av1_useable = decodings.len() > 0 + && decodings.iter().all(|(_, s)| s.ability_av1 > 0) + && !disable_av1(); + let _all_support_h264_decoding = + decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_h264 > 0); + let _all_support_h265_decoding = + decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_h265 > 0); + #[allow(unused_mut)] + let mut h264vram_encoding = false; + #[allow(unused_mut)] + let mut h265vram_encoding = false; + #[cfg(feature = "vram")] + if enable_vram_option(true) { + if _all_support_h264_decoding { + if VRamEncoder::available(CodecFormat::H264).len() > 0 { + h264vram_encoding = true; + } + } + if _all_support_h265_decoding { + if VRamEncoder::available(CodecFormat::H265).len() > 0 { + h265vram_encoding = true; + } + } + } + #[allow(unused_mut)] + let mut h264hw_encoding: Option = None; + #[allow(unused_mut)] + let mut h265hw_encoding: Option = None; + #[cfg(feature = "hwcodec")] + if enable_hwcodec_option() { + if _all_support_h264_decoding { + h264hw_encoding = + HwRamEncoder::try_get(CodecFormat::H264).map_or(None, |c| Some(c.name)); + } + if _all_support_h265_decoding { + h265hw_encoding = + HwRamEncoder::try_get(CodecFormat::H265).map_or(None, |c| Some(c.name)); + } + } + let h264_useable = + _all_support_h264_decoding && (h264vram_encoding || h264hw_encoding.is_some()); + let h265_useable = + _all_support_h265_decoding && (h265vram_encoding || h265hw_encoding.is_some()); + let mut format = ENCODE_CODEC_FORMAT.lock().unwrap(); + let preferences: Vec<_> = decodings + .iter() + .filter(|(_, s)| { + s.prefer == PreferCodec::VP9.into() + || s.prefer == PreferCodec::VP8.into() && vp8_useable + || s.prefer == PreferCodec::AV1.into() && av1_useable + || s.prefer == PreferCodec::H264.into() && h264_useable + || s.prefer == PreferCodec::H265.into() && h265_useable + }) + .map(|(_, s)| s.prefer) + .collect(); + *USABLE_ENCODING.lock().unwrap() = Some(SupportedEncoding { + vp8: vp8_useable, + av1: av1_useable, + h264: h264_useable, + h265: h265_useable, + ..Default::default() + }); + // find the most frequent preference + let mut counts = Vec::new(); + for pref in &preferences { + match counts.iter_mut().find(|(p, _)| p == pref) { + Some((_, count)) => *count += 1, + None => counts.push((pref.clone(), 1)), + } + } + let max_count = counts.iter().map(|(_, count)| *count).max().unwrap_or(0); + let (most_frequent, _) = counts + .into_iter() + .find(|(_, count)| *count == max_count) + .unwrap_or((PreferCodec::Auto.into(), 0)); + 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 mut auto_codec = if av1_useable && av1_test { + CodecFormat::AV1 + } else { + CodecFormat::VP9 + }; + if h264_useable { + auto_codec = CodecFormat::H264; + } + if h265_useable { + auto_codec = CodecFormat::H265; + } + if auto_codec == CodecFormat::VP9 || auto_codec == CodecFormat::AV1 { + let mut system = System::new(); + system.refresh_memory(); + if vp8_useable && system.total_memory() <= 4 * 1024 * 1024 * 1024 { + // 4 Gb + auto_codec = CodecFormat::VP8 + } + } + + *format = match preference { + PreferCodec::VP8 => CodecFormat::VP8, + PreferCodec::VP9 => CodecFormat::VP9, + PreferCodec::AV1 => CodecFormat::AV1, + PreferCodec::H264 => { + if h264vram_encoding || h264hw_encoding.is_some() { + CodecFormat::H264 + } else { + auto_codec + } + } + PreferCodec::H265 => { + if h265vram_encoding || h265hw_encoding.is_some() { + CodecFormat::H265 + } else { + auto_codec + } + } + PreferCodec::Auto => auto_codec, + }; + if decodings.len() > 0 { + log::info!( + "usable: vp8={vp8_useable}, av1={av1_useable}, h264={h264_useable}, h265={h265_useable}", + ); + log::info!( + "connection count: {}, used preference: {:?}, encoder: {:?}", + decodings.len(), + preference, + *format + ) + } + } + + #[inline] + pub fn negotiated_codec() -> CodecFormat { + ENCODE_CODEC_FORMAT.lock().unwrap().clone() + } + + pub fn supported_encoding() -> SupportedEncoding { + #[allow(unused_mut)] + let mut encoding = SupportedEncoding { + vp8: true, + av1: !disable_av1(), + i444: Some(CodecAbility { + vp9: true, + av1: true, + ..Default::default() + }) + .into(), + ..Default::default() + }; + #[cfg(feature = "hwcodec")] + if enable_hwcodec_option() { + encoding.h264 |= HwRamEncoder::try_get(CodecFormat::H264).is_some(); + encoding.h265 |= HwRamEncoder::try_get(CodecFormat::H265).is_some(); + } + #[cfg(feature = "vram")] + if enable_vram_option(true) { + encoding.h264 |= VRamEncoder::available(CodecFormat::H264).len() > 0; + encoding.h265 |= VRamEncoder::available(CodecFormat::H265).len() > 0; + } + encoding + } + + pub fn usable_encoding() -> Option { + USABLE_ENCODING.lock().unwrap().clone() + } + + pub fn set_fallback(config: &EncoderCfg) { + let format = match config { + EncoderCfg::VPX(vpx) => match vpx.codec { + VpxVideoCodecId::VP8 => CodecFormat::VP8, + VpxVideoCodecId::VP9 => CodecFormat::VP9, + }, + EncoderCfg::AOM(_) => CodecFormat::AV1, + #[cfg(feature = "hwcodec")] + EncoderCfg::HWRAM(hw) => { + let name = hw.name.to_lowercase(); + if name.contains("vp8") { + CodecFormat::VP8 + } else if name.contains("vp9") { + CodecFormat::VP9 + } else if name.contains("av1") { + CodecFormat::AV1 + } else if name.contains("h264") { + CodecFormat::H264 + } else { + CodecFormat::H265 + } + } + #[cfg(feature = "vram")] + EncoderCfg::VRAM(vram) => match vram.feature.data_format { + hwcodec::common::DataFormat::H264 => CodecFormat::H264, + hwcodec::common::DataFormat::H265 => CodecFormat::H265, + _ => { + log::error!( + "should not reach here, vram not support {:?}", + vram.feature.data_format + ); + return; + } + }, + }; + let current = ENCODE_CODEC_FORMAT.lock().unwrap().clone(); + if current != format { + log::info!("codec fallback: {:?} -> {:?}", current, format); + *ENCODE_CODEC_FORMAT.lock().unwrap() = format; + } + } + + pub fn use_i444(config: &EncoderCfg) -> bool { + let decodings = PEER_DECODINGS.lock().unwrap().clone(); + let prefer_i444 = decodings + .iter() + .all(|d| d.1.prefer_chroma == Chroma::I444.into()); + let i444_useable = match config { + EncoderCfg::VPX(vpx) => match vpx.codec { + VpxVideoCodecId::VP8 => false, + VpxVideoCodecId::VP9 => decodings.iter().all(|d| d.1.i444.vp9), + }, + EncoderCfg::AOM(_) => decodings.iter().all(|d| d.1.i444.av1), + #[cfg(feature = "hwcodec")] + EncoderCfg::HWRAM(_) => false, + #[cfg(feature = "vram")] + EncoderCfg::VRAM(_) => false, + }; + prefer_i444 && i444_useable && !decodings.is_empty() + } +} + +impl Decoder { + pub fn supported_decodings( + id_for_perfer: Option<&str>, + _use_texture_render: bool, + _luid: Option, + mark_unsupported: &Vec, + ) -> SupportedDecoding { + let (prefer, prefer_chroma) = Self::preference(id_for_perfer); + + #[allow(unused_mut)] + let mut decoding = SupportedDecoding { + ability_vp8: 1, + ability_vp9: 1, + ability_av1: if disable_av1() { 0 } else { 1 }, + i444: Some(CodecAbility { + vp9: true, + av1: true, + ..Default::default() + }) + .into(), + prefer: prefer.into(), + prefer_chroma: prefer_chroma.into(), + ..Default::default() + }; + #[cfg(feature = "hwcodec")] + { + decoding.ability_h264 |= if HwRamDecoder::try_get(CodecFormat::H264).is_some() { + 1 + } else { + 0 + }; + decoding.ability_h265 |= if HwRamDecoder::try_get(CodecFormat::H265).is_some() { + 1 + } else { + 0 + }; + } + #[cfg(feature = "vram")] + if enable_vram_option(false) && _use_texture_render { + decoding.ability_h264 |= if VRamDecoder::available(CodecFormat::H264, _luid).len() > 0 { + 1 + } else { + 0 + }; + decoding.ability_h265 |= if VRamDecoder::available(CodecFormat::H265, _luid).len() > 0 { + 1 + } else { + 0 + }; + } + #[cfg(feature = "mediacodec")] + if enable_hwcodec_option() { + decoding.ability_h264 = + if H264_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) { + 1 + } else { + 0 + }; + decoding.ability_h265 = + if H265_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) { + 1 + } else { + 0 + }; + } + for unsupported in mark_unsupported { + match unsupported { + CodecFormat::VP8 => decoding.ability_vp8 = 0, + CodecFormat::VP9 => decoding.ability_vp9 = 0, + CodecFormat::AV1 => decoding.ability_av1 = 0, + CodecFormat::H264 => decoding.ability_h264 = 0, + CodecFormat::H265 => decoding.ability_h265 = 0, + _ => {} + } + } + decoding + } + + pub fn new(format: CodecFormat, _luid: Option) -> Decoder { + log::info!("try create new decoder, format: {format:?}, _luid: {_luid:?}"); + let (mut vp8, mut vp9, mut av1) = (None, None, None); + #[cfg(feature = "hwcodec")] + let (mut h264_ram, mut h265_ram) = (None, None); + #[cfg(feature = "vram")] + let (mut h264_vram, mut h265_vram) = (None, None); + #[cfg(feature = "mediacodec")] + let (mut h264_media_codec, mut h265_media_codec) = (None, None); + let mut valid = false; + + match format { + CodecFormat::VP8 => { + match VpxDecoder::new(VpxDecoderConfig { + codec: VpxVideoCodecId::VP8, + }) { + Ok(v) => vp8 = Some(v), + Err(e) => log::error!("create VP8 decoder failed: {}", e), + } + valid = vp8.is_some(); + } + CodecFormat::VP9 => { + match VpxDecoder::new(VpxDecoderConfig { + codec: VpxVideoCodecId::VP9, + }) { + Ok(v) => vp9 = Some(v), + Err(e) => log::error!("create VP9 decoder failed: {}", e), + } + valid = vp9.is_some(); + } + CodecFormat::AV1 => { + match AomDecoder::new() { + Ok(v) => av1 = Some(v), + Err(e) => log::error!("create AV1 decoder failed: {}", e), + } + valid = av1.is_some(); + } + CodecFormat::H264 => { + #[cfg(feature = "vram")] + if !valid && enable_vram_option(false) && _luid.clone().unwrap_or_default() != 0 { + match VRamDecoder::new(format, _luid) { + Ok(v) => h264_vram = Some(v), + Err(e) => log::error!("create H264 vram decoder failed: {}", e), + } + valid = h264_vram.is_some(); + } + #[cfg(feature = "hwcodec")] + if !valid { + match HwRamDecoder::new(format) { + Ok(v) => h264_ram = Some(v), + Err(e) => log::error!("create H264 ram decoder failed: {}", e), + } + valid = h264_ram.is_some(); + } + #[cfg(feature = "mediacodec")] + if !valid && enable_hwcodec_option() { + h264_media_codec = MediaCodecDecoder::new(format); + if h264_media_codec.is_none() { + log::error!("create H264 media codec decoder failed"); + } + valid = h264_media_codec.is_some(); + } + } + CodecFormat::H265 => { + #[cfg(feature = "vram")] + if !valid && enable_vram_option(false) && _luid.clone().unwrap_or_default() != 0 { + match VRamDecoder::new(format, _luid) { + Ok(v) => h265_vram = Some(v), + Err(e) => log::error!("create H265 vram decoder failed: {}", e), + } + valid = h265_vram.is_some(); + } + #[cfg(feature = "hwcodec")] + if !valid { + match HwRamDecoder::new(format) { + Ok(v) => h265_ram = Some(v), + Err(e) => log::error!("create H265 ram decoder failed: {}", e), + } + valid = h265_ram.is_some(); + } + #[cfg(feature = "mediacodec")] + if !valid && enable_hwcodec_option() { + h265_media_codec = MediaCodecDecoder::new(format); + if h265_media_codec.is_none() { + log::error!("create H265 media codec decoder failed"); + } + valid = h265_media_codec.is_some(); + } + } + CodecFormat::Unknown => { + log::error!("unknown codec format, cannot create decoder"); + } + } + if !valid { + log::error!("failed to create {format:?} decoder"); + } else { + log::info!("create {format:?} decoder success"); + } + Decoder { + vp8, + vp9, + av1, + #[cfg(feature = "hwcodec")] + h264_ram, + #[cfg(feature = "hwcodec")] + h265_ram, + #[cfg(feature = "vram")] + h264_vram, + #[cfg(feature = "vram")] + h265_vram, + #[cfg(feature = "mediacodec")] + h264_media_codec, + #[cfg(feature = "mediacodec")] + h265_media_codec, + format, + valid, + #[cfg(feature = "hwcodec")] + i420: vec![], + } + } + + pub fn format(&self) -> CodecFormat { + self.format + } + + pub fn valid(&self) -> bool { + self.valid + } + + // rgb [in/out] fmt and stride must be set in ImageRgb + pub fn handle_video_frame( + &mut self, + frame: &video_frame::Union, + rgb: &mut ImageRgb, + _texture: &mut ImageTexture, + _pixelbuffer: &mut bool, + chroma: &mut Option, + ) -> ResultType { + match frame { + video_frame::Union::Vp8s(vp8s) => { + if let Some(vp8) = &mut self.vp8 { + Decoder::handle_vpxs_video_frame(vp8, vp8s, rgb, chroma) + } else { + bail!("vp8 decoder not available"); + } + } + video_frame::Union::Vp9s(vp9s) => { + if let Some(vp9) = &mut self.vp9 { + Decoder::handle_vpxs_video_frame(vp9, vp9s, rgb, chroma) + } else { + bail!("vp9 decoder not available"); + } + } + video_frame::Union::Av1s(av1s) => { + if let Some(av1) = &mut self.av1 { + Decoder::handle_av1s_video_frame(av1, av1s, rgb, chroma) + } else { + bail!("av1 decoder not available"); + } + } + #[cfg(any(feature = "hwcodec", feature = "vram"))] + video_frame::Union::H264s(h264s) => { + *chroma = Some(Chroma::I420); + #[cfg(feature = "vram")] + if let Some(decoder) = &mut self.h264_vram { + *_pixelbuffer = false; + return Decoder::handle_vram_video_frame(decoder, h264s, _texture); + } + #[cfg(feature = "hwcodec")] + if let Some(decoder) = &mut self.h264_ram { + return Decoder::handle_hwram_video_frame(decoder, h264s, rgb, &mut self.i420); + } + Err(anyhow!("don't support h264!")) + } + #[cfg(any(feature = "hwcodec", feature = "vram"))] + video_frame::Union::H265s(h265s) => { + *chroma = Some(Chroma::I420); + #[cfg(feature = "vram")] + if let Some(decoder) = &mut self.h265_vram { + *_pixelbuffer = false; + return Decoder::handle_vram_video_frame(decoder, h265s, _texture); + } + #[cfg(feature = "hwcodec")] + if let Some(decoder) = &mut self.h265_ram { + return Decoder::handle_hwram_video_frame(decoder, h265s, rgb, &mut self.i420); + } + Err(anyhow!("don't support h265!")) + } + #[cfg(feature = "mediacodec")] + video_frame::Union::H264s(h264s) => { + *chroma = Some(Chroma::I420); + if let Some(decoder) = &mut self.h264_media_codec { + Decoder::handle_mediacodec_video_frame(decoder, h264s, rgb) + } else { + Err(anyhow!("don't support h264!")) + } + } + #[cfg(feature = "mediacodec")] + video_frame::Union::H265s(h265s) => { + *chroma = Some(Chroma::I420); + if let Some(decoder) = &mut self.h265_media_codec { + Decoder::handle_mediacodec_video_frame(decoder, h265s, rgb) + } else { + Err(anyhow!("don't support h265!")) + } + } + _ => Err(anyhow!("unsupported video frame type!")), + } + } + + // rgb [in/out] fmt and stride must be set in ImageRgb + fn handle_vpxs_video_frame( + decoder: &mut VpxDecoder, + vpxs: &EncodedVideoFrames, + rgb: &mut ImageRgb, + chroma: &mut Option, + ) -> ResultType { + let mut last_frame = vpxcodec::Image::new(); + for vpx in vpxs.frames.iter() { + for frame in decoder.decode(&vpx.data)? { + drop(last_frame); + last_frame = frame; + } + } + for frame in decoder.flush()? { + drop(last_frame); + last_frame = frame; + } + if last_frame.is_null() { + Ok(false) + } else { + *chroma = Some(last_frame.chroma()); + last_frame.to(rgb); + Ok(true) + } + } + + // rgb [in/out] fmt and stride must be set in ImageRgb + fn handle_av1s_video_frame( + decoder: &mut AomDecoder, + av1s: &EncodedVideoFrames, + rgb: &mut ImageRgb, + chroma: &mut Option, + ) -> ResultType { + let mut last_frame = aom::Image::new(); + for av1 in av1s.frames.iter() { + for frame in decoder.decode(&av1.data)? { + drop(last_frame); + last_frame = frame; + } + } + for frame in decoder.flush()? { + drop(last_frame); + last_frame = frame; + } + if last_frame.is_null() { + Ok(false) + } else { + *chroma = Some(last_frame.chroma()); + last_frame.to(rgb); + Ok(true) + } + } + + // rgb [in/out] fmt and stride must be set in ImageRgb + #[cfg(feature = "hwcodec")] + fn handle_hwram_video_frame( + decoder: &mut HwRamDecoder, + frames: &EncodedVideoFrames, + rgb: &mut ImageRgb, + i420: &mut Vec, + ) -> ResultType { + let mut ret = false; + for h264 in frames.frames.iter() { + for image in decoder.decode(&h264.data)? { + // TODO: just process the last frame + if image.to_fmt(rgb, i420).is_ok() { + ret = true; + } + } + } + return Ok(ret); + } + + #[cfg(feature = "vram")] + fn handle_vram_video_frame( + decoder: &mut VRamDecoder, + frames: &EncodedVideoFrames, + texture: &mut ImageTexture, + ) -> ResultType { + let mut ret = false; + for h26x in frames.frames.iter() { + for image in decoder.decode(&h26x.data)? { + *texture = ImageTexture { + texture: image.frame.texture, + w: image.frame.width as _, + h: image.frame.height as _, + }; + ret = true; + } + } + return Ok(ret); + } + + // rgb [in/out] fmt and stride must be set in ImageRgb + #[cfg(feature = "mediacodec")] + fn handle_mediacodec_video_frame( + decoder: &mut MediaCodecDecoder, + frames: &EncodedVideoFrames, + rgb: &mut ImageRgb, + ) -> ResultType { + let mut ret = false; + for h264 in frames.frames.iter() { + return decoder.decode(&h264.data, rgb); + } + return Ok(false); + } + + fn preference(id: Option<&str>) -> (PreferCodec, Chroma) { + let id = id.unwrap_or_default(); + if id.is_empty() { + return (PreferCodec::Auto, Chroma::I420); + } + let options = PeerConfig::load(id).options; + let codec = options + .get("codec-preference") + .map_or("".to_owned(), |c| c.to_owned()); + let codec = if codec == "vp8" { + PreferCodec::VP8 + } else if codec == "vp9" { + PreferCodec::VP9 + } else if codec == "av1" { + PreferCodec::AV1 + } else if codec == "h264" { + PreferCodec::H264 + } else if codec == "h265" { + PreferCodec::H265 + } else { + PreferCodec::Auto + }; + let chroma = if options.get("i444") == Some(&"Y".to_string()) { + Chroma::I444 + } else { + Chroma::I420 + }; + (codec, chroma) + } +} + +#[cfg(any(feature = "hwcodec", feature = "mediacodec"))] +pub fn enable_hwcodec_option() -> bool { + use hbb_common::config::keys::OPTION_ENABLE_HWCODEC; + + if !cfg!(target_os = "ios") { + return option2bool( + OPTION_ENABLE_HWCODEC, + &Config::get_option(OPTION_ENABLE_HWCODEC), + ); + } + false +} +#[cfg(feature = "vram")] +pub fn enable_vram_option(encode: bool) -> bool { + use hbb_common::config::keys::OPTION_ENABLE_HWCODEC; + + if cfg!(windows) { + let enable = option2bool( + OPTION_ENABLE_HWCODEC, + &Config::get_option(OPTION_ENABLE_HWCODEC), + ); + if encode { + enable && enable_directx_capture() + } else { + enable && allow_d3d_render() + } + } else { + false + } +} + +#[cfg(windows)] +pub fn enable_directx_capture() -> bool { + use hbb_common::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; + option2bool(OPTION, &hbb_common::config::LocalConfig::get_option(OPTION)) +} + +pub const BR_BEST: f32 = 1.5; +pub const BR_BALANCED: f32 = 0.67; +pub const BR_SPEED: f32 = 0.5; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Quality { + Best, + Balanced, + Low, + Custom(f32), +} + +impl Default for Quality { + fn default() -> Self { + Self::Balanced + } +} + +impl Quality { + pub fn is_custom(&self) -> bool { + match self { + Quality::Custom(_) => true, + _ => false, + } + } + + pub fn ratio(&self) -> f32 { + match self { + Quality::Best => BR_BEST, + Quality::Balanced => BR_BALANCED, + Quality::Low => BR_SPEED, + Quality::Custom(v) => *v, + } + } +} + +pub fn base_bitrate(width: u32, height: u32) -> u32 { + const RESOLUTION_PRESETS: &[(u32, u32, u32)] = &[ + (640, 480, 400), // VGA, 307k pixels + (800, 600, 500), // SVGA, 480k pixels + (1024, 768, 800), // XGA, 786k pixels + (1280, 720, 1000), // 720p, 921k pixels + (1366, 768, 1100), // HD, 1049k pixels + (1440, 900, 1300), // WXGA+, 1296k pixels + (1600, 900, 1500), // HD+, 1440k pixels + (1920, 1080, 2073), // 1080p, 2073k pixels + (2048, 1080, 2200), // 2K DCI, 2211k pixels + (2560, 1440, 3000), // 2K QHD, 3686k pixels + (3440, 1440, 4000), // UWQHD, 4953k pixels + (3840, 2160, 5000), // 4K UHD, 8294k pixels + (7680, 4320, 12000), // 8K UHD, 33177k pixels + ]; + let pixels = width * height; + + let (preset_pixels, preset_bitrate) = RESOLUTION_PRESETS + .iter() + .map(|(w, h, bitrate)| (w * h, bitrate)) + .min_by_key(|(preset_pixels, _)| { + if *preset_pixels >= pixels { + preset_pixels - pixels + } else { + pixels - preset_pixels + } + }) + .unwrap_or(((1920 * 1080) as u32, &2073)); // default 1080p + + let bitrate = (*preset_bitrate as f32 * (pixels as f32 / preset_pixels as f32)).round() as u32; + + #[cfg(target_os = "android")] + { + let fix = crate::Display::fix_quality() as u32; + log::debug!("Android screen, fix quality:{}", fix); + bitrate * fix + } + #[cfg(not(target_os = "android"))] + { + bitrate + } +} + +pub fn codec_thread_num(limit: usize) -> usize { + let max: usize = num_cpus::get(); + let mut res; + let info; + let mut s = System::new(); + s.refresh_memory(); + let memory = s.available_memory() / 1024 / 1024 / 1024; + #[cfg(windows)] + { + res = 0; + let percent = hbb_common::platform::windows::cpu_uage_one_minute(); + info = format!("cpu usage: {:?}", percent); + if let Some(pecent) = percent { + if pecent < 100.0 { + res = ((100.0 - pecent) * (max as f64) / 200.0).round() as usize; + } + } + } + #[cfg(not(windows))] + { + s.refresh_cpu_usage(); + // https://man7.org/linux/man-pages/man3/getloadavg.3.html + let avg = s.load_average(); + info = format!("cpu loadavg: {}", avg.one); + res = (((max as f64) - avg.one) * 0.5).round() as usize; + } + res = std::cmp::min(res, max / 2); + res = std::cmp::min(res, memory as usize / 2); + // Use common thread count + res = match res { + _ if res >= 64 => 64, + _ if res >= 32 => 32, + _ if res >= 16 => 16, + _ if res >= 8 => 8, + _ if res >= 4 => 4, + _ if res >= 2 => 2, + _ => 1, + }; + // https://aomedia.googlesource.com/aom/+/refs/heads/main/av1/av1_cx_iface.c#677 + // https://aomedia.googlesource.com/aom/+/refs/heads/main/aom_util/aom_thread.h#26 + // https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp8/vp8_cx_iface.c#148 + // https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp9/vp9_cx_iface.c#190 + // https://github.com/FFmpeg/FFmpeg/blob/7c16bf0829802534004326c8e65fb6cdbdb634fa/libavcodec/pthread.c#L65 + // https://github.com/FFmpeg/FFmpeg/blob/7c16bf0829802534004326c8e65fb6cdbdb634fa/libavcodec/pthread_internal.h#L26 + // libaom: MAX_NUM_THREADS = 64 + // libvpx: MAX_NUM_THREADS = 64 + // ffmpeg: MAX_AUTO_THREADS = 16 + res = std::cmp::min(res, limit); + // avoid frequent log + let log = match THREAD_LOG_TIME.lock().unwrap().clone() { + Some(instant) => instant.elapsed().as_secs() > 1, + None => true, + }; + if log { + log::info!("cpu num: {max}, {info}, available memory: {memory}G, codec thread: {res}"); + *THREAD_LOG_TIME.lock().unwrap() = Some(Instant::now()); + } + res +} + +fn disable_av1() -> bool { + // aom is very slow for x86 sciter version on windows x64 + // disable it for all 32 bit platforms + std::mem::size_of::() == 4 +} + +#[cfg(not(target_os = "ios"))] +pub fn test_av1() { + use hbb_common::config::keys::OPTION_AV1_TEST; + use hbb_common::rand::Rng; + use std::{sync::Once, time::Duration}; + + if disable_av1() || !Config::get_option(OPTION_AV1_TEST).is_empty() { + log::info!("skip test av1"); + return; + } + + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + let f = || { + let (width, height, quality, keyframe_interval, i444) = (1920, 1080, 1.0, None, false); + let frame_count = 10; + let block_size = 300; + let move_step = 50; + let generate_fake_data = + |frame_index: u32, dst_fmt: EncodeYuvFormat| -> ResultType> { + let mut rng = hbb_common::rand::thread_rng(); + let mut bgra = vec![0u8; (width * height * 4) as usize]; + let gradient = frame_index as f32 / frame_count as f32; + // floating block + let x0 = (frame_index * move_step) % (width - block_size); + let y0 = (frame_index * move_step) % (height - block_size); + // Fill the block with random colors + for y in 0..block_size { + for x in 0..block_size { + let index = (((y0 + y) * width + x0 + x) * 4) as usize; + if index + 3 < bgra.len() { + let noise = rng.gen_range(0..255) as f32 / 255.0; + let value = (255.0 * gradient + noise * 50.0) as u8; + bgra[index] = value; + bgra[index + 1] = value; + bgra[index + 2] = value; + bgra[index + 3] = 255; + } + } + } + let dst_stride_y = dst_fmt.stride[0]; + let dst_stride_uv = dst_fmt.stride[1]; + let mut dst = vec![0u8; (dst_fmt.h * dst_stride_y * 2) as usize]; + let dst_y = dst.as_mut_ptr(); + let dst_u = dst[dst_fmt.u..].as_mut_ptr(); + let dst_v = dst[dst_fmt.v..].as_mut_ptr(); + let res = unsafe { + crate::ARGBToI420( + bgra.as_ptr(), + (width * 4) as _, + dst_y, + dst_stride_y as _, + dst_u, + dst_stride_uv as _, + dst_v, + dst_stride_uv as _, + width as _, + height as _, + ) + }; + if res != 0 { + bail!("ARGBToI420 failed: {}", res); + } + Ok(dst) + }; + let Ok(mut av1) = AomEncoder::new( + EncoderCfg::AOM(AomEncoderConfig { + width, + height, + quality, + keyframe_interval, + }), + i444, + ) else { + return false; + }; + let mut key_frame_time = Duration::ZERO; + let mut non_key_frame_time_sum = Duration::ZERO; + let pts = Instant::now(); + let yuvfmt = av1.yuvfmt(); + for i in 0..frame_count { + let Ok(yuv) = generate_fake_data(i, yuvfmt.clone()) else { + return false; + }; + let start = Instant::now(); + if av1 + .encode(pts.elapsed().as_millis() as _, &yuv, super::STRIDE_ALIGN) + .is_err() + { + log::debug!("av1 encode failed"); + if i == 0 { + return false; + } + } + if i == 0 { + key_frame_time = start.elapsed(); + } else { + non_key_frame_time_sum += start.elapsed(); + } + } + let non_key_frame_time = non_key_frame_time_sum / (frame_count - 1); + log::info!( + "av1 time: key: {:?}, non-key: {:?}, consume: {:?}", + key_frame_time, + non_key_frame_time, + pts.elapsed() + ); + key_frame_time < Duration::from_millis(90) + && non_key_frame_time < Duration::from_millis(30) + }; + std::thread::spawn(move || { + let v = f(); + Config::set_option( + OPTION_AV1_TEST.to_string(), + if v { "Y" } else { "N" }.to_string(), + ); + }); + }); +} diff --git a/vendor/rustdesk/libs/scrap/src/common/convert.rs b/vendor/rustdesk/libs/scrap/src/common/convert.rs new file mode 100644 index 0000000..40c17e5 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/convert.rs @@ -0,0 +1,236 @@ +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(improper_ctypes)] +#![allow(dead_code)] + +include!(concat!(env!("OUT_DIR"), "/yuv_ffi.rs")); + +#[cfg(not(target_os = "ios"))] +use crate::PixelBuffer; +use crate::{generate_call_macro, EncodeYuvFormat, TraitPixelBuffer}; +use hbb_common::{bail, log, ResultType}; + +generate_call_macro!(call_yuv, false); + +#[cfg(not(target_os = "ios"))] +pub fn convert_to_yuv( + captured: &PixelBuffer, + dst_fmt: EncodeYuvFormat, + dst: &mut Vec, + mid_data: &mut Vec, +) -> ResultType<()> { + let src = captured.data(); + let src_stride = captured.stride(); + let src_pixfmt = captured.pixfmt(); + let src_width = captured.width(); + let src_height = captured.height(); + if src_width > dst_fmt.w || src_height > dst_fmt.h { + bail!( + "src rect > dst rect: ({src_width}, {src_height}) > ({},{})", + dst_fmt.w, + dst_fmt.h + ); + } + if src_pixfmt == crate::Pixfmt::BGRA + || src_pixfmt == crate::Pixfmt::RGBA + || src_pixfmt == crate::Pixfmt::RGB565LE + { + // stride is calculated, not real, so we need to check it + if src_stride[0] < src_width * src_pixfmt.bytes_per_pixel() { + bail!( + "src_stride too small: {} < {}", + src_stride[0], + src_width * src_pixfmt.bytes_per_pixel() + ); + } + if src.len() < src_stride[0] * src_height { + bail!( + "wrong src len, {} < {} * {}", + src.len(), + src_stride[0], + src_height + ); + } + } + let align = |x: usize| (x + 63) / 64 * 64; + let unsupported = format!( + "unsupported pixfmt conversion: {src_pixfmt:?} -> {:?}", + dst_fmt.pixfmt + ); + + match (src_pixfmt, dst_fmt.pixfmt) { + (crate::Pixfmt::BGRA, crate::Pixfmt::I420) + | (crate::Pixfmt::RGBA, crate::Pixfmt::I420) + | (crate::Pixfmt::RGB565LE, crate::Pixfmt::I420) => { + let dst_stride_y = dst_fmt.stride[0]; + let dst_stride_uv = dst_fmt.stride[1]; + dst.resize(dst_fmt.h * dst_stride_y * 2, 0); // waste some memory to ensure memory safety + let dst_y = dst.as_mut_ptr(); + let dst_u = dst[dst_fmt.u..].as_mut_ptr(); + let dst_v = dst[dst_fmt.v..].as_mut_ptr(); + let f = match src_pixfmt { + crate::Pixfmt::BGRA => ARGBToI420, + crate::Pixfmt::RGBA => ABGRToI420, + crate::Pixfmt::RGB565LE => RGB565ToI420, + _ => bail!(unsupported), + }; + call_yuv!(f( + src.as_ptr(), + src_stride[0] as _, + dst_y, + dst_stride_y as _, + dst_u, + dst_stride_uv as _, + dst_v, + dst_stride_uv as _, + src_width as _, + src_height as _, + )); + } + (crate::Pixfmt::BGRA, crate::Pixfmt::NV12) + | (crate::Pixfmt::RGBA, crate::Pixfmt::NV12) + | (crate::Pixfmt::RGB565LE, crate::Pixfmt::NV12) => { + let dst_stride_y = dst_fmt.stride[0]; + let dst_stride_uv = dst_fmt.stride[1]; + dst.resize( + align(dst_fmt.h) * (align(dst_stride_y) + align(dst_stride_uv / 2)), + 0, + ); + let dst_y = dst.as_mut_ptr(); + let dst_uv = dst[dst_fmt.u..].as_mut_ptr(); + let (input, input_stride) = match src_pixfmt { + crate::Pixfmt::BGRA => (src.as_ptr(), src_stride[0]), + crate::Pixfmt::RGBA => (src.as_ptr(), src_stride[0]), + crate::Pixfmt::RGB565LE => { + let mid_stride = src_width * 4; + mid_data.resize(mid_stride * src_height, 0); + call_yuv!(RGB565ToARGB( + src.as_ptr(), + src_stride[0] as _, + mid_data.as_mut_ptr(), + mid_stride as _, + src_width as _, + src_height as _, + )); + (mid_data.as_ptr(), mid_stride) + } + _ => bail!(unsupported), + }; + let f = match src_pixfmt { + crate::Pixfmt::BGRA => ARGBToNV12, + crate::Pixfmt::RGBA => ABGRToNV12, + crate::Pixfmt::RGB565LE => ARGBToNV12, + _ => bail!(unsupported), + }; + call_yuv!(f( + input, + input_stride as _, + dst_y, + dst_stride_y as _, + dst_uv, + dst_stride_uv as _, + src_width as _, + src_height as _, + )); + } + (crate::Pixfmt::BGRA, crate::Pixfmt::I444) + | (crate::Pixfmt::RGBA, crate::Pixfmt::I444) + | (crate::Pixfmt::RGB565LE, crate::Pixfmt::I444) => { + let dst_stride_y = dst_fmt.stride[0]; + let dst_stride_u = dst_fmt.stride[1]; + let dst_stride_v = dst_fmt.stride[2]; + dst.resize( + align(dst_fmt.h) + * (align(dst_stride_y) + align(dst_stride_u) + align(dst_stride_v)), + 0, + ); + let dst_y = dst.as_mut_ptr(); + let dst_u = dst[dst_fmt.u..].as_mut_ptr(); + let dst_v = dst[dst_fmt.v..].as_mut_ptr(); + let (input, input_stride) = match src_pixfmt { + crate::Pixfmt::BGRA => (src.as_ptr(), src_stride[0]), + crate::Pixfmt::RGBA => { + mid_data.resize(src.len(), 0); + call_yuv!(ABGRToARGB( + src.as_ptr(), + src_stride[0] as _, + mid_data.as_mut_ptr(), + src_stride[0] as _, + src_width as _, + src_height as _, + )); + (mid_data.as_ptr(), src_stride[0]) + } + crate::Pixfmt::RGB565LE => { + let mid_stride = src_width * 4; + mid_data.resize(mid_stride * src_height, 0); + call_yuv!(RGB565ToARGB( + src.as_ptr(), + src_stride[0] as _, + mid_data.as_mut_ptr(), + mid_stride as _, + src_width as _, + src_height as _, + )); + (mid_data.as_ptr(), mid_stride) + } + _ => bail!(unsupported), + }; + + call_yuv!(ARGBToI444( + input, + input_stride as _, + dst_y, + dst_stride_y as _, + dst_u, + dst_stride_u as _, + dst_v, + dst_stride_v as _, + src_width as _, + src_height as _, + )); + } + _ => { + bail!(unsupported); + } + } + Ok(()) +} + +#[cfg(not(target_os = "ios"))] +pub fn convert(captured: &PixelBuffer, pixfmt: crate::Pixfmt, dst: &mut Vec) -> ResultType<()> { + if captured.pixfmt() == pixfmt { + dst.extend_from_slice(captured.data()); + return Ok(()); + } + + let src = captured.data(); + let src_stride = captured.stride(); + let src_pixfmt = captured.pixfmt(); + let src_width = captured.width(); + let src_height = captured.height(); + + let unsupported = format!( + "unsupported pixfmt conversion: {src_pixfmt:?} -> {:?}", + pixfmt + ); + + match (src_pixfmt, pixfmt) { + (crate::Pixfmt::BGRA, crate::Pixfmt::RGBA) | (crate::Pixfmt::RGBA, crate::Pixfmt::BGRA) => { + dst.resize(src.len(), 0); + call_yuv!(ABGRToARGB( + src.as_ptr(), + src_stride[0] as _, + dst.as_mut_ptr(), + src_stride[0] as _, + src_width as _, + src_height as _, + )); + } + _ => { + bail!(unsupported); + } + } + Ok(()) +} diff --git a/vendor/rustdesk/libs/scrap/src/common/dxgi.rs b/vendor/rustdesk/libs/scrap/src/common/dxgi.rs new file mode 100644 index 0000000..f7bf167 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/dxgi.rs @@ -0,0 +1,264 @@ +#[cfg(feature = "vram")] +use crate::AdapterDevice; +use crate::{common::TraitCapturer, dxgi, Frame, Pixfmt}; +use std::{ + io::{ + self, + ErrorKind::{NotFound, TimedOut, WouldBlock}, + }, + time::Duration, +}; + +pub struct Capturer { + inner: dxgi::Capturer, + width: usize, + height: usize, +} + +impl Capturer { + pub fn new(display: Display) -> io::Result { + let width = display.width(); + let height = display.height(); + let inner = dxgi::Capturer::new(display.0)?; + Ok(Capturer { + inner, + width, + height, + }) + } + + pub fn cancel_gdi(&mut self) { + self.inner.cancel_gdi() + } + + pub fn width(&self) -> usize { + self.width + } + + pub fn height(&self) -> usize { + self.height + } +} + +impl TraitCapturer for Capturer { + fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result> { + match self.inner.frame(timeout.as_millis() as _) { + Ok(frame) => Ok(frame), + Err(ref error) if error.kind() == TimedOut => Err(WouldBlock.into()), + Err(error) => Err(error), + } + } + + fn is_gdi(&self) -> bool { + self.inner.is_gdi() + } + + fn set_gdi(&mut self) -> bool { + self.inner.set_gdi() + } + + #[cfg(feature = "vram")] + fn device(&self) -> AdapterDevice { + self.inner.device() + } + + #[cfg(feature = "vram")] + fn set_output_texture(&mut self, texture: bool) { + self.inner.set_output_texture(texture); + } +} + +pub struct PixelBuffer<'a> { + data: &'a [u8], + pixfmt: Pixfmt, + width: usize, + height: usize, + stride: Vec, +} + +impl<'a> PixelBuffer<'a> { + pub fn new(data: &'a [u8], pixfmt: Pixfmt, width: usize, height: usize) -> Self { + let stride0 = data.len() / height; + let mut stride = Vec::new(); + stride.push(stride0); + PixelBuffer { + data, + pixfmt, + width, + height, + stride, + } + } + + #[allow(non_snake_case)] + pub fn with_BGRA(data: &'a [u8], width: usize, height: usize) -> Self { + Self::new(data, Pixfmt::BGRA, width, height) + } +} + +impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { + fn data(&self) -> &[u8] { + self.data + } + + fn width(&self) -> usize { + self.width + } + + fn height(&self) -> usize { + self.height + } + + fn stride(&self) -> Vec { + self.stride.clone() + } + + fn pixfmt(&self) -> Pixfmt { + self.pixfmt + } +} + +pub struct Display(dxgi::Display); + +impl Display { + pub fn primary() -> io::Result { + // not implemented yet + Err(NotFound.into()) + } + + pub fn all() -> io::Result> { + let displays_gdi = dxgi::Displays::get_from_gdi() + .drain(..) + .map(Display) + .collect::>(); + + let displays_dxgi = Self::all_().unwrap_or(Default::default()); + + // Return gdi displays if dxgi is not supported + if displays_dxgi.is_empty() { + println!("Display got from gdi"); + return Ok(displays_gdi); + } + + // Return dxgi displays if length is not equal + if displays_dxgi.len() != displays_gdi.len() { + return Ok(displays_dxgi); + } + + // Check if names are equal + let names_gdi = displays_gdi.iter().map(|d| d.name()).collect::>(); + let names_dxgi = displays_dxgi.iter().map(|d| d.name()).collect::>(); + for name in names_gdi.iter() { + if !names_dxgi.contains(name) { + return Ok(displays_dxgi); + } + } + + // Reorder displays from dxgi + let mut displays_dxgi = displays_dxgi; + let mut displays_dxgi_ordered = Vec::new(); + for name in names_gdi.iter() { + let pos = match displays_dxgi.iter().position(|d| d.name() == *name) { + Some(pos) => pos, + None => { + // unreachable! + 0 + } + }; + displays_dxgi_ordered.push(displays_dxgi.remove(pos)); + } + + Ok(displays_dxgi_ordered) + } + + fn all_() -> io::Result> { + Ok(dxgi::Displays::new()?.map(Display).collect::>()) + } + + pub fn width(&self) -> usize { + self.0.width() as usize + } + + pub fn height(&self) -> usize { + self.0.height() as usize + } + + pub fn name(&self) -> String { + use std::ffi::OsString; + use std::os::windows::prelude::*; + OsString::from_wide(self.0.name()) + .to_string_lossy() + .to_string() + } + + pub fn is_online(&self) -> bool { + self.0.is_online() + } + + pub fn origin(&self) -> (i32, i32) { + self.0.origin() + } + + pub fn is_primary(&self) -> bool { + // https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-devmodea + self.origin() == (0, 0) + } + + #[cfg(feature = "vram")] + pub fn adapter_luid(&self) -> Option { + self.0.adapter_luid() + } +} + +pub struct CapturerMag { + inner: dxgi::mag::CapturerMag, + data: Vec, +} + +impl CapturerMag { + pub fn is_supported() -> bool { + dxgi::mag::CapturerMag::is_supported() + } + + pub fn new(origin: (i32, i32), width: usize, height: usize) -> io::Result { + Ok(CapturerMag { + inner: dxgi::mag::CapturerMag::new(origin, width, height)?, + data: Vec::new(), + }) + } + + pub fn exclude(&mut self, cls: &str, name: &str) -> io::Result { + self.inner.exclude(cls, name) + } + // ((x, y), w, h) + pub fn get_rect(&self) -> ((i32, i32), usize, usize) { + self.inner.get_rect() + } +} + +impl TraitCapturer for CapturerMag { + fn frame<'a>(&'a mut self, _timeout_ms: Duration) -> io::Result> { + self.inner.frame(&mut self.data)?; + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( + &self.data, + self.inner.get_rect().1, + self.inner.get_rect().2, + ))) + } + + fn is_gdi(&self) -> bool { + false + } + + fn set_gdi(&mut self) -> bool { + false + } + + #[cfg(feature = "vram")] + fn device(&self) -> AdapterDevice { + AdapterDevice::default() + } + + #[cfg(feature = "vram")] + fn set_output_texture(&mut self, _texture: bool) {} +} diff --git a/vendor/rustdesk/libs/scrap/src/common/hwcodec.rs b/vendor/rustdesk/libs/scrap/src/common/hwcodec.rs new file mode 100644 index 0000000..17eda7f --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/hwcodec.rs @@ -0,0 +1,763 @@ +use crate::{ + codec::{base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg}, + convert::*, + CodecFormat, EncodeInput, ImageFormat, ImageRgb, Pixfmt, HW_STRIDE_ALIGN, +}; +use hbb_common::{ + anyhow::{anyhow, bail, Context}, + bytes::Bytes, + log, + message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame}, + serde_derive::{Deserialize, Serialize}, + serde_json, ResultType, +}; +use hwcodec::{ + common::{ + DataFormat, HwcodecErrno, + Quality::{self, *}, + RateControl::{self, *}, + }, + ffmpeg::AVPixelFormat, + ffmpeg_ram::{ + decode::{DecodeContext, DecodeFrame, Decoder}, + encode::{EncodeContext, EncodeFrame, Encoder}, + ffmpeg_linesize_offset_length, CodecInfo, + }, +}; + +const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_NV12; +pub const DEFAULT_FPS: i32 = 30; +const DEFAULT_GOP: i32 = i32::MAX; +const DEFAULT_HW_QUALITY: Quality = Quality_Default; +pub const ERR_HEVC_POC: i32 = HwcodecErrno::HWCODEC_ERR_HEVC_COULD_NOT_FIND_POC as i32; + +crate::generate_call_macro!(call_yuv, false); + +#[cfg(not(target_os = "android"))] +lazy_static::lazy_static! { + static ref CONFIG: std::sync::Arc>> = Default::default(); + static ref CONFIG_SET_BY_IPC: std::sync::Arc> = Default::default(); +} + +#[derive(Debug, Clone)] +pub struct HwRamEncoderConfig { + pub name: String, + pub mc_name: Option, + pub width: usize, + pub height: usize, + pub quality: f32, + pub keyframe_interval: Option, +} + +pub struct HwRamEncoder { + encoder: Encoder, + pub format: DataFormat, + pub pixfmt: AVPixelFormat, + bitrate: u32, //kbs + config: HwRamEncoderConfig, +} + +impl EncoderApi for HwRamEncoder { + fn new(cfg: EncoderCfg, _i444: bool) -> ResultType + where + Self: Sized, + { + match cfg { + EncoderCfg::HWRAM(config) => { + let rc = Self::rate_control(&config); + let mut bitrate = + Self::bitrate(&config.name, config.width, config.height, config.quality); + bitrate = Self::check_bitrate_range(&config, bitrate); + let gop = config.keyframe_interval.unwrap_or(DEFAULT_GOP as _) as i32; + let ctx = EncodeContext { + name: config.name.clone(), + mc_name: config.mc_name.clone(), + width: config.width as _, + height: config.height as _, + pixfmt: DEFAULT_PIXFMT, + align: HW_STRIDE_ALIGN as _, + kbs: bitrate as i32, + fps: DEFAULT_FPS, + gop, + quality: DEFAULT_HW_QUALITY, + rc, + q: -1, + thread_count: codec_thread_num(16) as _, // ffmpeg's thread_count is used for cpu + }; + let format = match Encoder::format_from_name(config.name.clone()) { + Ok(format) => format, + Err(_) => { + return Err(anyhow!(format!( + "failed to get format from name:{}", + config.name + ))) + } + }; + match Encoder::new(ctx.clone()) { + Ok(encoder) => Ok(HwRamEncoder { + encoder, + format, + pixfmt: ctx.pixfmt, + bitrate, + config, + }), + Err(_) => Err(anyhow!(format!("Failed to create encoder"))), + } + } + _ => Err(anyhow!("encoder type mismatch")), + } + } + + fn encode_to_message(&mut self, input: EncodeInput, ms: i64) -> ResultType { + let mut vf = VideoFrame::new(); + let mut frames = Vec::new(); + for frame in self + .encode(input.yuv()?, ms) + .with_context(|| "Failed to encode")? + { + frames.push(EncodedVideoFrame { + data: Bytes::from(frame.data), + pts: frame.pts, + key: frame.key == 1, + ..Default::default() + }); + } + if frames.len() > 0 { + let frames = EncodedVideoFrames { + frames: frames.into(), + ..Default::default() + }; + match self.format { + DataFormat::H264 => vf.set_h264s(frames), + DataFormat::H265 => vf.set_h265s(frames), + _ => bail!("unsupported format: {:?}", self.format), + } + Ok(vf) + } else { + Err(anyhow!("no valid frame")) + } + } + + fn yuvfmt(&self) -> crate::EncodeYuvFormat { + let pixfmt = if self.pixfmt == AVPixelFormat::AV_PIX_FMT_NV12 { + Pixfmt::NV12 + } else { + Pixfmt::I420 + }; + let stride = self + .encoder + .linesize + .clone() + .drain(..) + .map(|i| i as usize) + .collect(); + crate::EncodeYuvFormat { + pixfmt, + w: self.encoder.ctx.width as _, + h: self.encoder.ctx.height as _, + stride, + u: self.encoder.offset[0] as _, + v: if pixfmt == Pixfmt::NV12 { + 0 + } else { + self.encoder.offset[1] as _ + }, + } + } + + #[cfg(feature = "vram")] + fn input_texture(&self) -> bool { + false + } + + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { + let mut bitrate = Self::bitrate( + &self.config.name, + self.config.width, + self.config.height, + ratio, + ); + if bitrate > 0 { + bitrate = Self::check_bitrate_range(&self.config, bitrate); + self.encoder.set_bitrate(bitrate as _).ok(); + self.bitrate = bitrate; + } + self.config.quality = ratio; + Ok(()) + } + + fn bitrate(&self) -> u32 { + self.bitrate + } + + fn support_changing_quality(&self) -> bool { + ["vaapi"].iter().all(|&x| !self.config.name.contains(x)) + } + + fn latency_free(&self) -> bool { + ["mediacodec", "videotoolbox"] + .iter() + .all(|&x| !self.config.name.contains(x)) + } + + fn is_hardware(&self) -> bool { + true + } + + fn disable(&self) { + HwCodecConfig::clear(false, true); + } +} + +impl HwRamEncoder { + pub fn try_get(format: CodecFormat) -> Option { + let mut info = None; + let best = CodecInfo::prioritized(HwCodecConfig::get().ram_encode); + match format { + CodecFormat::H264 => { + if let Some(v) = best.h264 { + info = Some(v); + } + } + CodecFormat::H265 => { + if let Some(v) = best.h265 { + info = Some(v); + } + } + _ => {} + } + info + } + + pub fn encode(&mut self, yuv: &[u8], ms: i64) -> ResultType> { + match self.encoder.encode(yuv, ms) { + Ok(v) => { + let mut data = Vec::::new(); + data.append(v); + Ok(data) + } + Err(_) => Ok(Vec::::new()), + } + } + + fn rate_control(_config: &HwRamEncoderConfig) -> RateControl { + #[cfg(target_os = "android")] + if _config.name.contains("mediacodec") { + return RC_VBR; + } + RC_CBR + } + + pub fn bitrate(name: &str, width: usize, height: usize, ratio: f32) -> u32 { + Self::calc_bitrate(width, height, ratio, name.contains("h264")) + } + + pub fn calc_bitrate(width: usize, height: usize, ratio: f32, h264: bool) -> u32 { + let base = base_bitrate(width as _, height as _) as f32 * ratio; + let threshold = 2000.0; + let decay_rate = 0.001; // 1000 * 0.001 = 1 + let factor: f32 = if cfg!(target_os = "android") { + // https://stackoverflow.com/questions/26110337/what-are-valid-bit-rates-to-set-for-mediacodec?rq=3 + if base > threshold { + 1.0 + 4.0 / (1.0 + (base - threshold) * decay_rate) + } else { + 5.0 + } + } else if h264 { + if base > threshold { + 1.0 + 1.0 / (1.0 + (base - threshold) * decay_rate) + } else { + 2.0 + } + } else { + if base > threshold { + 1.0 + 0.5 / (1.0 + (base - threshold) * decay_rate) + } else { + 1.5 + } + }; + (base * factor) as u32 + } + + pub fn check_bitrate_range(_config: &HwRamEncoderConfig, bitrate: u32) -> u32 { + #[cfg(target_os = "android")] + if _config.name.contains("mediacodec") { + let info = crate::android::ffi::get_codec_info(); + if let Some(info) = info { + if let Some(codec) = info + .codecs + .iter() + .find(|c| Some(c.name.clone()) == _config.mc_name && c.is_encoder) + { + if codec.max_bitrate > codec.min_bitrate { + if bitrate > codec.max_bitrate { + return codec.max_bitrate; + } + if bitrate < codec.min_bitrate { + return codec.min_bitrate; + } + } + } + } + } + bitrate + } +} + +pub struct HwRamDecoder { + decoder: Decoder, + pub info: CodecInfo, +} + +impl HwRamDecoder { + pub fn try_get(format: CodecFormat) -> Option { + let mut info = None; + let soft = CodecInfo::soft(); + match format { + CodecFormat::H264 => { + if let Some(v) = soft.h264 { + info = Some(v); + } + } + CodecFormat::H265 => { + if let Some(v) = soft.h265 { + info = Some(v); + } + } + _ => {} + } + if enable_hwcodec_option() { + let best = CodecInfo::prioritized(HwCodecConfig::get().ram_decode); + match format { + CodecFormat::H264 => { + if let Some(v) = best.h264 { + info = Some(v); + } + } + CodecFormat::H265 => { + if let Some(v) = best.h265 { + info = Some(v); + } + } + _ => {} + } + } + info + } + + pub fn new(format: CodecFormat) -> ResultType { + let info = HwRamDecoder::try_get(format); + log::info!("try create {info:?} ram decoder"); + let Some(info) = info else { + bail!("unsupported format: {:?}", format); + }; + let ctx = DecodeContext { + name: info.name.clone(), + device_type: info.hwdevice.clone(), + thread_count: codec_thread_num(16) as _, + }; + match Decoder::new(ctx) { + Ok(decoder) => Ok(HwRamDecoder { decoder, info }), + Err(_) => { + HwCodecConfig::clear(false, false); + Err(anyhow!(format!("Failed to create decoder"))) + } + } + } + pub fn decode<'a>(&'a mut self, data: &[u8]) -> ResultType>> { + match self.decoder.decode(data) { + Ok(v) => Ok(v.iter().map(|f| HwRamDecoderImage { frame: f }).collect()), + Err(e) => Err(anyhow!(e)), + } + } +} + +pub struct HwRamDecoderImage<'a> { + frame: &'a DecodeFrame, +} + +impl HwRamDecoderImage<'_> { + // rgb [in/out] fmt and stride must be set in ImageRgb + pub fn to_fmt(&self, rgb: &mut ImageRgb, i420: &mut Vec) -> ResultType<()> { + let frame = self.frame; + let width = frame.width; + let height = frame.height; + rgb.w = width as _; + rgb.h = height as _; + let dst_align = rgb.align(); + let bytes_per_row = (rgb.w * 4 + dst_align - 1) & !(dst_align - 1); + rgb.raw.resize(rgb.h * bytes_per_row, 0); + match frame.pixfmt { + AVPixelFormat::AV_PIX_FMT_NV12 => { + // I420ToARGB is much faster than NV12ToARGB in tests on Windows + if cfg!(windows) { + let Ok((linesize_i420, offset_i420, len_i420)) = ffmpeg_linesize_offset_length( + AVPixelFormat::AV_PIX_FMT_YUV420P, + width as _, + height as _, + HW_STRIDE_ALIGN, + ) else { + bail!("failed to get i420 linesize, offset, length"); + }; + i420.resize(len_i420 as _, 0); + let i420_offset_y = unsafe { i420.as_ptr().add(0) as _ }; + let i420_offset_u = unsafe { i420.as_ptr().add(offset_i420[0] as _) as _ }; + let i420_offset_v = unsafe { i420.as_ptr().add(offset_i420[1] as _) as _ }; + call_yuv!(NV12ToI420( + frame.data[0].as_ptr(), + frame.linesize[0], + frame.data[1].as_ptr(), + frame.linesize[1], + i420_offset_y, + linesize_i420[0], + i420_offset_u, + linesize_i420[1], + i420_offset_v, + linesize_i420[2], + width, + height, + )); + let f = match rgb.fmt() { + ImageFormat::ARGB => I420ToARGB, + ImageFormat::ABGR => I420ToABGR, + _ => bail!("unsupported format: {:?} -> {:?}", frame.pixfmt, rgb.fmt()), + }; + call_yuv!(f( + i420_offset_y, + linesize_i420[0], + i420_offset_u, + linesize_i420[1], + i420_offset_v, + linesize_i420[2], + rgb.raw.as_mut_ptr(), + bytes_per_row as _, + width, + height, + )); + } else { + let f = match rgb.fmt() { + ImageFormat::ARGB => NV12ToARGB, + ImageFormat::ABGR => NV12ToABGR, + _ => bail!("unsupported format: {:?} -> {:?}", frame.pixfmt, rgb.fmt()), + }; + call_yuv!(f( + frame.data[0].as_ptr(), + frame.linesize[0], + frame.data[1].as_ptr(), + frame.linesize[1], + rgb.raw.as_mut_ptr(), + bytes_per_row as _, + width, + height, + )); + } + } + AVPixelFormat::AV_PIX_FMT_YUV420P => { + let f = match rgb.fmt() { + ImageFormat::ARGB => I420ToARGB, + ImageFormat::ABGR => I420ToABGR, + _ => bail!("unsupported format: {:?} -> {:?}", frame.pixfmt, rgb.fmt()), + }; + call_yuv!(f( + frame.data[0].as_ptr(), + frame.linesize[0], + frame.data[1].as_ptr(), + frame.linesize[1], + frame.data[2].as_ptr(), + frame.linesize[2], + rgb.raw.as_mut_ptr(), + bytes_per_row as _, + width, + height, + )); + } + } + Ok(()) + } +} + +#[cfg(target_os = "android")] +fn get_mime_type(codec: DataFormat) -> &'static str { + match codec { + DataFormat::VP8 => "video/x-vnd.on2.vp8", + DataFormat::VP9 => "video/x-vnd.on2.vp9", + DataFormat::AV1 => "video/av01", + DataFormat::H264 => "video/avc", + DataFormat::H265 => "video/hevc", + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct HwCodecConfig { + #[serde(default)] + pub signature: u64, + #[serde(default)] + pub ram_encode: Vec, + #[serde(default)] + pub ram_decode: Vec, + #[cfg(feature = "vram")] + #[serde(default)] + pub vram_encode: Vec, + #[cfg(feature = "vram")] + #[serde(default)] + pub vram_decode: Vec, +} + +// HwCodecConfig2 is used to store the config in json format, +// confy can't serde HwCodecConfig successfully if the non-first struct Vec is empty due to old toml version. +// struct T { a: Vec, b: Vec} will fail if b is empty, but struct T { a: Vec, b: Vec} is ok. +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +struct HwCodecConfig2 { + #[serde(default)] + pub config: String, +} + +// ipc server process start check process once, other process get from ipc server once +// install: --server start check process, check process send to --server, ui get from --server +// portable: ui start check process, check process send to ui +// sciter and unilink: get from ipc server +impl HwCodecConfig { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn set(config: String) { + let config = serde_json::from_str(&config).unwrap_or_default(); + log::info!("set hwcodec config"); + log::debug!("{config:?}"); + #[cfg(any(windows, target_os = "macos"))] + hbb_common::config::common_store( + &HwCodecConfig2 { + config: serde_json::to_string_pretty(&config).unwrap_or_default(), + }, + "_hwcodec", + ); + *CONFIG.lock().unwrap() = Some(config); + *CONFIG_SET_BY_IPC.lock().unwrap() = true; + } + + pub fn get() -> HwCodecConfig { + #[cfg(target_os = "android")] + { + let info = crate::android::ffi::get_codec_info(); + log::info!("all codec info: {info:?}"); + struct T { + name_prefix: &'static str, + data_format: DataFormat, + } + let ts = vec![ + T { + name_prefix: "h264", + data_format: DataFormat::H264, + }, + T { + name_prefix: "hevc", + data_format: DataFormat::H265, + }, + ]; + let mut e = vec![]; + if let Some(info) = info { + ts.iter().for_each(|t| { + let codecs: Vec<_> = info + .codecs + .iter() + .filter(|c| { + c.is_encoder + && c.mime_type.as_str() == get_mime_type(t.data_format) + && c.nv12 + && c.hw == Some(true) //only use hardware codec + }) + .collect(); + let screen_wh = std::cmp::max(info.w, info.h); + let mut best = None; + if let Some(codec) = codecs + .iter() + .find(|c| c.max_width >= screen_wh && c.max_height >= screen_wh) + { + best = Some(codec.name.clone()); + } else { + // find the max resolution + let mut max_area = 0; + for codec in codecs.iter() { + if codec.max_width * codec.max_height > max_area { + best = Some(codec.name.clone()); + max_area = codec.max_width * codec.max_height; + } + } + } + if let Some(best) = best { + e.push(CodecInfo { + name: format!("{}_mediacodec", t.name_prefix), + mc_name: Some(best), + format: t.data_format, + hwdevice: hwcodec::ffmpeg::AVHWDeviceType::AV_HWDEVICE_TYPE_NONE, + priority: 0, + }); + } + }); + } + log::debug!("e: {e:?}"); + HwCodecConfig { + ram_encode: e, + ..Default::default() + } + } + #[cfg(any(windows, target_os = "macos"))] + { + let config = CONFIG.lock().unwrap().clone(); + match config { + Some(c) => c, + None => { + log::info!("try load cached hwcodec config"); + let c = hbb_common::config::common_load::("_hwcodec"); + let c: HwCodecConfig = serde_json::from_str(&c.config).unwrap_or_default(); + let new_signature = hwcodec::common::get_gpu_signature(); + if c.signature == new_signature { + log::debug!("load cached hwcodec config: {c:?}"); + *CONFIG.lock().unwrap() = Some(c.clone()); + c + } else { + log::info!( + "gpu signature changed, {} -> {}", + c.signature, + new_signature + ); + HwCodecConfig::default() + } + } + } + } + #[cfg(target_os = "linux")] + { + CONFIG.lock().unwrap().clone().unwrap_or_default() + } + #[cfg(target_os = "ios")] + { + HwCodecConfig::default() + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn get_set_value() -> Option { + let set = CONFIG_SET_BY_IPC.lock().unwrap().clone(); + if set { + CONFIG.lock().unwrap().clone() + } else { + None + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn already_set() -> bool { + CONFIG_SET_BY_IPC.lock().unwrap().clone() + } + + pub fn clear(vram: bool, encode: bool) { + log::info!("clear hwcodec config, vram: {vram}, encode: {encode}"); + #[cfg(target_os = "android")] + crate::android::ffi::clear_codec_info(); + #[cfg(not(target_os = "android"))] + { + let mut c = CONFIG.lock().unwrap(); + if let Some(c) = c.as_mut() { + if vram { + #[cfg(feature = "vram")] + if encode { + c.vram_encode = vec![]; + } else { + c.vram_decode = vec![]; + } + } else { + if encode { + c.ram_encode = vec![]; + } else { + c.ram_decode = vec![]; + } + } + } + } + crate::codec::Encoder::update(crate::codec::EncodingUpdate::Check); + } +} + +pub fn check_available_hwcodec() -> String { + #[cfg(any(target_os = "linux", target_os = "macos"))] + hwcodec::common::setup_parent_death_signal(); + let ctx = EncodeContext { + name: String::from(""), + mc_name: None, + width: 1280, + height: 720, + pixfmt: DEFAULT_PIXFMT, + align: HW_STRIDE_ALIGN as _, + kbs: 1000, + fps: DEFAULT_FPS, + gop: DEFAULT_GOP, + quality: DEFAULT_HW_QUALITY, + rc: RC_CBR, + q: -1, + thread_count: 4, + }; + #[cfg(feature = "vram")] + let vram = crate::vram::check_available_vram(); + #[cfg(feature = "vram")] + let vram_string = vram.2; + #[cfg(not(feature = "vram"))] + let vram_string = "".to_owned(); + let c = HwCodecConfig { + ram_encode: Encoder::available_encoders(ctx, Some(vram_string)), + ram_decode: Decoder::available_decoders(), + #[cfg(feature = "vram")] + vram_encode: vram.0, + #[cfg(feature = "vram")] + vram_decode: vram.1, + signature: hwcodec::common::get_gpu_signature(), + }; + log::debug!("{c:?}"); + serde_json::to_string(&c).unwrap_or_default() +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn start_check_process() { + if !enable_hwcodec_option() || HwCodecConfig::already_set() { + return; + } + use hbb_common::allow_err; + use std::sync::Once; + let f = || { + if let Ok(exe) = std::env::current_exe() { + if let Some(_) = exe.file_name().to_owned() { + let arg = "--check-hwcodec-config"; + if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() { + #[cfg(windows)] + hwcodec::common::child_exit_when_parent_exit(child.id()); + // wait up to 30 seconds, it maybe slow on windows startup for poorly performing machines + for _ in 0..30 { + std::thread::sleep(std::time::Duration::from_secs(1)); + if let Ok(Some(_)) = child.try_wait() { + break; + } + } + allow_err!(child.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + match child.try_wait() { + Ok(Some(status)) => { + log::info!("Check hwcodec config, exit with: {status}") + } + Ok(None) => { + log::info!( + "Check hwcodec config, status not ready yet, let's really wait" + ); + let res = child.wait(); + log::info!("Check hwcodec config, wait result: {res:?}"); + } + Err(e) => { + log::error!("Check hwcodec config, error attempting to wait: {e}") + } + } + } + } + }; + }; + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + std::thread::spawn(f); + }); +} diff --git a/vendor/rustdesk/libs/scrap/src/common/linux.rs b/vendor/rustdesk/libs/scrap/src/common/linux.rs new file mode 100644 index 0000000..ba5e8e7 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/linux.rs @@ -0,0 +1,139 @@ +use crate::{ + common::{ + wayland, + x11::{self}, + TraitCapturer, + }, + Frame, +}; +use std::{io, time::Duration}; + +pub enum Capturer { + X11(x11::Capturer), + WAYLAND(wayland::Capturer), +} + +impl Capturer { + pub fn new(display: Display) -> io::Result { + Ok(match display { + Display::X11(d) => Capturer::X11(x11::Capturer::new(d)?), + Display::WAYLAND(d) => Capturer::WAYLAND(wayland::Capturer::new(d)?), + }) + } + + pub fn width(&self) -> usize { + match self { + Capturer::X11(d) => d.width(), + Capturer::WAYLAND(d) => d.width(), + } + } + + pub fn height(&self) -> usize { + match self { + Capturer::X11(d) => d.height(), + Capturer::WAYLAND(d) => d.height(), + } + } +} + +impl TraitCapturer for Capturer { + fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result> { + match self { + Capturer::X11(d) => d.frame(timeout), + Capturer::WAYLAND(d) => d.frame(timeout), + } + } +} + +pub enum Display { + X11(x11::Display), + WAYLAND(wayland::Display), +} + +impl Display { + pub fn primary() -> io::Result { + Ok(if super::is_x11() { + Display::X11(x11::Display::primary()?) + } else { + Display::WAYLAND(wayland::Display::primary()?) + }) + } + + // Currently, wayland need to call wayland::clear() before call Display::all() + pub fn all() -> io::Result> { + Ok(if super::is_x11() { + x11::Display::all()? + .drain(..) + .map(|x| Display::X11(x)) + .collect() + } else { + wayland::Display::all()? + .drain(..) + .map(|x| Display::WAYLAND(x)) + .collect() + }) + } + + pub fn width(&self) -> usize { + match self { + Display::X11(d) => d.width(), + Display::WAYLAND(d) => d.width(), + } + } + + pub fn height(&self) -> usize { + match self { + Display::X11(d) => d.height(), + Display::WAYLAND(d) => d.height(), + } + } + + pub fn scale(&self) -> f64 { + match self { + Display::X11(_d) => 1.0, + Display::WAYLAND(d) => d.scale(), + } + } + + pub fn logical_width(&self) -> usize { + match self { + Display::X11(d) => d.width(), + Display::WAYLAND(d) => d.logical_width(), + } + } + + pub fn logical_height(&self) -> usize { + match self { + Display::X11(d) => d.height(), + Display::WAYLAND(d) => d.logical_height(), + } + } + + pub fn origin(&self) -> (i32, i32) { + match self { + Display::X11(d) => d.origin(), + Display::WAYLAND(d) => d.origin(), + } + } + + pub fn is_online(&self) -> bool { + match self { + Display::X11(d) => d.is_online(), + Display::WAYLAND(d) => d.is_online(), + } + } + + pub fn is_primary(&self) -> bool { + match self { + Display::X11(d) => d.is_primary(), + Display::WAYLAND(d) => d.is_primary(), + } + } + + pub fn name(&self) -> String { + match self { + Display::X11(d) => d.name(), + Display::WAYLAND(d) => d.name(), + } + } +} diff --git a/vendor/rustdesk/libs/scrap/src/common/mediacodec.rs b/vendor/rustdesk/libs/scrap/src/common/mediacodec.rs new file mode 100644 index 0000000..8ec5e6b --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/mediacodec.rs @@ -0,0 +1,171 @@ +use hbb_common::{anyhow::Error, bail, log, ResultType}; +use ndk::media::media_codec::{MediaCodec, MediaCodecDirection, MediaFormat}; +use std::ops::Deref; +use std::{ + io::Write, + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; + +use crate::ImageFormat; +use crate::{ + codec::{EncoderApi, EncoderCfg}, + CodecFormat, I420ToABGR, I420ToARGB, ImageRgb, +}; + +/// MediaCodec mime type name +const H264_MIME_TYPE: &str = "video/avc"; +const H265_MIME_TYPE: &str = "video/hevc"; +// const VP8_MIME_TYPE: &str = "video/x-vnd.on2.vp8"; +// const VP9_MIME_TYPE: &str = "video/x-vnd.on2.vp9"; + +// TODO MediaCodecEncoder + +pub static H264_DECODER_SUPPORT: AtomicBool = AtomicBool::new(false); +pub static H265_DECODER_SUPPORT: AtomicBool = AtomicBool::new(false); + +pub struct MediaCodecDecoder { + decoder: MediaCodec, + name: String, +} + +impl Deref for MediaCodecDecoder { + type Target = MediaCodec; + + fn deref(&self) -> &Self::Target { + &self.decoder + } +} + +impl MediaCodecDecoder { + pub fn new(format: CodecFormat) -> Option { + match format { + CodecFormat::H264 => create_media_codec(H264_MIME_TYPE, MediaCodecDirection::Decoder), + CodecFormat::H265 => create_media_codec(H265_MIME_TYPE, MediaCodecDirection::Decoder), + _ => { + log::error!("Unsupported codec format: {}", format); + None + } + } + } + + // rgb [in/out] fmt and stride must be set in ImageRgb + pub fn decode(&mut self, data: &[u8], rgb: &mut ImageRgb) -> ResultType { + // take dst_stride into account please + let dst_stride = rgb.stride(); + match self.dequeue_input_buffer(Duration::from_millis(10))? { + Some(mut input_buffer) => { + let mut buf = input_buffer.buffer_mut(); + if data.len() > buf.len() { + log::error!("Failed to decode, the input data size is bigger than input buf"); + bail!("The input data size is bigger than input buf"); + } + buf.write_all(&data)?; + self.queue_input_buffer(input_buffer, 0, data.len(), 0, 0)?; + } + None => { + log::debug!("Failed to dequeue_input_buffer: No available input_buffer"); + } + }; + + return match self.dequeue_output_buffer(Duration::from_millis(100))? { + Some(output_buffer) => { + let res_format = self.output_format(); + let w = res_format + .i32("width") + .ok_or(Error::msg("Failed to dequeue_output_buffer, width is None"))? + as usize; + let h = res_format.i32("height").ok_or(Error::msg( + "Failed to dequeue_output_buffer, height is None", + ))? as usize; + let stride = res_format.i32("stride").ok_or(Error::msg( + "Failed to dequeue_output_buffer, stride is None", + ))?; + let buf = output_buffer.buffer(); + let bps = 4; + let u = buf.len() * 2 / 3; + let v = buf.len() * 5 / 6; + rgb.raw.resize(h * w * bps, 0); + let y_ptr = buf.as_ptr(); + let u_ptr = buf[u..].as_ptr(); + let v_ptr = buf[v..].as_ptr(); + unsafe { + match rgb.fmt() { + ImageFormat::ARGB => { + I420ToARGB( + y_ptr, + stride, + u_ptr, + stride / 2, + v_ptr, + stride / 2, + rgb.raw.as_mut_ptr(), + (w * bps) as _, + w as _, + h as _, + ); + } + ImageFormat::ARGB => { + I420ToABGR( + y_ptr, + stride, + u_ptr, + stride / 2, + v_ptr, + stride / 2, + rgb.raw.as_mut_ptr(), + (w * bps) as _, + w as _, + h as _, + ); + } + _ => { + bail!("Unsupported image format"); + } + } + } + self.release_output_buffer(output_buffer, false)?; + Ok(true) + } + None => { + log::debug!("Failed to dequeue_output: No available dequeue_output"); + Ok(false) + } + }; + } +} + +fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option { + let codec = MediaCodec::from_decoder_type(name)?; + let media_format = MediaFormat::new(); + media_format.set_str("mime", name); + media_format.set_i32("width", 0); + media_format.set_i32("height", 0); + media_format.set_i32("color-format", 19); // COLOR_FormatYUV420Planar + if let Err(e) = codec.configure(&media_format, None, direction) { + log::error!("Failed to init decoder: {:?}", e); + return None; + }; + log::error!("decoder init success"); + if let Err(e) = codec.start() { + log::error!("Failed to start decoder: {:?}", e); + return None; + }; + log::debug!("Init decoder succeeded!: {:?}", name); + return Some(MediaCodecDecoder { + decoder: codec, + name: name.to_owned(), + }); +} + +pub fn check_mediacodec() { + std::thread::spawn(move || { + // check decoders + let decoders = MediaCodecDecoder::new_decoders(); + H264_DECODER_SUPPORT.swap(decoders.h264.is_some(), Ordering::SeqCst); + H265_DECODER_SUPPORT.swap(decoders.h265.is_some(), Ordering::SeqCst); + decoders.h264.map(|d| d.stop()); + decoders.h265.map(|d| d.stop()); + // TODO encoders + }); +} diff --git a/vendor/rustdesk/libs/scrap/src/common/mod.rs b/vendor/rustdesk/libs/scrap/src/common/mod.rs new file mode 100644 index 0000000..2d74caa --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/mod.rs @@ -0,0 +1,547 @@ +pub use self::vpxcodec::*; +use hbb_common::{ + bail, log, + message_proto::{video_frame, Chroma, VideoFrame}, + ResultType, +}; +use std::{ffi::c_void, slice}; + +cfg_if! { + if #[cfg(quartz)] { + mod quartz; + pub use self::quartz::*; + } else if #[cfg(x11)] { + cfg_if! { + if #[cfg(feature="wayland")] { + mod linux; + mod wayland; + mod x11; + pub use self::linux::*; + pub use self::wayland::set_map_err; + pub use self::x11::PixelBuffer; + } else { + mod x11; + pub use self::x11::*; + } + } + } else if #[cfg(dxgi)] { + mod dxgi; + pub use self::dxgi::*; + } else if #[cfg(target_os = "android")] { + mod android; + pub use self::android::*; + }else { + //TODO: Fallback implementation. + } +} + +pub mod codec; +pub mod convert; +#[cfg(feature = "hwcodec")] +pub mod hwcodec; +#[cfg(feature = "mediacodec")] +pub mod mediacodec; +pub mod vpxcodec; +#[cfg(feature = "vram")] +pub mod vram; +pub use self::convert::*; +pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller +pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer + +pub mod aom; +#[cfg(not(any(target_os = "ios")))] +pub mod camera; +pub mod record; +mod vpx; + +#[repr(usize)] +#[derive(Debug, Copy, Clone)] +pub enum ImageFormat { + Raw, + ABGR, + ARGB, +} + +#[repr(C)] +#[derive(Clone)] +pub struct ImageRgb { + pub raw: Vec, + pub w: usize, + pub h: usize, + pub fmt: ImageFormat, + pub align: usize, +} + +impl ImageRgb { + pub fn new(fmt: ImageFormat, align: usize) -> Self { + Self { + raw: Vec::new(), + w: 0, + h: 0, + fmt, + align, + } + } + + #[inline] + pub fn fmt(&self) -> ImageFormat { + self.fmt + } + + #[inline] + pub fn align(&self) -> usize { + self.align + } + + #[inline] + pub fn set_align(&mut self, align: usize) { + self.align = align; + } +} + +pub struct ImageTexture { + pub texture: *mut c_void, + pub w: usize, + pub h: usize, +} + +impl Default for ImageTexture { + fn default() -> Self { + Self { + texture: std::ptr::null_mut(), + w: 0, + h: 0, + } + } +} + +#[inline] +pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { + // does this really help? + if b == &old[..] { + return Err(std::io::ErrorKind::WouldBlock.into()); + } + old.resize(b.len(), 0); + old.copy_from_slice(b); + Ok(()) +} + +pub trait TraitCapturer { + // We doesn't support + #[cfg(not(any(target_os = "ios")))] + fn frame<'a>(&'a mut self, timeout: std::time::Duration) -> std::io::Result>; + + #[cfg(windows)] + fn is_gdi(&self) -> bool; + #[cfg(windows)] + fn set_gdi(&mut self) -> bool; + + #[cfg(feature = "vram")] + fn device(&self) -> AdapterDevice; + + #[cfg(feature = "vram")] + fn set_output_texture(&mut self, texture: bool); +} + +#[derive(Debug, Clone, Copy)] +pub struct AdapterDevice { + pub device: *mut c_void, + pub vendor_id: ::std::os::raw::c_uint, + pub luid: i64, +} + +impl Default for AdapterDevice { + fn default() -> Self { + Self { + device: std::ptr::null_mut(), + vendor_id: Default::default(), + luid: Default::default(), + } + } +} + +pub trait TraitPixelBuffer { + fn data(&self) -> &[u8]; + + fn width(&self) -> usize; + + fn height(&self) -> usize; + + fn stride(&self) -> Vec; + + fn pixfmt(&self) -> Pixfmt; +} + +#[cfg(not(any(target_os = "ios")))] +pub enum Frame<'a> { + PixelBuffer(PixelBuffer<'a>), + Texture((*mut c_void, usize)), +} + +#[cfg(not(any(target_os = "ios")))] +impl Frame<'_> { + pub fn valid<'a>(&'a self) -> bool { + match self { + Frame::PixelBuffer(pixelbuffer) => !pixelbuffer.data().is_empty(), + Frame::Texture((texture, _)) => !texture.is_null(), + } + } + + pub fn to<'a>( + &'a self, + yuvfmt: EncodeYuvFormat, + yuv: &'a mut Vec, + mid_data: &mut Vec, + ) -> ResultType> { + match self { + Frame::PixelBuffer(pixelbuffer) => { + convert_to_yuv(&pixelbuffer, yuvfmt, yuv, mid_data)?; + Ok(EncodeInput::YUV(yuv)) + } + Frame::Texture(texture) => Ok(EncodeInput::Texture(*texture)), + } + } +} + +pub enum EncodeInput<'a> { + YUV(&'a [u8]), + Texture((*mut c_void, usize)), +} + +impl<'a> EncodeInput<'a> { + pub fn yuv(&self) -> ResultType<&'_ [u8]> { + match self { + Self::YUV(f) => Ok(f), + _ => bail!("not pixelfbuffer frame"), + } + } + + pub fn texture(&self) -> ResultType<(*mut c_void, usize)> { + match self { + Self::Texture(f) => Ok(*f), + _ => bail!("not texture frame"), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Pixfmt { + BGRA, + RGBA, + RGB565LE, + I420, + NV12, + I444, +} + +impl Pixfmt { + pub fn bpp(&self) -> usize { + match self { + Pixfmt::BGRA | Pixfmt::RGBA => 32, + Pixfmt::RGB565LE => 16, + Pixfmt::I420 | Pixfmt::NV12 => 12, + Pixfmt::I444 => 24, + } + } + + pub fn bytes_per_pixel(&self) -> usize { + (self.bpp() + 7) / 8 + } +} + +#[derive(Debug, Clone)] +pub struct EncodeYuvFormat { + pub pixfmt: Pixfmt, + pub w: usize, + pub h: usize, + pub stride: Vec, + pub u: usize, + pub v: usize, +} + +#[cfg(x11)] +#[inline] +pub fn is_x11() -> bool { + hbb_common::platform::linux::is_x11_or_headless() +} + +#[cfg(x11)] +#[inline] +pub fn is_cursor_embedded() -> bool { + if is_x11() { + x11::IS_CURSOR_EMBEDDED + } else { + false + } +} + +#[cfg(not(x11))] +#[inline] +pub fn is_cursor_embedded() -> bool { + false +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CodecName { + VP8, + VP9, + AV1, + H264RAM(String), + H265RAM(String), + H264VRAM, + H265VRAM, +} + +#[derive(PartialEq, Debug, Clone, Copy)] +pub enum CodecFormat { + VP8, + VP9, + AV1, + H264, + H265, + Unknown, +} + +impl From<&VideoFrame> for CodecFormat { + fn from(it: &VideoFrame) -> Self { + match it.union { + Some(video_frame::Union::Vp8s(_)) => CodecFormat::VP8, + Some(video_frame::Union::Vp9s(_)) => CodecFormat::VP9, + Some(video_frame::Union::Av1s(_)) => CodecFormat::AV1, + Some(video_frame::Union::H264s(_)) => CodecFormat::H264, + Some(video_frame::Union::H265s(_)) => CodecFormat::H265, + _ => CodecFormat::Unknown, + } + } +} + +impl From<&video_frame::Union> for CodecFormat { + fn from(it: &video_frame::Union) -> Self { + match it { + video_frame::Union::Vp8s(_) => CodecFormat::VP8, + video_frame::Union::Vp9s(_) => CodecFormat::VP9, + video_frame::Union::Av1s(_) => CodecFormat::AV1, + video_frame::Union::H264s(_) => CodecFormat::H264, + video_frame::Union::H265s(_) => CodecFormat::H265, + _ => CodecFormat::Unknown, + } + } +} + +impl From<&CodecName> for CodecFormat { + fn from(value: &CodecName) -> Self { + match value { + CodecName::VP8 => Self::VP8, + CodecName::VP9 => Self::VP9, + CodecName::AV1 => Self::AV1, + CodecName::H264RAM(_) | CodecName::H264VRAM => Self::H264, + CodecName::H265RAM(_) | CodecName::H265VRAM => Self::H265, + } + } +} + +impl ToString for CodecFormat { + fn to_string(&self) -> String { + match self { + CodecFormat::VP8 => "VP8".into(), + CodecFormat::VP9 => "VP9".into(), + CodecFormat::AV1 => "AV1".into(), + CodecFormat::H264 => "H264".into(), + CodecFormat::H265 => "H265".into(), + CodecFormat::Unknown => "Unknown".into(), + } + } +} + +#[derive(Debug)] +pub enum Error { + FailedCall(String), + BadPtr(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for Error {} + +pub type Result = std::result::Result; + +#[macro_export] +macro_rules! generate_call_macro { + ($func_name:ident, $allow_err:expr) => { + macro_rules! $func_name { + ($x:expr) => {{ + let result = unsafe { $x }; + let result_int = unsafe { std::mem::transmute::<_, i32>(result) }; + if result_int != 0 { + let message = format!( + "errcode={} {}:{}:{}:{}", + result_int, + module_path!(), + file!(), + line!(), + column!() + ); + if $allow_err { + log::warn!("Failed to call {}, {}", stringify!($func_name), message); + } else { + return Err(crate::Error::FailedCall(message).into()); + } + } + result + }}; + } + }; +} + +#[macro_export] +macro_rules! generate_call_ptr_macro { + ($func_name:ident) => { + macro_rules! $func_name { + ($x:expr) => {{ + let result = unsafe { $x }; + let result_int = unsafe { std::mem::transmute::<_, isize>(result) }; + if result_int == 0 { + return Err(crate::Error::BadPtr(format!( + "errcode={} {}:{}:{}:{}", + result_int, + module_path!(), + file!(), + line!(), + column!() + )) + .into()); + } + result + }}; + } + }; +} + +pub trait GoogleImage { + fn width(&self) -> usize; + fn height(&self) -> usize; + fn stride(&self) -> Vec; + fn planes(&self) -> Vec<*mut u8>; + fn chroma(&self) -> Chroma; + fn get_bytes_per_row(w: usize, fmt: ImageFormat, align: usize) -> usize { + let bytes_per_pixel = match fmt { + ImageFormat::Raw => 3, + ImageFormat::ARGB | ImageFormat::ABGR => 4, + }; + // https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L128 + // https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L129 + (w * bytes_per_pixel + align - 1) & !(align - 1) + } + // rgb [in/out] fmt and stride must be set in ImageRgb + fn to(&self, rgb: &mut ImageRgb) { + rgb.w = self.width(); + rgb.h = self.height(); + let bytes_per_row = Self::get_bytes_per_row(rgb.w, rgb.fmt, rgb.align()); + rgb.raw.resize(rgb.h * bytes_per_row, 0); + let stride = self.stride(); + let planes = self.planes(); + unsafe { + match (self.chroma(), rgb.fmt()) { + (Chroma::I420, ImageFormat::Raw) => { + super::I420ToRAW( + planes[0], + stride[0], + planes[1], + stride[1], + planes[2], + stride[2], + rgb.raw.as_mut_ptr(), + bytes_per_row as _, + self.width() as _, + self.height() as _, + ); + } + (Chroma::I420, ImageFormat::ARGB) => { + super::I420ToARGB( + planes[0], + stride[0], + planes[1], + stride[1], + planes[2], + stride[2], + rgb.raw.as_mut_ptr(), + bytes_per_row as _, + self.width() as _, + self.height() as _, + ); + } + (Chroma::I420, ImageFormat::ABGR) => { + super::I420ToABGR( + planes[0], + stride[0], + planes[1], + stride[1], + planes[2], + stride[2], + rgb.raw.as_mut_ptr(), + bytes_per_row as _, + self.width() as _, + self.height() as _, + ); + } + (Chroma::I444, ImageFormat::ARGB) => { + super::I444ToARGB( + planes[0], + stride[0], + planes[1], + stride[1], + planes[2], + stride[2], + rgb.raw.as_mut_ptr(), + bytes_per_row as _, + self.width() as _, + self.height() as _, + ); + } + (Chroma::I444, ImageFormat::ABGR) => { + super::I444ToABGR( + planes[0], + stride[0], + planes[1], + stride[1], + planes[2], + stride[2], + rgb.raw.as_mut_ptr(), + bytes_per_row as _, + self.width() as _, + self.height() as _, + ); + } + // (Chroma::I444, ImageFormat::Raw), new version libyuv have I444ToRAW + _ => log::error!("unsupported pixfmt: {:?}", self.chroma()), + } + } + } + fn data(&self) -> (&[u8], &[u8], &[u8]) { + unsafe { + let stride = self.stride(); + let planes = self.planes(); + let h = (self.height() as usize + 1) & !1; + let n = stride[0] as usize * h; + let y = slice::from_raw_parts(planes[0], n); + let n = stride[1] as usize * (h >> 1); + let u = slice::from_raw_parts(planes[1], n); + let v = slice::from_raw_parts(planes[2], n); + (y, u, v) + } + } +} + +#[cfg(target_os = "android")] +pub fn screen_size() -> (u16, u16, u16) { + SCREEN_SIZE.lock().unwrap().clone() +} + +#[cfg(target_os = "android")] +pub fn is_start() -> Option { + android::is_start() +} diff --git a/vendor/rustdesk/libs/scrap/src/common/quartz.rs b/vendor/rustdesk/libs/scrap/src/common/quartz.rs new file mode 100644 index 0000000..b6a63e8 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/quartz.rs @@ -0,0 +1,151 @@ +use crate::{quartz, Frame, Pixfmt}; +use std::marker::PhantomData; +use std::sync::{Arc, Mutex, TryLockError}; +use std::{io, mem}; + +pub struct Capturer { + inner: quartz::Capturer, + frame: Arc>>, + saved_raw_data: Vec, // for faster compare and copy +} + +impl Capturer { + pub fn new(display: Display) -> io::Result { + let frame = Arc::new(Mutex::new(None)); + + let f = frame.clone(); + let inner = quartz::Capturer::new( + display.0, + display.width(), + display.height(), + quartz::PixelFormat::Argb8888, + Default::default(), + move |inner| { + if let Ok(mut f) = f.lock() { + *f = Some(inner); + } + }, + ) + .map_err(|_| io::Error::from(io::ErrorKind::Other))?; + + Ok(Capturer { + inner, + frame, + saved_raw_data: Vec::new(), + }) + } + + pub fn width(&self) -> usize { + self.inner.width() + } + + pub fn height(&self) -> usize { + self.inner.height() + } +} + +impl crate::TraitCapturer for Capturer { + fn frame<'a>(&'a mut self, _timeout_ms: std::time::Duration) -> io::Result> { + match self.frame.try_lock() { + Ok(mut handle) => { + let mut frame = None; + mem::swap(&mut frame, &mut handle); + + match frame { + Some(mut frame) => { + crate::would_block_if_equal(&mut self.saved_raw_data, frame.inner())?; + frame.surface_to_bgra(self.height()); + Ok(Frame::PixelBuffer(PixelBuffer { + frame, + data: PhantomData, + width: self.width(), + height: self.height(), + })) + } + + None => Err(io::ErrorKind::WouldBlock.into()), + } + } + + Err(TryLockError::WouldBlock) => Err(io::ErrorKind::WouldBlock.into()), + + Err(TryLockError::Poisoned(..)) => Err(io::ErrorKind::Other.into()), + } + } +} + +pub struct PixelBuffer<'a> { + frame: quartz::Frame, + data: PhantomData<&'a [u8]>, + width: usize, + height: usize, +} + +impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { + fn data(&self) -> &[u8] { + &*self.frame + } + + fn width(&self) -> usize { + self.width + } + + fn height(&self) -> usize { + self.height + } + + fn stride(&self) -> Vec { + let mut v = Vec::new(); + v.push(self.frame.stride()); + v + } + + fn pixfmt(&self) -> Pixfmt { + Pixfmt::BGRA + } +} + +pub struct Display(quartz::Display); + +impl Display { + pub fn primary() -> io::Result { + Ok(Display(quartz::Display::primary())) + } + + pub fn all() -> io::Result> { + Ok(quartz::Display::online() + .map_err(|_| io::Error::from(io::ErrorKind::Other))? + .into_iter() + .map(Display) + .collect()) + } + + pub fn width(&self) -> usize { + self.0.width() + } + + pub fn height(&self) -> usize { + self.0.height() + } + + pub fn scale(&self) -> f64 { + self.0.scale() + } + + pub fn name(&self) -> String { + self.0.id().to_string() + } + + pub fn is_online(&self) -> bool { + self.0.is_online() + } + + pub fn origin(&self) -> (i32, i32) { + let o = self.0.bounds().origin; + (o.x as _, o.y as _) + } + + pub fn is_primary(&self) -> bool { + self.0.is_primary() + } +} diff --git a/vendor/rustdesk/libs/scrap/src/common/record.rs b/vendor/rustdesk/libs/scrap/src/common/record.rs new file mode 100644 index 0000000..d121984 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/record.rs @@ -0,0 +1,423 @@ +use crate::CodecFormat; +#[cfg(feature = "hwcodec")] +use hbb_common::anyhow::anyhow; +use hbb_common::{ + bail, chrono, log, + message_proto::{message, video_frame, EncodedVideoFrame, Message}, + ResultType, +}; +#[cfg(feature = "hwcodec")] +use hwcodec::mux::{MuxContext, Muxer}; +use std::{ + fs::{File, OpenOptions}, + io, + ops::{Deref, DerefMut}, + path::PathBuf, + sync::mpsc::Sender, + time::Instant, +}; +use webm::mux::{self, Segment, Track, VideoTrack, Writer}; + +const MIN_SECS: u64 = 1; + +#[derive(Debug, Clone)] +pub struct RecorderContext { + pub server: bool, + pub id: String, + pub dir: String, + pub display_idx: usize, + pub camera: bool, + pub tx: Option>, +} + +#[derive(Debug, Clone)] +pub struct RecorderContext2 { + pub filename: String, + pub width: usize, + pub height: usize, + pub format: CodecFormat, +} + +impl RecorderContext2 { + pub fn set_filename(&mut self, ctx: &RecorderContext) -> ResultType<()> { + if !PathBuf::from(&ctx.dir).exists() { + std::fs::create_dir_all(&ctx.dir)?; + } + let file = if ctx.server { "incoming" } else { "outgoing" }.to_string() + + "_" + + &ctx.id.clone() + + &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string() + + &format!( + "{}{}_", + if ctx.camera { "camera" } else { "display" }, + ctx.display_idx + ) + + &self.format.to_string().to_lowercase() + + if self.format == CodecFormat::VP9 + || self.format == CodecFormat::VP8 + || self.format == CodecFormat::AV1 + { + ".webm" + } else { + ".mp4" + }; + self.filename = PathBuf::from(&ctx.dir) + .join(file) + .to_string_lossy() + .to_string(); + Ok(()) + } +} + +unsafe impl Send for Recorder {} +unsafe impl Sync for Recorder {} + +pub trait RecorderApi { + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType + where + Self: Sized; + fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool; +} + +#[derive(Debug)] +pub enum RecordState { + NewFile(String), + NewFrame, + WriteTail, + RemoveFile, +} + +pub struct Recorder { + pub inner: Option>, + ctx: RecorderContext, + ctx2: Option, + pts: Option, + check_failed: bool, +} + +impl Deref for Recorder { + type Target = Option>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Recorder { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl Recorder { + pub fn new(ctx: RecorderContext) -> ResultType { + Ok(Self { + inner: None, + ctx, + ctx2: None, + pts: None, + check_failed: false, + }) + } + + fn check(&mut self, w: usize, h: usize, format: CodecFormat) -> ResultType<()> { + match self.ctx2 { + Some(ref ctx2) => { + if ctx2.width != w || ctx2.height != h || ctx2.format != format { + let mut ctx2 = RecorderContext2 { + width: w, + height: h, + format, + filename: Default::default(), + }; + ctx2.set_filename(&self.ctx)?; + self.ctx2 = Some(ctx2); + self.inner = None; + } + } + None => { + let mut ctx2 = RecorderContext2 { + width: w, + height: h, + format, + filename: Default::default(), + }; + ctx2.set_filename(&self.ctx)?; + self.ctx2 = Some(ctx2); + self.inner = None; + } + } + let Some(ctx2) = &self.ctx2 else { + bail!("ctx2 is None"); + }; + if self.inner.is_none() { + self.inner = match format { + CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Some(Box::new( + WebmRecorder::new(self.ctx.clone(), (*ctx2).clone())?, + )), + #[cfg(feature = "hwcodec")] + _ => Some(Box::new(HwRecorder::new( + self.ctx.clone(), + (*ctx2).clone(), + )?)), + #[cfg(not(feature = "hwcodec"))] + _ => bail!("unsupported codec type"), + }; + // pts is None when new inner is created + self.pts = None; + self.send_state(RecordState::NewFile(ctx2.filename.clone())); + } + Ok(()) + } + + pub fn write_message(&mut self, msg: &Message, w: usize, h: usize) { + if let Some(message::Union::VideoFrame(vf)) = &msg.union { + if let Some(frame) = &vf.union { + self.write_frame(frame, w, h).ok(); + } + } + } + + pub fn write_frame( + &mut self, + frame: &video_frame::Union, + w: usize, + h: usize, + ) -> ResultType<()> { + if self.check_failed { + bail!("check failed"); + } + let format = CodecFormat::from(frame); + if format == CodecFormat::Unknown { + bail!("unsupported frame type"); + } + let res = self.check(w, h, format); + if res.is_err() { + self.check_failed = true; + log::error!("check failed: {:?}", res); + res?; + } + match frame { + video_frame::Union::Vp8s(vp8s) => { + for f in vp8s.frames.iter() { + self.check_pts(f.pts, f.key, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); + } + } + video_frame::Union::Vp9s(vp9s) => { + for f in vp9s.frames.iter() { + self.check_pts(f.pts, f.key, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); + } + } + video_frame::Union::Av1s(av1s) => { + for f in av1s.frames.iter() { + self.check_pts(f.pts, f.key, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); + } + } + #[cfg(feature = "hwcodec")] + video_frame::Union::H264s(h264s) => { + for f in h264s.frames.iter() { + self.check_pts(f.pts, f.key, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); + } + } + #[cfg(feature = "hwcodec")] + video_frame::Union::H265s(h265s) => { + for f in h265s.frames.iter() { + self.check_pts(f.pts, f.key, w, h, format)?; + self.as_mut().map(|x| x.write_video(f)); + } + } + _ => bail!("unsupported frame type"), + } + self.send_state(RecordState::NewFrame); + Ok(()) + } + + fn check_pts( + &mut self, + pts: i64, + key: bool, + w: usize, + h: usize, + format: CodecFormat, + ) -> ResultType<()> { + // https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c + if self.pts.is_none() && !key { + bail!("first frame is not key frame"); + } + let old_pts = self.pts; + self.pts = Some(pts); + if old_pts.clone().unwrap_or_default() > pts { + log::info!("pts {:?} -> {}, change record filename", old_pts, pts); + self.inner = None; + self.ctx2 = None; + let res = self.check(w, h, format); + if res.is_err() { + self.check_failed = true; + log::error!("check failed: {:?}", res); + res?; + } + self.pts = Some(pts); + } + Ok(()) + } + + fn send_state(&self, state: RecordState) { + self.ctx.tx.as_ref().map(|tx| tx.send(state)); + } +} + +struct WebmRecorder { + vt: VideoTrack, + webm: Option>>, + ctx: RecorderContext, + ctx2: RecorderContext2, + key: bool, + written: bool, + start: Instant, +} + +impl RecorderApi for WebmRecorder { + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType { + let out = match { + OpenOptions::new() + .write(true) + .create_new(true) + .open(&ctx2.filename) + } { + Ok(file) => file, + Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx2.filename)?, + Err(e) => return Err(e.into()), + }; + let mut webm = match mux::Segment::new(mux::Writer::new(out)) { + Some(v) => v, + None => bail!("Failed to create webm mux"), + }; + let vt = webm.add_video_track( + ctx2.width as _, + ctx2.height as _, + None, + if ctx2.format == CodecFormat::VP9 { + mux::VideoCodecId::VP9 + } else if ctx2.format == CodecFormat::VP8 { + mux::VideoCodecId::VP8 + } else { + mux::VideoCodecId::AV1 + }, + ); + if ctx2.format == CodecFormat::AV1 { + // [129, 8, 12, 0] in 3.6.0, but zero works + let codec_private = vec![0, 0, 0, 0]; + if !webm.set_codec_private(vt.track_number(), &codec_private) { + bail!("Failed to set codec private"); + } + } + Ok(WebmRecorder { + vt, + webm: Some(webm), + ctx, + ctx2, + key: false, + written: false, + start: Instant::now(), + }) + } + + fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool { + if frame.key { + self.key = true; + } + if self.key { + let ok = self + .vt + .add_frame(&frame.data, frame.pts as u64 * 1_000_000, frame.key); + if ok { + self.written = true; + } + ok + } else { + false + } + } +} + +impl Drop for WebmRecorder { + fn drop(&mut self) { + let _ = std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None)); + let mut state = RecordState::WriteTail; + if !self.written || self.start.elapsed().as_secs() < MIN_SECS { + std::fs::remove_file(&self.ctx2.filename).ok(); + state = RecordState::RemoveFile; + } + self.ctx.tx.as_ref().map(|tx| tx.send(state)); + } +} + +#[cfg(feature = "hwcodec")] +struct HwRecorder { + muxer: Option, + ctx: RecorderContext, + ctx2: RecorderContext2, + written: bool, + key: bool, + start: Instant, +} + +#[cfg(feature = "hwcodec")] +impl RecorderApi for HwRecorder { + fn new(ctx: RecorderContext, ctx2: RecorderContext2) -> ResultType { + let muxer = Muxer::new(MuxContext { + filename: ctx2.filename.clone(), + width: ctx2.width, + height: ctx2.height, + is265: ctx2.format == CodecFormat::H265, + framerate: crate::hwcodec::DEFAULT_FPS as _, + }) + .map_err(|_| anyhow!("Failed to create hardware muxer"))?; + Ok(HwRecorder { + muxer: Some(muxer), + ctx, + ctx2, + written: false, + key: false, + start: Instant::now(), + }) + } + + fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool { + if frame.key { + self.key = true; + } + if self.key { + let ok = self + .muxer + .as_mut() + .map(|m| m.write_video(&frame.data, frame.key).is_ok()) + .unwrap_or_default(); + if ok { + self.written = true; + } + ok + } else { + false + } + } +} + +#[cfg(feature = "hwcodec")] +impl Drop for HwRecorder { + fn drop(&mut self) { + self.muxer.as_mut().map(|m| m.write_tail().ok()); + let mut state = RecordState::WriteTail; + if !self.written || self.start.elapsed().as_secs() < MIN_SECS { + // The process cannot access the file because it is being used by another process + self.muxer = None; + std::fs::remove_file(&self.ctx2.filename).ok(); + state = RecordState::RemoveFile; + } + self.ctx.tx.as_ref().map(|tx| tx.send(state)); + } +} diff --git a/vendor/rustdesk/libs/scrap/src/common/vpx.rs b/vendor/rustdesk/libs/scrap/src/common/vpx.rs new file mode 100644 index 0000000..d627dcf --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/vpx.rs @@ -0,0 +1,26 @@ +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(improper_ctypes)] +#![allow(dead_code)] +#![allow(unused_imports)] + +impl Default for vpx_codec_enc_cfg { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +impl Default for vpx_codec_ctx { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +impl Default for vpx_image_t { + fn default() -> Self { + unsafe { std::mem::zeroed() } + } +} + +include!(concat!(env!("OUT_DIR"), "/vpx_ffi.rs")); diff --git a/vendor/rustdesk/libs/scrap/src/common/vpxcodec.rs b/vendor/rustdesk/libs/scrap/src/common/vpxcodec.rs new file mode 100644 index 0000000..f41dfb1 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/vpxcodec.rs @@ -0,0 +1,597 @@ +// https://github.com/astraw/vpx-encode +// https://github.com/astraw/env-libvpx-sys +// https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs +// https://github.com/chromium/chromium/blob/e7b24573bc2e06fed4749dd6b6abfce67f29052f/media/video/vpx_video_encoder.cc#L522 + +use hbb_common::anyhow::{anyhow, Context}; +use hbb_common::log; +use hbb_common::message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame}; +use hbb_common::ResultType; + +use crate::codec::{base_bitrate, codec_thread_num, EncoderApi}; +use crate::{EncodeInput, EncodeYuvFormat, GoogleImage, Pixfmt, STRIDE_ALIGN}; + +use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *}; +use crate::{generate_call_macro, generate_call_ptr_macro, Error, Result}; +use hbb_common::bytes::Bytes; +use std::os::raw::{c_int, c_uint}; +use std::{ptr, slice}; + +generate_call_macro!(call_vpx, false); +generate_call_ptr_macro!(call_vpx_ptr); + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum VpxVideoCodecId { + VP8, + VP9, +} + +impl Default for VpxVideoCodecId { + fn default() -> VpxVideoCodecId { + VpxVideoCodecId::VP9 + } +} + +pub struct VpxEncoder { + ctx: vpx_codec_ctx_t, + width: usize, + height: usize, + id: VpxVideoCodecId, + i444: bool, + yuvfmt: EncodeYuvFormat, +} + +pub struct VpxDecoder { + ctx: vpx_codec_ctx_t, +} + +impl EncoderApi for VpxEncoder { + fn new(cfg: crate::codec::EncoderCfg, i444: bool) -> ResultType + where + Self: Sized, + { + match cfg { + crate::codec::EncoderCfg::VPX(config) => { + let i = match config.codec { + VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()), + VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()), + }; + let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; + call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0)); + + // https://www.webmproject.org/docs/encoder-parameters/ + // default: c.rc_min_quantizer = 0, c.rc_max_quantizer = 63 + // try rc_resize_allowed later + + c.g_w = config.width; + c.g_h = config.height; + c.g_timebase.num = 1; + c.g_timebase.den = 1000; // Output timestamp precision + c.rc_undershoot_pct = 95; + // When the data buffer falls below this percentage of fullness, a dropped frame is indicated. Set the threshold to zero (0) to disable this feature. + // In dynamic scenes, low bitrate gets low fps while high bitrate gets high fps. + c.rc_dropframe_thresh = 25; + c.g_threads = codec_thread_num(64) as _; + c.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT; + // https://developers.google.com/media/vp9/bitrate-modes/ + // Constant Bitrate mode (CBR) is recommended for live streaming with VP9. + c.rc_end_usage = vpx_rc_mode::VPX_CBR; + if let Some(keyframe_interval) = config.keyframe_interval { + c.kf_min_dist = 0; + c.kf_max_dist = keyframe_interval as _; + } else { + c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot + } + + let (q_min, q_max) = Self::calc_q_values(config.quality); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = + Self::bitrate(config.width as _, config.height as _, config.quality); + // https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp9/common/vp9_enums.h#29 + // https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp8/vp8_cx_iface.c#282 + c.g_profile = if i444 && config.codec == VpxVideoCodecId::VP9 { + 1 + } else { + 0 + }; + + /* + The VPX encoder supports two-pass encoding for rate control purposes. + In two-pass encoding, the entire encoding process is performed twice. + The first pass generates new control parameters for the second pass. + + This approach enables the best PSNR at the same bit rate. + */ + + let mut ctx = Default::default(); + call_vpx!(vpx_codec_enc_init_ver( + &mut ctx, + i, + &c, + 0, + VPX_ENCODER_ABI_VERSION as _ + )); + + if config.codec == VpxVideoCodecId::VP9 { + // set encoder internal speed settings + // in ffmpeg, it is --speed option + /* + set to 0 or a positive value 1-16, the codec will try to adapt its + complexity depending on the time it spends encoding. Increasing this + number will make the speed go up and the quality go down. + Negative values mean strict enforcement of this + while positive values are adaptive + */ + /* https://developers.google.com/media/vp9/live-encoding + Speed 5 to 8 should be used for live / real-time encoding. + Lower numbers (5 or 6) are higher quality but require more CPU power. + Higher numbers (7 or 8) will be lower quality but more manageable for lower latency + use cases and also for lower CPU power devices such as mobile. + */ + call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, 7,)); + // set row level multi-threading + /* + as some people in comments and below have already commented, + more recent versions of libvpx support -row-mt 1 to enable tile row + multi-threading. This can increase the number of tiles by up to 4x in VP9 + (since the max number of tile rows is 4, regardless of video height). + To enable this, use -tile-rows N where N is the number of tile rows in + log2 units (so -tile-rows 1 means 2 tile rows and -tile-rows 2 means 4 tile + rows). The total number of active threads will then be equal to + $tile_rows * $tile_columns + */ + call_vpx!(vpx_codec_control_( + &mut ctx, + VP9E_SET_ROW_MT as _, + 1 as c_int + )); + + call_vpx!(vpx_codec_control_( + &mut ctx, + VP9E_SET_TILE_COLUMNS as _, + 4 as c_int + )); + } else if config.codec == VpxVideoCodecId::VP8 { + // https://github.com/webmproject/libvpx/blob/972149cafeb71d6f08df89e91a0130d6a38c4b15/vpx/vp8cx.h#L172 + // https://groups.google.com/a/webmproject.org/g/webm-discuss/c/DJhSrmfQ61M + call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, 12,)); + } + + Ok(Self { + ctx, + width: config.width as _, + height: config.height as _, + id: config.codec, + i444, + yuvfmt: Self::get_yuvfmt(config.width, config.height, i444), + }) + } + _ => Err(anyhow!("encoder type mismatch")), + } + } + + fn encode_to_message(&mut self, input: EncodeInput, ms: i64) -> ResultType { + let mut frames = Vec::new(); + for ref frame in self + .encode(ms, input.yuv()?, STRIDE_ALIGN) + .with_context(|| "Failed to encode")? + { + frames.push(VpxEncoder::create_frame(frame)); + } + for ref frame in self.flush().with_context(|| "Failed to flush")? { + frames.push(VpxEncoder::create_frame(frame)); + } + + // to-do: flush periodically, e.g. 1 second + if frames.len() > 0 { + Ok(VpxEncoder::create_video_frame(self.id, frames)) + } else { + Err(anyhow!("no valid frame")) + } + } + + fn yuvfmt(&self) -> crate::EncodeYuvFormat { + self.yuvfmt.clone() + } + + #[cfg(feature = "vram")] + fn input_texture(&self) -> bool { + false + } + + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { + let mut c = unsafe { *self.ctx.config.enc.to_owned() }; + let (q_min, q_max) = Self::calc_q_values(ratio); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = Self::bitrate(self.width as _, self.height as _, ratio); + call_vpx!(vpx_codec_enc_config_set(&mut self.ctx, &c)); + Ok(()) + } + + fn bitrate(&self) -> u32 { + let c = unsafe { *self.ctx.config.enc.to_owned() }; + c.rc_target_bitrate + } + + fn support_changing_quality(&self) -> bool { + true + } + + fn latency_free(&self) -> bool { + true + } + + fn is_hardware(&self) -> bool { + false + } + + fn disable(&self) {} +} + +impl VpxEncoder { + pub fn encode<'a>(&'a mut self, pts: i64, data: &[u8], stride_align: usize) -> Result> { + let bpp = if self.i444 { 24 } else { 12 }; + if data.len() < self.width * self.height * bpp / 8 { + return Err(Error::FailedCall("len not enough".to_string())); + } + let fmt = if self.i444 { + vpx_img_fmt::VPX_IMG_FMT_I444 + } else { + vpx_img_fmt::VPX_IMG_FMT_I420 + }; + + let mut image = Default::default(); + call_vpx_ptr!(vpx_img_wrap( + &mut image, + fmt, + self.width as _, + self.height as _, + stride_align as _, + data.as_ptr() as _, + )); + + call_vpx!(vpx_codec_encode( + &mut self.ctx, + &image, + pts as _, + 1, // Duration + 0, // Flags + VPX_DL_REALTIME as _, + )); + + Ok(EncodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } + + /// Notify the encoder to return any pending packets + pub fn flush<'a>(&'a mut self) -> Result> { + call_vpx!(vpx_codec_encode( + &mut self.ctx, + ptr::null(), + -1, // PTS + 1, // Duration + 0, // Flags + VPX_DL_REALTIME as _, + )); + + Ok(EncodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } + + #[inline] + pub fn create_video_frame( + codec_id: VpxVideoCodecId, + frames: Vec, + ) -> VideoFrame { + let mut vf = VideoFrame::new(); + let vpxs = EncodedVideoFrames { + frames: frames.into(), + ..Default::default() + }; + match codec_id { + VpxVideoCodecId::VP8 => vf.set_vp8s(vpxs), + VpxVideoCodecId::VP9 => vf.set_vp9s(vpxs), + } + vf + } + + #[inline] + fn create_frame(frame: &EncodeFrame) -> EncodedVideoFrame { + EncodedVideoFrame { + data: Bytes::from(frame.data.to_vec()), + key: frame.key, + pts: frame.pts, + ..Default::default() + } + } + + fn bitrate(width: u32, height: u32, ratio: f32) -> u32 { + let bitrate = base_bitrate(width, height) as f32; + (bitrate * ratio) as u32 + } + + #[inline] + fn calc_q_values(ratio: f32) -> (u32, u32) { + let b = (ratio * 100.0) as u32; + let b = std::cmp::min(b, 200); + let q_min1 = 36; + let q_min2 = 0; + let q_max1 = 56; + let q_max2 = 37; + + let t = b as f32 / 200.0; + + let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32; + let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32; + + q_min = q_min.clamp(q_min2, q_min1); + q_max = q_max.clamp(q_max2, q_max1); + + (q_min, q_max) + } + + fn get_yuvfmt(width: u32, height: u32, i444: bool) -> EncodeYuvFormat { + let mut img = Default::default(); + let fmt = if i444 { + vpx_img_fmt::VPX_IMG_FMT_I444 + } else { + vpx_img_fmt::VPX_IMG_FMT_I420 + }; + unsafe { + vpx_img_wrap( + &mut img, + fmt, + width as _, + height as _, + crate::STRIDE_ALIGN as _, + 0x1 as _, + ); + } + let pixfmt = if i444 { Pixfmt::I444 } else { Pixfmt::I420 }; + EncodeYuvFormat { + pixfmt, + w: img.w as _, + h: img.h as _, + stride: img.stride.map(|s| s as usize).to_vec(), + u: img.planes[1] as usize - img.planes[0] as usize, + v: img.planes[2] as usize - img.planes[0] as usize, + } + } +} + +impl Drop for VpxEncoder { + fn drop(&mut self) { + unsafe { + let result = vpx_codec_destroy(&mut self.ctx); + if result != VPX_CODEC_OK { + panic!("failed to destroy vpx codec"); + } + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct EncodeFrame<'a> { + /// Compressed data. + pub data: &'a [u8], + /// Whether the frame is a keyframe. + pub key: bool, + /// Presentation timestamp (in timebase units). + pub pts: i64, +} + +#[derive(Clone, Copy, Debug)] +pub struct VpxEncoderConfig { + /// The width (in pixels). + pub width: c_uint, + /// The height (in pixels). + pub height: c_uint, + /// The bitrate ratio + pub quality: f32, + /// The codec + pub codec: VpxVideoCodecId, + /// keyframe interval + pub keyframe_interval: Option, +} + +#[derive(Clone, Copy, Debug)] +pub struct VpxDecoderConfig { + pub codec: VpxVideoCodecId, +} + +pub struct EncodeFrames<'a> { + ctx: &'a mut vpx_codec_ctx_t, + iter: vpx_codec_iter_t, +} + +impl<'a> Iterator for EncodeFrames<'a> { + type Item = EncodeFrame<'a>; + fn next(&mut self) -> Option { + loop { + unsafe { + let pkt = vpx_codec_get_cx_data(self.ctx, &mut self.iter); + if pkt.is_null() { + return None; + } else if (*pkt).kind == vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT { + let f = &(*pkt).data.frame; + return Some(Self::Item { + data: slice::from_raw_parts(f.buf as _, f.sz as _), + key: (f.flags & VPX_FRAME_IS_KEY) != 0, + pts: f.pts, + }); + } else { + // Ignore the packet. + } + } + } + } +} + +impl VpxDecoder { + /// Create a new decoder + /// + /// # Errors + /// + /// The function may fail if the underlying libvpx does not provide + /// the VP9 decoder. + pub fn new(config: VpxDecoderConfig) -> Result { + // This is sound because `vpx_codec_ctx` is a repr(C) struct without any field that can + // cause UB if uninitialized. + let i = match config.codec { + VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()), + VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()), + }; + let mut ctx = Default::default(); + let cfg = vpx_codec_dec_cfg_t { + threads: codec_thread_num(64) as _, + w: 0, + h: 0, + }; + /* + unsafe { + println!("{}", vpx_codec_get_caps(i)); + } + */ + call_vpx!(vpx_codec_dec_init_ver( + &mut ctx, + i, + &cfg, + 0, + VPX_DECODER_ABI_VERSION as _, + )); + Ok(Self { ctx }) + } + + /// Feed some compressed data to the encoder + /// + /// The `data` slice is sent to the decoder + /// + /// It matches a call to `vpx_codec_decode`. + pub fn decode<'a>(&'a mut self, data: &[u8]) -> Result> { + call_vpx!(vpx_codec_decode( + &mut self.ctx, + data.as_ptr(), + data.len() as _, + ptr::null_mut(), + 0, + )); + + Ok(DecodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } + + /// Notify the decoder to return any pending frame + pub fn flush<'a>(&'a mut self) -> Result> { + call_vpx!(vpx_codec_decode( + &mut self.ctx, + ptr::null(), + 0, + ptr::null_mut(), + 0 + )); + Ok(DecodeFrames { + ctx: &mut self.ctx, + iter: ptr::null(), + }) + } +} + +impl Drop for VpxDecoder { + fn drop(&mut self) { + unsafe { + let result = vpx_codec_destroy(&mut self.ctx); + if result != VPX_CODEC_OK { + panic!("failed to destroy vpx codec"); + } + } + } +} + +pub struct DecodeFrames<'a> { + ctx: &'a mut vpx_codec_ctx_t, + iter: vpx_codec_iter_t, +} + +impl<'a> Iterator for DecodeFrames<'a> { + type Item = Image; + fn next(&mut self) -> Option { + let img = unsafe { vpx_codec_get_frame(self.ctx, &mut self.iter) }; + if img.is_null() { + return None; + } else { + return Some(Image(img)); + } + } +} + +// https://chromium.googlesource.com/webm/libvpx/+/bali/vpx/src/vpx_image.c +pub struct Image(*mut vpx_image_t); +impl Image { + #[inline] + pub fn new() -> Self { + Self(std::ptr::null_mut()) + } + + #[inline] + pub fn is_null(&self) -> bool { + self.0.is_null() + } + + #[inline] + pub fn format(&self) -> vpx_img_fmt_t { + // VPX_IMG_FMT_I420 + self.inner().fmt + } + + #[inline] + pub fn inner(&self) -> &vpx_image_t { + unsafe { &*self.0 } + } +} + +impl GoogleImage for Image { + #[inline] + fn width(&self) -> usize { + self.inner().d_w as _ + } + + #[inline] + fn height(&self) -> usize { + self.inner().d_h as _ + } + + #[inline] + fn stride(&self) -> Vec { + self.inner().stride.iter().map(|x| *x as i32).collect() + } + + #[inline] + fn planes(&self) -> Vec<*mut u8> { + self.inner().planes.iter().map(|p| *p as *mut u8).collect() + } + + fn chroma(&self) -> Chroma { + match self.inner().fmt { + vpx_img_fmt::VPX_IMG_FMT_I444 => Chroma::I444, + _ => Chroma::I420, + } + } +} + +impl Drop for Image { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { vpx_img_free(self.0) }; + } + } +} + +unsafe impl Send for vpx_codec_ctx_t {} diff --git a/vendor/rustdesk/libs/scrap/src/common/vram.rs b/vendor/rustdesk/libs/scrap/src/common/vram.rs new file mode 100644 index 0000000..22645d9 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/vram.rs @@ -0,0 +1,404 @@ +use std::{ + collections::{HashMap, HashSet}, + ffi::c_void, + sync::{Arc, Mutex}, +}; + +use crate::{ + codec::{enable_vram_option, EncoderApi, EncoderCfg}, + hwcodec::HwCodecConfig, + AdapterDevice, CodecFormat, EncodeInput, EncodeYuvFormat, Pixfmt, +}; +use hbb_common::{ + anyhow::{anyhow, bail, Context}, + bytes::Bytes, + log, + message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame}, + ResultType, +}; +use hwcodec::{ + common::{DataFormat, Driver, MAX_GOP}, + vram::{ + decode::{self, DecodeFrame, Decoder}, + encode::{self, EncodeFrame, Encoder}, + Available, DecodeContext, DynamicContext, EncodeContext, FeatureContext, + }, +}; + +// https://www.reddit.com/r/buildapc/comments/d2m4ny/two_graphics_cards_two_monitors/ +// https://www.reddit.com/r/techsupport/comments/t2v9u6/dual_monitor_setup_with_dual_gpu/ +// https://cybersided.com/two-monitors-two-gpus/ +// https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-getadapterluid#remarks +lazy_static::lazy_static! { + static ref ENOCDE_NOT_USE: Arc>> = Default::default(); + static ref FALLBACK_GDI_DISPLAYS: Arc>> = Default::default(); +} + +#[derive(Debug, Clone)] +pub struct VRamEncoderConfig { + pub device: AdapterDevice, + pub width: usize, + pub height: usize, + pub quality: f32, + pub feature: FeatureContext, + pub keyframe_interval: Option, +} + +pub struct VRamEncoder { + encoder: Encoder, + pub format: DataFormat, + ctx: EncodeContext, + bitrate: u32, + last_frame_len: usize, + same_bad_len_counter: usize, +} + +impl EncoderApi for VRamEncoder { + fn new(cfg: EncoderCfg, _i444: bool) -> ResultType + where + Self: Sized, + { + match cfg { + EncoderCfg::VRAM(config) => { + let bitrate = Self::bitrate( + config.feature.data_format, + config.width, + config.height, + config.quality, + ); + let gop = config.keyframe_interval.unwrap_or(MAX_GOP as _) as i32; + let ctx = EncodeContext { + f: config.feature.clone(), + d: DynamicContext { + device: Some(config.device.device), + width: config.width as _, + height: config.height as _, + kbitrate: bitrate as _, + framerate: 30, + gop, + }, + }; + match Encoder::new(ctx.clone()) { + Ok(encoder) => Ok(VRamEncoder { + encoder, + ctx, + format: config.feature.data_format, + bitrate, + last_frame_len: 0, + same_bad_len_counter: 0, + }), + Err(_) => Err(anyhow!(format!("Failed to create encoder"))), + } + } + _ => Err(anyhow!("encoder type mismatch")), + } + } + + fn encode_to_message( + &mut self, + frame: EncodeInput, + ms: i64, + ) -> ResultType { + let (texture, rotation) = frame.texture()?; + if rotation != 0 { + // to-do: support rotation + // Both the encoder and display(w,h) information need to be changed. + bail!("rotation not supported"); + } + let mut vf = VideoFrame::new(); + let mut frames = Vec::new(); + for frame in self + .encode(texture, ms) + .with_context(|| "Failed to encode")? + { + frames.push(EncodedVideoFrame { + data: Bytes::from(frame.data), + pts: frame.pts, + key: frame.key == 1, + ..Default::default() + }); + } + if frames.len() > 0 { + // This kind of problem is occurred after a period of time when using AMD encoding, + // the encoding length is fixed at about 40, and the picture is still + const MIN_BAD_LEN: usize = 100; + const MAX_BAD_COUNTER: usize = 30; + let this_frame_len = frames[0].data.len(); + if this_frame_len < MIN_BAD_LEN && this_frame_len == self.last_frame_len { + self.same_bad_len_counter += 1; + if self.same_bad_len_counter >= MAX_BAD_COUNTER { + log::info!( + "{} times encoding len is {}, switch", + self.same_bad_len_counter, + self.last_frame_len + ); + bail!(crate::codec::ENCODE_NEED_SWITCH); + } + } else { + self.same_bad_len_counter = 0; + } + self.last_frame_len = this_frame_len; + let frames = EncodedVideoFrames { + frames: frames.into(), + ..Default::default() + }; + match self.format { + DataFormat::H264 => vf.set_h264s(frames), + DataFormat::H265 => vf.set_h265s(frames), + _ => bail!("{:?} not supported", self.format), + } + Ok(vf) + } else { + Err(anyhow!("no valid frame")) + } + } + + fn yuvfmt(&self) -> EncodeYuvFormat { + // useless + EncodeYuvFormat { + pixfmt: Pixfmt::BGRA, + w: self.ctx.d.width as _, + h: self.ctx.d.height as _, + stride: Vec::new(), + u: 0, + v: 0, + } + } + + #[cfg(feature = "vram")] + fn input_texture(&self) -> bool { + true + } + + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { + let bitrate = Self::bitrate( + self.ctx.f.data_format, + self.ctx.d.width as _, + self.ctx.d.height as _, + ratio, + ); + if bitrate > 0 { + if self.encoder.set_bitrate((bitrate) as _).is_ok() { + self.bitrate = bitrate; + } + } + Ok(()) + } + + fn bitrate(&self) -> u32 { + self.bitrate + } + + fn support_changing_quality(&self) -> bool { + true + } + + fn latency_free(&self) -> bool { + true + } + + fn is_hardware(&self) -> bool { + true + } + + fn disable(&self) { + HwCodecConfig::clear(true, true); + } +} + +impl VRamEncoder { + pub fn try_get(device: &AdapterDevice, format: CodecFormat) -> Option { + let v: Vec<_> = Self::available(format) + .drain(..) + .filter(|e| e.luid == device.luid) + .collect(); + if v.len() > 0 { + // prefer ffmpeg + if let Some(ctx) = v.iter().find(|c| c.driver == Driver::FFMPEG) { + return Some(ctx.clone()); + } + Some(v[0].clone()) + } else { + None + } + } + + pub fn available(format: CodecFormat) -> Vec { + let fallbacks = FALLBACK_GDI_DISPLAYS.lock().unwrap().clone(); + if !fallbacks.is_empty() { + log::info!("fallback gdi displays not empty: {fallbacks:?}"); + return vec![]; + } + let not_use = ENOCDE_NOT_USE.lock().unwrap().clone(); + if not_use.values().any(|not_use| *not_use) { + log::info!("currently not use vram encoders: {not_use:?}"); + return vec![]; + } + let data_format = match format { + CodecFormat::H264 => DataFormat::H264, + CodecFormat::H265 => DataFormat::H265, + _ => return vec![], + }; + let v: Vec<_> = crate::hwcodec::HwCodecConfig::get() + .vram_encode + .drain(..) + .filter(|c| c.data_format == data_format) + .collect(); + if crate::hwcodec::HwRamEncoder::try_get(format).is_some() { + // has fallback, no need to require all adapters support + v + } else { + let Ok(displays) = crate::Display::all() else { + log::error!("failed to get displays"); + return vec![]; + }; + if displays.is_empty() { + log::error!("no display found"); + return vec![]; + } + let luids = displays + .iter() + .map(|d| d.adapter_luid()) + .collect::>(); + if luids + .iter() + .all(|luid| v.iter().any(|f| Some(f.luid) == *luid)) + { + v + } else { + log::info!("not all adapters support {data_format:?}, luids = {luids:?}"); + vec![] + } + } + } + + pub fn encode(&mut self, texture: *mut c_void, ms: i64) -> ResultType> { + match self.encoder.encode(texture, ms) { + Ok(v) => { + let mut data = Vec::::new(); + data.append(v); + Ok(data) + } + Err(_) => Ok(Vec::::new()), + } + } + + pub fn bitrate(fmt: DataFormat, width: usize, height: usize, ratio: f32) -> u32 { + crate::hwcodec::HwRamEncoder::calc_bitrate(width, height, ratio, fmt == DataFormat::H264) + } + + pub fn set_not_use(video_service_name: String, not_use: bool) { + log::info!("set {video_service_name} not use vram encode to {not_use}"); + ENOCDE_NOT_USE + .lock() + .unwrap() + .insert(video_service_name, not_use); + } + + pub fn set_fallback_gdi(video_service_name: String, fallback: bool) { + if fallback { + FALLBACK_GDI_DISPLAYS + .lock() + .unwrap() + .insert(video_service_name); + } else { + FALLBACK_GDI_DISPLAYS + .lock() + .unwrap() + .remove(&video_service_name); + } + } +} + +pub struct VRamDecoder { + decoder: Decoder, +} + +impl VRamDecoder { + pub fn try_get(format: CodecFormat, luid: Option) -> Option { + let v: Vec<_> = Self::available(format, luid); + if v.len() > 0 { + // prefer ffmpeg + if let Some(ctx) = v.iter().find(|c| c.driver == Driver::FFMPEG) { + return Some(ctx.clone()); + } + Some(v[0].clone()) + } else { + None + } + } + + pub fn available(format: CodecFormat, luid: Option) -> Vec { + let luid = luid.unwrap_or_default(); + let data_format = match format { + CodecFormat::H264 => DataFormat::H264, + CodecFormat::H265 => DataFormat::H265, + _ => return vec![], + }; + crate::hwcodec::HwCodecConfig::get() + .vram_decode + .drain(..) + .filter(|c| c.data_format == data_format && c.luid == luid && luid != 0) + .collect() + } + + pub fn possible_available_without_check() -> (bool, bool) { + if !enable_vram_option(false) { + return (false, false); + } + let v = crate::hwcodec::HwCodecConfig::get().vram_decode; + ( + v.iter().any(|d| d.data_format == DataFormat::H264), + v.iter().any(|d| d.data_format == DataFormat::H265), + ) + } + + pub fn new(format: CodecFormat, luid: Option) -> ResultType { + let ctx = Self::try_get(format, luid).ok_or(anyhow!("Failed to get decode context"))?; + log::info!("try create vram decoder: {ctx:?}"); + match Decoder::new(ctx) { + Ok(decoder) => Ok(Self { decoder }), + Err(_) => { + HwCodecConfig::clear(true, false); + Err(anyhow!(format!( + "Failed to create decoder, format: {:?}", + format + ))) + } + } + } + pub fn decode<'a>(&'a mut self, data: &[u8]) -> ResultType>> { + match self.decoder.decode(data) { + Ok(v) => Ok(v.iter().map(|f| VRamDecoderImage { frame: f }).collect()), + Err(e) => Err(anyhow!(e)), + } + } +} + +pub struct VRamDecoderImage<'a> { + pub frame: &'a DecodeFrame, +} + +impl VRamDecoderImage<'_> {} + +pub(crate) fn check_available_vram() -> (Vec, Vec, String) { + let d = DynamicContext { + device: None, + width: 1280, + height: 720, + kbitrate: 5000, + framerate: 60, + gop: MAX_GOP as _, + }; + let encoders = encode::available(d); + let decoders = decode::available(); + let available = Available { + e: encoders.clone(), + d: decoders.clone(), + }; + ( + encoders, + decoders, + available.serialize().unwrap_or_default(), + ) +} diff --git a/vendor/rustdesk/libs/scrap/src/common/wayland.rs b/vendor/rustdesk/libs/scrap/src/common/wayland.rs new file mode 100644 index 0000000..30b5f4d --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/wayland.rs @@ -0,0 +1,129 @@ +use crate::{ + wayland::{capturable::*, *}, + Frame, TraitCapturer, +}; +use std::{io, sync::RwLock, time::Duration}; + +use super::x11::PixelBuffer; + +pub struct Capturer(Display, Box, Vec); + +lazy_static::lazy_static! { + static ref MAP_ERR: RwLock io::Error>> = Default::default(); +} + +pub fn set_map_err(f: fn(err: String) -> io::Error) { + *MAP_ERR.write().unwrap() = Some(f); +} + +fn map_err(err: E) -> io::Error { + if let Some(f) = *MAP_ERR.read().unwrap() { + f(err.to_string()) + } else { + io::Error::new(io::ErrorKind::Other, err.to_string()) + } +} + +impl Capturer { + pub fn new(display: Display) -> io::Result { + let r = display.0.recorder(false).map_err(map_err)?; + Ok(Capturer(display, r, Default::default())) + } + + pub fn width(&self) -> usize { + self.0.width() + } + + pub fn height(&self) -> usize { + self.0.height() + } +} + +impl TraitCapturer for Capturer { + fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result> { + match self.1.capture(timeout.as_millis() as _).map_err(map_err)? { + PixelProvider::BGR0(w, h, x) => Ok(Frame::PixelBuffer(PixelBuffer::new( + x, + crate::Pixfmt::BGRA, + w, + h, + ))), + PixelProvider::RGB0(w, h, x) => Ok(Frame::PixelBuffer(PixelBuffer::new( + x, + crate::Pixfmt::RGBA, + w, + h, + ))), + PixelProvider::NONE => Err(std::io::ErrorKind::WouldBlock.into()), + _ => Err(map_err("Invalid data")), + } + } +} + +pub struct Display(pub(crate) pipewire::PipeWireCapturable); + +impl Display { + pub fn primary() -> io::Result { + let mut all = Display::all()?; + if all.is_empty() { + return Err(io::ErrorKind::NotFound.into()); + } + Ok(all.remove(0)) + } + + pub fn all() -> io::Result> { + Ok(pipewire::get_capturables() + .map_err(map_err)? + .drain(..) + .map(|x| Display(x)) + .collect()) + } + + pub fn width(&self) -> usize { + self.physical_width() + } + + pub fn height(&self) -> usize { + self.physical_height() + } + + pub fn physical_width(&self) -> usize { + self.0.physical_size.0 + } + + pub fn physical_height(&self) -> usize { + self.0.physical_size.1 + } + + pub fn logical_width(&self) -> usize { + self.0.logical_size.0 + } + + pub fn logical_height(&self) -> usize { + self.0.logical_size.1 + } + + pub fn scale(&self) -> f64 { + if self.logical_width() == 0 { + 1.0 + } else { + self.physical_width() as f64 / self.logical_width() as f64 + } + } + + pub fn origin(&self) -> (i32, i32) { + self.0.position + } + + pub fn is_online(&self) -> bool { + true + } + + pub fn is_primary(&self) -> bool { + self.0.primary + } + + pub fn name(&self) -> String { + "".to_owned() + } +} diff --git a/vendor/rustdesk/libs/scrap/src/common/x11.rs b/vendor/rustdesk/libs/scrap/src/common/x11.rs new file mode 100644 index 0000000..2a7c19c --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/common/x11.rs @@ -0,0 +1,139 @@ +use crate::{common::TraitCapturer, x11, Frame, Pixfmt, TraitPixelBuffer}; +use std::{io, time::Duration}; + +pub struct Capturer(x11::Capturer); + +pub const IS_CURSOR_EMBEDDED: bool = false; + +impl Capturer { + pub fn new(display: Display) -> io::Result { + x11::Capturer::new(display.0).map(Capturer) + } + + pub fn width(&self) -> usize { + self.0.display().rect().w as usize + } + + pub fn height(&self) -> usize { + self.0.display().rect().h as usize + } +} + +impl TraitCapturer for Capturer { + fn frame<'a>(&'a mut self, _timeout: Duration) -> io::Result> { + let width = self.width(); + let height = self.height(); + let pixfmt = self.0.display().pixfmt(); + Ok(Frame::PixelBuffer(PixelBuffer::new( + self.0.frame()?, + pixfmt, + width, + height, + ))) + } +} + +pub struct PixelBuffer<'a> { + data: &'a [u8], + pixfmt: Pixfmt, + width: usize, + height: usize, + stride: Vec, +} + +impl<'a> PixelBuffer<'a> { + pub fn new(data: &'a [u8], pixfmt: Pixfmt, width: usize, height: usize) -> Self { + let stride0 = data.len() / height; + let mut stride = Vec::new(); + stride.push(stride0); + Self { + data, + pixfmt, + width, + height, + stride, + } + } +} + +impl<'a> TraitPixelBuffer for PixelBuffer<'a> { + fn data(&self) -> &[u8] { + self.data + } + + fn width(&self) -> usize { + self.width + } + + fn height(&self) -> usize { + self.height + } + + fn stride(&self) -> Vec { + self.stride.clone() + } + + fn pixfmt(&self) -> crate::Pixfmt { + self.pixfmt + } +} + +pub struct Display(x11::Display); + +impl Display { + pub fn primary() -> io::Result { + let server = match x11::Server::default() { + Ok(server) => server, + Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()), + }; + + let mut displays = x11::Server::displays(server); + let mut best = displays.next(); + if best.as_ref().map(|x| x.is_default()) == Some(false) { + best = displays.find(|x| x.is_default()).or(best); + } + + match best { + Some(best) => Ok(Display(best)), + None => Err(io::ErrorKind::NotFound.into()), + } + } + + pub fn all() -> io::Result> { + let server = match x11::Server::default() { + Ok(server) => server, + Err(_) => return Err(io::ErrorKind::ConnectionRefused.into()), + }; + + Ok(x11::Server::displays(server).map(Display).collect()) + } + + pub fn width(&self) -> usize { + self.0.rect().w as usize + } + + pub fn height(&self) -> usize { + self.0.rect().h as usize + } + + pub fn origin(&self) -> (i32, i32) { + let r = self.0.rect(); + (r.x as _, r.y as _) + } + + pub fn is_online(&self) -> bool { + true + } + + pub fn is_primary(&self) -> bool { + self.0.is_default() + } + + pub fn name(&self) -> String { + self.0.name() + } + + pub fn get_shm_status(&self) -> Result<(), x11::Error> { + self.0.server().get_shm_status() + } +} diff --git a/vendor/rustdesk/libs/scrap/src/dxgi/gdi.rs b/vendor/rustdesk/libs/scrap/src/dxgi/gdi.rs new file mode 100644 index 0000000..3044064 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/dxgi/gdi.rs @@ -0,0 +1,213 @@ +use std::mem::size_of; +use winapi::{ + shared::windef::{HBITMAP, HDC}, + um::wingdi::{ + BitBlt, + CreateCompatibleBitmap, + CreateCompatibleDC, + CreateDCW, + DeleteDC, + DeleteObject, + GetDIBits, + SelectObject, + BITMAPINFO, + BITMAPINFOHEADER, + BI_RGB, + CAPTUREBLT, + DIB_RGB_COLORS, //CAPTUREBLT, + HGDI_ERROR, + RGBQUAD, + SRCCOPY, + }, +}; + +const PIXEL_WIDTH: i32 = 4; + +pub struct CapturerGDI { + screen_dc: HDC, + dc: HDC, + bmp: HBITMAP, + width: i32, + height: i32, +} + +impl CapturerGDI { + pub fn new(name: &[u16], width: i32, height: i32) -> Result> { + /* or Enumerate monitors with EnumDisplayMonitors, + https://stackoverflow.com/questions/34987695/how-can-i-get-an-hmonitor-handle-from-a-display-device-name + #[no_mangle] + pub extern "C" fn callback(m: HMONITOR, dc: HDC, rect: LPRECT, lp: LPARAM) -> BOOL {} + */ + /* + shared::windef::HMONITOR, + winuser::{GetMonitorInfoW, GetSystemMetrics, MONITORINFOEXW}, + let mut mi: MONITORINFOEXW = std::mem::MaybeUninit::uninit().assume_init(); + mi.cbSize = size_of::() as _; + if GetMonitorInfoW(m, &mut mi as *mut MONITORINFOEXW as _) == 0 { + return Err(format!("Failed to get monitor information of: {:?}", m).into()); + } + */ + unsafe { + if name.is_empty() { + return Err("Empty display name".into()); + } + let screen_dc = CreateDCW(&name[0], 0 as _, 0 as _, 0 as _); + if screen_dc.is_null() { + return Err("Failed to create dc from monitor name".into()); + } + + // Create a Windows Bitmap, and copy the bits into it + let dc = CreateCompatibleDC(screen_dc); + if dc.is_null() { + DeleteDC(screen_dc); + return Err("Can't get a Windows display".into()); + } + + let bmp = CreateCompatibleBitmap(screen_dc, width, height); + if bmp.is_null() { + DeleteDC(screen_dc); + DeleteDC(dc); + return Err("Can't create a Windows buffer".into()); + } + + let res = SelectObject(dc, bmp as _); + if res.is_null() || res == HGDI_ERROR { + DeleteDC(screen_dc); + DeleteDC(dc); + DeleteObject(bmp as _); + return Err("Can't select Windows buffer".into()); + } + Ok(Self { + screen_dc, + dc, + bmp, + width, + height, + }) + } + } + + pub fn frame(&self, data: &mut Vec) -> Result<(), Box> { + unsafe { + let res = BitBlt( + self.dc, + 0, + 0, + self.width, + self.height, + self.screen_dc, + 0, + 0, + SRCCOPY | CAPTUREBLT, // CAPTUREBLT enable layered window but also make cursor blinking + ); + if res == 0 { + return Err("Failed to copy screen to Windows buffer".into()); + } + + let stride = self.width * PIXEL_WIDTH; + let size: usize = (stride * self.height) as usize; + let mut data1: Vec = Vec::with_capacity(size); + data1.set_len(size); + data.resize(size, 0); + + let mut bmi = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: size_of::() as _, + biWidth: self.width as _, + biHeight: self.height as _, + biPlanes: 1, + biBitCount: (8 * PIXEL_WIDTH) as _, + biCompression: BI_RGB, + biSizeImage: (self.width * self.height * PIXEL_WIDTH) as _, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }, + bmiColors: [RGBQUAD { + rgbBlue: 0, + rgbGreen: 0, + rgbRed: 0, + rgbReserved: 0, + }], + }; + + // copy bits into Vec + let res = GetDIBits( + self.dc, + self.bmp, + 0, + self.height as _, + &mut data[0] as *mut u8 as _, + &mut bmi as _, + DIB_RGB_COLORS, + ); + if res == 0 { + return Err("GetDIBits failed".into()); + } + crate::common::ARGBMirror( + data.as_ptr(), + stride, + data1.as_mut_ptr(), + stride, + self.width, + self.height, + ); + crate::common::ARGBRotate( + data1.as_ptr(), + stride, + data.as_mut_ptr(), + stride, + self.width, + self.height, + crate::RotationMode::kRotate180, + ); + Ok(()) + } + } +} + +impl Drop for CapturerGDI { + fn drop(&mut self) { + unsafe { + DeleteDC(self.screen_dc); + DeleteDC(self.dc); + DeleteObject(self.bmp as _); + } + } +} + +#[cfg(test)] +mod tests { + use super::super::*; + use super::*; + #[test] + fn test() { + match Displays::new().unwrap().next() { + Some(d) => { + let w = d.width(); + let h = d.height(); + let c = CapturerGDI::new(d.name(), w, h).unwrap(); + let mut data = Vec::new(); + c.frame(&mut data).unwrap(); + let mut bitflipped = Vec::with_capacity((w * h * 4) as usize); + for y in 0..h { + for x in 0..w { + let i = (w * 4 * y + 4 * x) as usize; + bitflipped.extend_from_slice(&[data[i + 2], data[i + 1], data[i], 255]); + } + } + repng::encode( + std::fs::File::create("gdi_screen.png").unwrap(), + d.width() as u32, + d.height() as u32, + &bitflipped, + ) + .unwrap(); + } + _ => { + assert!(false); + } + } + } +} diff --git a/vendor/rustdesk/libs/scrap/src/dxgi/mag.rs b/vendor/rustdesk/libs/scrap/src/dxgi/mag.rs new file mode 100644 index 0000000..75fc892 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/dxgi/mag.rs @@ -0,0 +1,651 @@ +// logic from webrtc -- https://github.com/shiguredo/libwebrtc/blob/main/modules/desktop_capture/win/screen_capturer_win_magnifier.cc +#![allow(non_snake_case)] + +use lazy_static; +use std::{ + ffi::CString, + io::{Error, ErrorKind, Result}, + mem::size_of, + sync::Mutex, +}; +use winapi::{ + shared::{ + basetsd::SIZE_T, + guiddef::{IsEqualGUID, GUID}, + minwindef::{BOOL, DWORD, FALSE, FARPROC, HINSTANCE, HMODULE, HRGN, TRUE, UINT}, + ntdef::{LONG, NULL}, + windef::{HWND, RECT}, + winerror::ERROR_CLASS_ALREADY_EXISTS, + }, + um::{ + errhandlingapi::GetLastError, + libloaderapi::{FreeLibrary, GetModuleHandleExA, GetProcAddress, LoadLibraryExA}, + winuser::*, + }, +}; + +pub const MW_FILTERMODE_EXCLUDE: u32 = 0; +pub const MW_FILTERMODE_INCLUDE: u32 = 1; +pub const GET_MODULE_HANDLE_EX_FLAG_PIN: u32 = 1; +pub const GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT: u32 = 2; +pub const GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS: u32 = 4; +pub const LOAD_LIBRARY_AS_DATAFILE: u32 = 2; +pub const LOAD_WITH_ALTERED_SEARCH_PATH: u32 = 8; +pub const LOAD_IGNORE_CODE_AUTHZ_LEVEL: u32 = 16; +pub const LOAD_LIBRARY_AS_IMAGE_RESOURCE: u32 = 32; +pub const LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE: u32 = 64; +pub const LOAD_LIBRARY_REQUIRE_SIGNED_TARGET: u32 = 128; +pub const LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR: u32 = 256; +pub const LOAD_LIBRARY_SEARCH_APPLICATION_DIR: u32 = 512; +pub const LOAD_LIBRARY_SEARCH_USER_DIRS: u32 = 1024; +pub const LOAD_LIBRARY_SEARCH_SYSTEM32: u32 = 2048; +pub const LOAD_LIBRARY_SEARCH_DEFAULT_DIRS: u32 = 4096; +pub const LOAD_LIBRARY_SAFE_CURRENT_DIRS: u32 = 8192; +pub const LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER: u32 = 16384; +pub const LOAD_LIBRARY_OS_INTEGRITY_CONTINUITY: u32 = 32768; + +extern "C" { + pub static GUID_WICPixelFormat32bppRGBA: GUID; +} + +lazy_static::lazy_static! { + static ref MAG_BUFFER: Mutex<(bool, Vec)> = Default::default(); +} + +pub type REFWICPixelFormatGUID = *const GUID; +pub type WICPixelFormatGUID = GUID; + +#[allow(non_snake_case)] +#[repr(C)] +#[derive(Copy, Clone)] +pub struct tagMAGIMAGEHEADER { + pub width: UINT, + pub height: UINT, + pub format: WICPixelFormatGUID, + pub stride: UINT, + pub offset: UINT, + pub cbSize: SIZE_T, +} +pub type MAGIMAGEHEADER = tagMAGIMAGEHEADER; +pub type PMAGIMAGEHEADER = *mut tagMAGIMAGEHEADER; + +// Function types +pub type MagImageScalingCallback = ::std::option::Option< + unsafe extern "C" fn( + hwnd: HWND, + srcdata: *mut ::std::os::raw::c_void, + srcheader: MAGIMAGEHEADER, + destdata: *mut ::std::os::raw::c_void, + destheader: MAGIMAGEHEADER, + unclipped: RECT, + clipped: RECT, + dirty: HRGN, + ) -> BOOL, +>; + +extern "C" { + pub fn MagShowSystemCursor(fShowCursor: BOOL) -> BOOL; +} +pub type MagInitializeFunc = ::std::option::Option BOOL>; +pub type MagUninitializeFunc = ::std::option::Option BOOL>; +pub type MagSetWindowSourceFunc = + ::std::option::Option BOOL>; +pub type MagSetWindowFilterListFunc = ::std::option::Option< + unsafe extern "C" fn( + hwnd: HWND, + dwFilterMode: DWORD, + count: ::std::os::raw::c_int, + pHWND: *mut HWND, + ) -> BOOL, +>; +pub type MagSetImageScalingCallbackFunc = ::std::option::Option< + unsafe extern "C" fn(hwnd: HWND, callback: MagImageScalingCallback) -> BOOL, +>; + +#[repr(C)] +#[derive(Debug, Clone)] +struct MagInterface { + init_succeeded: bool, + lib_handle: HINSTANCE, + pub mag_initialize_func: MagInitializeFunc, + pub mag_uninitialize_func: MagUninitializeFunc, + pub set_window_source_func: MagSetWindowSourceFunc, + pub set_window_filter_list_func: MagSetWindowFilterListFunc, + pub set_image_scaling_callback_func: MagSetImageScalingCallbackFunc, +} + +// NOTE: MagInitialize and MagUninitialize should not be called in global init and uninit. +// If so, strange errors occur. +impl MagInterface { + fn new() -> Result { + let mut s = MagInterface { + init_succeeded: false, + lib_handle: NULL as _, + mag_initialize_func: None, + mag_uninitialize_func: None, + set_window_source_func: None, + set_window_filter_list_func: None, + set_image_scaling_callback_func: None, + }; + s.init_succeeded = false; + unsafe { + // load lib + let lib_file_name = "Magnification.dll"; + let lib_file_name_c = CString::new(lib_file_name)?; + s.lib_handle = LoadLibraryExA( + lib_file_name_c.as_ptr() as _, + NULL, + LOAD_LIBRARY_SEARCH_SYSTEM32, + ); + if s.lib_handle.is_null() { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed to LoadLibraryExA {}, error {}", + lib_file_name, + Error::last_os_error() + ), + )); + }; + + // load functions + s.mag_initialize_func = Some(std::mem::transmute(Self::load_func( + s.lib_handle, + "MagInitialize", + )?)); + s.mag_uninitialize_func = Some(std::mem::transmute(Self::load_func( + s.lib_handle, + "MagUninitialize", + )?)); + s.set_window_source_func = Some(std::mem::transmute(Self::load_func( + s.lib_handle, + "MagSetWindowSource", + )?)); + s.set_window_filter_list_func = Some(std::mem::transmute(Self::load_func( + s.lib_handle, + "MagSetWindowFilterList", + )?)); + s.set_image_scaling_callback_func = Some(std::mem::transmute(Self::load_func( + s.lib_handle, + "MagSetImageScalingCallback", + )?)); + + // MagInitialize + if let Some(init_func) = s.mag_initialize_func { + if FALSE == init_func() { + return Err(Error::new( + ErrorKind::Other, + format!("Failed to MagInitialize, error {}", Error::last_os_error()), + )); + } else { + s.init_succeeded = true; + } + } else { + return Err(Error::new( + ErrorKind::Other, + "Unreachable, mag_initialize_func should not be none", + )); + } + } + Ok(s) + } + + unsafe fn load_func(lib_module: HMODULE, func_name: &str) -> Result { + let func_name_c = CString::new(func_name)?; + let func = GetProcAddress(lib_module, func_name_c.as_ptr() as _); + if func.is_null() { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed to GetProcAddress {}, error {}", + func_name, + Error::last_os_error() + ), + )); + } + Ok(func) + } + + pub(super) fn uninit(&mut self) { + if self.init_succeeded { + if let Some(uninit_func) = self.mag_uninitialize_func { + unsafe { + if FALSE == uninit_func() { + println!("Failed MagUninitialize, error {}", Error::last_os_error()) + } + } + } + if !self.lib_handle.is_null() { + unsafe { + if FALSE == FreeLibrary(self.lib_handle) { + println!("Failed FreeLibrary, error {}", Error::last_os_error()) + } + } + self.lib_handle = NULL as _; + } + } + self.init_succeeded = false; + } +} + +impl Drop for MagInterface { + fn drop(&mut self) { + self.uninit(); + } +} + +pub struct CapturerMag { + mag_interface: MagInterface, + host_window: HWND, + magnifier_window: HWND, + + magnifier_host_class: CString, + host_window_name: CString, + magnifier_window_class: CString, + magnifier_window_name: CString, + + rect: RECT, + width: usize, + height: usize, +} + +impl Drop for CapturerMag { + fn drop(&mut self) { + self.destroy_windows(); + self.mag_interface.uninit(); + } +} + +impl CapturerMag { + pub(crate) fn is_supported() -> bool { + MagInterface::new().is_ok() + } + + pub(crate) fn new(origin: (i32, i32), width: usize, height: usize) -> Result { + unsafe { + let x = GetSystemMetrics(SM_XVIRTUALSCREEN); + let y = GetSystemMetrics(SM_YVIRTUALSCREEN); + let w = GetSystemMetrics(SM_CXVIRTUALSCREEN); + let h = GetSystemMetrics(SM_CYVIRTUALSCREEN); + if !(origin.0 >= x as i32 + && origin.1 >= y as i32 + && width <= w as usize + && height <= h as usize) + { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed Check screen rect ({}, {}, {} , {}) to ({}, {}, {}, {})", + origin.0, + origin.1, + origin.0 + width as i32, + origin.1 + height as i32, + x, + y, + x + w, + y + h + ), + )); + } + } + + let mut s = Self { + mag_interface: MagInterface::new()?, + host_window: 0 as _, + magnifier_window: 0 as _, + magnifier_host_class: CString::new("ScreenCapturerWinMagnifierHost")?, + host_window_name: CString::new("MagnifierHost")?, + magnifier_window_class: CString::new("Magnifier")?, + magnifier_window_name: CString::new("MagnifierWindow")?, + rect: RECT { + left: origin.0 as _, + top: origin.1 as _, + right: origin.0 + width as LONG, + bottom: origin.1 + height as LONG, + }, + width, + height, + }; + + unsafe { + let mut instance = 0 as HMODULE; + if 0 == GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS + | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + DefWindowProcA as _, + &mut instance as _, + ) { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed to GetModuleHandleExA, error {}", + Error::last_os_error() + ), + )); + } + + // Register the host window class. See the MSDN documentation of the + // Magnification API for more information. + let wcex = WNDCLASSEXA { + cbSize: size_of::() as _, + style: 0, + lpfnWndProc: Some(DefWindowProcA), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: instance, + hIcon: 0 as _, + hCursor: LoadCursorA(NULL as _, IDC_ARROW as _), + hbrBackground: 0 as _, + lpszClassName: s.magnifier_host_class.as_ptr() as _, + lpszMenuName: 0 as _, + hIconSm: 0 as _, + }; + + // Ignore the error which may happen when the class is already registered. + if 0 == RegisterClassExA(&wcex) { + let code = GetLastError(); + if code != ERROR_CLASS_ALREADY_EXISTS { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed to RegisterClassExA, error {}", + Error::from_raw_os_error(code as _) + ), + )); + } + } + + // Create the host window. + s.host_window = CreateWindowExA( + WS_EX_LAYERED, + s.magnifier_host_class.as_ptr(), + s.host_window_name.as_ptr(), + WS_POPUP, + 0, + 0, + 0, + 0, + NULL as _, + NULL as _, + instance, + NULL, + ); + if s.host_window.is_null() { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed to CreateWindowExA host_window, error {}", + Error::last_os_error() + ), + )); + } + + // Create the magnifier control. + s.magnifier_window = CreateWindowExA( + 0, + s.magnifier_window_class.as_ptr(), + s.magnifier_window_name.as_ptr(), + WS_CHILD | WS_VISIBLE, + 0, + 0, + 0, + 0, + s.host_window, + NULL as _, + instance, + NULL, + ); + if s.magnifier_window.is_null() { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed CreateWindowA magnifier_window, error {}", + Error::last_os_error() + ), + )); + } + + // Hide the host window. + let _ = ShowWindow(s.host_window, SW_HIDE); + + // Set the scaling callback to receive captured image. + if let Some(set_callback_func) = s.mag_interface.set_image_scaling_callback_func { + if FALSE + == set_callback_func( + s.magnifier_window, + Some(Self::on_gag_image_scaling_callback), + ) + { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed to MagSetImageScalingCallback, error {}", + Error::last_os_error() + ), + )); + } + } else { + return Err(Error::new( + ErrorKind::Other, + "Unreachable, set_image_scaling_callback_func should not be none", + )); + } + } + + Ok(s) + } + + pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result { + let name_c = CString::new(name)?; + unsafe { + let mut hwnd = if cls.len() == 0 { + FindWindowExA(NULL as _, NULL as _, NULL as _, name_c.as_ptr()) + } else { + let cls_c = CString::new(cls).unwrap(); + FindWindowExA(NULL as _, NULL as _, cls_c.as_ptr(), name_c.as_ptr()) + }; + + if hwnd.is_null() { + return Ok(false); + } + + if let Some(set_window_filter_list_func) = + self.mag_interface.set_window_filter_list_func + { + if FALSE + == set_window_filter_list_func( + self.magnifier_window, + MW_FILTERMODE_EXCLUDE, + 1, + &mut hwnd, + ) + { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed MagSetWindowFilterList for cls {} name {}, error {}", + cls, + name, + Error::last_os_error() + ), + )); + } + } else { + return Err(Error::new( + ErrorKind::Other, + "Unreachable, MagSetWindowFilterList should not be none", + )); + } + } + + Ok(true) + } + + pub(crate) fn get_rect(&self) -> ((i32, i32), usize, usize) { + ( + (self.rect.left as _, self.rect.top as _), + self.width as _, + self.height as _, + ) + } + + fn clear_data() { + let mut lock = MAG_BUFFER.lock().unwrap(); + lock.0 = false; + lock.1.clear(); + } + + pub(crate) fn frame(&mut self, data: &mut Vec) -> Result<()> { + Self::clear_data(); + + unsafe { + let x = GetSystemMetrics(SM_XVIRTUALSCREEN); + let y = GetSystemMetrics(SM_YVIRTUALSCREEN); + let w = GetSystemMetrics(SM_CXVIRTUALSCREEN); + let h = GetSystemMetrics(SM_CYVIRTUALSCREEN); + if !(self.rect.left >= x as i32 + && self.rect.top >= y as i32 + && self.rect.right <= (x + w) as i32 + && self.rect.bottom <= (y + h) as i32) + { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed Check screen rect ({}, {}, {} , {}) to ({}, {}, {}, {})", + self.rect.left, + self.rect.top, + self.rect.right, + self.rect.bottom, + x, + y, + x + w, + y + h + ), + )); + } + + if FALSE + == SetWindowPos( + self.magnifier_window, + HWND_TOP, + self.rect.left, + self.rect.top, + self.rect.right - self.rect.left, + self.rect.bottom - self.rect.top, + 0, + ) + { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed SetWindowPos (x, y, w , h) - ({}, {}, {}, {}), error {}", + self.rect.left, + self.rect.top, + self.rect.right - self.rect.left, + self.rect.bottom - self.rect.top, + Error::last_os_error() + ), + )); + } + + // on_gag_image_scaling_callback will be called and fill in the + // frame before set_window_source_func_ returns. + if let Some(set_window_source_func) = self.mag_interface.set_window_source_func { + if FALSE == set_window_source_func(self.magnifier_window, self.rect) { + return Err(Error::new( + ErrorKind::Other, + format!( + "Failed to MagSetWindowSource, error {}", + Error::last_os_error() + ), + )); + } + } else { + return Err(Error::new( + ErrorKind::Other, + "Unreachable, set_window_source_func should not be none", + )); + } + } + + let mut lock = MAG_BUFFER.lock().unwrap(); + if !lock.0 { + return Err(Error::new( + ErrorKind::Other, + "No data captured by magnifier", + )); + } + + data.resize(lock.1.len(), 0); + unsafe { + std::ptr::copy_nonoverlapping(&mut lock.1[0], &mut data[0], data.len()); + } + + Ok(()) + } + + fn destroy_windows(&mut self) { + if !self.magnifier_window.is_null() { + unsafe { + if FALSE == DestroyWindow(self.magnifier_window) { + // + println!( + "Failed DestroyWindow magnifier window, error {}", + Error::last_os_error() + ) + } + } + } + self.magnifier_window = NULL as _; + + if !self.host_window.is_null() { + unsafe { + if FALSE == DestroyWindow(self.host_window) { + // + println!( + "Failed DestroyWindow host window, error {}", + Error::last_os_error() + ) + } + } + } + self.host_window = NULL as _; + } + + unsafe extern "C" fn on_gag_image_scaling_callback( + _hwnd: HWND, + srcdata: *mut ::std::os::raw::c_void, + srcheader: MAGIMAGEHEADER, + _destdata: *mut ::std::os::raw::c_void, + _destheader: MAGIMAGEHEADER, + _unclipped: RECT, + _clipped: RECT, + _dirty: HRGN, + ) -> BOOL { + Self::clear_data(); + + if !IsEqualGUID(&srcheader.format, &GUID_WICPixelFormat32bppRGBA) { + // log warning? + return FALSE; + } + let mut lock = MAG_BUFFER.lock().unwrap(); + lock.1.resize(srcheader.cbSize, 0); + std::ptr::copy_nonoverlapping(srcdata as _, &mut lock.1[0], srcheader.cbSize); + lock.0 = true; + TRUE + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test() { + let mut capture_mag = CapturerMag::new((0, 0), 1920, 1080).unwrap(); + capture_mag.exclude("", "RustDeskPrivacyWindow").unwrap(); + std::thread::sleep(std::time::Duration::from_millis(1000 * 10)); + let mut data = Vec::new(); + capture_mag.frame(&mut data).unwrap(); + println!("capture data len: {}", data.len()); + } +} diff --git a/vendor/rustdesk/libs/scrap/src/dxgi/mod.rs b/vendor/rustdesk/libs/scrap/src/dxgi/mod.rs new file mode 100644 index 0000000..1f52969 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/dxgi/mod.rs @@ -0,0 +1,884 @@ +use std::{io, mem, ptr, slice}; +pub mod gdi; +pub use gdi::CapturerGDI; +pub mod mag; + +use winapi::{ + shared::{ + dxgi::*, + dxgi1_2::*, + dxgitype::*, + minwindef::{DWORD, FALSE, TRUE, UINT}, + ntdef::LONG, + windef::{HMONITOR, RECT}, + winerror::*, + // dxgiformat::{DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE}, + }, + um::{ + d3d11::*, d3dcommon::D3D_DRIVER_TYPE_UNKNOWN, unknwnbase::IUnknown, wingdi::*, + winnt::HRESULT, winuser::*, + }, +}; + +use crate::RotationMode::*; + +use crate::{AdapterDevice, Frame, PixelBuffer}; +use std::ffi::c_void; + +pub struct ComPtr(*mut T); +impl ComPtr { + fn is_null(&self) -> bool { + self.0.is_null() + } +} +impl Drop for ComPtr { + fn drop(&mut self) { + unsafe { + if !self.is_null() { + (*(self.0 as *mut IUnknown)).Release(); + } + } + } +} + +pub struct Capturer { + device: ComPtr, + display: Display, + context: ComPtr, + duplication: ComPtr, + fastlane: bool, + surface: ComPtr, + texture: ComPtr, + width: usize, + height: usize, + rotated: Vec, + gdi_capturer: Option, + gdi_buffer: Vec, + saved_raw_data: Vec, // for faster compare and copy + output_texture: bool, + adapter_desc1: DXGI_ADAPTER_DESC1, + rotate: Rotate, +} + +impl Capturer { + pub fn new(display: Display) -> io::Result { + let mut device = ptr::null_mut(); + let mut context = ptr::null_mut(); + let mut duplication = ptr::null_mut(); + #[allow(invalid_value)] + let mut desc = unsafe { mem::MaybeUninit::uninit().assume_init() }; + #[allow(invalid_value)] + let mut adapter_desc1 = unsafe { mem::MaybeUninit::uninit().assume_init() }; + let mut gdi_capturer = None; + + let mut res = if display.gdi { + wrap_hresult(1) + } else { + let res = wrap_hresult(unsafe { + D3D11CreateDevice( + display.adapter.0 as *mut _, + D3D_DRIVER_TYPE_UNKNOWN, + ptr::null_mut(), // No software rasterizer. + 0, // No device flags. + ptr::null_mut(), // Feature levels. + 0, // Feature levels' length. + D3D11_SDK_VERSION, + &mut device, + ptr::null_mut(), + &mut context, + ) + }); + if res.is_ok() { + wrap_hresult(unsafe { (*display.adapter.0).GetDesc1(&mut adapter_desc1) }) + } else { + res + } + }; + let device = ComPtr(device); + let context = ComPtr(context); + + if res.is_err() { + gdi_capturer = display.create_gdi(); + println!("Fallback to GDI"); + if gdi_capturer.is_some() { + res = Ok(()); + } + } else { + res = wrap_hresult(unsafe { + let hres = (*display.inner.0).DuplicateOutput(device.0 as *mut _, &mut duplication); + if hres != S_OK { + gdi_capturer = display.create_gdi(); + println!("Fallback to GDI"); + if gdi_capturer.is_some() { + S_OK + } else { + hres + } + } else { + hres + } + + // NVFBC(NVIDIA Capture SDK) which xpra used already deprecated, https://developer.nvidia.com/capture-sdk + + // also try high version DXGI for better performance, e.g. + // https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/dxgi-1-2-improvements + // dxgi-1-6 may too high, only support win10 (2018) + // https://docs.microsoft.com/zh-cn/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format + // DXGI_FORMAT_420_OPAQUE + // IDXGIOutputDuplication::GetFrameDirtyRects and IDXGIOutputDuplication::GetFrameMoveRects + // can help us update screen incrementally + + /* // not supported on my PC, try in the future + use winapi::shared::dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM; + + let format : Vec = vec![DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE]; + (*display.inner).DuplicateOutput1( + device as *mut _, + 0 as UINT, + 2 as UINT, + format.as_ptr(), + &mut duplication + ) + */ + + // if above not work, I think below should not work either, try later + // https://developer.nvidia.com/capture-sdk deprecated + // examples using directx + nvideo sdk for GPU-accelerated video encoding/decoding + // https://github.com/NVIDIA/video-sdk-samples + }); + } + + res?; + + if !duplication.is_null() { + unsafe { + (*duplication).GetDesc(&mut desc); + } + } + let rotate = Self::create_rotations(device.0, context.0, &display); + + Ok(Capturer { + device, + context, + duplication: ComPtr(duplication), + fastlane: desc.DesktopImageInSystemMemory == TRUE, + surface: ComPtr(ptr::null_mut()), + texture: ComPtr(ptr::null_mut()), + width: display.width() as usize, + height: display.height() as usize, + display, + rotated: Vec::new(), + gdi_capturer, + gdi_buffer: Vec::new(), + saved_raw_data: Vec::new(), + output_texture: false, + adapter_desc1, + rotate, + }) + } + + fn create_rotations( + device: *mut ID3D11Device, + context: *mut ID3D11DeviceContext, + display: &Display, + ) -> Rotate { + let mut video_context: *mut ID3D11VideoContext = ptr::null_mut(); + let mut video_device: *mut ID3D11VideoDevice = ptr::null_mut(); + let mut video_processor_enum: *mut ID3D11VideoProcessorEnumerator = ptr::null_mut(); + let mut video_processor: *mut ID3D11VideoProcessor = ptr::null_mut(); + let processor_rotation = match display.rotation() { + DXGI_MODE_ROTATION_ROTATE90 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_90), + DXGI_MODE_ROTATION_ROTATE180 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_180), + DXGI_MODE_ROTATION_ROTATE270 => Some(D3D11_VIDEO_PROCESSOR_ROTATION_270), + _ => None, + }; + if let Some(processor_rotation) = processor_rotation { + println!("create rotations"); + if !device.is_null() && !context.is_null() { + unsafe { + (*context).QueryInterface( + &IID_ID3D11VideoContext, + &mut video_context as *mut *mut _ as *mut *mut _, + ); + if !video_context.is_null() { + (*device).QueryInterface( + &IID_ID3D11VideoDevice, + &mut video_device as *mut *mut _ as *mut *mut _, + ); + if !video_device.is_null() { + let (input_width, input_height) = match display.rotation() { + DXGI_MODE_ROTATION_ROTATE90 | DXGI_MODE_ROTATION_ROTATE270 => { + (display.height(), display.width()) + } + _ => (display.width(), display.height()), + }; + let (output_width, output_height) = (display.width(), display.height()); + let content_desc = D3D11_VIDEO_PROCESSOR_CONTENT_DESC { + InputFrameFormat: D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, + InputFrameRate: DXGI_RATIONAL { + Numerator: 30, + Denominator: 1, + }, + InputWidth: input_width as _, + InputHeight: input_height as _, + OutputFrameRate: DXGI_RATIONAL { + Numerator: 30, + Denominator: 1, + }, + OutputWidth: output_width as _, + OutputHeight: output_height as _, + Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL, + }; + (*video_device).CreateVideoProcessorEnumerator( + &content_desc, + &mut video_processor_enum, + ); + if !video_processor_enum.is_null() { + let mut caps: D3D11_VIDEO_PROCESSOR_CAPS = mem::zeroed(); + if S_OK == (*video_processor_enum).GetVideoProcessorCaps(&mut caps) + { + if caps.FeatureCaps + & D3D11_VIDEO_PROCESSOR_FEATURE_CAPS_ROTATION + != 0 + { + (*video_device).CreateVideoProcessor( + video_processor_enum, + 0, + &mut video_processor, + ); + if !video_processor.is_null() { + (*video_context).VideoProcessorSetStreamRotation( + video_processor, + 0, + TRUE, + processor_rotation, + ); + (*video_context) + .VideoProcessorSetStreamAutoProcessingMode( + video_processor, + 0, + FALSE, + ); + (*video_context).VideoProcessorSetStreamFrameFormat( + video_processor, + 0, + D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE, + ); + (*video_context).VideoProcessorSetStreamSourceRect( + video_processor, + 0, + TRUE, + &RECT { + left: 0, + top: 0, + right: input_width as _, + bottom: input_height as _, + }, + ); + (*video_context).VideoProcessorSetStreamDestRect( + video_processor, + 0, + TRUE, + &RECT { + left: 0, + top: 0, + right: output_width as _, + bottom: output_height as _, + }, + ); + } + } + } + } + } + } + } + } + } + + let video_context = ComPtr(video_context); + let video_device = ComPtr(video_device); + let video_processor_enum = ComPtr(video_processor_enum); + let video_processor = ComPtr(video_processor); + let rotated_texture = ComPtr(ptr::null_mut()); + Rotate { + video_context, + video_device, + video_processor_enum, + video_processor, + texture: (rotated_texture, false), + } + } + + pub fn is_gdi(&self) -> bool { + self.gdi_capturer.is_some() + } + + pub fn set_gdi(&mut self) -> bool { + self.gdi_capturer = self.display.create_gdi(); + self.is_gdi() + } + + pub fn cancel_gdi(&mut self) { + self.gdi_buffer = Vec::new(); + self.gdi_capturer.take(); + } + + #[cfg(feature = "vram")] + pub fn set_output_texture(&mut self, texture: bool) { + self.output_texture = texture; + } + + unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<(*const u8, i32)> { + let mut frame = ptr::null_mut(); + #[allow(invalid_value)] + let mut info = mem::MaybeUninit::uninit().assume_init(); + + wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?; + let frame = ComPtr(frame); + + if *info.LastPresentTime.QuadPart() == 0 { + return Err(std::io::ErrorKind::WouldBlock.into()); + } + + #[allow(invalid_value)] + let mut rect = mem::MaybeUninit::uninit().assume_init(); + if self.fastlane { + wrap_hresult((*self.duplication.0).MapDesktopSurface(&mut rect))?; + } else { + self.surface = ComPtr(self.ohgodwhat(frame.0)?); + wrap_hresult((*self.surface.0).Map(&mut rect, DXGI_MAP_READ))?; + } + Ok((rect.pBits, rect.Pitch)) + } + + // copy from GPU memory to system memory + unsafe fn ohgodwhat(&mut self, frame: *mut IDXGIResource) -> io::Result<*mut IDXGISurface> { + let mut texture: *mut ID3D11Texture2D = ptr::null_mut(); + (*frame).QueryInterface( + &IID_ID3D11Texture2D, + &mut texture as *mut *mut _ as *mut *mut _, + ); + let texture = ComPtr(texture); + + #[allow(invalid_value)] + let mut texture_desc = mem::MaybeUninit::uninit().assume_init(); + (*texture.0).GetDesc(&mut texture_desc); + + texture_desc.Usage = D3D11_USAGE_STAGING; + texture_desc.BindFlags = 0; + texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + texture_desc.MiscFlags = 0; + + let mut readable = ptr::null_mut(); + wrap_hresult((*self.device.0).CreateTexture2D( + &mut texture_desc, + ptr::null(), + &mut readable, + ))?; + (*readable).SetEvictionPriority(DXGI_RESOURCE_PRIORITY_MAXIMUM); + let readable = ComPtr(readable); + + let mut surface = ptr::null_mut(); + (*readable.0).QueryInterface( + &IID_IDXGISurface, + &mut surface as *mut *mut _ as *mut *mut _, + ); + + (*self.context.0).CopyResource(readable.0 as *mut _, texture.0 as *mut _); + + Ok(surface) + } + + pub fn frame<'a>(&'a mut self, timeout: UINT) -> io::Result> { + if self.output_texture { + Ok(Frame::Texture(self.get_texture(timeout)?)) + } else { + let width = self.width; + let height = self.height; + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( + self.get_pixelbuffer(timeout)?, + width, + height, + ))) + } + } + + fn get_pixelbuffer<'a>(&'a mut self, timeout: UINT) -> io::Result<&'a [u8]> { + unsafe { + // Release last frame. + // No error checking needed because we don't care. + // None of the errors crash anyway. + let result = { + if let Some(gdi_capturer) = &self.gdi_capturer { + match gdi_capturer.frame(&mut self.gdi_buffer) { + Ok(_) => { + crate::would_block_if_equal( + &mut self.saved_raw_data, + &self.gdi_buffer, + )?; + &self.gdi_buffer + } + Err(err) => { + return Err(io::Error::new(io::ErrorKind::Other, err.to_string())); + } + } + } else { + self.unmap(); + let r = self.load_frame(timeout)?; + let rotate = match self.display.rotation() { + DXGI_MODE_ROTATION_IDENTITY | DXGI_MODE_ROTATION_UNSPECIFIED => kRotate0, + DXGI_MODE_ROTATION_ROTATE90 => kRotate90, + DXGI_MODE_ROTATION_ROTATE180 => kRotate180, + DXGI_MODE_ROTATION_ROTATE270 => kRotate270, + _ => { + return Err(io::Error::new( + io::ErrorKind::Other, + "Unknown rotation".to_string(), + )); + } + }; + if rotate == kRotate0 { + slice::from_raw_parts(r.0, r.1 as usize * self.height) + } else { + self.rotated.resize(self.width * self.height * 4, 0); + crate::common::ARGBRotate( + r.0, + r.1, + self.rotated.as_mut_ptr(), + 4 * self.width as i32, + if rotate == kRotate180 { + self.width + } else { + self.height + } as _, + if rotate != kRotate180 { + self.width + } else { + self.height + } as _, + rotate, + ); + &self.rotated[..] + } + } + }; + Ok(result) + } + } + + fn get_texture(&mut self, timeout: UINT) -> io::Result<(*mut c_void, usize)> { + unsafe { + if self.duplication.0.is_null() { + return Err(std::io::ErrorKind::AddrNotAvailable.into()); + } + (*self.duplication.0).ReleaseFrame(); + let mut frame = ptr::null_mut(); + #[allow(invalid_value)] + let mut info = mem::MaybeUninit::uninit().assume_init(); + + wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?; + let frame = ComPtr(frame); + + if info.AccumulatedFrames == 0 || *info.LastPresentTime.QuadPart() == 0 { + return Err(std::io::ErrorKind::WouldBlock.into()); + } + + let mut texture: *mut ID3D11Texture2D = ptr::null_mut(); + (*frame.0).QueryInterface( + &IID_ID3D11Texture2D, + &mut texture as *mut *mut _ as *mut *mut _, + ); + let texture = ComPtr(texture); + self.texture = texture; + + let mut final_texture = self.texture.0 as *mut c_void; + let mut rotation = match self.display.rotation() { + DXGI_MODE_ROTATION_ROTATE90 => 90, + DXGI_MODE_ROTATION_ROTATE180 => 180, + DXGI_MODE_ROTATION_ROTATE270 => 270, + _ => 0, + }; + if rotation != 0 + && !self.texture.is_null() + && !self.rotate.video_context.is_null() + && !self.rotate.video_device.is_null() + && !self.rotate.video_processor_enum.is_null() + && !self.rotate.video_processor.is_null() + { + let mut desc: D3D11_TEXTURE2D_DESC = mem::zeroed(); + (*self.texture.0).GetDesc(&mut desc); + if rotation == 90 || rotation == 270 { + let tmp = desc.Width; + desc.Width = desc.Height; + desc.Height = tmp; + } + if !self.rotate.texture.1 { + self.rotate.texture.1 = true; + let mut rotated_texture: *mut ID3D11Texture2D = ptr::null_mut(); + desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED; + (*self.device.0).CreateTexture2D(&desc, ptr::null(), &mut rotated_texture); + self.rotate.texture.0 = ComPtr(rotated_texture); + } + if !self.rotate.texture.0.is_null() + && desc.Width == self.width as u32 + && desc.Height == self.height as u32 + { + let input_view_desc = D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC { + FourCC: 0, + ViewDimension: D3D11_VPIV_DIMENSION_TEXTURE2D, + Texture2D: D3D11_TEX2D_VPIV { + ArraySlice: 0, + MipSlice: 0, + }, + }; + let mut input_view = ptr::null_mut(); + (*self.rotate.video_device.0).CreateVideoProcessorInputView( + self.texture.0 as *mut _, + self.rotate.video_processor_enum.0 as *mut _, + &input_view_desc, + &mut input_view, + ); + if !input_view.is_null() { + let input_view = ComPtr(input_view); + let mut output_view_desc: D3D11_VIDEO_PROCESSOR_OUTPUT_VIEW_DESC = + mem::zeroed(); + output_view_desc.ViewDimension = D3D11_VPOV_DIMENSION_TEXTURE2D; + output_view_desc.u.Texture2D_mut().MipSlice = 0; + let mut output_view = ptr::null_mut(); + (*self.rotate.video_device.0).CreateVideoProcessorOutputView( + self.rotate.texture.0 .0 as *mut _, + self.rotate.video_processor_enum.0 as *mut _, + &output_view_desc, + &mut output_view, + ); + if !output_view.is_null() { + let output_view = ComPtr(output_view); + let mut stream_data: D3D11_VIDEO_PROCESSOR_STREAM = mem::zeroed(); + stream_data.Enable = TRUE; + stream_data.pInputSurface = input_view.0; + (*self.rotate.video_context.0).VideoProcessorBlt( + self.rotate.video_processor.0, + output_view.0, + 0, + 1, + &stream_data, + ); + final_texture = self.rotate.texture.0 .0 as *mut c_void; + rotation = 0; + } + } + } + } + Ok((final_texture, rotation)) + } + } + + fn unmap(&self) { + unsafe { + (*self.duplication.0).ReleaseFrame(); + if self.fastlane { + (*self.duplication.0).UnMapDesktopSurface(); + } else { + if !self.surface.is_null() { + (*self.surface.0).Unmap(); + } + } + } + } + + pub fn device(&self) -> AdapterDevice { + AdapterDevice { + device: self.device.0 as _, + vendor_id: self.adapter_desc1.VendorId, + luid: ((self.adapter_desc1.AdapterLuid.HighPart as i64) << 32) + | self.adapter_desc1.AdapterLuid.LowPart as i64, + } + } +} + +impl Drop for Capturer { + fn drop(&mut self) { + if !self.duplication.is_null() { + self.unmap(); + } + } +} + +pub struct Displays { + factory: ComPtr, + adapter: ComPtr, + /// Index of the CURRENT adapter. + nadapter: UINT, + /// Index of the NEXT display to fetch. + ndisplay: UINT, +} + +impl Displays { + pub fn new() -> io::Result { + let mut factory = ptr::null_mut(); + wrap_hresult(unsafe { CreateDXGIFactory1(&IID_IDXGIFactory1, &mut factory) })?; + + let factory = factory as *mut IDXGIFactory1; + let mut adapter = ptr::null_mut(); + unsafe { + // On error, our adapter is null, so it's fine. + (*factory).EnumAdapters1(0, &mut adapter); + }; + + Ok(Displays { + factory: ComPtr(factory), + adapter: ComPtr(adapter), + nadapter: 0, + ndisplay: 0, + }) + } + + pub fn get_from_gdi() -> Vec { + let mut all = Vec::new(); + let mut i: DWORD = 0; + loop { + #[allow(invalid_value)] + let mut d: DISPLAY_DEVICEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + d.cb = std::mem::size_of::() as _; + let ok = unsafe { EnumDisplayDevicesW(std::ptr::null(), i, &mut d as _, 0) }; + if ok == FALSE { + break; + } + i += 1; + if 0 == (d.StateFlags & DISPLAY_DEVICE_ACTIVE) + || (d.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER) > 0 + { + continue; + } + // let is_primary = (d.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) > 0; + let mut disp = Display { + inner: ComPtr(std::ptr::null_mut()), + adapter: ComPtr(std::ptr::null_mut()), + desc: unsafe { std::mem::zeroed() }, + gdi: true, + }; + disp.desc.DeviceName = d.DeviceName; + #[allow(invalid_value)] + let mut m: DEVMODEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + m.dmSize = std::mem::size_of::() as _; + m.dmDriverExtra = 0; + let ok = unsafe { + EnumDisplaySettingsExW( + disp.desc.DeviceName.as_ptr(), + ENUM_CURRENT_SETTINGS, + &mut m as _, + 0, + ) + }; + if ok == FALSE { + continue; + } + disp.desc.DesktopCoordinates.left = unsafe { m.u1.s2().dmPosition.x }; + disp.desc.DesktopCoordinates.top = unsafe { m.u1.s2().dmPosition.y }; + disp.desc.DesktopCoordinates.right = + disp.desc.DesktopCoordinates.left + m.dmPelsWidth as i32; + disp.desc.DesktopCoordinates.bottom = + disp.desc.DesktopCoordinates.top + m.dmPelsHeight as i32; + disp.desc.AttachedToDesktop = 1; + all.push(disp); + } + all + } + + // No Adapter => Some(None) + // Non-Empty Adapter => Some(Some(OUTPUT)) + // End of Adapter => None + fn read_and_invalidate(&mut self) -> Option> { + // If there is no adapter, there is nothing left for us to do. + + if self.adapter.is_null() { + return Some(None); + } + + // Otherwise, we get the next output of the current adapter. + + let output = unsafe { + let mut output = ptr::null_mut(); + (*self.adapter.0).EnumOutputs(self.ndisplay, &mut output); + ComPtr(output) + }; + + // If the current adapter is done, we free it. + // We return None so the caller gets the next adapter and tries again. + + if output.is_null() { + self.adapter = ComPtr(ptr::null_mut()); + return None; + } + + // Advance to the next display. + + self.ndisplay += 1; + + // We get the display's details. + + let desc = unsafe { + #[allow(invalid_value)] + let mut desc = mem::MaybeUninit::uninit().assume_init(); + (*output.0).GetDesc(&mut desc); + desc + }; + + // We cast it up to the version needed for desktop duplication. + + let mut inner: *mut IDXGIOutput1 = ptr::null_mut(); + unsafe { + (*output.0).QueryInterface(&IID_IDXGIOutput1, &mut inner as *mut *mut _ as *mut *mut _); + } + + // If it's null, we have an error. + // So we act like the adapter is done. + + if inner.is_null() { + self.adapter = ComPtr(ptr::null_mut()); + return None; + } + + unsafe { + (*self.adapter.0).AddRef(); + } + + Some(Some(Display { + inner: ComPtr(inner), + adapter: ComPtr(self.adapter.0), + desc, + gdi: false, + })) + } +} + +impl Iterator for Displays { + type Item = Display; + fn next(&mut self) -> Option { + if let Some(res) = self.read_and_invalidate() { + res + } else { + // We need to replace the adapter. + + self.ndisplay = 0; + self.nadapter += 1; + + self.adapter = unsafe { + let mut adapter = ptr::null_mut(); + (*self.factory.0).EnumAdapters1(self.nadapter, &mut adapter); + ComPtr(adapter) + }; + + if let Some(res) = self.read_and_invalidate() { + res + } else { + // All subsequent adapters will also be empty. + None + } + } + } +} + +pub struct Display { + inner: ComPtr, + adapter: ComPtr, + desc: DXGI_OUTPUT_DESC, + gdi: bool, +} + +// optimized for updated region +// https://github.com/dchapyshev/aspia/blob/master/source/base/desktop/win/dxgi_output_duplicator.cc +// rotation +// https://github.com/bryal/dxgcap-rs/blob/master/src/lib.rs + +impl Display { + pub fn width(&self) -> LONG { + self.desc.DesktopCoordinates.right - self.desc.DesktopCoordinates.left + } + + pub fn height(&self) -> LONG { + self.desc.DesktopCoordinates.bottom - self.desc.DesktopCoordinates.top + } + + pub fn attached_to_desktop(&self) -> bool { + self.desc.AttachedToDesktop != 0 + } + + pub fn rotation(&self) -> DXGI_MODE_ROTATION { + self.desc.Rotation + } + + fn create_gdi(&self) -> Option { + if let Ok(res) = CapturerGDI::new(self.name(), self.width(), self.height()) { + Some(res) + } else { + None + } + } + + pub fn hmonitor(&self) -> HMONITOR { + self.desc.Monitor + } + + pub fn name(&self) -> &[u16] { + let s = &self.desc.DeviceName; + let i = s.iter().position(|&x| x == 0).unwrap_or(s.len()); + &s[..i] + } + + pub fn is_online(&self) -> bool { + self.desc.AttachedToDesktop != 0 + } + + pub fn origin(&self) -> (LONG, LONG) { + ( + self.desc.DesktopCoordinates.left, + self.desc.DesktopCoordinates.top, + ) + } + + #[cfg(feature = "vram")] + pub fn adapter_luid(&self) -> Option { + unsafe { + if !self.adapter.is_null() { + #[allow(invalid_value)] + let mut adapter_desc1 = mem::MaybeUninit::uninit().assume_init(); + if wrap_hresult((*self.adapter.0).GetDesc1(&mut adapter_desc1)).is_ok() { + let luid = ((adapter_desc1.AdapterLuid.HighPart as i64) << 32) + | adapter_desc1.AdapterLuid.LowPart as i64; + return Some(luid); + } + } + None + } + } +} + +fn wrap_hresult(x: HRESULT) -> io::Result<()> { + use std::io::ErrorKind::*; + Err((match x { + S_OK => return Ok(()), + DXGI_ERROR_ACCESS_LOST => ConnectionReset, + DXGI_ERROR_WAIT_TIMEOUT => TimedOut, + DXGI_ERROR_INVALID_CALL => InvalidData, + E_ACCESSDENIED => PermissionDenied, + DXGI_ERROR_UNSUPPORTED => ConnectionRefused, + DXGI_ERROR_NOT_CURRENTLY_AVAILABLE => Interrupted, + DXGI_ERROR_SESSION_DISCONNECTED => ConnectionAborted, + E_INVALIDARG => InvalidInput, + _ => { + // 0x8000ffff https://www.auslogics.com/en/articles/windows-10-update-error-0x8000ffff-fixed/ + return Err(io::Error::new(Other, format!("Error code: {:#X}", x))); + } + }) + .into()) +} + +struct Rotate { + video_context: ComPtr, + video_device: ComPtr, + video_processor_enum: ComPtr, + video_processor: ComPtr, + texture: (ComPtr, bool), +} diff --git a/vendor/rustdesk/libs/scrap/src/lib.rs b/vendor/rustdesk/libs/scrap/src/lib.rs new file mode 100644 index 0000000..77070d1 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/lib.rs @@ -0,0 +1,26 @@ +#[cfg(quartz)] +extern crate block; +#[macro_use] +extern crate cfg_if; +pub use hbb_common::libc; +#[cfg(dxgi)] +extern crate winapi; + +pub use common::*; + +#[cfg(quartz)] +pub mod quartz; + +#[cfg(x11)] +pub mod x11; + +#[cfg(all(x11, feature = "wayland"))] +pub mod wayland; + +#[cfg(dxgi)] +pub mod dxgi; + +#[cfg(target_os = "android")] +pub mod android; + +mod common; diff --git a/vendor/rustdesk/libs/scrap/src/quartz/capturer.rs b/vendor/rustdesk/libs/scrap/src/quartz/capturer.rs new file mode 100644 index 0000000..cf442c2 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/quartz/capturer.rs @@ -0,0 +1,111 @@ +use std::ptr; + +use block::{Block, ConcreteBlock}; +use hbb_common::libc::c_void; +use std::sync::{Arc, Mutex}; + +use super::config::Config; +use super::display::Display; +use super::ffi::*; +use super::frame::Frame; + +pub struct Capturer { + stream: CGDisplayStreamRef, + queue: DispatchQueue, + + width: usize, + height: usize, + format: PixelFormat, + display: Display, + stopped: Arc>, +} + +impl Capturer { + pub fn new( + display: Display, + width: usize, + height: usize, + format: PixelFormat, + config: Config, + handler: F, + ) -> Result { + let stopped = Arc::new(Mutex::new(false)); + let cloned_stopped = stopped.clone(); + let handler: FrameAvailableHandler = ConcreteBlock::new(move |status, _, surface, _| { + use self::CGDisplayStreamFrameStatus::*; + if status == Stopped { + let mut lock = cloned_stopped.lock().unwrap(); + *lock = true; + return; + } + if status == FrameComplete { + handler(unsafe { Frame::new(surface) }); + } + }) + .copy(); + + let queue = unsafe { + dispatch_queue_create( + b"quadrupleslap.scrap\0".as_ptr() as *const i8, + ptr::null_mut(), + ) + }; + + let stream = unsafe { + let config = config.build(); + let stream = CGDisplayStreamCreateWithDispatchQueue( + display.id(), + width, + height, + format, + config, + queue, + &*handler as *const Block<_, _> as *const c_void, + ); + CFRelease(config); + stream + }; + + match unsafe { CGDisplayStreamStart(stream) } { + CGError::Success => Ok(Capturer { + stream, + queue, + width, + height, + format, + display, + stopped, + }), + x => Err(x), + } + } + + pub fn width(&self) -> usize { + self.width + } + pub fn height(&self) -> usize { + self.height + } + pub fn format(&self) -> PixelFormat { + self.format + } + pub fn display(&self) -> Display { + self.display + } +} + +impl Drop for Capturer { + fn drop(&mut self) { + unsafe { + let _ = CGDisplayStreamStop(self.stream); + loop { + if *self.stopped.lock().unwrap() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(30)); + } + CFRelease(self.stream); + dispatch_release(self.queue); + } + } +} diff --git a/vendor/rustdesk/libs/scrap/src/quartz/config.rs b/vendor/rustdesk/libs/scrap/src/quartz/config.rs new file mode 100644 index 0000000..d5f992f --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/quartz/config.rs @@ -0,0 +1,75 @@ +use std::ptr; + +use hbb_common::libc::c_void; + +use super::ffi::*; + +//TODO: Color space, YCbCr matrix. +pub struct Config { + /// Whether the cursor is visible. + pub cursor: bool, + /// Whether it should letterbox or stretch. + pub letterbox: bool, + /// Minimum seconds per frame. + pub throttle: f64, + /// How many frames are allocated. + /// 3 is the recommended value. + /// 8 is the maximum value. + pub queue_length: i8, +} + +impl Config { + /// Don't forget to CFRelease this! + pub fn build(self) -> CFDictionaryRef { + unsafe { + let throttle = CFNumberCreate( + ptr::null_mut(), + CFNumberType::Float64, + &self.throttle as *const _ as *const c_void, + ); + let queue_length = CFNumberCreate( + ptr::null_mut(), + CFNumberType::SInt8, + &self.queue_length as *const _ as *const c_void, + ); + + let keys: [CFStringRef; 4] = [ + kCGDisplayStreamShowCursor, + kCGDisplayStreamPreserveAspectRatio, + kCGDisplayStreamMinimumFrameTime, + kCGDisplayStreamQueueDepth, + ]; + let values: [*mut c_void; 4] = [ + cfbool(self.cursor), + cfbool(self.letterbox), + throttle, + queue_length, + ]; + + let res = CFDictionaryCreate( + ptr::null_mut(), + keys.as_ptr(), + values.as_ptr(), + 4, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ); + + CFRelease(throttle); + CFRelease(queue_length); + + res + } + } +} + +impl Default for Config { + fn default() -> Config { + Config { + cursor: false, + letterbox: true, + throttle: 0.0, + queue_length: 3, + } + } +} diff --git a/vendor/rustdesk/libs/scrap/src/quartz/display.rs b/vendor/rustdesk/libs/scrap/src/quartz/display.rs new file mode 100644 index 0000000..137d648 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/quartz/display.rs @@ -0,0 +1,87 @@ +use std::mem; + +use super::ffi::*; + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +#[repr(C)] +pub struct Display(u32); + +impl Display { + pub fn primary() -> Display { + Display(unsafe { CGMainDisplayID() }) + } + + pub fn online() -> Result, CGError> { + unsafe { + #[allow(invalid_value)] + let mut arr: [u32; 16] = mem::MaybeUninit::uninit().assume_init(); + let mut len: u32 = 0; + + match CGGetOnlineDisplayList(16, arr.as_mut_ptr(), &mut len) { + CGError::Success => (), + x => return Err(x), + } + + let mut res = Vec::with_capacity(16); + for i in 0..len as usize { + res.push(Display(*arr.get_unchecked(i))); + } + Ok(res) + } + } + + pub fn id(self) -> u32 { + self.0 + } + + pub fn width(self) -> usize { + let w = unsafe { CGDisplayPixelsWide(self.0) }; + let s = self.scale(); + if s > 1.0 { + ((w as f64) * s).round() as usize + } else { + w + } + } + + pub fn height(self) -> usize { + let h = unsafe { CGDisplayPixelsHigh(self.0) }; + let s = self.scale(); + if s > 1.0 { + ((h as f64) * s).round() as usize + } else { + h + } + } + + pub fn is_builtin(self) -> bool { + unsafe { CGDisplayIsBuiltin(self.0) != 0 } + } + + pub fn is_primary(self) -> bool { + unsafe { CGDisplayIsMain(self.0) != 0 } + } + + pub fn is_active(self) -> bool { + unsafe { CGDisplayIsActive(self.0) != 0 } + } + + pub fn is_online(self) -> bool { + unsafe { CGDisplayIsOnline(self.0) != 0 } + } + + pub fn scale(self) -> f64 { + let s = unsafe { BackingScaleFactor(self.0) as _ }; + if s > 1. { + let enable_retina = super::ENABLE_RETINA.lock().unwrap().clone(); + if enable_retina { + return s; + } + } + 1. + } + + pub fn bounds(self) -> CGRect { + unsafe { CGDisplayBounds(self.0) } + } +} diff --git a/vendor/rustdesk/libs/scrap/src/quartz/ffi.rs b/vendor/rustdesk/libs/scrap/src/quartz/ffi.rs new file mode 100644 index 0000000..c55bc8c --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/quartz/ffi.rs @@ -0,0 +1,241 @@ +#![allow(dead_code)] + +use block::RcBlock; +use hbb_common::libc::c_void; + +pub type CGDisplayStreamRef = *mut c_void; +pub type CFDictionaryRef = *mut c_void; +pub type CFBooleanRef = *mut c_void; +pub type CFNumberRef = *mut c_void; +pub type CFStringRef = *mut c_void; +pub type CGDisplayStreamUpdateRef = *mut c_void; +pub type IOSurfaceRef = *mut c_void; +pub type DispatchQueue = *mut c_void; +pub type DispatchQueueAttr = *mut c_void; +pub type CFAllocatorRef = *mut c_void; + +#[repr(C)] +pub struct CFDictionaryKeyCallBacks { + callbacks: [usize; 5], + version: i32, +} + +#[repr(C)] +pub struct CFDictionaryValueCallBacks { + callbacks: [usize; 4], + version: i32, +} + +macro_rules! pixel_format { + ($a:expr, $b:expr, $c:expr, $d:expr) => { + ($a as i32) << 24 | ($b as i32) << 16 | ($c as i32) << 8 | ($d as i32) + }; +} + +pub const SURFACE_LOCK_READ_ONLY: u32 = 0x0000_0001; +pub const SURFACE_LOCK_AVOID_SYNC: u32 = 0x0000_0002; + +pub fn cfbool(x: bool) -> CFBooleanRef { + unsafe { + if x { + kCFBooleanTrue + } else { + kCFBooleanFalse + } + } +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum CGDisplayStreamFrameStatus { + /// A new frame was generated. + FrameComplete = 0, + /// A new frame was not generated because the display did not change. + FrameIdle = 1, + /// A new frame was not generated because the display has gone blank. + FrameBlank = 2, + /// The display stream was stopped. + Stopped = 3, + #[doc(hidden)] + __Nonexhaustive, +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum CFNumberType { + /* Fixed-width types */ + SInt8 = 1, + SInt16 = 2, + SInt32 = 3, + SInt64 = 4, + Float32 = 5, + Float64 = 6, + /* 64-bit IEEE 754 */ + /* Basic C types */ + Char = 7, + Short = 8, + Int = 9, + Long = 10, + LongLong = 11, + Float = 12, + Double = 13, + /* Other */ + CFIndex = 14, + NSInteger = 15, + CGFloat = 16, +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +#[must_use] +pub enum CGError { + Success = 0, + Failure = 1000, + IllegalArgument = 1001, + InvalidConnection = 1002, + InvalidContext = 1003, + CannotComplete = 1004, + NotImplemented = 1006, + RangeCheck = 1007, + TypeCheck = 1008, + InvalidOperation = 1010, + NoneAvailable = 1011, + #[doc(hidden)] + __Nonexhaustive, +} + +#[repr(i32)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum PixelFormat { + /// Packed Little Endian ARGB8888 + Argb8888 = pixel_format!('B', 'G', 'R', 'A'), + /// Packed Little Endian ARGB2101010 + Argb2101010 = pixel_format!('l', '1', '0', 'r'), + /// 2-plane "video" range YCbCr 4:2:0 + YCbCr420Video = pixel_format!('4', '2', '0', 'v'), + /// 2-plane "full" range YCbCr 4:2:0 + YCbCr420Full = pixel_format!('4', '2', '0', 'f'), + #[doc(hidden)] + __Nonexhaustive, +} + +pub type CGDisplayStreamFrameAvailableHandler = *const c_void; + +pub type FrameAvailableHandler = RcBlock< + ( + CGDisplayStreamFrameStatus, // status + u64, // displayTime + IOSurfaceRef, // frameSurface + CGDisplayStreamUpdateRef, // updateRef + ), + (), +>; + +#[cfg(target_pointer_width = "64")] +pub type CGFloat = f64; +#[cfg(not(target_pointer_width = "64"))] +pub type CGFloat = f32; +#[repr(C)] +pub struct CGPoint { + pub x: CGFloat, + pub y: CGFloat, +} +#[repr(C)] +pub struct CGSize { + pub width: CGFloat, + pub height: CGFloat, +} +#[repr(C)] +pub struct CGRect { + pub origin: CGPoint, + pub size: CGSize, +} + +#[link(name = "System", kind = "dylib")] +#[link(name = "CoreGraphics", kind = "framework")] +#[link(name = "CoreFoundation", kind = "framework")] +#[link(name = "IOSurface", kind = "framework")] +extern "C" { + // CoreGraphics + + pub static kCGDisplayStreamShowCursor: CFStringRef; + pub static kCGDisplayStreamPreserveAspectRatio: CFStringRef; + pub static kCGDisplayStreamMinimumFrameTime: CFStringRef; + pub static kCGDisplayStreamQueueDepth: CFStringRef; + + pub fn CGDisplayStreamCreateWithDispatchQueue( + display: u32, + output_width: usize, + output_height: usize, + pixel_format: PixelFormat, + properties: CFDictionaryRef, + queue: DispatchQueue, + handler: CGDisplayStreamFrameAvailableHandler, + ) -> CGDisplayStreamRef; + + pub fn CGDisplayStreamStart(displayStream: CGDisplayStreamRef) -> CGError; + + pub fn CGDisplayStreamStop(displayStream: CGDisplayStreamRef) -> CGError; + + pub fn CGMainDisplayID() -> u32; + pub fn CGDisplayPixelsWide(display: u32) -> usize; + pub fn CGDisplayPixelsHigh(display: u32) -> usize; + + pub fn CGGetOnlineDisplayList( + max_displays: u32, + online_displays: *mut u32, + display_count: *mut u32, + ) -> CGError; + + pub fn CGDisplayIsBuiltin(display: u32) -> i32; + pub fn CGDisplayIsMain(display: u32) -> i32; + pub fn CGDisplayIsActive(display: u32) -> i32; + pub fn CGDisplayIsOnline(display: u32) -> i32; + + pub fn CGDisplayBounds(display: u32) -> CGRect; + pub fn BackingScaleFactor(display: u32) -> f32; + + // IOSurface + + pub fn IOSurfaceGetAllocSize(buffer: IOSurfaceRef) -> usize; + pub fn IOSurfaceGetBaseAddress(buffer: IOSurfaceRef) -> *mut c_void; + pub fn IOSurfaceIncrementUseCount(buffer: IOSurfaceRef); + pub fn IOSurfaceDecrementUseCount(buffer: IOSurfaceRef); + pub fn IOSurfaceLock(buffer: IOSurfaceRef, options: u32, seed: *mut u32) -> i32; + pub fn IOSurfaceUnlock(buffer: IOSurfaceRef, options: u32, seed: *mut u32) -> i32; + pub fn IOSurfaceGetBaseAddressOfPlane(buffer: IOSurfaceRef, index: usize) -> *mut c_void; + pub fn IOSurfaceGetBytesPerRowOfPlane(buffer: IOSurfaceRef, index: usize) -> usize; + + // Dispatch + + pub fn dispatch_queue_create(label: *const i8, attr: DispatchQueueAttr) -> DispatchQueue; + + pub fn dispatch_release(object: DispatchQueue); + + // Core Foundation + + pub static kCFTypeDictionaryKeyCallBacks: CFDictionaryKeyCallBacks; + pub static kCFTypeDictionaryValueCallBacks: CFDictionaryValueCallBacks; + + // EVEN THE BOOLEANS ARE REFERENCES. + pub static kCFBooleanTrue: CFBooleanRef; + pub static kCFBooleanFalse: CFBooleanRef; + + pub fn CFNumberCreate( + allocator: CFAllocatorRef, + theType: CFNumberType, + valuePtr: *const c_void, + ) -> CFNumberRef; + + pub fn CFDictionaryCreate( + allocator: CFAllocatorRef, + keys: *const *mut c_void, + values: *const *mut c_void, + numValues: i64, + keyCallBacks: *const CFDictionaryKeyCallBacks, + valueCallBacks: *const CFDictionaryValueCallBacks, + ) -> CFDictionaryRef; + + pub fn CFRetain(cf: *const c_void); + pub fn CFRelease(cf: *const c_void); +} diff --git a/vendor/rustdesk/libs/scrap/src/quartz/frame.rs b/vendor/rustdesk/libs/scrap/src/quartz/frame.rs new file mode 100644 index 0000000..0886e63 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/quartz/frame.rs @@ -0,0 +1,71 @@ +use std::{ops, ptr, slice}; + +use super::ffi::*; + +pub struct Frame { + surface: IOSurfaceRef, + inner: &'static [u8], + bgra: Vec, + bgra_stride: usize, +} + +impl Frame { + pub unsafe fn new(surface: IOSurfaceRef) -> Frame { + CFRetain(surface); + IOSurfaceIncrementUseCount(surface); + + IOSurfaceLock(surface, SURFACE_LOCK_READ_ONLY, ptr::null_mut()); + + let inner = slice::from_raw_parts( + IOSurfaceGetBaseAddress(surface) as *const u8, + IOSurfaceGetAllocSize(surface), + ); + + Frame { + surface, + inner, + bgra: Vec::new(), + bgra_stride: 0, + } + } + + #[inline] + pub fn inner(&self) -> &[u8] { + self.inner + } + + pub fn stride(&self) -> usize { + self.bgra_stride + } + + pub fn surface_to_bgra<'a>(&'a mut self, h: usize) { + unsafe { + let plane0 = IOSurfaceGetBaseAddressOfPlane(self.surface, 0); + self.bgra_stride = IOSurfaceGetBytesPerRowOfPlane(self.surface, 0); + self.bgra.resize(self.bgra_stride * h, 0); + std::ptr::copy_nonoverlapping( + plane0 as _, + self.bgra.as_mut_ptr(), + self.bgra_stride * h, + ); + } + } +} + +impl ops::Deref for Frame { + type Target = [u8]; + fn deref<'a>(&'a self) -> &'a [u8] { + &self.bgra + } +} + +impl Drop for Frame { + fn drop(&mut self) { + unsafe { + IOSurfaceUnlock(self.surface, SURFACE_LOCK_READ_ONLY, ptr::null_mut()); + + IOSurfaceDecrementUseCount(self.surface); + CFRelease(self.surface); + } + } +} diff --git a/vendor/rustdesk/libs/scrap/src/quartz/mod.rs b/vendor/rustdesk/libs/scrap/src/quartz/mod.rs new file mode 100644 index 0000000..c102032 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/quartz/mod.rs @@ -0,0 +1,17 @@ +pub use self::capturer::Capturer; +pub use self::config::Config; +pub use self::display::Display; +pub use self::ffi::{CGError, PixelFormat}; +pub use self::frame::Frame; + +mod capturer; +mod config; +mod display; +pub mod ffi; +mod frame; + +use std::sync::{Arc, Mutex}; + +lazy_static::lazy_static! { + pub static ref ENABLE_RETINA: Arc> = Arc::new(Mutex::new(true)); +} diff --git a/vendor/rustdesk/libs/scrap/src/wayland.rs b/vendor/rustdesk/libs/scrap/src/wayland.rs new file mode 100644 index 0000000..341f2b8 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/wayland.rs @@ -0,0 +1,6 @@ +pub mod capturable; +pub mod pipewire; +pub mod display; +mod screencast_portal; +mod request_portal; +pub mod remote_desktop_portal; diff --git a/vendor/rustdesk/libs/scrap/src/wayland/capturable.rs b/vendor/rustdesk/libs/scrap/src/wayland/capturable.rs new file mode 100644 index 0000000..070b667 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/wayland/capturable.rs @@ -0,0 +1,60 @@ +use std::boxed::Box; +use std::error::Error; + +pub enum PixelProvider<'a> { + // 8 bits per color + RGB(usize, usize, &'a [u8]), + RGB0(usize, usize, &'a [u8]), + BGR0(usize, usize, &'a [u8]), + // width, height, stride + BGR0S(usize, usize, usize, &'a [u8]), + NONE, +} + +impl<'a> PixelProvider<'a> { + pub fn size(&self) -> (usize, usize) { + match self { + PixelProvider::RGB(w, h, _) => (*w, *h), + PixelProvider::RGB0(w, h, _) => (*w, *h), + PixelProvider::BGR0(w, h, _) => (*w, *h), + PixelProvider::BGR0S(w, h, _, _) => (*w, *h), + PixelProvider::NONE => (0, 0), + } + } +} + +pub trait Recorder { + fn capture(&mut self, timeout_ms: u64) -> Result, Box>; +} + +pub trait BoxCloneCapturable { + fn box_clone(&self) -> Box; +} + +impl BoxCloneCapturable for T +where + T: Clone + Capturable + 'static, +{ + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +pub trait Capturable: Send + BoxCloneCapturable { + /// Name of the Capturable, for example the window title, if it is a window. + fn name(&self) -> String; + /// Return x, y, width, height of the Capturable as floats relative to the absolute size of the + /// screen. For example x=0.5, y=0.0, width=0.5, height=1.0 means the right half of the screen. + fn geometry_relative(&self) -> Result<(f64, f64, f64, f64), Box>; + /// Callback that is called right before input is simulated. + /// Useful to focus the window on input. + fn before_input(&mut self) -> Result<(), Box>; + /// Return a Recorder that can record the current capturable. + fn recorder(&self, capture_cursor: bool) -> Result, Box>; +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.box_clone() + } +} diff --git a/vendor/rustdesk/libs/scrap/src/wayland/display.rs b/vendor/rustdesk/libs/scrap/src/wayland/display.rs new file mode 100644 index 0000000..a5c9374 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/wayland/display.rs @@ -0,0 +1,256 @@ +use hbb_common::regex::Regex; +use lazy_static::lazy_static; +use std::sync::Mutex; +use std::{ + process::{Command, Output, Stdio}, + sync::Arc, + time::{Duration, Instant}, +}; +use tracing::warn; + +use hbb_common::platform::linux::{get_wayland_displays, WaylandDisplayInfo}; + +lazy_static! { + static ref DISPLAYS: Mutex>> = Mutex::new(None); +} + +const COMMAND_TIMEOUT: Duration = Duration::from_millis(1000); + +pub struct Displays { + pub primary: usize, + pub displays: Vec, +} + +// We need this helper to run commands with a timeout, as some commands may hang. +// `kscreen-doctor -o` is known to hang when: +// 1. On Archlinux, Both GNOME and KDE Plasma are installed. +// 2. Run this command in a GNOME session. +fn run_with_timeout( + program: &str, + args: &[&str], + timeout: Duration, + label: &str, +) -> Option { + let mut child = Command::new(program) + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .ok()?; + + let start = Instant::now(); + loop { + if let Ok(Some(_)) = child.try_wait() { + break; + } + if start.elapsed() >= timeout { + warn!("{} command timed out after {:?}", label, timeout); + if let Err(e) = child.kill() { + warn!("Failed to kill child process for '{}': {}", label, e); + } + if let Err(e) = child.wait() { + warn!("Failed to wait for child process for '{}': {}", label, e); + } + return None; + } + std::thread::sleep(Duration::from_millis(30)); + } + + match child.wait_with_output() { + Ok(output) => { + if !output.status.success() { + warn!("{} command failed with status: {}", label, output.status); + return None; + } + Some(output) + } + Err(_) => None, + } +} + +// There are some limitations with xrandr method: +// 1. It only works when XWayland is running. +// 2. The distro may not have xrandr installed by default. +// 3. xrandr may not report "primary" in its output. eg. openSUSE Leap 15.6 KDE Plasma. +fn try_xrandr_primary() -> Option { + let output = Command::new("xrandr").output().ok()?; + if !output.status.success() { + return None; + } + + let text = String::from_utf8_lossy(&output.stdout); + for line in text.lines() { + if line.contains("primary") && line.contains("connected") { + if let Some(name) = line.split_whitespace().next() { + return Some(name.to_string()); + } + } + } + None +} + +fn try_kscreen_primary() -> Option { + if !hbb_common::platform::linux::is_kde_session() { + return None; + } + + let output = run_with_timeout( + "kscreen-doctor", + &["-o"], + COMMAND_TIMEOUT, + "kscreen-doctor -o", + )?; + if !output.status.success() { + return None; + } + + let text = String::from_utf8_lossy(&output.stdout); + + // Remove ANSI color codes + let re_ansi = Regex::new(r"\x1b\[[0-9;]*m").ok()?; + let clean_text = re_ansi.replace_all(&text, ""); + + // Split the text into blocks, each starting with "Output:". + // The first element of the split will be empty, so we skip it. + for block in clean_text.split("Output:").skip(1) { + // Check if this block describes the primary monitor. + if block.contains("priority 1") { + // The monitor name is the second piece of text in the block, after the ID. + // e.g., " 1 eDP-1 enabled..." -> "eDP-1" + if let Some(name) = block.split_whitespace().nth(1) { + return Some(name.to_string()); + } + } + } + + None +} + +fn try_gdbus_primary() -> Option { + let output = run_with_timeout( + "gdbus", + &[ + "call", + "--session", + "--dest", + "org.gnome.Mutter.DisplayConfig", + "--object-path", + "/org/gnome/Mutter/DisplayConfig", + "--method", + "org.gnome.Mutter.DisplayConfig.GetCurrentState", + ], + COMMAND_TIMEOUT, + "gdbus DisplayConfig.GetCurrentState", + )?; + + if !output.status.success() { + return None; + } + + let text = String::from_utf8_lossy(&output.stdout); + + // Match logical monitor entries with primary=true + // Pattern: (x, y, scale, transform, true, [('connector-name', ...), ...], ...) + // Use regex to find entries where 5th field is true, then extract connector name + // Example matched text: "(0, 0, 1.5, 0, true, [('HDMI-1', 'MHH', 'Monitor', '0x00000000')], ...)" + let re = Regex::new(r"\([^()]*,\s*true,\s*\[\('([^']+)'").ok()?; + + if let Some(captures) = re.captures(&text) { + return captures.get(1).map(|m| m.as_str().to_string()); + } + + None +} + +fn get_primary_monitor() -> Option { + try_xrandr_primary() + .or_else(try_kscreen_primary) + .or_else(try_gdbus_primary) +} + +pub fn get_displays() -> Arc { + let mut lock = DISPLAYS.lock().unwrap(); + match lock.as_ref() { + Some(displays) => displays.clone(), + None => match get_wayland_displays() { + Ok(displays) => { + let mut primary_index = None; + if let Some(name) = get_primary_monitor() { + for (i, display) in displays.iter().enumerate() { + if display.name == name { + primary_index = Some(i); + break; + } + } + }; + if primary_index.is_none() { + for (i, display) in displays.iter().enumerate() { + if display.x == 0 && display.y == 0 { + primary_index = Some(i); + break; + } + } + } + let displays = Arc::new(Displays { + primary: primary_index.unwrap_or(0), + displays, + }); + *lock = Some(displays.clone()); + displays + } + Err(err) => { + warn!("Failed to get wayland displays: {}", err); + Arc::new(Displays { + primary: 0, + displays: Vec::new(), + }) + } + }, + } +} + +#[inline] +pub fn clear_wayland_displays_cache() { + let _ = DISPLAYS.lock().unwrap().take(); +} + +// Return (min_x, max_x, min_y, max_y) +pub fn get_desktop_rect_for_uinput() -> Option<(i32, i32, i32, i32)> { + let wayland_displays = get_displays(); + let displays = &wayland_displays.displays; + if displays.is_empty() { + return None; + } + + // For compatibility, if only one display, we use the physical size for `uinput`. + // Otherwise, we use the logical size for `uinput`. + if displays.len() == 1 { + let d = &displays[0]; + return Some((d.x, d.x + d.width, d.y, d.y + d.height)); + } + + let mut min_x = i32::MAX; + let mut min_y = i32::MAX; + let mut max_x = i32::MIN; + let mut max_y = i32::MIN; + for d in displays.iter() { + min_x = min_x.min(d.x); + min_y = min_y.min(d.y); + let size = if let Some(logical_size) = d.logical_size { + logical_size + } else { + // When `logical_size` is None, we cannot obtain the correct desktop rectangle. + // This may occur if the Wayland compositor does not provide logical size information, + // or if display information is incomplete. We fall back to physical size, which provides + // usable dimensions, but may not always be correct depending on compositor behavior. + warn!( + "Display at ({}, {}) is missing logical_size; falling back to physical size ({}, {}).", + d.x, d.y, d.width, d.height + ); + (d.width, d.height) + }; + max_x = max_x.max(d.x + size.0); + max_y = max_y.max(d.y + size.1); + } + Some((min_x, max_x, min_y, max_y)) +} diff --git a/vendor/rustdesk/libs/scrap/src/wayland/pipewire.rs b/vendor/rustdesk/libs/scrap/src/wayland/pipewire.rs new file mode 100644 index 0000000..aedf786 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/wayland/pipewire.rs @@ -0,0 +1,1528 @@ +use std::collections::HashMap; +use std::error::Error; +use std::os::unix::io::AsRawFd; +use std::process::Command; +use std::sync::{ + atomic::{AtomicBool, AtomicU8, Ordering}, + Arc, Mutex, +}; +use std::time::Duration; +use tracing::{debug, error, trace, warn}; + +use dbus::{ + arg::{OwnedFd, PropMap, RefArg, Variant}, + blocking::{Proxy, SyncConnection}, + message::{MatchRule, MessageType}, + Message, +}; + +use gstreamer as gst; +use gstreamer::prelude::*; +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 super::capturable::PixelProvider; +use super::capturable::{Capturable, Recorder}; +use super::display::{clear_wayland_displays_cache, get_displays, Displays}; +use super::remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop as remote_desktop_portal; +use super::request_portal::OrgFreedesktopPortalRequestResponse; +use super::screencast_portal::OrgFreedesktopPortalScreenCast as screencast_portal; + +lazy_static! { + pub static ref RDP_SESSION_INFO: Mutex> = Mutex::new(None); +} + +#[derive(Serialize, Deserialize)] +// For KDE Plasma only, because GNOME provides position info. +struct PipewireDisplayOffsetCache { + // We need to compare the displays, because: + // 1. On Archlinux KDE Plasma + // 2. One display, and connect, remember share choice. + // 3. Plug in another monitor. + // 4. The portal will reuse the restore token, no new share choice dialog, but the share screen is different. + // The controlling side will see the new monitor. + // All displays as one string for easy comparison + // name1-x1-y1-width1-height1;name2-x2-y2-width2-height2;... + display_key: String, + restore_token: String, + offsets: Vec<(i32, i32)>, +} + +// KDE Plasma may not provide position info +static HAS_POSITION_ATTR: AtomicBool = AtomicBool::new(false); +static IS_SERVER_RUNNING: AtomicU8 = AtomicU8::new(0); // 0: uninitialized, 1:true, 2: false + +impl PipewireDisplayOffsetCache { + fn displays_to_key(displays: &Arc) -> String { + displays + .displays + .iter() + .map(|d| format!("{}-{}-{}-{}-{}", d.name, d.x, d.y, d.width, d.height)) + .collect::>() + .join(";") + } +} + +#[inline] +pub fn close_session() { + let _ = RDP_SESSION_INFO.lock().unwrap().take(); + clear_wayland_displays_cache(); + HAS_POSITION_ATTR.store(false, Ordering::SeqCst); +} + +#[inline] +pub fn is_rdp_session_hold() -> bool { + RDP_SESSION_INFO.lock().unwrap().is_some() +} + +pub fn try_close_session() { + let mut rdp_info = RDP_SESSION_INFO.lock().unwrap(); + let mut close = false; + if let Some(rdp_info) = &*rdp_info { + // If is server running and restore token is supported, there's no need to keep the session. + if is_server_running() && rdp_info.is_support_restore_token { + close = true; + } + } + if close { + *rdp_info = None; + clear_wayland_displays_cache(); + HAS_POSITION_ATTR.store(false, Ordering::SeqCst); + } +} + +pub struct RdpSessionInfo { + pub conn: Arc, + pub streams: Vec, + pub fd: OwnedFd, + pub session: dbus::Path<'static>, + pub is_support_restore_token: bool, + pub resolution: Arc>>, +} +#[derive(Debug, Clone, Copy)] +pub struct PwStreamInfo { + pub path: u64, + source_type: u64, + position: (i32, i32), + size: (usize, usize), +} + +impl PwStreamInfo { + pub fn get_size(&self) -> (usize, usize) { + self.size + } + + pub fn get_position(&self) -> (i32, i32) { + self.position + } +} + +#[derive(Debug)] +pub struct DBusError(String); + +impl std::fmt::Display for DBusError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self(s) = self; + write!(f, "{}", s) + } +} + +impl Error for DBusError {} + +#[derive(Debug)] +pub struct GStreamerError(String); + +impl std::fmt::Display for GStreamerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self(s) = self; + write!(f, "{}", s) + } +} + +impl Error for GStreamerError {} + +#[derive(Clone)] +pub struct PipeWireCapturable { + // connection needs to be kept alive for recording + dbus_conn: Arc, + fd: OwnedFd, + path: u64, + source_type: u64, + pub primary: bool, + pub position: (i32, i32), + pub logical_size: (usize, usize), + pub physical_size: (usize, usize), +} + +impl PipeWireCapturable { + fn new( + conn: Arc, + fd: OwnedFd, + resolution: Arc>>, + stream: &PwStreamInfo, + ) -> Self { + // alternative to get screen resolution as stream.size is not always correct ex: on fractional scaling + // https://github.com/rustdesk/rustdesk/issues/6116#issuecomment-1817724244 + let physical_size = get_res(Self { + dbus_conn: conn.clone(), + fd: fd.clone(), + path: stream.path, + source_type: stream.source_type, + primary: false, + position: stream.position, + logical_size: stream.size, + physical_size: (0, 0), + }) + .unwrap_or(stream.size); + *resolution.lock().unwrap() = Some(physical_size); + Self { + dbus_conn: conn, + fd, + path: stream.path, + source_type: stream.source_type, + primary: false, + position: stream.position, + logical_size: stream.size, + physical_size, + } + } +} + +impl std::fmt::Debug for PipeWireCapturable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "PipeWireCapturable {{dbus: {}, fd: {}, path: {}, source_type: {}}}", + self.dbus_conn.unique_name(), + self.fd.as_raw_fd(), + self.path, + self.source_type + ) + } +} + +impl Capturable for PipeWireCapturable { + fn name(&self) -> String { + let type_str = match self.source_type { + 1 => "Desktop", + 2 => "Window", + _ => "Unknow", + }; + format!("Pipewire {}, path: {}", type_str, self.path) + } + + fn geometry_relative(&self) -> Result<(f64, f64, f64, f64), Box> { + Ok((0.0, 0.0, 1.0, 1.0)) + } + + fn before_input(&mut self) -> Result<(), Box> { + Ok(()) + } + + fn recorder(&self, _capture_cursor: bool) -> Result, Box> { + Ok(Box::new(PipeWireRecorder::new(self.clone())?)) + } +} + +fn get_res(capturable: PipeWireCapturable) -> Result<(usize, usize), Box> { + let rec = PipeWireRecorder::new(capturable)?; + if let Some(sample) = rec + .appsink + .try_pull_sample(gst::ClockTime::from_mseconds(300)) + { + let cap = sample + .get_caps() + .ok_or("Failed get caps")? + .get_structure(0) + .ok_or("Failed to get structure")?; + let w: i32 = cap.get_value("width")?.get_some()?; + let h: i32 = cap.get_value("height")?.get_some()?; + let w = w as usize; + let h = h as usize; + Ok((w, h)) + } else { + Err(Box::new(GStreamerError( + "Error getting screen resolution".into(), + ))) + } +} + +pub struct PipeWireRecorder { + buffer: Option>, + buffer_cropped: Vec, + pix_fmt: String, + is_cropped: bool, + pipeline: gst::Pipeline, + appsink: AppSink, + width: usize, + height: usize, + saved_raw_data: Vec, // for faster compare and copy +} + +impl PipeWireRecorder { + pub fn new(capturable: PipeWireCapturable) -> ResultType { + let pipeline = gst::Pipeline::new(None); + + let src = gst::ElementFactory::make("pipewiresrc", None)?; + src.set_property("fd", &capturable.fd.as_raw_fd())?; + src.set_property("path", &format!("{}", capturable.path))?; + src.set_property("keepalive_time", &1_000.as_raw_fd())?; + + // For some reason pipewire blocks on destruction of AppSink if this is not set to true, + // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 + src.set_property("always-copy", &true)?; + + let sink = gst::ElementFactory::make("appsink", None)?; + sink.set_property("drop", &true)?; + sink.set_property("max-buffers", &1u32)?; + + pipeline.add_many(&[&src, &sink])?; + src.link(&sink)?; + + let appsink = sink + .dynamic_cast::() + .map_err(|_| GStreamerError("Sink element is expected to be an appsink!".into()))?; + let mut caps = gst::Caps::new_empty(); + caps.merge_structure(gst::structure::Structure::new( + "video/x-raw", + &[("format", &"BGRx")], + )); + caps.merge_structure(gst::structure::Structure::new( + "video/x-raw", + &[("format", &"RGBx")], + )); + appsink.set_caps(Some(&caps)); + + // [Workaround] + // Crash may occur if there are multiple pipelines started at the same time. + // `pipeline.get_state()` can significantly reduce the probability of crashes, + // but cannot completely resolve this issue. + // Adding a short sleep period can also reduce the probability of crashes. + debug!( + "[gstreamer] Setting pipeline {} to PLAYING state...", + capturable.fd.as_raw_fd() + ); + pipeline.set_state(gst::State::Playing)?; + + // If `is_server_running()` is false, it means using remote_desktop_portal, + // which does not use multiple streams, so no need to wait for state change. + if is_server_running() { + // Wait for the state change to actually complete before proceeding. + // The 2000ms timeout for pipeline state change was chosen based on empirical testing. + let state_change = pipeline.get_state(gst::ClockTime::from_mseconds(2000)); + match state_change { + (Ok(_), gst::State::Playing, _) => { + debug!( + "[gstreamer] Pipeline {} state confirmed as PLAYING.", + capturable.fd.as_raw_fd() + ); + } + (result, state, pending) => { + warn!( + "[gstreamer] Pipeline {} state change incomplete: result={:?}, state={:?}, pending={:?}", + capturable.fd.as_raw_fd(), result, state, pending + ); + } + } + std::thread::sleep(std::time::Duration::from_millis(150)); + } + + Ok(Self { + pipeline, + appsink, + buffer: None, + pix_fmt: "".into(), + width: 0, + height: 0, + buffer_cropped: vec![], + is_cropped: false, + saved_raw_data: Vec::new(), + }) + } +} + +impl Recorder for PipeWireRecorder { + fn capture(&mut self, timeout_ms: u64) -> Result, Box> { + if let Some(sample) = self + .appsink + .try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms)) + { + let cap = sample + .get_caps() + .ok_or("Failed get caps")? + .get_structure(0) + .ok_or("Failed to get structure")?; + let w: i32 = cap.get_value("width")?.get_some()?; + let h: i32 = cap.get_value("height")?.get_some()?; + let w = w as usize; + let h = h as usize; + self.pix_fmt = cap + .get::<&str>("format")? + .ok_or("Failed to get pixel format")? + .to_string(); + + let buf = sample + .get_buffer_owned() + .ok_or_else(|| GStreamerError("Failed to get owned buffer.".into()))?; + let mut crop = buf + .get_meta::() + .map(|m| m.get_rect()); + // only crop if necessary + if Some((0, 0, w as u32, h as u32)) == crop { + crop = None; + } + let buf = buf + .into_mapped_buffer_readable() + .map_err(|_| GStreamerError("Failed to map buffer.".into()))?; + if let Err(..) = crate::would_block_if_equal(&mut self.saved_raw_data, buf.as_slice()) { + return Ok(PixelProvider::NONE); + } + let buf_size = buf.get_size(); + // BGRx is 4 bytes per pixel + if buf_size != (w * h * 4) { + // for some reason the width and height of the caps do not guarantee correct buffer + // size, so ignore those buffers, see: + // https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/985 + trace!( + "Size of mapped buffer: {} does NOT match size of capturable {}x{}@BGRx, \ + dropping it!", + buf_size, + w, + h + ); + } else { + // Copy region specified by crop into self.buffer_cropped + // TODO: Figure out if ffmpeg provides a zero copy alternative + if let Some((x_off, y_off, w_crop, h_crop)) = crop { + let x_off = x_off as usize; + let y_off = y_off as usize; + let w_crop = w_crop as usize; + let h_crop = h_crop as usize; + self.buffer_cropped.clear(); + let data = buf.as_slice(); + // BGRx is 4 bytes per pixel + self.buffer_cropped.reserve(w_crop * h_crop * 4); + for y in y_off..(y_off + h_crop) { + let i = 4 * (w * y + x_off); + self.buffer_cropped.extend(&data[i..i + 4 * w_crop]); + } + self.width = w_crop; + self.height = h_crop; + } else { + self.width = w; + self.height = h; + } + self.is_cropped = crop.is_some(); + self.buffer = Some(buf); + } + } else { + return Ok(PixelProvider::NONE); + } + if self.buffer.is_none() { + return Err(Box::new(GStreamerError("No buffer available!".into()))); + } + let buf = if self.is_cropped { + self.buffer_cropped.as_slice() + } else { + self.buffer + .as_ref() + .ok_or("Failed to get buffer as ref")? + .as_slice() + }; + match self.pix_fmt.as_str() { + "BGRx" => Ok(PixelProvider::BGR0(self.width, self.height, buf)), + "RGBx" => Ok(PixelProvider::RGB0(self.width, self.height, buf)), + _ => Err(Box::new(GStreamerError(format!( + "Unreachable! Unknown pix_fmt, {}", + &self.pix_fmt + )))), + } + } +} + +impl Drop for PipeWireRecorder { + fn drop(&mut self) { + if let Err(err) = self.pipeline.set_state(gst::State::Null) { + warn!("Failed to stop GStreamer pipeline: {}.", err); + } + // Wait for state change to complete to avoid races during PipeWire teardown. + let _ = self.pipeline.get_state(gst::ClockTime::from_mseconds(2000)); + } +} + +fn handle_response( + conn: &SyncConnection, + path: dbus::Path<'static>, + mut f: F, + failure_out: Arc, +) -> Result +where + F: FnMut( + OrgFreedesktopPortalRequestResponse, + &SyncConnection, + &Message, + ) -> Result<(), Box> + + Send + + Sync + + 'static, +{ + let mut m = MatchRule::new(); + m.path = Some(path); + m.msg_type = Some(MessageType::Signal); + m.sender = Some("org.freedesktop.portal.Desktop".into()); + m.interface = Some("org.freedesktop.portal.Request".into()); + conn.add_match(m, move |r: OrgFreedesktopPortalRequestResponse, c, m| { + debug!("Response from DBus: response: {:?}, message: {:?}", r, m); + match r.response { + 0 => {} + 1 => { + warn!("DBus response: User cancelled interaction."); + failure_out.store(true, Ordering::SeqCst); + return true; + } + c => { + warn!("DBus response: Unknown error, code: {}.", c); + failure_out.store(true, Ordering::SeqCst); + return true; + } + } + if let Err(err) = f(r, c, m) { + warn!("Error requesting screen capture via dbus: {}", err); + failure_out.store(true, Ordering::SeqCst); + } + true + }) +} + +pub fn get_portal(conn: &SyncConnection) -> Proxy<&SyncConnection> { + conn.with_proxy( + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + Duration::from_millis(1000), + ) +} + +fn streams_from_response(response: OrgFreedesktopPortalRequestResponse) -> Vec { + (move || { + Some( + response + .results + .get("streams")? + .as_iter()? + .next()? + .as_iter()? + .filter_map(|stream| { + let mut itr = stream.as_iter()?; + let path = itr.next()?.as_u64()?; + let (keys, values): (Vec<(usize, &dyn RefArg)>, Vec<(usize, &dyn RefArg)>) = + itr.next()? + .as_iter()? + .enumerate() + .partition(|(i, _)| i % 2 == 0); + let attributes = keys + .iter() + .filter_map(|(_, key)| Some(key.as_str()?.to_owned())) + .zip( + values + .iter() + .map(|(_, arg)| *arg) + .collect::>(), + ) + .collect::>(); + let mut info = PwStreamInfo { + path, + source_type: attributes + .get("source_type") + .map_or(Some(0), |v| v.as_u64())?, + position: (0, 0), + size: (0, 0), + }; + let v = attributes + .get("size")? + .as_iter()? + .filter_map(|v| { + Some( + v.as_iter()? + .map(|x| x.as_i64().unwrap_or(0)) + .collect::>(), + ) + }) + .next(); + if let Some(v) = v { + if v.len() == 2 { + info.size.0 = v[0] as _; + info.size.1 = v[1] as _; + } + } + if let Some(pos) = attributes.get("position") { + let v = pos + .as_iter()? + .filter_map(|v| { + Some( + v.as_iter()? + .map(|x| x.as_i64().unwrap_or(0)) + .collect::>(), + ) + }) + .next(); + if let Some(v) = v { + if v.len() == 2 { + info.position.0 = v[0] as _; + info.position.1 = v[1] as _; + HAS_POSITION_ATTR.store(true, Ordering::SeqCst); + } + } + } + Some(info) + }) + .collect::>(), + ) + })() + .unwrap_or_default() +} + +static mut INIT: bool = false; +const RESTORE_TOKEN: &str = "restore_token"; +const RESTORE_TOKEN_CONF_KEY: &str = "wayland-restore-token"; +const PIPEWIRE_DISPLAY_OFFSET_CONF_KEY: &str = "wayland-pipewire-display-offset"; + +pub fn get_available_cursor_modes() -> Result { + let conn = SyncConnection::new_session()?; + let portal = get_portal(&conn); + portal.available_cursor_modes() +} + +// mostly inspired by https://gitlab.gnome.org/-/snippets/39 +pub fn request_remote_desktop( + capture_cursor: bool, +) -> ResultType<( + SyncConnection, + OwnedFd, + Vec, + dbus::Path<'static>, + bool, +)> { + unsafe { + if !INIT { + gstreamer::init()?; + INIT = true; + } + } + let conn = SyncConnection::new_session()?; + let portal = get_portal(&conn); + let mut args: PropMap = HashMap::new(); + let fd: Arc>> = Arc::new(Mutex::new(None)); + let fd_res = fd.clone(); + let streams: Arc>> = Arc::new(Mutex::new(Vec::new())); + let streams_res = streams.clone(); + let failure = Arc::new(AtomicBool::new(false)); + let failure_res = failure.clone(); + let session: Arc>> = Arc::new(Mutex::new(None)); + let session_res = session.clone(); + args.insert( + "session_handle_token".to_string(), + Variant(Box::new("u1".to_string())), + ); + args.insert( + "handle_token".to_string(), + Variant(Box::new("u1".to_string())), + ); + + let mut is_support_restore_token = false; + if let Ok(version) = screencast_portal::version(&portal) { + if version >= 4 { + is_support_restore_token = true; + } + } + + // The following code may be improved. + // https://flatpak.github.io/xdg-desktop-portal/#:~:text=To%20avoid%20a%20race%20condition + // To avoid a race condition + // between the caller subscribing to the signal after receiving the reply for the method call and the signal getting emitted, + // a convention for Request object paths has been established that allows + // the caller to subscribe to the signal before making the method call. + let path; + if is_server_running() { + path = screencast_portal::create_session(&portal, args)?; + } else { + path = remote_desktop_portal::create_session(&portal, args)?; + } + handle_response( + &conn, + path, + on_create_session_response( + fd.clone(), + streams.clone(), + session.clone(), + failure.clone(), + is_support_restore_token, + capture_cursor, + ), + failure_res.clone(), + )?; + + // wait 3 minutes for user interaction + for _ in 0..1800 { + conn.process(Duration::from_millis(100))?; + // Once we got a file descriptor we are done! + if fd_res.lock().unwrap().is_some() { + break; + } + + if failure_res.load(Ordering::SeqCst) { + break; + } + } + let fd_res = fd_res.lock().unwrap(); + let streams_res = streams_res.lock().unwrap(); + let session_res = session_res.lock().unwrap(); + + if let Some(fd_res) = fd_res.clone() { + if let Some(session) = session_res.clone() { + if !streams_res.is_empty() { + return Ok(( + conn, + fd_res, + streams_res.clone(), + session, + is_support_restore_token, + )); + } + } + } + bail!("Failed to obtain screen capture. You may need to upgrade the PipeWire library for better compatibility. Please check https://github.com/rustdesk/rustdesk/issues/8600#issuecomment-2254720954 for more details.") +} + +fn on_create_session_response( + fd: Arc>>, + streams: Arc>>, + session: Arc>>>, + failure: Arc, + is_support_restore_token: bool, + capture_cursor: bool, +) -> impl Fn( + OrgFreedesktopPortalRequestResponse, + &SyncConnection, + &dbus::Message, +) -> Result<(), Box> { + move |r: OrgFreedesktopPortalRequestResponse, c, _| { + let ses: dbus::Path = r + .results + .get("session_handle") + .ok_or_else(|| { + DBusError(format!( + "Failed to obtain session_handle from response: {:?}", + r + )) + })? + .as_str() + .ok_or_else(|| DBusError("Failed to convert session_handle to string.".into()))? + .to_string() + .into(); + + let mut session = match session.lock() { + Ok(session) => session, + Err(_) => return Err(Box::new(DBusError("Failed to lock session.".into()))), + }; + session.replace(ses.clone()); + + let portal = get_portal(c); + let mut args: PropMap = HashMap::new(); + // See `is_server_running()` to understand the following code. + if is_server_running() { + if is_support_restore_token { + let restore_token = config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY); + if !restore_token.is_empty() { + args.insert(RESTORE_TOKEN.to_string(), Variant(Box::new(restore_token))); + } + // persist_mode may be configured by the user. + args.insert("persist_mode".to_string(), Variant(Box::new(2u32))); + } + args.insert( + "handle_token".to_string(), + Variant(Box::new("u3".to_string())), + ); + // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html + if is_server_running() { + args.insert("multiple".into(), Variant(Box::new(true))); + } + args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32))); + + if capture_cursor { + get_available_cursor_modes().ok().map(|modes| { + if modes & 0x2 != 0 { + args.insert("cursor_mode".to_string(), Variant(Box::new(2u32))); + } + }); + } + + let path = portal.select_sources(ses.clone(), args)?; + handle_response( + c, + path, + on_select_sources_response( + fd.clone(), + streams.clone(), + failure.clone(), + ses, + is_support_restore_token, + ), + failure.clone(), + )?; + } else { + // TODO: support persist_mode for remote_desktop_portal + // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html + + args.insert( + "handle_token".to_string(), + Variant(Box::new("u2".to_string())), + ); + args.insert("types".to_string(), Variant(Box::new(7u32))); + + let path = portal.select_devices(ses.clone(), args)?; + handle_response( + c, + path, + on_select_devices_response( + fd.clone(), + streams.clone(), + failure.clone(), + ses, + is_support_restore_token, + ), + failure.clone(), + )?; + } + + Ok(()) + } +} + +fn on_select_devices_response( + fd: Arc>>, + streams: Arc>>, + failure: Arc, + session: dbus::Path<'static>, + is_support_restore_token: bool, +) -> impl Fn( + OrgFreedesktopPortalRequestResponse, + &SyncConnection, + &dbus::Message, +) -> Result<(), Box> { + move |_: OrgFreedesktopPortalRequestResponse, c, _| { + let portal = get_portal(c); + let mut args: PropMap = HashMap::new(); + args.insert( + "handle_token".to_string(), + Variant(Box::new("u3".to_string())), + ); + // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html + if is_server_running() { + args.insert("multiple".into(), Variant(Box::new(true))); + } + args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32))); + + let session = session.clone(); + let path = portal.select_sources(session.clone(), args)?; + handle_response( + c, + path, + on_select_sources_response( + fd.clone(), + streams.clone(), + failure.clone(), + session, + is_support_restore_token, + ), + failure.clone(), + )?; + + Ok(()) + } +} + +fn on_select_sources_response( + fd: Arc>>, + streams: Arc>>, + failure: Arc, + session: dbus::Path<'static>, + is_support_restore_token: bool, +) -> impl Fn( + OrgFreedesktopPortalRequestResponse, + &SyncConnection, + &dbus::Message, +) -> Result<(), Box> { + move |_: OrgFreedesktopPortalRequestResponse, c, _| { + let portal = get_portal(c); + let mut args: PropMap = HashMap::new(); + args.insert( + "handle_token".to_string(), + Variant(Box::new("u4".to_string())), + ); + let path; + if is_server_running() { + path = screencast_portal::start(&portal, session.clone(), "", args)?; + } else { + path = remote_desktop_portal::start(&portal, session.clone(), "", args)?; + } + handle_response( + c, + path, + on_start_response( + fd.clone(), + streams.clone(), + session.clone(), + is_support_restore_token, + ), + failure.clone(), + )?; + + Ok(()) + } +} + +fn on_start_response( + fd: Arc>>, + streams: Arc>>, + session: dbus::Path<'static>, + is_support_restore_token: bool, +) -> impl Fn( + OrgFreedesktopPortalRequestResponse, + &SyncConnection, + &dbus::Message, +) -> Result<(), Box> { + move |r: OrgFreedesktopPortalRequestResponse, c, _| { + let portal = get_portal(c); + // See `is_server_running()` to understand the following code. + if is_server_running() { + if is_support_restore_token { + if let Some(restore_token) = r.results.get(RESTORE_TOKEN) { + if let Some(restore_token) = restore_token.as_str() { + config::LocalConfig::set_option( + RESTORE_TOKEN_CONF_KEY.to_owned(), + restore_token.to_owned(), + ); + } + } + } + } + + streams + .clone() + .lock() + .unwrap() + .append(&mut streams_from_response(r)); + fd.clone() + .lock() + .unwrap() + .replace(portal.open_pipe_wire_remote(session.clone(), HashMap::new())?); + + Ok(()) + } +} + +pub fn get_capturables() -> Result, Box> { + let mut rdp_connection = match RDP_SESSION_INFO.lock() { + Ok(conn) => conn, + Err(err) => return Err(Box::new(err)), + }; + + if rdp_connection.is_none() { + let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop(false)?; + let conn = Arc::new(conn); + + let rdp_info = RdpSessionInfo { + conn, + streams, + fd, + session, + is_support_restore_token, + resolution: Arc::new(Mutex::new(None)), + }; + *rdp_connection = Some(rdp_info); + } + + let rdp_info = match rdp_connection.as_mut() { + Some(res) => res, + None => { + return Err(Box::new(DBusError("RDP response is None.".into()))); + } + }; + + Ok(rdp_info + .streams + .iter() + .map(|s| { + PipeWireCapturable::new( + rdp_info.conn.clone(), + rdp_info.fd.clone(), + rdp_info.resolution.clone(), + s, + ) + }) + .collect()) +} + +// If `is_server_running()` is true, then `screencast_portal::start` is called. +// Otherwise, `remote_desktop_portal::start` is called. +// +// If `is_server_running()` is true, `--service` process is running, +// then we can use uinput as the input method. +// Otherwise, we have to use remote_desktop_portal's input method. +// +// `screencast_portal` supports restore_token and persist_mode if the version is greater than or equal to 4. +// `remote_desktop_portal` does not support restore_token and persist_mode. +pub(crate) fn is_server_running() -> bool { + let v = IS_SERVER_RUNNING.load(Ordering::SeqCst); + if v > 0 { + return v == 1; + } + + let app_name = config::APP_NAME.read().unwrap().clone().to_lowercase(); + let output = match Command::new(CMD_SH.as_str()) + .arg("-c") + .arg(&format!("ps aux | grep {}", app_name)) + .output() + { + Ok(output) => output, + Err(_) => { + return false; + } + }; + + let output_str = String::from_utf8_lossy(&output.stdout); + let is_running = output_str.contains(&format!("{} --server", app_name)); + IS_SERVER_RUNNING.store(if is_running { 1 } else { 2 }, Ordering::SeqCst); + is_running +} + +// The logical size reported by portal may be different from the size reported by `get_displays()`. +// So we need to use the workaround here. +// 1. openSUSE, KDE Plasma +// 2. Kubuntu 24.04 TLS, after running `sudo apt install plasma-workspace-wayland` +// Maybe it's a bug, and we can remove this workaround in the future. +pub fn try_fix_logical_size(shared_displays: &mut Vec) { + if !is_server_running() { + return; + } + + let wayland_displays = get_displays(); + if wayland_displays.displays.is_empty() { + return; + } + + for sd in shared_displays.iter_mut() { + if let crate::Display::WAYLAND(d) = sd { + let capturable = &mut d.0; + for wd in wayland_displays.displays.iter() { + if capturable.position.0 == wd.x && capturable.position.1 == wd.y { + if let Some(logical_size) = wd.logical_size { + if capturable.physical_size.0 != wd.width as usize + || capturable.physical_size.1 != wd.height as usize + { + // If "Full Workspace" is selected in the portal dialog, + // the physical size reported by portal may not match the display info. + debug!( + "Physical size of capturable ({:?}) does not match display info: ({:?}) - ({:?}). Skipping logical size fix.", + capturable.position, + capturable.physical_size, + (wd.width as usize, wd.height as usize) + ); + break; + } + + if capturable.logical_size.0 != logical_size.0 as usize + || capturable.logical_size.1 != logical_size.1 as usize + { + warn!( + "Fixing logical size of capturable from {:?} to {:?} based on display info {:?}.", + capturable.logical_size, + logical_size, + wd + ); + capturable.logical_size = + (logical_size.0 as usize, logical_size.1 as usize); + } + } + break; + } + } + } + } +} + +pub fn fill_displays( + mouse_move_to: impl Fn(i32, i32), + get_cursor_pos: fn() -> Option<(i32, i32)>, + shared_displays: &mut Vec, +) -> ResultType<()> { + if !is_server_running() { + return Ok(()); + } + + let mut rdp_connection = RDP_SESSION_INFO.lock().unwrap(); + let rdp_info = match rdp_connection.as_mut() { + Some(res) => res, + None => { + // Unreachable + bail!("RDP session info is None when filling display positions."); + } + }; + + let all_displays = get_displays(); + if !HAS_POSITION_ATTR.load(Ordering::SeqCst) { + if all_displays.displays.len() > 1 { + debug!("Multiple Wayland displays detected, adjusting stream positions accordingly."); + try_fill_positions( + mouse_move_to, + get_cursor_pos, + &all_displays, + shared_displays, + &mut rdp_info.streams, + )?; + } + HAS_POSITION_ATTR.store(true, Ordering::SeqCst); + } + + if all_displays.displays.len() > 1 { + sort_streams(&all_displays, shared_displays, &mut rdp_info.streams); + } + + shared_displays.iter_mut().next().map(|d| { + if let crate::Display::WAYLAND(d) = d { + d.0.primary = true; + } + }); + + Ok(()) +} + +fn try_fill_positions( + mouse_move_to: impl Fn(i32, i32), + get_cursor_pos: fn() -> Option<(i32, i32)>, + displays: &Arc, + shared_displays: &mut Vec, + streams: &mut Vec, +) -> ResultType<()> { + let pipewire_display_offset = config::LocalConfig::get_option(PIPEWIRE_DISPLAY_OFFSET_CONF_KEY); + if !pipewire_display_offset.is_empty() { + if try_fill_positions_from_cache( + pipewire_display_offset, + displays, + shared_displays, + streams, + ) { + return Ok(()); + } + config::LocalConfig::set_option(PIPEWIRE_DISPLAY_OFFSET_CONF_KEY.to_owned(), "".to_owned()); + } + + let mut multi_matched_indices = Vec::new(); + for (i, sd) in shared_displays.iter_mut().enumerate() { + if let crate::Display::WAYLAND(d) = sd { + let capturable = &mut d.0; + let mut match_count = 0; + for wd in displays.displays.iter() { + if capturable.physical_size.0 == wd.width as usize + && capturable.physical_size.1 == wd.height as usize + { + capturable.position = (wd.x, wd.y); + if let Some(pw_stream) = streams.get_mut(i) { + pw_stream.position = (wd.x, wd.y); + } + match_count += 1; + } + } + if match_count == 0 { + warn!( + "No matching display found for capturable with size {:?}.", + capturable.physical_size + ); + } else if match_count > 1 { + multi_matched_indices.push(i); + } + } + } + + if !multi_matched_indices.is_empty() { + fill_multi_matched_positions( + mouse_move_to, + get_cursor_pos, + displays, + shared_displays, + streams, + multi_matched_indices, + )?; + } + + save_positions_to_cache(displays, shared_displays); + Ok(()) +} + +fn try_fill_positions_from_cache( + cache_str: String, + displays: &Arc, + shared_displays: &mut Vec, + streams: &mut Vec, +) -> bool { + let Ok(cache) = serde_json::from_str::(&cache_str) else { + return false; + }; + + if cache.offsets.len() != shared_displays.len() { + return false; + } + + let display_key = PipewireDisplayOffsetCache::displays_to_key(displays); + if cache.display_key != display_key { + return false; + } + + let restore_token = config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY); + if cache.restore_token != restore_token { + return false; + } + + for (i, sd) in shared_displays.iter_mut().enumerate() { + if let crate::Display::WAYLAND(d) = sd { + let capturable = &mut d.0; + if let Some((x_off, y_off)) = cache.offsets.get(i) { + capturable.position = (*x_off, *y_off); + if let Some(pw_stream) = streams.get_mut(i) { + pw_stream.position = (*x_off, *y_off); + } + } + } + } + true +} + +fn save_positions_to_cache(displays: &Arc, shared_displays: &Vec) { + let restore_token = config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY); + if restore_token.is_empty() { + return; + } + + let mut offsets = Vec::new(); + for sd in shared_displays.iter() { + if let crate::Display::WAYLAND(d) = sd { + let capturable = &d.0; + offsets.push((capturable.position.0, capturable.position.1)); + } + } + + let display_key = PipewireDisplayOffsetCache::displays_to_key(displays); + let cache = PipewireDisplayOffsetCache { + display_key, + restore_token, + offsets, + }; + + if let Ok(s) = serde_json::to_string(&cache) { + config::LocalConfig::set_option(PIPEWIRE_DISPLAY_OFFSET_CONF_KEY.to_owned(), s); + } +} + +fn compare_left_up_corner(w: usize, d1: &[u8], d2: &[u8]) -> bool { + if w == 0 { + return false; + } + if d1.len() != d2.len() { + return false; + } + let bpp = 4; // BGR0/RGB0 + let stride = w.saturating_mul(bpp); + if stride == 0 || d1.len() < stride || d2.len() < stride { + return false; + } + let h = d1.len() / stride; + if h == 0 { + return false; + } + + let roi_w = std::cmp::min(36, w); + let roi_h = std::cmp::min(36, h); + let mut diff_px = 0usize; + let total_px = roi_w * roi_h; + // Minimum number of differing pixels required to consider images different. + const MIN_DIFF_PIXELS: usize = 8; + // Divisor for threshold calculation: allows up to 1/8 of ROI pixels to differ before returning true. + const DIFF_THRESHOLD_DIVISOR: usize = 8; + let threshold = std::cmp::max(MIN_DIFF_PIXELS, total_px / DIFF_THRESHOLD_DIVISOR); + + for y in 0..roi_h { + let row_off = y * stride; + for x in 0..roi_w { + let i = row_off + x * bpp; + let a = &d1[i..i + bpp]; + let b = &d2[i..i + bpp]; + if a != b { + diff_px += 1; + if diff_px >= threshold { + return true; + } + } + } + } + false +} + +fn fill_multi_matched_positions( + mouse_move_to: impl Fn(i32, i32), + get_cursor_pos: fn() -> Option<(i32, i32)>, + displays: &Arc, + shared_displays: &mut Vec, + streams: &mut Vec, + multi_matched_indices: Vec, +) -> ResultType<()> { + debug!( + "Multiple capturables ({:?}) match the same display size, attempting to disambiguate positions.", + &multi_matched_indices); + if multi_matched_indices.is_empty() { + return Ok(()); + } + + let is_support_embeded_cursor = get_available_cursor_modes() + .ok() + .map(|modes| modes & 0x2 != 0) + .unwrap_or(false); + if is_support_embeded_cursor { + fill_multi_matched_positions_cursor( + mouse_move_to, + get_cursor_pos, + displays, + shared_displays, + streams, + multi_matched_indices, + )?; + } + + Ok(()) +} + +fn mouse_move_to_( + mouse_move_to: &impl Fn(i32, i32), + get_cursor_pos: fn() -> Option<(i32, i32)>, + x: i32, + y: i32, +) { + const MOVE_MOUSE_TIMEOUT: Duration = Duration::from_millis(150); + let start = std::time::Instant::now(); + while start.elapsed() < MOVE_MOUSE_TIMEOUT { + mouse_move_to(x, y); + std::thread::sleep(Duration::from_millis(20)); + if let Some((x1, y1)) = get_cursor_pos() { + if x1 == x && y1 == y { + return; + } + } + } + warn!( + "Failed to move mouse to ({}, {}) within timeout: {:?}.", + x, y, &MOVE_MOUSE_TIMEOUT + ); +} + +fn fill_multi_matched_positions_cursor( + mouse_move_to: impl Fn(i32, i32), + get_cursor_pos: fn() -> Option<(i32, i32)>, + displays: &Arc, + shared_displays: &mut Vec, + streams: &mut Vec, + multi_matched_indices: Vec, +) -> ResultType<()> { + // This creates a new remote desktop session for cursor-based position detection. + // The session is temporary, used only for disambiguation, and is dropped after detection completes. + let (conn, fd, streams_with_cursor, _session, _is_support_restore_token) = + request_remote_desktop(true)?; + let conn = Arc::new(conn); + + let mut matched_indices = Vec::new(); + const CAPTURE_TIMEOUT_MS: u64 = 1_000; + for idx in multi_matched_indices { + match ( + shared_displays.get_mut(idx), + streams.get_mut(idx), + streams_with_cursor.get(idx), + ) { + (Some(crate::Display::WAYLAND(d)), Some(pw_stream), Some(pw_stream_with_cursor)) => { + // Check if only one display matches the size + let mut match_count = 0; + for (i, wd) in displays.displays.iter().enumerate() { + if matched_indices.contains(&i) { + continue; + } + if d.0.physical_size.0 == wd.width as usize + && d.0.physical_size.1 == wd.height as usize + { + match_count += 1; + } + } + if match_count == 0 { + error!( + "No matching display found for capturable with size {:?}.", + d.0.physical_size + ); + continue; + } + if match_count == 1 { + for (i, wd) in displays.displays.iter().enumerate() { + if matched_indices.contains(&i) { + continue; + } + if d.0.physical_size.0 == wd.width as usize + && d.0.physical_size.1 == wd.height as usize + { + d.0.position = (wd.x, wd.y); + pw_stream.position = (wd.x, wd.y); + matched_indices.push(i); + debug!( + "Disambiguated position for capturable with size {:?} to ({}, {}).", + d.0.physical_size, wd.x, wd.y + ); + break; + } + } + continue; + } + + // Move the mouse to a neutral position first, + // to avoid interference from previous position. + mouse_move_to_(&mouse_move_to, get_cursor_pos, 300, 300); + + let mut rec = PipeWireRecorder::new(PipeWireCapturable { + dbus_conn: conn.clone(), + fd: fd.clone(), + path: pw_stream_with_cursor.path, + source_type: pw_stream_with_cursor.source_type, + primary: false, + position: pw_stream_with_cursor.position, + logical_size: pw_stream_with_cursor.size, + physical_size: (0, 0), + })?; + // Take first frame and copy owned buffer to avoid borrow across second capture + let (is_bgr, w, first_buf): (bool, usize, Vec) = + match rec.capture(CAPTURE_TIMEOUT_MS) { + Ok(PixelProvider::BGR0(w, _, data1)) => (true, w, data1.to_vec()), + Ok(PixelProvider::RGB0(w, _, data1)) => (false, w, data1.to_vec()), + Ok(_) => { + error!("Unexpected pixel format on first capture."); + continue; + } + Err(e) => { + error!( + "Failed to capture screen for position disambiguation: {}", + e + ); + continue; + } + }; + + let matched_len = matched_indices.len(); + for (i, wd) in displays.displays.iter().enumerate() { + if matched_indices.contains(&i) { + continue; + } + + if wd.width as usize == d.0.physical_size.0 + && wd.height as usize == d.0.physical_size.1 + { + mouse_move_to_(&mouse_move_to, get_cursor_pos, wd.x + 8, wd.y + 8); + rec.saved_raw_data.clear(); + match rec.capture(CAPTURE_TIMEOUT_MS) { + Ok(PixelProvider::BGR0(_, _, data2)) if is_bgr => { + if compare_left_up_corner(w, &first_buf, data2) { + d.0.position = (wd.x, wd.y); + pw_stream.position = (wd.x, wd.y); + matched_indices.push(i); + debug!( + "Disambiguated position for capturable with size {:?} to ({}, {}).", + d.0.physical_size, wd.x, wd.y + ); + break; + } + } + Ok(PixelProvider::RGB0(_, _, data2)) if !is_bgr => { + if compare_left_up_corner(w, &first_buf, data2) { + d.0.position = (wd.x, wd.y); + pw_stream.position = (wd.x, wd.y); + matched_indices.push(i); + debug!( + "Disambiguated position for capturable with size {:?} to ({}, {}).", + d.0.physical_size, wd.x, wd.y + ); + break; + } + } + Ok(_) => { + // unreachable + error!("Pixel format changed between captures, cannot disambiguate position."); + } + Err(e) => { + error!( + "Failed to capture screen for position disambiguation: {}", + e + ); + } + } + } + } + if matched_len == matched_indices.len() { + error!( + "Failed to disambiguate position for capturable with size {:?}.", + d.0.physical_size + ); + } + } + _ => {} + } + } + + Ok(()) +} + +fn sort_streams( + displays: &Arc, + shared_displays: &mut Vec, + streams: &mut Vec, +) { + if streams.is_empty() { + // unreachable + error!("No streams available to sort."); + return; + } + + // put the main display first, then the rest by the order of displays + let mut display_order: Vec<(i32, i32)> = Vec::new(); + if let Some(d) = displays.displays.get(displays.primary) { + display_order.push((d.x, d.y)); + } + for (i, d) in displays.displays.iter().enumerate() { + if i != displays.primary { + display_order.push((d.x, d.y)); + } + } + + let mut sorted_streams = Vec::new(); + let mut sorted_shared_displays = Vec::new(); + // Move matching items in order without cloning + for (x, y) in display_order.into_iter() { + for i in 0..streams.len() { + if streams[i].position.0 == x && streams[i].position.1 == y { + sorted_streams.push(streams.remove(i)); + // shared_displays.len() must be equal to streams.len() + // But we still check the length to avoid panic + if shared_displays.len() > i { + sorted_shared_displays.push(shared_displays.remove(i)); + } + break; + } + } + } + *streams = sorted_streams; + *shared_displays = sorted_shared_displays; +} diff --git a/vendor/rustdesk/libs/scrap/src/wayland/remote_desktop_portal.rs b/vendor/rustdesk/libs/scrap/src/wayland/remote_desktop_portal.rs new file mode 100644 index 0000000..22ccddc --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/wayland/remote_desktop_portal.rs @@ -0,0 +1,315 @@ +// This code was autogenerated with `dbus-codegen-rust -c blocking -m None`, see https://github.com/diwic/dbus-rs +// https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.RemoteDesktop.xml +use dbus; +#[allow(unused_imports)] +use dbus::arg; +use dbus::blocking; + +pub trait OrgFreedesktopPortalRemoteDesktop { + fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error>; + fn select_devices( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result, dbus::Error>; + fn start( + &self, + session_handle: dbus::Path, + parent_window: &str, + options: arg::PropMap, + ) -> Result, dbus::Error>; + fn notify_pointer_motion( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + dx: f64, + dy: f64, + ) -> Result<(), dbus::Error>; + fn notify_pointer_motion_absolute( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + stream: u32, + x_: f64, + y_: f64, + ) -> Result<(), dbus::Error>; + fn notify_pointer_button( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + button: i32, + state: u32, + ) -> Result<(), dbus::Error>; + fn notify_pointer_axis( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + dx: f64, + dy: f64, + ) -> Result<(), dbus::Error>; + fn notify_pointer_axis_discrete( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + axis: u32, + steps: i32, + ) -> Result<(), dbus::Error>; + fn notify_keyboard_keycode( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + keycode: i32, + state: u32, + ) -> Result<(), dbus::Error>; + fn notify_keyboard_keysym( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + keysym: i32, + state: u32, + ) -> Result<(), dbus::Error>; + fn notify_touch_down( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + stream: u32, + slot: u32, + x_: f64, + y_: f64, + ) -> Result<(), dbus::Error>; + fn notify_touch_motion( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + stream: u32, + slot: u32, + x_: f64, + y_: f64, + ) -> Result<(), dbus::Error>; + fn notify_touch_up( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + slot: u32, + ) -> Result<(), dbus::Error>; + fn connect_to_eis( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + ) -> Result; + fn available_device_types(&self) -> Result; + fn version(&self) -> Result; +} + +impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> + OrgFreedesktopPortalRemoteDesktop for blocking::Proxy<'a, C> +{ + fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "CreateSession", + (options,), + ) + .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) + } + + fn select_devices( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "SelectDevices", + (session_handle, options), + ) + .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) + } + + fn start( + &self, + session_handle: dbus::Path, + parent_window: &str, + options: arg::PropMap, + ) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "Start", + (session_handle, parent_window, options), + ) + .and_then(|r: (dbus::Path<'static>,)| Ok(r.0)) + } + + fn notify_pointer_motion( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + dx: f64, + dy: f64, + ) -> Result<(), dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "NotifyPointerMotion", + (session_handle, options, dx, dy), + ) + } + + fn notify_pointer_motion_absolute( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + stream: u32, + x_: f64, + y_: f64, + ) -> Result<(), dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "NotifyPointerMotionAbsolute", + (session_handle, options, stream, x_, y_), + ) + } + + fn notify_pointer_button( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + button: i32, + state: u32, + ) -> Result<(), dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "NotifyPointerButton", + (session_handle, options, button, state), + ) + } + + fn notify_pointer_axis( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + dx: f64, + dy: f64, + ) -> Result<(), dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "NotifyPointerAxis", + (session_handle, options, dx, dy), + ) + } + + fn notify_pointer_axis_discrete( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + axis: u32, + steps: i32, + ) -> Result<(), dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "NotifyPointerAxisDiscrete", + (session_handle, options, axis, steps), + ) + } + + fn notify_keyboard_keycode( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + keycode: i32, + state: u32, + ) -> Result<(), dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "NotifyKeyboardKeycode", + (session_handle, options, keycode, state), + ) + } + + fn notify_keyboard_keysym( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + keysym: i32, + state: u32, + ) -> Result<(), dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "NotifyKeyboardKeysym", + (session_handle, options, keysym, state), + ) + } + + fn notify_touch_down( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + stream: u32, + slot: u32, + x_: f64, + y_: f64, + ) -> Result<(), dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "NotifyTouchDown", + (session_handle, options, stream, slot, x_, y_), + ) + } + + fn notify_touch_motion( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + stream: u32, + slot: u32, + x_: f64, + y_: f64, + ) -> Result<(), dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "NotifyTouchMotion", + (session_handle, options, stream, slot, x_, y_), + ) + } + + fn notify_touch_up( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + slot: u32, + ) -> Result<(), dbus::Error> { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "NotifyTouchUp", + (session_handle, options, slot), + ) + } + + fn connect_to_eis( + &self, + session_handle: &dbus::Path, + options: arg::PropMap, + ) -> Result { + self.method_call( + "org.freedesktop.portal.RemoteDesktop", + "ConnectToEIS", + (session_handle, options), + ) + .and_then(|r: (arg::OwnedFd,)| Ok(r.0)) + } + + fn available_device_types(&self) -> Result { + ::get( + &self, + "org.freedesktop.portal.RemoteDesktop", + "AvailableDeviceTypes", + ) + } + + fn version(&self) -> Result { + ::get( + &self, + "org.freedesktop.portal.RemoteDesktop", + "version", + ) + } +} diff --git a/vendor/rustdesk/libs/scrap/src/wayland/request_portal.rs b/vendor/rustdesk/libs/scrap/src/wayland/request_portal.rs new file mode 100644 index 0000000..c2314a3 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/wayland/request_portal.rs @@ -0,0 +1,45 @@ +// This code was autogenerated with `dbus-codegen-rust -c blocking -m None`, see https://github.com/diwic/dbus-rs +// https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.Request.xml +use dbus; +#[allow(unused_imports)] +use dbus::arg; +use dbus::blocking; + +pub trait OrgFreedesktopPortalRequest { + fn close(&self) -> Result<(), dbus::Error>; +} + +impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> OrgFreedesktopPortalRequest + for blocking::Proxy<'a, C> +{ + fn close(&self) -> Result<(), dbus::Error> { + self.method_call("org.freedesktop.portal.Request", "Close", ()) + } +} + +#[derive(Debug)] +pub struct OrgFreedesktopPortalRequestResponse { + pub response: u32, + pub results: arg::PropMap, +} + +impl arg::AppendAll for OrgFreedesktopPortalRequestResponse { + fn append(&self, i: &mut arg::IterAppend) { + arg::RefArg::append(&self.response, i); + arg::RefArg::append(&self.results, i); + } +} + +impl arg::ReadAll for OrgFreedesktopPortalRequestResponse { + fn read(i: &mut arg::Iter) -> Result { + Ok(OrgFreedesktopPortalRequestResponse { + response: i.read()?, + results: i.read()?, + }) + } +} + +impl dbus::message::SignalArgs for OrgFreedesktopPortalRequestResponse { + const NAME: &'static str = "Response"; + const INTERFACE: &'static str = "org.freedesktop.portal.Request"; +} diff --git a/vendor/rustdesk/libs/scrap/src/wayland/screencast_portal.rs b/vendor/rustdesk/libs/scrap/src/wayland/screencast_portal.rs new file mode 100644 index 0000000..a8f7a91 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/wayland/screencast_portal.rs @@ -0,0 +1,106 @@ +// This code was autogenerated with `dbus-codegen-rust -c blocking -m None`, see https://github.com/diwic/dbus-rs +// https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.ScreenCast.xml +use dbus; +#[allow(unused_imports)] +use dbus::arg; +use dbus::blocking; + +pub trait OrgFreedesktopPortalScreenCast { + fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error>; + fn select_sources( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result, dbus::Error>; + fn start( + &self, + session_handle: dbus::Path, + parent_window: &str, + options: arg::PropMap, + ) -> Result, dbus::Error>; + fn open_pipe_wire_remote( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result; + fn available_source_types(&self) -> Result; + fn available_cursor_modes(&self) -> Result; + fn version(&self) -> Result; +} + +impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> + OrgFreedesktopPortalScreenCast for blocking::Proxy<'a, C> +{ + fn create_session(&self, options: arg::PropMap) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "CreateSession", + (options,), + ) + .map(|r: (dbus::Path<'static>,)| r.0) + } + + fn select_sources( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "SelectSources", + (session_handle, options), + ) + .map(|r: (dbus::Path<'static>,)| r.0) + } + + fn start( + &self, + session_handle: dbus::Path, + parent_window: &str, + options: arg::PropMap, + ) -> Result, dbus::Error> { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "Start", + (session_handle, parent_window, options), + ) + .map(|r: (dbus::Path<'static>,)| r.0) + } + + fn open_pipe_wire_remote( + &self, + session_handle: dbus::Path, + options: arg::PropMap, + ) -> Result { + self.method_call( + "org.freedesktop.portal.ScreenCast", + "OpenPipeWireRemote", + (session_handle, options), + ) + .map(|r: (arg::OwnedFd,)| r.0) + } + + fn available_source_types(&self) -> Result { + ::get( + &self, + "org.freedesktop.portal.ScreenCast", + "AvailableSourceTypes", + ) + } + + fn available_cursor_modes(&self) -> Result { + ::get( + &self, + "org.freedesktop.portal.ScreenCast", + "AvailableCursorModes", + ) + } + + fn version(&self) -> Result { + ::get( + &self, + "org.freedesktop.portal.ScreenCast", + "version", + ) + } +} diff --git a/vendor/rustdesk/libs/scrap/src/x11/capturer.rs b/vendor/rustdesk/libs/scrap/src/x11/capturer.rs new file mode 100644 index 0000000..550bfb3 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/x11/capturer.rs @@ -0,0 +1,115 @@ +use super::ffi::*; +use super::Display; +use hbb_common::libc; +use std::{io, ptr, slice}; + +pub struct Capturer { + display: Display, + shmid: i32, + xcbid: u32, + buffer: *const u8, + + size: usize, + saved_raw_data: Vec, // for faster compare and copy +} + +impl Capturer { + pub fn new(display: Display) -> io::Result { + // Calculate dimensions. + + let pixel_width = display.pixfmt().bytes_per_pixel(); + let rect = display.rect(); + let size = (rect.w as usize) * (rect.h as usize) * pixel_width; + + // Create a shared memory segment. + + let shmid = unsafe { + libc::shmget( + libc::IPC_PRIVATE, + size, + // Everyone can do anything. + libc::IPC_CREAT | 0o777, + ) + }; + + if shmid == -1 { + return Err(io::Error::last_os_error()); + } + + // Attach the segment to a readable address. + + let buffer = unsafe { libc::shmat(shmid, ptr::null(), libc::SHM_RDONLY) } as *mut u8; + + if buffer as isize == -1 { + return Err(io::Error::last_os_error()); + } + + // Attach the segment to XCB. + + let server = display.server().raw(); + let xcbid = unsafe { xcb_generate_id(server) }; + unsafe { + xcb_shm_attach( + server, + xcbid, + shmid as u32, + 0, // False, i.e. not read-only. + ); + } + + let c = Capturer { + display, + shmid, + xcbid, + buffer, + size, + saved_raw_data: Vec::new(), + }; + Ok(c) + } + + pub fn display(&self) -> &Display { + &self.display + } + + fn get_image(&self) { + let rect = self.display.rect(); + unsafe { + let request = xcb_shm_get_image_unchecked( + self.display.server().raw(), + self.display.root(), + rect.x, + rect.y, + rect.w, + rect.h, + !0, + XCB_IMAGE_FORMAT_Z_PIXMAP, + self.xcbid, + 0, + ); + let response = + xcb_shm_get_image_reply(self.display.server().raw(), request, ptr::null_mut()); + libc::free(response as *mut _); + } + } + + pub fn frame<'b>(&'b mut self) -> std::io::Result<&'b [u8]> { + self.get_image(); + let result = unsafe { slice::from_raw_parts(self.buffer, self.size) }; + crate::would_block_if_equal(&mut self.saved_raw_data, result)?; + Ok(result) + } +} + +impl Drop for Capturer { + fn drop(&mut self) { + unsafe { + // Detach segment from XCB. + xcb_shm_detach(self.display.server().raw(), self.xcbid); + // Detach segment from our space. + libc::shmdt(self.buffer as *mut _); + // Destroy the shared memory segment. + libc::shmctl(self.shmid, libc::IPC_RMID, ptr::null_mut()); + } + } +} diff --git a/vendor/rustdesk/libs/scrap/src/x11/display.rs b/vendor/rustdesk/libs/scrap/src/x11/display.rs new file mode 100644 index 0000000..0bc18b3 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/x11/display.rs @@ -0,0 +1,70 @@ +use std::rc::Rc; + +use super::ffi::*; +use super::Server; +use crate::Pixfmt; + +#[derive(Debug)] +pub struct Display { + server: Rc, + default: bool, + rect: Rect, + root: xcb_window_t, + name: String, + pixfmt: Pixfmt, +} + +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct Rect { + pub x: i16, + pub y: i16, + pub w: u16, + pub h: u16, +} + +impl Display { + pub unsafe fn new( + server: Rc, + default: bool, + rect: Rect, + root: xcb_window_t, + name: String, + pixfmt: Pixfmt, + ) -> Display { + Display { + server, + default, + rect, + root, + name, + pixfmt, + } + } + + pub fn server(&self) -> &Rc { + &self.server + } + pub fn is_default(&self) -> bool { + self.default + } + pub fn rect(&self) -> Rect { + self.rect + } + pub fn w(&self) -> usize { + self.rect.w as _ + } + pub fn h(&self) -> usize { + self.rect.h as _ + } + pub fn root(&self) -> xcb_window_t { + self.root + } + + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn pixfmt(&self) -> Pixfmt { + self.pixfmt + } +} diff --git a/vendor/rustdesk/libs/scrap/src/x11/ffi.rs b/vendor/rustdesk/libs/scrap/src/x11/ffi.rs new file mode 100644 index 0000000..370ce78 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/x11/ffi.rs @@ -0,0 +1,283 @@ +#![allow(non_camel_case_types)] + +use hbb_common::libc::c_void; + +#[link(name = "xcb")] +#[link(name = "xcb-shm")] +#[link(name = "xcb-randr")] +extern "C" { + pub fn xcb_connect(displayname: *const i8, screenp: *mut i32) -> *mut xcb_connection_t; + + pub fn xcb_disconnect(c: *mut xcb_connection_t); + + pub fn xcb_connection_has_error(c: *mut xcb_connection_t) -> i32; + + pub fn xcb_get_setup(c: *mut xcb_connection_t) -> *const xcb_setup_t; + + pub fn xcb_setup_roots_iterator(r: *const xcb_setup_t) -> xcb_screen_iterator_t; + + pub fn xcb_screen_next(i: *mut xcb_screen_iterator_t); + + pub fn xcb_generate_id(c: *mut xcb_connection_t) -> u32; + + pub fn xcb_shm_attach( + c: *mut xcb_connection_t, + shmseg: xcb_shm_seg_t, + shmid: u32, + read_only: u8, + ) -> xcb_void_cookie_t; + + pub fn xcb_shm_detach(c: *mut xcb_connection_t, shmseg: xcb_shm_seg_t) -> xcb_void_cookie_t; + + pub fn xcb_shm_get_image_unchecked( + c: *mut xcb_connection_t, + drawable: xcb_drawable_t, + x: i16, + y: i16, + width: u16, + height: u16, + plane_mask: u32, + format: u8, + shmseg: xcb_shm_seg_t, + offset: u32, + ) -> xcb_shm_get_image_cookie_t; + + pub fn xcb_shm_get_image_reply( + c: *mut xcb_connection_t, + cookie: xcb_shm_get_image_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *mut xcb_shm_get_image_reply_t; + + pub fn xcb_randr_get_monitors_unchecked( + c: *mut xcb_connection_t, + window: xcb_window_t, + get_active: u8, + ) -> xcb_randr_get_monitors_cookie_t; + + pub fn xcb_randr_get_monitors_reply( + c: *mut xcb_connection_t, + cookie: xcb_randr_get_monitors_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *mut xcb_randr_get_monitors_reply_t; + + pub fn xcb_randr_get_monitors_monitors_iterator( + r: *const xcb_randr_get_monitors_reply_t, + ) -> xcb_randr_monitor_info_iterator_t; + + pub fn xcb_randr_monitor_info_next(i: *mut xcb_randr_monitor_info_iterator_t); + + pub fn xcb_get_atom_name( + c: *mut xcb_connection_t, + atom: xcb_atom_t, + ) -> xcb_get_atom_name_cookie_t; + + pub fn xcb_get_atom_name_reply( + c: *mut xcb_connection_t, + cookie: xcb_get_atom_name_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *const xcb_get_atom_name_reply_t; + + pub fn xcb_get_atom_name_name(reply: *const xcb_get_atom_name_request_t) -> *const u8; + + pub fn xcb_get_atom_name_name_length(reply: *const xcb_get_atom_name_reply_t) -> i32; + + pub fn xcb_shm_query_version(c: *mut xcb_connection_t) -> xcb_shm_query_version_cookie_t; + + pub fn xcb_shm_query_version_reply( + c: *mut xcb_connection_t, + cookie: xcb_shm_query_version_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *const xcb_shm_query_version_reply_t; + + pub fn xcb_get_geometry_unchecked( + c: *mut xcb_connection_t, + drawable: xcb_drawable_t, + ) -> xcb_get_geometry_cookie_t; + + pub fn xcb_get_geometry_reply( + c: *mut xcb_connection_t, + cookie: xcb_get_geometry_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *mut xcb_get_geometry_reply_t; + +} + +pub const XCB_IMAGE_FORMAT_Z_PIXMAP: u8 = 2; + +pub type xcb_atom_t = u32; +pub type xcb_connection_t = c_void; +pub type xcb_window_t = u32; +pub type xcb_keycode_t = u8; +pub type xcb_visualid_t = u32; +pub type xcb_timestamp_t = u32; +pub type xcb_colormap_t = u32; +pub type xcb_shm_seg_t = u32; +pub type xcb_drawable_t = u32; +pub type xcb_get_atom_name_cookie_t = u32; +pub type xcb_get_atom_name_reply_t = u32; +pub type xcb_get_atom_name_request_t = xcb_get_atom_name_reply_t; + +#[repr(C)] +pub struct xcb_setup_t { + pub status: u8, + pub pad0: u8, + pub protocol_major_version: u16, + pub protocol_minor_version: u16, + pub length: u16, + pub release_number: u32, + pub resource_id_base: u32, + pub resource_id_mask: u32, + pub motion_buffer_size: u32, + pub vendor_len: u16, + pub maximum_request_length: u16, + pub roots_len: u8, + pub pixmap_formats_len: u8, + pub image_byte_order: u8, + pub bitmap_format_bit_order: u8, + pub bitmap_format_scanline_unit: u8, + pub bitmap_format_scanline_pad: u8, + pub min_keycode: xcb_keycode_t, + pub max_keycode: xcb_keycode_t, + pub pad1: [u8; 4], +} + +#[repr(C)] +pub struct xcb_screen_iterator_t { + pub data: *mut xcb_screen_t, + pub rem: i32, + pub index: i32, +} + +#[repr(C)] +pub struct xcb_screen_t { + pub root: xcb_window_t, + pub default_colormap: xcb_colormap_t, + pub white_pixel: u32, + pub black_pixel: u32, + pub current_input_masks: u32, + pub width_in_pixels: u16, + pub height_in_pixels: u16, + pub width_in_millimeters: u16, + pub height_in_millimeters: u16, + pub min_installed_maps: u16, + pub max_installed_maps: u16, + pub root_visual: xcb_visualid_t, + pub backing_stores: u8, + pub save_unders: u8, + pub root_depth: u8, + pub allowed_depths_len: u8, +} + +#[repr(C)] +pub struct xcb_randr_monitor_info_iterator_t { + pub data: *mut xcb_randr_monitor_info_t, + pub rem: i32, + pub index: i32, +} + +#[repr(C)] +pub struct xcb_randr_monitor_info_t { + pub name: xcb_atom_t, + pub primary: u8, + pub automatic: u8, + pub n_output: u16, + pub x: i16, + pub y: i16, + pub width: u16, + pub height: u16, + pub width_mm: u32, + pub height_mm: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct xcb_randr_get_monitors_cookie_t { + pub sequence: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct xcb_shm_get_image_cookie_t { + pub sequence: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct xcb_void_cookie_t { + pub sequence: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct xcb_get_geometry_cookie_t { + pub sequence: u32, +} + +#[repr(C)] +pub struct xcb_generic_error_t { + pub response_type: u8, + pub error_code: u8, + pub sequence: u16, + pub resource_id: u32, + pub minor_code: u16, + pub major_code: u8, + pub pad0: u8, + pub pad: [u32; 5], + pub full_sequence: u32, +} + +#[repr(C)] +pub struct xcb_shm_get_image_reply_t { + pub response_type: u8, + pub depth: u8, + pub sequence: u16, + pub length: u32, + pub visual: xcb_visualid_t, + pub size: u32, +} + +#[repr(C)] +pub struct xcb_randr_get_monitors_reply_t { + pub response_type: u8, + pub pad0: u8, + pub sequence: u16, + pub length: u32, + pub timestamp: xcb_timestamp_t, + pub n_monitors: u32, + pub n_outputs: u32, + pub pad1: [u8; 12], +} + +#[repr(C)] +pub struct xcb_shm_query_version_cookie_t { + pub sequence: u32, +} + +#[repr(C)] +pub struct xcb_shm_query_version_reply_t { + pub response_type: u8, + pub shared_pixmaps: u8, + pub sequence: u16, + pub length: u32, + pub major_version: u16, + pub minor_version: u16, + pub uid: u16, + pub gid: u16, + pub pixmap_format: u8, + pub pad0: [u8; 15], +} + +#[repr(C)] +pub struct xcb_get_geometry_reply_t { + pub response_type: u8, + pub depth: u8, + pub sequence: u16, + pub length: u32, + pub root: xcb_window_t, + pub x: i16, + pub y: i16, + pub width: u16, + pub height: u16, + pub border_width: u16, + pub pad0: [u8; 2], +} diff --git a/vendor/rustdesk/libs/scrap/src/x11/iter.rs b/vendor/rustdesk/libs/scrap/src/x11/iter.rs new file mode 100644 index 0000000..f800d5a --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/x11/iter.rs @@ -0,0 +1,138 @@ +use std::ffi::CString; +use std::ptr; +use std::rc::Rc; + +use crate::Pixfmt; +use hbb_common::libc; + +use super::ffi::*; +use super::{Display, Rect, Server}; + +//TODO: Do I have to free the displays? + +pub struct DisplayIter { + outer: xcb_screen_iterator_t, + inner: Option<(xcb_randr_monitor_info_iterator_t, xcb_window_t)>, + server: Rc, +} + +impl DisplayIter { + pub unsafe fn new(server: Rc) -> DisplayIter { + let mut outer = xcb_setup_roots_iterator(server.setup()); + let inner = Self::next_screen(&mut outer, &server); + DisplayIter { + outer, + inner, + server, + } + } + + fn next_screen( + outer: &mut xcb_screen_iterator_t, + server: &Server, + ) -> Option<(xcb_randr_monitor_info_iterator_t, xcb_window_t)> { + if outer.rem == 0 { + return None; + } + + unsafe { + let root = (*outer.data).root; + + let cookie = xcb_randr_get_monitors_unchecked( + server.raw(), + root, + 1, //TODO: I don't know if this should be true or false. + ); + + let response = xcb_randr_get_monitors_reply(server.raw(), cookie, ptr::null_mut()); + + let inner = xcb_randr_get_monitors_monitors_iterator(response); + + libc::free(response as *mut _); + xcb_screen_next(outer); + + Some((inner, root)) + } + } +} + +impl Iterator for DisplayIter { + type Item = Display; + + fn next(&mut self) -> Option { + loop { + if let Some((ref mut inner, root)) = self.inner { + // If there is something in the current screen, return that. + if inner.rem != 0 { + unsafe { + let data = &*inner.data; + let name = get_atom_name(self.server.raw(), data.name); + let pixfmt = get_pixfmt(self.server.raw(), root).unwrap_or(Pixfmt::BGRA); + let display = Display::new( + self.server.clone(), + data.primary != 0, + Rect { + x: data.x, + y: data.y, + w: data.width, + h: data.height, + }, + root, + name, + pixfmt, + ); + + xcb_randr_monitor_info_next(inner); + return Some(display); + } + } + } else { + // If there is no current screen, the screen iterator is empty. + return None; + } + + // The current screen was empty, so try the next screen. + self.inner = Self::next_screen(&mut self.outer, &self.server); + } + } +} + +fn get_atom_name(conn: *mut xcb_connection_t, atom: xcb_atom_t) -> String { + let empty = "".to_owned(); + if atom == 0 { + return empty; + } + unsafe { + let mut e: *mut xcb_generic_error_t = std::ptr::null_mut(); + let reply = xcb_get_atom_name_reply(conn, xcb_get_atom_name(conn, atom), &mut e as _); + if reply == std::ptr::null() { + return empty; + } + let length = xcb_get_atom_name_name_length(reply); + let name = xcb_get_atom_name_name(reply); + let mut v = vec![0u8; length as _]; + std::ptr::copy_nonoverlapping(name as _, v.as_mut_ptr(), length as _); + libc::free(reply as *mut _); + if let Ok(s) = CString::new(v) { + return s.to_string_lossy().to_string(); + } + empty + } +} + +unsafe fn get_pixfmt(conn: *mut xcb_connection_t, root: xcb_window_t) -> Option { + let geo_cookie = xcb_get_geometry_unchecked(conn, root); + let geo = xcb_get_geometry_reply(conn, geo_cookie, ptr::null_mut()); + if geo.is_null() { + return None; + } + let depth = (*geo).depth; + libc::free(geo as _); + // now only support little endian + // https://github.com/FFmpeg/FFmpeg/blob/a9c05eb657d0d05f3ac79fe9973581a41b265a5e/libavdevice/xcbgrab.c#L519 + match depth { + 16 => Some(Pixfmt::RGB565LE), + 32 => Some(Pixfmt::BGRA), + _ => None, + } +} diff --git a/vendor/rustdesk/libs/scrap/src/x11/mod.rs b/vendor/rustdesk/libs/scrap/src/x11/mod.rs new file mode 100644 index 0000000..382d1f6 --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/x11/mod.rs @@ -0,0 +1,10 @@ +pub use self::capturer::*; +pub use self::display::*; +pub use self::iter::*; +pub use self::server::*; + +mod capturer; +mod display; +mod ffi; +mod iter; +mod server; diff --git a/vendor/rustdesk/libs/scrap/src/x11/server.rs b/vendor/rustdesk/libs/scrap/src/x11/server.rs new file mode 100644 index 0000000..7ae145d --- /dev/null +++ b/vendor/rustdesk/libs/scrap/src/x11/server.rs @@ -0,0 +1,146 @@ +use hbb_common::libc; +use std::ptr; +use std::rc::Rc; + +use super::ffi::*; +use super::DisplayIter; + +#[derive(Debug)] +pub struct Server { + raw: *mut xcb_connection_t, + screenp: i32, + setup: *const xcb_setup_t, +} + +/* +use std::cell::RefCell; +thread_local! { + static SERVER: RefCell>> = RefCell::new(None); +} +*/ + +impl Server { + pub fn displays(slf: Rc) -> DisplayIter { + unsafe { DisplayIter::new(slf) } + } + + pub fn default() -> Result, Error> { + Ok(Rc::new(Server::connect(ptr::null())?)) + /* + let mut res = Err(Error::from(0)); + SERVER.with(|xdo| { + if let Ok(mut server) = xdo.try_borrow_mut() { + if server.is_some() { + unsafe { + if 0 != xcb_connection_has_error(server.as_ref().unwrap().raw) { + *server = None; + println!("Reset x11 connection"); + } + } + } + if server.is_none() { + println!("New x11 connection"); + match Server::connect(ptr::null()) { + Ok(s) => { + let s = Rc::new(s); + res = Ok(s.clone()); + *server = Some(s); + } + Err(err) => { + res = Err(err); + } + } + } else { + res = Ok(server.as_ref().map(|x| x.clone()).unwrap()); + } + } + }); + res + */ + } + + pub fn connect(addr: *const i8) -> Result { + unsafe { + let mut screenp = 0; + let raw = xcb_connect(addr, &mut screenp); + + let error = xcb_connection_has_error(raw); + if error != 0 { + xcb_disconnect(raw); + Err(Error::from(error)) + } else { + let setup = xcb_get_setup(raw); + Ok(Server { + raw, + screenp, + setup, + }) + } + } + } + + pub fn raw(&self) -> *mut xcb_connection_t { + self.raw + } + pub fn screenp(&self) -> i32 { + self.screenp + } + pub fn setup(&self) -> *const xcb_setup_t { + self.setup + } + pub fn get_shm_status(&self) -> Result<(), Error> { + unsafe { check_x11_shm_available(self.raw) } + } +} + +unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error> { + let cookie = xcb_shm_query_version(c); + let mut e: *mut xcb_generic_error_t = std::ptr::null_mut(); + let reply = xcb_shm_query_version_reply(c, cookie, &mut e as _); + if reply.is_null() { + // TODO: Should separate SHM disabled from SHM not supported? + return Err(Error::UnsupportedExtension); + } else { + // https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229 + libc::free(reply as *mut _); + if e.is_null() { + return Ok(()); + } else { + libc::free(e as *mut _); + // TODO: Does "This request does never generate any errors" in manual means `e` is never set, so we would never reach here? + return Err(Error::Generic); + } + } +} + +impl Drop for Server { + fn drop(&mut self) { + unsafe { + xcb_disconnect(self.raw); + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum Error { + Generic, + UnsupportedExtension, + InsufficientMemory, + RequestTooLong, + ParseError, + InvalidScreen, +} + +impl From for Error { + fn from(x: i32) -> Error { + use self::Error::*; + match x { + 2 => UnsupportedExtension, + 3 => InsufficientMemory, + 4 => RequestTooLong, + 5 => ParseError, + 6 => InvalidScreen, + _ => Generic, + } + } +} diff --git a/vendor/rustdesk/libs/virtual_display/Cargo.toml b/vendor/rustdesk/libs/virtual_display/Cargo.toml new file mode 100644 index 0000000..2559ffb --- /dev/null +++ b/vendor/rustdesk/libs/virtual_display/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "virtual_display" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4" +hbb_common = { path = "../hbb_common" } \ No newline at end of file diff --git a/vendor/rustdesk/libs/virtual_display/dylib/Cargo.toml b/vendor/rustdesk/libs/virtual_display/dylib/Cargo.toml new file mode 100644 index 0000000..fee4e3e --- /dev/null +++ b/vendor/rustdesk/libs/virtual_display/dylib/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "dylib_virtual_display" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib", "rlib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +cc = "1.0" + +[dependencies] +thiserror = "1.0.30" +lazy_static = "1.4" +serde = "1.0" +serde_derive = "1.0" +hbb_common = { path = "../../hbb_common" } diff --git a/vendor/rustdesk/libs/virtual_display/dylib/build.rs b/vendor/rustdesk/libs/virtual_display/dylib/build.rs new file mode 100644 index 0000000..29c3dd5 --- /dev/null +++ b/vendor/rustdesk/libs/virtual_display/dylib/build.rs @@ -0,0 +1,35 @@ +use cc; + +fn build_c_impl() { + let mut build = cc::Build::new(); + + #[cfg(target_os = "windows")] + build.file("src/win10/IddController.c"); + + build.flag_if_supported("-Wno-c++0x-extensions"); + build.flag_if_supported("-Wno-return-type-c-linkage"); + build.flag_if_supported("-Wno-invalid-offsetof"); + build.flag_if_supported("-Wno-unused-parameter"); + + if build.get_compiler().is_like_msvc() { + build.define("WIN32", ""); + build.flag("-Z7"); + build.flag("-GR-"); + // build.flag("-std:c++11"); + } else { + build.flag("-fPIC"); + // build.flag("-std=c++11"); + // build.flag("-include"); + // build.flag(&confdefs_path.to_string_lossy()); + } + + #[cfg(target_os = "windows")] + build.compile("win_virtual_display"); + + #[cfg(target_os = "windows")] + println!("cargo:rerun-if-changed=src/win10/IddController.c"); +} + +fn main() { + build_c_impl(); +} diff --git a/vendor/rustdesk/libs/virtual_display/dylib/src/lib.rs b/vendor/rustdesk/libs/virtual_display/dylib/src/lib.rs new file mode 100644 index 0000000..aec07ab --- /dev/null +++ b/vendor/rustdesk/libs/virtual_display/dylib/src/lib.rs @@ -0,0 +1,191 @@ +#[cfg(windows)] +pub mod win10; +use hbb_common::ResultType; +#[cfg(windows)] +use hbb_common::{bail, lazy_static}; +#[cfg(windows)] +use std::path::PathBuf; + +#[cfg(windows)] +use std::sync::Mutex; + +#[cfg(windows)] +lazy_static::lazy_static! { + // If device is uninstalled though "Device Manager" Window. + // RustDesk is unable to handle device any more... + static ref H_SW_DEVICE: Mutex = Mutex::new(0); +} + +#[no_mangle] +#[cfg(windows)] +pub fn get_driver_install_path() -> &'static str { + win10::DRIVER_INSTALL_PATH +} + +#[no_mangle] +pub fn download_driver() -> ResultType<()> { + #[cfg(windows)] + let _download_url = win10::DRIVER_DOWNLOAD_URL; + #[cfg(target_os = "linux")] + let _download_url = ""; + + // process download and report progress + + Ok(()) +} + +#[cfg(windows)] +fn get_driver_install_abs_path() -> ResultType { + let install_path = win10::DRIVER_INSTALL_PATH; + let exe_file = std::env::current_exe()?; + let abs_path = match exe_file.parent() { + Some(cur_dir) => cur_dir.join(install_path), + None => bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ), + }; + if !abs_path.exists() { + bail!("{} not exists", install_path) + } + Ok(abs_path) +} + +#[no_mangle] +pub fn install_update_driver(_reboot_required: &mut bool) -> ResultType<()> { + #[cfg(windows)] + unsafe { + { + // Device must be created before install driver. + // https://github.com/fufesou/RustDeskIddDriver/issues/1 + if let Err(e) = create_device() { + bail!("{}", e); + } + + let abs_path = get_driver_install_abs_path()?; + let full_install_path: Vec = abs_path + .to_string_lossy() + .as_ref() + .encode_utf16() + .chain(Some(0).into_iter()) + .collect(); + + let mut reboot_required_tmp = win10::idd::FALSE; + if win10::idd::InstallUpdate(full_install_path.as_ptr() as _, &mut reboot_required_tmp) + == win10::idd::FALSE + { + bail!("{}", win10::get_last_msg()?); + } + *_reboot_required = reboot_required_tmp == win10::idd::TRUE; + } + } + + Ok(()) +} + +#[no_mangle] +pub fn uninstall_driver(_reboot_required: &mut bool) -> ResultType<()> { + #[cfg(windows)] + unsafe { + { + let abs_path = get_driver_install_abs_path()?; + let full_install_path: Vec = abs_path + .to_string_lossy() + .as_ref() + .encode_utf16() + .chain(Some(0).into_iter()) + .collect(); + + let mut reboot_required_tmp = win10::idd::FALSE; + if win10::idd::Uninstall(full_install_path.as_ptr() as _, &mut reboot_required_tmp) + == win10::idd::FALSE + { + bail!("{}", win10::get_last_msg()?); + } + *_reboot_required = reboot_required_tmp == win10::idd::TRUE; + } + } + + Ok(()) +} + +#[no_mangle] +pub fn is_device_created() -> bool { + #[cfg(windows)] + return *H_SW_DEVICE.lock().unwrap() != 0; + #[cfg(not(windows))] + return false; +} + +#[no_mangle] +pub fn create_device() -> ResultType<()> { + if is_device_created() { + return Ok(()); + } + #[cfg(windows)] + unsafe { + let mut lock_device = H_SW_DEVICE.lock().unwrap(); + let mut h_sw_device = *lock_device as win10::idd::HSWDEVICE; + if win10::idd::DeviceCreate(&mut h_sw_device) == win10::idd::FALSE { + bail!("{}", win10::get_last_msg()?); + } else { + *lock_device = h_sw_device as u64; + } + } + Ok(()) +} + +#[no_mangle] +pub fn close_device() { + #[cfg(windows)] + unsafe { + win10::idd::DeviceClose(*H_SW_DEVICE.lock().unwrap() as win10::idd::HSWDEVICE); + *H_SW_DEVICE.lock().unwrap() = 0; + } +} + +#[no_mangle] +pub fn plug_in_monitor(_monitor_index: u32, _edid: u32, _retries: u32) -> ResultType<()> { + #[cfg(windows)] + unsafe { + if win10::idd::MonitorPlugIn(_monitor_index as _, _edid as _, _retries as _) + == win10::idd::FALSE + { + bail!("{}", win10::get_last_msg()?); + } + } + Ok(()) +} + +#[no_mangle] +pub fn plug_out_monitor(_monitor_index: u32) -> ResultType<()> { + #[cfg(windows)] + unsafe { + if win10::idd::MonitorPlugOut(_monitor_index) == win10::idd::FALSE { + bail!("{}", win10::get_last_msg()?); + } + } + Ok(()) +} + +#[cfg(windows)] +type PMonitorMode = win10::idd::PMonitorMode; +#[cfg(not(windows))] +type PMonitorMode = *mut std::ffi::c_void; + +#[no_mangle] +pub fn update_monitor_modes( + _monitor_index: u32, + _mode_count: u32, + _modes: PMonitorMode, +) -> ResultType<()> { + #[cfg(windows)] + unsafe { + if win10::idd::FALSE + == win10::idd::MonitorModesUpdate(_monitor_index as _, _mode_count as _, _modes) + { + bail!("{}", win10::get_last_msg()?); + } + } + Ok(()) +} diff --git a/vendor/rustdesk/libs/virtual_display/dylib/src/win10/IddController.c b/vendor/rustdesk/libs/virtual_display/dylib/src/win10/IddController.c new file mode 100644 index 0000000..0c6d067 --- /dev/null +++ b/vendor/rustdesk/libs/virtual_display/dylib/src/win10/IddController.c @@ -0,0 +1,1006 @@ +#include "./IddController.h" +#include +#include +#include +#include +#include +#include +#include + +#include "./Public.h" + +typedef struct DeviceCreateCallbackContext +{ + HANDLE hEvent; + SW_DEVICE_LIFETIME* lifetime; + HRESULT hrCreateResult; +} DeviceCreateCallbackContext; + +const GUID GUID_DEVINTERFACE_IDD_DRIVER_DEVICE = \ +{ 0x781EF630, 0x72B2, 0x11d2, { 0xB8, 0x52, 0x00, 0xC0, 0x4E, 0xAF, 0x52, 0x72 } }; +//{781EF630-72B2-11d2-B852-00C04EAF5272} + +BOOL g_printMsg = TRUE; +char g_lastMsg[1024]; +const char* g_msgHeader = "RustDeskIdd: "; + +VOID WINAPI +CreationCallback( + _In_ HSWDEVICE hSwDevice, + _In_ HRESULT hrCreateResult, + _In_opt_ PVOID pContext, + _In_opt_ PCWSTR pszDeviceInstanceId +); +// https://github.com/microsoft/Windows-driver-samples/blob/9f03207ae1e8df83325f067de84494ae55ab5e97/general/DCHU/osrfx2_DCHU_base/osrfx2_DCHU_testapp/testapp.c#L88 +// Not a good way for this device, I don't not why. I'm not familiar with dirver. +BOOLEAN GetDevicePath( + _In_ LPCGUID InterfaceGuid, + _Out_writes_(BufLen) PTCHAR DevicePath, + _In_ size_t BufLen +); +// https://github.com/microsoft/Windows-driver-samples/blob/9f03207ae1e8df83325f067de84494ae55ab5e97/usb/umdf_fx2/exe/testapp.c#L90 +// Works good to check whether device is created before. +BOOLEAN GetDevicePath2( + _In_ LPCGUID InterfaceGuid, + _Out_writes_(BufLen) PTCHAR DevicePath, + _In_ size_t BufLen +); + +HANDLE DeviceOpenHandle(); +VOID DeviceCloseHandle(HANDLE handle); + +LPSTR formatErrorString(DWORD error); + +void SetLastMsg(const char* format, ...) +{ + memset(g_lastMsg, 0, sizeof(g_lastMsg)); + memcpy_s(g_lastMsg, sizeof(g_lastMsg), g_msgHeader, strlen(g_msgHeader)); + + va_list args; + va_start(args, format); + vsnprintf_s( + g_lastMsg + strlen(g_msgHeader), + sizeof(g_lastMsg) - strlen(g_msgHeader), + _TRUNCATE, + format, + args); + va_end(args); +} + +const char* GetLastMsg() +{ + return g_lastMsg; +} + +BOOL InstallUpdate(LPCWSTR fullInfPath, PBOOL rebootRequired) +{ + SetLastMsg("Success"); + + // UpdateDriverForPlugAndPlayDevicesW may return FALSE while driver was successfully installed... + if (FALSE == UpdateDriverForPlugAndPlayDevicesW( + NULL, + L"RustDeskIddDriver", // match hardware id in the inf file + fullInfPath, + INSTALLFLAG_FORCE + // | INSTALLFLAG_NONINTERACTIVE // INSTALLFLAG_NONINTERACTIVE may cause error 0xe0000247 + , + rebootRequired + )) + { + DWORD error = GetLastError(); + if (error != 0) + { + LPSTR errorString = formatErrorString(error); + switch (error) + { + case 0x109: + SetLastMsg("Failed InstallUpdate UpdateDriverForPlugAndPlayDevicesW, error: 0x%x, %s Please try: Reinstall RustDesk with the cert option.\n", error, errorString == NULL ? "(NULL)\n" : errorString); + break; + case 0xe0000247: + SetLastMsg("Failed InstallUpdate UpdateDriverForPlugAndPlayDevicesW, error: 0x%x, %s Please try: \n1. Check the device manager and event viewer.\n2. Uninstall \"RustDeskIddDriver Device\" in device manager, then reinstall RustDesk with the cert option.\n", error, errorString == NULL ? "(NULL)\n" : errorString); + break; + default: + SetLastMsg("Failed InstallUpdate UpdateDriverForPlugAndPlayDevicesW, error: 0x%x, %s Please try: Check the device manager and event viewer.\n", error, errorString == NULL ? "(NULL)\n" : errorString); + break; + } + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + return FALSE; + } + } + + return TRUE; +} + +BOOL Uninstall(LPCWSTR fullInfPath, PBOOL rebootRequired) +{ + SetLastMsg("Success"); + + if (FALSE == DiUninstallDriverW( + NULL, + fullInfPath, + 0, + rebootRequired + )) + { + DWORD error = GetLastError(); + if (error != 0) + { + LPSTR errorString = formatErrorString(error); + SetLastMsg("Failed Uninstall DiUninstallDriverW, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + return FALSE; + } + } + + return TRUE; +} + +BOOL IsDeviceCreated(PBOOL created) +{ + SetLastMsg("Success"); + + HDEVINFO hardwareDeviceInfo = SetupDiGetClassDevs( + &GUID_DEVINTERFACE_IDD_DRIVER_DEVICE, + NULL, // Define no enumerator (global) + NULL, // Define no + (DIGCF_PRESENT | // Only Devices present + DIGCF_DEVICEINTERFACE)); // Function class devices. + if (INVALID_HANDLE_VALUE == hardwareDeviceInfo) + { + DWORD error = GetLastError(); + LPSTR errorString = formatErrorString(error); + SetLastMsg("Idd device: Failed IsDeviceCreated SetupDiGetClassDevs, error 0x%x (%s)\n", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + return FALSE; + } + + SP_DEVICE_INTERFACE_DATA deviceInterfaceData; + deviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); + + BOOL ret = FALSE; + do + { + if (TRUE == SetupDiEnumDeviceInterfaces(hardwareDeviceInfo, + 0, // No care about specific PDOs + &GUID_DEVINTERFACE_IDD_DRIVER_DEVICE, + 0, // + &deviceInterfaceData)) + { + *created = TRUE; + ret = TRUE; + break; + } + + DWORD error = GetLastError(); + if (error == ERROR_NO_MORE_ITEMS) + { + *created = FALSE; + ret = TRUE; + break; + } + + LPSTR errorString = formatErrorString(error); + SetLastMsg("Idd device: Failed IsDeviceCreated SetupDiEnumDeviceInterfaces, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + ret = FALSE; + break; + + } while (0); + + (VOID)SetupDiDestroyDeviceInfoList(hardwareDeviceInfo); + return ret; +} + +BOOL DeviceCreate(PHSWDEVICE hSwDevice) +{ + SW_DEVICE_LIFETIME lifetime = SWDeviceLifetimeHandle; + return DeviceCreateWithLifetime(&lifetime, hSwDevice); +} + +BOOL DeviceCreateWithLifetime(SW_DEVICE_LIFETIME *lifetime, PHSWDEVICE hSwDevice) +{ + SetLastMsg("Success"); + + if (*hSwDevice != NULL) + { + SetLastMsg("Device handle is not NULL\n"); + return FALSE; + } + + // No need to check if the device is previous created. + // https://learn.microsoft.com/en-us/windows/win32/api/swdevice/nf-swdevice-swdevicesetlifetime + // When a client app calls SwDeviceCreate for a software device that was previously marked for + // SwDeviceLifetimeParentPresent, SwDeviceCreate succeeds if there are no open software device handles for the device + // (only one handle can be open for a device). A client app can then regain control over a persistent software device + // for the purposes of updating properties and interfaces or changing the lifetime. + // + + // create device + HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + if (hEvent == INVALID_HANDLE_VALUE || hEvent == NULL) + { + DWORD error = GetLastError(); + LPSTR errorString = formatErrorString(error); + SetLastMsg("Failed DeviceCreate CreateEvent, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + + return FALSE; + } + + DeviceCreateCallbackContext callbackContext = { hEvent, lifetime, E_FAIL, }; + + SW_DEVICE_CREATE_INFO createInfo = { 0 }; + PCWSTR description = L"RustDesk Idd Driver"; + + // These match the Pnp id's in the inf file so OS will load the driver when the device is created + PCWSTR instanceId = L"RustDeskIddDriver"; + PCWSTR hardwareIds = L"RustDeskIddDriver\0\0"; + PCWSTR compatibleIds = L"RustDeskIddDriver\0\0"; + + createInfo.cbSize = sizeof(createInfo); + createInfo.pszzCompatibleIds = compatibleIds; + createInfo.pszInstanceId = instanceId; + createInfo.pszzHardwareIds = hardwareIds; + createInfo.pszDeviceDescription = description; + + createInfo.CapabilityFlags = SWDeviceCapabilitiesRemovable | + SWDeviceCapabilitiesSilentInstall | + SWDeviceCapabilitiesDriverRequired; + + // Create the device + HRESULT hr = SwDeviceCreate(L"RustDeskIddDriver", + L"HTREE\\ROOT\\0", + &createInfo, + 0, + NULL, + CreationCallback, + &callbackContext, + hSwDevice); + if (FAILED(hr)) + { + LPSTR errorString = formatErrorString((DWORD)hr); + SetLastMsg("Failed DeviceCreate SwDeviceCreate, hresult 0x%lx, %s", hr, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + + return FALSE; + } + + // Wait for callback to signal that the device has been created + printf("Waiting for device to be created....\n"); + DWORD waitResult = WaitForSingleObject(hEvent, 10 * 1000); + CloseHandle(hEvent); + if (waitResult != WAIT_OBJECT_0) + { + DWORD error = 0; + LPSTR errorString = NULL; + switch (waitResult) + { + case WAIT_ABANDONED: + SetLastMsg("Failed DeviceCreate wait for device creation 0x%d, WAIT_ABANDONED\n", waitResult); + break; + case WAIT_TIMEOUT: + SetLastMsg("Failed DeviceCreate wait for device creation 0x%d, WAIT_TIMEOUT\n", waitResult); + break; + default: + error = GetLastError(); + if (error != 0) + { + errorString = formatErrorString(error); + SetLastMsg("Failed DeviceCreate wait for device creation, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + } + break; + } + if (g_printMsg) + { + printf(g_lastMsg); + } + return FALSE; + } + + if (SUCCEEDED(callbackContext.hrCreateResult)) + { + // printf("Device created\n\n"); + return TRUE; + } + else + { + LPSTR errorString = formatErrorString((DWORD)callbackContext.hrCreateResult); + SetLastMsg("Failed DeviceCreate SwDeviceCreate, hrCreateResult 0x%lx, %s", callbackContext.hrCreateResult, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + return FALSE; + } +} + +VOID DeviceClose(HSWDEVICE hSwDevice) +{ + SetLastMsg("Success"); + + if (hSwDevice != INVALID_HANDLE_VALUE && hSwDevice != NULL) + { + HRESULT result = SwDeviceSetLifetime(hSwDevice, SWDeviceLifetimeHandle); + SwDeviceClose(hSwDevice); + } + else + { + BOOL created = TRUE; + if (TRUE == IsDeviceCreated(&created)) + { + if (created == FALSE) + { + return; + } + } + else + { + // Try crete sw device, and close + } + + HSWDEVICE hSwDevice2 = NULL; + if (DeviceCreateWithLifetime(NULL, &hSwDevice2)) + { + if (hSwDevice2 != NULL) + { + HRESULT result = SwDeviceSetLifetime(hSwDevice2, SWDeviceLifetimeHandle); + SwDeviceClose(hSwDevice2); + } + } + else + { + // + } + } +} + +BOOL MonitorPlugIn(UINT index, UINT edid, INT retries) +{ + SetLastMsg("Success"); + + if (retries < 0) + { + SetLastMsg("Failed MonitorPlugIn invalid tries %d\n", retries); + if (g_printMsg) + { + printf(g_lastMsg); + } + return FALSE; + } + + HANDLE hDevice = INVALID_HANDLE_VALUE; + for (; retries >= 0; --retries) + { + hDevice = DeviceOpenHandle(); + if (hDevice != INVALID_HANDLE_VALUE && hDevice != NULL) + { + break; + } + Sleep(1000); + } + if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) + { + return FALSE; + } + + BOOL ret = FALSE; + DWORD junk = 0; + CtlPlugIn plugIn; + plugIn.ConnectorIndex = index; + plugIn.MonitorEDID = edid; + HRESULT hr = CoCreateGuid(&plugIn.ContainerId); + if (!SUCCEEDED(hr)) + { + LPSTR errorString = formatErrorString((DWORD)hr); + SetLastMsg("Failed MonitorPlugIn CoCreateGuid, hresult 0x%lx, %s", hr, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + ret = FALSE; + } + else + { + ret = FALSE; + for (; retries >= 0; --retries) + { + if (TRUE == DeviceIoControl( + hDevice, + IOCTL_CHANGER_IDD_PLUG_IN, + &plugIn, // Ptr to InBuffer + sizeof(CtlPlugIn), // Length of InBuffer + NULL, // Ptr to OutBuffer + 0, // Length of OutBuffer + &junk, // BytesReturned + 0)) // Ptr to Overlapped structure + { + ret = TRUE; + break; + } + } + if (ret == FALSE) + { + DWORD error = GetLastError(); + LPSTR errorString = formatErrorString(error); + SetLastMsg("Failed MonitorPlugIn DeviceIoControl, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + } + } + + DeviceCloseHandle(hDevice); + return ret; +} + +BOOL MonitorPlugOut(UINT index) +{ + SetLastMsg("Success"); + + HANDLE hDevice = DeviceOpenHandle(); + if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) + { + return FALSE; + } + + BOOL ret = FALSE; + DWORD junk = 0; + CtlPlugOut plugOut; + plugOut.ConnectorIndex = index; + if (!DeviceIoControl( + hDevice, + IOCTL_CHANGER_IDD_PLUG_OUT, + &plugOut, // Ptr to InBuffer + sizeof(CtlPlugOut), // Length of InBuffer + NULL, // Ptr to OutBuffer + 0, // Length of OutBuffer + &junk, // BytesReturned + 0)) // Ptr to Overlapped structure + { + DWORD error = GetLastError(); + LPSTR errorString = formatErrorString(error); + SetLastMsg("Failed MonitorPlugOut DeviceIoControl, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + ret = FALSE; + } + else + { + ret = TRUE; + } + + DeviceCloseHandle(hDevice); + return ret; +} + +BOOL MonitorModesUpdate(UINT index, UINT modeCount, PMonitorMode modes) +{ + SetLastMsg("Success"); + + HANDLE hDevice = DeviceOpenHandle(); + if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) + { + return FALSE; + } + + BOOL ret = FALSE; + DWORD junk = 0; + size_t buflen = sizeof(UINT) * 2 + modeCount * sizeof(MonitorMode); + PCtlMonitorModes pMonitorModes = (PCtlMonitorModes)malloc(buflen); + if (pMonitorModes == NULL) + { + SetLastMsg("Failed MonitorModesUpdate CtlMonitorModes malloc\n"); + if (g_printMsg) + { + printf(g_lastMsg); + } + return FALSE; + } + + pMonitorModes->ConnectorIndex = index; + pMonitorModes->ModeCount = modeCount; + for (UINT i = 0; i < modeCount; ++i) + { + pMonitorModes->Modes[i].Width = modes[i].width; + pMonitorModes->Modes[i].Height = modes[i].height; + pMonitorModes->Modes[i].Sync = modes[i].sync; + } + if (!DeviceIoControl( + hDevice, + IOCTL_CHANGER_IDD_UPDATE_MONITOR_MODE, + pMonitorModes, // Ptr to InBuffer + buflen, // Length of InBuffer + NULL, // Ptr to OutBuffer + 0, // Length of OutBuffer + &junk, // BytesReturned + 0)) // Ptr to Overlapped structure + { + DWORD error = GetLastError(); + LPSTR errorString = formatErrorString(error); + SetLastMsg("Failed MonitorModesUpdate DeviceIoControl, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + ret = FALSE; + } + else + { + ret = TRUE; + } + + free(pMonitorModes); + DeviceCloseHandle(hDevice); + return ret; +} + +VOID WINAPI +CreationCallback( + _In_ HSWDEVICE hSwDevice, + _In_ HRESULT hrCreateResult, + _In_opt_ PVOID pContext, + _In_opt_ PCWSTR pszDeviceInstanceId +) +{ + DeviceCreateCallbackContext* callbackContext = NULL; + + if (pContext != NULL) + { + callbackContext = (DeviceCreateCallbackContext*)pContext; + callbackContext->hrCreateResult = hrCreateResult; + if (SUCCEEDED(hrCreateResult)) + { + if (callbackContext->lifetime) + { + HRESULT result = SwDeviceSetLifetime(hSwDevice, *callbackContext->lifetime); + if (FAILED(result)) + { + // TODO: debug log error here + } + } + } + + if (callbackContext->hEvent != NULL) + { + SetEvent(callbackContext->hEvent); + } + } + + // printf("Idd device %ls created\n", pszDeviceInstanceId); +} + +BOOLEAN +GetDevicePath( + _In_ LPCGUID InterfaceGuid, + _Out_writes_(BufLen) PTCHAR DevicePath, + _In_ size_t BufLen +) +{ + CONFIGRET cr = CR_SUCCESS; + PTSTR deviceInterfaceList = NULL; + ULONG deviceInterfaceListLength = 0; + PTSTR nextInterface; + HRESULT hr = E_FAIL; + BOOLEAN bRet = TRUE; + + cr = CM_Get_Device_Interface_List_Size( + &deviceInterfaceListLength, + (LPGUID)InterfaceGuid, + NULL, + CM_GET_DEVICE_INTERFACE_LIST_ALL_DEVICES); + if (cr != CR_SUCCESS) + { + SetLastMsg("Failed GetDevicePath 0x%x, retrieving device interface list size.\n", cr); + if (g_printMsg) + { + printf(g_lastMsg); + } + + goto clean0; + } + + // CAUTION: BUG here. deviceInterfaceListLength is greater than 1, even device was not created... + if (deviceInterfaceListLength <= 1) + { + SetLastMsg("Error: GetDevicePath No active device interfaces found. Is the sample driver loaded?\n"); + if (g_printMsg) + { + printf(g_lastMsg); + } + bRet = FALSE; + goto clean0; + } + + deviceInterfaceList = (PTSTR)malloc(deviceInterfaceListLength * sizeof(TCHAR)); + if (deviceInterfaceList == NULL) + { + SetLastMsg("Error GetDevicePath allocating memory for device interface list.\n"); + if (g_printMsg) + { + printf(g_lastMsg); + } + bRet = FALSE; + goto clean0; + } + ZeroMemory(deviceInterfaceList, deviceInterfaceListLength * sizeof(TCHAR)); + + for (int i = 0; i < 3 && _tcslen(deviceInterfaceList) == 0; i++) + { + // CAUTION: BUG here. deviceInterfaceList is NULL, even device was not created... + cr = CM_Get_Device_Interface_List( + (LPGUID)InterfaceGuid, + NULL, + deviceInterfaceList, + deviceInterfaceListLength, + CM_GET_DEVICE_INTERFACE_LIST_PRESENT); + if (cr != CR_SUCCESS) + { + SetLastMsg("Error GetDevicePath 0x%x retrieving device interface list.\n", cr); + if (g_printMsg) + { + printf(g_lastMsg); + } + goto clean0; + } + _tprintf(_T("get deviceInterfaceList %s\n"), deviceInterfaceList); + Sleep(1000); + } + + nextInterface = deviceInterfaceList + _tcslen(deviceInterfaceList) + 1; +#ifdef UNICODE + if (*nextInterface != UNICODE_NULL) { +#else + if (*nextInterface != ANSI_NULL) { +#endif + printf("Warning: More than one device interface instance found. \n" + "Selecting first matching device.\n\n"); + } + + printf("begin copy device path\n"); + hr = StringCchCopy(DevicePath, BufLen, deviceInterfaceList); + if (FAILED(hr)) + { + LPSTR errorString = formatErrorString((DWORD)hr); + SetLastMsg("Failed GetDevicePath StringCchCopy, hresult 0x%lx, %s", hr, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + bRet = FALSE; + goto clean0; + } + +clean0: + if (deviceInterfaceList != NULL) + { + free(deviceInterfaceList); + } + if (CR_SUCCESS != cr) + { + bRet = FALSE; + } + + return bRet; +} + +BOOLEAN GetDevicePath2( + _In_ LPCGUID InterfaceGuid, + _Out_writes_(BufLen) PTCHAR DevicePath, + _In_ size_t BufLen +) +{ + HANDLE hDevice = INVALID_HANDLE_VALUE; + PSP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData = NULL; + ULONG predictedLength = 0; + ULONG requiredLength = 0; + ULONG bytes; + HDEVINFO hardwareDeviceInfo; + SP_DEVICE_INTERFACE_DATA deviceInterfaceData; + BOOLEAN status = FALSE; + HRESULT hr; + + hardwareDeviceInfo = SetupDiGetClassDevs( + InterfaceGuid, + NULL, // Define no enumerator (global) + NULL, // Define no + (DIGCF_PRESENT | // Only Devices present + DIGCF_DEVICEINTERFACE)); // Function class devices. + if (INVALID_HANDLE_VALUE == hardwareDeviceInfo) + { + DWORD error = GetLastError(); + LPSTR errorString = formatErrorString(error); + SetLastMsg("Failed GetDevicePath2 SetupDiGetClassDevs, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + return FALSE; + } + + deviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); + + if (!SetupDiEnumDeviceInterfaces(hardwareDeviceInfo, + 0, // No care about specific PDOs + InterfaceGuid, + 0, // + &deviceInterfaceData)) + { + DWORD error = GetLastError(); + LPSTR errorString = formatErrorString(error); + SetLastMsg("Failed GetDevicePath2 SetupDiEnumDeviceInterfaces, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + goto Clean0; + } + + // + // Allocate a function class device data structure to receive the + // information about this particular device. + // + SetupDiGetDeviceInterfaceDetail( + hardwareDeviceInfo, + &deviceInterfaceData, + NULL, // probing so no output buffer yet + 0, // probing so output buffer length of zero + &requiredLength, + NULL);//not interested in the specific dev-node + + DWORD error = GetLastError(); + if (ERROR_INSUFFICIENT_BUFFER != error) + { + LPSTR errorString = formatErrorString(error); + SetLastMsg("GetDevicePath2 SetupDiGetDeviceInterfaceDetail failed, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + goto Clean0; + } + + predictedLength = requiredLength; + deviceInterfaceDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)HeapAlloc( + GetProcessHeap(), + HEAP_ZERO_MEMORY, + predictedLength + ); + + if (deviceInterfaceDetailData) + { + deviceInterfaceDetailData->cbSize = + sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); + } + else + { + DWORD error = GetLastError(); + LPSTR errorString = formatErrorString(error); + SetLastMsg("Failed GetDevicePath2 HeapAlloc, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + goto Clean0; + } + + if (!SetupDiGetDeviceInterfaceDetail( + hardwareDeviceInfo, + &deviceInterfaceData, + deviceInterfaceDetailData, + predictedLength, + &requiredLength, + NULL)) + { + DWORD error = GetLastError(); + LPSTR errorString = formatErrorString(error); + SetLastMsg("Failed GetDevicePath2 SetupDiGetDeviceInterfaceDetail, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + goto Clean1; + } + + hr = StringCchCopy(DevicePath, BufLen, deviceInterfaceDetailData->DevicePath); + if (FAILED(hr)) + { + LPSTR errorString = formatErrorString((DWORD)hr); + SetLastMsg("Failed GetDevicePath2 StringCchCopy, hresult 0x%lx, %s", hr, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + status = FALSE; + goto Clean1; + } + else + { + status = TRUE; + } + +Clean1: + (VOID)HeapFree(GetProcessHeap(), 0, deviceInterfaceDetailData); +Clean0: + (VOID)SetupDiDestroyDeviceInfoList(hardwareDeviceInfo); + return status; +} + +// https://stackoverflow.com/questions/67164846/createfile-fails-unless-i-disable-enable-my-device +HANDLE DeviceOpenHandle() +{ + SetLastMsg("Success"); + + // const int maxDevPathLen = 256; + TCHAR devicePath[256] = { 0 }; + HANDLE hDevice = INVALID_HANDLE_VALUE; + do + { + if (FALSE == GetDevicePath2( + &GUID_DEVINTERFACE_IDD_DRIVER_DEVICE, + devicePath, + sizeof(devicePath) / sizeof(devicePath[0]))) + { + break; + } + if (_tcslen(devicePath) == 0) + { + SetLastMsg("DeviceOpenHandle GetDevicePath got empty device path\n"); + if (g_printMsg) + { + printf(g_lastMsg); + } + break; + } + + _tprintf(_T("Idd device: try open %s\n"), devicePath); + hDevice = CreateFile( + devicePath, + GENERIC_READ | GENERIC_WRITE, + // FILE_SHARE_READ | FILE_SHARE_WRITE, + 0, + NULL, // no SECURITY_ATTRIBUTES structure + OPEN_EXISTING, // No special create flags + 0, // No special attributes + NULL + ); + if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) + { + DWORD error = GetLastError(); + LPSTR errorString = formatErrorString(error); + SetLastMsg("Failed DeviceOpenHandle CreateFile, error: 0x%x, %s", error, errorString == NULL ? "(NULL)\n" : errorString); + if (errorString != NULL) + { + LocalFree(errorString); + } + if (g_printMsg) + { + printf(g_lastMsg); + } + } + } while (0); + + return hDevice; +} + +VOID DeviceCloseHandle(HANDLE handle) +{ + if (handle != INVALID_HANDLE_VALUE && handle != NULL) + { + CloseHandle(handle); + } +} + +VOID SetPrintErrMsg(BOOL b) +{ + g_printMsg = (b == TRUE); +} + +// Use en-us for simple, or we may need to handle wide string. +LPSTR formatErrorString(DWORD error) +{ + LPSTR errorString = NULL; + FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + error, + MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), + (LPSTR)&errorString, + 0, + NULL + ); + return errorString; +} + diff --git a/vendor/rustdesk/libs/virtual_display/dylib/src/win10/IddController.h b/vendor/rustdesk/libs/virtual_display/dylib/src/win10/IddController.h new file mode 100644 index 0000000..99c5dad --- /dev/null +++ b/vendor/rustdesk/libs/virtual_display/dylib/src/win10/IddController.h @@ -0,0 +1,161 @@ +#pragma once +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Install or Update RustDeskIddDriver. + * + * @param fullInfPath [in] Full path of the driver inf file. + * @param rebootRequired [out] Indicates whether a restart is required. + * + * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() + * + * @see GetLastMsg#GetLastMsg + */ +BOOL InstallUpdate(LPCTSTR fullInfPath, PBOOL rebootRequired); + +/** + * @brief Uninstall RustDeskIddDriver. + * + * @param fullInfPath [in] Full path of the driver inf file. + * @param rebootRequired [out] Indicates whether a restart is required. + * + * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() + * + * @see GetLastMsg#GetLastMsg + */ +BOOL Uninstall(LPCTSTR fullInfPath, PBOOL rebootRequired); + +/** + * @brief Check if RustDeskIddDriver device is created before. + * The driver device(adapter) should be single instance. + * + * @param created [out] Indicates whether the device is created before. + * + * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() + * + * @see GetLastMsg#GetLastMsg + * + */ +BOOL IsDeviceCreated(PBOOL created); + +/** + * @brief Create device. + * Only one device should be created. + * If device is installed earlier, this function returns FALSE. + * + * @param hSwDevice [out] Handler of software device, used by DeviceCreate(). Should be **NULL**. + * + * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() + * + * @see GetLastMsg#GetLastMsg + * + */ +BOOL DeviceCreate(PHSWDEVICE hSwDevice); + +/** + * @brief Create device and set the lifetime. + * Only one device should be created. + * If device is installed earlier, this function returns FALSE. + * + * @param lifetime [in] The lifetime to set after creating the device. NULL means do not set the lifetime. + * https://learn.microsoft.com/en-us/windows/win32/api/swdevice/nf-swdevice-swdevicesetlifetime + * @param hSwDevice [out] Handler of software device, used by DeviceCreate(). Should be **NULL**. + * + * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() + * + * @see GetLastMsg#GetLastMsg + * + */ +BOOL DeviceCreateWithLifetime(SW_DEVICE_LIFETIME * lifetime, PHSWDEVICE hSwDevice); + +/** + * @brief Close device. + * + * @param hSwDevice Handler of software device, used by SwDeviceClose(). + * If hSwDevice is INVALID_HANDLE_VALUE or NULL, try find and close the device. + * + */ +VOID DeviceClose(HSWDEVICE hSwDevice); + +/** + * @brief Plug in monitor. + * + * @param index [in] Monitor index, should be 0, 1, 2. + * @param edid [in] Monitor edid. + * 0 Modified EDID from Dell S2719DGF + * 1 Modified EDID from Lenovo Y27fA + * @param retries [in] Retry times. Retry 1 time / sec. 25~30 seconds may be good choices. + * -1 is invalid. + * 0 means doing once and no retries. + * 1 means doing once and retry one time... + * + * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() + * + * @see GetLastMsg#GetLastMsg + * + * @remark Plug in monitor may fail if device is created in a very short time. + * System need some time to prepare the device. + * + */ +BOOL MonitorPlugIn(UINT index, UINT edid, INT retries); + +/** + * @brief Plug out monitor. + * + * @param index [in] Monitor index, should be 0, 1, 2. + * + * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() + * + * @see GetLastMsg#GetLastMsg + * + */ +BOOL MonitorPlugOut(UINT index); + +typedef struct _MonitorMode { + DWORD width; + DWORD height; + // Sync affects frequency. + DWORD sync; +} MonitorMode, *PMonitorMode; + +/** + * @brief Update monitor mode. + * + * @param index [in] Monitor index, should be 0, 1, 2. + * @param modeCount [in] Monitor mode count. + * @param MonitorMode [in] Monitor mode data. + * + * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() + * + * @see GetLastMsg#GetLastMsg + * + */ +BOOL MonitorModesUpdate(UINT index, UINT modeCount, PMonitorMode modes); + +/** + * @brief Get last error message. + * + * @return Message string. The string is at most 1024 bytes. + * + */ +const char* GetLastMsg(); + +/** + * @brief Set if print error message when debug. + * + * @param b [in] TRUE to enable printing message. + * + * @remark For now, no need to read environment variable to check if should print. + * + */ +VOID SetPrintErrMsg(BOOL b); + +#ifdef __cplusplus +} +#endif diff --git a/vendor/rustdesk/libs/virtual_display/dylib/src/win10/Public.h b/vendor/rustdesk/libs/virtual_display/dylib/src/win10/Public.h new file mode 100644 index 0000000..d7f294a --- /dev/null +++ b/vendor/rustdesk/libs/virtual_display/dylib/src/win10/Public.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +#define IOCTL_CHANGER_IDD_PLUG_IN CTL_CODE(IOCTL_CHANGER_BASE, \ + 0x1001, \ + METHOD_BUFFERED, \ + FILE_READ_ACCESS | FILE_WRITE_ACCESS) +#define IOCTL_CHANGER_IDD_PLUG_OUT CTL_CODE(IOCTL_CHANGER_BASE, \ + 0x1002, \ + METHOD_BUFFERED, \ + FILE_READ_ACCESS | FILE_WRITE_ACCESS) +#define IOCTL_CHANGER_IDD_UPDATE_MONITOR_MODE CTL_CODE(IOCTL_CHANGER_BASE, \ + 0x1003, \ + METHOD_BUFFERED, \ + FILE_READ_ACCESS | FILE_WRITE_ACCESS) + + +#define STATUS_ERROR_ADAPTER_NOT_INIT (3 << 30) + 11 +//#define STATUS_ERROR_IO_CTL_GET_INPUT (3 << 30) + 21 +//#define STATUS_ERROR_IO_CTL_GET_OUTPUT (3 << 30) + 22 +#define STATUS_ERROR_MONITOR_EXISTS (3 << 30) + 51 +#define STATUS_ERROR_MONITOR_NOT_EXISTS (3 << 30) + 52 +#define STATUS_ERROR_MONITOR_INVALID_PARAM (3 << 30) + 53 +#define STATUS_ERROR_MONITOR_OOM (3 << 30) + 54 + +#define MONITOR_EDID_MOD_DELL_S2719DGF 0 +#define MONITOR_EDID_MOD_LENOVO_Y27fA 1 + +typedef struct _CtlPlugIn { + UINT ConnectorIndex; + UINT MonitorEDID; + GUID ContainerId; +} CtlPlugIn, *PCtlPlugIn; + +typedef struct _CtlPlugOut { + UINT ConnectorIndex; +} CtlPlugOut, *PCtlPlugOut; + +typedef struct _CtlMonitorModes { + UINT ConnectorIndex; + UINT ModeCount; + struct { + DWORD Width; + DWORD Height; + DWORD Sync; + } Modes[1]; +} CtlMonitorModes, *PCtlMonitorModes; + + +#define SYMBOLIC_LINK_NAME L"\\Device\\RustDeskIddDriver" + diff --git a/vendor/rustdesk/libs/virtual_display/dylib/src/win10/idd.rs b/vendor/rustdesk/libs/virtual_display/dylib/src/win10/idd.rs new file mode 100644 index 0000000..ff614c0 --- /dev/null +++ b/vendor/rustdesk/libs/virtual_display/dylib/src/win10/idd.rs @@ -0,0 +1,215 @@ +#![allow(dead_code)] +#![allow(non_camel_case_types)] +#![allow(unused_variables)] +#![allow(non_snake_case)] +#![allow(deref_nullptr)] + +pub type size_t = ::std::os::raw::c_ulonglong; +pub type __vcrt_bool = bool; +pub type wchar_t = ::std::os::raw::c_ushort; + +pub type POINTER_64_INT = ::std::os::raw::c_ulonglong; +pub type INT8 = ::std::os::raw::c_schar; +pub type PINT8 = *mut ::std::os::raw::c_schar; +pub type INT16 = ::std::os::raw::c_short; +pub type PINT16 = *mut ::std::os::raw::c_short; +pub type INT32 = ::std::os::raw::c_int; +pub type PINT32 = *mut ::std::os::raw::c_int; +pub type INT64 = ::std::os::raw::c_longlong; +pub type PINT64 = *mut ::std::os::raw::c_longlong; +pub type UINT8 = ::std::os::raw::c_uchar; +pub type PUINT8 = *mut ::std::os::raw::c_uchar; +pub type UINT16 = ::std::os::raw::c_ushort; +pub type PUINT16 = *mut ::std::os::raw::c_ushort; +pub type UINT32 = ::std::os::raw::c_uint; +pub type PUINT32 = *mut ::std::os::raw::c_uint; +pub type UINT64 = ::std::os::raw::c_ulonglong; +pub type PUINT64 = *mut ::std::os::raw::c_ulonglong; +pub type LONG32 = ::std::os::raw::c_int; +pub type PLONG32 = *mut ::std::os::raw::c_int; +pub type ULONG32 = ::std::os::raw::c_uint; +pub type PULONG32 = *mut ::std::os::raw::c_uint; +pub type DWORD32 = ::std::os::raw::c_uint; +pub type PDWORD32 = *mut ::std::os::raw::c_uint; +pub type INT_PTR = ::std::os::raw::c_longlong; +pub type PINT_PTR = *mut ::std::os::raw::c_longlong; +pub type UINT_PTR = ::std::os::raw::c_ulonglong; +pub type PUINT_PTR = *mut ::std::os::raw::c_ulonglong; +pub type LONG_PTR = ::std::os::raw::c_longlong; +pub type PLONG_PTR = *mut ::std::os::raw::c_longlong; +pub type ULONG_PTR = ::std::os::raw::c_ulonglong; +pub type PULONG_PTR = *mut ::std::os::raw::c_ulonglong; +pub type SHANDLE_PTR = ::std::os::raw::c_longlong; +pub type HANDLE_PTR = ::std::os::raw::c_ulonglong; +pub type UHALF_PTR = ::std::os::raw::c_uint; +pub type PUHALF_PTR = *mut ::std::os::raw::c_uint; +pub type HALF_PTR = ::std::os::raw::c_int; +pub type PHALF_PTR = *mut ::std::os::raw::c_int; +pub type SIZE_T = ULONG_PTR; +pub type PSIZE_T = *mut ULONG_PTR; +pub type SSIZE_T = LONG_PTR; +pub type PSSIZE_T = *mut LONG_PTR; +pub type DWORD_PTR = ULONG_PTR; +pub type PDWORD_PTR = *mut ULONG_PTR; +pub type LONG64 = ::std::os::raw::c_longlong; +pub type PLONG64 = *mut ::std::os::raw::c_longlong; +pub type ULONG64 = ::std::os::raw::c_ulonglong; +pub type PULONG64 = *mut ::std::os::raw::c_ulonglong; +pub type DWORD64 = ::std::os::raw::c_ulonglong; +pub type PDWORD64 = *mut ::std::os::raw::c_ulonglong; +pub type KAFFINITY = ULONG_PTR; +pub type PKAFFINITY = *mut KAFFINITY; +pub type PVOID = *mut ::std::os::raw::c_void; +pub type CHAR = ::std::os::raw::c_char; +pub type SHORT = ::std::os::raw::c_short; +pub type LONG = ::std::os::raw::c_long; +pub type WCHAR = wchar_t; +pub type PWCHAR = *mut WCHAR; +pub type LPWCH = *mut WCHAR; +pub type PWCH = *mut WCHAR; +pub type LPCWCH = *const WCHAR; +pub type PCWCH = *const WCHAR; +pub type NWPSTR = *mut WCHAR; +pub type LPWSTR = *mut WCHAR; +pub type PWSTR = *mut WCHAR; +pub type PZPWSTR = *mut PWSTR; +pub type PCZPWSTR = *const PWSTR; +pub type LPUWSTR = *mut WCHAR; +pub type PUWSTR = *mut WCHAR; +pub type LPCWSTR = *const WCHAR; +pub type PCWSTR = *const WCHAR; +pub type PZPCWSTR = *mut PCWSTR; +pub type PCZPCWSTR = *const PCWSTR; +pub type LPCUWSTR = *const WCHAR; +pub type PCUWSTR = *const WCHAR; +pub type PZZWSTR = *mut WCHAR; +pub type PCZZWSTR = *const WCHAR; +pub type PUZZWSTR = *mut WCHAR; +pub type PCUZZWSTR = *const WCHAR; +pub type PNZWCH = *mut WCHAR; +pub type PCNZWCH = *const WCHAR; +pub type PUNZWCH = *mut WCHAR; +pub type PCUNZWCH = *const WCHAR; +pub type PCHAR = *mut CHAR; +pub type LPCH = *mut CHAR; +pub type PCH = *mut CHAR; +pub type LPCCH = *const CHAR; +pub type PCCH = *const CHAR; +pub type NPSTR = *mut CHAR; +pub type LPSTR = *mut CHAR; +pub type PSTR = *mut CHAR; +pub type PZPSTR = *mut PSTR; +pub type PCZPSTR = *const PSTR; +pub type LPCSTR = *const CHAR; +pub type PCSTR = *const CHAR; +pub type PZPCSTR = *mut PCSTR; +pub type PCZPCSTR = *const PCSTR; +pub type PZZSTR = *mut CHAR; +pub type PCZZSTR = *const CHAR; +pub type PNZCH = *mut CHAR; +pub type PCNZCH = *const CHAR; +pub type TCHAR = ::std::os::raw::c_char; +pub type PTCHAR = *mut ::std::os::raw::c_char; +pub type TBYTE = ::std::os::raw::c_uchar; +pub type PTBYTE = *mut ::std::os::raw::c_uchar; +pub type LPTCH = LPCH; +pub type PTCH = LPCH; +pub type LPCTCH = LPCCH; +pub type PCTCH = LPCCH; +pub type PTSTR = LPSTR; +pub type LPTSTR = LPSTR; +pub type PUTSTR = LPSTR; +pub type LPUTSTR = LPSTR; +pub type PCTSTR = LPCSTR; +pub type LPCTSTR = LPCSTR; +pub type PCUTSTR = LPCSTR; +pub type LPCUTSTR = LPCSTR; +pub type PZZTSTR = PZZSTR; +pub type PUZZTSTR = PZZSTR; +pub type PCZZTSTR = PCZZSTR; +pub type PCUZZTSTR = PCZZSTR; +pub type PZPTSTR = PZPSTR; +pub type PNZTCH = PNZCH; +pub type PUNZTCH = PNZCH; +pub type PCNZTCH = PCNZCH; +pub type PCUNZTCH = PCNZCH; +pub type PSHORT = *mut SHORT; +pub type PLONG = *mut LONG; +pub type ULONG = ::std::os::raw::c_ulong; +pub type PULONG = *mut ULONG; +pub type USHORT = ::std::os::raw::c_ushort; +pub type PUSHORT = *mut USHORT; +pub type UCHAR = ::std::os::raw::c_uchar; +pub type PUCHAR = *mut UCHAR; +pub type PSZ = *mut ::std::os::raw::c_char; +pub type DWORD = ::std::os::raw::c_ulong; +pub type BOOL = ::std::os::raw::c_int; +pub type BYTE = ::std::os::raw::c_uchar; +pub type WORD = ::std::os::raw::c_ushort; +pub type FLOAT = f32; +pub type PFLOAT = *mut FLOAT; +pub type PBOOL = *mut BOOL; +pub type LPBOOL = *mut BOOL; +pub type PBYTE = *mut BYTE; +pub type LPBYTE = *mut BYTE; +pub type PINT = *mut ::std::os::raw::c_int; +pub type LPINT = *mut ::std::os::raw::c_int; +pub type PWORD = *mut WORD; +pub type LPWORD = *mut WORD; +pub type LPLONG = *mut ::std::os::raw::c_long; +pub type PDWORD = *mut DWORD; +pub type LPDWORD = *mut DWORD; +pub type LPVOID = *mut ::std::os::raw::c_void; +pub type LPCVOID = *const ::std::os::raw::c_void; +pub type INT = ::std::os::raw::c_int; +pub type UINT = ::std::os::raw::c_uint; +pub type PUINT = *mut ::std::os::raw::c_uint; +pub type va_list = *mut ::std::os::raw::c_char; + +pub const TRUE: ::std::os::raw::c_int = 1; +pub const FALSE: ::std::os::raw::c_int = 0; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _MonitorMode { + pub width: DWORD, + pub height: DWORD, + pub sync: DWORD, +} +pub type MonitorMode = _MonitorMode; +pub type PMonitorMode = *mut MonitorMode; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct HSWDEVICE__ { + pub unused: ::std::os::raw::c_int, +} +pub type HSWDEVICE = *mut HSWDEVICE__; +pub type PHSWDEVICE = *mut HSWDEVICE; + +#[link(name = "Newdev")] +extern "C" { + pub fn InstallUpdate(fullInfPath: LPCTSTR, rebootRequired: PBOOL) -> BOOL; +} + +#[link(name = "Setupapi")] +extern "C" { + pub fn IsDeviceCreated(created: PBOOL) -> BOOL; +} + +#[link(name = "Swdevice")] +#[link(name = "OneCoreUAP")] +extern "C" { + pub fn DeviceCreate(hSwDevice: PHSWDEVICE) -> BOOL; + pub fn DeviceClose(hSwDevice: HSWDEVICE); +} + +extern "C" { + pub fn Uninstall(fullInfPath: LPCTSTR, rebootRequired: PBOOL) -> BOOL; + pub fn MonitorPlugIn(index: UINT, edid: UINT, retries: INT) -> BOOL; + pub fn MonitorPlugOut(index: UINT) -> BOOL; + pub fn MonitorModesUpdate(index: UINT, modeCount: UINT, modes: PMonitorMode) -> BOOL; + + pub fn GetLastMsg() -> PCHAR; + pub fn SetPrintErrMsg(b: BOOL); +} diff --git a/vendor/rustdesk/libs/virtual_display/dylib/src/win10/mod.rs b/vendor/rustdesk/libs/virtual_display/dylib/src/win10/mod.rs new file mode 100644 index 0000000..7b787ca --- /dev/null +++ b/vendor/rustdesk/libs/virtual_display/dylib/src/win10/mod.rs @@ -0,0 +1,9 @@ +pub mod idd; +use std::ffi::CStr; + +pub const DRIVER_INSTALL_PATH: &str = "RustDeskIddDriver/RustDeskIddDriver.inf"; +pub const DRIVER_DOWNLOAD_URL: &str = ""; + +pub unsafe fn get_last_msg() -> Result<&'static str, std::str::Utf8Error> { + CStr::from_ptr(idd::GetLastMsg()).to_str() +} diff --git a/vendor/rustdesk/libs/virtual_display/src/lib.rs b/vendor/rustdesk/libs/virtual_display/src/lib.rs new file mode 100644 index 0000000..6d602aa --- /dev/null +++ b/vendor/rustdesk/libs/virtual_display/src/lib.rs @@ -0,0 +1,196 @@ +use hbb_common::{anyhow, dlopen::symbor::Library, log, ResultType}; +use std::{ + collections::HashSet, + sync::{Arc, Mutex}, +}; + +const LIB_NAME_VIRTUAL_DISPLAY: &str = "dylib_virtual_display"; + +pub type DWORD = ::std::os::raw::c_ulong; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _MonitorMode { + pub width: DWORD, + pub height: DWORD, + pub sync: DWORD, +} +pub type MonitorMode = _MonitorMode; +pub type PMonitorMode = *mut MonitorMode; + +pub type GetDriverInstallPath = fn() -> &'static str; +pub type IsDeviceCreated = fn() -> bool; +pub type CloseDevice = fn(); +pub type DownLoadDriver = fn() -> ResultType<()>; +pub type CreateDevice = fn() -> ResultType<()>; +pub type InstallUpdateDriver = fn(&mut bool) -> ResultType<()>; +pub type UninstallDriver = fn(&mut bool) -> ResultType<()>; +pub type PlugInMonitor = fn(u32, u32, u32) -> ResultType<()>; +pub type PlugOutMonitor = fn(u32) -> ResultType<()>; +pub type UpdateMonitorModes = fn(u32, u32, PMonitorMode) -> ResultType<()>; + +macro_rules! make_lib_wrapper { + ($($field:ident : $tp:ty),+) => { + struct LibWrapper { + _lib: Option, + $($field: Option<$tp>),+ + } + + impl LibWrapper { + fn new() -> Self { + let lib = match Library::open(get_lib_name()) { + Ok(lib) => Some(lib), + Err(e) => { + log::warn!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e); + None + } + }; + + $(let $field = if let Some(lib) = &lib { + match unsafe { lib.symbol::<$tp>(stringify!($field)) } { + Ok(m) => { + Some(*m) + }, + Err(e) => { + log::warn!("Failed to load func {}, {}", stringify!($field), e); + None + } + } + } else { + None + };)+ + + Self { + _lib: lib, + $( $field ),+ + } + } + } + + impl Default for LibWrapper { + fn default() -> Self { + Self::new() + } + } + } +} + +make_lib_wrapper!( + get_driver_install_path: GetDriverInstallPath, + is_device_created: IsDeviceCreated, + close_device: CloseDevice, + download_driver: DownLoadDriver, + create_device: CreateDevice, + install_update_driver: InstallUpdateDriver, + uninstall_driver: UninstallDriver, + plug_in_monitor: PlugInMonitor, + plug_out_monitor: PlugOutMonitor, + update_monitor_modes: UpdateMonitorModes +); + +lazy_static::lazy_static! { + static ref LIB_WRAPPER: Arc> = Default::default(); + static ref MONITOR_INDICES: Mutex> = Mutex::new(HashSet::new()); +} + +#[cfg(target_os = "windows")] +fn get_lib_name() -> String { + format!("{}.dll", LIB_NAME_VIRTUAL_DISPLAY) +} + +#[cfg(target_os = "linux")] +fn get_lib_name() -> String { + format!("lib{}.so", LIB_NAME_VIRTUAL_DISPLAY) +} + +#[cfg(target_os = "macos")] +fn get_lib_name() -> String { + format!("lib{}.dylib", LIB_NAME_VIRTUAL_DISPLAY) +} + +#[cfg(windows)] +pub fn get_driver_install_path() -> Option<&'static str> { + Some(LIB_WRAPPER.lock().unwrap().get_driver_install_path?()) +} + +pub fn is_device_created() -> bool { + LIB_WRAPPER + .lock() + .unwrap() + .is_device_created + .map(|f| f()) + .unwrap_or(false) +} + +pub fn close_device() { + let _r = LIB_WRAPPER.lock().unwrap().close_device.map(|f| f()); +} + +pub fn download_driver() -> ResultType<()> { + LIB_WRAPPER + .lock() + .unwrap() + .download_driver + .ok_or(anyhow::Error::msg("download_driver method not found"))?() +} + +pub fn create_device() -> ResultType<()> { + LIB_WRAPPER + .lock() + .unwrap() + .create_device + .ok_or(anyhow::Error::msg("create_device method not found"))?() +} + +pub fn install_update_driver(reboot_required: &mut bool) -> ResultType<()> { + LIB_WRAPPER + .lock() + .unwrap() + .install_update_driver + .ok_or(anyhow::Error::msg("install_update_driver method not found"))?(reboot_required) +} + +pub fn uninstall_driver(reboot_required: &mut bool) -> ResultType<()> { + LIB_WRAPPER + .lock() + .unwrap() + .uninstall_driver + .ok_or(anyhow::Error::msg("uninstall_driver method not found"))?(reboot_required) +} + +#[cfg(windows)] +pub fn plug_in_monitor(monitor_index: u32) -> ResultType<()> { + let mut lock = MONITOR_INDICES.lock().unwrap(); + if lock.contains(&monitor_index) { + return Ok(()); + } + let f = LIB_WRAPPER + .lock() + .unwrap() + .plug_in_monitor + .ok_or(anyhow::Error::msg("plug_in_monitor method not found"))?; + f(monitor_index, 0, 20)?; + lock.insert(monitor_index); + Ok(()) +} + +#[cfg(windows)] +pub fn plug_out_monitor(monitor_index: u32) -> ResultType<()> { + let f = LIB_WRAPPER + .lock() + .unwrap() + .plug_out_monitor + .ok_or(anyhow::Error::msg("plug_out_monitor method not found"))?; + f(monitor_index)?; + MONITOR_INDICES.lock().unwrap().remove(&monitor_index); + Ok(()) +} + +#[cfg(windows)] +pub fn update_monitor_modes(monitor_index: u32, modes: &[MonitorMode]) -> ResultType<()> { + let f = LIB_WRAPPER + .lock() + .unwrap() + .update_monitor_modes + .ok_or(anyhow::Error::msg("update_monitor_modes method not found"))?; + f(monitor_index, modes.len() as _, modes.as_ptr() as _) +} diff --git a/vendor/rustdesk/res/128x128.png b/vendor/rustdesk/res/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..306d505e998367c072b87cb1ca7242bdcc25c469 GIT binary patch literal 2978 zcmb_e`8yQ)9{vt98hf@vXbdSShfXWS$TG-jv4l#dLpMuQ$G&Au*-Dh9jEL%HCre}< z29YdDlP$~G#xiCw2D8s~f4P6beV^z3EYI`)_`c8QlX&?eT3SL?0stUwV{Ku-)3N_0 zTy&>0$uHOdh)^%vIa= zJdn%;?^ZxA4LoLn%r)?G9b~P7hFOrt0H5bT!7>QugAY^?%>g-dP)z|POQ3cR6fbf4 zLQuHO69~Z@I%t{!6$_wpK_CHPKJwbX`>ul< zSk5(h#{D|ODd^$giJbIij#&n~aFAt~%-Wy8deyldQM>K>Vf$tGmS5$j_KVH>8Rok| zWXDuF8c&&2u7z>H1zl zdac0!t$@zr>pkRyQ2z7=ue6&ty3URL%=t9J!jx<)CTv^gY}HO|?oQcMc(zHPFfrxK zr0$K__Kh&YhGXeQ{%^+H;dOuFIx=e=+q1emm!3$XU1_A*e5ReRqDg&N(JrAH*DNa( zFY8qN}j;w??Qe6iIfzI4a#s)6RJv4fL03Q(s2fZN$vm^p@y_5BWtRxniFwf1tF&LA6&$HgP!HNGraUB6aK z6ua9xG9o4w@;?aE%*F0T{nLEGq?9+=y5XlktaC}#JgMVC*2$nYs1*xssAHXJ@eo71Pz@D=?hEmfpPo1mOAjHef9JfkQcF>GMiqD+3 zf5(iD5?GMqw4ZEq*%8*{IBSg`8;C@?ae0qa_f>bX`wsSk$bIKp{cP4}xhWf5^t;Dj ztGcy&j=fc^lQ*pvv6gBupQlWh^@n(y-gWRj@H%c{q9WLvWsYCY?McjVHqcs>I1yqJ zUi3Ss?^m(Y_t&THu^FQoj-@Weq}OFyr_R#u{W_zlGOLoh90n__*(Uq82T*!ljok^6 zmCz5i>4)S_Fl_5mG#(iiX&&{G8Esh^quDAQp7Rt>Kdy3u;S{ZM{9ume39qF3?bz2D zdc$RNwwG&sNSz8M*9y#4&CfrZe}rs&sqc2}b>4De#Dyr7gS@J|mnzTZgNZ#T2h~lRc@`S^R#g? z8>SZER*tY z&oy15p0AOFzOnL3$sk({fi}BQ5pFbJwBO^|rz^qYnUQCA>AOsr&lFCXP*imfk1Ms3 zPN8KScQnrYk&;i+Bp3o?BjdRHM?8ZRa_yjhRH01XpBr;Ts4pd;x5bV2B4hcCjS~4B zLiX_KCFusOoZq-oN3@F*B)L7zfcOOr?EgpUdXd$pv*>_peSim!oi2)Lh%L_;wpYT2 zq5SPC+Qif}X+M5(&bh({JZ}2i8JN#NtkIt_xH7I2n0=~f2G#RxRcg7u`0$qQK2-i5 zA~faDeyogD$Pucp0_Keykdw-SW-pnlDI`6|1MaLy0yzP{7ekAq;cHLc5mDnyAaMBf zy(s5f5&5U4?Xc0$RQqurX)(rT?uJD1J=oxHFZ*08I$rIsh(29=@9ZCjz|-fX#r=*B z*^0@w;Bu<2q=Du>qSiOrJZeP_)nHHS@vW~P?szYd>@6gBpvV@A-@vv@0mxI3Q zw*|_-%%A~{8=^WvU&6h8UBSrBU2@ETwHD~EO8=WRS65)&qL(ZTEN5~bxtKl(xTn(^ z^7$Zg@iqmvCh%5`%+=ujqid~*2nj+t@tJ!H>6 zohm(;VhJo)^CB)-?VhG<$LWCPQ56!z^G;c&2!RuTR9>)qaZDuPgtUjmSXcnbH1x%T z!60M!4M^{MU;Y7diRl88T1qSabpUkMvb74^9j1<%;(~^P-)B4q&ovYF1d`m{8E%BQ zu(9oNk#fj@=G-fcEIO+GyF|uM0%AY}q3+99+zIdBNO=SE@FNq3C_~#ep@_?jioZJK(9qz6vzE*QecnG# zc0Rl{Ies%rwm19O?V%WRG({tYu2&8FD%uKJ;e72^qL$6#G^4Z)vG%4V2^cwuTHbp? z0yzn00c@kb8zFIb9wMM#hMhkhEbb49A;Aa2tAYe_n)+if(G?n&2=5;=gQG%@Z^+AK ze~M9VZM<_7Ld=3aM8Lf&8~3yz{wBask{LmZ2kqO|Qzap%oFVsq==%xz)a5kBUUUE`-q)2br z;x5btWD_KH)3iefoE#**{9%DLWwK`#FI~EG@%8&!ngzK!ZlzS=6d1;9_OMm2EbV+B Nz{c{TMfrIQ?myFd6OjM_ literal 0 HcmV?d00001 diff --git a/vendor/rustdesk/res/128x128@2x.png b/vendor/rustdesk/res/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..89abf23a68ae85d0f28cdb90d1b08ad36db9d2c8 GIT binary patch literal 7689 zcmW+*c_36>6uVN*oPw)Mo}UBUd?UGtoBtN=1crvD;h@E+g~>{H295=#6hKm@)}CX=X&ie2so#5hYl)3^Il9}zDRt+sVNp94V%&|}@t zgqOl=J2<(iGZg3?j}WtLBW(5(_}&2l1%~NL!?^2vK67Y^}2d zKGm1x8pT5_BTm&Ar%EX>J!3F~lSTUj9!lYb=++CYI4?K(1^9tXdKSQm6L;fHm5Rvc z#4Wvq==&%TKDDv`JpIlfGd&Kc3rn6y(wQnyR1cge{P42F(W#qAe4%kL+1zjf)lO%G?qVUB9f2iFl>mVV=p-b|i0EpFHc-6SM67A}k7IE+9I_N>r4L zXpR%Qn=yA)=at+?>n|po-VwLL6wBKjZ4#CZSA9pD!rz_OCL9H9B*~XgA_f-EYx&l8X_{|%RJU>qRk)@i=iy|2$H#4k&g~5tFzAwKNif4JCY%=2xt z{CI?}N!{i>!Qi>wsNjD@Vgpz4>5-}u(&OK>uenFCwJzgYB1E1VC%aRfo+pgsyB?VS z+#oQ~?yqV=wCdeo*fUMG9nn!2{Vmy;14dF>5kppG(o_Bk0)8vov-bqObjNQgHz2~^ zm{1R&ZutFK`|o1%sf;@B^`g&gV3t#}=49_T*1J*akHfpRF|hyM0SlV39&Hju3I)F*AGv24~ z0yT-FY38PKiAMh5PyreZ`Fd48`EK3t_E$$%&b3~21f$I-=DWa|OF#Pv8+}>*`$0wg z-lWra;X*Cq?+d(CbA+(?e%dCA&%C{p2Z?LP& zJgRvQxUi3cibGL)jH?{(C(-V{t69TB2m?xlu7g9*A6!o30xaL+z<#6;sR)WAM zwD9A=s=FX>>tc;D4l~>Q^v*3GNmkJIZ*Z(oSS6>2Fw5IN!4B=<=%xRz6cs{PwM(MS zjZ&ptu9g@E50p1%lPm=q-S3vM=V<8OQPwMvMsKVn+>}q)wb}>bFL(4j-<)kLofJTB z-zEOUvVuj$rR^H|;{Vl^A>#y3Eus{#FZS0{)hF5Rv*kq+lBsHC^SFKBqFlKe~v;&(U>4u5ZUD1(P@v6r2w7h zdZxBlIE)l<@DNSyesSA_5(s)vs8*t~vRa@77Tq}V8oef8dBLTc2#>=Ya4lKi5$>vc z2KjdbMVxS16)bf3B^ko&c*9&u~yHL9Rqv*@QL##SyXHei0P?O%&)LRW_x; zQ!D+iiw1S&?H3ZwM4Wa0hn@Zf^$!N)#3xN-+<>rqotjS059O-fhd6M?pGu;x*dZ{2 z@bS`$K@!VR;#v2vChw-f;5r|EJCP?~c=qqrjc&u7I?8P^_5(jv>w3uOH7AEIE3Fs= zU3|(Z(Tmr2=@two!r@kShRJ4MmX!5na}^KQ^walHPkYsUI<)e`Bpf{a$IyJ4E|1(D zjpJhjEf}v>4$m|X4Nap}#dnWxI^=<>@D}6y+A(3*K}HK^LOk%$L&t~ie*JGlya%2h z<7c10I?|x_@t(&8?M^>k>C+*&4HSssU=H&ATY6LsI=-%>sVuV;t0 zMGH*44;-vgO5*`mbtlFqwo}s}EDn8ES&^1Gs&yR<^8~$ic7#gT zm9=28R&P3LIaB?VH%7vzZH0-W(h3b8_bE(iB}&X~#1W~rtL~ph^V+U4JS-!!d!qWb zS1`=31cgs$#&0ny6|PJVczzt_Iv;Q`ASEx1Zh5?XG;0= z>G1o8;7oNf^DG;3L2LD?ShdYpm8WgVFOWp?M}(f9%3WotQ12aMN?*}o?!XTz@|xj_ zK4)vSci^6{h6As~!~J|{M|s_YPJVuj9@)0LE^5opEz`E7=d)3>=b&oKjUecthh=@e z)B;3TL26sx>J1C#?|4&)KQy=D@@wG7=dGc?ks=8ChHg^{QQ1tic^tDr-$pjL^N(2` zd3MtFhh9LvF;C{#6n>C&T7D*I@k0$n^+Io7G|sB|Ngpkmd1@Ep_+8JxrkJPx`{x4| zra;q|yU8Iad0Ymq+LBSVR3b8Fbs$Q~nB@KrH z4WbG3O{sMumOvTK(Tsu*(ZfO6l1fc_3IP0&4_`XG{<6d64D=n$p$ik*5}qDnP~4_wa72}X49C;hRw z^+NaJ?l+7jW4De2*4lP79KI*SL_){-n$Q`X?Bg8HZmPJB@ijrKzP6A%EIfEy>RF+o<=&2~oKolY9 z++ALKT0mp=4?=8@mi}d{yOJ%tHuH*2QM>N5Rl5+C0g~M^=>qM(+8PCao zMf2$zvOv{XDI<@M_MW(vvF9#vI0qI5h86K5@T_z>}ikm$g zu=uQ!9-elhwz>zZA}#&%63JJAGfG35Q(wr|+q^YKXAwJ58lCf^fVpp387Q6}614UhP&K4K_{X1m{&bPNw)~Sm zE|SK@h+T-$^}b}U^Y&7|DpzV!^y)n{jnIS%uQ|5p3Z98QY70O1m=7X{Wtm03tdXcZ zZ~2S|5|1r=lLu6bMek%KyIQg%vs^86>rzw~W6iT`4C$Xd28tBgVuAcnKw0Y-Gzl@! zW)`X88)$-{@IX`db9O;}@Wb27C#-~#or38_N2jnq&lR}^|Gv&#feH5wd>ac07cUwh zD6EjHYl`tohX7Nd;Obnh0eyZxdmBYuz}3afZG;-Q!TujBWvQEa8hBr)M#%YYqo~hP z;+E^R38I);y@Qs+?_}_@efjh3oX!$9%*sP7-xD@dJIlDEv*n8G?d>g6CEKGIfVNfD;@hWb-zGD^})Q_2C=& ztWYa=(HaYV{H}bW>54Np$i+!4RwjC8U68iQ2|tc6*6)DrADfHXw@840L+5143dNiOVT}c*2)%$?sS{?HHb8sNv{2 z#d?4;Dai&ZC4-Eg1I&#A6pOn<96vhb+}SdHKr@=oj}RwuCw5Kj7L^x+ZUpHC|0W<7 zBv>e5>YtqeXhW?AA)<)_Ml(2{8)!~m;GHyBk__6+gy&$J3P^l-&l%f2#U~EBf7+sm zUwAb2F|Nq8-;e)C+!%Fb4_NHmJ;aS(-dwvf0m*V81V?W?v<`2@<4SEprDV4cj(=CC zg7xA5fVVw3|1t#)VAv-Sx){c8GGwl3Kcnoa>Pw=oPu+t>linyI7ur7{gb~*JqCtfX zaAG!KO8F15@m+IIBLA#rIjr}Q@TsFf%#<$d_x3LnqwgUH`mlHGSnm%wExi-{Npr?#dK=IhnC>M z@9s=iN0)oI&vWBKMNn`YNn5B}_UauUKY3)&e{i&3anMFO%Iz|YWelBUkCl!Ny3miJ zPNG9%O2xhzgaP|6+_nFoX)kF3hO6B*I*8UR zoAxR?92%H_V|<;!Ko}rE#JRg&TJVM%(8Vh=XY53)K+uR z#3=s&`1IO{^_i>|Ek|9Q+Zd?9(Z>Sty9h@6Wfaj04adg9u--O-_x%%oH>m8jTDu6$ zXIw&5Km}%FI}Q3P-8ANX(Sl9f*-RLDhpR$1aElWRBqY27IjRtM4L9U+KxED0?|CP? zy_ot%o)fXbOKcF(77UxeNfmcJ)}!bXHd0`;I$_P>cay4+y>ACasWC)Qc%esmoje~4 zoO;DwtxG4*^f8k%e1msTSUg34cTTR&Smq8 z2mnjW#%Ffvzf#fePLR8yqopKL?~E9Vp#cv0?FZTtjLnbIkx~x^vlf^A;;B`45_b{a zW`=E1{;*B$_M#v~o<*-7n*`G}QJ8~W!v<`Mf`P{-Uj@T{#!>b8ox=5hKK$$iA2w%+ zAR`|nRuFKw+}w-Tp8hZ3)TT4j`0;Jq#49`sJmH>R+L}OBYT{Frr~+oTN5h)-t`(oo zq|uUQ(^gr+2M_;JKLCXS0~&1_O!R1Eu9L^aLeY*(wcsK6o;*SC^WY)6A(Vo7*#NgB zHfVBeSU!zQ32E1>3@$cVi(7iBm!hjiQaiBG^gn z%s+#+dtDuhM<>4sk_Pe(Z4frma44mtkVgf*tJ_xF6m=Vq-c?qA3g=ql;fElMKbQ`D zkD`e3)CW=U9`E4Pr|a@(_x)v)Uqnf%?d9-c4YG`Vd=3^9x!M#h%MN$%u}sYpT}XH4 z<_G5_02m{_{S$RekN|g$>V|KN>eK?{wX?fCpG+lx>wSU*SEU>OC_nzpQ}0duc)lxk z#k{p{`e!$R;ocu&N}2zK$UhI_u*BO#MZQ@o5fjnFn7S*RoRJtlAVEk}XuA z?8Oe+o?cQr3KVWpA22CAoebC9>=`p1Ylk`ikm=J+PEy;;OPW>?ae->ed|crsk-1GV zfa@#alEVRanCiwD(W8SUi}teh`wqVllfTmq=`;;<;|4$uqN{O#u&TV zd2lIu(tDcqC)RlCJyR^tLEO@xN8PW!PumZ3++p}wSoFNUgsf`{L`wj=EPfh+>R3?c! zL(^0(VO$Cv&?MB?GdcsYk=yzmSKRTrJ}v<({LV8#aY=3SR}KfQs;B|x%~99VyvO#K zU>(=n$0j1eB=}(K*s`VHlM#MSn@((7SzH!pAx*CfrOx!6QCNsM_5A3RQg}4UPo`Ko zUV0S8K@!?p3D;%1=Ugiw=JHN2U)QI*(BpyJNlS2k00_C&Hc3e zoE0T@L_=2Jt#1C*-MX9p%+KT)#ad3fC5Hn7>9eZmDD2o_#MeBjBaPT)g8qw}B#QZLIc}^p*PI`&_Px{5cd??<=8y#dG670?h~Wp1IG=m zR$EMd#$*e~$)9}bs8jQlS}8ES`6*FeAo;T!hgpO0>|zUC-HLf}1O=P*Dn>2RXsTP&G!1b>JbDkN#6#puZYLW8jMNTgAdEcVVc5p z6W(R7$egSiPBb9>>alBs2_6M5u#BlG)8bS1a6NCHg72a0H;CAv>H5QW%Par_drPA0wdv;5yX3Rg8K zdw^DJIfwxBPVD?#J-0BY2y~q)-l3woH<^t2=p^?6=sNN5k1&8F($&J`_K#MGsUFaQ zr;2j$vSkG%xt#b#?pM~rT>P-&srlgl%@4l~GhsRwi_TG&?#x_>fKh%p_csy3?v>a^ zY0{42R3rvmKWmwDDq0{P+ih))?95BMrjBUhzbIyEqL?A;tD%-9oO|-cG^DE45G4rX(<;1qkDVbc-Z_D&0rs}dOhCN z&j%0R6)fx75^(hwlBm;b8!RGN4|Ud`5`c|QG9qukkT-=(3ZIG^5jV6h_YP`ry!Eb> z#G*%(6k3NPg@4{)6QUxDcrSmwTwjv+Wv@&oo?Q}t_cmO#4d3E$6Yj-7P`U7-tb6tT zZxb9Q{Ld}b=dYWJ<06_{<>We*jvbk}i2>I@C;Em?f=D=!a{+2BMPIsE|ja8cip zR8m{|+bA)9q_Z$7jI$$XsU6Bsj0+j&)F8T~IR8p!CJ(;li$;;sO(=gpa3nXL$Bv}t zZS?AM(#y;goZnpknmwUXh$QtH)8jvCNe8HWbTLyH&)}d4(6P0{b=<%!C*H=7R)jVe6TPs4>Ve%fkR^*^?is&Oum+R6BX`C_JTaaiK!UdBgRu}nw5k-T6 zDsG2?n`S_|y=dGUxe2mlfcA*!S){fOd1@LT!CawSir233>4m=lZ0OlF*k=%X0<{SN zIYt>Q+sV|g(Ju%QCb>Pr^1f5E-(CcYbK%ZlZ}&g*uyp+Wvh|VTae~_|%Y>+PQ_5_& z9^-88adS)lGs8vfW(%&+?01NwE@XY^eRY+0Mr)k8h@Q(Y#-eFcGVafgRQY1*{UUvHz_@{8%P80ney%qBGx(kG1lfTQX3 zQgvyfrlxnw+GZF;kqp8pke^ zh)1c+AtzIdLUA*FEVu<7Hw|qme6JWqz^#S@_+~(%;aYp&1?51Y2f}6x*dzP-WD>L5ZFQ5XPh=8+smsYSp2k9DJzhWSBQ3o^CF zBUS1UF|Q$P=sb9Gj!QmY0fx=@r!A@p5E}v!EGbf@YEKa{3uD<|V14Ehc2z9i&fK1R zWMN0IB?A%k1+`JP&G$xD2ERp$72T0+`U~jKm>779pjlA46uuq2AO#}4>cff@f$7fa zH+7_ZDu0CBbEyfCJz&XLm?W56M}efX*7Oc)3-Lbnn45|X>j|Pxqzpa+3)w^2q+%+X ho=43ftq!j;?{b%9>l7=Jc#i=1hc~u5@yyWe`hTNxznK64 literal 0 HcmV?d00001 diff --git a/vendor/rustdesk/res/32x32.png b/vendor/rustdesk/res/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..f9b1fc6b64987444ed9764f6e63010fbd78a6d80 GIT binary patch literal 938 zcmV;b16BNqP){r>=h`2elx0FmHYzC{Q!FV0E_hij`RSd>;R?f0;lQ#w&Vf2al_xt|w_5O*e{_yzx==A%x&HKsX_W+dg0G9CpnD79b?=q(D0G;j^ ztmy!%=}Nlj>F@vL>;Kv2{NeBVh_CvEviJ*$_P5&fG?w%dmhy(d@nOE}W5nq0^8ef8 z|F+2fXqWw6k^MA({ma_?40-&q%KJ);`$~@b+VA+M$M_kD_Y;Wsda(90koKp|^|aXZ zw%_p!ned9p?uEzh6rS#h(Cag->kO*tUc%@*wde@0=tRQgGrr^ux#LHK{zHQP#Mb-6 z*86Fj`+=+aJd61`ius+z^_9W(vDEa4y!0-W^n1AS7MSsSyzmvA?-88uiqGtX(CTTy z>SxF2H-LuP0000AbW%=J_XqUASBU^t^}!N}I0^s&0ozGLK~y-)g_Czf!Y~lUi{Qeh zVnvaP3tBcf5o8bd-h++c-g|v{lZHU*y#Lz!wU>J@k7-hA)D7AntD#k+YGPE4jP}=J z8dZ#j?gNY6ZUHpW%&2P*Y$%pa`TfP*zLl-LR5LB)V= z9q%6)oWz9LTy~RRQ~}3bwxfe0K3qybZ#x*9kO`mf5E9c?tpWfWiIGuAj3@WJ-dLve zj`}Wl9SFvC!y}{5Hj$OLaijlRr_1XEU?Y7x-SFeJo)REbG@Em56)^6YAYr5Xt`t0J z#aabCQQp)S7io>%C8Wnb20+-r^QH((18fFu`wC>BQ{oJK)T6X%Pu|vofi;dZY%VDQ zuMaBbw_{+no8!I%P*Va(UEQ4F%ZOW~dJ2yK&R$4_6^FDuAz#t}uQ|62iOT>Dh{)SQ zj0m+T^md&)sJ{LIIhPL+shxr_KSlR%UXZs*mY6krEca&u*sS1oLiSdy--mz$O_HuI zR!+}Ww(pU*W7%DR0aC^VwC&aw51oU=0WyyUW)=jrEmU155;lic2~3>`6cOJb<})OA zVAi>~jEU$@Bv`xlba0GwPHA;b3VV!naFo&Ogr$t$zv2Be{%RQiPX_T{6NtHJ-v9sr M07*qoM6N<$f|zChl>h($ literal 0 HcmV?d00001 diff --git a/vendor/rustdesk/res/64x64.png b/vendor/rustdesk/res/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..5bc11ece0f3ef63588e142f555141e196931723f GIT binary patch literal 1795 zcmbVM`!|$(AN|fyWQai|dO3=vW!#sdI+2EwYf^DsyQrfhI#G>VZlfV&P@y79a*J`l zMCObbPB@ejxx|QJ%otgH2Z4AI zND%Vza2C zUx>yFFO>>D(s^EQIHh0KQz-0MGW*5&nrZdw>8@pS(z0U*9o@bl*Gc;{BVAy@1~KsT zilpbOlam{yxh+M0ragVa5gtUPF+zgFL=9fW@^;R4FK8Q&e-GLK0U9;Hxtw> z)B*dl-@7@LFk+A8b0mRr+^^zOt#(+7R;EQ1MWcl2uE6Pax+*Nx(IHXO@1qSN)#sU$ZgxS@xK~5;A)|r8W8TwwjdIPQpKA&YnQ)@~nO#`$&7yR3NytDWL)iLJ_a zn3Gv%{xOEzOJZk7a*%Tr^=G^L?ZzVU_HE{l2#4VFSM!UJr3%yy@W&|8^wCXA$mP7R z@7Hu456_CHW#E@YFFt<+6*(Jz+Diq0FeJ2(YE>MxKXAZ?7$%EBMX^eEPy=mq!tdty zMf5_4uFm|_S`uSQ!gZ&7)pmBEt$cOtx(I%%ni+H5DMUG39^PB3;ap$KA@kGU@Co2WD~luGnM~BZKxyHIJb34>p)7&63U<3jigXfMP)BMKut+p{BA`=hQI~CA=u#&9KR{KsEBa}*HV^3`r z#VPf%QRmEzk5(oZHl`r${&Se0IZ`#V-s~DcR9n6CFq z9pC$&(k$-2VRr7&1%yUoW1Q8WnYZWf6BwwtS_?~u25Wx!Xkh&Gs)%V&2Cn*0r%}ZN zO%&>~UrSlH(ahc|&8ME%x2co+X8*dYwsie;e-u(7m`0`cDd6vh+slN-`9HU}Y0`L* zWQ@XSBoUYI+bjA9{Jw;OGT0(vBEP)ISk~*Uo=&i67u~Jk24?l!d&T;MmZ9y`J69d_ z_bFm#I{iatZ~Pr<(>b2M|KT{kiNYQ8pCHZsXgS*OamlAsKZJsRWWDX-(fq+bqVaB; zxhGCcMLAnEUx%(w;G7QC1~9Y(f)uj)q!V#eS?$^1nxsk&rQHZ46RH2U!JCq;1p&^E L?$}y~;N<@T/dev/null 2>&1 + fi + mkdir -p /usr/lib/systemd/system/ + cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service + # try fix error in Ubuntu 18.04 + # Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error. + # /usr/lib/systemd/system/rustdesk.service:10: Executable path is not absolute: pkill -f "rustdesk --" + if [ -e /usr/bin/pkill ]; then + sed -i "s|pkill|/usr/bin/pkill|g" /usr/lib/systemd/system/rustdesk.service + fi + systemctl daemon-reload + systemctl enable rustdesk + systemctl start rustdesk + fi +fi diff --git a/vendor/rustdesk/res/DEBIAN/postrm b/vendor/rustdesk/res/DEBIAN/postrm new file mode 100755 index 0000000..003a152 --- /dev/null +++ b/vendor/rustdesk/res/DEBIAN/postrm @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +case $1 in + purge) + rm -rf /root/.config/rustdesk || true + ;; +esac + +exit 0 diff --git a/vendor/rustdesk/res/DEBIAN/preinst b/vendor/rustdesk/res/DEBIAN/preinst new file mode 100755 index 0000000..8b73e99 --- /dev/null +++ b/vendor/rustdesk/res/DEBIAN/preinst @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +case $1 in + install|upgrade) + INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') + if [ "systemd" == "${INITSYS}" ]; then + service rustdesk stop || true + sleep 1 + rm -rf /usr/bin/libsciter-gtk.so + fi + ;; +esac + +exit 0 diff --git a/vendor/rustdesk/res/DEBIAN/prerm b/vendor/rustdesk/res/DEBIAN/prerm new file mode 100755 index 0000000..133ff11 --- /dev/null +++ b/vendor/rustdesk/res/DEBIAN/prerm @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e + +case $1 in + remove|upgrade) + INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') + rm -f /usr/bin/rustdesk + + if [ "systemd" == "${INITSYS}" ]; then + + systemctl stop rustdesk || true + systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service || true + + # workaround temp dev build between 1.1.9 and 1.2.0 + serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1) + if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] + then + systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true + fi + rm /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 || true + fi + ;; +esac + +exit 0 diff --git a/vendor/rustdesk/res/PKGBUILD b/vendor/rustdesk/res/PKGBUILD new file mode 100644 index 0000000..dd266eb --- /dev/null +++ b/vendor/rustdesk/res/PKGBUILD @@ -0,0 +1,35 @@ +pkgname=rustdesk +pkgver=1.4.6 +pkgrel=0 +epoch= +pkgdesc="" +arch=('x86_64') +url="" +license=('AGPL-3.0') +groups=() +depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'libva' 'libappindicator-gtk3' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire') +makedepends=() +checkdepends=() +optdepends=() +provides=() +conflicts=() +replaces=() +backup=() +options=() +install=pacman_install +changelog= +noextract=() +md5sums=() #generate with 'makepkg -g' + +package() { + if [[ ${FLUTTER} ]]; then + mkdir -p "${pkgdir}/usr/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/share/rustdesk" + fi + mkdir -p "${pkgdir}/usr/bin" + pushd ${pkgdir} && ln -s /usr/share/rustdesk/rustdesk usr/bin/rustdesk && popd + install -Dm 644 $HBB/res/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" + install -Dm 644 $HBB/res/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" + install -Dm 644 $HBB/res/rustdesk-link.desktop -t "${pkgdir}/usr/share/rustdesk/files" + install -Dm 644 $HBB/res/128x128@2x.png "${pkgdir}/usr/share/icons/hicolor/256x256/apps/rustdesk.png" + install -Dm 644 $HBB/res/scalable.svg "${pkgdir}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" +} diff --git a/vendor/rustdesk/res/ab.py b/vendor/rustdesk/res/ab.py new file mode 100644 index 0000000..11f5228 --- /dev/null +++ b/vendor/rustdesk/res/ab.py @@ -0,0 +1,791 @@ +#!/usr/bin/env python3 + +import requests +import argparse +import json +from datetime import datetime, timedelta + + +def get_personal_ab(url, token): + """Get personal address book GUID""" + headers = {"Authorization": f"Bearer {token}"} + + response = requests.get(f"{url}/api/ab/personal", headers=headers) + + if response.status_code != 200: + return f"Error: {response.status_code} - {response.text}" + + return response.json() + + +def view_shared_abs(url, token, name=None): + """View all shared address books (excluding personal ones)""" + headers = {"Authorization": f"Bearer {token}"} + pageSize = 30 + params = { + "name": name, + } + + filtered_params = { + k: "%" + v + "%" if (v != "-" and "%" not in v and k != "name") else v + for k, v in params.items() + if v is not None + } + filtered_params["pageSize"] = pageSize + + abs = [] + current = 0 + + while True: + current += 1 + filtered_params["current"] = current + response = requests.get(f"{url}/api/ab/shared/profiles", headers=headers, params=filtered_params) + if response.status_code != 200: + print(f"Error: HTTP {response.status_code} - {response.text}") + exit(1) + + response_json = response.json() + if "error" in response_json: + print(f"Error: {response_json['error']}") + exit(1) + + data = response_json.get("data", []) + abs.extend(data) + + total = response_json.get("total", 0) + if len(data) < pageSize or current * pageSize >= total: + break + + return abs + + +def get_ab_by_name(url, token, ab_name): + """Get address book by name""" + abs = view_shared_abs(url, token, ab_name) + for ab in abs: + if ab["name"] == ab_name: + return ab + return None + + +def view_ab_peers(url, token, ab_guid, peer_id=None, alias=None): + """View peers in an address book""" + headers = {"Authorization": f"Bearer {token}"} + pageSize = 30 + params = { + "ab": ab_guid, + "id": peer_id, + "alias": alias, + } + + filtered_params = { + k: "%" + v + "%" if (v != "-" and "%" not in v and k not in ["ab"]) else v + for k, v in params.items() + if v is not None + } + filtered_params["pageSize"] = pageSize + + peers = [] + current = 0 + + while True: + current += 1 + filtered_params["current"] = current + response = requests.get(f"{url}/api/ab/peers", headers=headers, params=filtered_params) + if response.status_code != 200: + print(f"Error: HTTP {response.status_code} - {response.text}") + exit(1) + + response_json = response.json() + if "error" in response_json: + print(f"Error: {response_json['error']}") + exit(1) + + data = response_json.get("data", []) + peers.extend(data) + + total = response_json.get("total", 0) + if len(data) < pageSize or current * pageSize >= total: + break + + return peers + + +def view_ab_tags(url, token, ab_guid): + """View tags in an address book""" + headers = {"Authorization": f"Bearer {token}"} + response = requests.get(f"{url}/api/ab/tags/{ab_guid}", headers=headers) + response_json = check_response(response) + + # Format color values as hex + if response_json: + for tag in response_json: + if "color" in tag and tag["color"] is not None: + # Convert color to hex format + color_value = tag["color"] + if isinstance(color_value, int): + tag["color"] = f"0x{color_value:08X}" + + return response_json if response_json else [] + + +def check_response(response): + """Check API response and return result""" + if response.status_code != 200: + print(f"Error: HTTP {response.status_code} - {response.text}") + exit(1) + + try: + response_json = response.json() + if "error" in response_json: + print(f"Error: {response_json['error']}") + exit(1) + return response_json + except ValueError: + return response.text or "Success" + + +def add_peer(url, token, ab_guid, peer_id, alias=None, note=None, tags=None, password=None): + """Add a peer to address book""" + print(f"Adding peer {peer_id} to address book") + headers = {"Authorization": f"Bearer {token}"} + + payload = { + "id": peer_id, + "note": note, + } + + # Add peer info if provided + info = {} + if alias: + info["alias"] = alias + if tags: + info["tags"] = tags if isinstance(tags, list) else [tags] + if password: + info["password"] = password + + if info: + payload.update(info) + + response = requests.post(f"{url}/api/ab/peer/add/{ab_guid}", headers=headers, json=payload) + return check_response(response) + + +def delete_peer(url, token, ab_guid, peer_ids): + """Delete peers from address book by IDs""" + if isinstance(peer_ids, str): + peer_ids = [peer_ids] + + print(f"Deleting peers {peer_ids} from address book") + headers = {"Authorization": f"Bearer {token}"} + response = requests.delete(f"{url}/api/ab/peer/{ab_guid}", headers=headers, json=peer_ids) + return check_response(response) + +def update_peer(url, token, ab_guid, peer_id, alias=None, note=None, tags=None, password=None): + """Update a peer in address book""" + print(f"Updating peer {peer_id} in address book") + headers = {"Authorization": f"Bearer {token}"} + + # Check if at least one parameter is provided for update + update_params = [alias, note, tags, password] + if all(param is None for param in update_params): + return "Error: At least one parameter must be specified for update" + + payload = { + "id": peer_id, + } + + # Add fields to update + info = {} + if alias is not None: + info["alias"] = alias + if tags is not None: + info["tags"] = tags if isinstance(tags, list) else [tags] + if password is not None: + info["password"] = password + + if info: + payload.update(info) + + if note is not None: + payload["note"] = note + + response = requests.put(f"{url}/api/ab/peer/update/{ab_guid}", headers=headers, json=payload) + return check_response(response) + + +def str2color(tag_name, existing_colors=None): + """Generate color for tag name similar to str2color2 function""" + if existing_colors is None: + existing_colors = [] + + color_map = { + "red": 0xFFFF0000, + "green": 0xFF008000, + "blue": 0xFF0000FF, + "orange": 0xFFFF9800, + "purple": 0xFF9C27B0, + "grey": 0xFF9E9E9E, + "cyan": 0xFF00BCD4, + "lime": 0xFFCDDC39, + "teal": 0xFF009688, + "pink": 0xFFF48FB1, + "indigo": 0xFF3F51B5, + "brown": 0xFF795548, + } + + lower_name = tag_name.lower() + + # Check if tag name matches a predefined color + if lower_name in color_map: + return color_map[lower_name] + + # Special case for yellow + if lower_name == "yellow": + return 0xFFFFFF00 + + # Generate hash-based color + hash_value = 0 + for char in tag_name: + hash_value += ord(char) + + color_list = list(color_map.values()) + hash_value = hash_value % len(color_list) + result = color_list[hash_value] + + # If color is already used, try to find an unused one + if result in existing_colors: + for color in color_list: + if color not in existing_colors: + result = color + break + + return result + + +def add_tag(url, token, ab_guid, tag_name, color=None): + """Add a tag to address book""" + print(f"Adding tag '{tag_name}' to address book") + headers = {"Authorization": f"Bearer {token}"} + + # If no color specified, generate one based on tag name + if color is None: + # Get existing tags to avoid color conflicts + try: + existing_tags = view_ab_tags(url, token, ab_guid) + existing_colors = [tag.get("color", 0) for tag in existing_tags] + color = str2color(tag_name, existing_colors) + except: + # Fallback to default color if we can't get existing tags + color = str2color(tag_name) + + payload = { + "name": tag_name, + "color": color, + } + + response = requests.post(f"{url}/api/ab/tag/add/{ab_guid}", headers=headers, json=payload) + return check_response(response) + + +def update_tag(url, token, ab_guid, tag_name, color): + """Update a tag in address book""" + print(f"Updating tag '{tag_name}' in address book") + headers = {"Authorization": f"Bearer {token}"} + + payload = { + "name": tag_name, + "color": color, + } + + response = requests.put(f"{url}/api/ab/tag/update/{ab_guid}", headers=headers, json=payload) + return check_response(response) + + +def delete_tags(url, token, ab_guid, tag_names): + """Delete tags from address book""" + if isinstance(tag_names, str): + tag_names = [tag_names] + + print(f"Deleting tags {tag_names} from address book") + headers = {"Authorization": f"Bearer {token}"} + response = requests.delete(f"{url}/api/ab/tag/{ab_guid}", headers=headers, json=tag_names) + return check_response(response) + + +def add_shared_ab(url, token, name, note=None, password=None): + """Add a new shared address book""" + print(f"Adding shared address book '{name}'") + headers = {"Authorization": f"Bearer {token}"} + + payload = { + "name": name, + "note": note, + } + + # Add info if password is provided + if password: + payload["info"] = { + "password": password + } + + response = requests.post(f"{url}/api/ab/shared/add", headers=headers, json=payload) + return check_response(response) + + +def update_shared_ab(url, token, ab_guid, name=None, note=None, owner=None, password=None): + """Update a shared address book""" + print(f"Updating shared address book {ab_guid}") + headers = {"Authorization": f"Bearer {token}"} + + # Check if at least one parameter is provided for update + update_params = [name, note, owner, password] + if all(param is None for param in update_params): + return "Error: At least one parameter must be specified for update" + + payload = { + "guid": ab_guid, + } + + if name is not None: + payload["name"] = name + if note is not None: + payload["note"] = note + if owner is not None: + payload["owner"] = owner + if password is not None: + payload["info"] = { + "password": password + } + + response = requests.put(f"{url}/api/ab/shared/update/profile", headers=headers, json=payload) + return check_response(response) + + +def delete_shared_abs(url, token, ab_guids): + """Delete shared address books""" + if isinstance(ab_guids, str): + ab_guids = [ab_guids] + + print(f"Deleting shared address books {ab_guids}") + headers = {"Authorization": f"Bearer {token}"} + response = requests.delete(f"{url}/api/ab/shared", headers=headers, json=ab_guids) + return check_response(response) + + +def permission_to_string(permission): + """Convert numeric permission to string representation""" + permission_map = { + 1: "ro", # Read + 2: "rw", # ReadWrite + 3: "full" # FullControl + } + return permission_map.get(permission, str(permission)) + + +def string_to_permission(permission_str): + """Convert string permission to numeric representation""" + permission_map = { + "ro": 1, # Read + "rw": 2, # ReadWrite + "full": 3 # FullControl + } + return permission_map.get(permission_str.lower(), None) + + +def view_ab_rules(url, token, ab_guid): + """View rules in an address book""" + headers = {"Authorization": f"Bearer {token}"} + pageSize = 30 + params = { + "ab": ab_guid, + "pageSize": pageSize, + } + + rules = [] + current = 0 + + while True: + current += 1 + params["current"] = current + response = requests.get(f"{url}/api/ab/rules", headers=headers, params=params) + if response.status_code != 200: + print(f"Error: HTTP {response.status_code} - {response.text}") + exit(1) + + response_json = response.json() + if "error" in response_json: + print(f"Error: {response_json['error']}") + exit(1) + + data = response_json.get("data", []) + rules.extend(data) + + total = response_json.get("total", 0) + if len(data) < pageSize or current * pageSize >= total: + break + + # Convert numeric permissions to string format + for rule in rules: + if "rule" in rule: + rule["rule"] = permission_to_string(rule["rule"]) + + return rules + + +def add_ab_rule(url, token, ab_guid, rule_type, user=None, group=None, rule=1): + """Add a rule to address book""" + print(f"Adding {rule_type} rule to address book") + headers = {"Authorization": f"Bearer {token}"} + + payload = { + "guid": ab_guid, + "rule": rule, + } + + if rule_type == "user" and user: + payload["user"] = user + elif rule_type == "group" and group: + payload["group"] = group + elif rule_type == "everyone": + # For everyone, both user and group are None (not included in payload) + pass + + response = requests.post(f"{url}/api/ab/rule", headers=headers, json=payload) + return check_response(response) + + +def update_ab_rule(url, token, rule_guid, rule): + """Update an address book rule""" + print(f"Updating rule {rule_guid}") + headers = {"Authorization": f"Bearer {token}"} + + payload = { + "guid": rule_guid, + "rule": rule, + } + + response = requests.patch(f"{url}/api/ab/rule", headers=headers, json=payload) + return check_response(response) + + +def delete_ab_rules(url, token, rule_guids): + """Delete address book rules""" + if isinstance(rule_guids, str): + rule_guids = [rule_guids] + + print(f"Deleting rules {rule_guids}") + headers = {"Authorization": f"Bearer {token}"} + response = requests.delete(f"{url}/api/ab/rules", headers=headers, json=rule_guids) + return check_response(response) + + +def main(): + def parse_color(value): + """Parse color value - supports both hex (0xFF00FF00) and decimal""" + if value.startswith('0x') or value.startswith('0X'): + return int(value, 16) + else: + return int(value) + + def parse_permission(value): + """Parse permission value - supports both string (ro/rw/full) and numeric (1/2/3)""" + # Try to parse as string first + permission_num = string_to_permission(value) + if permission_num is not None: + return permission_num + + # Try to parse as integer for backward compatibility + try: + num_value = int(value) + if num_value in [1, 2, 3]: + return num_value + else: + raise argparse.ArgumentTypeError(f"Invalid permission value: {value}. Must be one of: ro, rw, full, 1, 2, 3") + except ValueError: + raise argparse.ArgumentTypeError(f"Invalid permission value: {value}. Must be one of: ro, rw, full, 1, 2, 3") + + parser = argparse.ArgumentParser(description="Address Book manager") + + # Required arguments + parser.add_argument( + "command", + choices=["view-ab", "add-ab", "update-ab", "delete-ab", "get-personal-ab", + "view-peer", "add-peer", "update-peer", "delete-peer", + "view-tag", "add-tag", "update-tag", "delete-tag", + "view-rule", "add-rule", "update-rule", "delete-rule"], + help="Command to execute", + ) + + # Global arguments (used by all commands) + parser.add_argument("--url", required=True, help="URL of the API") + parser.add_argument("--token", required=True, help="Bearer token for authentication") + + # Address book identification (used by most commands except get-personal-ab) + parser.add_argument("--ab-name", help="Address book name (for identification)") + parser.add_argument("--ab-guid", help="Address book GUID (alternative to ab-name)") + + # Address book management arguments + parser.add_argument("--ab-update-name", help="New address book name (for update)") + parser.add_argument("--note", help="Note field") + parser.add_argument("--password", help="Password field") + parser.add_argument("--owner", help="Address book owner (username)") + + # Peer management arguments + parser.add_argument("--peer-id", help="Peer ID") + parser.add_argument("--alias", help="Peer alias") + parser.add_argument("--tags", help="Peer tags (supports both 'tag1,tag2' and '[tag1,tag2]' formats, use '[]' to clear tags)") + + # Tag management arguments + parser.add_argument("--tag-name", help="Tag name") + parser.add_argument("--tag-color", type=parse_color, help="Tag color (hex number like 0xFF00FF00 or decimal, auto-generated if not specified)") + + # Rule management arguments + parser.add_argument("--rule-type", choices=["user", "group", "everyone"], help="Rule type (auto-detected if not specified)") + parser.add_argument("--rule-user", help="Rule target user name (auto-sets rule-type=user)") + parser.add_argument("--rule-group", help="Rule target group name (auto-sets rule-type=group)") + parser.add_argument("--rule-permission", type=parse_permission, help="Rule permission (ro=Read, rw=ReadWrite, full=FullControl, or numeric 1/2/3)") + parser.add_argument("--rule-guid", help="Rule GUID (for update/delete)") + + args = parser.parse_args() + + # Remove trailing slashes from URL + while args.url.endswith("/"): + args.url = args.url[:-1] + + if args.command == "view-ab": + # View all shared address books + abs = view_shared_abs(args.url, args.token, args.ab_name) + print(json.dumps(abs, indent=2)) + + elif args.command == "get-personal-ab": + # Get personal address book GUID + personal_ab = get_personal_ab(args.url, args.token) + print(json.dumps(personal_ab, indent=2)) + + elif args.command in ["add-ab", "update-ab", "delete-ab"]: + # Address book management commands + if args.command == "add-ab": + if not args.ab_name: + print("Error: --ab-name is required for add-ab command") + return + + result = add_shared_ab(args.url, args.token, args.ab_name, args.note, args.password) + print(f"Result: {result}") + + elif args.command in ["update-ab", "delete-ab"]: + # Commands that need ab-name or ab-guid + if not args.ab_name and not args.ab_guid: + print("Error: --ab-name or --ab-guid is required for this command") + return + + if args.ab_name and args.ab_guid: + print("Error: Cannot specify both --ab-name and --ab-guid") + return + + if args.ab_guid: + ab_guid = args.ab_guid + print(f"Working with address book GUID: {ab_guid}") + else: + # Get address book by name + ab = get_ab_by_name(args.url, args.token, args.ab_name) + if not ab: + print(f"Error: Address book '{args.ab_name}' not found") + return + ab_guid = ab["guid"] + print(f"Working with address book: {args.ab_name} (GUID: {ab_guid})") + + if args.command == "update-ab": + result = update_shared_ab(args.url, args.token, ab_guid, args.ab_update_name, args.note, args.owner, args.password) + print(f"Result: {result}") + + elif args.command == "delete-ab": + result = delete_shared_abs(args.url, args.token, ab_guid) + print(f"Result: {result}") + + elif args.command in ["view-peer", "add-peer", "update-peer", "delete-peer", "view-tag", "add-tag", "update-tag", "delete-tag", "view-rule", "add-rule", "update-rule", "delete-rule"]: + if not args.ab_name and not args.ab_guid: + print("Error: --ab-name or --ab-guid is required for this command") + return + + if args.ab_name and args.ab_guid: + print("Error: Cannot specify both --ab-name and --ab-guid") + return + + if args.ab_guid: + ab_guid = args.ab_guid + print(f"Working with address book GUID: {ab_guid}") + else: + # Get address book by name + ab = get_ab_by_name(args.url, args.token, args.ab_name) + if not ab: + print(f"Error: Address book '{args.ab_name}' not found") + return + + ab_guid = ab["guid"] + print(f"Working with address book: {args.ab_name} (GUID: {ab_guid})") + + if args.command == "view-peer": + peers = view_ab_peers(args.url, args.token, ab_guid, args.peer_id, args.alias) + print(json.dumps(peers, indent=2)) + + elif args.command == "add-peer": + if not args.peer_id: + print("Error: --peer-id is required for add-peer command") + return + + # Handle tags parsing - support both [tag1,tag2] and tag1,tag2 formats + tags = None + if args.tags is not None: + if args.tags == "[]": + tags = [] # Empty list to clear tags + else: + # Remove brackets if present and split by comma + tags_str = args.tags.strip() + if tags_str.startswith('[') and tags_str.endswith(']'): + tags_str = tags_str[1:-1] # Remove brackets + tags = [tag.strip() for tag in tags_str.split(",") if tag.strip()] + + result = add_peer( + args.url, + args.token, + ab_guid, + args.peer_id, + args.alias, + args.note, + tags, + args.password + ) + print(f"Result: {result}") + + elif args.command == "update-peer": + if not args.peer_id: + print("Error: --peer-id is required for update-peer command") + return + + # Handle tags parsing - support both [tag1,tag2] and tag1,tag2 formats + tags = None + if args.tags is not None: + if args.tags == "[]": + tags = [] # Empty list to clear tags + else: + # Remove brackets if present and split by comma + tags_str = args.tags.strip() + if tags_str.startswith('[') and tags_str.endswith(']'): + tags_str = tags_str[1:-1] # Remove brackets + tags = [tag.strip() for tag in tags_str.split(",") if tag.strip()] + + result = update_peer( + args.url, + args.token, + ab_guid, + args.peer_id, + args.alias, + args.note, + tags, + args.password + ) + print(f"Result: {result}") + + elif args.command == "delete-peer": + if not args.peer_id: + print("Error: --peer-id is required for delete-peer command") + return + + result = delete_peer(args.url, args.token, ab_guid, args.peer_id) + print(f"Result: {result}") + + elif args.command == "view-tag": + tags = view_ab_tags(args.url, args.token, ab_guid) + print(json.dumps(tags, indent=2)) + + elif args.command == "add-tag": + if not args.tag_name: + print("Error: --tag-name is required for add-tag command") + return + + result = add_tag(args.url, args.token, ab_guid, args.tag_name, args.tag_color) + print(f"Result: {result}") + + elif args.command == "update-tag": + if not args.tag_name: + print("Error: --tag-name is required for update-tag command") + return + + result = update_tag(args.url, args.token, ab_guid, args.tag_name, args.tag_color) + print(f"Result: {result}") + + elif args.command == "delete-tag": + if not args.tag_name: + print("Error: --tag-name is required for delete-tag command") + return + + result = delete_tags(args.url, args.token, ab_guid, args.tag_name) + print(f"Result: {result}") + + elif args.command == "view-rule": + rules = view_ab_rules(args.url, args.token, ab_guid) + print(json.dumps(rules, indent=2)) + + elif args.command == "add-rule": + if not args.rule_permission: + print("Error: --rule-permission is required for add-rule command") + return + + # Auto-detect rule type if not explicitly specified + if not args.rule_type: + if args.rule_user and args.rule_group: + print("Error: Cannot specify both --rule-user and --rule-group") + return + elif args.rule_user: + rule_type = "user" + elif args.rule_group: + rule_type = "group" + else: + print("Error: Must specify --rule-type=everyone, --rule-user, or --rule-group") + return + else: + rule_type = args.rule_type + + # Validate explicit rule type with parameters + if rule_type == "user" and not args.rule_user: + print("Error: --rule-user is required when rule-type=user") + return + elif rule_type == "group" and not args.rule_group: + print("Error: --rule-group is required when rule-type=group") + return + elif rule_type == "user" and args.rule_group: + print("Error: Cannot specify --rule-group when rule-type=user") + return + elif rule_type == "group" and args.rule_user: + print("Error: Cannot specify --rule-user when rule-type=group") + return + elif rule_type == "everyone" and (args.rule_user or args.rule_group): + print("Error: Cannot specify --rule-user or --rule-group when rule-type=everyone") + return + + result = add_ab_rule(args.url, args.token, ab_guid, rule_type, args.rule_user, args.rule_group, args.rule_permission) + print(f"Result: {result}") + + elif args.command == "update-rule": + if not args.rule_guid: + print("Error: --rule-guid is required for update-rule command") + return + if not args.rule_permission: + print("Error: --rule-permission is required for update-rule command") + return + + result = update_ab_rule(args.url, args.token, args.rule_guid, args.rule_permission) + print(f"Result: {result}") + + elif args.command == "delete-rule": + if not args.rule_guid: + print("Error: --rule-guid is required for delete-rule command") + return + + result = delete_ab_rules(args.url, args.token, args.rule_guid) + print(f"Result: {result}") + + +if __name__ == "__main__": + main() diff --git a/vendor/rustdesk/res/audits.py b/vendor/rustdesk/res/audits.py new file mode 100755 index 0000000..77b0fd1 --- /dev/null +++ b/vendor/rustdesk/res/audits.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 + +import requests +import argparse +import json +from datetime import datetime, timedelta, timezone + + +def format_timestamp(timestamp): + """Convert Unix timestamp to readable local datetime""" + if timestamp is None: + return None + try: + # Convert to local time + local_dt = datetime.fromtimestamp(timestamp) + return local_dt.strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, TypeError): + return timestamp + + +def parse_local_time_to_utc_string(time_str): + """Parse local time string to UTC time string for API filtering""" + try: + # Parse the local time string + local_dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S.%f") + # Make the datetime object timezone-aware using system's local timezone + local_dt = local_dt.replace(tzinfo=datetime.now().astimezone().tzinfo) + utc_dt = local_dt.astimezone(timezone.utc) + return utc_dt.strftime("%Y-%m-%d %H:%M:%S.000") + except ValueError: + try: + # Try without microseconds + local_dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S") + # Make the datetime object timezone-aware using system's local timezone + local_dt = local_dt.replace(tzinfo=datetime.now().astimezone().tzinfo) + utc_dt = local_dt.astimezone(timezone.utc) + return utc_dt.strftime("%Y-%m-%d %H:%M:%S.000") + except ValueError: + return None + + +def get_connection_type_name(conn_type): + """Convert connection type number to readable name""" + type_map = { + 0: "Remote Desktop", + 1: "File Transfer", + 2: "Port Transfer", + 3: "View Camera", + 4: "Terminal" + } + return type_map.get(conn_type, f"Unknown ({conn_type})") + + +def get_console_type_name(console_type): + """Convert console audit type number to readable name""" + type_map = { + 0: "Group Management", + 1: "User Management", + 2: "Device Management", + 3: "Address Book Management" + } + return type_map.get(console_type, f"Unknown ({console_type})") + + +def get_console_operation_name(operation_code): + """Convert console operation code to readable name""" + operation_map = { + 0: "User Login", + 1: "Add Group", + 2: "Add User", + 3: "Add Device", + 4: "Delete Groups", + 5: "Disconnect Device", + 6: "Enable Users", + 7: "Disable Users", + 8: "Enable Devices", + 9: "Disable Devices", + 10: "Update Group", + 11: "Update User", + 12: "Update Device", + 13: "Delete User", + 14: "Delete Device", + 15: "Add Address Book", + 16: "Delete Address Book", + 17: "Change Address Book Name", + 18: "Delete Devices in the Address Book Recycle Bin", + 19: "Empty Address Book Recycle Bin", + 20: "Add Address Book Permission", + 21: "Delete Address Book Permission", + 22: "Update Address Book Permission" + } + return operation_map.get(operation_code, f"Unknown ({operation_code})") + + +def get_alarm_type_name(alarm_type): + """Convert alarm type number to readable name""" + type_map = { + 0: "Access attempt outside the IP whitelist", + 1: "Over 30 consecutive access attempts", + 2: "Multiple access attempts within one minute", + 3: "Over 30 consecutive login attempts", + 4: "Multiple login attempts within one minute", + 5: "Multiple login attempts within one hour" + } + return type_map.get(alarm_type, f"Unknown ({alarm_type})") + + +def enhance_audit_data(data, audit_type): + """Enhance audit data with readable formats""" + if not data: + return data + + enhanced_data = [] + for item in data: + enhanced_item = item.copy() + + # Convert timestamps - replace original values + if 'created_at' in enhanced_item: + enhanced_item['created_at'] = format_timestamp(enhanced_item['created_at']) + if 'end_time' in enhanced_item: + enhanced_item['end_time'] = format_timestamp(enhanced_item['end_time']) + + # Add type-specific enhancements - replace original values + if audit_type == 'conn': + if 'conn_type' in enhanced_item: + enhanced_item['conn_type'] = get_connection_type_name(enhanced_item['conn_type']) + else: + enhanced_item['conn_type'] = "Not Logged In" + + elif audit_type == 'console': + if 'typ' in enhanced_item: + # Replace typ field with type and convert to readable name + enhanced_item['type'] = get_console_type_name(enhanced_item['typ']) + del enhanced_item['typ'] + if 'iop' in enhanced_item: + # Replace iop field with operation and convert to readable name + enhanced_item['operation'] = get_console_operation_name(enhanced_item['iop']) + del enhanced_item['iop'] + + elif audit_type == 'alarm' and 'typ' in enhanced_item: + # Replace typ field with type and convert to readable name + enhanced_item['type'] = get_alarm_type_name(enhanced_item['typ']) + del enhanced_item['typ'] + + enhanced_data.append(enhanced_item) + + return enhanced_data + + +def check_response(response): + """Check API response and return result""" + if response.status_code != 200: + print(f"Error: HTTP {response.status_code} - {response.text}") + exit(1) + + try: + response_json = response.json() + if "error" in response_json: + print(f"Error: {response_json['error']}") + exit(1) + return response_json + except ValueError: + return response.text or "Success" + + +def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None, + created_at=None, days_ago=None, non_wildcard_fields=None): + """Common function for viewing audits""" + headers = {"Authorization": f"Bearer {token}"} + + # Set default page size and current page + if page_size is None: + page_size = 10 + if current is None: + current = 1 + + params = { + "pageSize": page_size, + "current": current + } + + # Add filter parameters if provided + if filters: + for key, value in filters.items(): + if value is not None: + params[key] = value + + # Handle time filters + if days_ago is not None: + # Calculate datetime from days ago + target_time = datetime.now() - timedelta(days=days_ago) + # Convert to UTC time string using system timezone + utc_timestamp = target_time.timestamp() + utc_dt = datetime.fromtimestamp(utc_timestamp, timezone.utc) + params["created_at"] = utc_dt.strftime("%Y-%m-%d %H:%M:%S.000") + elif created_at: + # Parse local time string and convert to UTC time string + utc_time_str = parse_local_time_to_utc_string(created_at) + if utc_time_str is not None: + params["created_at"] = utc_time_str + else: + # If parsing fails, pass the original value + params["created_at"] = created_at + + # Apply wildcard patterns for string fields (excluding specific fields) + if non_wildcard_fields is None: + non_wildcard_fields = set() + + # Always exclude these fields from wildcard treatment + non_wildcard_fields.update(["created_at", "pageSize", "current"]) + + string_params = {} + for k, v in params.items(): + if isinstance(v, str) and k not in non_wildcard_fields: + if v != "-" and "%" not in v: + string_params[k] = "%" + v + "%" + else: + string_params[k] = v + else: + string_params[k] = v + + response = requests.get(f"{url}/api/audits/{endpoint}", headers=headers, params=string_params) + response_json = check_response(response) + + # Enhance the data with readable formats + data = enhance_audit_data(response_json.get("data", []), endpoint) + + return { + "data": data, + "total": response_json.get("total", 0), + "current": current, + "pageSize": page_size + } + + +def view_conn_audits(url, token, remote=None, conn_type=None, + page_size=None, current=None, created_at=None, days_ago=None): + """View connection audits""" + filters = { + "remote": remote, + "conn_type": conn_type + } + non_wildcard_fields = {"conn_type"} + + return view_audits_common( + url, token, "conn", filters, page_size, current, created_at, days_ago, non_wildcard_fields + ) + + +def view_file_audits(url, token, remote=None, + page_size=None, current=None, created_at=None, days_ago=None): + """View file audits""" + filters = { + "remote": remote + } + non_wildcard_fields = set() + + return view_audits_common( + url, token, "file", filters, page_size, current, created_at, days_ago, non_wildcard_fields + ) + + +def view_alarm_audits(url, token, device=None, + page_size=None, current=None, created_at=None, days_ago=None): + """View alarm audits""" + filters = { + "device": device + } + non_wildcard_fields = set() + + return view_audits_common( + url, token, "alarm", filters, page_size, current, created_at, days_ago, non_wildcard_fields + ) + + +def view_console_audits(url, token, operator=None, + page_size=None, current=None, created_at=None, days_ago=None): + """View console audits""" + filters = { + "operator": operator + } + non_wildcard_fields = set() + + return view_audits_common( + url, token, "console", filters, page_size, current, created_at, days_ago, non_wildcard_fields + ) + + +def main(): + parser = argparse.ArgumentParser(description="Audits manager") + parser.add_argument( + "command", + choices=["view-conn", "view-file", "view-alarm", "view-console"], + help="Command to execute", + ) + parser.add_argument("--url", required=True, help="URL of the API") + parser.add_argument("--token", required=True, help="Bearer token for authentication") + + # Pagination parameters + parser.add_argument("--page-size", type=int, default=10, help="Number of records per page (default: 10)") + parser.add_argument("--current", type=int, default=1, help="Current page number (default: 1)") + + # Time filtering parameters + parser.add_argument("--created-at", help="Filter by creation time in local time (format: 2025-09-16 14:15:57 or 2025-09-16 14:15:57.000)") + parser.add_argument("--days-ago", type=int, help="Filter by days ago (e.g., 7 for last 7 days)") + + # Audit filters (simplified) + parser.add_argument("--remote", help="Remote peer ID filter (for conn/file audits)") + parser.add_argument("--device", help="Device ID filter (for alarm audits)") + parser.add_argument("--conn-type", type=int, help="Connection type filter (for conn audits only): 0=Remote Desktop, 1=File Transfer, 2=Port Transfer, 3=View Camera, 4=Terminal") + parser.add_argument("--operator", help="Operator filter (for console audits only)") + + args = parser.parse_args() + + # Remove trailing slashes from URL + while args.url.endswith("/"): + args.url = args.url[:-1] + + if args.command == "view-conn": + # View connection audits + result = view_conn_audits( + args.url, + args.token, + args.remote, + args.conn_type, + args.page_size, + args.current, + args.created_at, + args.days_ago + ) + print(json.dumps(result, indent=2)) + + elif args.command == "view-file": + # View file audits + result = view_file_audits( + args.url, + args.token, + args.remote, + args.page_size, + args.current, + args.created_at, + args.days_ago + ) + print(json.dumps(result, indent=2)) + + elif args.command == "view-alarm": + # View alarm audits + result = view_alarm_audits( + args.url, + args.token, + args.device, + args.page_size, + args.current, + args.created_at, + args.days_ago + ) + print(json.dumps(result, indent=2)) + + elif args.command == "view-console": + # View console audits + result = view_console_audits( + args.url, + args.token, + args.operator, + args.page_size, + args.current, + args.created_at, + args.days_ago + ) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/vendor/rustdesk/res/bump.sh b/vendor/rustdesk/res/bump.sh new file mode 100644 index 0000000..0fb62a0 --- /dev/null +++ b/vendor/rustdesk/res/bump.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +sed -i "s/$1/$2/g" res/*spec res/PKGBUILD flutter/pubspec.yaml Cargo.toml .github/workflows/*yml flatpak/*json appimage/*yml libs/portable/Cargo.toml +cargo run # to bump version in cargo lock diff --git a/vendor/rustdesk/res/design.svg b/vendor/rustdesk/res/design.svg new file mode 100644 index 0000000..62e5682 --- /dev/null +++ b/vendor/rustdesk/res/design.svg @@ -0,0 +1,374 @@ + +rustdesk diff --git a/vendor/rustdesk/res/device-groups.py b/vendor/rustdesk/res/device-groups.py new file mode 100755 index 0000000..dd861ae --- /dev/null +++ b/vendor/rustdesk/res/device-groups.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 + +import requests +import argparse +import json + + +def check_response(response): + """ + Check API response and handle errors. + + Two error cases: + 1. Status code is not 200 -> exit with error + 2. Response contains {"error": "xxx"} -> exit with error + """ + if response.status_code != 200: + print(f"Error: HTTP {response.status_code}: {response.text}") + exit(1) + + # Check for {"error": "xxx"} in response + if response.text and response.text.strip(): + try: + json_data = response.json() + if isinstance(json_data, dict) and "error" in json_data: + print(f"Error: {json_data['error']}") + exit(1) + return json_data + except ValueError: + return response.text + + return None + + +def headers_with(token): + return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + +# ---------- Device Group APIs ---------- + +def list_groups(url, token, name=None, page_size=50): + headers = headers_with(token) + params = {"pageSize": page_size} + if name: + params["name"] = name + data, current = [], 0 + while True: + current += 1 + params["current"] = current + r = requests.get(f"{url}/api/device-groups", headers=headers, params=params) + if r.status_code != 200: + print(f"Error: HTTP {r.status_code} - {r.text}") + exit(1) + res = r.json() + if "error" in res: + print(f"Error: {res['error']}") + exit(1) + rows = res.get("data", []) + data.extend(rows) + total = res.get("total", 0) + if len(rows) < page_size or current * page_size >= total: + break + return data + + +def get_group_by_name(url, token, name): + groups = list_groups(url, token, name) + for g in groups: + if str(g.get("name")) == name: + return g + return None + + +def create_group(url, token, name, note=None, accessed_from=None): + headers = headers_with(token) + payload = {"name": name} + if note: + payload["note"] = note + if accessed_from: + payload["allowed_incomings"] = accessed_from + r = requests.post(f"{url}/api/device-groups", headers=headers, json=payload) + return check_response(r) + + +def update_group(url, token, name, new_name=None, note=None, accessed_from=None): + headers = headers_with(token) + g = get_group_by_name(url, token, name) + if not g: + print(f"Error: Group '{name}' not found") + exit(1) + guid = g.get("guid") + payload = {} + if new_name is not None: + payload["name"] = new_name + if note is not None: + payload["note"] = note + if accessed_from is not None: + payload["allowed_incomings"] = accessed_from + r = requests.patch(f"{url}/api/device-groups/{guid}", headers=headers, json=payload) + check_response(r) + return "Success" + + +def delete_groups(url, token, names): + headers = headers_with(token) + if isinstance(names, str): + names = [names] + for n in names: + g = get_group_by_name(url, token, n) + if not g: + print(f"Error: Group '{n}' not found") + exit(1) + guid = g.get("guid") + r = requests.delete(f"{url}/api/device-groups/{guid}", headers=headers) + check_response(r) + return "Success" + + +# ---------- Device group assign APIs (name -> guid) ---------- + +def view_devices(url, token, group_name=None, id=None, device_name=None, + user_name=None, device_username=None, page_size=50): + """View devices in a device group with filters""" + headers = headers_with(token) + + # Separate exact match and fuzzy match params + params = {} + fuzzy_params = { + "id": id, + "device_name": device_name, + "user_name": user_name, + "device_username": device_username, + } + + # Add device_group_name without wildcard (exact match) + if group_name: + params["device_group_name"] = group_name + + # Add wildcard for fuzzy search to other params + for k, v in fuzzy_params.items(): + if v is not None: + params[k] = "%" + v + "%" if (v != "-" and "%" not in v) else v + + params["pageSize"] = page_size + + data, current = [], 0 + while True: + current += 1 + params["current"] = current + r = requests.get(f"{url}/api/devices", headers=headers, params=params) + if r.status_code != 200: + return check_response(r) + res = r.json() + rows = res.get("data", []) + data.extend(rows) + total = res.get("total", 0) + if len(rows) < page_size or current * page_size >= total: + break + return data + + +def add_devices(url, token, group_name, device_ids): + headers = headers_with(token) + g = get_group_by_name(url, token, group_name) + if not g: + return f"Group '{group_name}' not found" + guid = g.get("guid") + payload = device_ids if isinstance(device_ids, list) else [device_ids] + r = requests.post(f"{url}/api/device-groups/{guid}", headers=headers, json=payload) + return check_response(r) + + +def remove_devices(url, token, group_name, device_ids): + headers = headers_with(token) + g = get_group_by_name(url, token, group_name) + if not g: + return f"Group '{group_name}' not found" + guid = g.get("guid") + payload = device_ids if isinstance(device_ids, list) else [device_ids] + r = requests.delete(f"{url}/api/device-groups/{guid}/devices", headers=headers, json=payload) + return check_response(r) + + +def parse_rules(s): + if not s: + return None + try: + v = json.loads(s) + if isinstance(v, list): + # expect list of {"type": number, "name": string} + return v + except Exception: + pass + return None + + +def main(): + parser = argparse.ArgumentParser(description="Device Group manager") + parser.add_argument("command", choices=[ + "view", "add", "update", "delete", + "view-devices", "add-devices", "remove-devices" + ], help=( + "Command to execute. " + "[view/add/update/delete/add-devices/remove-devices: require Device Group Permission] " + "[view-devices: require Device Permission]" + )) + parser.add_argument("--url", required=True) + parser.add_argument("--token", required=True) + + parser.add_argument("--name", help="Device group name (exact match)") + parser.add_argument("--new-name", help="New device group name (for update)") + parser.add_argument("--note", help="Note") + + parser.add_argument("--accessed-from", help="JSON array: '[{\"type\":0|2,\"name\":\"...\"}]' (0=User Group, 2=User)") + + parser.add_argument("--ids", help="Comma separated device IDs for add-devices/remove-devices") + + # Filters for view-devices command + parser.add_argument("--id", help="Device ID filter (for view-devices)") + parser.add_argument("--device-name", help="Device name filter (for view-devices)") + parser.add_argument("--user-name", help="User name filter (owner of device, for view-devices)") + parser.add_argument("--device-username", help="Device username filter (logged in user on device, for view-devices)") + + args = parser.parse_args() + while args.url.endswith("/"): args.url = args.url[:-1] + + if args.command == "view": + res = list_groups(args.url, args.token, args.name) + print(json.dumps(res, indent=2)) + elif args.command == "add": + if not args.name: + print("Error: --name is required") + exit(1) + print(create_group( + args.url, args.token, args.name, args.note, + parse_rules(args.accessed_from) + )) + elif args.command == "update": + if not args.name: + print("Error: --name is required") + exit(1) + print(update_group( + args.url, args.token, args.name, args.new_name, args.note, + parse_rules(args.accessed_from) + )) + elif args.command == "delete": + if not args.name: + print("Error: --name is required (supports comma separated)") + exit(1) + names = [x.strip() for x in args.name.split(",") if x.strip()] + print(delete_groups(args.url, args.token, names)) + elif args.command == "view-devices": + res = view_devices( + args.url, + args.token, + group_name=args.name, + id=args.id, + device_name=args.device_name, + user_name=args.user_name, + device_username=args.device_username + ) + print(json.dumps(res, indent=2)) + elif args.command in ("add-devices", "remove-devices"): + if not args.name or not args.ids: + print("Error: --name and --ids are required for add/remove devices") + exit(1) + ids = [x.strip() for x in args.ids.split(",") if x.strip()] + if args.command == "add-devices": + print(add_devices(args.url, args.token, args.name, ids)) + else: + print(remove_devices(args.url, args.token, args.name, ids)) + + +if __name__ == "__main__": + main() diff --git a/vendor/rustdesk/res/devices.py b/vendor/rustdesk/res/devices.py new file mode 100755 index 0000000..832f050 --- /dev/null +++ b/vendor/rustdesk/res/devices.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 + +import requests +import argparse +from datetime import datetime, timedelta + + +def view( + url, + token, + id=None, + device_name=None, + user_name=None, + group_name=None, + device_group_name=None, + offline_days=None, +): + headers = {"Authorization": f"Bearer {token}"} + pageSize = 30 + params = { + "id": id, + "device_name": device_name, + "user_name": user_name, + "group_name": group_name, + "device_group_name": device_group_name, + } + + params = { + k: "%" + v + "%" if (v != "-" and "%" not in v) else v + for k, v in params.items() + if v is not None + } + params["pageSize"] = pageSize + + devices = [] + + current = 0 + + while True: + current += 1 + params["current"] = current + response = requests.get(f"{url}/api/devices", headers=headers, params=params) + if response.status_code != 200: + print(f"Error: HTTP {response.status_code} - {response.text}") + exit(1) + + response_json = response.json() + if "error" in response_json: + print(f"Error: {response_json['error']}") + exit(1) + + data = response_json.get("data", []) + + for device in data: + if offline_days is None: + devices.append(device) + continue + last_online = datetime.strptime( + device["last_online"].split(".")[0], "%Y-%m-%dT%H:%M:%S" + ) # assuming date is in this format + if (datetime.utcnow() - last_online).days >= offline_days: + devices.append(device) + + total = response_json.get("total", 0) + if len(data) < pageSize or current * pageSize >= total: + break + + return devices + + +def check(response): + if response.status_code != 200: + print(f"Error: HTTP {response.status_code} - {response.text}") + exit(1) + + try: + response_json = response.json() + if "error" in response_json: + print(f"Error: {response_json['error']}") + exit(1) + return response_json + except ValueError: + return response.text or "Success" + + +def disable(url, token, guid, id): + print("Disable", id) + headers = {"Authorization": f"Bearer {token}"} + response = requests.post(f"{url}/api/devices/{guid}/disable", headers=headers) + return check(response) + + +def enable(url, token, guid, id): + print("Enable", id) + headers = {"Authorization": f"Bearer {token}"} + response = requests.post(f"{url}/api/devices/{guid}/enable", headers=headers) + return check(response) + + +def delete(url, token, guid, id): + print("Delete", id) + headers = {"Authorization": f"Bearer {token}"} + response = requests.delete(f"{url}/api/devices/{guid}", headers=headers) + return check(response) + + +def assign(url, token, guid, id, type, value): + print("assign", id, type, value) + valid_types = [ + "ab", + "strategy_name", + "user_name", + "device_group_name", + "note", + "device_username", + "device_name", + ] + if type not in valid_types: + print(f"Invalid type, it must be one of: {', '.join(valid_types)}") + return + data = {"type": type, "value": value} + headers = {"Authorization": f"Bearer {token}"} + response = requests.post( + f"{url}/api/devices/{guid}/assign", headers=headers, json=data + ) + return check(response) + + +def main(): + parser = argparse.ArgumentParser(description="Device manager") + parser.add_argument( + "command", + choices=["view", "disable", "enable", "delete", "assign"], + help="Command to execute", + ) + parser.add_argument("--url", required=True, help="URL of the API") + parser.add_argument( + "--token", required=True, help="Bearer token for authentication" + ) + parser.add_argument("--id", help="Device ID") + parser.add_argument("--device_name", help="Device name") + parser.add_argument("--user_name", help="User name") + parser.add_argument("--group_name", help="User group name") + parser.add_argument("--device_group_name", help="Device group name") + parser.add_argument( + "--assign_to", + help="=, e.g. user_name=mike, strategy_name=test, device_group_name=group1, note=note1, device_username=username1, device_name=name1, ab=ab1, ab=ab1,tag1,alias1,password1,note1" + ) + parser.add_argument( + "--offline_days", type=int, help="Offline duration in days, e.g., 7" + ) + + args = parser.parse_args() + + while args.url.endswith("/"): args.url = args.url[:-1] + + devices = view( + args.url, + args.token, + args.id, + args.device_name, + args.user_name, + args.group_name, + args.device_group_name, + args.offline_days, + ) + + if args.command == "view": + for device in devices: + print(device) + elif args.command in ["disable", "enable", "delete", "assign"]: + # Check if we need user confirmation for multiple devices + if len(devices) > 1: + print(f"Found {len(devices)} devices. Do you want to proceed with {args.command} operation on the devices? (Y/N)") + confirmation = input("Type 'Y' to confirm: ").strip() + if confirmation.upper() != 'Y': + print("Operation cancelled.") + return + + if args.command == "disable": + for device in devices: + response = disable(args.url, args.token, device["guid"], device["id"]) + print(response) + elif args.command == "enable": + for device in devices: + response = enable(args.url, args.token, device["guid"], device["id"]) + print(response) + elif args.command == "delete": + for device in devices: + response = delete(args.url, args.token, device["guid"], device["id"]) + print(response) + elif args.command == "assign": + if "=" not in args.assign_to: + print("Invalid assign_to format, it must be =") + return + type, value = args.assign_to.split("=", 1) + for device in devices: + response = assign( + args.url, args.token, device["guid"], device["id"], type, value + ) + print(response) + + +if __name__ == "__main__": + main() diff --git a/vendor/rustdesk/res/fdroid/patches/0000-flutter-android-x86.patch b/vendor/rustdesk/res/fdroid/patches/0000-flutter-android-x86.patch new file mode 100644 index 0000000..1bb4436 --- /dev/null +++ b/vendor/rustdesk/res/fdroid/patches/0000-flutter-android-x86.patch @@ -0,0 +1,16 @@ +diff --git a/flutter-sdk/.gclient b/flutter-sdk/.gclient +new file mode 100644 +index 0000000..fd12886 +--- /dev/null ++++ b/flutter-sdk/.gclient +@@ -0,0 +1,10 @@ ++solutions = [ ++ { ++ "managed": False, ++ "name": "src/flutter", ++ "url": "https://github.com/flutter/engine.git@FLUTTER_VERSION_PLACEHOLDER", ++ "custom_deps": {}, ++ "deps_file": "DEPS", ++ "safesync_url": "", ++ }, ++] diff --git a/vendor/rustdesk/res/fdroid/patches/0001-x86-no-debuggable.patch b/vendor/rustdesk/res/fdroid/patches/0001-x86-no-debuggable.patch new file mode 100644 index 0000000..c160d60 --- /dev/null +++ b/vendor/rustdesk/res/fdroid/patches/0001-x86-no-debuggable.patch @@ -0,0 +1,24 @@ +diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle +index f4dc69e..6b835fd 100644 +--- a/flutter/android/app/build.gradle ++++ b/flutter/android/app/build.gradle +@@ -67,6 +67,19 @@ android { + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules' + } + } ++ ++ applicationVariants.all { variant -> ++ variant.outputs.each { output -> ++ output.processManifest.doLast { task -> ++ def outputDir = multiApkManifestOutputDirectory.asFile.get() ++ File manifestOutFile = new File(outputDir, "AndroidManifest.xml") ++ if (manifestOutFile.exists()) { ++ def newFileContents = manifestOutFile.getText('UTF-8').replace("android:debuggable=\"true\"", "") ++ manifestOutFile.write(newFileContents, 'UTF-8') ++ } ++ } ++ } ++ } + } + + flutter { diff --git a/vendor/rustdesk/res/gen_icon.sh b/vendor/rustdesk/res/gen_icon.sh new file mode 100644 index 0000000..e9f3835 --- /dev/null +++ b/vendor/rustdesk/res/gen_icon.sh @@ -0,0 +1,8 @@ +#!/bin/bash +for size in 16 32 64 128 256 512 1024; do + #inkscape -z -o $size.png -w $size -h $size icon.svg >/dev/null 2>/dev/null + convert icon.png -resize ${size}x${size} app_icon_$size.png +done +# from ImageMagick +convert 16.png 32.png 48.png 128.png 256.png -colors 256 icon.ico +#/bin/rm 16.png 32.png 48.png 128.png 256.png diff --git a/vendor/rustdesk/res/icon.ico b/vendor/rustdesk/res/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..eedb92614ca9591605fc624c7d1b64d7587188f7 GIT binary patch literal 99678 zcmeHw2Yg(`wf@R7wgCeM5|S5^U~1@(o&X^@bVx`DNqB+0{|hhV5lTWJ)PTVT0>(DB zv2no#_bwaUn`}w$y;t0A)sXJ*bh zbH-#UFr8-V*3E=#chhqnO{OPICR6wB>i6mRy#&AAaD(>!a+B%VZ<|a#dZ^#8yvJlZ z`%aUocW>=`mjcsm@0v_kA`d)@9M#`(F`1Ch%LfVsiSPJBL_I*8K?*3*#P{+u`m^EZ z6#T2<7%4kvki4=FNvlD7XOLn$hBq{N1?M4!AI7JxlSuyL5(@S#q`md^o0J@@ZI(y?ZquUSGhb2?MiTs(g`TuvGI zGDhzHCi1WA75&=Kcjq@Bi)SkrqYkTQhJNR^;Tdzez{olN2NbMW5&EC;gSV=Ps*1Z& z^_t5l=sjlq7uv=r0$;|+Hth%G+3_CvtCx}Ayo_An)msf*lpwilW{|sjGM!g#5=Vwp*F~Qcz5@uhIy9>jJTe;u9)Ad)fbbi zdQ$ZB{4DnYLsHN-jtP7jBj@2hsH^D|^l*ET4flzU#y%LyQ1qRV-}zqgtS$#qy|SF? z4(b77`6UtrPYgN3vgQoXv!LxDKPX#?^X7N(yZD{_?$bdZfjppGC4Lvbli$r{ae)#~ zl8?w;M`Sw*`FT?P%)cd<_?`UjZ3#W*$L&YTNLkvGCgFqv$qrkL> zDYz2;{=!7oQ5aXp zpnU6|hnP%2wCr0*O79Cu9&|k^C)P!l!E%6<%8h8>jk~MjcTn)?JaW&z6MPqtjo&?* z=LIIJ9}Ak&mHa2?MC8}#3;LKc<`I%#MO#BVmp;21Wmwzjw&s3NTovRlenRD&-;FV= zegej=iNK=dK@4@x^iA|{`Q5X_?Hqk9IQ&0JvGN#`c#=^D$AY_5zVU9$B)oqLXx3R2 zFqbr$k+SsN$ac>jv;m+&xK57xyJ8i*#Zi# z?3Lgf8{g*I{sz@f<#{3AJ#k^ZvubMV*Ux&%vB)9?DUI_j?<^pT|=k z$AnyiJomCZhg=J+c{i%0iGOwOd0W(Zwf64H1>k=*>OWv`%e?Dj31q5w z*8Pw-?#B1W`9+c3)Uf7$szRMJ-~8^1h3Jc0A3^(zEeC^TJ;}pknLds|p1X!a9-L@; z?%z}~0@lq`v+%s|yBC|Nd`kD)af zPq@xmo*QKg_wS05u2j4Ie)7~|{v980es@BHyApI!pmrs>_PmR>_6YGf#$yS~bMxwJ zscy?dWIOZ@`5H>l$C{br7+BNZo%H&P<)`eoh;fX^5*|-uj_Hx_ivFgrzfQ+LJ_02_ z$%xJ|@$bgGi$Tu(ZE2cKUCtp_DFAaU8aj9gZ|&iTsUzX7$D#AVfaU_4?gF!P%m zE;E;%+f*R!qDsiZR!+*X)ue2nO!ArmB$s|bav5kHKJUVBC-6UeIo9jxzfLPNmpyVZ zj~^0FicD00l#~slNSX9IlKVqX-|I~1Lplh3P=MELyl(0R`k({(J~@;8!!M)2?B9~K zeI(Y;M<5}PN9u{oZcqMkhy*iq77N}arO%}#ztMr@*Q4}UD7VmML64@d`+U&r2%VN# z3v?jws7uJd@^zBy_n`}DeKPKL z3e0(u0!v>2Jx~6*Pmp)gE#w~BP1PlHd*?QTvWM2fdfB1Z75dfDxM%reu-#f?yg9+2 zcFiVb#66@G{U4F_M9hbjy8RTKg|d#no1}I9NU7NYOBJSDM!Bv?kS$h{t2e^l*^^uo zE<<}S2)CK&vVRGE;8^Hq$HQ*B_tTi~jr(IQr&{tZI$9@cJ`^0rg z3dy!Z?6-08``^QGkU#;$_||p+pfQYUd^^Q_IsQ8Ray8iRle&o5`9$~2R*g* zSTj#0@h|Ar{JTFV``B(|9U0citM#tx#x2l|*Fi_^+V)x!8RJ%-6MS-cZ#0+ntU+vhKHu&08@Cmiu4ccx>Xbi@8_Uh4G*%Y*FqWYQViT19x8MW+Y=mFWz zV4e#*n`Kqpm-5;}dZ$3OY4W&gC_CE#0+W88gshDFy3h?uz(&?3w+a1aE!qp)W<=Sc zPgCs_=mX{KhthnK*9;VO&+Cqeva|gFbArHvXPVC-)&D{k+1EWuX7txaWoLaLbfK;z z1LEe+Yd*1VGS)lFF4i8H&jd@}h?_^MH#oQcP4v}>dgrpwRmpMigSc6X@}ut6zN-2G zh_dtA19P!J>FaUxNc9HC_LqbXwnlF=dfC~|GaqxMLkW~0Yi+S!MO$Yd2EFXU_Jnz) zfAODFEnb}G6tZvn@339YSa#T`*iPje5V6n`_&+FUTz1&(*dM_={ciN%=4~x; zUlQX6wx8FR{WPrq<~GCi&Si&vtzvNpa#u}>@nqyrtmE)~0v{2ybuZeCZg1qZD*Ma1 zM|_9my4{fvW#C)TzKd!Xen-{aN0)sOcqoSaOu%-+`WgSQ%Y>dFy6pOTHL#_B21*>6 zh3nA3*kxY~+Xtw6HO5q5MdU-VzCpLd`Wdlq;x?nzyIywo3w2L{?}Ib`SQsn&SiiB2 zu71tELPkcm8MGVNPS}28+4%(KT(Q?t(Rs3N$_4{>s73^ee2##IKSxp8+}Ke zS+_hzm1sBN_O6wEDav2mfn1ei8a>w353CpSj=EHoKUD8}*|qhmWAxb+I4}WOB(H*r z_1>U!Kh>?hQ?-Yqy@+~8+1X}RweE^W{-d$-F#hIW`G&9;3;m3t>|(t-4D&&(o9q)V zApb$|(8T)Km|r5FnTOi-KNao0T+}QnGy+N`o@%|ueVokK=pbI=~x=YmiiDK0Kdepr;HPwD#+d!cgW8G4y-u1HU z>(w#5*6u*Ixz~|rPe159cA=Z`_+V^T3YI%=3OZ0HhX+%`I@q9hJPVupv4}d;zp!1~ zvilEIv-M7L*B0qNCw&z@#tR=;ZSKZ;*UPT2wa2MzZOcU1q-R|~w&J_UvEgZQ?s%1) zJ6|LF=08)z%Dbs{(RU#uJE*=N=&P0u_c!tj&PJT}#B$pH_wo5(TeR%(D=EF)!)l9Ba${|9~?8L`VW z2@kKcoh83VouAh9S{v;}U-Rj{Wn%troQt8}b@^Tn8N+g>X3I~t_TJ>{oBBsm9YhJ( z$-93DS--wi=$3eYMy&N}B4y{X=_JZtx%MJ*)x@7$#$bTU&bHws6ZRa~zaiK5x2S&p zWzf$Qs_Q21s|mDsp3m@{plWS*vL5;;rsJ$1OsX37^0NJi?b@j&tXak0i6f)Qw*L3f zaa>O3d0nY`4$6l0s@RKxb(8RsTU>xS@R^uLUP6`~zaeM&2*@4i)Qo!G1;BU@wE{9 zD*4)Co=g1lwwZb}Td3DEzM`Iw^A+{Zep)?1y+I*AE&LF@pA7#FCE%#{nIGcwKR_%C zYC#z?Y9OcFB`!0Uoq1s2Gv-O~7h>4(LHi&37CsGvu`;ja1&WtcV)?>6F>lPHfpe3N zc{lt%7~}=D)x^9pkIbv61HOW~fcSSA)YcR8$hSz8<*j{l@ZW8l+5$I)5cIHMLr@)Q9JIB|MvTi8J zUq44Od=;djH<2_5`l|umDL4=`I)XV|gX zmg@TucAEaMb$odP1(v@|@^KWA^UQo&jx5a?%hqd0Pnt%`DEM-`(^d7ac^!Tc;5p<@9EENc&&S|n3R#= z`_0ag{+Frp&V0kpBKDTCy@F{_C+u&#m!xBh67!j8j&t>Ynd$e3OG)YVUEx!u0Pm3h zW~O-8eGB2&>15wd?6LE$dmdC*7QUT*-eF&J4FdmcGXXgU!;UrUPLirw zRno#~-EdM~hp+S-n%{1waX0eLHW#*Au)Pc9oYAZYvr6gV`sW$VCML-Lg0L)ODbNE_c0mh-9d4qKtHxxsd2pNKj* z#_ga56W77CCos074w@R{v~f=CTZS)#?uX2>KH4rF>hH!juj8)s{)K99Wc!hQdUpz% zv+Q3TuX<4j()1@VuPF%oS@FDvNz7{?yRp~9KmI4=pL!4Zrv03J6K^HY$nNC+>`c`* z{3&D|+P#o<2HtgBr5-0kx3#kE$vOu18(bX=?o7(&8jl_Ef1ZPuZy{Rs8PV>qF&B&D zC&P76^dx+p3RRz>{^wI*`lFcFeok`5dQu!1x7ZgNdj>dG0?%m_`w5cD%P6o3F_wz% z7O^>a|AH>-;_|NBJjbFPZG0(H^8>YdczplxhhlGr@)qVdbN_)zFmcQyo(7V4P9$k9 ze7`H8A`D{PlVIZ5AkwjgW44&gSXTequIj{Sqi~b&qclf6Bo_EXCPUJf_N#`Q9 zE60~%>|c%I{a@M|4zcE>c`u+2Y^mj+#8XoJE^>Z#i^@OCx@LH1+bNT+1aTmOar$X< zW#E3_^X#y{eyn-jd#Hm2Xa_08+-UCAvHlk{uM=Y&$0rKe>bbvjo7Zp`yu(j|;~mr$ z!8fm_I965({ie)+Rb9hk-QMK94r}+ohh0cMFgKw*)4j=eU^F$1@2titYKC{=D?JTk z=r+jeR1)U2^5C1){Q|LYe;0h~zQ`Ec<;^2fc}vpIus^AD#jitgnquKD{00zrfnzV! zFS~(cAM{AcrekZMw^3s|#nfFGc=r#$+P9k3X}L*(ip7X=(KTk95An|a9!zr(L#l3d z(gjo24HNpRW_f2_6|fJ?{zGQx8*xg@ZNR?l-pG9!=a26Hue+(V?t z4GgViqIu`F4E7}|=AFsqpMkprwX4Z8^DK2=2;^KX_jhjZI_}_G_zHc6u>yXHNyQuS zScQ%{Vh?OI@4N>9o~hxg^g;(adohku`WMR)8`gPkeC z#5O6m%Lv;9=%D$(D$&4Xus{|f7@ zykoD0{9`wZ+$PyuO_rrUitwuxaRUvw3;)E0;D7DS$;m(1Vc8a`$5xEVdtX&w=NXDv zx8`lRWgc$}G;D@XX17qB$XL9K{Z-)KvV~)4VlR6ViS4k$7OBfRBkmgSyzk020J_dy zqmsxuGq-w%QF1lfxV35$^}Kzx(>171`gTd1u=M(}?p)s>+?-nPX*|7k)cF z?@QpLyy9%^&r9A$rXaql@5>ui8?@05%Wa-=_#Ltc}+(# zx2bB1_hsrHHS59GQhqJ_U5NOqYyy~K4BvZ< z@9I9+8|7D+< z8n*obycdZ5Lb|NedFTBfwL1{I7yHxFN$hjNeivfDrLoOxxQBSx?QzgiJLlg+Qp28f zUYpRfvLAJ@<4NqHE+Db5QNx}0HkOxmA$RO^HWJEPzlm*A98;P3PuL!Z{g3upSCju( zQP#&r*E{*Z7wr#kmg6M zZ+$CYQDZEB4w=`C4LZUuGZy++*yI|folB0=Uy=9V7Zj}DO0w4so;>Qh8T(!Nv+S#- zplt_vj(&ZUNhSCh5m=VV*`E3&Qm63fOWB6ZwT6?LO9zfs+f#KJ)U( zeZ$S!;Ld5oK(%6ff#)Az5( z9OFWA9&Mgw9ojwbWiKy16MWx8uIh;x{~FR}+K>VBp7H$;>e&NjBF;vA@y%2_=WOU{ z3)C1;th2&6C-#xg!`f9_zw=ouOsuaH=OU~)lWNvqPYnnDPTu-uBxh(YZ{VFjGVd>g zvSh$POi+J41uW~xePlS4)Cp`-Y?oo%gt-j+>(|^!b(3J+N)D!c}yk7*$ne&1;3j+EJw#TteN)SHFfd*mI$|ApDGa@n17lC*W*r~k# zjrl)i_doI`P^}Gg!*wu-_Xf7r#Jn+&%xkPYz^CK)r$IYFS(1s!IroWqVV=03GmjCx z>tFbHjedyh%OD;fSO}*-M?xk!PF!X#dkKhnVV(qkI=_7Nsb4NSu3kHrsn^2Z>a{?> zs$bjxww4;uanrA5YQYPStJlst?i?qgH~)qh5}`N|_|#mS2)@0!q_wjGT)zi;48#F$ z%Ruae!ph8yOx}@CduqA{xQ@B5xz1OD27+Ly7ML(L%|V#b|2R)jN6^inPe3el(=)&z zgZ7lM8elATg8G6OBgQHvtc?$G9vWSZD?1zTy0SgxcMb5|fU&v;)M#E{#J9Q6+Os^T zdKPp9)Sg;b4KQYZ1vNP@j)gz&*Zceh_{{C7Ew7uvUYkyg-3Sn48CMqQ_u%?{&{v?$ z8e5TJd&+qYFoujJW7=#R(EiVDh3{`qZL0 z0Ke&DKF=50$9q&l8%>NYV;nIKXy19ga2V8{+I9^v#$s*A;{ZN06@pmqx2IF11{iC` zT*vUfDfY@4t@cHwrD2Tn0!Erq( zI0N@CdQrqUlJ_BxD&*xt`^d~W?DcqbDOq~haGBwYbvmK9ZM{G}OJ&7^Lxv>hzuqt|nxp=xIPl&ZNQvkUWDRaXqa=n+EC0|#+>)X>wDvQpR|EQe80Y@lW*izI9v2- zlJ-s$G2Ypaob+g zVz<44Yc8?p{qjtG5aW}H_Ymu!2c9Y7Mg(__CB@w;rqiVWp8W{&GcoS#N0Bo4$E3V* zni{8)afgf-F^Tg5d&VF8s6+9*c~AAHkO`A+rr`FW`Q8o~`}`aGSX)-Lmn{XPPF zv;jS?&X?b#z^-A~jo}H`!KwVIVjE&opBt9@Jjc)HdA@P3XB_Y2VXwy-;p^}&B-z`P%^=@HisZzKsizO37F@&)@Z&;~Se z6hRKuZ9RpB0MulaXahswtHHLNw%+e`>_=*`261)F`N=6iCsZo8k&H8ih2Mtevyqbh zK9&tP-}=3d{V247(I8x1OMiLFuMN2k@VV0ZnH@>{ZnQA=p_rOH*VpHI`gkAR?{(}M z|1pTWI0kFObuYkVaBAy_lq$3V`0a_awb^GUQNJAn_I##?em1B04F3&rMknMz=sarJ z@!f2{&CpygJ?t5O(D*Lo+xPh?Jr`i+xeWw|-$eS^EeZK-!G}wnP1^Si@_qh8@{hZX z{6&unzpcQszf)kvtDt{UVA%`gpZ`Y@6WcrPdh!hW4!H+*Qe%S~EEMZ4TpMYC5g{f^&yU@%L0Gu%xxzUZD7RBYX8^$GmZXwj5ogf zKI%mN;a5=L>!(QCG=$_6rKDJoqD*$phd0LdHZC(_IV&EVk7wCO(&4!jSldg)$aa5m z0nP;|5cb@hVvjh>;ygrL?TdSm?12w*zNZGblq`7H8vP)Act*mv9(A9A z#OLt^$K0;^=(3+K<1gAk0r@|>kb-mnMDqUGIE%L&SZBbGANCo!ZWjf%4I&TD?{yA6 zBi#S>SozuL_d53D)L7f~;|s{MttSLwewpkeAT=K#V%%SW?VjnM8odD@o*7R=9&l)i z^pj#cPST2ZC@>1LVDLp07;`I0t3Dv5ZlBO2r~ii7Iq2sXdl&wWoFmR62W+}ok@=0V zM;jOqSup+E6v)?cAb>gQz6K$#uurHTy+s9-d(^bVkTBjS*>&5B!&0#-TZ3EWDIQusrWWcs5*qhNQaqJ^( zIcNjZe~&hhN3txkTPObwdxU+v29fQ{uIf1|jdZ$cVvjRd#2FOR3dwtTbn@lP;ur_u}w!R!ny$G61J73cFvMjueB-inr^29Gwb{47o?-}<@VBZ_~ zqa6gNJqkOFKj-{p^M$_sBVfnv)L5^VYQJaf`TQxSBJ4Fjk>fMtpnFwj{a?86r`zut zdnU*L*6EYC?Ti7UuRvexEdC|V(}NyY*XgE$J>DnZlm&2U(U}3EOaGGQ~;jUghu# z#M?PD{qbJ!_l!N$2OVIqADUL#^7dibPtKKpj66>>TCc~tomg|dSpA-{FVg7i)5%|x zyLEt4+9x#ThvF>pTtChlPt5c6xn4B(jQ_AJReN6Ef|y>x0#(bXe(G5y&J)cL_Tv0W zv;)`v57R4LqEGWatx3NVvOiEC?$0-Qga?-^f zXRFtM>WZ-z&w)LF_hm@$pO;*Gy?EH8?+3>ISom`!QN&6nRm{vfiyTi^jQi=#^^Djv{vSi{w=er;&wAw~_yW(KKAIgLeLu~9 zFR=ewJ$u`Auy2ATNc}dBrObQs;>J{sC-WJ1od#V%O6{K13)RY=3{)*8%k(ap!+ws6 zzkTNm*{uoqQ*#_`5li_Uv;n4A@pKc#{@-dF@Q=9>e)sj+Eqfm2E;$cV{haTIZ8<57 z_o05z*n_Gd0~$6x4n1Hndz>ORzBKyo@P5p6u?POVC(B>-tK6(zvR5-n<*Wp3WLBp*J?Y7c-fD&)9#Y;vZP@ ze3lBFr`ckiZd>(h75_|N&-jDtR^A8|KtuMJhQB7CouKc<75gw@w{KRj7d_WwnXhBd z_~UbM)qB}1bDrkx*z^|_f1@qOpwmsb-!t~}RQzj8VF&PK(Wlm4PSSu&#JHak>_1jz zKyc%yd5Ti@isjn=s_@?sHr!NYzK%WP4>GT~gd{Kay=5z5uTAh%?6<Fft3i*$-%(9g*{|gSe zQpGj~YCah|Wu^>mrfxHDCN>Na9ONH$|dLH`?^ z@wkeACa~|P##{1^zaIMEy5_2^P5n>yR8ZZL%hkT03G5fC_}6d8KB_FAFCr~`QTS=5 z-R~KDjXXooC%JYB_Px~HYcujcX>DJ1 z&L2;&7d_86%6uLB{%8YC13KW0q{$g6Uu($3b8IxQFHrH%1on(S$ayULdNg@o5za&{ zOm26&J-dDYlYUXyyDE8&Rzh>=qSo_yzpF!9z`5bH=fBhU>U7sJ)>m}0f z8TWx2;j?eqGg+U%X1{`(djn+u_UF{TA8o%+WR0iK^$h*Ke5r{Tf6Gq9Qpxh%U*}9( z^O4Z+W(@m5n7eR081sF^-fX*mV3P9?)h@kQ?fXXGjdZYQ{6UUm{d9P;ewB|FlNT{{ z1^&s*^R=;F$a}rtGxkiMsaOBHcd}momU@BfV1KI4WWQ(Zmtp*0(Up7+WjSL@L97P1S>LG`Qd3io~P^Xc|`V9)s1tiK*(zd2W| zV4RT=L;KGm{KXmQ$&UBY*bfH&AP2}>_V=vDam`&th2JE3E2*~lGPVCR?o68BhHk%a zq|ZO6i*ocy+B*$n0M?xB!;vZMhX8+&bJV#MJh3E4oaE*;3hXkL zmA45$&1mce{+bOpq|Z+P`|@J+{}r9cQ=9#De{?M=_7mhCd4-BU+ws_z6Rp>aH{P?% zM}HUnUc+9*(#2=@?4Oe2%H5v!X#TSC-Lb!?ivK*V@57dp4)!O3fAxCo#q_mA+$@Cx zOJ4}#pE2w|hYT2sc=sUhDvSe&$=_Czynw`W>DUQC{WU1Oh_ z!hPSkzHe&0H}-oS`(ePJX(Zwr>>djETOhtwmKt5azJuyk-9nX+0jZ4lEb|%nlglyh zFY87=>*_3(G=XgW4)V>oPsLxi<)kX}qp=^ZwgJc3Z&BdL%mfP4dTt5WcT(-@TZPYN zvg5suJzwi~K8mS0;_kO#3ho|9?oT_acDzht&-jmkO%qqg#0x2Kd~OSJnfX@-9B2b; zZc}|X5}fOWut(o7FYQcj^Nh?FEXf=dx0$??ZWD7qw&SEN^BH#?d&d6@5U%z~Xag(< z^7;IeB!;Gs=9H`%LDocjL(HEbjG5f|uZq5x~7L zJJ>V+BQek6>xA>kw+FuBh^^XIQoymBYS;fn_2Eba`xU^yv=h0iC+1BRDDGPFOub#$ zafLl6mAPK5ey?NC_>aMp9}D}@XagWV7t=BCb_$d)YBT$W zxeb^%-l59=Ca^!reSc+F^3=^q0io>2;DDsfCz>&zdLd&`?7HS9t=$YT5h=piKc334A9LJeylBunYd)Ue^# z}=XpPUuK5%BPtHL+RGcZ25t*RCw$?!Y+7fc?dWGtiT>%?TVaR`n<8D;VJB_Sc zan>koJef#A?{TsodY!5_UPqN{E~KiB*HFX3f8cnTLz&1vt9h^=zvrv_BF{$5CicKx zKO;}X)tmrdm?=1SWBxVdT>lsH9vMr)`Yni!R|EGL)+INuOJsR2`|BuZ+ezM&)5x*o zC8{sEh0OE1Hul+L8y=IU&u`fLn~b#qZo6`z7MLGKICR){bL88TiN+ySuOsK!A4Hsu zDfGFl(@k`q&vU(|WWL_-qp_a|>?f%t;_Kma-INYwDLR)L7F@=LiyT}2Le@2pkfr!~s-6EW_~jQ0KivplP4ssWcO?pY-G*Dg zC4+4s6JWGZ9)Ue0$uZ(|)s~~{^K*o~WioJQ(&Ovl%ID>XvoogRI!z_9|9%G6QyPi= zcr)QoQ3RR=nhi4gXg7ns(T>;N2GHmrYkjL`9rnfX8qb>XyMt9UKsUi^3%f7a*fI^Cw$_i4!de8HagVli2^J$A~s z0oi_(Twni;Y>)w`4EBqGzeZ{sXgj*fRD^<5znh%1ZWgvYVb5WGu2H9(=3FnY`n`_* z63v!_t7SXOfm6H<)NNJU0ObBDgZ#9>$V(R>vlel zHT|g*=OETtA=?g+bK(7=b5TS3e4|b`f%Sb}%lv5Ui($86T3$f*V;{w;mQ(6C*;P*N zlBZzHDGb|lbbWrC#h&qpK4H_XdE>Xyl8sLBTgkKeUu2!oRkh~_0S-Y(*DSJ>-9*vk_UB-&U~K zYc*fiEO%o zoK>Uq&R!%HvnIL1IoKOfU%H`Kf@;z=iU1%W|L#>@c1l?}M^3GJf_HwG<{YP?DO;GhYt;j!5t53%KG0=seyjzrLWsHAe zKS`e%{<)>(`lcV*Ha;QZE1DOa2S2@zYP=QTFZ^`pX#Sd-&nBOlA;=JWgX3rmVjm91 zerX|9t~iIP*Zhd;wm(Yt!@bB`w+Qkb=j>$F*E~M!Y2Lw@GxjD>-!${epT~iT?5m}K zZ993Zz9QG50c79x5;bi89nQ!5InGGCiE2x)!dXd|;=G)TR4ToM%qxF{{kS(!-G;k_ z-;QyZZ)#+WnqOt*mGWZZ@m{>+aWyfXG-g<~sg`_&hw32ixyy6E(nCGv+2vd<9(#It*%0ZMy~- z<7+?ydjtO52KXSJwi9p(%(SP78enW0W6|eB4InH7PRIX7fSTVVg&W#atE&OVl(A)u zjnIca^Y5SwK)DYnh?lmftkwWy%GieRj{d;^)9CxS^8M{{0F}_n6Jy9&GNz5>elu-= z+X~Bz{0ugLg0-i(HNfLNV<`GPuF-He`JL|t@mRt0g(IN$)VgZmD2mJ2H96il3BL9Z zJgbifyj~a%%E196cuRXqrv?}!?(>XU0Vp2KasSVGfI5P12K5E8?$`}#Px(;;jKxk6 zW5aqq#wsqZ$=<+ug1Ak655%@3wjY*(PJ*B~r9AQ_LuuM((tWnL&R2p4f!K!2m?Yib z$!2T?=KxLPcc=(^8^0HjV*F|{^^zjL-|dV1eo;jBCet~`Bmdty`g>XA{|kFZelLjk zJ@WSUZ|ybEUIXnl&|U-WHISJa=pC6&-0`R^^8Ui(k>5KV^v*~MWt9;n*()>iG|6mIc4oG+_i3P%oxM}p zE63>IoZtO=9rfw^`1Qy8?RCy|U*ma=`?{~!%>ykBRhCU$n*abTYNwSh0Dyx(;s7%v z{GT7U@ErhRnADU`>Udx#`xt^nXHzuq$I}?{~c2ZQlW(+bzD3vkKm4 z+!;~xJ*v6vC#KPHkp%zQS}o+ec8mG!^AU)7=KEaqv{b5GtJzo@*~N2(H$>ePiW_d} zp^c7_bvzIEy|CBdNa9V&YmsVEZSl35$oGsCaaBvKKhh(ELk(DwKb=8YHZ=F z3;-mXu|jf&ivdNcTO=#mvrqgOu+05=&M>JlrP9TWE3S?Sfai~S&Qxg(sC%XS=#~G) zHr^3!s}ug|L0 zMZ;48iplSG9-GWpo2UhxAm5X6+4C6u(4}sEtUUkTQ4)y%S)eB?FaHGc4*WUhVr}3| zUJBg;A|<(>XP-8Qq>dJp(~;t?EU{aFf#O2)lJ+I|;n~ww8gfb5Pp^Td$+0J6R0yP& z>gz!c6t{_ZNV@Sj}=*!@>7-{d=$@*y7_*H2;zLSxI{dRti3@`F`6hLDQMBLP$a=>3yu{*ue!fD16%@X%&iA!-M!)aT3GSpcnTaSY)~C@6pI=DNfMuAi|;MQ|lZp3_WC zAISF=<*|k0;A>)7(!q^TbI-`gN^9`SJ}-rLixO0v z)Yv-7NVG0ETdD{(B>9z!iP(WxD!Wtkw<9<|*Kc8DB;F{}Wr zC-Hje-g-MAq%xn5rnitZ}M0`$ii(JVs& z9FFVPbVZCAZaJgN4Ln=z4kCb+bLAEVGLSeBH_tl6Wp#txvlJwE0PVPfh+#5j@C~Bk zYE@9iF|Y?I0K~W4dW#s-TVzjUJ>!y!LGBj@k{IH=Bjvo$TtVk?WVt|*ZWkl5-^B!> zQkkCqs-1!vG(*(fRblq|82Iw+vKo9KjaEedJoFwo>LGMB)87+V!RJ&pl&IC&@A7uoeJ&UMKaW=)y(6c*Kwx1X6xNeht(sX5-^)#KXytf#;$)#}))Z|sPV zEv<4s?!}9?ZJx=!b#zZ-_J)@-xRPUrLj6T0KeOa!uE6aK0W~Oo8 z$J7b-6>_P$Z(k*Ud{H58;a1yTAA6sRCfz%l-3wI~q6&+tujO8*mOkN{(nynecwT*9 z2KT4(+a(DU766#f$Y$=r44&j>1K>3K*{QQT@}-}*dAJ+UEH%ef4b4g&jFo0gI9ql{ zFuJsPS2b8JT7T6k3RcVZiSo{!=qfts)BE*QXTH9ndyeE(ms7oHjN(Az?uccVr8=TQ zrD#%){ew~WvTvR#a%onT`Rz3~ZkTVXi}G}ZsYAViBNkYj!X_Y3IV7UOPDi39Lh;AKyC!@e!xfCB~DL1Nqc}*46o^!w48XF^Bo178hu@I_@kVXR017mIMG@c1$p5!|vl5svL*Oqjnis3Z*@O zXX?zw9H#QF1h^YAu(Ij^X`>5g0r>h*>8*~sP^zF?D!D%7XocC$(;N~j64w%@irp`U zr~-b%Mmf(ybt#egG3|*{l=|%FDU{akm_y8f$h9rP|w*?PTr0=!;6nkia9=z z?4G$eQ7e}i`Az+EoNziU6vdf}45S$xAaIhCf+D{+jMhdCm-;xJ77+9_4_@m0Ss2Dl zz`ZiXIrbmiY(ycI5~(ftjs(a1Rfllk>H8dtFpyBQ<{)Cg>=^YiL8GcKys0eWo@<>> z+*)T*P6|K36PZoFbQj_C%3N)N^i6u$n{g(A`dC1`)pQ^&O%Y3~$Ab7cs)^ruzSIF* z_0cKycd|Dg>(@v(`ewvI*?d^(nO|!d&)$4X@h_h`x?KemJ6HI;>3rBj?V| zUn7Ss9PN`3d$o61UW3W+lnQ`yNtJo*rQ|u5lk#0pnqU#|fhE#`RTApkEdsz9=(Kh% zjjFE5rm4-RxD@(-Z}&x&)q1!2R>(>DmKZJf6h27Tv^Q-`3rot9g;=*M|DvoFCQV=7 z(Q;*b`LP<$MP6tg0XNzcS@t#GLSJ-#RJi9|B>QP$U5O2VDBkBGgc%fjg8_Vxq$EE* zYnIPQkTpvm7#o}!K`mp$Ozu9KYFL!>YfNhDXn{%|Bpqb+-v(qi087i1q_&}Q--7*V z9rc^7K1%x!S7xHp_^_Ss(^rdlI=8i7^E>n4_CY{6Wiw*~ij4sA{!`KC){EB{k|mGE zC{&A?1s5&VY=iNqL_8D-3Og{y?GwZ*mk-NR09YDvEbRy0+Hev6BfDg&aH3|ulIwTe z0992HmGg?qY??Li=RS6!$hVjMb>$(`O`wUdvO5ACQ3b@(LgDkdzNDGLlD#ZE6OP|I z+@P!2ps2!_yv}4Xhs`!uq!SaMZflWFxtPHtYMdA_uHlssUu9bq>MI$tShvegaeP7s zU84kS^5k4sZ`)5eFbe+Gu8YEguMzo4=b5&He#oNF(pj2fXH&cm+TxV7;g*jtQUGur z%7P@+{XmlS#0>4n&%2^=oG#MK4}cO90j9K=++4#Y2NtXI(gM&EKrNjevJMh)>^n&MI6mBTOXB`uoK=*IBtx zRC%&hInEIl(2Byg3_;_K)tjIZx>I+OaYVmOKgkvlG6Nd_2@uCmW!{85ZmpRAxQ_u3*J@inO2s`Z-Lv`0qW zi1I1>&`Ka|m=wBo^rCTy7a++63I;!a2>AoJjnet-d|h{OexhCmyYh487EpO&Gq1=t9RjyJozseFAT`sy(38Mp2LPq&LpLgH$*MHGfJhO|gzLHF>kYSLR+y-K* z>uCV8-j*x8mPpy#esUm!?mMJ%QD-oX#oOzm#^nHMSOa5P002{#=@~Ow@ltcWi#0)1 z`T42cDC^Ny#NR6ohT|;*rQZTM&SJn;gU>V;aB5%j1|W1P#d5qk#wtcp<{CG3JvjIY zV*=QxrDp6d6JB=sn|T{B;7s^aiH|%$lpVzHn=4PNvOPCO-gwggJ-z<2VJOo4gipH_ zs??WkS<(cI*&x|S4+E(O2c-G@QeN@qXpVQj#;m;5gpMQopy3oh6V;RUa=w)Op#G$t z#!pBYFE<6792qQd5e%Z-u=&rwV%x*H+f&6kF6)H@t|JzloR?jA8{(Vwz~Topa14_N zs~UiKx%9EJeEzX)yqKYXl^xvT5u?$(HO;cJ_wO%kNDa5tgLJ;;tRNAdqFoLIQ7+iL z8=B1-U#}U+3LEZrdz0Zl4!G@!bu$oxK>SlJ_x`}Hc04+n_#$GpJoM!>b4 zMw1u*#k_ z-I_Xd|Ez#k$>Yi`?wCYvlfe+~9NiysY@hKVIJ}_E(dtv4hI&@ZuG(eH|EuSnpRS_} zyVts3z`)}ASbaZ9iWzVbjya5r8D5ZevTgE(TlzW#@pBNgm-Di&jqUNhUAI9zro83{lq9GaUxd-N~% z{_#3O?ay8GO5JZd_@JSLu0I%@zn?Ud(EteQd^u8I0_!~NXNDZ;TOb`Crv4Y|UgTi8 zz1prDK&9Df%9C+sK>bYHuKvxs+iST;6_<%*lU#r;u-4~zPq$N4Ok-yLSMp|MtT#t>dL&VOYb+) zwPfKGxR<_?3}R8Ufvb|l&wmwUuboo$Dft^9NC>oN+qv2}?Gpg6_GYlC8A3hyVx<=P z+SEck_6iMm5ev>J(_@ovXXN7K6GQ9`^Uct_+~$X04}e!Uo|L|kwK4iAOR>wi_P;QJ ziQj!?v)sb)V!$zFZJ<@{w(D^z4(M7N=EuuLCNAEo>i)lqh+Z1Hpng~wyt=@Be_tGk zuS?pL;#+<^ZK!aB+Q#!ILJ%8w9jiJuV82<|5Vj?{HD;#+nL+x;yA7EQJTtSsRTKd_ z5_Gq;+grNY=*x2sXW9emQ+RaWu?0iekj1jo<^9)J!4{LX^lroQ zBlD3G1yqwG9=`suPJ%e_>^d_zv?$VjPDAOX%cdAKKlfNSS`?A87|M-P=h*~}OSY7dXMgz8*&-3;J)@jCgt zOU?7#;M6;`PyO;hZL!a3&r~NIdAeRruVRu5D`Cc_ZXepok9x|3i3uVQ@wd*9l$Ipk|e1=Gfxx#KzQoK{@a_RhQ zRt!{u2z+<3VF^TX4tTP5{!7q43{Wd|SvkVvd{OdDP*GT;eX#FeRiLg;=KFuTJO;#l zA#VcWOj;jAiS@Gc1rCLiewAITy-!mJz7)#^=Db%y3@qcH7{8BpQp zoH&m;bHl3!s5J?8?>(AMj@2ptW9xMu%(Q2_5(MOV;l8!iar<6xCob~8EaY*W?Y#F8 zW4-cK6oaJVx`SKZ-H`p4L>E{5QWC{|@uIMGqDSw`0*TeV|FhzVAlqPqr`SGdMYjOA zcypNCl(E_!UhJPpBZ9oUfVL5&K9xhGjn*a6qw*1X@Eo^pOYCJ~j1GAnxqqphV-U8X|gMr*kv zbQALVSUmr6%NJu@abX)P5=d?tye3@_qJnEzZoB*w0Y{4PrU`OeP>QsEp>Jyzh5Rgj z|FU4h6<1qrKl<@MMAaCnuKI8HYQMHZ_myn|Y^D?pz@Alv;c^fIy~GHJKpRICG%l7w z^|I0r_n#xJ*_=1fQakl8O!JJGexLe9G->_03$l5Oj19QMX#dlUn8pD4ZNM!Lm3po= zBFrcYl6}RW-u>$jDa=nnY_##zeZ;A>^X&M6lBDg)in>%!_Wyar9{WWfS3Lh>Vh#3X zfNxB3VoRRK{K0w&>c4R3;0LGlHEXK&Aw?o(Q7G2ULq6o+)c)dy>$GqdranSs=|}j_ z5!bA(CjTi!ru_n>DUG;*ZqpsZh`xdIg_2MZ6>*ckAisDbVCC2~J^C2(J<5RLB{d?+mG|)kf>oxqri^cP}X_I~sBLgnmfaAW@8 z2quQWWcacXxBOw@2NF#?bJYu3hV9jBhpQq1OcQhqAc#63Kg||q9C)AS2R(eql*3_n zO-#h9&#L~vc9}tR_ZueKtf@oO=VUGBkJwlJ9*@)%9URG z^IU(6)i1B@P%Lr8IDbDE3@4~jj<^(y+y9BShYYyW!+sfaVh|l=o2SZq^6c4)ElMIbi)`axqzEgG&F z-b-8gn@8X0WdLL01>sIa(!b0xR1Y`5xg%?)}w~!iNy`5CGtQ9NPH5g_Q+3zpONpz zl6UzQHgS_p5)M9tQl5}>HR<38Daq8kY!NzM0H5Za^umz%OF;y z