diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index c50d55025..d73dc7bfd 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -8,6 +8,7 @@ on: env: FLUTTER_VERSION: "3.16.9" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" + RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 jobs: generate_bridge: @@ -49,9 +50,9 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: - toolchain: stable + toolchain: ${{ env.RUST_VERSION }} targets: ${{ matrix.job.target }} - components: '' + components: "rustfmt" - uses: Swatinem/rust-cache@v2 with: diff --git a/.github/workflows/build-macos-arm64.yml b/.github/workflows/build-macos-arm64.yml index 9bbfb66a2..c8c984e1a 100644 --- a/.github/workflows/build-macos-arm64.yml +++ b/.github/workflows/build-macos-arm64.yml @@ -54,8 +54,3 @@ jobs: - name: Run shell: bash run: | - cd /opt/build - #./update_mac_template.sh - #security default-keychain -s rustdesk.keychain - #security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain - ./agent.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccfc1bdf4..9a8ecf5cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,12 +71,12 @@ jobs: # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - # - { target: i686-pc-windows-msvc , os: windows-2019 } + # - { target: i686-pc-windows-msvc , os: windows-2022 } # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: x86_64-apple-darwin , os: macos-10.15 } - # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - # - { target: x86_64-pc-windows-msvc , os: windows-2019 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + # - { target: x86_64-pc-windows-msvc , os: windows-2022 } - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 0cf2dd461..f6123e4a2 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -14,7 +14,7 @@ env: RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 CARGO_NDK_VERSION: "3.1.2" LLVM_VERSION: "15.0.6" - FLUTTER_VERSION: "3.16.9" + FLUTTER_VERSION: "3.19.6" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" # for arm64 linux because official Dart SDK does not work FLUTTER_ELINUX_VERSION: "3.16.9" @@ -41,7 +41,7 @@ jobs: uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml with: upload-artifact: ${{ inputs.upload-artifact }} - target: windows-2019 + target: windows-2022 configuration: Release platform: x64 target_version: Windows10 @@ -56,10 +56,10 @@ jobs: fail-fast: false matrix: job: - # - { target: i686-pc-windows-msvc , os: windows-2019 } - # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - - { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64 } - # - { target: aarch64-pc-windows-msvc, os: windows-2019, arch: aarch64 } + # - { target: i686-pc-windows-msvc , os: windows-2022 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + - { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64 } + # - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 } steps: - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 @@ -83,14 +83,6 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - - name: Replace engine with rustdesk custom flutter engine - run: | - flutter doctor -v - flutter precache --windows - Invoke-WebRequest -Uri https://github.com/fufesou/flutter-engine/releases/download/bugfix-subwindow-crash-3.16.9-apply-pull-47787/windows-x64-release.zip -OutFile windows-x64-flutter-release.zip - Expand-Archive windows-x64-flutter-release.zip -DestinationPath . - mv -Force windows-x64-release/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - - name: Install Rust toolchain uses: dtolnay/rust-toolchain@v1 with: @@ -121,7 +113,13 @@ jobs: shell: bash - name: Build rustdesk - run: python3 .\build.py --portable --hwcodec --flutter --gpucodec --skip-portable-pack + run: | + Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip + Expand-Archive usbmmidd_v2.zip -DestinationPath . + python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack --virtual-display + Remove-Item -Path usbmmidd_v2\Win32 -Recurse + Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat" + mv -Force .\usbmmidd_v2 ./flutter/build/windows/x64/runner/Release/ - name: find Runner.res # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res @@ -170,6 +168,19 @@ jobs: mkdir -p ./SignOutput mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.exe + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Build msi + if: env.UPLOAD_ARTIFACT == 'true' + run: | + pushd ./res/msi + python preprocess.py --arp -d ../../rustdesk + nuget restore msi.sln + msbuild msi.sln -p:Configuration=Release -p:Platform=x64 /p:TargetVersion=Windows10 + mv ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-beta.msi + sha256sum ../../SignOutput/rustdesk-*.msi + - name: Sign rustdesk self-extracted file if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' shell: bash @@ -183,6 +194,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | + ./SignOutput/rustdesk-*.msi ./SignOutput/rustdesk-*.exe ./rustdesk-*.tar.gz @@ -196,10 +208,10 @@ jobs: fail-fast: false matrix: job: - # - { target: i686-pc-windows-msvc , os: windows-2019 } - # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - - { target: i686-pc-windows-msvc, os: windows-2019, arch: x86 } - # - { target: aarch64-pc-windows-msvc, os: windows-2019 } + # - { target: i686-pc-windows-msvc , os: windows-2022 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + - { target: i686-pc-windows-msvc, os: windows-2022, arch: x86 } + # - { target: aarch64-pc-windows-msvc, os: windows-2022 } steps: - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 @@ -245,11 +257,15 @@ jobs: python3 res/inline-sciter.py # Patch sciter x86 sed -i 's/branch = "dyn"/branch = "dyn_x86"/g' ./Cargo.toml - cargo build --features inline,gpucodec --release --bins + cargo build --features inline,vram,hwcodec,virtual_display_driver --release --bins mkdir -p ./Release mv ./target/release/rustdesk.exe ./Release/rustdesk.exe curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll echo "output_folder=./Release" >> $GITHUB_OUTPUT + curl -LJ -o ./usbmmidd_v2.zip https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip + unzip usbmmidd_v2.zip + rm -rf ./usbmmidd_v2/x64 ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/deviceinstaller64.exe ./usbmmidd_v2/usbmmidd.bat + mv ./usbmmidd_v2 ./Release || true - name: find Runner.res # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res @@ -325,8 +341,7 @@ jobs: - name: Build rustdesk run: | - # --hwcodec not supported on macos yet - ./build.py --flutter + ./build.py --flutter --hwcodec - name: create unsigned dmg if: env.UPLOAD_ARTIFACT == 'true' @@ -480,8 +495,7 @@ jobs: - name: Build rustdesk run: | - # --hwcodec not supported on macos yet - ./build.py --flutter ${{ matrix.job.extra-build-args }} + ./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }} - name: create unsigned dmg if: env.UPLOAD_ARTIFACT == 'true' @@ -630,7 +644,7 @@ jobs: - name: Build rustdesk lib run: | rustup target add ${{ matrix.job.target }} - cargo build --features flutter --release --target aarch64-apple-ios --lib + cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib - name: Build rustdesk shell: bash @@ -1251,7 +1265,7 @@ jobs: export DEFAULT_FEAT=linux_headless fi export CARGO_INCREMENTAL=0 - cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }},$DEFAULT_FEAT --release + cargo build --lib --features flutter,flutter_texture_render,hwcodec,${{ matrix.job.extra-build-features }},$DEFAULT_FEAT --release - name: Upload Artifacts uses: actions/upload-artifact@master @@ -1594,7 +1608,7 @@ jobs: ;; esac export CARGO_INCREMENTAL=0 - python3 ./build.py --flutter --hwcodec --skip-cargo + python3 ./build.py --flutter --skip-cargo # rpm package echo -e "start packaging fedora package" pushd /workspace diff --git a/.github/workflows/history.yml b/.github/workflows/history.yml index 91e695f8e..a0c2294c0 100644 --- a/.github/workflows/history.yml +++ b/.github/workflows/history.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: job: - - { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 } + - { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 } steps: - name: Checkout source code uses: actions/checkout@v4 diff --git a/.github/workflows/third-party-RustDeskTempTopMostWindow.yml b/.github/workflows/third-party-RustDeskTempTopMostWindow.yml index 78f3ad2f1..2f89092b7 100644 --- a/.github/workflows/third-party-RustDeskTempTopMostWindow.yml +++ b/.github/workflows/third-party-RustDeskTempTopMostWindow.yml @@ -10,7 +10,7 @@ on: description: 'Target' required: true type: string - default: 'windows-2019' + default: 'windows-2022' configuration: description: 'Configuration' required: true diff --git a/Cargo.lock b/Cargo.lock index 5c0a17603..99f7180bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,17 +115,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "amf" -version = "0.1.0" -source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b" -dependencies = [ - "bindgen 0.59.2", - "cc", - "gpu_common", - "log", -] - [[package]] name = "android-tzdata" version = "0.1.1" @@ -2716,36 +2705,6 @@ dependencies = [ "system-deps 6.1.2", ] -[[package]] -name = "gpu_common" -version = "0.1.0" -source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b" -dependencies = [ - "bindgen 0.59.2", - "cc", - "log", - "serde 1.0.190", - "serde_derive", - "serde_json 1.0.107", -] - -[[package]] -name = "gpucodec" -version = "0.1.0" -source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b" -dependencies = [ - "amf", - "bindgen 0.59.2", - "cc", - "gpu_common", - "log", - "nv", - "serde 1.0.190", - "serde_derive", - "serde_json 1.0.107", - "vpl", -] - [[package]] name = "gstreamer" version = "0.16.7" @@ -3140,8 +3099,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" -version = "0.2.0" -source = "git+https://github.com/21pages/hwcodec?branch=stable#52e1da2aae86acec5f374bc065f5921945b55e7b" +version = "0.3.3" +source = "git+https://github.com/21pages/hwcodec#eeebf980d4eb41daaf05090b097d5a59d688d3d8" dependencies = [ "bindgen 0.59.2", "cc", @@ -4224,17 +4183,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nv" -version = "0.1.0" -source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b" -dependencies = [ - "bindgen 0.59.2", - "cc", - "gpu_common", - "log", -] - [[package]] name = "objc" version = "0.2.7" @@ -4852,9 +4800,9 @@ dependencies = [ [[package]] name = "protobuf" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190" +checksum = "58678a64de2fced2bdec6bca052a6716a0efe692d6e3f53d1bda6a1def64cfc0" dependencies = [ "bytes", "once_cell", @@ -4864,9 +4812,9 @@ dependencies = [ [[package]] name = "protobuf-codegen" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e85514a216b1c73111d9032e26cc7a5ecb1bb3d4d9539e91fb72a4395060f78" +checksum = "32777b0b3f6538d9d2e012b3fad85c7e4b9244b5958d04a6415f4333782b7a77" dependencies = [ "anyhow", "once_cell", @@ -4879,9 +4827,9 @@ dependencies = [ [[package]] name = "protobuf-parse" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77d6fbd6697c9e531873e81cec565a85e226b99a0f10e1acc079be057fe2fcba" +checksum = "96cb37955261126624a25b5e6bda40ae34cf3989d52a783087ca6091b29b5642" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -4895,9 +4843,9 @@ dependencies = [ [[package]] name = "protobuf-support" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c" +checksum = "e1ed294a835b0f30810e13616b1cd34943c6d1e84a8f3b0dcfe466d256c3e7e7" dependencies = [ "thiserror", ] @@ -5856,7 +5804,6 @@ dependencies = [ "cfg-if 1.0.0", "dbus", "docopt", - "gpucodec", "gstreamer", "gstreamer-app", "gstreamer-video", @@ -7097,17 +7044,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "vpl" -version = "0.1.0" -source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b" -dependencies = [ - "bindgen 0.59.2", - "cc", - "gpu_common", - "log", -] - [[package]] name = "waker-fn" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index 66fae0be8..a9bbcef9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ use_dasp = ["dasp"] flutter = ["flutter_rust_bridge"] default = ["use_dasp"] hwcodec = ["scrap/hwcodec"] -gpucodec = ["scrap/gpucodec"] +vram = ["scrap/vram"] mediacodec = ["scrap/mediacodec"] linux_headless = ["pam" ] virtual_display_driver = ["virtual_display"] @@ -99,7 +99,20 @@ 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"] } +winapi = { version = "0.3", features = [ + "winuser", + "wincrypt", + "shellscalingapi", + "pdh", + "synchapi", + "memoryapi", + "shellapi", + "devguid", + "setupapi", + "cguid", + "cfgmgr32", + "ioapiset", +] } winreg = "0.11" windows-service = "0.6" virtual_display = { path = "libs/virtual_display", optional = true } diff --git a/build.py b/build.py index cb588f7d0..1779d7d46 100755 --- a/build.py +++ b/build.py @@ -33,9 +33,9 @@ def get_arch() -> str: def system2(cmd): - err = os.system(cmd) - if err != 0: - print(f"Error occurred when executing: {cmd}. Exiting.") + exit_code = os.system(cmd) + if exit_code != 0: + sys.stderr.write(f"Error occurred when executing: `{cmd}`. Exiting.\n") sys.exit(-1) @@ -118,9 +118,9 @@ def make_parser(): '' if windows or osx else ', need libva-dev, libvdpau-dev.') ) parser.add_argument( - '--gpucodec', + '--vram', action='store_true', - help='Enable feature gpucodec, only available on windows now.' + help='Enable feature vram, only available on windows now.' ) parser.add_argument( '--portable', @@ -153,6 +153,12 @@ def make_parser(): action='store_true', help='Skip packing, only flutter version + Windows supported' ) + parser.add_argument( + '--virtual-display', + action='store_true', + default=False, + help='Build rustdesk libs with the virtual display feature enabled' + ) parser.add_argument( "--package", type=str @@ -282,8 +288,8 @@ def get_features(args): features = ['inline'] if not args.flutter else [] if args.hwcodec: features.append('hwcodec') - if args.gpucodec: - features.append('gpucodec') + if args.vram: + features.append('vram') if args.flutter: features.append('flutter') features.append('flutter_texture_render') @@ -293,6 +299,9 @@ def get_features(args): features.append('appimage') if args.unix_file_copy_paste: features.append('unix-file-copy-paste') + if windows: + if args.virtual_display: + features.append('virtual_display_driver') print("features:", features) return features diff --git a/build.rs b/build.rs index 47526d639..d332a43a2 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,11 @@ #[cfg(windows)] fn build_windows() { let file = "src/platform/windows.cc"; - cc::Build::new().file(file).compile("windows"); + 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")] diff --git a/docs/README-VN.md b/docs/README-VN.md index 5ce0a7688..9c8ebcf23 100644 --- a/docs/README-VN.md +++ b/docs/README-VN.md @@ -1,27 +1,29 @@ + +

- RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn
- Máy chủ • + RustDesk - Your remote desktop
+ ServerBuildDocker • - Cấu trúc tệp tin • + StructureSnapshot
[English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Ελληνικά]
- Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, RustDesk UItài liệu sang ngôn ngữ bản địa của bạn + Chúng tôi rất hoan nghênh sự hỗ trợ của bạn trong việc dịch trang README, trang giao diện người dùng của RustDesk - RustDesk UI và trang tài liệu của RustDesk - RustDesk Doc sang Tiếng Việt

-Chat với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Một phần mềm điểu khiển máy tính từ xa, đuợc lập trình bằng ngôn ngữ Rust. Hoạt động tức thì, không cần phải cài đặt. Bạn có toàn quyền điểu khiển với dữ liệu của bạn mà không cần phải lo lắng về sự bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi, [tự cài đặt máy chủ](https://rustdesk.com/server), hay thậm chí [tự tạo máy chủ rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). +RustDesk là một phần mềm điểu khiển máy tính từ xa mã nguồn mở, được viết bằng Rust. Nó hoạt động ngay sau khi cài đặt, không yêu cầu cấu hình phức tạp. Bạn có toàn quyền kiểm soát với dữ liệu của mình mà không cần phải lo lắng về vấn đề bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi hoặc [tự cài đặt máy chủ của riêng mình](https://rustdesk.com/server) hay thậm chí [tự tạo máy chủ rendezvous/relay cho riêng bạn](https://github.com/rustdesk/rustdesk-server-demo). ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`docs/CONTRIBUTING.md`](CONTRIBUTING.md). +**RustDesk** luôn hoan nghênh mọi đóng góp từ mọi người. Hãy xem tệp [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) để bắt đầu. -[**RustDesk hoạt động như thế nào?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) - -[**CÁC BẢN PHÂN PHÁT MÃ NHỊ PHÂN**](https://github.com/rustdesk/rustdesk/releases) +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/FAQreleases/tag/nightly) [Get it on F-Droid`. Ví dụ nếu bạn cần build phiên bản đuợc tối ưu hóa, bạn sẽ chạy lệnh trên cùng với cài đặt lệnh ‘--release’. Kết quả build sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh: +Lưu ý rằng **lần build đầu tiên có thể mất thời gian hơn trước khi các dependencies được lưu vào bộ nhớ cache**, nhưng các lần build sau sẽ nhanh hơn. Ngoài ra, nếu bạn cần chỉ định các đối số khác cho lệnh build, bạn có thể thêm chúng vào cuối lệnh ở phần ``. Ví dụ, nếu bạn muốn build phiên bản tối ưu hóa, bạn sẽ chạy lệnh trên với tùy chọn `--release`. Kết quả biên dịch sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh: ```sh target/debug/rustdesk ``` -Nếu bạn đang chạy bản build đuợc tối ưu hóa, thì bạn có thể chạy với lệnh: +Nếu bạn đang chạy bản build được tối ưu hóa, thì bạn có thể chạy với lệnh: ```sh target/release/rustdesk ``` -Hãy đảm bảo là bạn đang chạy những lệnh này từ thu mục rễ của repo RustDesk, vì nếu không thì ứng dụng có thể sẽ không tìm đuợc những tệp tài nguyên cần thiết. Cũng như nhớ rằng những lệnh con của cargo như `install` hoặc `run` hiện chưa được hỗ trợ bởi phương pháp này vì chúng sẽ cài đặt hoặc chạy ứng dụng trong container thay vì trên máy tính của bạn. +Hãy đảm bảo rằng bạn đang chạy các lệnh này từ gốc của thư mục **RustDesk**, nếu không, ứng dụng có thể không thể tìm thấy các tệp tài nguyên cần thiết. Hãy lưu ý rằng các câu lệnh con khác của **cargo** như **install** hoặc **run** hiện không được hỗ trợ qua phương pháp này, vì chúng sẽ cài đặt hoặc chạy chương trình bên trong **container** thay vì trên máy tính của bạn. ## Cấu trúc tệp tin - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác -- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: để ghi lại màn hình -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: để điều khiển máy tính/con chuột trên những nền tảng khác nhau +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ghi lại màn hình +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: điều khiển máy tính/chuột trên các nền tảng khác nhau - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng -- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: để bắt đầu kết nối với một peer -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Để liên lạc với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi cho kết nối trực tiếp (TCP hole punching) hoặc kết nối được relayed. +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bắt đầu kết nối với một peer +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp. - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng -- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành cho điện thoại +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter ## Snapshot diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index 9e32e163e..ca405647b 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -108,4 +108,3 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } } } -apply plugin: 'com.google.gms.google-services' diff --git a/flutter/android/app/google-services.json b/flutter/android/app/google-services.json deleted file mode 100644 index 3945e432a..000000000 --- a/flutter/android/app/google-services.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "project_info": { - "project_number": "768133699366", - "firebase_url": "https://rustdesk.firebaseio.com", - "project_id": "rustdesk", - "storage_bucket": "rustdesk.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:768133699366:android:5fc9015370e344457993e7", - "android_client_info": { - "package_name": "com.carriez.flutter_hbb" - } - }, - "oauth_client": [ - { - "client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyAPOsKcXjrAR-7Z148sYr_gdB_JQZkamTM" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index edec8c42d..75304be64 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -1,5 +1,7 @@ package com.carriez.flutter_hbb +import ffi.FFI + /** * Capture screen,get video and audio,send to rust. * Dispatch notifications @@ -64,10 +66,6 @@ const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO class MainService : Service() { - init { - System.loadLibrary("rustdesk") - } - @Keep @RequiresApi(Build.VERSION_CODES.N) fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) { @@ -156,23 +154,9 @@ class MainService : Service() { private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager } private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")} - // jvm call rust - private external fun init(ctx: Context) - - /// When app start on boot, app_dir will not be passed from flutter - /// so pass a app_dir here to rust server - private external fun startServer(app_dir: String) - private external fun startService() - private external fun onVideoFrameUpdate(buf: ByteBuffer) - private external fun onAudioFrameUpdate(buf: ByteBuffer) - private external fun translateLocale(localeName: String, input: String): String - private external fun refreshScreen() - private external fun setFrameRawEnable(name: String, value: Boolean) - // private external fun sendVp9(data: ByteArray) - private fun translate(input: String): String { Log.d(logTag, "translate:$LOCAL_NAME") - return translateLocale(LOCAL_NAME, input) + return FFI.translateLocale(LOCAL_NAME, input) } companion object { @@ -211,7 +195,7 @@ class MainService : Service() { override fun onCreate() { super.onCreate() Log.d(logTag,"MainService onCreate") - init(this) + FFI.init(this) HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply { start() serviceLooper = looper @@ -223,7 +207,7 @@ class MainService : Service() { // keep the config dir same with flutter val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE) val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: "" - startServer(configPath) + FFI.startServer(configPath, "") createForegroundNotification() } @@ -278,7 +262,7 @@ class MainService : Service() { SCREEN_INFO.dpi = dpi if (isStart) { stopCapture() - refreshScreen() + FFI.refreshScreen() startCapture() } } @@ -306,7 +290,7 @@ class MainService : Service() { createForegroundNotification() if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) { - startService() + FFI.startService() } Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}") val mediaProjectionManager = @@ -354,12 +338,15 @@ class MainService : Service() { ).apply { setOnImageAvailableListener({ imageReader: ImageReader -> try { + if (!isStart) { + return@setOnImageAvailableListener + } imageReader.acquireLatestImage().use { image -> - if (image == null) return@setOnImageAvailableListener + if (image == null || !isStart) return@setOnImageAvailableListener val planes = image.planes val buffer = planes[0].buffer buffer.rewind() - onVideoFrameUpdate(buffer) + FFI.onVideoFrameUpdate(buffer) } } catch (ignored: java.lang.Exception) { } @@ -393,21 +380,24 @@ class MainService : Service() { } checkMediaPermission() _isStart = true - setFrameRawEnable("video",true) - setFrameRawEnable("audio",true) + FFI.setFrameRawEnable("video",true) + FFI.setFrameRawEnable("audio",true) return true } @Synchronized fun stopCapture() { Log.d(logTag, "Stop Capture") - setFrameRawEnable("video",false) - setFrameRawEnable("audio",false) + FFI.setFrameRawEnable("video",false) + FFI.setFrameRawEnable("audio",false) _isStart = false // release video virtualDisplay?.release() - surface?.release() imageReader?.close() + imageReader = null + // suface needs to be release after imageReader.close to imageReader access released surface + // https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629 + surface?.release() videoEncoder?.let { it.signalEndOfInputStream() it.stop() @@ -418,9 +408,6 @@ class MainService : Service() { // release audio audioRecordStat = false - audioRecorder?.release() - audioRecorder = null - minBufferSize = 0 } fun destroy() { @@ -428,8 +415,6 @@ class MainService : Service() { _isReady = false stopCapture() - imageReader?.close() - imageReader = null mediaProjection = null checkMediaPermission() @@ -537,9 +522,13 @@ class MainService : Service() { thread { while (audioRecordStat) { audioReader!!.readSync(audioRecorder!!)?.let { - onAudioFrameUpdate(it) + FFI.onAudioFrameUpdate(it) } } + // let's release here rather than onDestroy to avoid threading issue + audioRecorder?.release() + audioRecorder = null + minBufferSize = 0 Log.d(logTag, "Exit audio thread") } } catch (e: Exception) { diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt new file mode 100644 index 000000000..ba29021b3 --- /dev/null +++ b/flutter/android/app/src/main/kotlin/ffi.kt @@ -0,0 +1,21 @@ +// ffi.kt + +package ffi + +import android.content.Context +import java.nio.ByteBuffer + +object FFI { + init { + System.loadLibrary("rustdesk") + } + + external fun init(ctx: Context) + external fun startServer(app_dir: String, custom_client_config: String) + external fun startService() + external fun onVideoFrameUpdate(buf: ByteBuffer) + external fun onAudioFrameUpdate(buf: ByteBuffer) + external fun translateLocale(localeName: String, input: String): String + external fun refreshScreen() + external fun setFrameRawEnable(name: String, value: Boolean) +} \ No newline at end of file diff --git a/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png deleted file mode 100644 index 60e95748c..000000000 Binary files a/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png and /dev/null differ diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index fc5b537e1..3106aaaaf 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3106,6 +3106,27 @@ Color? disabledTextColor(BuildContext context, bool enabled) { : Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6); } +Widget loadPowered(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + launchUrl(Uri.parse('https://rustdesk.com')); + }, + child: Opacity( + opacity: 0.5, + child: Text( + translate("powered_by_me"), + overflow: TextOverflow.clip, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(fontSize: 9, decoration: TextDecoration.underline), + )), + ), + ).marginOnly(top: 6); +} + // max 300 x 60 Widget loadLogo() { return FutureBuilder( @@ -3155,3 +3176,41 @@ bool isInHomePage() { final controller = Get.find(); return controller.state.value.selected == 0; } + +Widget buildPresetPasswordWarning() { + return FutureBuilder( + future: bind.isPresetPassword(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete + } else if (snapshot.hasError) { + return Text( + 'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error + } else if (snapshot.hasData && snapshot.data == true) { + return Container( + color: Colors.yellow, + child: Column( + children: [ + Align( + child: Text( + translate("Security Alert"), + style: TextStyle( + color: Colors.red, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + )).paddingOnly(bottom: 8), + Text( + translate("preset_password_warning"), + style: TextStyle(color: Colors.red), + ) + ], + ).paddingAll(8), + ); // Show a warning message if the Future completed with true + } else { + return SizedBox + .shrink(); // Show nothing if the Future completed with false or null + } + }, + ); +} diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 8840fa474..7011e722e 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -137,11 +137,13 @@ class _PeerTabPageState extends State Widget _createSwitchBar(BuildContext context) { final model = Provider.of(context); - - return ListView( + var counter = -1; + return ReorderableListView( + buildDefaultDragHandles: false, + onReorder: model.reorder, scrollDirection: Axis.horizontal, physics: NeverScrollableScrollPhysics(), - children: model.visibleIndexs.map((t) { + children: model.visibleEnabledOrderedIndexs.map((t) { final selected = model.currentTab == t; final color = selected ? MyTheme.tabbar(context).selectedTextColor @@ -155,43 +157,47 @@ class _PeerTabPageState extends State border: Border( bottom: BorderSide(width: 2, color: color!), )); - return Obx(() => Tooltip( - preferBelow: false, - message: model.tabTooltip(t), - onTriggered: isMobile ? mobileShowTabVisibilityMenu : null, - child: InkWell( - child: Container( - decoration: (hover.value - ? (selected ? decoBorder : deco) - : (selected ? decoBorder : null)), - child: Icon(model.tabIcon(t), color: color) - .paddingSymmetric(horizontal: 4), - ).paddingSymmetric(horizontal: 4), - onTap: () async { - await handleTabSelection(t); - await bind.setLocalFlutterOption( - k: 'peer-tab-index', v: t.toString()); - }, - onHover: (value) => hover.value = value, - ), - )); + counter += 1; + return ReorderableDragStartListener( + key: ValueKey(t), + index: counter, + child: Obx(() => Tooltip( + preferBelow: false, + message: model.tabTooltip(t), + onTriggered: isMobile ? mobileShowTabVisibilityMenu : null, + child: InkWell( + child: Container( + decoration: (hover.value + ? (selected ? decoBorder : deco) + : (selected ? decoBorder : null)), + child: Icon(model.tabIcon(t), color: color) + .paddingSymmetric(horizontal: 4), + ).paddingSymmetric(horizontal: 4), + onTap: () async { + await handleTabSelection(t); + await bind.setLocalFlutterOption( + k: PeerTabModel.kPeerTabIndex, v: t.toString()); + }, + onHover: (value) => hover.value = value, + ), + ))); }).toList()); } Widget _createPeersView() { final model = Provider.of(context); Widget child; - if (model.visibleIndexs.isEmpty) { + if (model.visibleEnabledOrderedIndexs.isEmpty) { child = visibleContextMenuListener(Row( children: [Expanded(child: InkWell())], )); } else { - if (model.visibleIndexs.contains(model.currentTab)) { + if (model.visibleEnabledOrderedIndexs.contains(model.currentTab)) { child = entries[model.currentTab].widget; } else { debugPrint("should not happen! currentTab not in visibleIndexs"); Future.delayed(Duration.zero, () { - model.setCurrentTab(model.indexs[0]); + model.setCurrentTab(model.visibleEnabledOrderedIndexs[0]); }); child = entries[0].widget; } @@ -255,16 +261,17 @@ class _PeerTabPageState extends State void mobileShowTabVisibilityMenu() { final model = gFFI.peerTabModel; final items = List.empty(growable: true); - for (int i = 0; i < model.tabNames.length; i++) { + for (int i = 0; i < PeerTabModel.maxTabCount; i++) { + if (!model.isEnabled[i]) continue; items.add(PopupMenuItem( height: kMinInteractiveDimension * 0.8, - onTap: () => model.setTabVisible(i, !model.isVisible[i]), + onTap: () => model.setTabVisible(i, !model.isVisibleEnabled[i]), child: Row( children: [ Checkbox( - value: model.isVisible[i], + value: model.isVisibleEnabled[i], onChanged: (_) { - model.setTabVisible(i, !model.isVisible[i]); + model.setTabVisible(i, !model.isVisibleEnabled[i]); if (Navigator.canPop(context)) { Navigator.pop(context); } @@ -314,16 +321,17 @@ class _PeerTabPageState extends State Widget visibleContextMenu(CancelFunc cancelFunc) { final model = Provider.of(context); - final menu = List.empty(growable: true); - for (int i = 0; i < model.tabNames.length; i++) { - menu.add(MenuEntrySwitch( + final menu = List.empty(growable: true); + for (int i = 0; i < model.orders.length; i++) { + int tabIndex = model.orders[i]; + if (tabIndex < 0 || tabIndex >= PeerTabModel.maxTabCount) continue; + if (!model.isEnabled[tabIndex]) continue; + menu.add(MenuEntrySwitchSync( switchType: SwitchType.scheckbox, - text: model.tabTooltip(i), - getter: () async { - return model.isVisible[i]; - }, + text: model.tabTooltip(tabIndex), + currentValue: model.isVisibleEnabled[tabIndex], setter: (show) async { - model.setTabVisible(i, show); + model.setTabVisible(tabIndex, show); cancelFunc(); })); } @@ -434,7 +442,7 @@ class _PeerTabPageState extends State model.setMultiSelectionMode(false); showToast(translate('Successful')); }, - child: Icon(model.icons[PeerTabIndex.fav.index]), + child: Icon(PeerTabModel.icons[PeerTabIndex.fav.index]), ).marginOnly(left: isMobile ? 11 : 6), ); } @@ -455,7 +463,7 @@ class _PeerTabPageState extends State addPeersToAbDialog(peers); model.setMultiSelectionMode(false); }, - child: Icon(model.icons[PeerTabIndex.ab.index]), + child: Icon(PeerTabModel.icons[PeerTabIndex.ab.index]), ).marginOnly(left: isMobile ? 11 : 6), ); } @@ -563,7 +571,7 @@ class _PeerTabPageState extends State final screenWidth = MediaQuery.of(context).size.width; final leftIconSize = Theme.of(context).iconTheme.size ?? 24; final leftActionsSize = - (leftIconSize + (4 + 4) * 2) * model.visibleIndexs.length; + (leftIconSize + (4 + 4) * 2) * model.visibleEnabledOrderedIndexs.length; final availableWidth = screenWidth - 10 * 2 - leftActionsSize - 2 * 2; final searchWidth = 120; final otherActionWidth = 18 + 10; diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index a8638a302..e38e3fb75 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -441,10 +441,17 @@ Future> toolbarDisplayToggle( child: Text(translate('Mute')))); } // file copy and paste + // If the version is less than 1.2.4, file copy and paste is supported on Windows only. + final isSupportIfPeer_1_2_3 = versionCmp(pi.version, '1.2.4') < 0 && + isWindows && + pi.platform == kPeerPlatformWindows; + // If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set. + final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 && + bind.mainHasFileClipboard() && + pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard); if (ffiModel.keyboard && perms['file'] != false && - bind.mainHasFileClipboard() && - pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) { + (isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) { final enabled = !ffiModel.viewOnly; final option = 'enable-file-transfer'; final value = diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 0e4a39d0a..090ca62a4 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -19,7 +19,9 @@ const kKeyTranslateMode = 'translate'; const String kPlatformAdditionsIsWayland = "is_wayland"; const String kPlatformAdditionsHeadless = "headless"; const String kPlatformAdditionsIsInstalled = "is_installed"; -const String kPlatformAdditionsVirtualDisplays = "virtual_displays"; +const String kPlatformAdditionsIddImpl = "idd_impl"; +const String kPlatformAdditionsRustDeskVirtualDisplays = "rustdesk_virtual_displays"; +const String kPlatformAdditionsAmyuniVirtualDisplays = "amyuni_virtual_displays"; const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard"; const String kPlatformAdditionsSupportedPrivacyModeImpl = "supported_privacy_mode_impl"; @@ -121,12 +123,11 @@ double kNewWindowOffset = isWindows ? 30.0 : 50.0; -EdgeInsets get kDragToResizeAreaPadding => - !kUseCompatibleUiMode && isLinux - ? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value - ? EdgeInsets.zero - : EdgeInsets.all(5.0) - : EdgeInsets.zero; +EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux + ? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value + ? EdgeInsets.zero + : EdgeInsets.all(5.0) + : EdgeInsets.zero; // https://en.wikipedia.org/wiki/Non-breaking_space const int $nbsp = 0x00A0; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index b56bc0c75..dd87e0939 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -69,44 +69,6 @@ class _DesktopHomePageState extends State ); } - Widget buildPresetPasswordWarning() { - return FutureBuilder( - future: bind.isPresetPassword(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete - } else if (snapshot.hasError) { - return Text( - 'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error - } else if (snapshot.hasData && snapshot.data == true) { - return Container( - color: Colors.yellow, - child: Column( - children: [ - Align( - child: Text( - translate("Security Alert"), - style: TextStyle( - color: Colors.red, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - )).paddingOnly(bottom: 8), - Text( - translate("preset_password_warning"), - style: TextStyle(color: Colors.red), - ) - ], - ).paddingAll(8), - ); // Show a warning message if the Future completed with true - } else { - return SizedBox - .shrink(); // Show nothing if the Future completed with false or null - } - }, - ); - } - Widget buildLeftPane(BuildContext context) { final isIncomingOnly = bind.isIncomingOnly(); final isOutgoingOnly = bind.isOutgoingOnly(); @@ -115,22 +77,7 @@ class _DesktopHomePageState extends State if (bind.isCustomClient()) Align( alignment: Alignment.center, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - launchUrl(Uri.parse('https://rustdesk.com')); - }, - child: Opacity( - opacity: 0.5, - child: Text( - translate("powered_by_me"), - overflow: TextOverflow.clip, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 9, decoration: TextDecoration.underline), - )), - ), - ).marginOnly(top: 6), + child: loadPowered(context), ), Align( alignment: Alignment.center, @@ -298,19 +245,20 @@ class _DesktopHomePageState extends State RxBool hover = false.obs; return InkWell( onTap: DesktopTabPage.onAddSetting, - child: Obx( - () => CircleAvatar( - radius: 15, - backgroundColor: hover.value - ? Theme.of(context).scaffoldBackgroundColor - : Theme.of(context).colorScheme.background, - child: Tooltip( - message: translate('Settings'), - child: Icon( - Icons.more_vert_outlined, - size: 20, - color: hover.value ? textColor : textColor?.withOpacity(0.5), - )), + child: Tooltip( + message: translate('Settings'), + child: Obx( + () => CircleAvatar( + radius: 15, + backgroundColor: hover.value + ? Theme.of(context).scaffoldBackgroundColor + : Theme.of(context).colorScheme.background, + child: Icon( + Icons.more_vert_outlined, + size: 20, + color: hover.value ? textColor : textColor?.withOpacity(0.5), + ), + ), ), ), onHover: (value) => hover.value = value, @@ -371,31 +319,33 @@ class _DesktopHomePageState extends State ), AnimatedRotationWidget( onPressed: () => bind.mainUpdateTemporaryPassword(), - child: Obx(() => RotatedBox( - quarterTurns: 2, - child: Tooltip( - message: translate('Refresh Password'), - child: Icon( - Icons.refresh, - color: refreshHover.value - ? textColor - : Color(0xFFDDDDDD), - size: 22, - )))), + child: Tooltip( + message: translate('Refresh Password'), + child: Obx(() => RotatedBox( + quarterTurns: 2, + child: Icon( + Icons.refresh, + color: refreshHover.value + ? textColor + : Color(0xFFDDDDDD), + size: 22, + ))), + ), onHover: (value) => refreshHover.value = value, ).marginOnly(right: 8, top: 4), if (!bind.isDisableSettings()) InkWell( - child: Obx( - () => Tooltip( - message: translate('Change Password'), - child: Icon( - Icons.edit, - color: editHover.value - ? textColor - : Color(0xFFDDDDDD), - size: 22, - )).marginOnly(right: 8, top: 4), + child: Tooltip( + message: translate('Change Password'), + child: Obx( + () => Icon( + Icons.edit, + color: editHover.value + ? textColor + : Color(0xFFDDDDDD), + size: 22, + ).marginOnly(right: 8, top: 4), + ), ), onTap: () => DesktopSettingPage.switch2page(0), onHover: (value) => editHover.value = value, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index cd1af3922..17998dd9a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -400,11 +400,20 @@ class _GeneralState extends State<_General> { Widget hwcodec() { final hwcodec = bind.mainHasHwcodec(); - final gpucodec = bind.mainHasGpucodec(); + final vram = bind.mainHasVram(); return Offstage( - offstage: !(hwcodec || gpucodec), + offstage: !(hwcodec || vram), child: _Card(title: 'Hardware Codec', children: [ - _OptionCheckBox(context, 'Enable hardware codec', 'enable-hwcodec') + _OptionCheckBox( + context, + 'Enable hardware codec', + 'enable-hwcodec', + update: () { + if (mainGetBoolOptionSync('enable-hwcodec')) { + bind.mainCheckHwcodec(); + } + }, + ) ]), ); } @@ -835,6 +844,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ...directIp(context), whitelist(), ...autoDisconnect(context), + if (bind.mainIsInstalled()) + _OptionCheckBox(context, 'allow-only-conn-window-open-tip', + 'allow-only-conn-window-open', + reverse: false, enabled: enabled), ]); } diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 9833dcbca..086d7a622 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -568,6 +568,47 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { } } +// Compatible with MenuEntrySwitch, it uses value instead of getter +class MenuEntrySwitchSync extends MenuEntrySwitchBase { + final SwitchSetter setter; + final RxBool _curOption = false.obs; + + MenuEntrySwitchSync({ + required SwitchType switchType, + required String text, + required bool currentValue, + required this.setter, + Rx? textStyle, + EdgeInsets? padding, + dismissOnClicked = false, + RxBool? enabled, + dismissCallback, + }) : super( + switchType: switchType, + text: text, + textStyle: textStyle, + padding: padding, + dismissOnClicked: dismissOnClicked, + enabled: enabled, + dismissCallback: dismissCallback, + ) { + _curOption.value = currentValue; + } + + @override + RxBool get curOption => _curOption; + @override + setOption(bool? option) async { + if (option != null) { + await setter(option); + // Notice: no ensure with getter, best used on menus that are destroyed on click + if (_curOption.value != option) { + _curOption.value = option; + } + } + } +} + typedef Switch2Getter = RxBool Function(); typedef Switch2Setter = Future Function(bool); diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 2ac91d95f..fd048337c 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -636,7 +636,7 @@ class _MonitorMenu extends StatelessWidget { menuStyle: MenuStyle( padding: MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))), - menuChildren: [buildMonitorSubmenuWidget()]); + menuChildrenGetter: () => [buildMonitorSubmenuWidget()]); } Widget buildMultiMonitorMenu() { @@ -843,17 +843,17 @@ class _ControlMenu extends StatelessWidget { color: _ToolbarTheme.blueColor, hoverColor: _ToolbarTheme.hoverBlueColor, ffi: ffi, - menuChildren: toolbarControls(context, id, ffi).map((e) { - if (e.divider) { - return Divider(); - } else { - return MenuButton( - child: e.child, - onPressed: e.onPressed, - ffi: ffi, - trailingIcon: e.trailingIcon); - } - }).toList()); + menuChildrenGetter: () => toolbarControls(context, id, ffi).map((e) { + if (e.divider) { + return Divider(); + } else { + return MenuButton( + child: e.child, + onPressed: e.onPressed, + ffi: ffi, + trailingIcon: e.trailingIcon); + } + }).toList()); } } @@ -1046,53 +1046,61 @@ class _DisplayMenuState extends State<_DisplayMenu> { Widget build(BuildContext context) { _screenAdjustor.updateScreen(); - final menuChildren = [ - _screenAdjustor.adjustWindow(context), - viewStyle(), - scrollStyle(), - imageQuality(), - codec(), - _ResolutionsMenu( - id: widget.id, - ffi: widget.ffi, - screenAdjustor: _screenAdjustor, - ), - // We may add this feature if it is needed and we have an EV certificate. - // _VirtualDisplayMenu( - // id: widget.id, - // ffi: widget.ffi, - // ), - Divider(), - toggles(), - ]; - // privacy mode - if (ffiModel.keyboard && pi.features.privacyMode) { - final privacyModeState = PrivacyModeState.find(id); - final privacyModeList = - toolbarPrivacyMode(privacyModeState, context, id, ffi); - if (privacyModeList.length == 1) { - menuChildren.add(CkbMenuButton( - value: privacyModeList[0].value, - onChanged: privacyModeList[0].onChanged, - child: privacyModeList[0].child, - ffi: ffi)); - } else if (privacyModeList.length > 1) { - menuChildren.addAll([ - Divider(), - _SubmenuButton( - ffi: widget.ffi, - child: Text(translate('Privacy mode')), - menuChildren: privacyModeList - .map((e) => CkbMenuButton( - value: e.value, - onChanged: e.onChanged, - child: e.child, - ffi: ffi)) - .toList()), - ]); + menuChildrenGetter() { + final menuChildren = [ + _screenAdjustor.adjustWindow(context), + viewStyle(), + scrollStyle(), + imageQuality(), + codec(), + _ResolutionsMenu( + id: widget.id, + ffi: widget.ffi, + screenAdjustor: _screenAdjustor, + ), + if (pi.isRustDeskIdd) + _RustDeskVirtualDisplayMenu( + id: widget.id, + ffi: widget.ffi, + ), + if (pi.isAmyuniIdd) + _AmyuniVirtualDisplayMenu( + id: widget.id, + ffi: widget.ffi, + ), + Divider(), + toggles(), + ]; + // privacy mode + if (ffiModel.keyboard && pi.features.privacyMode) { + final privacyModeState = PrivacyModeState.find(id); + final privacyModeList = + toolbarPrivacyMode(privacyModeState, context, id, ffi); + if (privacyModeList.length == 1) { + menuChildren.add(CkbMenuButton( + value: privacyModeList[0].value, + onChanged: privacyModeList[0].onChanged, + child: privacyModeList[0].child, + ffi: ffi)); + } else if (privacyModeList.length > 1) { + menuChildren.addAll([ + Divider(), + _SubmenuButton( + ffi: widget.ffi, + child: Text(translate('Privacy mode')), + menuChildren: privacyModeList + .map((e) => CkbMenuButton( + value: e.value, + onChanged: e.onChanged, + child: e.child, + ffi: ffi)) + .toList()), + ]); + } } + menuChildren.add(widget.pluginItem); + return menuChildren; } - menuChildren.add(widget.pluginItem); return _IconSubmenuButton( tooltip: 'Display Settings', @@ -1100,7 +1108,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { ffi: widget.ffi, color: _ToolbarTheme.blueColor, hoverColor: _ToolbarTheme.hoverBlueColor, - menuChildren: menuChildren, + menuChildrenGetter: menuChildrenGetter, ); } @@ -1537,21 +1545,23 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { } } -class _VirtualDisplayMenu extends StatefulWidget { +class _RustDeskVirtualDisplayMenu extends StatefulWidget { final String id; final FFI ffi; - _VirtualDisplayMenu({ + _RustDeskVirtualDisplayMenu({ Key? key, required this.id, required this.ffi, }) : super(key: key); @override - State<_VirtualDisplayMenu> createState() => _VirtualDisplayMenuState(); + State<_RustDeskVirtualDisplayMenu> createState() => + _RustDeskVirtualDisplayMenuState(); } -class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> { +class _RustDeskVirtualDisplayMenuState + extends State<_RustDeskVirtualDisplayMenu> { @override void initState() { super.initState(); @@ -1566,7 +1576,7 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> { return Offstage(); } - final virtualDisplays = widget.ffi.ffiModel.pi.virtualDisplays; + final virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays; final privacyModeState = PrivacyModeState.find(widget.id); final children = []; @@ -1608,6 +1618,82 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> { } } +class _AmyuniVirtualDisplayMenu extends StatefulWidget { + final String id; + final FFI ffi; + + _AmyuniVirtualDisplayMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + State<_AmyuniVirtualDisplayMenu> createState() => + _AmiyuniVirtualDisplayMenuState(); +} + +class _AmiyuniVirtualDisplayMenuState extends State<_AmyuniVirtualDisplayMenu> { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) { + return Offstage(); + } + if (!widget.ffi.ffiModel.pi.isInstalled) { + return Offstage(); + } + + final count = widget.ffi.ffiModel.pi.amyuniVirtualDisplayCount; + final privacyModeState = PrivacyModeState.find(widget.id); + + final children = [ + Obx(() => Row( + children: [ + TextButton( + onPressed: privacyModeState.isNotEmpty || count == 0 + ? null + : () => bind.sessionToggleVirtualDisplay( + sessionId: widget.ffi.sessionId, index: 0, on: false), + child: Icon(Icons.remove), + ), + Text(count.toString()), + TextButton( + onPressed: privacyModeState.isNotEmpty || count == 4 + ? null + : () => bind.sessionToggleVirtualDisplay( + sessionId: widget.ffi.sessionId, index: 0, on: true), + child: Icon(Icons.add), + ), + ], + )), + Divider(), + Obx(() => MenuButton( + onPressed: privacyModeState.isNotEmpty || count == 0 + ? null + : () { + bind.sessionToggleVirtualDisplay( + sessionId: widget.ffi.sessionId, + index: kAllVirtualDisplay, + on: false); + }, + ffi: widget.ffi, + child: Text(translate('Plug out all')), + )), + ]; + + return _SubmenuButton( + ffi: widget.ffi, + menuChildren: children, + child: Text(translate("Virtual display")), + ); + } +} + class _KeyboardMenu extends StatelessWidget { final String id; final FFI ffi; @@ -1623,19 +1709,7 @@ class _KeyboardMenu extends StatelessWidget { Widget build(BuildContext context) { var ffiModel = Provider.of(context); if (!ffiModel.keyboard) return Offstage(); - // If use flutter to grab keys, we can only use one mode. - // Map mode and Legacy mode, at least one of them is supported. - String? modeOnly; - if (isInputSourceFlutter) { - if (bind.sessionIsKeyboardModeSupported( - sessionId: ffi.sessionId, mode: kKeyMapMode)) { - modeOnly = kKeyMapMode; - } else if (bind.sessionIsKeyboardModeSupported( - sessionId: ffi.sessionId, mode: kKeyLegacyMode)) { - modeOnly = kKeyLegacyMode; - } - } - final toolbarToggles = toolbarKeyboardToggles(ffi) + toolbarToggles() => toolbarKeyboardToggles(ffi) .map((e) => CkbMenuButton( value: e.value, onChanged: e.onChanged, child: e.child, ffi: ffi)) .toList(); @@ -1645,18 +1719,18 @@ class _KeyboardMenu extends StatelessWidget { ffi: ffi, color: _ToolbarTheme.blueColor, hoverColor: _ToolbarTheme.hoverBlueColor, - menuChildren: [ - keyboardMode(modeOnly), - localKeyboardType(), - inputSource(), - Divider(), - viewMode(), - Divider(), - ...toolbarToggles, - ]); + menuChildrenGetter: () => [ + keyboardMode(), + localKeyboardType(), + inputSource(), + Divider(), + viewMode(), + Divider(), + ...toolbarToggles(), + ]); } - keyboardMode(String? modeOnly) { + keyboardMode() { return futureBuilder(future: () async { return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ?? kKeyLegacyMode; @@ -1676,6 +1750,19 @@ class _KeyboardMenu extends StatelessWidget { await ffi.inputModel.updateKeyboardMode(); } + // If use flutter to grab keys, we can only use one mode. + // Map mode and Legacy mode, at least one of them is supported. + String? modeOnly; + if (isInputSourceFlutter) { + if (bind.sessionIsKeyboardModeSupported( + sessionId: ffi.sessionId, mode: kKeyMapMode)) { + modeOnly = kKeyMapMode; + } else if (bind.sessionIsKeyboardModeSupported( + sessionId: ffi.sessionId, mode: kKeyLegacyMode)) { + modeOnly = kKeyLegacyMode; + } + } + for (InputModeMenu mode in modes) { if (modeOnly != null && mode.key != modeOnly) { continue; @@ -1804,7 +1891,7 @@ class _ChatMenuState extends State<_ChatMenu> { ffi: widget.ffi, color: _ToolbarTheme.blueColor, hoverColor: _ToolbarTheme.hoverBlueColor, - menuChildren: [textChat(), voiceCall()]); + menuChildrenGetter: () => [textChat(), voiceCall()]); } textChat() { @@ -2008,7 +2095,7 @@ class _IconSubmenuButton extends StatefulWidget { final Widget? icon; final Color color; final Color hoverColor; - final List menuChildren; + final List Function() menuChildrenGetter; final MenuStyle? menuStyle; final FFI ffi; final double? width; @@ -2020,7 +2107,7 @@ class _IconSubmenuButton extends StatefulWidget { required this.tooltip, required this.color, required this.hoverColor, - required this.menuChildren, + required this.menuChildrenGetter, required this.ffi, this.menuStyle, this.width, @@ -2064,7 +2151,8 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> { color: hover ? widget.hoverColor : widget.color, ), child: icon))), - menuChildren: widget.menuChildren + menuChildren: widget + .menuChildrenGetter() .map((e) => _buildPointerTrackWidget(e, widget.ffi)) .toList())); return MenuBar(children: [ diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 76777b19f..fb13efbef 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -865,6 +865,7 @@ class _ListView extends StatelessWidget { label: labelGetter == null ? Rx(tab.label) : labelGetter!(tab.label), + tabType: controller.tabType, selectedIcon: tab.selectedIcon, unselectedIcon: tab.unselectedIcon, closable: tab.closable, @@ -896,6 +897,7 @@ class _Tab extends StatefulWidget { final int index; final String tabInfoKey; final Rx label; + final DesktopTabType tabType; final IconData? selectedIcon; final IconData? unselectedIcon; final bool closable; @@ -914,6 +916,7 @@ class _Tab extends StatefulWidget { required this.index, required this.tabInfoKey, required this.label, + required this.tabType, this.selectedIcon, this.unselectedIcon, this.tabBuilder, @@ -953,7 +956,9 @@ class _TabState extends State<_Tab> with RestorationMixin { return ConstrainedBox( constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200), child: Tooltip( - message: translate(widget.label.value), + message: widget.tabType == DesktopTabType.main + ? '' + : translate(widget.label.value), child: Text( translate(widget.label.value), textAlign: TextAlign.center, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index d2a3c0347..51612c9e7 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -84,7 +84,7 @@ class _ConnectionPageState extends State { slivers: [ SliverList( delegate: SliverChildListDelegate([ - _buildUpdateUI(), + if (!bind.isCustomClient()) _buildUpdateUI(), _buildRemoteIDTextField(), ])), SliverFillRemaining( diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index f19541c8c..078e2b2f7 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_hbb/mobile/pages/settings_page.dart'; import 'package:get/get.dart'; import '../../common.dart'; import '../../common/widgets/chat_page.dart'; +import '../../models/platform_model.dart'; import 'connection_page.dart'; abstract class PageShape extends Widget { @@ -25,8 +26,9 @@ class HomePageState extends State { var _selectedIndex = 0; int get selectedIndex => _selectedIndex; final List _pages = []; + int _chatPageTabIndex = -1; bool get isChatPageCurrentTab => isAndroid - ? _selectedIndex == 1 + ? _selectedIndex == _chatPageTabIndex : false; // change this when ios have chat page void refreshPages() { @@ -43,8 +45,9 @@ class HomePageState extends State { void initPages() { _pages.clear(); - _pages.add(ConnectionPage()); - if (isAndroid) { + if (!bind.isIncomingOnly()) _pages.add(ConnectionPage()); + if (isAndroid && !bind.isOutgoingOnly()) { + _chatPageTabIndex = _pages.length; _pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]); } _pages.add(SettingsPage()); @@ -141,7 +144,7 @@ class HomePageState extends State { ], ); } - return Text("RustDesk"); + return Text(bind.mainGetAppNameSync()); } } @@ -154,7 +157,7 @@ class WebHomePage extends StatelessWidget { // backgroundColor: MyTheme.grayBg, appBar: AppBar( centerTitle: true, - title: Text("RustDesk${isWeb ? " (Beta) " : ""}"), + title: Text(bind.mainGetAppNameSync()), actions: connectionPage.appBarActions, ), body: connectionPage, diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index f17efb828..8303c91b1 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -68,7 +68,9 @@ class _RemotePageState extends State { gFFI.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); }); - WakelockPlus.enable(); + if (!isWeb) { + WakelockPlus.enable(); + } _physicalFocusNode.requestFocus(); gFFI.inputModel.listenToMouse(true); gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId); @@ -95,7 +97,9 @@ class _RemotePageState extends State { gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); - await WakelockPlus.disable(); + if (!isWeb) { + await WakelockPlus.disable(); + } await keyboardSubscription.cancel(); removeSharedStates(widget.id); } diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index e389d795c..a7bf12148 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -158,6 +158,7 @@ class _ServerPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ + buildPresetPasswordWarning(), gFFI.serverModel.isStart ? ServerInfo() : ServiceNotRunningNotification(), @@ -226,7 +227,7 @@ class ScamWarningDialog extends StatefulWidget { } class ScamWarningDialogState extends State { - int _countdown = 12; + int _countdown = bind.isCustomClient() ? 0 : 12; bool show_warning = false; late Timer _timer; late ServerModel _serverModel; @@ -383,7 +384,7 @@ class ScamWarningDialogState extends State { Navigator.of(context).pop(); }, style: ElevatedButton.styleFrom( - primary: Colors.blueAccent, + backgroundColor: Colors.blueAccent, ), child: Text( translate("Decline"), diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index f6b217533..9477cbe6a 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -27,7 +27,7 @@ class SettingsPage extends StatefulWidget implements PageShape { final icon = Icon(Icons.settings); @override - final appBarActions = [ScanButton()]; + final appBarActions = bind.isDisableSettings() ? [] : [ScanButton()]; @override State createState() => _SettingsState(); @@ -218,6 +218,21 @@ class _SettingsState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { Provider.of(context); + final outgoingOnly = bind.isOutgoingOnly(); + final customClientSection = CustomSettingsSection( + child: Column( + children: [ + if (bind.isCustomClient()) + Align( + alignment: Alignment.center, + child: loadPowered(context), + ), + Align( + alignment: Alignment.center, + child: loadLogo(), + ) + ], + )); final List enhancementsTiles = []; final List shareScreenTiles = [ SettingsTile.switchTile( @@ -448,33 +463,37 @@ class _SettingsState extends State with WidgetsBindingObserver { gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue); })); - return SettingsList( + final disabledSettings = bind.isDisableSettings(); + final settings = SettingsList( sections: [ - SettingsSection( - title: Text(translate('Account')), - tiles: [ - SettingsTile( - title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty - ? translate('Login') - : '${translate('Logout')} (${gFFI.userModel.userName.value})')), - leading: Icon(Icons.person), - onPressed: (context) { - if (gFFI.userModel.userName.value.isEmpty) { - loginDialog(); - } else { - logOutConfirmDialog(); - } - }, - ), - ], - ), + customClientSection, + if (!bind.isDisableAccount()) + SettingsSection( + title: Text(translate('Account')), + tiles: [ + SettingsTile( + title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty + ? translate('Login') + : '${translate('Logout')} (${gFFI.userModel.userName.value})')), + leading: Icon(Icons.person), + onPressed: (context) { + if (gFFI.userModel.userName.value.isEmpty) { + loginDialog(); + } else { + logOutConfirmDialog(); + } + }, + ), + ], + ), SettingsSection(title: Text(translate("Settings")), tiles: [ - SettingsTile( - title: Text(translate('ID/Relay Server')), - leading: Icon(Icons.cloud), - onPressed: (context) { - showServerSettings(gFFI.dialogManager); - }), + if (!disabledSettings) + SettingsTile( + title: Text(translate('ID/Relay Server')), + leading: Icon(Icons.cloud), + onPressed: (context) { + showServerSettings(gFFI.dialogManager); + }), SettingsTile( title: Text(translate('Language')), leading: Icon(Icons.translate), @@ -494,7 +513,7 @@ class _SettingsState extends State with WidgetsBindingObserver { }, ) ]), - if (isAndroid) + if (isAndroid && !outgoingOnly) SettingsSection( title: Text(translate("Recording")), tiles: [ @@ -523,13 +542,13 @@ class _SettingsState extends State with WidgetsBindingObserver { ), ], ), - if (isAndroid) + if (isAndroid && !disabledSettings && !outgoingOnly) SettingsSection( title: Text(translate("Share Screen")), tiles: shareScreenTiles, ), - defaultDisplaySection(), - if (isAndroid) + if (!bind.isIncomingOnly()) defaultDisplaySection(), + if (isAndroid && !disabledSettings && !outgoingOnly) SettingsSection( title: Text(translate("Enhancements")), tiles: enhancementsTiles, @@ -578,6 +597,7 @@ class _SettingsState extends State with WidgetsBindingObserver { ), ], ); + return settings; } Future canStartOnBoot() async { diff --git a/flutter/lib/models/desktop_render_texture.dart b/flutter/lib/models/desktop_render_texture.dart index 2e3c0ead6..80477fede 100644 --- a/flutter/lib/models/desktop_render_texture.dart +++ b/flutter/lib/models/desktop_render_texture.dart @@ -10,7 +10,7 @@ import './platform_model.dart'; import 'package:texture_rgba_renderer/texture_rgba_renderer.dart' if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart'; -// Feature flutter_texture_render need to be enabled if feature gpucodec is enabled. +// Feature flutter_texture_render need to be enabled if feature vram is enabled. final useTextureRender = !isWeb && (bind.mainHasPixelbufferTextureRender() || bind.mainHasGpuTextureRender()); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 0a58aa023..4da7d54cd 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -561,8 +561,12 @@ class FfiModel with ChangeNotifier { showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId); } else if (text == 'Connected, waiting for image...') { showConnectedWaitingForImage(dialogManager, sessionId, type, title, text); + } else if (title == 'Privacy mode') { + final hasRetry = evt['hasRetry'] == 'true'; + showPrivacyFailedDialog( + sessionId, type, title, text, link, hasRetry, dialogManager); } else { - var hasRetry = evt['hasRetry'] == 'true'; + final hasRetry = evt['hasRetry'] == 'true'; showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager); } } @@ -657,6 +661,27 @@ class FfiModel with ChangeNotifier { bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId); } + void showPrivacyFailedDialog( + SessionID sessionId, + String type, + String title, + String text, + String link, + bool hasRetry, + OverlayDialogManager dialogManager) { + if (text == 'no_need_privacy_mode_no_physical_displays_tip' || + text == 'Enter privacy mode') { + // There are display changes on the remote side, + // which will cause some messages to refresh the canvas and dismiss dialogs. + // So we add a delay here to ensure the dialog is displayed. + Future.delayed(Duration(milliseconds: 3000), () { + showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager); + }); + } else { + showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager); + } + } + _updateSessionWidthHeight(SessionID sessionId) { if (_rect == null) return; if (_rect!.width <= 0 || _rect!.height <= 0) { @@ -690,6 +715,8 @@ class FfiModel with ChangeNotifier { // Map clone is required here, otherwise "evt" may be changed by other threads through the reference. // Because this function is asynchronous, there's an "await" in this function. cachedPeerData.peerInfo = {...evt}; + // Do not cache resolutions, because a new display connection have different resolutions. + cachedPeerData.peerInfo.remove('resolutions'); // Recent peer is updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) bind.mainLoadRecentPeers(); @@ -745,7 +772,9 @@ class FfiModel with ChangeNotifier { } Map features = json.decode(evt['features']); _pi.features.privacyMode = features['privacy_mode'] == 1; - handleResolutions(peerId, evt["resolutions"]); + if (!isCache) { + handleResolutions(peerId, evt["resolutions"]); + } parent.target?.elevationModel.onPeerInfo(_pi); } if (connType == ConnType.defaultConn) { @@ -986,15 +1015,21 @@ class FfiModel with ChangeNotifier { } if (updateData.isEmpty) { - _pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays); + _pi.platformAdditions.remove(kPlatformAdditionsRustDeskVirtualDisplays); + _pi.platformAdditions.remove(kPlatformAdditionsAmyuniVirtualDisplays); } else { try { final updateJson = json.decode(updateData) as Map; for (final key in updateJson.keys) { _pi.platformAdditions[key] = updateJson[key]; } - if (!updateJson.containsKey(kPlatformAdditionsVirtualDisplays)) { - _pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays); + if (!updateJson + .containsKey(kPlatformAdditionsRustDeskVirtualDisplays)) { + _pi.platformAdditions + .remove(kPlatformAdditionsRustDeskVirtualDisplays); + } + if (!updateJson.containsKey(kPlatformAdditionsAmyuniVirtualDisplays)) { + _pi.platformAdditions.remove(kPlatformAdditionsAmyuniVirtualDisplays); } } catch (e) { debugPrint('Failed to decode platformAdditions $e'); @@ -2286,6 +2321,8 @@ class FFI { } await ffiModel.handleCachedPeerData(data, id); await sessionRefreshVideo(sessionId, ffiModel.pi); + await bind.sessionRequestNewDisplayInitMsgs( + sessionId: sessionId, display: ffiModel.pi.currentDisplay); }); isToNewWindowNotified.value = true; } @@ -2490,14 +2527,21 @@ class PeerInfo with ChangeNotifier { bool get isInstalled => platform != kPeerPlatformWindows || platformAdditions[kPlatformAdditionsIsInstalled] == true; - List get virtualDisplays => List.from( - platformAdditions[kPlatformAdditionsVirtualDisplays] ?? []); + List get RustDeskVirtualDisplays => List.from( + platformAdditions[kPlatformAdditionsRustDeskVirtualDisplays] ?? []); + int get amyuniVirtualDisplayCount => + platformAdditions[kPlatformAdditionsAmyuniVirtualDisplays] ?? 0; bool get isSupportMultiDisplay => (isDesktop || isWebDesktop) && isSupportMultiUiSession; bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false; + bool get isRustDeskIdd => + platformAdditions[kPlatformAdditionsIddImpl] == 'rustdesk_idd'; + bool get isAmyuniIdd => + platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd'; + Display? tryGetDisplay() { if (displays.isEmpty) { return null; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index fe8fba732..3f81ba422 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -198,7 +198,10 @@ class PlatformFFI { await _ffiBind.mainDeviceId(id: id); await _ffiBind.mainDeviceName(name: name); await _ffiBind.mainSetHomeDir(home: _homeDir); - await _ffiBind.mainInit(appDir: _dir); + await _ffiBind.mainInit( + appDir: _dir, + customClientConfig: '', + ); } catch (e) { debugPrintStack(label: 'initialize failed: $e'); } diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 43bbfda72..52e43bfea 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -21,24 +21,43 @@ class PeerTabModel with ChangeNotifier { WeakReference parent; int get currentTab => _currentTab; int _currentTab = 0; // index in tabNames - List tabNames = [ + static const int maxTabCount = 5; + static const String kPeerTabIndex = 'peer-tab-index'; + static const String kPeerTabOrder = 'peer-tab-order'; + static const String kPeerTabVisible = 'peer-tab-visible'; + static const List tabNames = [ 'Recent sessions', 'Favorites', - if (!isWeb) 'Discovered', - if (!(bind.isDisableAb() || bind.isDisableAccount())) 'Address book', - if (!bind.isDisableAccount()) 'Group', + 'Discovered', + 'Address book', + 'Group', ]; - final List icons = [ + static const List icons = [ Icons.access_time_filled, Icons.star, - if (!isWeb) Icons.explore, - if (!(bind.isDisableAb() || bind.isDisableAccount())) IconFont.addressBook, - if (!bind.isDisableAccount()) Icons.group, + Icons.explore, + IconFont.addressBook, + Icons.group, ]; - final List _isVisible = List.filled(5, true, growable: false); - List get isVisible => _isVisible; - List get indexs => List.generate(tabNames.length, (index) => index); - List get visibleIndexs => indexs.where((e) => _isVisible[e]).toList(); + List isEnabled = List.from([ + true, + true, + !isWeb, + !(bind.isDisableAb() || bind.isDisableAccount()), + !bind.isDisableAccount(), + ]); + final List _isVisible = List.filled(maxTabCount, true, growable: false); + List get isVisibleEnabled => () { + final list = _isVisible.toList(); + for (int i = 0; i < maxTabCount; i++) { + list[i] = list[i] && isEnabled[i]; + } + return list; + }(); + final List orders = + List.generate(maxTabCount, (index) => index, growable: false); + List get visibleEnabledOrderedIndexs => + orders.where((e) => isVisibleEnabled[e]).toList(); List _selectedPeers = List.empty(growable: true); List get selectedPeers => _selectedPeers; bool _multiSelectionMode = false; @@ -53,7 +72,7 @@ class PeerTabModel with ChangeNotifier { PeerTabModel(this.parent) { // visible try { - final option = bind.getLocalFlutterOption(k: 'peer-tab-visible'); + final option = bind.getLocalFlutterOption(k: kPeerTabVisible); if (option.isNotEmpty) { List decodeList = jsonDecode(option); if (decodeList.length == _isVisible.length) { @@ -67,13 +86,37 @@ class PeerTabModel with ChangeNotifier { } catch (e) { debugPrint("failed to get peer tab visible list:$e"); } + // order + try { + final option = bind.getLocalFlutterOption(k: kPeerTabOrder); + if (option.isNotEmpty) { + List decodeList = jsonDecode(option); + if (decodeList.length == maxTabCount) { + var sortedList = decodeList.toList(); + sortedList.sort(); + bool valid = true; + for (int i = 0; i < maxTabCount; i++) { + if (sortedList[i] is! int || sortedList[i] != i) { + valid = false; + } + } + if (valid) { + for (int i = 0; i < orders.length; i++) { + orders[i] = decodeList[i]; + } + } + } + } + } catch (e) { + debugPrint("failed to get peer tab order list: $e"); + } // init currentTab _currentTab = - int.tryParse(bind.getLocalFlutterOption(k: 'peer-tab-index')) ?? 0; - if (_currentTab < 0 || _currentTab >= tabNames.length) { + int.tryParse(bind.getLocalFlutterOption(k: kPeerTabIndex)) ?? 0; + if (_currentTab < 0 || _currentTab >= maxTabCount) { _currentTab = 0; } - _trySetCurrentTabToFirstVisible(); + _trySetCurrentTabToFirstVisibleEnabled(); } setCurrentTab(int index) { @@ -87,15 +130,13 @@ class PeerTabModel with ChangeNotifier { if (index >= 0 && index < tabNames.length) { return translate(tabNames[index]); } - assert(false); return index.toString(); } IconData tabIcon(int index) { - if (index >= 0 && index < tabNames.length) { + if (index >= 0 && index < icons.length) { return icons[index]; } - assert(false); return Icons.help; } @@ -171,29 +212,54 @@ class PeerTabModel with ChangeNotifier { } setTabVisible(int index, bool visible) { - if (index >= 0 && index < _isVisible.length) { + if (index >= 0 && index < maxTabCount) { if (_isVisible[index] != visible) { _isVisible[index] = visible; if (index == _currentTab && !visible) { - _trySetCurrentTabToFirstVisible(); - } else if (visible && visibleIndexs.length == 1) { + _trySetCurrentTabToFirstVisibleEnabled(); + } else if (visible && visibleEnabledOrderedIndexs.length == 1) { _currentTab = index; } try { bind.setLocalFlutterOption( - k: 'peer-tab-visible', v: jsonEncode(_isVisible)); + k: kPeerTabVisible, v: jsonEncode(_isVisible)); } catch (_) {} notifyListeners(); } } } - _trySetCurrentTabToFirstVisible() { - if (!_isVisible[_currentTab]) { - int firstVisible = _isVisible.indexWhere((e) => e); - if (firstVisible >= 0) { - _currentTab = firstVisible; + _trySetCurrentTabToFirstVisibleEnabled() { + if (!visibleEnabledOrderedIndexs.contains(_currentTab)) { + if (visibleEnabledOrderedIndexs.isNotEmpty) { + _currentTab = visibleEnabledOrderedIndexs.first; } } } + + reorder(int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + if (oldIndex < 0 || oldIndex >= visibleEnabledOrderedIndexs.length) { + return; + } + if (newIndex < 0 || newIndex >= visibleEnabledOrderedIndexs.length) { + return; + } + final oldTabValue = visibleEnabledOrderedIndexs[oldIndex]; + final newTabValue = visibleEnabledOrderedIndexs[newIndex]; + int oldValueIndex = orders.indexOf(oldTabValue); + int newValueIndex = orders.indexOf(newTabValue); + final list = orders.toList(); + if (oldIndex != -1 && newIndex != -1) { + list.removeAt(oldValueIndex); + list.insert(newValueIndex, oldTabValue); + for (int i = 0; i < list.length; i++) { + orders[i] = list[i]; + } + bind.setLocalFlutterOption(k: kPeerTabOrder, v: jsonEncode(orders)); + notifyListeners(); + } + } } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 15ae78d2c..fd0d7189b 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1052,7 +1052,7 @@ class RustdeskImpl { throw UnimplementedError(); } - bool mainHasGpucodec({dynamic hint}) { + bool mainHasVram({dynamic hint}) { throw UnimplementedError(); } @@ -1573,5 +1573,14 @@ class RustdeskImpl { throw UnimplementedError(); } + Future mainCheckHwcodec({dynamic hint}) { + throw UnimplementedError(); + } + + Future sessionRequestNewDisplayInitMsgs( + {required UuidValue sessionId, required int display, dynamic hint}) { + throw UnimplementedError(); + } + void dispose() {} } diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 514baaebc..f38badcbb 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -210,7 +210,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 12c2b8aa8..08643aee9 100644 --- a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ PathBuf { 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) - { + if let Ok(output) = crate::platform::linux::run_cmds_trim_newline(&cmd) { return output.into(); } return format!("/home/{user}").into(); @@ -505,7 +503,7 @@ impl Config { fn store_(config: &T, suffix: &str) { let file = Self::file_(suffix); if let Err(err) = store_path(file, config) { - log::error!("Failed to store config: {}", err); + log::error!("Failed to store {suffix} config: {err}"); } } @@ -1495,8 +1493,10 @@ impl LanPeers { #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct HwCodecConfig { - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - pub options: HashMap, + #[serde(default, deserialize_with = "deserialize_string")] + pub ram: String, + #[serde(default, deserialize_with = "deserialize_string")] + pub vram: String, } impl HwCodecConfig { @@ -1511,25 +1511,17 @@ impl HwCodecConfig { pub fn clear() { HwCodecConfig::default().store(); } -} -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct GpucodecConfig { - #[serde(default, deserialize_with = "deserialize_string")] - pub available: String, -} - -impl GpucodecConfig { - pub fn load() -> GpucodecConfig { - Config::load_::("_gpucodec") + pub fn clear_ram() { + let mut c = Self::load(); + c.ram = Default::default(); + c.store(); } - pub fn store(&self) { - Config::store_(self, "_gpucodec"); - } - - pub fn clear() { - GpucodecConfig::default().store(); + pub fn clear_vram() { + let mut c = Self::load(); + c.vram = Default::default(); + c.store(); } } @@ -1569,6 +1561,7 @@ impl UserDefaultConfig { } "custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 0xFFF as f64), "custom-fps" => self.get_double_string(key, 30.0, 5.0, 120.0), + "enable_file_transfer" => self.get_string(key, "Y", vec![""]), _ => self .get_after(key) .map(|v| v.to_string()) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 37d9e58db..57d38db09 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -47,10 +47,13 @@ pub mod keyboard; 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 sysinfo; pub use toml; pub use uuid; pub use base64; +pub use thiserror; #[cfg(feature = "quic")] pub type Stream = quic::Connection; diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index d22120df6..64805e0e5 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -13,6 +13,8 @@ edition = "2018" wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing"] mediacodec = ["ndk"] linux-pkg-config = ["dep:pkg-config"] +hwcodec = ["dep:hwcodec"] +vram = ["hwcodec/vram"] [dependencies] cfg-if = "1.0" @@ -20,6 +22,7 @@ num_cpus = "1.15" lazy_static = "1.4" hbb_common = { path = "../hbb_common" } webm = { git = "https://github.com/21pages/rust-webm" } +serde = {version="1.0", features=["derive"]} [dependencies.winapi] version = "0.3" @@ -41,7 +44,6 @@ ndk-context = "0.1" [target.'cfg(not(target_os = "android"))'.dev-dependencies] repng = "0.2" docopt = "1.1" -serde = {version="1.0", features=["derive"]} quest = "0.3" [build-dependencies] @@ -56,8 +58,8 @@ gstreamer = { version = "0.16", optional = true } gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } gstreamer-video = { version = "0.16", optional = true } -[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] -hwcodec = { git = "https://github.com/21pages/hwcodec", branch = "stable", optional = true } +[dependencies.hwcodec] +git = "https://github.com/21pages/hwcodec" +optional = true + -[target.'cfg(target_os = "windows")'.dependencies] -gpucodec = { git = "https://github.com/21pages/gpucodec", optional = true } diff --git a/libs/scrap/examples/benchmark.rs b/libs/scrap/examples/benchmark.rs index 688e06edd..ccddccc81 100644 --- a/libs/scrap/examples/benchmark.rs +++ b/libs/scrap/examples/benchmark.rs @@ -239,16 +239,16 @@ fn test_av1( #[cfg(feature = "hwcodec")] mod hw { - use hwcodec::ffmpeg::CodecInfo; + use hwcodec::ffmpeg_ram::CodecInfo; use scrap::{ - hwcodec::{HwDecoder, HwEncoder, HwEncoderConfig}, + hwcodec::{HwRamDecoder, HwRamEncoder, HwRamEncoderConfig}, CodecFormat, }; use super::*; pub fn test(c: &mut Capturer, width: usize, height: usize, quality: Q, yuv_count: usize) { - let best = HwEncoder::best(); + let best = HwRamEncoder::best(); let mut h264s = Vec::new(); let mut h265s = Vec::new(); if let Some(info) = best.h264 { @@ -270,8 +270,8 @@ mod hw { yuv_count: usize, h26xs: &mut Vec>, ) { - let mut encoder = HwEncoder::new( - EncoderCfg::HW(HwEncoderConfig { + let mut encoder = HwRamEncoder::new( + EncoderCfg::HWRAM(HwRamEncoderConfig { name: info.name.clone(), width, height, @@ -321,7 +321,7 @@ mod hw { } fn test_decoder(format: CodecFormat, h26xs: &Vec>) { - let mut decoder = HwDecoder::new(format).unwrap(); + let mut decoder = HwRamDecoder::new(format).unwrap(); let start = Instant::now(); let mut cnt = 0; for h26x in h26xs { diff --git a/libs/scrap/src/android/ffi.rs b/libs/scrap/src/android/ffi.rs index e4a8877de..3c1ca87da 100644 --- a/libs/scrap/src/android/ffi.rs +++ b/libs/scrap/src/android/ffi.rs @@ -94,7 +94,7 @@ pub fn get_audio_raw<'a>() -> Option<&'a [u8]> { } #[no_mangle] -pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onVideoFrameUpdate( +pub extern "system" fn Java_ffi_FFI_onVideoFrameUpdate( env: JNIEnv, _class: JClass, buffer: JObject, @@ -108,7 +108,7 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onVideoFrameUpd } #[no_mangle] -pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onAudioFrameUpdate( +pub extern "system" fn Java_ffi_FFI_onAudioFrameUpdate( env: JNIEnv, _class: JClass, buffer: JObject, @@ -122,7 +122,7 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onAudioFrameUpd } #[no_mangle] -pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_setFrameRawEnable( +pub extern "system" fn Java_ffi_FFI_setFrameRawEnable( env: JNIEnv, _class: JClass, name: JString, @@ -141,7 +141,7 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_setFrameRawEnab } #[no_mangle] -pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_init( +pub extern "system" fn Java_ffi_FFI_init( env: JNIEnv, _class: JClass, ctx: JObject, diff --git a/libs/scrap/src/common/aom.rs b/libs/scrap/src/common/aom.rs index bf7c96c44..759bf3fbe 100644 --- a/libs/scrap/src/common/aom.rs +++ b/libs/scrap/src/common/aom.rs @@ -268,7 +268,7 @@ impl EncoderApi for AomEncoder { self.yuvfmt.clone() } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn input_texture(&self) -> bool { false } diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 71a518b94..fd283386b 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -5,12 +5,12 @@ use std::{ sync::{Arc, Mutex}, }; -#[cfg(feature = "gpucodec")] -use crate::gpucodec::*; #[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, @@ -31,7 +31,7 @@ use hbb_common::{ tokio::time::Instant, ResultType, }; -#[cfg(any(feature = "hwcodec", feature = "mediacodec", feature = "gpucodec"))] +#[cfg(any(feature = "hwcodec", feature = "mediacodec", feature = "vram"))] use hbb_common::{config::Config2, lazy_static}; lazy_static::lazy_static! { @@ -47,9 +47,9 @@ pub enum EncoderCfg { VPX(VpxEncoderConfig), AOM(AomEncoderConfig), #[cfg(feature = "hwcodec")] - HW(HwEncoderConfig), - #[cfg(feature = "gpucodec")] - GPU(GpuEncoderConfig), + HWRAM(HwRamEncoderConfig), + #[cfg(feature = "vram")] + VRAM(VRamEncoderConfig), } pub trait EncoderApi { @@ -61,7 +61,7 @@ pub trait EncoderApi { fn yuvfmt(&self) -> EncodeYuvFormat; - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn input_texture(&self) -> bool; fn set_quality(&mut self, quality: Quality) -> ResultType<()>; @@ -94,13 +94,13 @@ pub struct Decoder { vp9: Option, av1: Option, #[cfg(feature = "hwcodec")] - h264_ram: Option, + h264_ram: Option, #[cfg(feature = "hwcodec")] - h265_ram: Option, - #[cfg(feature = "gpucodec")] - h264_vram: Option, - #[cfg(feature = "gpucodec")] - h265_vram: Option, + 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")] @@ -131,25 +131,25 @@ impl Encoder { }), #[cfg(feature = "hwcodec")] - EncoderCfg::HW(_) => match HwEncoder::new(config, i444) { + 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"); - hbb_common::config::HwCodecConfig::clear(); + hbb_common::config::HwCodecConfig::clear_ram(); *ENCODE_CODEC_NAME.lock().unwrap() = CodecName::VP9; Err(e) } }, - #[cfg(feature = "gpucodec")] - EncoderCfg::GPU(_) => match GpuEncoder::new(config, i444) { + #[cfg(feature = "vram")] + EncoderCfg::VRAM(_) => match VRamEncoder::new(config, i444) { Ok(tex) => Ok(Encoder { codec: Box::new(tex), }), Err(e) => { - log::error!("new gpu encoder failed: {e:?}, clear config"); - hbb_common::config::GpucodecConfig::clear(); + log::error!("new vram encoder failed: {e:?}, clear config"); + hbb_common::config::HwCodecConfig::clear_vram(); *ENCODE_CODEC_NAME.lock().unwrap() = CodecName::VP9; Err(e) } @@ -186,19 +186,19 @@ impl Encoder { let _all_support_h265_decoding = decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_h265 > 0); #[allow(unused_mut)] - let mut h264gpu_encoding = false; + let mut h264vram_encoding = false; #[allow(unused_mut)] - let mut h265gpu_encoding = false; - #[cfg(feature = "gpucodec")] - if enable_gpucodec_option() { + let mut h265vram_encoding = false; + #[cfg(feature = "vram")] + if enable_vram_option() { if _all_support_h264_decoding { - if GpuEncoder::available(CodecName::H264GPU).len() > 0 { - h264gpu_encoding = true; + if VRamEncoder::available(CodecName::H264VRAM).len() > 0 { + h264vram_encoding = true; } } if _all_support_h265_decoding { - if GpuEncoder::available(CodecName::H265GPU).len() > 0 { - h265gpu_encoding = true; + if VRamEncoder::available(CodecName::H265VRAM).len() > 0 { + h265vram_encoding = true; } } } @@ -208,7 +208,7 @@ impl Encoder { let mut h265hw_encoding = None; #[cfg(feature = "hwcodec")] if enable_hwcodec_option() { - let best = HwEncoder::best(); + let best = HwRamEncoder::best(); if _all_support_h264_decoding { h264hw_encoding = best.h264.map_or(None, |c| Some(c.name)); } @@ -217,9 +217,9 @@ impl Encoder { } } let h264_useable = - _all_support_h264_decoding && (h264gpu_encoding || h264hw_encoding.is_some()); + _all_support_h264_decoding && (h264vram_encoding || h264hw_encoding.is_some()); let h265_useable = - _all_support_h265_decoding && (h265gpu_encoding || h265hw_encoding.is_some()); + _all_support_h265_decoding && (h265vram_encoding || h265hw_encoding.is_some()); let mut name = ENCODE_CODEC_NAME.lock().unwrap(); let mut preference = PreferCodec::Auto; let preferences: Vec<_> = decodings @@ -239,7 +239,8 @@ impl Encoder { #[allow(unused_mut)] let mut auto_codec = CodecName::VP9; - if av1_useable { + // aom is very slow for x86 sciter version on windows x64 + if av1_useable && !(cfg!(windows) && std::env::consts::ARCH == "x86") { auto_codec = CodecName::AV1; } let mut system = System::new(); @@ -254,19 +255,19 @@ impl Encoder { PreferCodec::VP9 => CodecName::VP9, PreferCodec::AV1 => CodecName::AV1, PreferCodec::H264 => { - if h264gpu_encoding { - CodecName::H264GPU + if h264vram_encoding { + CodecName::H264VRAM } else if let Some(v) = h264hw_encoding { - CodecName::H264HW(v) + CodecName::H264RAM(v) } else { auto_codec } } PreferCodec::H265 => { - if h265gpu_encoding { - CodecName::H265GPU + if h265vram_encoding { + CodecName::H265VRAM } else if let Some(v) = h265hw_encoding { - CodecName::H265HW(v) + CodecName::H265RAM(v) } else { auto_codec } @@ -306,14 +307,14 @@ impl Encoder { }; #[cfg(feature = "hwcodec")] if enable_hwcodec_option() { - let best = HwEncoder::best(); + let best = HwRamEncoder::best(); encoding.h264 |= best.h264.is_some(); encoding.h265 |= best.h265.is_some(); } - #[cfg(feature = "gpucodec")] - if enable_gpucodec_option() { - encoding.h264 |= GpuEncoder::available(CodecName::H264GPU).len() > 0; - encoding.h265 |= GpuEncoder::available(CodecName::H265GPU).len() > 0; + #[cfg(feature = "vram")] + if enable_vram_option() { + encoding.h264 |= VRamEncoder::available(CodecName::H264VRAM).len() > 0; + encoding.h265 |= VRamEncoder::available(CodecName::H265VRAM).len() > 0; } encoding } @@ -326,21 +327,21 @@ impl Encoder { }, EncoderCfg::AOM(_) => CodecName::AV1, #[cfg(feature = "hwcodec")] - EncoderCfg::HW(hw) => { + EncoderCfg::HWRAM(hw) => { if hw.name.to_lowercase().contains("h264") { - CodecName::H264HW(hw.name.clone()) + CodecName::H264RAM(hw.name.clone()) } else { - CodecName::H265HW(hw.name.clone()) + CodecName::H265RAM(hw.name.clone()) } } - #[cfg(feature = "gpucodec")] - EncoderCfg::GPU(gpu) => match gpu.feature.data_format { - gpucodec::gpu_common::DataFormat::H264 => CodecName::H264GPU, - gpucodec::gpu_common::DataFormat::H265 => CodecName::H265GPU, + #[cfg(feature = "vram")] + EncoderCfg::VRAM(vram) => match vram.feature.data_format { + hwcodec::common::DataFormat::H264 => CodecName::H264VRAM, + hwcodec::common::DataFormat::H265 => CodecName::H265VRAM, _ => { log::error!( - "should not reach here, gpucodec not support {:?}", - gpu.feature.data_format + "should not reach here, vram not support {:?}", + vram.feature.data_format ); return; } @@ -365,9 +366,9 @@ impl Encoder { }, EncoderCfg::AOM(_) => decodings.iter().all(|d| d.1.i444.av1), #[cfg(feature = "hwcodec")] - EncoderCfg::HW(_) => false, - #[cfg(feature = "gpucodec")] - EncoderCfg::GPU(_) => false, + EncoderCfg::HWRAM(_) => false, + #[cfg(feature = "vram")] + EncoderCfg::VRAM(_) => false, }; prefer_i444 && i444_useable && !decodings.is_empty() } @@ -398,19 +399,19 @@ impl Decoder { ..Default::default() }; #[cfg(feature = "hwcodec")] - if enable_hwcodec_option() { - let best = HwDecoder::best(); + { + let best = HwRamDecoder::best(); decoding.ability_h264 |= if best.h264.is_some() { 1 } else { 0 }; decoding.ability_h265 |= if best.h265.is_some() { 1 } else { 0 }; } - #[cfg(feature = "gpucodec")] - if enable_gpucodec_option() && _flutter { - decoding.ability_h264 |= if GpuDecoder::available(CodecFormat::H264, _luid).len() > 0 { + #[cfg(feature = "vram")] + if enable_vram_option() && _flutter { + decoding.ability_h264 |= if VRamDecoder::available(CodecFormat::H264, _luid).len() > 0 { 1 } else { 0 }; - decoding.ability_h265 |= if GpuDecoder::available(CodecFormat::H265, _luid).len() > 0 { + decoding.ability_h265 |= if VRamDecoder::available(CodecFormat::H265, _luid).len() > 0 { 1 } else { 0 @@ -449,7 +450,7 @@ impl Decoder { let (mut vp8, mut vp9, mut av1) = (None, None, None); #[cfg(feature = "hwcodec")] let (mut h264_ram, mut h265_ram) = (None, None); - #[cfg(feature = "gpucodec")] + #[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); @@ -482,17 +483,17 @@ impl Decoder { valid = av1.is_some(); } CodecFormat::H264 => { - #[cfg(feature = "gpucodec")] - if !valid && enable_gpucodec_option() && _luid.clone().unwrap_or_default() != 0 { - match GpuDecoder::new(format, _luid) { + #[cfg(feature = "vram")] + if !valid && enable_vram_option() && _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 && enable_hwcodec_option() { - match HwDecoder::new(format) { + if !valid { + match HwRamDecoder::new(format) { Ok(v) => h264_ram = Some(v), Err(e) => log::error!("create H264 ram decoder failed: {}", e), } @@ -508,17 +509,17 @@ impl Decoder { } } CodecFormat::H265 => { - #[cfg(feature = "gpucodec")] - if !valid && enable_gpucodec_option() && _luid.clone().unwrap_or_default() != 0 { - match GpuDecoder::new(format, _luid) { + #[cfg(feature = "vram")] + if !valid && enable_vram_option() && _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 && enable_hwcodec_option() { - match HwDecoder::new(format) { + if !valid { + match HwRamDecoder::new(format) { Ok(v) => h265_ram = Some(v), Err(e) => log::error!("create H265 ram decoder failed: {}", e), } @@ -550,9 +551,9 @@ impl Decoder { h264_ram, #[cfg(feature = "hwcodec")] h265_ram, - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] h264_vram, - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] h265_vram, #[cfg(feature = "mediacodec")] h264_media_codec, @@ -604,31 +605,31 @@ impl Decoder { bail!("av1 decoder not available"); } } - #[cfg(any(feature = "hwcodec", feature = "gpucodec"))] + #[cfg(any(feature = "hwcodec", feature = "vram"))] video_frame::Union::H264s(h264s) => { *chroma = Some(Chroma::I420); - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] if let Some(decoder) = &mut self.h264_vram { *_pixelbuffer = false; - return Decoder::handle_gpu_video_frame(decoder, h264s, _texture); + return Decoder::handle_vram_video_frame(decoder, h264s, _texture); } #[cfg(feature = "hwcodec")] if let Some(decoder) = &mut self.h264_ram { - return Decoder::handle_hw_video_frame(decoder, h264s, rgb, &mut self.i420); + return Decoder::handle_hwram_video_frame(decoder, h264s, rgb, &mut self.i420); } Err(anyhow!("don't support h264!")) } - #[cfg(any(feature = "hwcodec", feature = "gpucodec"))] + #[cfg(any(feature = "hwcodec", feature = "vram"))] video_frame::Union::H265s(h265s) => { *chroma = Some(Chroma::I420); - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] if let Some(decoder) = &mut self.h265_vram { *_pixelbuffer = false; - return Decoder::handle_gpu_video_frame(decoder, h265s, _texture); + return Decoder::handle_vram_video_frame(decoder, h265s, _texture); } #[cfg(feature = "hwcodec")] if let Some(decoder) = &mut self.h265_ram { - return Decoder::handle_hw_video_frame(decoder, h265s, rgb, &mut self.i420); + return Decoder::handle_hwram_video_frame(decoder, h265s, rgb, &mut self.i420); } Err(anyhow!("don't support h265!")) } @@ -710,8 +711,8 @@ impl Decoder { // rgb [in/out] fmt and stride must be set in ImageRgb #[cfg(feature = "hwcodec")] - fn handle_hw_video_frame( - decoder: &mut HwDecoder, + fn handle_hwram_video_frame( + decoder: &mut HwRamDecoder, frames: &EncodedVideoFrames, rgb: &mut ImageRgb, i420: &mut Vec, @@ -728,9 +729,9 @@ impl Decoder { return Ok(ret); } - #[cfg(feature = "gpucodec")] - fn handle_gpu_video_frame( - decoder: &mut GpuDecoder, + #[cfg(feature = "vram")] + fn handle_vram_video_frame( + decoder: &mut VRamDecoder, frames: &EncodedVideoFrames, texture: &mut *mut c_void, ) -> ResultType { @@ -791,13 +792,16 @@ impl Decoder { #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] pub fn enable_hwcodec_option() -> bool { - if let Some(v) = Config2::get().options.get("enable-hwcodec") { - return v != "N"; + if cfg!(windows) || cfg!(target_os = "linux") || cfg!(feature = "mediacodec") { + if let Some(v) = Config2::get().options.get("enable-hwcodec") { + return v != "N"; + } + return true; // default is true } - return true; // default is true + false } -#[cfg(feature = "gpucodec")] -pub fn enable_gpucodec_option() -> bool { +#[cfg(feature = "vram")] +pub fn enable_vram_option() -> bool { if let Some(v) = Config2::get().options.get("enable-hwcodec") { return v != "N"; } diff --git a/libs/scrap/src/common/convert.rs b/libs/scrap/src/common/convert.rs index 09640e35c..66bd8d358 100644 --- a/libs/scrap/src/common/convert.rs +++ b/libs/scrap/src/common/convert.rs @@ -18,7 +18,7 @@ pub mod hw { use super::*; use crate::ImageFormat; #[cfg(target_os = "windows")] - use hwcodec::{ffmpeg::ffmpeg_linesize_offset_length, AVPixelFormat}; + use hwcodec::{ffmpeg::AVPixelFormat, ffmpeg_ram::ffmpeg_linesize_offset_length}; #[cfg(target_os = "windows")] pub fn hw_nv12_to( @@ -222,9 +222,7 @@ pub fn convert_to_yuv( ); } } - let align = |x:usize| { - (x + 63) / 64 * 64 - }; + let align = |x: usize| (x + 63) / 64 * 64; match (src_pixfmt, dst_fmt.pixfmt) { (crate::Pixfmt::BGRA, crate::Pixfmt::I420) | (crate::Pixfmt::RGBA, crate::Pixfmt::I420) => { @@ -282,7 +280,8 @@ pub fn convert_to_yuv( 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)), + align(dst_fmt.h) + * (align(dst_stride_y) + align(dst_stride_u) + align(dst_stride_v)), 0, ); let dst_y = dst.as_mut_ptr(); diff --git a/libs/scrap/src/common/dxgi.rs b/libs/scrap/src/common/dxgi.rs index ec81dbff9..ae2f1130f 100644 --- a/libs/scrap/src/common/dxgi.rs +++ b/libs/scrap/src/common/dxgi.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "gpucodec")] +#[cfg(feature = "vram")] use crate::AdapterDevice; use crate::{common::TraitCapturer, dxgi, Frame, Pixfmt}; use std::{ @@ -57,12 +57,12 @@ impl TraitCapturer for Capturer { self.inner.set_gdi() } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn device(&self) -> AdapterDevice { self.inner.device() } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn set_output_texture(&mut self, texture: bool) { self.inner.set_output_texture(texture); } @@ -197,7 +197,7 @@ impl Display { self.origin() == (0, 0) } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] pub fn adapter_luid(&self) -> Option { self.0.adapter_luid() } @@ -247,11 +247,11 @@ impl TraitCapturer for CapturerMag { false } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn device(&self) -> AdapterDevice { AdapterDevice::default() } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn set_output_texture(&mut self, _texture: bool) {} } diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 4f19d8d6b..92f58cde2 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -1,28 +1,30 @@ use crate::{ - codec::{base_bitrate, codec_thread_num, EncoderApi, EncoderCfg, Quality as Q}, + codec::{ + base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg, Quality as Q, + }, hw, CodecFormat, EncodeInput, ImageFormat, ImageRgb, Pixfmt, HW_STRIDE_ALIGN, }; use hbb_common::{ - allow_err, anyhow::{anyhow, bail, Context}, bytes::Bytes, config::HwCodecConfig, log, message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame}, - ResultType, + serde_derive::{Deserialize, Serialize}, + serde_json, ResultType, }; use hwcodec::{ - decode::{DecodeContext, DecodeFrame, Decoder}, - encode::{EncodeContext, EncodeFrame, Encoder}, - ffmpeg::{CodecInfo, CodecInfos, DataFormat}, - AVPixelFormat, - Quality::{self, *}, - RateControl::{self, *}, + common::DataFormat, + ffmpeg::AVPixelFormat, + ffmpeg_ram::{ + decode::{DecodeContext, DecodeFrame, Decoder}, + encode::{EncodeContext, EncodeFrame, Encoder}, + CodecInfo, CodecInfos, + Quality::{self, *}, + RateControl::{self, *}, + }, }; -const CFG_KEY_ENCODER: &str = "bestHwEncoders"; -const CFG_KEY_DECODER: &str = "bestHwDecoders"; - const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_NV12; pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30]; const DEFAULT_GOP: i32 = i32::MAX; @@ -30,7 +32,7 @@ const DEFAULT_HW_QUALITY: Quality = Quality_Default; const DEFAULT_RC: RateControl = RC_DEFAULT; #[derive(Debug, Clone)] -pub struct HwEncoderConfig { +pub struct HwRamEncoderConfig { pub name: String, pub width: usize, pub height: usize, @@ -38,7 +40,7 @@ pub struct HwEncoderConfig { pub keyframe_interval: Option, } -pub struct HwEncoder { +pub struct HwRamEncoder { encoder: Encoder, name: String, pub format: DataFormat, @@ -48,13 +50,13 @@ pub struct HwEncoder { bitrate: u32, //kbs } -impl EncoderApi for HwEncoder { +impl EncoderApi for HwRamEncoder { fn new(cfg: EncoderCfg, _i444: bool) -> ResultType where Self: Sized, { match cfg { - EncoderCfg::HW(config) => { + EncoderCfg::HWRAM(config) => { let b = Self::convert_quality(config.quality); let base_bitrate = base_bitrate(config.width as _, config.height as _); let mut bitrate = base_bitrate * b / 100; @@ -85,7 +87,7 @@ impl EncoderApi for HwEncoder { } }; match Encoder::new(ctx.clone()) { - Ok(encoder) => Ok(HwEncoder { + Ok(encoder) => Ok(HwRamEncoder { encoder, name: config.name, format, @@ -95,7 +97,7 @@ impl EncoderApi for HwEncoder { bitrate, }), Err(_) => { - HwCodecConfig::clear(); + HwCodecConfig::clear_ram(); Err(anyhow!(format!("Failed to create encoder"))) } } @@ -126,6 +128,7 @@ impl EncoderApi for HwEncoder { match self.format { DataFormat::H264 => vf.set_h264s(frames), DataFormat::H265 => vf.set_h265s(frames), + _ => bail!("unsupported format: {:?}", self.format), } Ok(vf) } else { @@ -160,7 +163,7 @@ impl EncoderApi for HwEncoder { } } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn input_texture(&self) -> bool { false } @@ -184,9 +187,9 @@ impl EncoderApi for HwEncoder { } } -impl HwEncoder { +impl HwRamEncoder { pub fn best() -> CodecInfos { - get_config(CFG_KEY_ENCODER).unwrap_or(CodecInfos { + get_config().map(|c| c.e).unwrap_or(CodecInfos { h264: None, h265: None, }) @@ -214,28 +217,30 @@ impl HwEncoder { } } -pub struct HwDecoder { +pub struct HwRamDecoder { decoder: Decoder, pub info: CodecInfo, } -#[derive(Default)] -pub struct HwDecoders { - pub h264: Option, - pub h265: Option, -} - -impl HwDecoder { +impl HwRamDecoder { pub fn best() -> CodecInfos { - get_config(CFG_KEY_DECODER).unwrap_or(CodecInfos { - h264: None, - h265: None, - }) + let mut info = CodecInfo::soft(); + if enable_hwcodec_option() { + if let Ok(hw) = get_config().map(|c| c.d) { + if let Some(h264) = hw.h264 { + info.h264 = Some(h264); + } + if let Some(h265) = hw.h265 { + info.h265 = Some(h265); + } + } + } + info } pub fn new(format: CodecFormat) -> ResultType { log::info!("try create {format:?} ram decoder"); - let best = HwDecoder::best(); + let best = HwRamDecoder::best(); let info = match format { CodecFormat::H264 => { if let Some(info) = best.h264 { @@ -259,26 +264,26 @@ impl HwDecoder { thread_count: codec_thread_num(16) as _, }; match Decoder::new(ctx) { - Ok(decoder) => Ok(HwDecoder { decoder, info }), + Ok(decoder) => Ok(HwRamDecoder { decoder, info }), Err(_) => { - HwCodecConfig::clear(); + HwCodecConfig::clear_ram(); Err(anyhow!(format!("Failed to create decoder"))) } } } - pub fn decode(&mut self, data: &[u8]) -> ResultType> { + pub fn decode(&mut self, data: &[u8]) -> ResultType> { match self.decoder.decode(data) { - Ok(v) => Ok(v.iter().map(|f| HwDecoderImage { frame: f }).collect()), + Ok(v) => Ok(v.iter().map(|f| HwRamDecoderImage { frame: f }).collect()), Err(e) => Err(anyhow!(e)), } } } -pub struct HwDecoderImage<'a> { +pub struct HwRamDecoderImage<'a> { frame: &'a DecodeFrame, } -impl HwDecoderImage<'_> { +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; @@ -332,23 +337,24 @@ impl HwDecoderImage<'_> { } } -fn get_config(k: &str) -> ResultType { - let v = HwCodecConfig::load() - .options - .get(k) - .unwrap_or(&"".to_owned()) - .to_owned(); - match CodecInfos::deserialize(&v) { +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +struct Available { + e: CodecInfos, + d: CodecInfos, +} + +fn get_config() -> ResultType { + match serde_json::from_str(&HwCodecConfig::load().ram) { Ok(v) => Ok(v), - Err(_) => Err(anyhow!("Failed to get config:{}", k)), + Err(e) => Err(anyhow!("Failed to get config:{e:?}")), } } pub fn check_available_hwcodec() { let ctx = EncodeContext { name: String::from(""), - width: 1920, - height: 1080, + width: 1280, + height: 720, pixfmt: DEFAULT_PIXFMT, align: HW_STRIDE_ALIGN as _, bitrate: 0, @@ -358,30 +364,27 @@ pub fn check_available_hwcodec() { rc: DEFAULT_RC, thread_count: 4, }; - let encoders = CodecInfo::score(Encoder::available_encoders(ctx)); - let decoders = CodecInfo::score(Decoder::available_decoders()); - - if let Ok(old_encoders) = get_config(CFG_KEY_ENCODER) { - if let Ok(old_decoders) = get_config(CFG_KEY_DECODER) { - if encoders == old_encoders && decoders == old_decoders { - return; - } - } + #[cfg(feature = "vram")] + let vram = crate::vram::check_available_vram(); + #[cfg(not(feature = "vram"))] + let vram = "".to_owned(); + let encoders = CodecInfo::prioritized(Encoder::available_encoders(ctx, Some(vram.clone()))); + let decoders = CodecInfo::prioritized(Decoder::available_decoders(Some(vram.clone()))); + let ram = Available { + e: encoders, + d: decoders, + }; + if let Ok(ram) = serde_json::to_string_pretty(&ram) { + HwCodecConfig { ram, vram }.store(); } - - if let Ok(encoders) = encoders.serialize() { - if let Ok(decoders) = decoders.serialize() { - let mut config = HwCodecConfig::load(); - config.options.insert(CFG_KEY_ENCODER.to_owned(), encoders); - config.options.insert(CFG_KEY_DECODER.to_owned(), decoders); - config.store(); - return; - } - } - log::error!("Failed to serialize codec info"); } -pub fn hwcodec_new_check_process() { +#[cfg(any(target_os = "windows", target_os = "linux"))] +pub fn start_check_process(force: bool) { + if !force && !enable_hwcodec_option() { + return; + } + use hbb_common::allow_err; use std::sync::Once; let f = || { // Clear to avoid checking process errors @@ -421,7 +424,11 @@ pub fn hwcodec_new_check_process() { }; }; static ONCE: Once = Once::new(); - ONCE.call_once(|| { + if force && ONCE.is_completed() { std::thread::spawn(f); - }); + } else { + ONCE.call_once(|| { + std::thread::spawn(f); + }); + } } diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 85ecddfbd..6ba7d991f 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -37,13 +37,13 @@ cfg_if! { pub mod codec; pub mod convert; -#[cfg(feature = "gpucodec")] -pub mod gpucodec; #[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 @@ -111,10 +111,10 @@ pub trait TraitCapturer { #[cfg(windows)] fn set_gdi(&mut self) -> bool; - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn device(&self) -> AdapterDevice; - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn set_output_texture(&mut self, texture: bool); } @@ -245,10 +245,10 @@ pub enum CodecName { VP8, VP9, AV1, - H264HW(String), - H265HW(String), - H264GPU, - H265GPU, + H264RAM(String), + H265RAM(String), + H264VRAM, + H265VRAM, } #[derive(PartialEq, Debug, Clone, Copy)] @@ -280,8 +280,8 @@ impl From<&CodecName> for CodecFormat { CodecName::VP8 => Self::VP8, CodecName::VP9 => Self::VP9, CodecName::AV1 => Self::AV1, - CodecName::H264HW(_) | CodecName::H264GPU => Self::H264, - CodecName::H265HW(_) | CodecName::H265GPU => Self::H265, + CodecName::H264RAM(_) | CodecName::H264VRAM => Self::H264, + CodecName::H265RAM(_) | CodecName::H265VRAM => Self::H265, } } } diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs index a6cb73d07..a3e7e99e6 100644 --- a/libs/scrap/src/common/vpxcodec.rs +++ b/libs/scrap/src/common/vpxcodec.rs @@ -207,7 +207,7 @@ impl EncoderApi for VpxEncoder { self.yuvfmt.clone() } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn input_texture(&self) -> bool { false } diff --git a/libs/scrap/src/common/gpucodec.rs b/libs/scrap/src/common/vram.rs similarity index 68% rename from libs/scrap/src/common/gpucodec.rs rename to libs/scrap/src/common/vram.rs index 126a027b8..daeece519 100644 --- a/libs/scrap/src/common/gpucodec.rs +++ b/libs/scrap/src/common/vram.rs @@ -5,24 +5,24 @@ use std::{ }; use crate::{ - codec::{base_bitrate, enable_gpucodec_option, EncoderApi, EncoderCfg, Quality}, + codec::{base_bitrate, enable_vram_option, EncoderApi, EncoderCfg, Quality}, AdapterDevice, CodecFormat, CodecName, EncodeInput, EncodeYuvFormat, Pixfmt, }; -use gpucodec::gpu_common::{ - self, Available, DecodeContext, DynamicContext, EncodeContext, FeatureContext, MAX_GOP, -}; -use gpucodec::{ - decode::{self, DecodeFrame, Decoder}, - encode::{self, EncodeFrame, Encoder}, -}; use hbb_common::{ - allow_err, anyhow::{anyhow, bail, Context}, bytes::Bytes, log, message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame}, ResultType, }; +use hwcodec::{ + common::{DataFormat, Driver, MAX_GOP}, + native::{ + decode::{self, DecodeFrame, Decoder}, + encode::{self, EncodeFrame, Encoder}, + Available, DecodeContext, DynamicContext, EncodeContext, FeatureContext, + }, +}; const OUTPUT_SHARED_HANDLE: bool = false; @@ -35,31 +35,31 @@ lazy_static::lazy_static! { } #[derive(Debug, Clone)] -pub struct GpuEncoderConfig { +pub struct VRamEncoderConfig { pub device: AdapterDevice, pub width: usize, pub height: usize, pub quality: Quality, - pub feature: gpucodec::gpu_common::FeatureContext, + pub feature: FeatureContext, pub keyframe_interval: Option, } -pub struct GpuEncoder { +pub struct VRamEncoder { encoder: Encoder, - pub format: gpu_common::DataFormat, + pub format: DataFormat, ctx: EncodeContext, bitrate: u32, last_frame_len: usize, same_bad_len_counter: usize, } -impl EncoderApi for GpuEncoder { +impl EncoderApi for VRamEncoder { fn new(cfg: EncoderCfg, _i444: bool) -> ResultType where Self: Sized, { match cfg { - EncoderCfg::GPU(config) => { + EncoderCfg::VRAM(config) => { let b = Self::convert_quality(config.quality, &config.feature); let base_bitrate = base_bitrate(config.width as _, config.height as _); let mut bitrate = base_bitrate * b / 100; @@ -79,7 +79,7 @@ impl EncoderApi for GpuEncoder { }, }; match Encoder::new(ctx.clone()) { - Ok(encoder) => Ok(GpuEncoder { + Ok(encoder) => Ok(VRamEncoder { encoder, ctx, format: config.feature.data_format, @@ -88,7 +88,7 @@ impl EncoderApi for GpuEncoder { same_bad_len_counter: 0, }), Err(_) => { - hbb_common::config::GpucodecConfig::clear(); + hbb_common::config::HwCodecConfig::clear_vram(); Err(anyhow!(format!("Failed to create encoder"))) } } @@ -138,8 +138,8 @@ impl EncoderApi for GpuEncoder { ..Default::default() }; match self.format { - gpu_common::DataFormat::H264 => vf.set_h264s(frames), - gpu_common::DataFormat::H265 => vf.set_h265s(frames), + DataFormat::H264 => vf.set_h264s(frames), + DataFormat::H265 => vf.set_h265s(frames), _ => bail!("{:?} not supported", self.format), } Ok(vf) @@ -160,7 +160,7 @@ impl EncoderApi for GpuEncoder { } } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn input_texture(&self) -> bool { true } @@ -181,11 +181,11 @@ impl EncoderApi for GpuEncoder { } fn support_abr(&self) -> bool { - self.ctx.f.driver != gpu_common::EncodeDriver::VPL + self.ctx.f.driver != Driver::VPL } } -impl GpuEncoder { +impl VRamEncoder { pub fn try_get(device: &AdapterDevice, name: CodecName) -> Option { let v: Vec<_> = Self::available(name) .drain(..) @@ -201,12 +201,12 @@ impl GpuEncoder { pub fn available(name: CodecName) -> Vec { let not_use = ENOCDE_NOT_USE.lock().unwrap().clone(); if not_use.values().any(|not_use| *not_use) { - log::info!("currently not use gpucodec encoders: {not_use:?}"); + log::info!("currently not use vram encoders: {not_use:?}"); return vec![]; } let data_format = match name { - CodecName::H264GPU => gpu_common::DataFormat::H264, - CodecName::H265GPU => gpu_common::DataFormat::H265, + CodecName::H264VRAM => DataFormat::H264, + CodecName::H265VRAM => DataFormat::H265, _ => return vec![], }; let Ok(displays) = crate::Display::all() else { @@ -252,27 +252,21 @@ impl GpuEncoder { pub fn convert_quality(quality: Quality, f: &FeatureContext) -> u32 { match quality { Quality::Best => { - if f.driver == gpu_common::EncodeDriver::VPL - && f.data_format == gpu_common::DataFormat::H264 - { + if f.driver == Driver::VPL && f.data_format == DataFormat::H264 { 200 } else { 150 } } Quality::Balanced => { - if f.driver == gpu_common::EncodeDriver::VPL - && f.data_format == gpu_common::DataFormat::H264 - { + if f.driver == Driver::VPL && f.data_format == DataFormat::H264 { 150 } else { 100 } } Quality::Low => { - if f.driver == gpu_common::EncodeDriver::VPL - && f.data_format == gpu_common::DataFormat::H264 - { + if f.driver == Driver::VPL && f.data_format == DataFormat::H264 { 75 } else { 50 @@ -283,7 +277,7 @@ impl GpuEncoder { } pub fn set_not_use(display: usize, not_use: bool) { - log::info!("set display#{display} not use gpucodec encode to {not_use}"); + log::info!("set display#{display} not use vram encode to {not_use}"); ENOCDE_NOT_USE.lock().unwrap().insert(display, not_use); } @@ -292,17 +286,11 @@ impl GpuEncoder { } } -pub struct GpuDecoder { +pub struct VRamDecoder { decoder: Decoder, } -#[derive(Default)] -pub struct GpuDecoders { - pub h264: Option, - pub h265: Option, -} - -impl GpuDecoder { +impl VRamDecoder { pub fn try_get(format: CodecFormat, luid: Option) -> Option { let v: Vec<_> = Self::available(format, luid); if v.len() > 0 { @@ -315,8 +303,8 @@ impl GpuDecoder { pub fn available(format: CodecFormat, luid: Option) -> Vec { let luid = luid.unwrap_or_default(); let data_format = match format { - CodecFormat::H264 => gpu_common::DataFormat::H264, - CodecFormat::H265 => gpu_common::DataFormat::H265, + CodecFormat::H264 => DataFormat::H264, + CodecFormat::H265 => DataFormat::H265, _ => return vec![], }; get_available_config() @@ -328,15 +316,13 @@ impl GpuDecoder { } pub fn possible_available_without_check() -> (bool, bool) { - if !enable_gpucodec_option() { + if !enable_vram_option() { return (false, false); } let v = get_available_config().map(|c| c.d).unwrap_or_default(); ( - v.iter() - .any(|d| d.data_format == gpu_common::DataFormat::H264), - v.iter() - .any(|d| d.data_format == gpu_common::DataFormat::H265), + v.iter().any(|d| d.data_format == DataFormat::H264), + v.iter().any(|d| d.data_format == DataFormat::H265), ) } @@ -346,7 +332,7 @@ impl GpuDecoder { match Decoder::new(ctx) { Ok(decoder) => Ok(Self { decoder }), Err(_) => { - hbb_common::config::GpucodecConfig::clear(); + hbb_common::config::HwCodecConfig::clear_vram(); Err(anyhow!(format!( "Failed to create decoder, format: {:?}", format @@ -354,33 +340,33 @@ impl GpuDecoder { } } } - pub fn decode(&mut self, data: &[u8]) -> ResultType> { + pub fn decode(&mut self, data: &[u8]) -> ResultType> { match self.decoder.decode(data) { - Ok(v) => Ok(v.iter().map(|f| GpuDecoderImage { frame: f }).collect()), + Ok(v) => Ok(v.iter().map(|f| VRamDecoderImage { frame: f }).collect()), Err(e) => Err(anyhow!(e)), } } } -pub struct GpuDecoderImage<'a> { +pub struct VRamDecoderImage<'a> { pub frame: &'a DecodeFrame, } -impl GpuDecoderImage<'_> {} +impl VRamDecoderImage<'_> {} fn get_available_config() -> ResultType { - let available = hbb_common::config::GpucodecConfig::load().available; + let available = hbb_common::config::HwCodecConfig::load().vram; match Available::deserialize(&available) { Ok(v) => Ok(v), Err(_) => Err(anyhow!("Failed to deserialize:{}", available)), } } -pub fn check_available_gpucodec() { +pub(crate) fn check_available_vram() -> String { let d = DynamicContext { device: None, - width: 1920, - height: 1080, + width: 1280, + height: 720, kbitrate: 5000, framerate: 60, gop: MAX_GOP as _, @@ -391,54 +377,5 @@ pub fn check_available_gpucodec() { e: encoders, d: decoders, }; - - if let Ok(available) = available.serialize() { - let mut config = hbb_common::config::GpucodecConfig::load(); - config.available = available; - config.store(); - return; - } - log::error!("Failed to serialize gpucodec"); -} - -pub fn gpucodec_new_check_process() { - use std::sync::Once; - - static ONCE: Once = Once::new(); - ONCE.call_once(|| { - std::thread::spawn(move || { - // Remove to avoid checking process errors - // But when the program is just started, the configuration file has not been updated, and the new connection will read an empty configuration - hbb_common::config::GpucodecConfig::clear(); - if let Ok(exe) = std::env::current_exe() { - let arg = "--check-gpucodec-config"; - if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() { - // wait up to 30 seconds - 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 gpucodec config, exit with: {status}") - } - Ok(None) => { - log::info!( - "Check gpucodec config, status not ready yet, let's really wait" - ); - let res = child.wait(); - log::info!("Check gpucodec config, wait result: {res:?}"); - } - Err(e) => { - log::error!("Check gpucodec config, error attempting to wait: {e}") - } - } - } - }; - }); - }); + available.serialize().unwrap_or_default() } diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 6f1b3411e..abd1f5026 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -185,7 +185,7 @@ impl Capturer { self.gdi_capturer.take(); } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] pub fn set_output_texture(&mut self, texture: bool) { self.output_texture = texture; } @@ -620,7 +620,7 @@ impl Display { ) } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] pub fn adapter_luid(&self) -> Option { unsafe { if !self.adapter.is_null() { diff --git a/res/devices.py b/res/devices.py new file mode 100755 index 000000000..246a432df --- /dev/null +++ b/res/devices.py @@ -0,0 +1,143 @@ +#!/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, + 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, + } + + 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 = 1 + + while True: + params["current"] = current + response = requests.get(f"{url}/api/devices", headers=headers, params=params) + response_json = response.json() + + 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"], "%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) + current += pageSize + if len(data) < pageSize or current > total: + break + + return devices + + +def check(response): + if response.status_code == 200: + try: + response_json = response.json() + return response_json + except ValueError: + return response.text or "Success" + else: + return "Failed", response.status_code, response.text + + +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 main(): + parser = argparse.ArgumentParser(description="Device manager") + parser.add_argument( + "command", + choices=["view", "disable", "enable", "delete"], + 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="Group name") + parser.add_argument( + "--offline_days", type=int, help="Offline duration in days, e.g., 7" + ) + + args = parser.parse_args() + + devices = view( + args.url, + args.token, + args.id, + args.device_name, + args.user_name, + args.group_name, + args.offline_days, + ) + + if args.command == "view": + for device in devices: + print(device) + elif 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) + + +if __name__ == "__main__": + main() diff --git a/res/msi/.gitignore b/res/msi/.gitignore index a2df938b4..44c377c55 100644 --- a/res/msi/.gitignore +++ b/res/msi/.gitignore @@ -2,3 +2,10 @@ **/bin **/obj + +x64 +packages + +CustomActions/x64 +CustomActions/*.user +CustomActions/*.filters diff --git a/res/msi/CustomActions/Common.h b/res/msi/CustomActions/Common.h new file mode 100644 index 000000000..e01262f00 --- /dev/null +++ b/res/msi/CustomActions/Common.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +bool AddFirewallRule(bool add, LPWSTR exeName, LPWSTR exeFile); + +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); diff --git a/res/msi/CustomActions/CustomAction.config b/res/msi/CustomActions/CustomAction.config deleted file mode 100644 index cfae001eb..000000000 --- a/res/msi/CustomActions/CustomAction.config +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/res/msi/CustomActions/CustomAction.cs b/res/msi/CustomActions/CustomAction.cs deleted file mode 100644 index cd048f95b..000000000 --- a/res/msi/CustomActions/CustomAction.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Diagnostics; -using System.Runtime.InteropServices; -using WixToolset.Dtf.WindowsInstaller; - -namespace CustomActions -{ - public class CustomActions - { - [CustomAction] - public static ActionResult CustomActionHello(Session session) - { - try - { - session.Log("================= Example CustomAction Hello"); - return ActionResult.Success; - } - catch (Exception e) - { - session.Log("An error occurred: " + e.Message); - return ActionResult.Failure; - } - } - - [CustomAction] - public static ActionResult RunCommandAsSystem(Session session) - { - try - { - ProcessStartInfo psi = new ProcessStartInfo - { - - FileName = "cmd.exe", - Arguments = "/c " + session["CMD"], - UseShellExecute = false, - WindowStyle = ProcessWindowStyle.Hidden, - Verb = "runas" - }; - - using (Process process = Process.Start(psi)) - { - process.WaitForExit(); - } - - return ActionResult.Success; - } - catch (Exception e) - { - session.Log("An error occurred: " + e.Message); - return ActionResult.Failure; - } - } - } -} diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp new file mode 100644 index 000000000..bea7c0525 --- /dev/null +++ b/res/msi/CustomActions/CustomActions.cpp @@ -0,0 +1,611 @@ +// 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); +} + +UINT __stdcall RemoveInstallFolder( + __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, "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); + + SHFILEOPSTRUCTW fileOp; + ZeroMemory(&fileOp, sizeof(SHFILEOPSTRUCT)); + + fileOp.wFunc = FO_DELETE; + fileOp.pFrom = installFolder; + fileOp.fFlags = FOF_NOCONFIRMATION | FOF_SILENT; + + nResult = SHFileOperationW(&fileOp); + if (nResult == 0) + { + WcaLog(LOGMSG_STANDARD, "The directory \"%ls\" has been deleted.", installFolder); + } + else + { + WcaLog(LOGMSG_STANDARD, "The directory \"%ls\" has not been deleted, error code: 0X%02X. Please refer to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationa for the error codes.", installFolder, nResult); + } + +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) + { + WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length); + if (commandLine != NULL) + { + 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 (hr < 0) { + 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 (hr < 0) { + 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 (hr < 0) { + 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 (hr < 0) { + 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); +} + +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); + } + +LExit: + if (pwzData) { + ReleaseStr(pwzData); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +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]); + + 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; + } + } + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stopped", svcName); + } + else { + WcaLog(LOGMSG_STANDARD, "Failed to stop service: \"%ls\"", svcName); + } + + if (MyDeleteServiceW(svcName)) { + WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is deleted", svcName); + } + else { + WcaLog(LOGMSG_STANDARD, "Failed to delete service: \"%ls\"", svcName); + } + + // It's really strange that we need sleep here. + // But the upgrading may be stucked 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; + + BOOL rebootRequired = FALSE; + + hr = WcaInitialize(hInstall, "RemoveAmyuniIdd"); + ExitOnFailure(hr, "Failed to initialize"); + + UninstallDriver(L"usbmmidd", rebootRequired); + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} diff --git a/res/msi/CustomActions/CustomActions.csproj b/res/msi/CustomActions/CustomActions.csproj deleted file mode 100644 index e21734032..000000000 --- a/res/msi/CustomActions/CustomActions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - net472 - Release - - - - - - - - - - - diff --git a/res/msi/CustomActions/CustomActions.def b/res/msi/CustomActions/CustomActions.def new file mode 100644 index 000000000..557bfaf18 --- /dev/null +++ b/res/msi/CustomActions/CustomActions.def @@ -0,0 +1,14 @@ +LIBRARY "CustomActions" + +EXPORTS + CustomActionHello + RemoveInstallFolder + TerminateProcesses + AddFirewallRules + SetPropertyIsServiceRunning + TryStopDeleteService + CreateStartService + TryDeleteStartupShortcut + SetPropertyFromConfig + AddRegSoftwareSASGeneration + RemoveAmyuniIdd diff --git a/res/msi/CustomActions/CustomActions.vcxproj b/res/msi/CustomActions/CustomActions.vcxproj new file mode 100644 index 000000000..1bff7b154 --- /dev/null +++ b/res/msi/CustomActions/CustomActions.vcxproj @@ -0,0 +1,85 @@ + + + + + + + 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/res/msi/CustomActions/DeviceUtils.cpp b/res/msi/CustomActions/DeviceUtils.cpp new file mode 100644 index 000000000..5bc6c10bc --- /dev/null +++ b/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/res/msi/CustomActions/FirewallRules.cpp b/res/msi/CustomActions/FirewallRules.cpp new file mode 100644 index 000000000..bca2739b8 --- /dev/null +++ b/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/res/msi/CustomActions/ReadConfig.cpp b/res/msi/CustomActions/ReadConfig.cpp new file mode 100644 index 000000000..695ebcada --- /dev/null +++ b/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/res/msi/CustomActions/ServiceUtils.cpp b/res/msi/CustomActions/ServiceUtils.cpp new file mode 100644 index 000000000..234f70371 --- /dev/null +++ b/res/msi/CustomActions/ServiceUtils.cpp @@ -0,0 +1,173 @@ +// 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"); + return false; + } + + SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_STOP | DELETE); + 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, "Stopping service: %ls", serviceName); + } + + bool success = DeleteService(hService); + if (!success) { + WcaLog(LOGMSG_STANDARD, "Failed to delete service: %ls", serviceName); + } + + 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"); + return false; + } + + SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_START); + if (hService == NULL) { + WcaLog(LOGMSG_STANDARD, "Failed to open service: %ls", serviceName); + CloseServiceHandle(hSCManager); + return false; + } + + bool success = StartServiceW(hService, 0, NULL); + if (!success) { + WcaLog(LOGMSG_STANDARD, "Failed to start service: %ls", serviceName); + } + + 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 IsServiceRunningW(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_QUERY_STATUS); + if (hService == NULL) { + WcaLog(LOGMSG_STANDARD, "Failed to open service: %ls", serviceName); + CloseServiceHandle(hSCManager); + return false; + } + + SERVICE_STATUS_PROCESS serviceStatus; + DWORD bytesNeeded; + if (!QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, reinterpret_cast(&serviceStatus), sizeof(serviceStatus), &bytesNeeded)) { + WcaLog(LOGMSG_STANDARD, "Failed to query service: %ls", serviceName); + CloseServiceHandle(hService); + CloseServiceHandle(hSCManager); + return false; + } + + bool isRunning = (serviceStatus.dwCurrentState == SERVICE_RUNNING); + + CloseServiceHandle(hService); + CloseServiceHandle(hSCManager); + + return isRunning; +} diff --git a/res/msi/CustomActions/dllmain.cpp b/res/msi/CustomActions/dllmain.cpp new file mode 100644 index 000000000..7288d7c64 --- /dev/null +++ b/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/res/msi/CustomActions/framework.h b/res/msi/CustomActions/framework.h new file mode 100644 index 000000000..4cd0bc474 --- /dev/null +++ b/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/res/msi/CustomActions/packages.config b/res/msi/CustomActions/packages.config new file mode 100644 index 000000000..e25f8328a --- /dev/null +++ b/res/msi/CustomActions/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/msi/CustomActions/pch.cpp b/res/msi/CustomActions/pch.cpp new file mode 100644 index 000000000..64b7eef6d --- /dev/null +++ b/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/res/msi/CustomActions/pch.h b/res/msi/CustomActions/pch.h new file mode 100644 index 000000000..885d5d62e --- /dev/null +++ b/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/res/msi/Package/Components/Regs.wxs b/res/msi/Package/Components/Regs.wxs index 4aec900df..23d4b6b8d 100644 --- a/res/msi/Package/Components/Regs.wxs +++ b/res/msi/Package/Components/Regs.wxs @@ -14,7 +14,7 @@ - + @@ -22,7 +22,7 @@ - + @@ -36,10 +36,20 @@ - + + + + + + + + + + + + - diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index 4abf20bdf..a79f870b8 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -5,87 +5,125 @@ - - - - - - + + + - - - - - + + + + + + + + + + + + + + + + + - + + + - + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + - + Fix ICE 38 by adding a dummy registry key that is the key for this shortcut. + https://learn.microsoft.com/en-us/windows/win32/msi/ice38 + --> + - - - - + + + - - - - + + + + + + + + - + - + - + - + + diff --git a/res/msi/Package/Fragments/AddRemoveProperties.wxs b/res/msi/Package/Fragments/AddRemoveProperties.wxs index 3c0745144..f93852867 100644 --- a/res/msi/Package/Fragments/AddRemoveProperties.wxs +++ b/res/msi/Package/Fragments/AddRemoveProperties.wxs @@ -1,30 +1,32 @@ - + + + + - + Support entries shown when clicking "Click here for support information" + in Control Panel's Add/Remove Programs https://learn.microsoft.com/en-us/windows/win32/msi/property-reference + --> + - - - - - - + + + + + diff --git a/res/msi/Package/Fragments/CustomActions.wxs b/res/msi/Package/Fragments/CustomActions.wxs index e7759a1a6..b443eff52 100644 --- a/res/msi/Package/Fragments/CustomActions.wxs +++ b/res/msi/Package/Fragments/CustomActions.wxs @@ -1,26 +1,21 @@  - - - - + + - - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/res/msi/Package/Fragments/ShortcutProperties.wxs b/res/msi/Package/Fragments/ShortcutProperties.wxs index 93af85423..e800262fb 100644 --- a/res/msi/Package/Fragments/ShortcutProperties.wxs +++ b/res/msi/Package/Fragments/ShortcutProperties.wxs @@ -1,51 +1,52 @@  - + - + - + - - - + + + + - - - - - - - + + + + + + + - - - - - - - - - + + + + + + + + + - - - + + + - - - + + + - - - + + + - + diff --git a/res/msi/Package/Fragments/Upgrades.wxs b/res/msi/Package/Fragments/Upgrades.wxs index 95034f262..8109efd9c 100644 --- a/res/msi/Package/Fragments/Upgrades.wxs +++ b/res/msi/Package/Fragments/Upgrades.wxs @@ -1,11 +1,10 @@  - + - + - - - + + - + diff --git a/res/msi/Package/Includes.wxi b/res/msi/Package/Includes.wxi index a0e537812..4b43f74c5 100644 --- a/res/msi/Package/Includes.wxi +++ b/res/msi/Package/Includes.wxi @@ -4,7 +4,4 @@ - - - diff --git a/res/msi/Package/Language/Package.en-us.wxl b/res/msi/Package/Language/Package.en-us.wxl index 92f76c106..517bde2af 100644 --- a/res/msi/Package/Language/Package.en-us.wxl +++ b/res/msi/Package/Language/Package.en-us.wxl @@ -47,6 +47,6 @@ This file contains the declaration of all the localizable strings. - + diff --git a/res/msi/Package/Package.wixproj b/res/msi/Package/Package.wixproj index 7500619b1..17dbc4f13 100644 --- a/res/msi/Package/Package.wixproj +++ b/res/msi/Package/Package.wixproj @@ -17,6 +17,6 @@ - + \ No newline at end of file diff --git a/res/msi/Package/Package.wxs b/res/msi/Package/Package.wxs index d9f7d125a..8361e2a12 100644 --- a/res/msi/Package/Package.wxs +++ b/res/msi/Package/Package.wxs @@ -4,12 +4,12 @@ - + - + @@ -22,39 +22,20 @@ - - - + - - - - - - - - - - - - - - - - - - + + + + - - - - + @@ -64,8 +45,13 @@ + + + + + diff --git a/res/msi/Package/UI/AnotherApp.wxs b/res/msi/Package/UI/AnotherApp.wxs index fdf184e6d..ea46812f7 100644 --- a/res/msi/Package/UI/AnotherApp.wxs +++ b/res/msi/Package/UI/AnotherApp.wxs @@ -1,15 +1,15 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/res/msi/README.md b/res/msi/README.md index 79ddbd8ad..5ff2a1080 100644 --- a/res/msi/README.md +++ b/res/msi/README.md @@ -4,14 +4,41 @@ Use Visual Studio 2022 to compile this project. This project is mainly derived from . +## Steps + +1. `python preprocess.py`, see `python preprocess.py -h` for help. +2. Build the .sln solution. + +Run `msiexec /i package.msi /l*v install.log` to record the log. + +## Usage + +1. Put the custom dialog bitmaps in "Resources" directory. The supported bitmaps are `['WixUIBannerBmp', 'WixUIDialogBmp', 'WixUIExclamationIco', 'WixUIInfoIco', 'WixUINewIco', 'WixUIUpIco']`. + +## Knowledge + +### properties + +[wix-toolset-set-custom-action-run-only-on-uninstall](https://www.advancedinstaller.com/versus/wix-toolset/wix-toolset-set-custom-action-run-only-on-uninstall.html) + +| Property Name | Install | Uninstall | Change | Repair | Upgrade | +| ------ | ------ | ------ | ------ | ------ | ------ | +| Installed | False | True | True | True | True | +| REINSTALL | False | False | False | True | False | +| UPGRADINGPRODUCTCODE | False | False | False | False | True | +| REMOVE | False | True | False | False | True | + ## TODOs -1. tray, uninstall shortcut -1. launch client after installation -1. github ci -1. options +1. Start menu. Uninstall +1. custom options 1. Custom client. 1. firewall and tcp allow. Outgoing - 1. Custom icon. Current `Resources/icon.ico`. 1. Show license ? 1. Do create service. Outgoing. + +## Refs + +1. [windows-installer-portal](https://learn.microsoft.com/en-us/windows/win32/Msi/windows-installer-portal) +1. [wxs](https://wixtoolset.org/docs/schema/wxs/) +1. [wxs github](https://github.com/wixtoolset/wix) diff --git a/res/msi/msi.sln b/res/msi/msi.sln index 5a8303f4d..70d28fb86 100644 --- a/res/msi/msi.sln +++ b/res/msi/msi.sln @@ -4,34 +4,18 @@ Microsoft Visual Studio Solution File, Format Version 12.00 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}" - ProjectSection(ProjectDependencies) = postProject - {95BE171E-6438-4F45-9876-0B667D9F7830} = {95BE171E-6438-4F45-9876-0B667D9F7830} - EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomActions", "CustomActions\CustomActions.csproj", "{95BE171E-6438-4F45-9876-0B667D9F7830}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CustomActions", "CustomActions\CustomActions.vcxproj", "{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F403A403-CEFF-4399-B51C-CC646C8E98CF}.Debug|Any CPU.ActiveCfg = Release|x64 - {F403A403-CEFF-4399-B51C-CC646C8E98CF}.Debug|Any CPU.Build.0 = Release|x64 - {F403A403-CEFF-4399-B51C-CC646C8E98CF}.Debug|x64.ActiveCfg = Release|x64 - {F403A403-CEFF-4399-B51C-CC646C8E98CF}.Debug|x64.Build.0 = Release|x64 - {F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|Any CPU.ActiveCfg = Release|x64 - {F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|Any CPU.Build.0 = Release|x64 {F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|x64.ActiveCfg = Release|x64 {F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|x64.Build.0 = Release|x64 - {95BE171E-6438-4F45-9876-0B667D9F7830}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {95BE171E-6438-4F45-9876-0B667D9F7830}.Debug|x64.ActiveCfg = Release|Any CPU - {95BE171E-6438-4F45-9876-0B667D9F7830}.Release|Any CPU.ActiveCfg = Release|Any CPU - {95BE171E-6438-4F45-9876-0B667D9F7830}.Release|Any CPU.Build.0 = Release|Any CPU - {95BE171E-6438-4F45-9876-0B667D9F7830}.Release|x64.ActiveCfg = Release|Any CPU - {95BE171E-6438-4F45-9876-0B667D9F7830}.Release|x64.Build.0 = Release|Any CPU + {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 diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py index cc1031b88..1d85efe21 100644 --- a/res/msi/preprocess.py +++ b/res/msi/preprocess.py @@ -1,69 +1,111 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import json import sys import uuid import argparse +import datetime +import subprocess +import re from pathlib import Path 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/fufesou/rustdesk", + }, +} def make_parser(): parser = argparse.ArgumentParser(description="Msi preprocess script.") parser.add_argument( - "-d", "--debug", action="store_true", help="Is debug", default=False + "-d", + "--dist-dir", + type=str, + default="../../rustdesk", + help="The dist direcotry 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( - "-an", "--app-name", type=str, default="RustDesk", help="The app name." + "--app-name", type=str, default="RustDesk", help="The app name." ) parser.add_argument( - "-v", "--version", type=str, default="1.2.4", help="The app version." + "-v", "--version", type=str, default="", help="The app version." ) parser.add_argument( "-m", "--manufacturer", type=str, - default="Purslane Ltd", + default="PURSLANE", help="The app manufacturer.", ) return parser -def read_lines_and_start_index(file_path, start_tag, end_tag): +def read_lines_and_start_index(file_path, tag_start, tag_end): with open(file_path, "r") as f: lines = f.readlines() - start_index = -1 - end_index = -1 + index_start = -1 + index_end = -1 for i, line in enumerate(lines): - if start_tag in line: - start_index = i - if end_tag in line: - end_index = i + if tag_start in line: + index_start = i + if tag_end in line: + index_end = i - if start_index == -1 or end_index == -1: - print("Error: start or end tag not found") + if index_start == -1: + print(f'Error: start tag "{tag_start}" not found') return None, None - return lines, start_index + 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, start_index, app_name, build_dir): +def insert_components_between_tags(lines, index_start, app_name, dist_dir): indent = g_indent_unit * 3 - path = Path(build_dir) + 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 - relative_file_path = file_path.relative_to(path) - guid = uuid.uuid5( - uuid.NAMESPACE_OID, app_name + "/" + str(relative_file_path) - ) - subdir = str(file_path.parent.relative_to(path)) dir_attr = "" if subdir != ".": @@ -73,67 +115,59 @@ def insert_components_between_tags(lines, start_index, app_name, build_dir): # because it will cause error # "Error WIX0130 The primary key 'xxxx' is duplicated in table 'Directory'" to_insert_lines = f""" -{indent} +{indent} {indent}{g_indent_unit} {indent} """ - lines.insert(start_index + 1, to_insert_lines[1:]) - start_index += 1 + lines.insert(index_start + 1, to_insert_lines[1:]) + index_start += 1 idx += 1 return True -def gen_auto_component(app_name, build_dir): - target_file = Path(sys.argv[0]).parent.joinpath("Package/Components/RustDesk.wxs") - start_tag = "" - end_tag = "" - - lines, start_index = read_lines_and_start_index(target_file, start_tag, end_tag) - if lines is None: - return False - - if not insert_components_between_tags(lines, start_index, app_name, build_dir): - return False - - with open(target_file, "w") as f: - f.writelines(lines) - - 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, build_dir): - target_file = Path(sys.argv[0]).parent.joinpath("Package/Includes.wxi") - start_tag = "" - end_tag = "" +def gen_pre_vars(args, dist_dir): + def func(lines, index_start): + upgrade_code = uuid.uuid5(uuid.NAMESPACE_OID, app_name + ".exe") - lines, start_index = read_lines_and_start_index(target_file, start_tag, end_tag) - if lines is None: - return False + 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', + ] - 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', - ] + for i, line in enumerate(to_insert_lines): + lines.insert(index_start + i + 1, line) + return lines - for i, line in enumerate(to_insert_lines): - lines.insert(start_index + i + 1, line) - - with open(target_file, "w") as f: - f.writelines(lines) - - return True + return gen_content_between_tags( + "Package/Includes.wxi", "", "", func + ) -def replace_app_name_in_lans(app_name): +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("*.wxs"): + for file_path in langs_dir.glob("*.wxl"): with open(file_path, "r") as f: lines = f.readlines() for i, line in enumerate(lines): @@ -142,23 +176,295 @@ def replace_app_name_in_lans(app_name): 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' + ) + + estimated_size = get_folder_size(dist_dir) + 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_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") as f: + f.writelines(lines) + + return True + + +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 + + 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 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") 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") as f: + f.writelines(lines) + + if __name__ == "__main__": parser = make_parser() args = parser.parse_args() app_name = args.app_name - build_dir = ( - Path(sys.argv[0]) - .parent.joinpath( - f'../../flutter/build/windows/x64/runner/{"Debug" if args.debug else "Release"}' - ) - .resolve() - ) + dist_dir = Path(sys.argv[0]).parent.joinpath(args.dist_dir).resolve() - if not gen_pre_vars(args, build_dir): + if not init_global_vars(dist_dir, app_name, args): sys.exit(-1) - if not gen_auto_component(app_name, build_dir): + if not gen_pre_vars(args, dist_dir): sys.exit(-1) - replace_app_name_in_lans(args.app_name) + 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_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) diff --git a/src/client.rs b/src/client.rs index 4d11049c4..4fa2ff062 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1038,9 +1038,9 @@ pub struct VideoHandler { impl VideoHandler { /// Create a new video handler. pub fn new(format: CodecFormat, _display: usize) -> Self { - #[cfg(all(feature = "gpucodec", feature = "flutter"))] + #[cfg(all(feature = "vram", feature = "flutter"))] let luid = crate::flutter::get_adapter_luid(); - #[cfg(not(all(feature = "gpucodec", feature = "flutter")))] + #[cfg(not(all(feature = "vram", feature = "flutter")))] let luid = Default::default(); log::info!("new video handler for display #{_display}, format: {format:?}, luid: {luid:?}"); VideoHandler { @@ -1097,9 +1097,9 @@ impl VideoHandler { /// Reset the decoder, change format if it is Some pub fn reset(&mut self, format: Option) { - #[cfg(all(feature = "flutter", feature = "gpucodec"))] + #[cfg(all(feature = "flutter", feature = "vram"))] let luid = crate::flutter::get_adapter_luid(); - #[cfg(not(all(feature = "flutter", feature = "gpucodec")))] + #[cfg(not(all(feature = "flutter", feature = "vram")))] let luid = None; let format = format.unwrap_or(self.decoder.format()); self.decoder = Decoder::new(format, luid); @@ -1564,22 +1564,13 @@ impl LoginConfigHandler { /// /// * `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) { + 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 n = 0; let mut msg = OptionMessage::new(); - // Version 1.2.5 can remove this, and OptionMessage is not needed for file transfer - msg.support_windows_specific_session = BoolOption::Yes.into(); - n += 1; - - if self.conn_type.eq(&ConnType::FILE_TRANSFER) { - return Some(msg); - } let q = self.image_quality.clone(); if let Some(q) = self.get_image_quality_enum(&q, ignore_default) { msg.image_quality = q.into(); - n += 1; } else if q == "custom" { let config = self.load_config(); let allow_more = !crate::using_public_server() || self.direct == Some(true); @@ -1602,32 +1593,25 @@ impl LoginConfigHandler { msg.custom_fps = custom_fps; *self.custom_fps.lock().unwrap() = Some(custom_fps as _); } - n += 1; } let view_only = self.get_toggle_option("view-only"); if view_only { msg.disable_keyboard = BoolOption::Yes.into(); - n += 1; } if view_only || self.get_toggle_option("show-remote-cursor") { msg.show_remote_cursor = BoolOption::Yes.into(); - n += 1; } if !view_only && self.get_toggle_option("lock-after-session-end") { msg.lock_after_session_end = BoolOption::Yes.into(); - n += 1; } if self.get_toggle_option("disable-audio") { msg.disable_audio = BoolOption::Yes.into(); - n += 1; } if !view_only && self.get_toggle_option("enable-file-transfer") { msg.enable_file_transfer = BoolOption::Yes.into(); - n += 1; } if view_only || self.get_toggle_option("disable-clipboard") { msg.disable_clipboard = BoolOption::Yes.into(); - n += 1; } msg.supported_decoding = hbb_common::protobuf::MessageField::some(Decoder::supported_decodings( @@ -1636,12 +1620,7 @@ impl LoginConfigHandler { self.adapter_luid, &self.mark_unsupported, )); - n += 1; - if n > 0 { - Some(msg) - } else { - None - } + Some(msg) } pub fn get_option_message_after_login(&self) -> Option { diff --git a/src/common.rs b/src/common.rs index ce393fe35..41b872afc 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1304,8 +1304,14 @@ pub async fn get_next_nonkeyexchange_msg( None } +#[allow(unused_mut)] #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn check_process(arg: &str, same_uid: bool) -> bool { +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(); @@ -1332,8 +1338,13 @@ pub fn check_process(arg: &str, same_uid: bool) -> bool { 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 == parg { + if arg.is_empty() { + if !parg.starts_with("--") { + return true; + } + } else if arg == parg { return true; } } diff --git a/src/core_main.rs b/src/core_main.rs index caac2ca99..5055626cd 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -114,9 +114,14 @@ pub fn core_main() -> Option> { if args.contains(&"--noinstall".to_string()) { args.clear(); } - if args.len() > 0 && args[0] == "--version" { - println!("{}", crate::VERSION); - return None; + 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)] { @@ -208,34 +213,16 @@ pub fn core_main() -> Option> { .show() .ok(); return None; - } else if args[0] == "--install-cert" { - #[cfg(windows)] - hbb_common::allow_err!(crate::platform::windows::install_cert( - crate::platform::windows::DRIVER_CERT_FILE - )); - if args.len() > 1 && args[1] == "silent" { - return None; - } - #[cfg(all(windows, feature = "virtual_display_driver"))] - if crate::virtual_display_manager::is_virtual_display_supported() { - hbb_common::allow_err!(crate::virtual_display_manager::install_update_driver()); - } - 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)] - { - // It's ok to install cert multiple times. - hbb_common::allow_err!(crate::platform::windows::install_cert( - crate::platform::windows::DRIVER_CERT_FILE - )); - } #[cfg(all(windows, feature = "virtual_display_driver"))] if crate::virtual_display_manager::is_virtual_display_supported() { - hbb_common::allow_err!(crate::virtual_display_manager::install_update_driver()); + hbb_common::allow_err!( + crate::virtual_display_manager::rustdesk_idd::install_update_driver() + ); } return None; } else if args[0] == "--portable-service" { @@ -245,6 +232,12 @@ pub fn core_main() -> Option> { _is_run_as_system, ); return None; + } else if args[0] == "--uninstall-amyuni-idd" { + #[cfg(all(windows, feature = "virtual_display_driver"))] + hbb_common::allow_err!( + crate::virtual_display_manager::amyuni_idd::uninstall_driver() + ); + return None; } } if args[0] == "--remove" { @@ -274,7 +267,7 @@ pub fn core_main() -> Option> { } else if args[0] == "--server" { log::info!("start --server with user {}", crate::username()); #[cfg(all(windows, feature = "virtual_display_driver"))] - crate::privacy_mode::restore_reg_connectivity(); + crate::privacy_mode::restore_reg_connectivity(true); #[cfg(any(target_os = "linux", target_os = "windows"))] { crate::start_server(true); @@ -430,10 +423,6 @@ pub fn core_main() -> Option> { #[cfg(feature = "hwcodec")] scrap::hwcodec::check_available_hwcodec(); return None; - } else if args[0] == "--check-gpucodec-config" { - #[cfg(feature = "gpucodec")] - scrap::gpucodec::check_available_gpucodec(); - return None; } else if args[0] == "--cm" { // call connection manager to establish connections // meanwhile, return true to call flutter window to show control panel diff --git a/src/flutter.rs b/src/flutter.rs index ca4057d36..f86755952 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -4,7 +4,7 @@ use crate::{ ui_session_interface::{io_loop, InvokeUiSession, Session}, }; use flutter_rust_bridge::StreamSink; -#[cfg(any(feature = "flutter_texture_render", feature = "gpucodec"))] +#[cfg(any(feature = "flutter_texture_render", feature = "vram"))] use hbb_common::dlopen::{ symbor::{Library, Symbol}, Error as LibError, @@ -16,7 +16,7 @@ use hbb_common::{ use serde::Serialize; use serde_json::json; -#[cfg(any(feature = "flutter_texture_render", feature = "gpucodec"))] +#[cfg(any(feature = "flutter_texture_render", feature = "vram"))] use std::os::raw::c_void; use std::{ @@ -63,7 +63,7 @@ lazy_static::lazy_static! { pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open_self(); } -#[cfg(all(target_os = "windows", feature = "gpucodec"))] +#[cfg(all(target_os = "windows", feature = "vram"))] lazy_static::lazy_static! { pub static ref TEXTURE_GPU_RENDERER_PLUGIN: Result = Library::open("flutter_gpu_texture_renderer_plugin.dll"); } @@ -168,15 +168,15 @@ pub unsafe extern "C" fn get_rustdesk_app_name(buffer: *mut u16, length: i32) -> #[derive(Default)] struct SessionHandler { event_stream: Option>, - #[cfg(any(feature = "flutter_texture_render", feature = "gpucodec"))] + #[cfg(any(feature = "flutter_texture_render", feature = "vram"))] renderer: VideoRenderer, } -#[cfg(any(feature = "flutter_texture_render", feature = "gpucodec"))] +#[cfg(any(feature = "flutter_texture_render", feature = "vram"))] #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum RenderType { PixelBuffer, - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] Texture, } @@ -214,41 +214,41 @@ pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn( dst_rgba_stride: c_int, ); -#[cfg(feature = "gpucodec")] +#[cfg(feature = "vram")] pub type FlutterGpuTextureRendererPluginCApiSetTexture = unsafe extern "C" fn(output: *mut c_void, texture: *mut c_void); -#[cfg(feature = "gpucodec")] +#[cfg(feature = "vram")] pub type FlutterGpuTextureRendererPluginCApiGetAdapterLuid = unsafe extern "C" fn() -> i64; #[cfg(feature = "flutter_texture_render")] pub(super) type TextureRgbaPtr = usize; -#[cfg(any(feature = "flutter_texture_render", feature = "gpucodec"))] +#[cfg(any(feature = "flutter_texture_render", feature = "vram"))] struct DisplaySessionInfo { // TextureRgba pointer in flutter native. #[cfg(feature = "flutter_texture_render")] texture_rgba_ptr: TextureRgbaPtr, #[cfg(feature = "flutter_texture_render")] size: (usize, usize), - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] gpu_output_ptr: usize, notify_render_type: Option, } // Video Texture Renderer in Flutter -#[cfg(any(feature = "flutter_texture_render", feature = "gpucodec"))] +#[cfg(any(feature = "flutter_texture_render", feature = "vram"))] #[derive(Clone)] struct VideoRenderer { is_support_multi_ui_session: bool, map_display_sessions: Arc>>, #[cfg(feature = "flutter_texture_render")] on_rgba_func: Option>, - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] on_texture_func: Option>, } -#[cfg(any(feature = "flutter_texture_render", feature = "gpucodec"))] +#[cfg(any(feature = "flutter_texture_render", feature = "vram"))] impl Default for VideoRenderer { fn default() -> Self { #[cfg(feature = "flutter_texture_render")] @@ -270,7 +270,7 @@ impl Default for VideoRenderer { None } }; - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] let on_texture_func = match &*TEXTURE_GPU_RENDERER_PLUGIN { Ok(lib) => { let find_sym_res = unsafe { @@ -297,13 +297,13 @@ impl Default for VideoRenderer { is_support_multi_ui_session: false, #[cfg(feature = "flutter_texture_render")] on_rgba_func, - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] on_texture_func, } } } -#[cfg(any(feature = "flutter_texture_render", feature = "gpucodec"))] +#[cfg(any(feature = "flutter_texture_render", feature = "vram"))] impl VideoRenderer { #[inline] #[cfg(feature = "flutter_texture_render")] @@ -318,7 +318,7 @@ impl VideoRenderer { DisplaySessionInfo { texture_rgba_ptr: usize::default(), size: (width, height), - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] gpu_output_ptr: usize::default(), notify_render_type: None, }, @@ -345,7 +345,7 @@ impl VideoRenderer { DisplaySessionInfo { texture_rgba_ptr: ptr as _, size: (0, 0), - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] gpu_output_ptr: usize::default(), notify_render_type: None, }, @@ -355,7 +355,7 @@ impl VideoRenderer { } } - #[cfg(feature = "gpucodec")] + #[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 { @@ -434,7 +434,7 @@ impl VideoRenderer { } } - #[cfg(feature = "gpucodec")] + #[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 { @@ -793,7 +793,7 @@ impl InvokeUiSession for FlutterHandler { } #[inline] - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn on_texture(&self, display: usize, texture: *mut c_void) { for (_, session) in self.session_handlers.read().unwrap().iter() { if session.renderer.on_texture(display, texture) { @@ -1073,9 +1073,9 @@ pub fn session_add( Some(switch_uuid.to_string()) }; - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] let adapter_luid = get_adapter_luid(); - #[cfg(not(feature = "gpucodec"))] + #[cfg(not(feature = "vram"))] let adapter_luid = None; session.lc.write().unwrap().initialize( @@ -1106,7 +1106,7 @@ pub fn session_start_( ) -> 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 connnection. + // 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() { @@ -1453,7 +1453,7 @@ pub fn session_register_pixelbuffer_texture(_session_id: SessionID, _display: us #[inline] pub fn session_register_gpu_texture(_session_id: SessionID, _display: usize, _output_ptr: usize) { - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] for s in sessions::get_sessions() { if let Some(h) = s .ui_handler @@ -1468,7 +1468,7 @@ pub fn session_register_gpu_texture(_session_id: SessionID, _display: usize, _ou } } -#[cfg(feature = "gpucodec")] +#[cfg(feature = "vram")] pub fn get_adapter_luid() -> Option { let get_adapter_luid_func = match &*TEXTURE_GPU_RENDERER_PLUGIN { Ok(lib) => { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index af086687c..64527898c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -39,11 +39,15 @@ lazy_static::lazy_static! { static ref TEXTURE_RENDER_KEY: Arc = Arc::new(AtomicI32::new(0)); } -fn initialize(app_dir: &str) { +fn initialize(app_dir: &str, custom_client_config: &str) { flutter::async_tasks::start_flutter_async_runner(); *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 - crate::load_custom_client(); + 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. @@ -1282,8 +1286,8 @@ pub fn cm_get_clients_length() -> usize { crate::ui_cm_interface::get_clients_length() } -pub fn main_init(app_dir: String) { - initialize(&app_dir); +pub fn main_init(app_dir: String, custom_client_config: String) { + initialize(&app_dir, &custom_client_config); } pub fn main_device_id(id: String) { @@ -1302,8 +1306,8 @@ pub fn main_has_hwcodec() -> SyncReturn { SyncReturn(has_hwcodec()) } -pub fn main_has_gpucodec() -> SyncReturn { - SyncReturn(has_gpucodec()) +pub fn main_has_vram() -> SyncReturn { + SyncReturn(has_vram()) } pub fn main_supported_hwdecodings() -> SyncReturn { @@ -1789,7 +1793,7 @@ pub fn main_has_file_clipboard() -> SyncReturn { } pub fn main_has_gpu_texture_render() -> SyncReturn { - SyncReturn(cfg!(feature = "gpucodec")) + SyncReturn(cfg!(feature = "vram")) } pub fn cm_init() { @@ -2103,6 +2107,16 @@ pub fn main_get_hard_option(key: String) -> SyncReturn { SyncReturn(get_hard_option(key)) } +pub fn main_check_hwcodec() { + check_hwcodec() +} + +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); + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; @@ -2115,31 +2129,35 @@ pub mod server_side { use crate::start_server; #[no_mangle] - pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startServer( + 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_com_carriez_flutter_1hbb_MainService_startService( - _env: JNIEnv, - _class: JClass, - ) { + 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_com_carriez_flutter_1hbb_MainService_translateLocale( + pub unsafe extern "system" fn Java_ffi_FFI_translateLocale( env: JNIEnv, _class: JClass, locale: JString, @@ -2158,10 +2176,7 @@ pub mod server_side { } #[no_mangle] - pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_refreshScreen( - _env: JNIEnv, - _class: JClass, - ) { + pub unsafe extern "system" fn Java_ffi_FFI_refreshScreen(_env: JNIEnv, _class: JClass) { crate::server::video_service::refresh() } } diff --git a/src/ipc.rs b/src/ipc.rs index 50cc1de47..3ee18be43 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -232,6 +232,7 @@ pub enum Data { #[cfg(windows)] ControlledSessionCount(usize), CmErr(String), + CheckHwcodec, } #[tokio::main(flavor = "current_thread")] @@ -502,6 +503,14 @@ async fn handle(data: Data, stream: &mut Connection) { .await ); } + Data::CheckHwcodec => + { + #[cfg(feature = "hwcodec")] + #[cfg(any(target_os = "windows", target_os = "linux"))] + if crate::platform::is_root() { + scrap::hwcodec::start_check_process(true); + } + } _ => {} } } @@ -929,6 +938,12 @@ pub async fn connect_to_user_session(usid: Option) -> ResultType<()> { 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(test)] mod test { use super::*; diff --git a/src/lang.rs b/src/lang.rs index 25768a0c3..c53ac1c4f 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -16,6 +16,7 @@ mod et; mod fa; mod fr; mod he; +mod hr; mod hu; mod id; mod it; @@ -81,6 +82,7 @@ pub const LANGS: &[(&str, &str)] = &[ ("lv", "Latviešu"), ("ar", "العربية"), ("he", "עברית"), + ("hr", "Hrvatski"), ]; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -152,6 +154,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ar" => ar::T.deref(), "bg" => bg::T.deref(), "he" => he::T.deref(), + "hr" => hr::T.deref(), _ => en::T.deref(), }; let (name, placeholder_value) = extract_placeholder(&name); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 56795e55b..2b3d9faec 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 84cf93f9e..6384083d6 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 7b54f28eb..8cf9738c4 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index ae2b0c83e..25851db83 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -590,7 +590,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("powered_by_me", "由 RustDesk 提供支持"), ("outgoing_only_desk_tip", "当前版本的软件是定制版本。\n您可以连接至其他设备,但是其他设备无法连接至您的设备。"), ("preset_password_warning", "此定制版本附有预设密码。 任何知晓此密码的人都能完全控制您的设备。如果这不是您所预期的,请立即卸载此软件。"), - ("Security Alert", ""), + ("Security Alert", "安全警告"), ("My address book", "我的地址簿"), ("Personal", "个人的"), ("Owner", "所有者"), @@ -602,5 +602,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", "上述的字段為共享且对其他人可见。"), ("Everyone", "所有人"), ("ab_web_console_tip", "打开 Web 控制台以执行更多操作"), + ("allow-only-conn-window-open-tip", "仅当 RustDesk 窗口打开时允许连接"), + ("no_need_privacy_mode_no_physical_displays_tip", "没有物理显示器,没必要使用隐私模式。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d877242f0..10c78f118 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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í."), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 581380e3a..9e28f4b4d 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 19b0c0640..ed97679b2 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 510946bea..8fcb403b9 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 11101c5c6..35e4eff53 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -220,5 +220,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0ddd3d6af..0e38c6d9f 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 383653009..92ebb67e9 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index de859d6ed..7bb359cd8 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 87fb3b844..8cef3ff08 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -92,8 +92,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Refresh File", "به روزرسانی فایل"), ("Local", "محلی"), ("Remote", "از راه دور"), - ("Remote Computer", "سیستم میزبان"), - ("Local Computer", "سیستم راه دور"), + ("Remote Computer", "سیستم راه دور"), + ("Local Computer", "سیسستم محلی"), ("Confirm Delete", "تایید حذف"), ("Delete", "حذف"), ("Properties", "مشخصات"), @@ -112,10 +112,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Waiting", "انتظار"), ("Finished", "تکمیل شد"), ("Speed", "سرعت"), - ("Custom Image Quality", "سفارشی سازی کیفیت تصاویر"), + ("Custom Image Quality", "سفارشی سازی : کیفیت تصاویر"), ("Privacy mode", "حالت حریم خصوصی"), - ("Block user input", "بلاک کردن ورودی کاربر"), - ("Unblock user input", "آنبلاک کردن ورودی کاربر"), + ("Block user input", "بستن ورودی کاربر"), + ("Unblock user input", "بازکردن ورودی کاربر"), ("Adjust Window", "تنظیم پنجره"), ("Original", "اصل"), ("Shrink", "کوچک کردن"), @@ -124,14 +124,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ScrollAuto", "پیمایش/اسکرول خودکار"), ("Good image quality", "کیفیت خوب تصویر"), ("Balanced", "متعادل"), - ("Optimize reaction time", "بهینه سازی زمان واکنش"), + ("Optimize reaction time", "بهینه سازی : زمان واکنش"), ("Custom", "سفارشی"), ("Show remote cursor", "نمایش مکان نما موس میزبان"), ("Show quality monitor", "نمایش کیفیت مانیتور"), ("Disable clipboard", " غیرفعالسازی کلیپبورد"), ("Lock after session end", "قفل کردن حساب کاربری سیستم عامل پس از پایان جلسه"), ("Insert", "افزودن"), - ("Insert Lock", "افزودن قفل"), + ("Insert Lock", "قفل کردن سیستم"), ("Refresh", "تازه سازی"), ("ID does not exist", "شناسه وجود ندارد"), ("Failed to connect to rendezvous server", "اتصال به سرور تولید شناسه انجام نشد"), @@ -353,7 +353,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "سرور"), ("Direct IP Access", "IP دسترسی مستقیم به"), ("Proxy", "پروکسی"), - ("Apply", "ثبت"), + ("Apply", "اعمال تغییرات"), ("Disconnect all devices?", "همه دستگاه ها قطع شوند؟"), ("Clear", "پاک کردن"), ("Audio Input Device", "منبع صدا"), @@ -484,7 +484,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("System Sound", "صدای سیستم"), ("Default", "پیش فرض"), ("New RDP", "ریموت جدید"), - ("Fingerprint", "اثر انگشت"), + ("Fingerprint", "\n اثر انگشت"), ("Copy Fingerprint", "کپی کردن اثر انگشت"), ("no fingerprints", "بدون اثر انگشت"), ("Select a peer", "یک همتا را انتخاب کنید"), @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", "فیلدهای بالا به اشتراک گذاشته شده و برای دیگران قابل مشاهده است"), ("Everyone", "هر کس"), ("ab_web_console_tip", "اطلاعات بیشتر در کنسول وب"), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4fa303da4..4353b5889 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 1ac8aae1f..d9b3d0c54 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs new file mode 100644 index 000000000..def491c52 --- /dev/null +++ b/src/lang/hr.rs @@ -0,0 +1,606 @@ +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 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", "Umetni"), + ("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"), + ("Click to download", "Klik za preuzimanje"), + ("Click to update", "Klik za ažuriranje"), + ("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"), + ("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"), + ("File Connection", "Spajanje preko datoteke"), + ("Screen Connection", "Podijelite vezu"), + ("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"), + ("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"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahtijeva Ubuntu verziju 21.04 ili višu"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahtijeva višu verziju Linux distribucije. Molimo isprobjate X11 ili promijenite OS."), + ("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"), + ("Continue with", "Nastavi sa"), + ("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."), + ("eg: admin", "napr. admin"), + ("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"), + ("elevated_switch_display_msg", "Prijeđite na primarni zaslon jer više zaslona nije podržano u povišenom načinu rada."), + ("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."), + ("switch_display_elevated_connections_tip", "Prebacivanje na zaslon koji nije primarni nije podržan u povišenom načinu rada kada postoji više veza. Ako želite kontrolirati više zaslona, pokušajte ponovno nakon instalacije."), + ("input_source_1_tip", "Ulazni izvor 1"), + ("input_source_2_tip", "Ulazni izvor 2"), + ("capture_display_elevated_connections_tip", "Skeniranje na više zaslona nije podržano u korisničkom načinu rada s povišenim pravima. Ako želite kontrolirati više zaslona, pokušajte ponovno nakon instalacije."), + ("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", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 1e68523ee..6cd18ce80 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 97e6660c4..696e75b81 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index c0594fa7b..bb27633ac 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 la 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."), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index ea9754e47..6d1164115 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index faba24ae3..95c003de6 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index f8e042194..9ef2113cb 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 6e6d040f6..5f21af1c1 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index b1b6a3e00..6e0fd5a84 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 7df5ddaec..e6d6b3e7b 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 687707748..23f7b721a 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 4f1f9c453..9f8124b15 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -69,7 +69,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Retry", "Ponów"), ("OK", "OK"), ("Password Required", "Wymagane jest hasło"), - ("Please enter your password", "Wpisz proszę twoje 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?"), @@ -105,7 +105,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Do this for all conflicts", "Wykonaj dla wszystkich konfliktów"), ("This is irreversible!", "To jest nieodwracalne!"), ("Deleting", "Usuwanie"), ("files", "pliki"), @@ -586,20 +586,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("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", ""), + ("powered_by_me", "Rowiązanie 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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 7bdec3e70..d483a7ed3 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 16f5a8d2b..cf71f8a57 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -504,21 +504,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("clipboard_wait_response_timeout_tip", ""), ("Incoming connection", ""), ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), + ("Exit", "Sair"), + ("Open", "Abrir"), ("logout_tip", ""), - ("Service", ""), - ("Start", ""), - ("Stop", ""), + ("Service", "Serviço"), + ("Start", "Iniciar"), + ("Stop", "Parar"), ("exceed_max_devices", ""), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), + ("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", ""), - ("Change Password", ""), - ("Refresh Password", ""), + ("Change Password", "Alterar senha"), + ("Refresh Password", "Atualizar senha"), ("ID", ""), ("Grid View", ""), ("List View", ""), @@ -527,8 +527,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("pull_ab_failed_tip", ""), ("push_ab_failed_tip", ""), ("synced_peer_readded_tip", ""), - ("Change Color", ""), - ("Primary Color", ""), + ("Change Color", "Alterar cor"), + ("Primary Color", "Cor principal"), ("HSV Color", ""), ("Installation Successful!", ""), ("Installation failed!", ""), @@ -538,7 +538,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("scam_text1", ""), ("scam_text2", ""), ("Don't show again", ""), - ("I Agree", ""), + ("I Agree", "Eu concordo"), ("Decline", ""), ("Timeout in minutes", ""), ("auto_disconnect_option_tip", ""), @@ -547,8 +547,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("upgrade_rustdesk_server_pro_to_{}_tip", ""), ("pull_group_failed_tip", ""), ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), + ("Remove wallpaper during incoming sessions", "Remover papel de parede durante sessão remota"), + ("Test", "Teste"), ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), @@ -556,10 +556,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show displays as individual windows", ""), ("Use all my displays for the remote session", ""), ("selinux_tip", ""), - ("Change view", ""), + ("Change view", "Alterar visualização"), ("Big tiles", ""), ("Small tiles", ""), - ("List", ""), + ("List", "Lista"), ("Virtual display", ""), ("Plug out all", ""), ("True color (4:4:4)", ""), @@ -585,7 +585,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Please select the session you want to connect to", "Por favor, selecione a sessão que você deseja se conectar"), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), ("preset_password_warning", ""), @@ -593,13 +593,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("My address book", ""), ("Personal", ""), ("Owner", ""), - ("Set shared password", ""), + ("Set shared password", "Definir senha compartilhada"), ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), + ("Read-only", "Apenas leitura"), + ("Read/Write", "Leitura/escrita"), + ("Full Control", "Controle total"), ("share_warning_tip", ""), - ("Everyone", ""), + ("Everyone", "Todos"), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 638270182..abf0e078c 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 89b3d4f0f..ab9fcc0b1 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", "Поля выше являются общими и видны другим."), ("Everyone", "Все"), ("ab_web_console_tip", "Больше в веб-консоли"), + ("allow-only-conn-window-open-tip", "Разрешать подключение только при открытом окне RustDesk"), + ("no_need_privacy_mode_no_physical_displays_tip", "Физические дисплеи отсутствуют, нет необходимости использовать режим конфиденциальности."), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 361857832..3c63acdf5 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index d80fce177..6559c0cf1 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 99dd9d534..1a1f9c291 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 90410d00c..18918f645 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 0440869bf..c2a0a73f3 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 8d0035ff9..3a2a007db 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index c1511de08..05285579d 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 32dfeb217..0b2de53d5 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 89a639e86..c7ae7e427 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -589,7 +589,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("powered_by_me", "由 RustDesk 提供支援"), ("outgoing_only_desk_tip", "目前版本的軟體是自定義版本。\n您可以連接至其他設備,但是其他設備無法連接至您的設備。"), ("preset_password_warning", "此客製化版本附有預設密碼。任何知曉此密碼的人都能完全控制您的裝置。如果這不是您所預期的,請立即卸載此軟體。"), - ("Security Alert", ""), + ("Security Alert", "安全警告"), ("My address book", "我的通訊錄"), ("Personal", "個人的"), ("Owner", "擁有者"), @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", "上述的欄位為共享且對其他人可見。"), ("Everyone", "所有人"), ("ab_web_console_tip", "打開 Web 控制台以進行更多操作"), + ("allow-only-conn-window-open-tip", "只在 RustDesk 視窗開啟時允許連接"), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 8a0155b1b..6d18e7a70 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -134,14 +134,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert Lock", "Встановити замок"), ("Refresh", "Оновити"), ("ID does not exist", "ID не існує"), - ("Failed to connect to rendezvous server", "Не вдалося підключитися до проміжного сервера"), + ("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 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", "Пароль ОС"), @@ -182,9 +182,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable file copy and paste", "Дозволити копіювання та вставку файлів"), ("Connected", "Підключено"), ("Direct and encrypted connection", "Пряме та зашифроване підключення"), - ("Relayed and encrypted connection", "Релейне та зашифроване підключення"), + ("Relayed and encrypted connection", "Ретрансльоване та зашифроване підключення"), ("Direct and unencrypted connection", "Пряме та незашифроване підключення"), - ("Relayed and unencrypted connection", "Релейне та незашифроване підключення"), + ("Relayed and unencrypted connection", "Ретрансльоване та незашифроване підключення"), ("Enter Remote ID", "Введіть віддалений ID"), ("Enter your password", "Введіть пароль"), ("Logging in...", "Вхід..."), @@ -210,8 +210,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Завершено вручну з боку віддаленого пристрою"), ("Enable remote configuration modification", "Дозволити віддалену зміну конфігурації"), ("Run without install", "Запустити без встановлення"), - ("Connect via relay", "Підключитися через реле"), - ("Always connect via relay", "Завжди підключатися через реле"), + ("Connect via relay", "Підключитися через ретранслятор"), + ("Always connect via relay", "Завжди підключатися через ретранслятор"), ("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"), ("Login", "Увійти"), ("Verify", "Підтвердити"), @@ -319,7 +319,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restart remote device", "Перезапустити віддалений пристрій"), ("Are you sure you want to restart", "Ви впевнені, що хочете виконати перезапуск?"), ("Restarting remote device", "Перезапуск віддаленого пристрою"), - ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення та через деякий час перепідʼєднайтесь, використовуючи постійний пароль."), + ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення та через деякий час перепідключіться, використовуючи постійний пароль."), ("Copied", "Скопійовано"), ("Exit Fullscreen", "Вийти з повноекранного режиму"), ("Fullscreen", "Повноекранний"), @@ -333,7 +333,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show Toolbar", "Показати панель інструментів"), ("Hide Toolbar", "Приховати панель інструментів"), ("Direct Connection", "Пряме підключення"), - ("Relay Connection", "Релейне підключення"), + ("Relay Connection", "Ретрансльоване підключення"), ("Secure Connection", "Безпечне підключення"), ("Insecure Connection", "Небезпечне підключення"), ("Scale original", "Оригінал масштабу"), @@ -445,7 +445,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Голосовий виклик"), ("Text chat", "Текстовий чат"), ("Stop voice call", "Завершити голосовий виклик"), - ("relay_hint_tip", "Якщо відсутня можливості підключитись напряму, ви можете спробувати підключення по реле. \nТакож, якщо ви хочете відразу використовувати реле, можна додати суфікс \"/r\" до ID, або ж вибрати опцію \"Завжди підключатися через реле\" в картці нещодавніх сеансів."), + ("relay_hint_tip", "Якщо відсутня можливості підключитись напряму, ви можете спробувати підключення через ретранслятор. \nТакож, якщо ви хочете відразу використовувати ретранслятор, можна додати суфікс \"/r\" до ID, або ж вибрати опцію \"Завжди підключатися через ретранслятор\" в картці нещодавніх сеансів."), ("Reconnect", "Перепідключитися"), ("Codec", "Кодек"), ("Resolution", "Роздільна здатність"), @@ -462,7 +462,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Досі немає улюблених вузлів?\nДавайте організуємо нове підключення та додамо його до улюблених!"), ("empty_lan_tip", "О ні, схоже ми поки не виявили жодного віддаленого пристрою"), ("empty_address_book_tip", "Ой лишенько, схоже до вашої адресної книги немає жодного віддаленого пристрою"), - ("eg: admin", "напр. admin"), + ("eg: admin", "напр., admin"), ("Empty Username", "Незаповнене імʼя"), ("Empty Password", "Незаповнений пароль"), ("Me", "Я"), @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("List", "Список"), ("Virtual display", "Віртуальний дисплей"), ("Plug out all", "Відключити все"), - ("True color (4:4:4)", "Спражній колір (4:4:4)"), + ("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"), @@ -586,20 +586,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("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", ""), + ("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", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index a106ba967..117442756 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -601,5 +601,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", ""), ("Everyone", ""), ("ab_web_console_tip", ""), + ("allow-only-conn-window-open-tip", ""), + ("no_need_privacy_mode_no_physical_displays_tip", ""), ].iter().cloned().collect(); } diff --git a/src/platform/linux_desktop_manager.rs b/src/platform/linux_desktop_manager.rs index 8e8cce95c..21b123799 100644 --- a/src/platform/linux_desktop_manager.rs +++ b/src/platform/linux_desktop_manager.rs @@ -277,7 +277,7 @@ impl DesktopManager { } } - // The logic mainly fron https://github.com/neutrinolabs/xrdp/blob/34fe9b60ebaea59e8814bbc3ca5383cabaa1b869/sesman/session.c#L334. + // 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() { diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 1ae35e56b..edacdc363 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -485,21 +485,31 @@ pub fn lock_screen() { pub fn start_os_service() { crate::platform::macos::hide_dock(); - let exe = std::env::current_exe().unwrap_or_default(); log::info!("Username: {}", crate::username()); - log::info!("Startime: {:?}", get_server_start_time()); + let mut sys = System::new(); + sys.refresh_processes_specifics(ProcessRefreshKind::new()); + let path = + std::fs::canonicalize(std::env::current_exe().unwrap_or_default()).unwrap_or_default(); + let my_start_time = sys + .process((std::process::id() as usize).into()) + .map(|p| p.start_time()) + .unwrap_or_default() as i64; + log::info!( + "Startime: {my_start_time} vs {:?}", + get_server_start_time(&mut sys, &path) + ); std::thread::spawn(move || loop { loop { std::thread::sleep(std::time::Duration::from_secs(1)); - let Some(start_time) = get_server_start_time() else { + let Some(start_time) = get_server_start_time(&mut sys, &path) else { continue; }; - if start_time.0 <= start_time.1 { + + if my_start_time <= start_time { // I tried add delegate (using tao and with its main loop0, but it works in normal mode, but not work as daemon log::info!( - "Agent start later, {:?}, will restart --service to make delegate work", - start_time + "Agent start later, {my_start_time} vs {start_time}, will restart --service to make delegate work", ); std::process::exit(0); } @@ -604,36 +614,25 @@ pub fn hide_dock() { } } -fn get_server_start_time() -> Option<(i64, i64)> { - 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 Some(my_start_time) = sys - .process((std::process::id() as usize).into()) - .map(|p| p.start_time()) - else { - return None; - }; +use hbb_common::sysinfo::{ProcessRefreshKind, System}; +#[inline] +fn get_server_start_time(sys: &mut System, path: &PathBuf) -> Option { + sys.refresh_processes_specifics(ProcessRefreshKind::new()); for (_, p) in sys.processes() { - 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 { + let cmd = p.cmd(); + if cmd.len() <= 1 { continue; } - if p.pid().as_u32() == std::process::id() { + if &cmd[1] != "--server" { continue; } - let parg = if p.cmd().len() <= 1 { "" } else { &p.cmd()[1] }; - if parg == "--server" { - return Some((my_start_time as _, p.start_time() as _)); + let Ok(cur) = std::fs::canonicalize(p.exe()) else { + continue; + }; + if &cur != path { + continue; } + return Some(p.start_time() as _); } None } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 8100b6516..6f7a028f3 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -8,6 +8,9 @@ pub use windows::*; #[cfg(windows)] pub mod windows; +#[cfg(windows)] +pub mod win_device; + #[cfg(target_os = "macos")] pub mod macos; diff --git a/src/platform/win_device.rs b/src/platform/win_device.rs new file mode 100644 index 000000000..f010c7f8a --- /dev/null +++ b/src/platform/win_device.rs @@ -0,0 +1,485 @@ +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), +} + +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::WinApiLastErr( + "SetupDiCreateDeviceInfoList".to_string(), + io::Error::last_os_error(), + )); + } + + 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::WinApiLastErr( + "SetupDiGetClassDevsExW".to_string(), + io::Error::last_os_error(), + )); + } + 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::WinApiLastErr( + "SetupDiGetINFClassW".to_string(), + io::Error::last_os_error(), + )); + } + + 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::WinApiLastErr( + "SetupDiCreateDeviceInfoW".to_string(), + io::Error::last_os_error(), + )); + } + + if SetupDiSetDeviceRegistryPropertyW( + *dev_info, + &mut dev_info_data, + SPDRP_HARDWAREID, + hardware_id.as_ptr() as _, + (hardware_id.len() * 2) as _, + ) == FALSE + { + return Err(DeviceError::WinApiLastErr( + "SetupDiSetDeviceRegistryPropertyW".to_string(), + io::Error::last_os_error(), + )); + } + + if SetupDiCallClassInstaller(DIF_REGISTERDEVICE, *dev_info, &mut dev_info_data) == FALSE { + return Err(DeviceError::WinApiLastErr( + "SetupDiCallClassInstaller".to_string(), + io::Error::last_os_error(), + )); + } + + 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::WinApiLastErr( + "UpdateDriverForPlugAndPlayDevicesW".to_string(), + io::Error::last_os_error(), + )); + } + *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::WinApiLastErr( + "SetupDiGetDeviceRegistryPropertyW".to_string(), + io::Error::last_os_error(), + )); + } + + 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::WinApiLastErr( + "SetupDiGetDeviceInfoListDetailW".to_string(), + io::Error::last_os_error(), + )); + } + + 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::WinApiLastErr( + "SetupDiSetClassInstallParams".to_string(), + io::Error::last_os_error(), + )); + } + + if SetupDiCallClassInstaller(DIF_REMOVE, *dev_info, &mut devinfo_data) == FALSE { + return Err(DeviceError::WinApiLastErr( + "SetupDiCallClassInstaller".to_string(), + io::Error::last_os_error(), + )); + } + + 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::WinApiLastErr( + "DeviceIoControl".to_string(), + io::Error::last_os_error(), + )); + } + 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::WinApiLastErr( + "SetupDiEnumDeviceInterfaces".to_string(), + io::Error::last_os_error(), + )); + } + + 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::WinApiLastErr( + "SetupDiGetDeviceInterfaceDetailW".to_string(), + io::Error::last_os_error(), + )); + } + + 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)?; + println!("device_path: {:?}", String::from_utf16_lossy(&device_path)); + 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::WinApiLastErr( + "CreateFileW".to_string(), + io::Error::last_os_error(), + )); + } + Ok(h_device) +} diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 381f4dc63..4a9888ad3 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -669,4 +669,33 @@ extern "C" AllocConsole(); freopen("CONOUT$", "w", stdout); } -} // end of extern "C" + + 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" \ No newline at end of file diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 0659422ef..d0b995406 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -64,8 +64,6 @@ use windows_service::{ use winreg::enums::*; use winreg::RegKey; -pub const DRIVER_CERT_FILE: &str = "RustDeskIddDriver.cer"; - pub fn get_cursor_pos() -> Option<(i32, i32)> { unsafe { #[allow(invalid_value)] @@ -462,6 +460,7 @@ extern "C" { 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; } extern "system" { @@ -1187,17 +1186,6 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} ); let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_string(); - let install_cert = if options.contains("driverCert") { - let s = format!(r#""{}" --install-cert"#, src_exe); - if silent { - format!("{} silent", s) - } else { - s - } - } else { - "".to_owned() - }; - // 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); @@ -1241,11 +1229,10 @@ cscript \"{uninstall_shortcut}\" copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" {dels} {import_config} -{install_cert} {after_install} {sleep} ", - version = crate::VERSION, + version = crate::VERSION.replace("-", "."), build_date = crate::BUILD_DATE, after_install = get_after_install(&exe), sleep = if debug { "timeout 300" } else { "" }, @@ -1291,6 +1278,11 @@ fn get_before_uninstall(kill_self: bool) -> String { } fn get_uninstall(kill_self: 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(); if let Ok(exe) = std::env::current_exe() { if let Some(exe_path) = exe.to_str() { @@ -1303,12 +1295,14 @@ fn get_uninstall(kill_self: bool) -> String { {before_uninstall} {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(&path), app_name = crate::get_app_name(), ) } @@ -1432,24 +1426,6 @@ pub fn add_recent_document(path: &str) { pub fn is_installed() -> bool { let (_, _, _, exe) = get_install_info(); std::fs::metadata(exe).is_ok() - /* - use windows_service::{ - service::ServiceAccess, - service_manager::{ServiceManager, ServiceManagerAccess}, - }; - if !std::fs::metadata(exe).is_ok() { - return false; - } - let manager_access = ServiceManagerAccess::CONNECT; - if let Ok(service_manager) = ServiceManager::local_computer(None::<&str>, manager_access) { - if let Ok(_) = - service_manager.open_service(crate::get_app_name(), ServiceAccess::QUERY_CONFIG) - { - return true; - } - } - return false; - */ } pub fn get_reg(name: &str) -> String { @@ -1899,7 +1875,7 @@ pub fn current_resolution(name: &str) -> ResultType { dm.dmSize = std::mem::size_of::() as _; if EnumDisplaySettingsW(device_name.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) == 0 { bail!( - "failed to get currrent resolution, error {}", + "failed to get current resolution, error {}", io::Error::last_os_error() ); } @@ -1958,251 +1934,20 @@ pub fn user_accessible_folder() -> ResultType { Ok(dir) } -#[inline] -pub fn install_cert(cert_file: &str) -> ResultType<()> { - let exe_file = std::env::current_exe()?; - if let Some(cur_dir) = exe_file.parent() { - allow_err!(cert::install_cert(cur_dir.join(cert_file))); - } else { - bail!( - "Invalid exe parent for {}", - exe_file.to_string_lossy().as_ref() - ); - } - Ok(()) -} - #[inline] pub fn uninstall_cert() -> ResultType<()> { cert::uninstall_cert() } mod cert { - use hbb_common::{bail, log, ResultType}; - use std::{ffi::OsStr, io::Error, os::windows::ffi::OsStrExt, path::Path, str::from_utf8}; - use winapi::{ - shared::{ - minwindef::{BYTE, DWORD, FALSE, TRUE}, - ntdef::NULL, - }, - um::{ - wincrypt::{ - CertAddEncodedCertificateToStore, CertCloseStore, CertDeleteCertificateFromStore, - CertEnumCertificatesInStore, CertNameToStrA, CertOpenStore, CryptHashCertificate, - ALG_ID, CALG_SHA1, CERT_ID_SHA1_HASH, CERT_STORE_ADD_REPLACE_EXISTING, - CERT_STORE_PROV_SYSTEM_W, CERT_SYSTEM_STORE_LOCAL_MACHINE, CERT_X500_NAME_STR, - PCCERT_CONTEXT, PKCS_7_ASN_ENCODING, X509_ASN_ENCODING, - }, - winreg::HKEY_LOCAL_MACHINE, - }, - }; - use winreg::{ - enums::{KEY_WRITE, REG_BINARY}, - RegKey, - }; + use hbb_common::ResultType; - const ROOT_CERT_STORE_PATH: &str = - "SOFTWARE\\Microsoft\\SystemCertificates\\ROOT\\Certificates\\"; - const THUMBPRINT_ALG: ALG_ID = CALG_SHA1; - const THUMBPRINT_LEN: DWORD = 20; - const CERT_ISSUER_1: &str = "CN=\"WDKTestCert admin,133225435702113567\"\0"; - const CERT_ENCODING_TYPE: DWORD = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; - - lazy_static::lazy_static! { - static ref CERT_STORE_LOC: Vec = OsStr::new("ROOT\0").encode_wide().collect::>(); + extern "C" { + fn DeleteRustDeskTestCertsW(); } - - #[inline] - unsafe fn compute_thumbprint(pb_encoded: *const BYTE, cb_encoded: DWORD) -> (Vec, String) { - let mut size = THUMBPRINT_LEN; - let mut thumbprint = [0u8; THUMBPRINT_LEN as usize]; - if CryptHashCertificate( - 0, - THUMBPRINT_ALG, - 0, - pb_encoded, - cb_encoded, - thumbprint.as_mut_ptr(), - &mut size, - ) == TRUE - { - ( - thumbprint.to_vec(), - hex::encode(thumbprint).to_ascii_uppercase(), - ) - } else { - (thumbprint.to_vec(), "".to_owned()) - } - } - - #[inline] - unsafe fn open_reg_cert_store() -> ResultType { - let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); - Ok(hklm.open_subkey_with_flags(ROOT_CERT_STORE_PATH, KEY_WRITE)?) - } - - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpef/6a9e35fa-2ac7-4c10-81e1-eabe8d2472f1 - fn create_cert_blob(thumbprint: Vec, encoded: Vec) -> Vec { - let mut blob = Vec::new(); - - let mut property_id = (CERT_ID_SHA1_HASH as u32).to_le_bytes().to_vec(); - let mut pro_reserved = [0x01, 0x00, 0x00, 0x00].to_vec(); - let mut pro_length = (THUMBPRINT_LEN as u32).to_le_bytes().to_vec(); - let mut pro_val = thumbprint; - blob.append(&mut property_id); - blob.append(&mut pro_reserved); - blob.append(&mut pro_length); - blob.append(&mut pro_val); - - let mut blob_reserved = [0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00].to_vec(); - let mut blob_length = (encoded.len() as u32).to_le_bytes().to_vec(); - let mut blob_val = encoded; - blob.append(&mut blob_reserved); - blob.append(&mut blob_length); - blob.append(&mut blob_val); - - blob - } - - pub fn install_cert>(path: P) -> ResultType<()> { - let mut cert_bytes = std::fs::read(path)?; - install_cert_reg(&mut cert_bytes)?; - install_cert_add_cert_store(&mut cert_bytes)?; - Ok(()) - } - - fn install_cert_reg(cert_bytes: &mut [u8]) -> ResultType<()> { - unsafe { - let thumbprint = compute_thumbprint(cert_bytes.as_mut_ptr(), cert_bytes.len() as _); - log::debug!("Thumbprint of cert {}", &thumbprint.1); - - let reg_cert_key = open_reg_cert_store()?; - let (cert_key, _) = reg_cert_key.create_subkey(&thumbprint.1)?; - let data = winreg::RegValue { - vtype: REG_BINARY, - bytes: create_cert_blob(thumbprint.0, cert_bytes.to_vec()), - }; - cert_key.set_raw_value("Blob", &data)?; - } - Ok(()) - } - - fn install_cert_add_cert_store(cert_bytes: &mut [u8]) -> ResultType<()> { - unsafe { - let store_handle = CertOpenStore( - CERT_STORE_PROV_SYSTEM_W, - 0, - 0, - CERT_SYSTEM_STORE_LOCAL_MACHINE, - CERT_STORE_LOC.as_ptr() as _, - ); - if store_handle.is_null() { - bail!( - "Error opening certificate store: {}", - Error::last_os_error() - ); - } - - // Create the certificate context - let cert_context = winapi::um::wincrypt::CertCreateCertificateContext( - CERT_ENCODING_TYPE, - cert_bytes.as_ptr(), - cert_bytes.len() as DWORD, - ); - if cert_context.is_null() { - bail!( - "Error creating certificate context: {}", - Error::last_os_error() - ); - } - - if FALSE - == CertAddEncodedCertificateToStore( - store_handle, - CERT_ENCODING_TYPE, - (*cert_context).pbCertEncoded, - (*cert_context).cbCertEncoded, - CERT_STORE_ADD_REPLACE_EXISTING, - std::ptr::null_mut(), - ) - { - log::error!( - "Failed to call CertAddEncodedCertificateToStore: {}", - Error::last_os_error() - ); - } else { - log::info!("Add cert to store successfully"); - } - - CertCloseStore(store_handle, 0); - } - Ok(()) - } - - fn get_thumbprints_to_rm() -> ResultType> { - let issuers_to_rm = [CERT_ISSUER_1]; - - let mut thumbprints = Vec::new(); - let mut buf = [0u8; 1024]; - - unsafe { - let store_handle = CertOpenStore( - CERT_STORE_PROV_SYSTEM_W, - 0, - 0, - CERT_SYSTEM_STORE_LOCAL_MACHINE, - CERT_STORE_LOC.as_ptr() as _, - ); - if store_handle.is_null() { - bail!( - "Error opening certificate store: {}", - Error::last_os_error() - ); - } - - let mut cert_ctx: PCCERT_CONTEXT = CertEnumCertificatesInStore(store_handle, NULL as _); - while !cert_ctx.is_null() { - // https://stackoverflow.com/a/66432736 - let cb_size = CertNameToStrA( - (*cert_ctx).dwCertEncodingType, - &mut ((*(*cert_ctx).pCertInfo).Issuer) as _, - CERT_X500_NAME_STR, - buf.as_mut_ptr() as _, - buf.len() as _, - ); - if cb_size != 1 { - if let Ok(issuer) = from_utf8(&buf[..cb_size as _]) { - for iss in issuers_to_rm.iter() { - if issuer == *iss { - let (_, thumbprint) = compute_thumbprint( - (*cert_ctx).pbCertEncoded, - (*cert_ctx).cbCertEncoded, - ); - if !thumbprint.is_empty() { - thumbprints.push(thumbprint); - } - // Delete current cert context and re-enumerate. - CertDeleteCertificateFromStore(cert_ctx); - cert_ctx = CertEnumCertificatesInStore(store_handle, NULL as _); - } - } - } - } - cert_ctx = CertEnumCertificatesInStore(store_handle, cert_ctx); - } - CertCloseStore(store_handle, 0); - } - - Ok(thumbprints) - } - pub fn uninstall_cert() -> ResultType<()> { - let thumbprints = get_thumbprints_to_rm()?; - let reg_cert_key = unsafe { open_reg_cert_store()? }; - log::info!("Found {} certs to remove", thumbprints.len()); - for thumbprint in thumbprints.iter() { - // Deleting cert from registry may fail, because the CertDeleteCertificateFromStore() is called before. - let _ = reg_cert_key.delete_subkey(thumbprint); + unsafe { + DeleteRustDeskTestCertsW(); } Ok(()) } @@ -2449,14 +2194,6 @@ pub fn try_kill_broker() { #[cfg(test)] mod tests { use super::*; - #[test] - fn test_install_cert() { - println!( - "install driver cert: {:?}", - cert::install_cert("RustDeskIddDriver.cer") - ); - } - #[test] fn test_uninstall_cert() { println!("uninstall driver certs: {:?}", cert::uninstall_cert()); @@ -2631,3 +2368,25 @@ impl Drop for WallPaperRemover { allow_err!(Self::set_wallpaper(Some(self.old_path.clone()))); } } + +fn get_uninstall_amyuni_idd(path: &str) -> 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 _) + } +} diff --git a/src/platform/windows_delete_test_cert.cc b/src/platform/windows_delete_test_cert.cc new file mode 100644 index 000000000..94949a827 --- /dev/null +++ b/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/src/privacy_mode.rs b/src/privacy_mode.rs index d0781d993..859eef123 100644 --- a/src/privacy_mode.rs +++ b/src/privacy_mode.rs @@ -6,9 +6,7 @@ use crate::{ display_service, ipc::{connect, Data}, }; -#[cfg(windows)] -use hbb_common::tokio; -use hbb_common::{anyhow::anyhow, bail, lazy_static, ResultType}; +use hbb_common::{anyhow::anyhow, bail, lazy_static, tokio, ResultType}; use serde_derive::{Deserialize, Serialize}; use std::{ collections::HashMap, @@ -30,10 +28,10 @@ mod win_virtual_display; 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 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_DISPLAYS: &'static str = "No displays"; + "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"; #[cfg(windows)] pub const PRIVACY_MODE_IMPL_WIN_MAG: &str = win_mag::PRIVACY_MODE_IMPL; @@ -202,7 +200,6 @@ fn get_supported_impl(impl_key: &str) -> String { cur_impl } -#[inline] pub fn turn_on_privacy(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(); @@ -300,7 +297,7 @@ pub fn get_supported_privacy_mode_impl() -> Vec<(&'static str, &'static str)> { } #[cfg(feature = "virtual_display_driver")] - if is_installed() { + 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", diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index 287bfd51e..7e7543d67 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -1,9 +1,11 @@ -use super::{PrivacyMode, PrivacyModeState, INVALID_PRIVACY_MODE_CONN_ID, NO_DISPLAYS}; +use super::{PrivacyMode, PrivacyModeState, INVALID_PRIVACY_MODE_CONN_ID, NO_PHYSICAL_DISPLAYS}; use crate::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::{ @@ -27,7 +29,6 @@ use winapi::{ pub(super) const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_virtual_display"; -const IDD_DEVICE_STRING: &'static str = "RustDeskIddDriver Device\0"; const CONFIG_KEY_REG_RECOVERY: &str = "reg_recovery"; struct Display { @@ -137,8 +138,9 @@ impl PrivacyModeImpl { primary, }; - if let Ok(s) = std::string::String::from_utf16(&dd.DeviceString) { - if &s[..IDD_DEVICE_STRING.len()] == IDD_DEVICE_STRING { + 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; } @@ -155,7 +157,7 @@ impl PrivacyModeImpl { } fn restore_plug_out_monitor(&mut self) { - let _ = virtual_display_manager::plug_out_peer_request(&self.virtual_displays_added); + let _ = virtual_display_manager::plug_out_monitor_indices(&self.virtual_displays_added); self.virtual_displays_added.clear(); } @@ -304,8 +306,18 @@ impl PrivacyModeImpl { if self.virtual_displays.is_empty() { let displays = virtual_display_manager::plug_in_peer_request(vec![Self::default_display_modes()])?; - self.virtual_displays_added.extend(displays); + if virtual_display_manager::is_amyuni_idd() { + thread::sleep(Duration::from_secs(3)); + } 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)?; + bail!(NO_PHYSICAL_DISPLAYS); + } + + self.virtual_displays_added.extend(displays); } Ok(()) @@ -367,8 +379,8 @@ impl PrivacyMode for PrivacyModeImpl { } self.set_displays(); if self.displays.is_empty() { - log::debug!("No displays"); - bail!(NO_DISPLAYS); + log::debug!("{}", NO_PHYSICAL_DISPLAYS); + bail!(NO_PHYSICAL_DISPLAYS); } let mut guard = TurnOnGuard { @@ -379,7 +391,7 @@ impl PrivacyMode for PrivacyModeImpl { guard.ensure_virtual_display()?; if guard.virtual_displays.is_empty() { log::debug!("No virtual displays"); - bail!("No virtual displays"); + bail!("No virtual displays."); } let reg_connectivity_1 = reg_display_settings::read_reg_connectivity()?; @@ -416,7 +428,7 @@ impl PrivacyMode for PrivacyModeImpl { self.check_off_conn_id(conn_id)?; super::win_input::unhook()?; self.restore(); - restore_reg_connectivity(); + restore_reg_connectivity(false); if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID { if let Some(state) = state { @@ -457,11 +469,14 @@ fn reset_config_reg_connectivity() { Config::set_option(CONFIG_KEY_REG_RECOVERY.to_owned(), "".to_owned()); } -pub fn restore_reg_connectivity() { +pub fn restore_reg_connectivity(plug_out_monitors: 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); + } if let Ok(reg_recovery) = serde_json::from_str::(&config_recovery_value) { diff --git a/src/server.rs b/src/server.rs index ef89c5e3b..9345936e0 100644 --- a/src/server.rs +++ b/src/server.rs @@ -449,9 +449,8 @@ pub async fn start_server(is_server: bool) { log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY")); } #[cfg(feature = "hwcodec")] - scrap::hwcodec::hwcodec_new_check_process(); - #[cfg(feature = "gpucodec")] - scrap::gpucodec::gpucodec_new_check_process(); + #[cfg(any(target_os = "windows", target_os = "linux"))] + scrap::hwcodec::start_check_process(false); #[cfg(windows)] hbb_common::platform::windows::start_cpu_performance_monitor(); diff --git a/src/server/connection.rs b/src/server/connection.rs index 158441785..afd090a92 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -235,7 +235,7 @@ pub struct Connection { auto_disconnect_timer: Option<(Instant, u64)>, authed_conn_id: Option, file_remove_log_control: FileRemoveLogControl, - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] supported_encoding_flag: (bool, Option), services_subed: bool, delayed_read_dir: Option<(String, bool)>, @@ -386,7 +386,7 @@ impl Connection { auto_disconnect_timer: None, authed_conn_id: None, file_remove_log_control: FileRemoveLogControl::new(id), - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] supported_encoding_flag: (false, None), services_subed: false, delayed_read_dir: None, @@ -691,7 +691,7 @@ impl Connection { } } conn.file_remove_log_control.on_timer().drain(..).map(|x| conn.send_to_cm(x)).count(); - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] conn.update_supported_encoding(); } _ = test_delay_timer.tick() => { @@ -956,6 +956,13 @@ impl Connection { 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()); @@ -1126,10 +1133,7 @@ impl Connection { ); #[cfg(feature = "virtual_display_driver")] if crate::platform::is_installed() { - let virtual_displays = virtual_display_manager::get_virtual_displays(); - if !virtual_displays.is_empty() { - platform_additions.insert("virtual_displays".into(), json!(&virtual_displays)); - } + platform_additions.extend(virtual_display_manager::get_platform_additions()); } platform_additions.insert( "supported_privacy_mode_impl".into(), @@ -1333,8 +1337,7 @@ impl Connection { && raii::AuthedConnID::remote_and_file_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") - || self.lr.option.support_windows_specific_session == BoolOption::Yes.into()) + && get_version_number(&self.lr.version) >= get_version_number("1.2.4") { pi.windows_sessions = Some(WindowsSessions { sessions, @@ -2296,7 +2299,11 @@ impl Connection { } } #[cfg(not(any(target_os = "android", target_os = "ios")))] - Some(misc::Union::ChangeResolution(r)) => self.change_resolution(&r), + 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)) => { @@ -2339,6 +2346,13 @@ impl Connection { } } } + Some(misc::Union::MessageQuery(mq)) => { + if let Some(msg_out) = + video_service::make_display_changed_msg(mq.switch_display as _, None) + { + self.send(msg_out).await; + } + } _ => {} }, Some(message::Union::AudioFrame(frame)) => { @@ -2468,11 +2482,14 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] if s.width != 0 && s.height != 0 { - self.change_resolution(&Resolution { - width: s.width, - height: s.height, - ..Default::default() - }); + self.change_resolution( + None, + &Resolution { + width: s.width, + height: s.height, + ..Default::default() + }, + ); } } @@ -2588,8 +2605,7 @@ impl Connection { self.send(make_msg("idd_not_support_under_win10_2004_tip".to_string())) .await; } else { - if let Err(e) = - virtual_display_manager::plug_in_index_modes(t.display as _, Vec::new()) + 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!( @@ -2600,13 +2616,8 @@ impl Connection { } } } else { - let indices = if t.display == -1 { - virtual_display_manager::get_virtual_displays() - } else { - vec![t.display as _] - }; - if let Err(e) = virtual_display_manager::plug_out_peer_request(&indices) { - log::error!("Failed to plug out virtual display {:?}: {}", &indices, e); + if let Err(e) = virtual_display_manager::plug_out_monitor(t.display) { + log::error!("Failed to plug out virtual display {}: {}", t.display, e); self.send(make_msg(format!( "Failed to plug out virtual displays: {}", e @@ -2625,14 +2636,15 @@ impl Connection { } #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn change_resolution(&mut self, r: &Resolution) { + fn change_resolution(&mut self, d: Option, r: &Resolution) { if self.keyboard { if let Ok(displays) = display_service::try_get_displays() { - if let Some(display) = displays.get(self.display_idx) { + let display_idx = d.unwrap_or(self.display_idx); + if let Some(display) = displays.get(display_idx) { let name = display.name(); #[cfg(all(windows, feature = "virtual_display_driver"))] if let Some(_ok) = - virtual_display_manager::change_resolution_if_is_virtual_display( + virtual_display_manager::rustdesk_idd::change_resolution_if_is_virtual_display( &name, r.width as _, r.height as _, @@ -2640,11 +2652,18 @@ impl Connection { { return; } - display_service::set_last_changed_resolution( - &name, - (display.width() as _, display.height() as _), - (r.width, r.height), - ); + let mut record_changed = true; + #[cfg(all(windows, feature = "virtual_display_driver"))] + if virtual_display_manager::amyuni_idd::is_my_display(&name) { + record_changed = false; + } + if record_changed { + display_service::set_last_changed_resolution( + &name, + (display.width() as _, display.height() as _), + (r.width, r.height), + ); + } if let Err(e) = crate::platform::change_resolution(&name, r.width as _, r.height as _) { @@ -2851,7 +2870,6 @@ impl Connection { } else { let is_pre_privacy_on = privacy_mode::is_in_privacy_mode(); let pre_impl_key = privacy_mode::get_cur_impl_key(); - let turn_on_res = privacy_mode::turn_on_privacy(&impl_key, self.inner.id); if is_pre_privacy_on { if let Some(pre_impl_key) = pre_impl_key { @@ -2865,6 +2883,7 @@ impl Connection { } } + let turn_on_res = privacy_mode::turn_on_privacy(&impl_key, self.inner.id); match turn_on_res { Some(Ok(res)) => { if res { @@ -2899,7 +2918,7 @@ impl Connection { } Some(Err(e)) => { log::error!("Failed to turn on privacy mode. {}", e); - if !privacy_mode::is_in_privacy_mode() { + if privacy_mode::is_in_privacy_mode() { let _ = Self::turn_off_privacy_to_msg( privacy_mode::INVALID_PRIVACY_MODE_CONN_ID, ); @@ -3090,9 +3109,9 @@ impl Connection { .map(|t| t.0 = Instant::now()); } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn update_supported_encoding(&mut self) { - let not_use = Some(scrap::gpucodec::GpuEncoder::not_use()); + let not_use = Some(scrap::vram::VRamEncoder::not_use()); if !self.authorized || self.supported_encoding_flag.0 && self.supported_encoding_flag.1 == not_use { diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 838937057..0c8263cbd 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -160,15 +160,8 @@ fn displays_to_msg(displays: Vec) -> Message { #[cfg(all(windows, feature = "virtual_display_driver"))] if crate::platform::is_installed() { - let virtual_displays = crate::virtual_display_manager::get_virtual_displays(); - if !virtual_displays.is_empty() { - let mut platform_additions = serde_json::Map::new(); - platform_additions.insert( - "virtual_displays".into(), - serde_json::json!(&virtual_displays), - ); - pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into()); - } + 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. @@ -227,10 +220,11 @@ pub(super) fn get_original_resolution( h: usize, ) -> MessageField { #[cfg(all(windows, feature = "virtual_display_driver"))] - let is_virtual_display = crate::virtual_display_manager::is_virtual_display(&display_name); + let is_rustdesk_virtual_display = + crate::virtual_display_manager::rustdesk_idd::is_virtual_display(&display_name); #[cfg(not(all(windows, feature = "virtual_display_driver")))] - let is_virtual_display = false; - Some(if is_virtual_display { + let is_rustdesk_virtual_display = false; + Some(if is_rustdesk_virtual_display { Resolution { width: 0, height: 0, @@ -382,8 +376,10 @@ pub fn try_get_displays() -> ResultType> { #[cfg(all(windows, feature = "virtual_display_driver"))] pub fn try_get_displays() -> ResultType> { let mut displays = Display::all()?; + let no_displays_v = no_displays(&displays); + virtual_display_manager::set_can_plug_out_all(!no_displays_v); if crate::platform::is_installed() - && no_displays(&displays) + && no_displays_v && virtual_display_manager::is_virtual_display_supported() { log::debug!("no displays, create virtual display"); diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 7d1392bbf..81ca155d7 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -187,7 +187,7 @@ impl LockModesHandler { 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 simuation of `VIRTUAL_INPUT_STATE` in this file. + // 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, diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 8ed581020..e181ac7fe 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -8,7 +8,7 @@ use hbb_common::{ tokio::{self, sync::mpsc}, ResultType, }; -#[cfg(feature = "gpucodec")] +#[cfg(feature = "vram")] use scrap::AdapterDevice; use scrap::{Capturer, Frame, TraitCapturer, TraitPixelBuffer}; use shared_memory::*; @@ -744,12 +744,12 @@ pub mod client { true } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn device(&self) -> AdapterDevice { AdapterDevice::default() } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] fn set_output_texture(&mut self, _texture: bool) {} } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index f4ba3a4e6..19d711bdb 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -42,10 +42,10 @@ use hbb_common::{ Mutex as TokioMutex, }, }; -#[cfg(feature = "gpucodec")] -use scrap::gpucodec::{GpuEncoder, GpuEncoderConfig}; #[cfg(feature = "hwcodec")] -use scrap::hwcodec::{HwEncoder, HwEncoderConfig}; +use scrap::hwcodec::{HwRamEncoder, HwRamEncoderConfig}; +#[cfg(feature = "vram")] +use scrap::vram::{VRamEncoder, VRamEncoderConfig}; #[cfg(not(windows))] use scrap::Capturer; use scrap::{ @@ -430,7 +430,7 @@ fn run(vs: VideoService) -> ResultType<()> { Ok(x) => encoder = x, Err(err) => bail!("Failed to create encoder: {}", err), } - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] c.set_output_texture(encoder.input_texture()); VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate()); VIDEO_QOS @@ -490,9 +490,9 @@ fn run(vs: VideoService) -> ResultType<()> { if Encoder::use_i444(&encoder_cfg) != use_i444 { bail!("SWITCH"); } - #[cfg(all(windows, feature = "gpucodec"))] - if c.is_gdi() && (codec_name == CodecName::H264GPU || codec_name == CodecName::H265GPU) { - log::info!("changed to gdi when using gpucodec"); + #[cfg(all(windows, feature = "vram"))] + if c.is_gdi() && (codec_name == CodecName::H264VRAM || codec_name == CodecName::H265VRAM) { + log::info!("changed to gdi when using vram"); bail!("SWITCH"); } check_privacy_mode_changed(&sp, c.privacy_mode_id)?; @@ -624,8 +624,8 @@ impl Raii { impl Drop for Raii { fn drop(&mut self) { - #[cfg(feature = "gpucodec")] - GpuEncoder::set_not_use(self.0, false); + #[cfg(feature = "vram")] + VRamEncoder::set_not_use(self.0, false); VIDEO_QOS.lock().unwrap().set_support_abr(self.0, true); } } @@ -637,21 +637,21 @@ fn get_encoder_config( record: bool, _portable_service: bool, ) -> EncoderCfg { - #[cfg(all(windows, feature = "gpucodec"))] + #[cfg(all(windows, feature = "vram"))] if _portable_service || c.is_gdi() { log::info!("gdi:{}, portable:{}", c.is_gdi(), _portable_service); - GpuEncoder::set_not_use(_display_idx, true); + VRamEncoder::set_not_use(_display_idx, true); } - #[cfg(feature = "gpucodec")] + #[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.clone() { - CodecName::H264GPU | CodecName::H265GPU => { - #[cfg(feature = "gpucodec")] - if let Some(feature) = GpuEncoder::try_get(&c.device(), negotiated_codec.clone()) { - EncoderCfg::GPU(GpuEncoderConfig { + CodecName::H264VRAM | CodecName::H265VRAM => { + #[cfg(feature = "vram")] + if let Some(feature) = VRamEncoder::try_get(&c.device(), negotiated_codec.clone()) { + EncoderCfg::VRAM(VRamEncoderConfig { device: c.device(), width: c.width, height: c.height, @@ -668,7 +668,7 @@ fn get_encoder_config( keyframe_interval, ) } - #[cfg(not(feature = "gpucodec"))] + #[cfg(not(feature = "vram"))] handle_hw_encoder( negotiated_codec.clone(), c.width, @@ -677,7 +677,7 @@ fn get_encoder_config( keyframe_interval, ) } - CodecName::H264HW(_name) | CodecName::H265HW(_name) => handle_hw_encoder( + CodecName::H264RAM(_name) | CodecName::H265RAM(_name) => handle_hw_encoder( negotiated_codec.clone(), c.width, c.height, @@ -714,15 +714,12 @@ fn handle_hw_encoder( let f = || { #[cfg(feature = "hwcodec")] match _name { - CodecName::H264GPU | CodecName::H265GPU => { - if !scrap::codec::enable_hwcodec_option() { - return Err(()); - } - let is_h265 = _name == CodecName::H265GPU; - let best = HwEncoder::best(); + CodecName::H264VRAM | CodecName::H265VRAM => { + let is_h265 = _name == CodecName::H265VRAM; + let best = HwRamEncoder::best(); if let Some(h264) = best.h264 { if !is_h265 { - return Ok(EncoderCfg::HW(HwEncoderConfig { + return Ok(EncoderCfg::HWRAM(HwRamEncoderConfig { name: h264.name, width, height, @@ -733,7 +730,7 @@ fn handle_hw_encoder( } if let Some(h265) = best.h265 { if is_h265 { - return Ok(EncoderCfg::HW(HwEncoderConfig { + return Ok(EncoderCfg::HWRAM(HwRamEncoderConfig { name: h265.name, width, height, @@ -743,8 +740,8 @@ fn handle_hw_encoder( } } } - CodecName::H264HW(name) | CodecName::H265HW(name) => { - return Ok(EncoderCfg::HW(HwEncoderConfig { + CodecName::H264RAM(name) | CodecName::H265RAM(name) => { + return Ok(EncoderCfg::HWRAM(HwRamEncoderConfig { name, width, height, diff --git a/src/ui.rs b/src/ui.rs index bb42588f8..2e9ea2f91 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -584,8 +584,8 @@ impl UI { has_hwcodec() } - fn has_gpucodec(&self) -> bool { - has_gpucodec() + fn has_vram(&self) -> bool { + has_vram() } fn get_langs(&self) -> String { @@ -629,6 +629,10 @@ impl UI { ); format!("data:image/png;base64,{s}") } + + pub fn check_hwcodec(&self) { + check_hwcodec() + } } impl sciter::EventHandler for UI { @@ -709,7 +713,7 @@ impl sciter::EventHandler for UI { fn get_lan_peers(); fn get_uuid(); fn has_hwcodec(); - fn has_gpucodec(); + fn has_vram(); fn get_langs(); fn default_video_save_directory(); fn handle_relay_id(String); @@ -719,6 +723,7 @@ impl sciter::EventHandler for UI { fn generate2fa(); fn generate_2fa_img_src(String); fn verify2fa(String); + fn check_hwcodec(); } } diff --git a/src/ui/index.tis b/src/ui/index.tis index 88ad75362..f202d0fad 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -210,13 +210,13 @@ class Enhancements: Reactor.Component { function render() { var has_hwcodec = handler.has_hwcodec(); - var has_gpucodec = handler.has_gpucodec(); + 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_gpucodec) ?
  • {svg_checkmark}{translate("Enable hardware codec")}
  • : ""} + {(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")}
  • : ""} @@ -240,7 +240,11 @@ class Enhancements: Reactor.Component { event click $(menu#enhancements-menu>li) (_, me) { var v = me.id; if (v.indexOf("enable-") == 0) { - handler.set_option(v, handler.get_option(v) != 'N' ? 'N' : ''); + var set_value = handler.get_option(v) != 'N' ? 'N' : ''; + handler.set_option(v, set_value); + if (v == "enable-hwcodec" && set_value == '') { + handler.check_hwcodec(); + } } else if (v.indexOf("allow-") == 0) { handler.set_option(v, handler.get_option(v) == 'Y' ? '' : 'Y'); } else if (v == 'screen-recording') { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 40ffc2d36..313b6e562 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -862,15 +862,14 @@ pub fn get_api_server() -> String { #[inline] pub fn has_hwcodec() -> bool { - #[cfg(not(any(feature = "hwcodec", feature = "mediacodec")))] - return false; - #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] - return true; + // Has real hardware codec using gpu + (cfg!(feature = "hwcodec") && (cfg!(windows) || cfg!(target_os = "linux"))) + || cfg!(feature = "mediacodec") } #[inline] -pub fn has_gpucodec() -> bool { - cfg!(feature = "gpucodec") +pub fn has_vram() -> bool { + cfg!(feature = "vram") } #[cfg(feature = "flutter")] @@ -879,14 +878,14 @@ pub fn supported_hwdecodings() -> (bool, bool) { let decoding = scrap::codec::Decoder::supported_decodings(None, true, None, &vec![]); #[allow(unused_mut)] let (mut h264, mut h265) = (decoding.ability_h264 > 0, decoding.ability_h265 > 0); - #[cfg(feature = "gpucodec")] + #[cfg(feature = "vram")] { // supported_decodings check runtime luid - let gpu = scrap::gpucodec::GpuDecoder::possible_available_without_check(); - if gpu.0 { + let vram = scrap::vram::VRamDecoder::possible_available_without_check(); + if vram.0 { h264 = true; } - if gpu.1 { + if vram.1 { h265 = true; } } @@ -1348,3 +1347,14 @@ pub fn verify2fa(code: String) -> bool { } res } + +pub fn check_hwcodec() { + #[cfg(feature = "hwcodec")] + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + scrap::hwcodec::start_check_process(true); + if crate::platform::is_installed() { + ipc::notify_server_to_check_hwcodec().ok(); + } + } +} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 348bcbd7b..61c11feb0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1222,7 +1222,7 @@ impl Session { pub fn change_resolution(&self, display: i32, width: i32, height: i32) { *self.last_change_display.lock().unwrap() = ChangeDisplayRecord::new(display, width, height); - self.do_change_resolution(width, height); + self.do_change_resolution(display, width, height); } #[inline] @@ -1232,13 +1232,22 @@ impl Session { } } - fn do_change_resolution(&self, width: i32, height: i32) { + fn do_change_resolution(&self, display: i32, width: i32, height: i32) { let mut misc = Misc::new(); - misc.set_change_resolution(Resolution { + let resolution = Resolution { width, height, ..Default::default() - }); + }; + if crate::common::is_support_multi_ui_session_num(self.lc.read().unwrap().version) { + misc.set_change_display_resolution(DisplayResolution { + display, + resolution: Some(resolution).into(), + ..Default::default() + }); + } else { + misc.set_change_resolution(resolution); + } let mut msg = Message::new(); msg.set_misc(misc); self.send(Data::Message(msg)); @@ -1293,6 +1302,22 @@ impl Session { log::error!("selected invalid sid: {}", sid); } } + + #[inline] + pub fn request_init_msgs(&self, display: usize) { + self.send_message_query(display); + } + + fn send_message_query(&self, display: usize) { + let mut misc = Misc::new(); + misc.set_message_query(MessageQuery { + switch_display: display as _, + ..Default::default() + }); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(Data::Message(msg)); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -1350,7 +1375,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_incoming(&self); fn get_rgba(&self, display: usize) -> *const u8; fn next_rgba(&self, display: usize); - #[cfg(all(feature = "gpucodec", feature = "flutter"))] + #[cfg(all(feature = "vram", feature = "flutter"))] fn on_texture(&self, display: usize, texture: *mut c_void); fn set_multiple_windows_session(&self, sessions: Vec); } @@ -1663,7 +1688,7 @@ pub async fn io_loop(handler: Session, round: u32) { if pixelbuffer { ui_handler.on_rgba(display, data); } else { - #[cfg(all(feature = "gpucodec", feature = "flutter"))] + #[cfg(all(feature = "vram", feature = "flutter"))] ui_handler.on_texture(display, _texture); } }, diff --git a/src/virtual_display_manager.rs b/src/virtual_display_manager.rs index da8780c7f..4358e6561 100644 --- a/src/virtual_display_manager.rs +++ b/src/virtual_display_manager.rs @@ -1,57 +1,47 @@ -#[cfg(target_os = "windows")] -use hbb_common::platform::windows::is_windows_version_or_greater; -use hbb_common::{allow_err, bail, lazy_static, log, ResultType}; -use std::{ - collections::{HashMap, HashSet}, - sync::{Arc, Mutex}, -}; +use hbb_common::{bail, platform::windows::is_windows_version_or_greater, ResultType}; +use std::sync::atomic; -// virtual display index range: 0 - 2 are reserved for headless and other special uses. -const VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS: u32 = 0; -const VIRTUAL_DISPLAY_START_FOR_PEER: u32 = 1; -const VIRTUAL_DISPLAY_MAX_COUNT: u32 = 5; +// This string is defined here. +// https://github.com/fufesou/RustDeskIddDriver/blob/b370aad3f50028b039aad211df60c8051c4a64d6/RustDeskIddDriver/RustDeskIddDriver.inf#LL73C1-L73C40 +pub const RUSTDESK_IDD_DEVICE_STRING: &'static str = "RustDeskIddDriver Device\0"; +pub const AMYUNI_IDD_DEVICE_STRING: &'static str = "USB Mobile Monitor Virtual Display\0"; -lazy_static::lazy_static! { - static ref VIRTUAL_DISPLAY_MANAGER: Arc> = - Arc::new(Mutex::new(VirtualDisplayManager::default())); +const IDD_IMPL: &str = IDD_IMPL_AMYUNI; +const IDD_IMPL_RUSTDESK: &str = "rustdesk_idd"; +const IDD_IMPL_AMYUNI: &str = "amyuni_idd"; + +const IS_CAN_PLUG_OUT_ALL_NOT_SET: i8 = 0; +const IS_CAN_PLUG_OUT_ALL_YES: i8 = 1; +const IS_CAN_PLUG_OUT_ALL_NO: i8 = 2; +static IS_CAN_PLUG_OUT_ALL: atomic::AtomicI8 = atomic::AtomicI8::new(IS_CAN_PLUG_OUT_ALL_NOT_SET); + +pub fn is_can_plug_out_all() -> bool { + IS_CAN_PLUG_OUT_ALL.load(atomic::Ordering::Relaxed) != IS_CAN_PLUG_OUT_ALL_NO } -#[derive(Default)] -struct VirtualDisplayManager { - headless_index_name: Option<(u32, String)>, - peer_index_name: HashMap, - is_driver_installed: bool, +// No need to consider concurrency here. +pub fn set_can_plug_out_all(v: bool) { + if IS_CAN_PLUG_OUT_ALL.load(atomic::Ordering::Relaxed) == IS_CAN_PLUG_OUT_ALL_NOT_SET { + IS_CAN_PLUG_OUT_ALL.store( + if v { + IS_CAN_PLUG_OUT_ALL_YES + } else { + IS_CAN_PLUG_OUT_ALL_NO + }, + atomic::Ordering::Relaxed, + ); + } } -impl VirtualDisplayManager { - fn prepare_driver(&mut self) -> ResultType<()> { - if !self.is_driver_installed { - self.install_update_driver()?; - } - Ok(()) - } +pub fn is_amyuni_idd() -> bool { + IDD_IMPL == IDD_IMPL_AMYUNI +} - fn install_update_driver(&mut self) -> ResultType<()> { - if let Err(e) = virtual_display::create_device() { - if !e.to_string().contains("Device is already created") { - bail!("Create device failed {}", e); - } - } - // Reboot is not required for this case. - let mut _reboot_required = false; - virtual_display::install_update_driver(&mut _reboot_required)?; - self.is_driver_installed = true; - Ok(()) - } - - fn plug_in_monitor(index: u32, modes: &[virtual_display::MonitorMode]) -> ResultType<()> { - if let Err(e) = virtual_display::plug_in_monitor(index) { - bail!("Plug in monitor failed {}", e); - } - if let Err(e) = virtual_display::update_monitor_modes(index, &modes) { - log::error!("Update monitor modes failed {}", e); - } - Ok(()) +pub fn get_cur_device_string() -> &'static str { + match IDD_IMPL { + IDD_IMPL_RUSTDESK => RUSTDESK_IDD_DEVICE_STRING, + IDD_IMPL_AMYUNI => AMYUNI_IDD_DEVICE_STRING, + _ => "", } } @@ -66,209 +56,519 @@ pub fn is_virtual_display_supported() -> bool { } } -pub fn install_update_driver() -> ResultType<()> { - VIRTUAL_DISPLAY_MANAGER - .lock() - .unwrap() - .install_update_driver() -} - pub fn plug_in_headless() -> ResultType<()> { - let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); - manager.prepare_driver()?; - let modes = [virtual_display::MonitorMode { - width: 1920, - height: 1080, - sync: 60, - }]; - let device_names = windows::get_device_names(); - VirtualDisplayManager::plug_in_monitor(VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS, &modes)?; - let device_name = get_new_device_name(&device_names); - manager.headless_index_name = Some((VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS, device_name)); - Ok(()) -} - -pub fn plug_out_headless() -> bool { - let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); - if let Some((index, _)) = manager.headless_index_name.take() { - if let Err(e) = virtual_display::plug_out_monitor(index) { - log::error!("Plug out monitor failed {}", e); - } - true - } else { - false + match IDD_IMPL { + IDD_IMPL_RUSTDESK => rustdesk_idd::plug_in_headless(), + IDD_IMPL_AMYUNI => amyuni_idd::plug_in_headless(), + _ => bail!("Unsupported virtual display implementation."), } } -fn get_new_device_name(device_names: &HashSet) -> String { - for _ in 0..3 { - let device_names_af = windows::get_device_names(); - let diff_names: Vec<_> = device_names_af.difference(&device_names).collect(); - if diff_names.len() == 1 { - return diff_names[0].clone(); - } else if diff_names.len() > 1 { - log::error!( - "Failed to get diff device names after plugin virtual display, more than one diff names: {:?}", - &diff_names - ); - return "".to_string(); - } - // Sleep is needed here to wait for the virtual display to be ready. - std::thread::sleep(std::time::Duration::from_millis(50)); +pub fn get_platform_additions() -> serde_json::Map { + let mut map = serde_json::Map::new(); + if !crate::platform::windows::is_self_service_running() { + return map; } - log::error!("Failed to get diff device names after plugin virtual display",); - "".to_string() -} - -pub fn get_virtual_displays() -> Vec { - VIRTUAL_DISPLAY_MANAGER - .lock() - .unwrap() - .peer_index_name - .keys() - .cloned() - .collect() -} - -pub fn plug_in_index_modes( - idx: u32, - mut modes: Vec, -) -> ResultType<()> { - let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); - manager.prepare_driver()?; - if !manager.peer_index_name.contains_key(&idx) { - let device_names = windows::get_device_names(); - if modes.is_empty() { - modes.push(virtual_display::MonitorMode { - width: 1920, - height: 1080, - sync: 60, - }); - } - match VirtualDisplayManager::plug_in_monitor(idx, modes.as_slice()) { - Ok(_) => { - let device_name = get_new_device_name(&device_names); - manager.peer_index_name.insert(idx, device_name); - } - Err(e) => { - log::error!("Plug in monitor failed {}", e); + map.insert("idd_impl".into(), serde_json::json!(IDD_IMPL)); + match IDD_IMPL { + IDD_IMPL_RUSTDESK => { + let virtual_displays = rustdesk_idd::get_virtual_displays(); + if !virtual_displays.is_empty() { + map.insert( + "rustdesk_virtual_displays".into(), + serde_json::json!(virtual_displays), + ); } } + IDD_IMPL_AMYUNI => { + let c = amyuni_idd::get_monitor_count(); + if c > 0 { + map.insert("amyuni_virtual_displays".into(), serde_json::json!(c)); + } + } + _ => {} } - Ok(()) + map } -pub fn reset_all() -> ResultType<()> { - if is_virtual_display_supported() { - return Ok(()); +#[inline] +pub fn plug_in_monitor(idx: u32, modes: Vec) -> ResultType<()> { + match IDD_IMPL { + IDD_IMPL_RUSTDESK => rustdesk_idd::plug_in_index_modes(idx, modes), + IDD_IMPL_AMYUNI => amyuni_idd::plug_in_monitor(), + _ => bail!("Unsupported virtual display implementation."), } +} - if let Err(e) = plug_out_peer_request(&get_virtual_displays()) { - log::error!("Failed to plug out virtual displays: {}", e); +pub fn plug_out_monitor(index: i32) -> ResultType<()> { + match IDD_IMPL { + IDD_IMPL_RUSTDESK => { + let indices = if index == -1 { + rustdesk_idd::get_virtual_displays() + } else { + vec![index as _] + }; + rustdesk_idd::plug_out_peer_request(&indices) + } + IDD_IMPL_AMYUNI => amyuni_idd::plug_out_monitor(index), + _ => bail!("Unsupported virtual display implementation."), } - let _ = plug_out_headless(); - Ok(()) } pub fn plug_in_peer_request(modes: Vec>) -> ResultType> { - let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); - manager.prepare_driver()?; + match IDD_IMPL { + IDD_IMPL_RUSTDESK => rustdesk_idd::plug_in_peer_request(modes), + IDD_IMPL_AMYUNI => { + amyuni_idd::plug_in_monitor()?; + Ok(vec![0]) + } + _ => bail!("Unsupported virtual display implementation."), + } +} - let mut indices: Vec = Vec::new(); - for m in modes.iter() { - for idx in VIRTUAL_DISPLAY_START_FOR_PEER..VIRTUAL_DISPLAY_MAX_COUNT { - if !manager.peer_index_name.contains_key(&idx) { - let device_names = windows::get_device_names(); - match VirtualDisplayManager::plug_in_monitor(idx, m) { - Ok(_) => { - let device_name = get_new_device_name(&device_names); - manager.peer_index_name.insert(idx, device_name); - indices.push(idx); - } - Err(e) => { - log::error!("Plug in monitor failed {}", e); - } +pub fn plug_out_monitor_indices(indices: &[u32]) -> ResultType<()> { + match IDD_IMPL { + IDD_IMPL_RUSTDESK => rustdesk_idd::plug_out_peer_request(indices), + IDD_IMPL_AMYUNI => { + for _idx in indices.iter() { + amyuni_idd::plug_out_monitor(0)?; + } + Ok(()) + } + _ => bail!("Unsupported virtual display implementation."), + } +} + +pub fn reset_all() -> ResultType<()> { + match IDD_IMPL { + IDD_IMPL_RUSTDESK => rustdesk_idd::reset_all(), + IDD_IMPL_AMYUNI => crate::privacy_mode::turn_off_privacy(0, None).unwrap_or(Ok(())), + _ => bail!("Unsupported virtual display implementation."), + } +} + +pub mod rustdesk_idd { + use super::windows; + use hbb_common::{allow_err, bail, lazy_static, log, ResultType}; + use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, Mutex}, + }; + + // virtual display index range: 0 - 2 are reserved for headless and other special uses. + const VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS: u32 = 0; + const VIRTUAL_DISPLAY_START_FOR_PEER: u32 = 1; + const VIRTUAL_DISPLAY_MAX_COUNT: u32 = 5; + + lazy_static::lazy_static! { + static ref VIRTUAL_DISPLAY_MANAGER: Arc> = + Arc::new(Mutex::new(VirtualDisplayManager::default())); + } + + #[derive(Default)] + struct VirtualDisplayManager { + headless_index_name: Option<(u32, String)>, + peer_index_name: HashMap, + is_driver_installed: bool, + } + + impl VirtualDisplayManager { + fn prepare_driver(&mut self) -> ResultType<()> { + if !self.is_driver_installed { + self.install_update_driver()?; + } + Ok(()) + } + + fn install_update_driver(&mut self) -> ResultType<()> { + if let Err(e) = virtual_display::create_device() { + if !e.to_string().contains("Device is already created") { + bail!("Create device failed {}", e); } - break; + } + // Reboot is not required for this case. + let mut _reboot_required = false; + virtual_display::install_update_driver(&mut _reboot_required)?; + self.is_driver_installed = true; + Ok(()) + } + + fn plug_in_monitor(index: u32, modes: &[virtual_display::MonitorMode]) -> ResultType<()> { + if let Err(e) = virtual_display::plug_in_monitor(index) { + bail!("Plug in monitor failed {}", e); + } + if let Err(e) = virtual_display::update_monitor_modes(index, &modes) { + log::error!("Update monitor modes failed {}", e); + } + Ok(()) + } + } + + pub fn install_update_driver() -> ResultType<()> { + VIRTUAL_DISPLAY_MANAGER + .lock() + .unwrap() + .install_update_driver() + } + + #[inline] + fn get_device_names() -> Vec { + windows::get_device_names(Some(super::RUSTDESK_IDD_DEVICE_STRING)) + } + + pub fn plug_in_headless() -> ResultType<()> { + let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + manager.prepare_driver()?; + let modes = [virtual_display::MonitorMode { + width: 1920, + height: 1080, + sync: 60, + }]; + let device_names = get_device_names().into_iter().collect(); + VirtualDisplayManager::plug_in_monitor(VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS, &modes)?; + let device_name = get_new_device_name(&device_names); + manager.headless_index_name = Some((VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS, device_name)); + Ok(()) + } + + pub fn plug_out_headless() -> bool { + let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + if let Some((index, _)) = manager.headless_index_name.take() { + if let Err(e) = virtual_display::plug_out_monitor(index) { + log::error!("Plug out monitor failed {}", e); + } + true + } else { + false + } + } + + fn get_new_device_name(device_names: &HashSet) -> String { + for _ in 0..3 { + let device_names_af: HashSet = get_device_names().into_iter().collect(); + let diff_names: Vec<_> = device_names_af.difference(&device_names).collect(); + if diff_names.len() == 1 { + return diff_names[0].clone(); + } else if diff_names.len() > 1 { + log::error!( + "Failed to get diff device names after plugin virtual display, more than one diff names: {:?}", + &diff_names + ); + return "".to_string(); + } + // Sleep is needed here to wait for the virtual display to be ready. + std::thread::sleep(std::time::Duration::from_millis(50)); + } + log::error!("Failed to get diff device names after plugin virtual display",); + "".to_string() + } + + pub fn get_virtual_displays() -> Vec { + VIRTUAL_DISPLAY_MANAGER + .lock() + .unwrap() + .peer_index_name + .keys() + .cloned() + .collect() + } + + pub fn plug_in_index_modes( + idx: u32, + mut modes: Vec, + ) -> ResultType<()> { + let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + manager.prepare_driver()?; + if !manager.peer_index_name.contains_key(&idx) { + let device_names = get_device_names().into_iter().collect(); + if modes.is_empty() { + modes.push(virtual_display::MonitorMode { + width: 1920, + height: 1080, + sync: 60, + }); + } + match VirtualDisplayManager::plug_in_monitor(idx, modes.as_slice()) { + Ok(_) => { + let device_name = get_new_device_name(&device_names); + manager.peer_index_name.insert(idx, device_name); + } + Err(e) => { + log::error!("Plug in monitor failed {}", e); + } + } + } + Ok(()) + } + + pub fn reset_all() -> ResultType<()> { + if super::is_virtual_display_supported() { + return Ok(()); + } + + if let Err(e) = plug_out_peer_request(&get_virtual_displays()) { + log::error!("Failed to plug out virtual displays: {}", e); + } + let _ = plug_out_headless(); + Ok(()) + } + + pub fn plug_in_peer_request( + modes: Vec>, + ) -> ResultType> { + let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + manager.prepare_driver()?; + + let mut indices: Vec = Vec::new(); + for m in modes.iter() { + for idx in VIRTUAL_DISPLAY_START_FOR_PEER..VIRTUAL_DISPLAY_MAX_COUNT { + if !manager.peer_index_name.contains_key(&idx) { + let device_names = get_device_names().into_iter().collect(); + match VirtualDisplayManager::plug_in_monitor(idx, m) { + Ok(_) => { + let device_name = get_new_device_name(&device_names); + manager.peer_index_name.insert(idx, device_name); + indices.push(idx); + } + Err(e) => { + log::error!("Plug in monitor failed {}", e); + } + } + break; + } + } + } + + Ok(indices) + } + + pub fn plug_out_peer_request(indices: &[u32]) -> ResultType<()> { + let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + for idx in indices.iter() { + if manager.peer_index_name.contains_key(idx) { + allow_err!(virtual_display::plug_out_monitor(*idx)); + manager.peer_index_name.remove(idx); + } + } + Ok(()) + } + + pub fn is_virtual_display(name: &str) -> bool { + let lock = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + if let Some((_, device_name)) = &lock.headless_index_name { + if windows::is_device_name(device_name, name) { + return true; + } + } + for (_, v) in lock.peer_index_name.iter() { + if windows::is_device_name(v, name) { + return true; + } + } + false + } + + fn change_resolution(index: u32, w: u32, h: u32) -> bool { + let modes = [virtual_display::MonitorMode { + width: w, + height: h, + sync: 60, + }]; + match virtual_display::update_monitor_modes(index, &modes) { + Ok(_) => true, + Err(e) => { + log::error!("Update monitor {} modes {:?} failed: {}", index, &modes, e); + false } } } - Ok(indices) -} + pub fn change_resolution_if_is_virtual_display(name: &str, w: u32, h: u32) -> Option { + let lock = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + if let Some((index, device_name)) = &lock.headless_index_name { + if windows::is_device_name(device_name, name) { + return Some(change_resolution(*index, w, h)); + } + } -pub fn plug_out_peer_request(indices: &[u32]) -> ResultType<()> { - let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); - for idx in indices.iter() { - if manager.peer_index_name.contains_key(idx) { - allow_err!(virtual_display::plug_out_monitor(*idx)); - manager.peer_index_name.remove(idx); - } - } - Ok(()) -} - -pub fn is_virtual_display(name: &str) -> bool { - let lock = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); - if let Some((_, device_name)) = &lock.headless_index_name { - if windows::is_device_name(device_name, name) { - return true; - } - } - for (_, v) in lock.peer_index_name.iter() { - if windows::is_device_name(v, name) { - return true; - } - } - false -} - -fn change_resolution(index: u32, w: u32, h: u32) -> bool { - let modes = [virtual_display::MonitorMode { - width: w, - height: h, - sync: 60, - }]; - match virtual_display::update_monitor_modes(index, &modes) { - Ok(_) => true, - Err(e) => { - log::error!("Update monitor {} modes {:?} failed: {}", index, &modes, e); - false + for (k, v) in lock.peer_index_name.iter() { + if windows::is_device_name(v, name) { + return Some(change_resolution(*k, w, h)); + } } + None } } -pub fn change_resolution_if_is_virtual_display(name: &str, w: u32, h: u32) -> Option { - let lock = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); - if let Some((index, device_name)) = &lock.headless_index_name { - if windows::is_device_name(device_name, name) { - return Some(change_resolution(*index, w, h)); - } +pub mod amyuni_idd { + use super::windows; + use crate::platform::win_device; + use hbb_common::{bail, lazy_static, log, ResultType}; + use std::sync::{Arc, Mutex}; + use winapi::shared::guiddef::GUID; + + const INF_PATH: &str = r#"usbmmidd_v2\usbmmIdd.inf"#; + const INTERFACE_GUID: GUID = GUID { + Data1: 0xb5ffd75f, + Data2: 0xda40, + Data3: 0x4353, + Data4: [0x8f, 0xf8, 0xb6, 0xda, 0xf6, 0xf1, 0xd8, 0xca], + }; + const HARDWARE_ID: &str = "usbmmidd"; + const PLUG_MONITOR_IO_CONTROL_CDOE: u32 = 2307084; + + lazy_static::lazy_static! { + static ref LOCK: Arc> = Default::default(); } - for (k, v) in lock.peer_index_name.iter() { - if windows::is_device_name(v, name) { - return Some(change_resolution(*k, w, h)); + pub fn uninstall_driver() -> ResultType<()> { + let mut reboot_required = false; + unsafe { + win_device::uninstall_driver(HARDWARE_ID, &mut reboot_required)?; } + Ok(()) + } + + fn check_install_driver() -> ResultType<()> { + let _l = LOCK.lock().unwrap(); + let drivers = windows::get_display_drivers(); + if drivers + .iter() + .any(|(s, c)| s == super::AMYUNI_IDD_DEVICE_STRING && *c == 0) + { + return Ok(()); + } + + let exe_file = std::env::current_exe()?; + let Some(cur_dir) = exe_file.parent() else { + bail!("Cannot get parent of current exe file"); + }; + + let inf_path = cur_dir.join(INF_PATH); + if !inf_path.exists() { + bail!("Driver inf file not found."); + } + let inf_path = inf_path.to_string_lossy().to_string(); + + let mut reboot_required = false; + unsafe { + win_device::install_driver(&inf_path, HARDWARE_ID, &mut reboot_required)?; + } + Ok(()) + } + + #[inline] + fn plug_in_monitor_(add: bool) -> ResultType<()> { + let cmd = if add { 0x10 } else { 0x00 }; + let cmd = [cmd, 0x00, 0x00, 0x00]; + unsafe { + win_device::device_io_control(&INTERFACE_GUID, PLUG_MONITOR_IO_CONTROL_CDOE, &cmd, 0)?; + } + Ok(()) + } + + pub fn plug_in_headless() -> ResultType<()> { + if get_monitor_count() > 0 { + return Ok(()); + } + + if let Err(e) = check_install_driver() { + log::error!("Failed to install driver: {}", e); + bail!("Failed to install driver."); + } + + plug_in_monitor_(true) + } + + pub fn plug_in_monitor() -> ResultType<()> { + if let Err(e) = check_install_driver() { + log::error!("Failed to install driver: {}", e); + bail!("Failed to install driver."); + } + + if get_monitor_count() == 4 { + bail!("There are already 4 monitors plugged in."); + } + + plug_in_monitor_(true) + } + + pub fn plug_out_monitor(index: i32) -> ResultType<()> { + let all_count = windows::get_device_names(None).len(); + let amyuni_count = get_monitor_count(); + let mut to_plug_out_count = match all_count { + 0 => return Ok(()), + 1 => { + if amyuni_count == 0 { + bail!("No virtual displays to plug out.") + } else { + if super::is_can_plug_out_all() { + 1 + } else { + bail!("This only virtual display cannot be pulled out.") + } + } + } + _ => { + if all_count == amyuni_count { + if super::is_can_plug_out_all() { + all_count + } else { + all_count - 1 + } + } else { + amyuni_count + } + } + }; + if to_plug_out_count != 0 && index != -1 { + to_plug_out_count = 1; + } + for _i in 0..to_plug_out_count { + let _ = plug_in_monitor_(false); + } + Ok(()) + } + + #[inline] + pub fn get_monitor_count() -> usize { + windows::get_device_names(Some(super::AMYUNI_IDD_DEVICE_STRING)).len() + } + + #[inline] + pub fn is_my_display(name: &str) -> bool { + windows::get_device_names(Some(super::AMYUNI_IDD_DEVICE_STRING)) + .iter() + .any(|s| windows::is_device_name(s, name)) } - None } mod windows { - use std::{collections::HashSet, ptr::null_mut}; + use std::ptr::null_mut; use winapi::{ - shared::minwindef::{DWORD, FALSE}, + shared::{ + devguid::GUID_DEVCLASS_DISPLAY, + minwindef::{DWORD, FALSE}, + ntdef::ULONG, + }, um::{ + cfgmgr32::{CM_Get_DevNode_Status, CR_SUCCESS}, + cguid::GUID_NULL, + setupapi::{ + SetupDiEnumDeviceInfo, SetupDiGetClassDevsW, SetupDiGetDeviceRegistryPropertyW, + SP_DEVINFO_DATA, + }, wingdi::{ DEVMODEW, DISPLAY_DEVICEW, DISPLAY_DEVICE_ACTIVE, DISPLAY_DEVICE_MIRRORING_DRIVER, }, + winnt::HANDLE, winuser::{EnumDisplayDevicesW, EnumDisplaySettingsExW, ENUM_CURRENT_SETTINGS}, }, }; - // This string is defined here. - // https://github.com/fufesou/RustDeskIddDriver/blob/b370aad3f50028b039aad211df60c8051c4a64d6/RustDeskIddDriver/RustDeskIddDriver.inf#LL73C1-L73C40 - const IDD_DEVICE_STRING: &'static str = "RustDeskIddDriver Device\0"; + const DIGCF_PRESENT: DWORD = 0x00000002; + const SPDRP_DEVICEDESC: DWORD = 0x00000000; + const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE; #[inline] pub(super) fn is_device_name(device_name: &str, name: &str) -> bool { @@ -281,8 +581,8 @@ mod windows { } } - pub(super) fn get_device_names() -> HashSet { - let mut device_names = HashSet::new(); + pub(super) fn get_device_names(device_string: Option<&str>) -> Vec { + let mut device_names = Vec::new(); let mut dd: DISPLAY_DEVICEW = unsafe { std::mem::zeroed() }; dd.cb = std::mem::size_of::() as DWORD; let mut i_dev_num = 0; @@ -317,15 +617,115 @@ mod windows { continue; } - if let (Ok(device_name), Ok(device_string)) = ( + if let (Ok(device_name), Ok(ds)) = ( String::from_utf16(&dd.DeviceName), String::from_utf16(&dd.DeviceString), ) { - if &device_string[..IDD_DEVICE_STRING.len()] == IDD_DEVICE_STRING { - device_names.insert(device_name); + if let Some(s) = device_string { + if ds.len() >= s.len() && &ds[..s.len()] == s { + device_names.push(device_name); + } + } else { + device_names.push(device_name); } } } device_names } + + pub(super) fn get_display_drivers() -> Vec<(String, u32)> { + let mut display_drivers: Vec<(String, u32)> = Vec::new(); + + let device_info_set = unsafe { + SetupDiGetClassDevsW( + &GUID_DEVCLASS_DISPLAY, + null_mut(), + null_mut(), + DIGCF_PRESENT, + ) + }; + + if device_info_set == INVALID_HANDLE_VALUE { + println!( + "Failed to get device information set. Error: {}", + std::io::Error::last_os_error() + ); + return display_drivers; + } + + let mut device_info_data = SP_DEVINFO_DATA { + cbSize: std::mem::size_of::() as u32, + ClassGuid: GUID_NULL, + DevInst: 0, + Reserved: 0, + }; + + let mut device_index = 0; + loop { + let result = unsafe { + SetupDiEnumDeviceInfo(device_info_set, device_index, &mut device_info_data) + }; + if result == 0 { + break; + } + + let mut data_type: DWORD = 0; + let mut required_size: DWORD = 0; + + // Get the required buffer size for the driver description + let mut buffer; + unsafe { + SetupDiGetDeviceRegistryPropertyW( + device_info_set, + &mut device_info_data, + SPDRP_DEVICEDESC, + &mut data_type, + null_mut(), + 0, + &mut required_size, + ); + + buffer = vec![0; required_size as usize / 2]; + SetupDiGetDeviceRegistryPropertyW( + device_info_set, + &mut device_info_data, + SPDRP_DEVICEDESC, + &mut data_type, + buffer.as_mut_ptr() as *mut u8, + required_size, + null_mut(), + ); + } + + let Ok(driver_description) = String::from_utf16(&buffer) else { + println!("Failed to convert driver description to string"); + device_index += 1; + continue; + }; + + let mut status: ULONG = 0; + let mut problem_number: ULONG = 0; + // Get the device status and problem number + let config_ret = unsafe { + CM_Get_DevNode_Status( + &mut status, + &mut problem_number, + device_info_data.DevInst, + 0, + ) + }; + if config_ret != CR_SUCCESS { + println!( + "Failed to get device status. Error: {}", + std::io::Error::last_os_error() + ); + device_index += 1; + continue; + } + display_drivers.push((driver_description, problem_number)); + device_index += 1; + } + + display_drivers + } }