diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cd82c75e3..953196eb3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,7 +10,7 @@ "features": { "ghcr.io/devcontainers/features/java:1": {}, "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": { - "PACKAGES": "platform-tools,ndk;22.1.7171670" + "PACKAGES": "platform-tools,ndk;23.2.8568313" } }, "customizations": { @@ -31,4 +31,4 @@ } } } -} \ No newline at end of file +} diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml new file mode 100644 index 000000000..21d8fbce5 --- /dev/null +++ b/.github/workflows/bridge.yml @@ -0,0 +1,75 @@ +# This yaml shares the build bridge steps with ci and nightly. +name: Build flutter-rust-bridge +# 2023-04-19 15:48:00+00:00 + +on: + workflow_call: + +jobs: + generate_bridge: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install prerequisites + run: | + sudo apt install ca-certificates -y + sudo apt update -y + sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: bridge-${{ matrix.job.os }} + workspace: "/tmp/flutter_rust_bridge/frb_codegen" + + - name: Cache Bridge + id: cache-bridge + uses: actions/cache@v3 + with: + path: /tmp/flutter_rust_bridge + key: vcpkg-${{ matrix.job.arch }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen + pushd flutter && flutter pub get && popd + + - name: Run flutter rust bridge + run: | + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Upload Artifact + uses: actions/upload-artifact@master + with: + name: bridge-artifact + path: | + ./src/bridge_generated.rs + ./src/bridge_generated.io.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml new file mode 100644 index 000000000..ae7db7135 --- /dev/null +++ b/.github/workflows/flutter-build.yml @@ -0,0 +1,1709 @@ +name: Build the flutter version of the RustDesk + +on: + workflow_call: + inputs: + upload-artifact: + type: boolean + default: true + +env: + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.7.0" + TAG_NAME: "nightly" + # vcpkg version: 2022.05.10 + # for multiarch gcc compatibility + VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" + VERSION: "1.2.0" + NDK_VERSION: "r23" + #signing keys env variable checks + ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}' + MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}' + # To make a custom build with your own servers set the below secret values + RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' + RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' + UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}" + +jobs: + build-for-windows-flutter: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + 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 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + 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/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + run: | + cargo install flutter_rust_bridge_codegen + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Install vcpkg dependencies + run: | + cd C:\ + git clone https://github.com/Kingtous/rustdesk_thirdpary_lib --depth=1 + + - name: Build rustdesk + env: + VCPKG_ROOT: C:\rustdesk_thirdpary_lib\vcpkg + run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver + + - name: Sign rustdesk files + uses: GermanBluefox/code-sign-action@v7 + if: env.UPLOAD_ARTIFACT == 'true' + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.CERTNAME }}' + folder: './flutter/build/windows/runner/Release/' + recursive: true + + - name: Build self-extracted executable + shell: bash + if: env.UPLOAD_ARTIFACT == 'true' + run: | + pushd ./libs/portable + python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.exe + + - name: Sign rustdesk self-extracted file + uses: GermanBluefox/code-sign-action@v7 + if: env.UPLOAD_ARTIFACT == 'true' + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.WINDOWS_PFX_NAME }}' + folder: './SignOutput' + recursive: false + + - name: Publish Release + uses: softprops/action-gh-release@v1 + if: env.UPLOAD_ARTIFACT == 'true' + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./SignOutput/rustdesk-*.exe + + # The fallback for the flutter version, we use Sciter for 32bit Windows. + build-for-windows-sciter: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + # Temporarily disable this action due to additional test is needed. + # if: false + strategy: + 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 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install LLVM and Clang + uses: Kingtous/install-llvm-action-32bit@master + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly-${{ matrix.job.target }} + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }}-sciter + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + cd C:\ + git clone https://github.com/Kingtous/rustdesk_thirdpary_lib --depth=1 + + - name: Build rustdesk + id: build + shell: bash + env: + VCPKG_ROOT: C:\rustdesk_thirdpary_lib\vcpkg + run: | + python3 res/inline-sciter.py + # Patch sciter x86 + sed -i 's/branch = "dyn"/branch = "dyn_x86"/g' ./Cargo.toml + # Replace the link for the ico. + rm res/icon.ico && cp flutter/windows/runner/resources/app_icon.ico res/icon.ico + cargo build --features inline --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 + + - name: Sign rustdesk files + uses: GermanBluefox/code-sign-action@v7 + if: env.UPLOAD_ARTIFACT == 'true' + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.CERTNAME }}' + folder: './Release/' + recursive: true + + - name: Build self-extracted executable + shell: bash + run: | + pushd ./libs/portable + pip3 install -r requirements.txt + python3 ./generate.py -f ../../Release/ -o . -e ../../Release/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe + + - name: Sign rustdesk self-extracted file + uses: GermanBluefox/code-sign-action@v7 + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.WINDOWS_PFX_NAME }}' + folder: './SignOutput' + recursive: false + + - name: Publish Release + uses: softprops/action-gh-release@v1 + if: env.UPLOAD_ARTIFACT == 'true' + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./SignOutput/rustdesk-*.exe + + build-for-macOS: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-latest, + extra-build-args: "", + arch: x86_64 + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Import the codesign cert + if: env.MACOS_P12_BASE64 != null + uses: apple-actions/import-codesign-certs@v1 + with: + p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} + p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + keychain: rustdesk + + - name: Check sign and import sign key + if: env.MACOS_P12_BASE64 != null + run: | + security default-keychain -s rustdesk.keychain + security find-identity -v + + - name: Import notarize key + if: env.MACOS_P12_BASE64 != null + uses: timheuer/base64-to-file@v1.2 + with: + # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling + fileName: rustdesk.json + fileDir: ${{ github.workspace }} + encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} + + - name: Install rcodesign tool + if: env.MACOS_P12_BASE64 != null + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + + - name: Install build runtime + run: | + brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx libyuv opus + + - name: Show version information (Rust, cargo, Clang) + shell: bash + run: | + clang --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: Build rustdesk + run: | + # --hwcodec not supported on macos yet + ./build.py --flutter ${{ matrix.job.extra-build-args }} + + - name: Codesign app and create signed dmg + if: env.MACOS_P12_BASE64 != null + run: | + security default-keychain -s rustdesk.keychain + security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain + # start sign the rustdesk.app and dmg + rm rustdesk-${{ env.VERSION }}.dmg || true + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv + create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv + # notarize the rustdesk-${{ env.VERSION }}.dmg + rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg + + - name: Rename rustdesk + run: | + for name in rustdesk*??.dmg; do + mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}.dmg" + done + + - name: Publish DMG package + if: env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk*-${{ matrix.job.arch }}.dmg + + build-vcpkg-deps-linux: + uses: ./.github/workflows/vcpkg-deps-linux.yml + + generate-bridge-linux: + uses: ./.github/workflows/bridge.yml + + build-rustdesk-ios: + needs: [generate-bridge-linux] + name: build rustdesk ios ipa ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + arch: aarch64, + target: aarch64-apple-ios, + os: macos-latest, + extra-build-features: "", + } + steps: + - name: Install dependencies + run: | + brew install nasm yasm + - name: Checkout source code + uses: actions/checkout@v3 + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Clone deps + shell: bash + run: | + pushd /opt + sudo git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + + - name: Disable rust bridge build + shell: bash + run: | + sed -i build.rs.bak "s/gen_flutter_rust_bridge();/\/\//g" build.rs + + - name: Build rustdesk lib + env: + VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg + run: | + rustup target add ${{ matrix.job.target }} + cargo build --features flutter --release --target aarch64-apple-ios --lib + + - name: Build rustdesk + shell: bash + run: | + pushd flutter + flutter build ios --release + + # - name: Upload Artifacts + # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + # uses: actions/upload-artifact@master + # with: + # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + # path: flutter/build/ios/ipa/*.ipa + + # - name: Publish ipa package + # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # flutter/build/ios/ipa/*.ipa + + + build-rustdesk-android: + needs: [generate-bridge-linux] + name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + arch: aarch64, + target: aarch64-linux-android, + os: ubuntu-20.04, + extra-build-features: "", + openssl-arch: android-arm64 + } + - { + arch: armv7, + target: armv7-linux-androideabi, + os: ubuntu-20.04, + extra-build-features: "", + openssl-arch: android-arm + } + steps: + - name: Install dependencies + run: | + sudo apt update + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libpam0g-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless + - name: Checkout source code + uses: actions/checkout@v3 + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: ${{ env.NDK_VERSION }} + add-to-path: true + + - name: Clone deps + shell: bash + run: | + pushd /opt + git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + + - name: Build rustdesk lib + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg + run: | + rustup target add ${{ matrix.job.target }} + cargo install cargo-ndk + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + esac + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/arm64-v8a/*.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/armeabi-v7a/*.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + ;; + esac + popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk . + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + if: env.ANDROID_SIGNING_KEY != null + id: sign-rustdesk + with: + releaseDirectory: ./signed-apk + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} + env: + # override default build-tools version (29.0.3) -- optional + BUILD_TOOLS_VERSION: "30.0.2" + + - name: Upload Artifacts + if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + uses: actions/upload-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish signed apk package + if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish unsigned apk package + if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk + + build-rustdesk-lib-linux-amd64: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "", + enable-headless: true + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "flatpak", + enable-headless: false + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "appimage", + enable-headless: false + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + # not ready yet + # distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libpam0g-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + x86_64) + # no need mock on x86_64 + export VCPKG_ROOT=/opt/artifacts/vcpkg + export DEFAULT_FEAT="" + if ${{ matrix.job.enable-headless }}; then + export DEFAULT_FEAT=linux_headless + fi + cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }},$DEFAULT_FEAT --release + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-lib-linux-arm: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-20.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "", + enable-headless: true + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-20.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + enable-headless: false + } + # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { + # arch: armv7, + # target: armv7-unknown-linux-gnueabihf, + # os: ubuntu-20.04, + # use-cross: true, + # extra-build-features: "", + # } + # - { arch: armv7, target: armv7-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "appimage" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libpam0g-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + export VCPKG_ROOT=/opt/artifacts/vcpkg + export DEFAULT_FEAT="" + if ${{ matrix.job.enable-headless }}; then + export DEFAULT_FEAT=linux_headless + fi + cargo build --lib --features flutter,${{ matrix.job.extra-build-features }},$DEFAULT_FEAT --release + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-sciter-arm: + needs: [build-vcpkg-deps-linux] + name: build-rustdesk(sciter) ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + - { + arch: armv7, + target: armv7-unknown-linux-gnueabihf, + deb-arch: armhf, + os: ubuntu-latest, + use-cross: true, + extra-build-features: "", + enable-headless: true + } + # - { arch: armv7, target: armv7-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "appimage" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk sciter binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm libclang-dev + apt-get -qq install -y libdbus-1-dev pkg-config nasm yasm libglib2.0-dev libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev + apt-get -qq install -y libpulse-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvpx-dev libvdpau-dev libva-dev libpam0g-dev + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + + # build + pushd /workspace + python3 ./res/inline-sciter.py + export VCPKG_ROOT=/opt/artifacts/vcpkg + export ARCH=armhf + export DEFAULT_FEAT="" + if ${{ matrix.job.enable-headless }}; then + export DEFAULT_FEAT=linux_headless + fi + cargo build --features inline,${{ matrix.job.extra-build-features }},$DEFAULT_FEAT --release --bins + # package + mkdir -p ./Release + mv ./target/release/rustdesk ./Release/rustdesk + wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/arm32/libsciter-gtk.so + ./build.py --package ./Release + + - name: Rename rustdesk + shell: bash + run: | + for name in rustdesk*??.deb; do + # use cp to duplicate deb files to fit other packages. + cp "$name" "${name%%.deb}-${{ matrix.job.arch }}-sciter.deb" + done + + - name: Publish debian package + if: matrix.job.extra-build-features == '' && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb + + - name: Upload Artifact + uses: actions/upload-artifact@master + if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb + + build-rustdesk-linux-arm: + needs: [build-rustdesk-lib-linux-arm] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 # 20.04 has more performance on arm build + strategy: + fail-fast: false + matrix: + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } + # - { + # arch: aarch64, + # target: aarch64-unknown-linux-gnu, + # os: ubuntu-18.04, # just for naming package, not running host + # use-cross: true, + # extra-build-features: "flatpak", + # } + # - { arch: armv7, target: armv7-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } + # - { arch: armv7, target: armv7-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "appimage" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - name: Download Flutter + shell: bash + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /opt + # clone repo and reset to flutter 3.7.0 + git clone https://github.com/sony/flutter-elinux.git || true + pushd flutter-elinux + # reset to flutter 3.7.0 + git fetch + git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 + popd + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/flutter-elinux:/opt/flutter-elinux" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # we use flutter-elinux to build our rustdesk + export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux. Run doctor to check if issues here. + flutter-elinux doctor -v + # Patch arm64 engine for flutter 3.6.0+ + flutter-elinux precache --linux + pushd /tmp + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz + tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + popd + # edit to corresponding arch + case ${{ matrix.job.arch }} in + aarch64) + export ARCH=arm64 + sed -i "s/x64\/release/arm64\/release/g" ./build.py + ;; + armv7) + export ARCH=armhf + sed -i "s/x64\/release/arm\/release/g" ./build.py + ;; + esac + python3 ./build.py --flutter --hwcodec --skip-cargo + # rpm package + echo -e "start packaging fedora package" + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec + sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter.spec + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" + done + # rpm suse package + echo -e "start packaging suse package" + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec + sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter-suse.spec + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter-suse.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm" + done + + - name: Rename rustdesk + shell: bash + run: | + for name in rustdesk*??.deb; do + cp "$name" "${name%%.deb}-${{ matrix.job.arch }}.deb" + done + + - name: Publish debian package + if: matrix.job.extra-build-features == '' && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb + + - name: Build appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + shell: bash + run: | + # set-up appimage-builder + pushd /tmp + wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage + chmod +x appimage-builder-x86_64.AppImage + sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder + popd + # run appimage-builder + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml + + - name: Publish appimage package + if: matrix.job.extra-build-features == 'appimage' && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage + + - name: Upload Artifact + uses: actions/upload-artifact@master + if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb + + - name: Patch archlinux PKGBUILD + if: ${{ matrix.job.extra-build-features == '' }} + run: | + sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/linux\/x64/linux\/arm/g" ./res/PKGBUILD + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD + ;; + esac + + # Temporary disable for there is no many archlinux arm hosts + # - name: Build archlinux package + # if: ${{ matrix.job.extra-build-features == '' }} + # uses: vufa/arch-makepkg-action@master + # with: + # packages: > + # llvm + # clang + # libva + # libvdpau + # rust + # gstreamer + # unzip + # git + # cmake + # gcc + # curl + # wget + # yasm + # nasm + # zip + # make + # pkg-config + # clang + # gtk3 + # xdotool + # libxcb + # libxfixes + # alsa-lib + # pipewire + # python + # ttf-arphic-uming + # libappindicator-gtk3 + # scripts: | + # cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + # - name: Publish archlinux package + # if: ${{ matrix.job.extra-build-features == '' }} + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # res/rustdesk*.zst + + - name: Publish fedora28/centos8 package + if: matrix.job.extra-build-features == '' && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + /opt/artifacts/rpm/*.rpm + + build-rustdesk-linux-amd64: + needs: [build-rustdesk-lib-linux-amd64] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # Setup Flutter + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + ls -l . + export PATH=/opt/flutter/bin:$PATH + flutter doctor -v + pushd /workspace + python3 ./build.py --flutter --hwcodec --skip-cargo + # rpm package + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" + done + # rpm suse package + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter-suse.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm" + done + + - name: Rename rustdesk + shell: bash + run: | + for name in rustdesk*??.deb; do + # use cp to duplicate deb files to fit other packages. + cp "$name" "${name%%.deb}-${{ matrix.job.arch }}.deb" + done + + - name: Publish debian package + if: matrix.job.extra-build-features == '' && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb + + - name: Upload Artifact + uses: actions/upload-artifact@master + if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb + + - name: Patch archlinux PKGBUILD + if: ${{ matrix.job.extra-build-features == '' }} + run: | + sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD + + - name: Build archlinux package + if: ${{ matrix.job.extra-build-features == '' }} + uses: vufa/arch-makepkg-action@master + with: + packages: > + llvm + clang + libva + libvdpau + rust + gstreamer + unzip + git + cmake + gcc + curl + wget + yasm + nasm + zip + make + pkg-config + clang + gtk3 + xdotool + libxcb + libxfixes + alsa-lib + pipewire + python + ttf-arphic-uming + libappindicator-gtk3 + pam + gst-plugins-base + gst-plugin-pipewire + scripts: | + cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + - name: Publish archlinux package + if: matrix.job.extra-build-features == '' && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + res/rustdesk*.zst + + - name: Build appimage package + if: matrix.job.extra-build-features == 'appimage' && env.UPLOAD_ARTIFACT == 'true' + shell: bash + run: | + # set-up appimage-builder + pushd /tmp + wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage + chmod +x appimage-builder-x86_64.AppImage + sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder + popd + # run appimage-builder + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-x86_64.yml + + - name: Publish appimage package + if: matrix.job.extra-build-features == 'appimage' && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage + + - name: Publish fedora28/centos8 package + if: matrix.job.extra-build-features == '' && env.UPLOAD_ARTIFACT == 'true' + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + /opt/artifacts/rpm/*.rpm + + # Temporary disable flatpak arm build + # + # build-flatpak-arm: + # name: Build Flatpak + # needs: [build-rustdesk-linux-arm] + # runs-on: ${{ matrix.job.os }} + # strategy: + # fail-fast: false + # matrix: + # job: + # # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, arch: arm64 } + # - { target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, arch: arm64 } + # steps: + # - name: Checkout source code + # uses: actions/checkout@v3 + + # - name: Download Binary + # uses: actions/download-artifact@master + # with: + # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + # path: . + + # - name: Rename Binary + # run: | + # mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + + # - uses: Kingtous/run-on-arch-action@amd64-support + # name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + # id: rpm + # with: + # arch: ${{ matrix.job.arch }} + # distro: ubuntu18.04 + # githubToken: ${{ github.token }} + # setup: | + # ls -l "${PWD}" + # dockerRunArgs: | + # --volume "${PWD}:/workspace" + # shell: /bin/bash + # install: | + # apt update -y + # apt install -y rpm + # run: | + # pushd /workspace + # # install + # apt update -y + # apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git + # # flatpak deps + # flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + # flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + # flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + # # package + # pushd flatpak + # git clone https://github.com/flathub/shared-modules.git --depth=1 + # flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + # flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk + + # - name: Publish flatpak package + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak + + build-flatpak-amd64: + name: Build Flatpak + needs: [build-rustdesk-linux-amd64] + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + job: + - { target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, arch: x86_64 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Download Binary + uses: actions/download-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb + path: . + + - name: Rename Binary + run: | + mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + id: rpm + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + shell: /bin/bash + install: | + apt update -y + apt install -y rpm git wget curl + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # install + apt update -y + apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git + # flatpak deps + flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + # package + pushd flatpak + git clone https://github.com/flathub/shared-modules.git --depth=1 + flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak com.rustdesk.RustDesk + + - name: Publish flatpak package + uses: softprops/action-gh-release@v1 + if: env.UPLOAD_ARTIFACT == 'true' + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index a4ddb15b5..d40d6f736 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -16,959 +16,9 @@ on: - "docs/**" - "README.md" -env: - LLVM_VERSION: "15.0.6" - FLUTTER_VERSION: "3.7.5" - # vcpkg version: 2022.05.10 - # for multiarch gcc compatibility - VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" - VERSION: "1.2.0" - NDK_VERSION: "r23" - jobs: - build-for-windows: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: true - 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 } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Install LLVM and Clang - uses: KyleMayes/install-llvm-action@v1 - with: - version: ${{ env.LLVM_VERSION }} - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - 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/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip - Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - components: rustfmt - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.job.os }} - - - name: Install flutter rust bridge deps - run: | - cargo install flutter_rust_bridge_codegen - Push-Location flutter ; flutter pub get ; Pop-Location - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Restore from cache and install vcpkg - uses: lukka/run-vcpkg@v7 - with: - setupOnly: true - vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - - - name: Install vcpkg dependencies - run: | - $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static - shell: bash - - - name: Build rustdesk - run: python3 .\build.py --portable --hwcodec --flutter - - build-for-macOS: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: true - matrix: - job: - - { - target: x86_64-apple-darwin, - os: macos-latest, - extra-build-args: "", - } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Install build runtime - run: | - brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.job.os }} - - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen - pushd flutter && flutter pub get && popd - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Restore from cache and install vcpkg - uses: lukka/run-vcpkg@v7 - with: - setupOnly: true - vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - - - name: Install vcpkg dependencies - run: | - $VCPKG_ROOT/vcpkg install libvpx libyuv opus - - - name: Show version information (Rust, cargo, Clang) - shell: bash - run: | - clang --version || true - rustup -V - rustup toolchain list - rustup default - cargo -V - rustc -V - - - name: Build rustdesk - run: | - # --hwcodec not supported on macos yet - ./build.py --flutter ${{ matrix.job.extra-build-args }} - - build-vcpkg-deps-linux: - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: true - matrix: - job: - # - { arch: armv7, os: ubuntu-20.04 } - - { arch: x86_64, os: ubuntu-20.04 } - - { arch: aarch64, os: ubuntu-20.04 } - steps: - - name: Create vcpkg artifacts folder - run: mkdir -p /opt/artifacts - - - name: Cache Vcpkg - id: cache-vcpkg - uses: actions/cache@v3 - with: - path: /opt/artifacts - key: vcpkg-${{ matrix.job.arch }} - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Run vcpkg install on ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "/opt/artifacts" - dockerRunArgs: | - --volume "/opt/artifacts:/artifacts" - shell: /bin/bash - install: | - apt update -y - case "${{ matrix.job.arch }}" in - x86_64) - # CMake 3.15+ - apt install -y gpg wget ca-certificates - echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null - wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null - apt update -y - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev - ;; - aarch64|armv7) - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool - esac - cmake --version - gcc -v - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - case "${{ matrix.job.arch }}" in - x86_64) - export VCPKG_FORCE_SYSTEM_BINARIES=1 - pushd /artifacts - git clone https://github.com/microsoft/vcpkg.git || true - pushd vcpkg - git reset --hard ${{ env.VCPKG_COMMIT_ID }} - ./bootstrap-vcpkg.sh - ./vcpkg install libvpx libyuv opus - ;; - aarch64|armv7) - pushd /artifacts - # libyuv - git clone https://chromium.googlesource.com/libyuv/libyuv || true - pushd libyuv - git pull - mkdir -p build - pushd build - mkdir -p /artifacts/vcpkg/installed - cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed - make -j4 && make install - popd - popd - # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC - wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz - tar -zxvf opus.tar.gz; ls -l - pushd opus-1.1.2 - ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed - make -j4; make install - ;; - esac - - name: Upload artifacts - uses: actions/upload-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: | - /opt/artifacts/vcpkg/installed - - generate-bridge-linux: - name: generate bridge - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: true - matrix: - job: - - { - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-args: "", - } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Install prerequisites - run: | - sudo apt update -y - sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: bridge-${{ matrix.job.os }} - workspace: "/tmp/flutter_rust_bridge/frb_codegen" - - - name: Cache Bridge - id: cache-bridge - uses: actions/cache@v3 - with: - path: /tmp/flutter_rust_bridge - key: vcpkg-${{ matrix.job.arch }} - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen - pushd flutter && flutter pub get && popd - - - name: Run flutter rust bridge - run: | - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Upload Artifact - uses: actions/upload-artifact@master - with: - name: bridge-artifact - path: | - ./src/bridge_generated.rs - ./src/bridge_generated.io.rs - ./flutter/lib/generated_bridge.dart - ./flutter/lib/generated_bridge.freezed.dart - - build-rustdesk-android: - needs: [generate-bridge-linux] - name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: true - matrix: - job: - - { - arch: x86_64, - target: aarch64-linux-android, - os: ubuntu-20.04, - extra-build-features: "", - openssl-arch: android-arm64 - } - - { - arch: x86_64, - target: armv7-linux-androideabi, - os: ubuntu-18.04, - extra-build-features: "", - openssl-arch: android-arm - } - steps: - - name: Install dependencies - run: | - sudo apt update - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless - - name: Checkout source code - uses: actions/checkout@v3 - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - uses: nttld/setup-ndk@v1 - id: setup-ndk - with: - ndk-version: ${{ env.NDK_VERSION }} - add-to-path: true - - - name: Clone deps - shell: bash - run: | - pushd /opt - git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1 - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - - - name: Build rustdesk lib - env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} - VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg - run: | - rustup target add ${{ matrix.job.target }} - cargo install cargo-ndk - case ${{ matrix.job.target }} in - aarch64-linux-android) - ./flutter/ndk_arm64.sh - mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so - ;; - armv7-linux-androideabi) - ./flutter/ndk_arm.sh - mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so - ;; - esac - - - name: Build rustdesk - shell: bash - env: - JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 - run: | - export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH - # temporary use debug sign config - sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle - case ${{ matrix.job.target }} in - aarch64-linux-android) - mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a - cp /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/arm64-v8a/*.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/ - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so - # build flutter - pushd flutter - flutter build apk --release --target-platform android-arm64 --split-per-abi - mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - ;; - armv7-linux-androideabi) - mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a - cp /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/armeabi-v7a/*.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/ - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so - # build flutter - pushd flutter - flutter build apk --release --target-platform android-arm --split-per-abi - mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - ;; - esac - popd - mkdir -p signed-apk; pushd signed-apk - mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . - - build-rustdesk-lib-linux-amd64: - needs: [generate-bridge-linux, build-vcpkg-deps-linux] - name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: true - matrix: - # use a high level qemu-user-static - job: - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "flatpak", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "appimage", - } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - steps: - - name: Maximize build space - run: | - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/lib/android - sudo rm -rf /usr/share/dotnet - sudo apt update -y - sudo apt install qemu-user-static - - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 12 - - - name: Free Space - run: | - df - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - cache-directories: "/opt/rust-registry" - - - name: Install local registry - run: | - mkdir -p /opt/rust-registry - cargo install cargo-local-registry - - - name: Build local registry - uses: nick-fields/retry@v2 - id: build-local-registry - continue-on-error: true - with: - max_attempts: 3 - timeout_minutes: 15 - retry_on: error - command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - # only build cdylib - sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Restore vcpkg files - uses: actions/download-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: /opt/artifacts/vcpkg/installed - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk library for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - # not ready yet - # distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - ls -l /opt/artifacts/vcpkg/installed - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/rust-registry:/opt/rust-registry" - shell: /bin/bash - install: | - apt update -y - echo -e "installing deps" - apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null - # we have libopus compiled by us. - apt remove -y libopus-dev || true - # output devs - ls -l ./ - tree -L 3 /opt/artifacts/vcpkg/installed - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # rust - pushd /opt - wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz - tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz - cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh - rm -rf rust-1.64.0-${{ matrix.job.target }} - # edit config - mkdir -p ~/.cargo/ - echo """ - [source.crates-io] - registry = 'https://github.com/rust-lang/crates.io-index' - replace-with = 'local-registry' - - [source.local-registry] - local-registry = '/opt/rust-registry/' - """ > ~/.cargo/config - cat ~/.cargo/config - # start build - pushd /workspace - # mock - case "${{ matrix.job.arch }}" in - x86_64) - # no need mock on x86_64 - export VCPKG_ROOT=/opt/artifacts/vcpkg - cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release - ;; - esac - - - name: Upload Artifacts - uses: actions/upload-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: target/release/liblibrustdesk.so - - build-rustdesk-lib-linux-arm: - needs: [generate-bridge-linux, build-vcpkg-deps-linux] - name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: true - matrix: - # use a high level qemu-user-static - job: - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-20.04, - use-cross: true, - extra-build-features: "", - } - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "appimage", - } - # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { - # arch: armv7, - # target: arm-unknown-linux-gnueabihf, - # os: ubuntu-20.04, - # use-cross: true, - # extra-build-features: "", - # } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - steps: - - name: Maximize build space - run: | - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/lib/android - sudo rm -rf /usr/share/dotnet - sudo apt update -y - sudo apt install qemu-user-static - - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 12 - - - name: Free Space - run: | - df - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - cache-directories: "/opt/rust-registry" - - - name: Install local registry - run: | - mkdir -p /opt/rust-registry - cargo install cargo-local-registry - - - name: Build local registry - uses: nick-fields/retry@v2 - id: build-local-registry - continue-on-error: true - with: - max_attempts: 3 - timeout_minutes: 15 - retry_on: error - command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - # only build cdylib - sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Restore vcpkg files - uses: actions/download-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: /opt/artifacts/vcpkg/installed - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk library for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - ls -l /opt/artifacts/vcpkg/installed - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/rust-registry:/opt/rust-registry" - shell: /bin/bash - install: | - apt update -y - echo -e "installing deps" - apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null - # we have libopus compiled by us. - apt remove -y libopus-dev || true - # output devs - ls -l ./ - tree -L 3 /opt/artifacts/vcpkg/installed - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # rust - pushd /opt - wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz - tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz - cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh - rm -rf rust-1.64.0-${{ matrix.job.target }} - # edit config - mkdir -p ~/.cargo/ - echo """ - [source.crates-io] - registry = 'https://github.com/rust-lang/crates.io-index' - replace-with = 'local-registry' - - [source.local-registry] - local-registry = '/opt/rust-registry/' - """ > ~/.cargo/config - cat ~/.cargo/config - # start build - pushd /workspace - # mock - case "${{ matrix.job.arch }}" in - aarch64) - cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/ - cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ - ls -l /opt/artifacts/vcpkg/installed/lib/ - mkdir -p /vcpkg/installed/arm64-linux - ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib - ln -s /usr/include /vcpkg/installed/arm64-linux/include - export VCPKG_ROOT=/vcpkg - # disable hwcodec for compilation - cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release - ;; - armv7) - cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ - cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ - mkdir -p /vcpkg/installed/arm-linux - ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib - ln -s /usr/include /vcpkg/installed/arm-linux/include - export VCPKG_ROOT=/vcpkg - # disable hwcodec for compilation - cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release - ;; - esac - - - name: Upload Artifacts - uses: actions/upload-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: target/release/liblibrustdesk.so - - build-rustdesk-linux-arm: - needs: [build-rustdesk-lib-linux-arm] - name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ubuntu-20.04 # 20.04 has more performance on arm build - strategy: - fail-fast: true - matrix: - job: - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "", - } - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "appimage", - } - # - { - # arch: aarch64, - # target: aarch64-unknown-linux-gnu, - # os: ubuntu-18.04, # just for naming package, not running host - # use-cross: true, - # extra-build-features: "flatpak", - # } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Prepare env - run: | - sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools - mkdir -p ./target/release/ - - - name: Restore the rustdesk lib file - uses: actions/download-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: ./target/release/ - - - name: Download Flutter - shell: bash - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - pushd /opt - # clone repo and reset to flutter 3.7.0 - git clone https://github.com/sony/flutter-elinux.git || true - pushd flutter-elinux - # reset to flutter 3.7.0 - git fetch - git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 - popd - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk binary for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/flutter-elinux:/opt/flutter-elinux" - shell: /bin/bash - install: | - apt update -y - apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - pushd /workspace - # we use flutter-elinux to build our rustdesk - export PATH=/opt/flutter-elinux/bin:$PATH - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux. Run doctor to check if issues here. - flutter-elinux doctor -v - # Patch arm64 engine for flutter 3.6.0+ - flutter-elinux precache --linux - pushd /tmp - curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz - tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib - cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 - popd - case ${{ matrix.job.arch }} in - aarch64) - sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py - sed -i "s/x64\/release/arm64\/release/g" ./build.py - ;; - armv7) - sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py - sed -i "s/x64\/release/arm\/release/g" ./build.py - ;; - esac - python3 ./build.py --flutter --hwcodec --skip-cargo - - build-rustdesk-linux-amd64: - needs: [build-rustdesk-lib-linux-amd64] - name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ubuntu-20.04 - strategy: - fail-fast: true - matrix: - job: - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "flatpak", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "appimage", - } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Prepare env - run: | - sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools - mkdir -p ./target/release/ - - - name: Restore the rustdesk lib file - uses: actions/download-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: ./target/release/ - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk binary for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - shell: /bin/bash - install: | - apt update -y - apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # Setup Flutter - pushd /opt - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz - tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz - ls -l . - export PATH=/opt/flutter/bin:$PATH - flutter doctor -v - pushd /workspace - python3 ./build.py --flutter --hwcodec --skip-cargo + run-ci: + uses: ./.github/workflows/flutter-build.yml + with: + upload-artifact: false + \ No newline at end of file diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index e2093afc1..c251a2b25 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -6,1516 +6,9 @@ on: - cron: "0 0 * * *" workflow_dispatch: -env: - LLVM_VERSION: "15.0.6" - FLUTTER_VERSION: "3.7.0" - TAG_NAME: "nightly" - # vcpkg version: 2022.05.10 - # for multiarch gcc compatibility - VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" - VERSION: "1.2.0" - NDK_VERSION: "r23" - #signing keys env variable checks - ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}' - MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}' - # To make a custom build with your own servers set the below secret values - RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' - RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' - jobs: - build-for-windows: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) - runs-on: ${{ matrix.job.os }} - strategy: - 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 } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Install LLVM and Clang - uses: KyleMayes/install-llvm-action@v1 - with: - version: ${{ env.LLVM_VERSION }} - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - 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/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip - Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.job.os }} - - - name: Install flutter rust bridge deps - run: | - cargo install flutter_rust_bridge_codegen - Push-Location flutter ; flutter pub get ; Pop-Location - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Restore from cache and install vcpkg - uses: lukka/run-vcpkg@v7 - with: - setupOnly: true - vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - - - name: Install vcpkg dependencies - run: | - $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static - shell: bash - - - name: Build rustdesk - run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver - - - name: Sign rustdesk files - uses: GermanBluefox/code-sign-action@v7 - with: - certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' - password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' - certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' - # certificatename: '${{ secrets.CERTNAME }}' - folder: './flutter/build/windows/runner/Release/' - recursive: true - - - name: Build self-extracted executable - shell: bash - run: | - pushd ./libs/portable - python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe - popd - mkdir -p ./SignOutput - mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.exe - - # - name: Rename rustdesk - # shell: bash - # run: | - # for name in rustdesk*??-install.exe; do - # mv "$name" ./SignOutput/"${name%%-install.exe}-${{ matrix.job.target }}.exe" - # done - - - name: Sign rustdesk self-extracted file - uses: GermanBluefox/code-sign-action@v7 - with: - certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' - password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' - certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' - # certificatename: '${{ secrets.WINDOWS_PFX_NAME }}' - folder: './SignOutput' - recursive: false - - - name: Publish Release - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ./SignOutput/rustdesk-*.exe - - build-for-macOS: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - - { - target: x86_64-apple-darwin, - os: macos-latest, - extra-build-args: "", - } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Import the codesign cert - if: env.MACOS_P12_BASE64 != null - uses: apple-actions/import-codesign-certs@v1 - with: - p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} - p12-password: ${{ secrets.MACOS_P12_PASSWORD }} - keychain: rustdesk - - - name: Check sign and import sign key - if: env.MACOS_P12_BASE64 != null - run: | - security default-keychain -s rustdesk.keychain - security find-identity -v - - - name: Import notarize key - if: env.MACOS_P12_BASE64 != null - uses: timheuer/base64-to-file@v1.2 - with: - # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling - fileName: rustdesk.json - fileDir: ${{ github.workspace }} - encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - - - name: Install rcodesign tool - if: env.MACOS_P12_BASE64 != null - shell: bash - run: | - pushd /tmp - wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz - tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz - mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin - popd - - - name: Install build runtime - run: | - brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.job.os }} - - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen - pushd flutter && flutter pub get && popd - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Restore from cache and install vcpkg - uses: lukka/run-vcpkg@v7 - with: - setupOnly: true - vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - - - name: Install vcpkg dependencies - run: | - $VCPKG_ROOT/vcpkg install libvpx libyuv opus - - - name: Show version information (Rust, cargo, Clang) - shell: bash - run: | - clang --version || true - rustup -V - rustup toolchain list - rustup default - cargo -V - rustc -V - - - name: Build rustdesk - run: | - # --hwcodec not supported on macos yet - ./build.py --flutter ${{ matrix.job.extra-build-args }} - - - name: Codesign app and create signed dmg - if: env.MACOS_P12_BASE64 != null - run: | - security default-keychain -s rustdesk.keychain - security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain - # start sign the rustdesk.app and dmg - rm rustdesk-${{ env.VERSION }}.dmg || true - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv - create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv - # notarize the rustdesk-${{ env.VERSION }}.dmg - rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg - - - name: Rename rustdesk - run: | - for name in rustdesk*??.dmg; do - mv "$name" "${name%%.dmg}-${{ matrix.job.target }}.dmg" - done - - - name: Publish DMG package - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - rustdesk*-${{ matrix.job.target }}.dmg - - build-vcpkg-deps-linux: - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - # - { arch: armv7, os: ubuntu-20.04 } - - { arch: x86_64, os: ubuntu-20.04 } - - { arch: aarch64, os: ubuntu-20.04 } - steps: - - name: Create vcpkg artifacts folder - run: mkdir -p /opt/artifacts - - - name: Cache Vcpkg - id: cache-vcpkg - uses: actions/cache@v3 - with: - path: /opt/artifacts - key: vcpkg-${{ matrix.job.arch }} - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Run vcpkg install on ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "/opt/artifacts" - dockerRunArgs: | - --volume "/opt/artifacts:/artifacts" - shell: /bin/bash - install: | - apt update -y - case "${{ matrix.job.arch }}" in - x86_64) - # CMake 3.15+ - apt install -y gpg wget ca-certificates - echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null - wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null - apt update -y - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev - ;; - aarch64|armv7) - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool - esac - cmake --version - gcc -v - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - case "${{ matrix.job.arch }}" in - x86_64) - export VCPKG_FORCE_SYSTEM_BINARIES=1 - pushd /artifacts - git clone https://github.com/microsoft/vcpkg.git || true - pushd vcpkg - git reset --hard ${{ env.VCPKG_COMMIT_ID }} - ./bootstrap-vcpkg.sh - ./vcpkg install libvpx libyuv opus - ;; - aarch64|armv7) - pushd /artifacts - # libyuv - git clone https://chromium.googlesource.com/libyuv/libyuv || true - pushd libyuv - git pull - mkdir -p build - pushd build - mkdir -p /artifacts/vcpkg/installed - cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed - make -j4 && make install - popd - popd - # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC - wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz - tar -zxvf opus.tar.gz; ls -l - pushd opus-1.1.2 - ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed - make -j4; make install - ;; - esac - - name: Upload artifacts - uses: actions/upload-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: | - /opt/artifacts/vcpkg/installed - - generate-bridge-linux: - name: generate bridge - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - - { - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-args: "", - } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Install prerequisites - run: | - sudo apt update -y - sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: bridge-${{ matrix.job.os }} - workspace: "/tmp/flutter_rust_bridge/frb_codegen" - - - name: Cache Bridge - id: cache-bridge - uses: actions/cache@v3 - with: - path: /tmp/flutter_rust_bridge - key: vcpkg-${{ matrix.job.arch }} - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - - - name: Install flutter rust bridge deps - shell: bash - run: | - cargo install flutter_rust_bridge_codegen - pushd flutter && flutter pub get && popd - - - name: Run flutter rust bridge - run: | - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Upload Artifact - uses: actions/upload-artifact@master - with: - name: bridge-artifact - path: | - ./src/bridge_generated.rs - ./src/bridge_generated.io.rs - ./flutter/lib/generated_bridge.dart - ./flutter/lib/generated_bridge.freezed.dart - - build-rustdesk-android: - needs: [generate-bridge-linux] - name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - - { - arch: x86_64, - target: aarch64-linux-android, - os: ubuntu-20.04, - extra-build-features: "", - openssl-arch: android-arm64 - } - - { - arch: x86_64, - target: armv7-linux-androideabi, - os: ubuntu-18.04, - extra-build-features: "", - openssl-arch: android-arm - } - steps: - - name: Install dependencies - run: | - sudo apt update - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless - - name: Checkout source code - uses: actions/checkout@v3 - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - uses: nttld/setup-ndk@v1 - id: setup-ndk - with: - ndk-version: ${{ env.NDK_VERSION }} - add-to-path: true - - - name: Clone deps - shell: bash - run: | - pushd /opt - git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1 - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - - - name: Build rustdesk lib - env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} - VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg - run: | - rustup target add ${{ matrix.job.target }} - cargo install cargo-ndk - case ${{ matrix.job.target }} in - aarch64-linux-android) - ./flutter/ndk_arm64.sh - mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so - ;; - armv7-linux-androideabi) - ./flutter/ndk_arm.sh - mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so - ;; - esac - - - name: Build rustdesk - shell: bash - env: - JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 - run: | - export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH - # temporary use debug sign config - sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle - case ${{ matrix.job.target }} in - aarch64-linux-android) - mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a - cp /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/arm64-v8a/*.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/ - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so - # build flutter - pushd flutter - flutter build apk --release --target-platform android-arm64 --split-per-abi - mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - ;; - armv7-linux-androideabi) - mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a - cp /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/armeabi-v7a/*.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/ - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so - # build flutter - pushd flutter - flutter build apk --release --target-platform android-arm --split-per-abi - mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - ;; - esac - popd - mkdir -p signed-apk; pushd signed-apk - mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . - - - uses: r0adkll/sign-android-release@v1 - name: Sign app APK - if: env.ANDROID_SIGNING_KEY != null - id: sign-rustdesk - with: - releaseDirectory: ./signed-apk - signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} - alias: ${{ secrets.ANDROID_ALIAS }} - keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} - keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} - env: - # override default build-tools version (29.0.3) -- optional - BUILD_TOOLS_VERSION: "30.0.2" - - - name: Upload Artifacts - if: env.ANDROID_SIGNING_KEY != null - uses: actions/upload-artifact@master - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk - path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - - name: Publish signed apk package - if: env.ANDROID_SIGNING_KEY != null - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - - name: Publish unsigned apk package - if: env.ANDROID_SIGNING_KEY == null - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - - build-rustdesk-lib-linux-amd64: - needs: [generate-bridge-linux, build-vcpkg-deps-linux] - name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - # use a high level qemu-user-static - job: - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "flatpak", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "appimage", - } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - steps: - - name: Maximize build space - run: | - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/lib/android - sudo rm -rf /usr/share/dotnet - sudo apt update -y - sudo apt install qemu-user-static - - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 12 - - - name: Free Space - run: | - df - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - cache-directories: "/opt/rust-registry" - - - name: Install local registry - run: | - mkdir -p /opt/rust-registry - cargo install cargo-local-registry - - - name: Build local registry - uses: nick-fields/retry@v2 - id: build-local-registry - continue-on-error: true - with: - max_attempts: 3 - timeout_minutes: 15 - retry_on: error - command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - # only build cdylib - sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Restore vcpkg files - uses: actions/download-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: /opt/artifacts/vcpkg/installed - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk library for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - # not ready yet - # distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - ls -l /opt/artifacts/vcpkg/installed - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/rust-registry:/opt/rust-registry" - shell: /bin/bash - install: | - apt update -y - echo -e "installing deps" - apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null - # we have libopus compiled by us. - apt remove -y libopus-dev || true - # output devs - ls -l ./ - tree -L 3 /opt/artifacts/vcpkg/installed - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # rust - pushd /opt - wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz - tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz - cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh - rm -rf rust-1.64.0-${{ matrix.job.target }} - # edit config - mkdir -p ~/.cargo/ - echo """ - [source.crates-io] - registry = 'https://github.com/rust-lang/crates.io-index' - replace-with = 'local-registry' - - [source.local-registry] - local-registry = '/opt/rust-registry/' - """ > ~/.cargo/config - cat ~/.cargo/config - # start build - pushd /workspace - # mock - case "${{ matrix.job.arch }}" in - x86_64) - # no need mock on x86_64 - export VCPKG_ROOT=/opt/artifacts/vcpkg - cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release - ;; - esac - - - name: Upload Artifacts - uses: actions/upload-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: target/release/liblibrustdesk.so - - build-rustdesk-lib-linux-arm: - needs: [generate-bridge-linux, build-vcpkg-deps-linux] - name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - # use a high level qemu-user-static - job: - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-20.04, - use-cross: true, - extra-build-features: "", - } - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "appimage", - } - # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { - # arch: armv7, - # target: arm-unknown-linux-gnueabihf, - # os: ubuntu-20.04, - # use-cross: true, - # extra-build-features: "", - # } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - steps: - - name: Maximize build space - run: | - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/lib/android - sudo rm -rf /usr/share/dotnet - sudo apt update -y - sudo apt install qemu-user-static - - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 12 - - - name: Free Space - run: | - df - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - cache-directories: "/opt/rust-registry" - - - name: Install local registry - run: | - mkdir -p /opt/rust-registry - cargo install cargo-local-registry - - - name: Build local registry - uses: nick-fields/retry@v2 - id: build-local-registry - continue-on-error: true - with: - max_attempts: 3 - timeout_minutes: 15 - retry_on: error - command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - # only build cdylib - sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Restore vcpkg files - uses: actions/download-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: /opt/artifacts/vcpkg/installed - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk library for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - ls -l /opt/artifacts/vcpkg/installed - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/rust-registry:/opt/rust-registry" - shell: /bin/bash - install: | - apt update -y - echo -e "installing deps" - apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null - # we have libopus compiled by us. - apt remove -y libopus-dev || true - # output devs - ls -l ./ - tree -L 3 /opt/artifacts/vcpkg/installed - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # rust - pushd /opt - wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz - tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz - cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh - rm -rf rust-1.64.0-${{ matrix.job.target }} - # edit config - mkdir -p ~/.cargo/ - echo """ - [source.crates-io] - registry = 'https://github.com/rust-lang/crates.io-index' - replace-with = 'local-registry' - - [source.local-registry] - local-registry = '/opt/rust-registry/' - """ > ~/.cargo/config - cat ~/.cargo/config - # start build - pushd /workspace - # mock - case "${{ matrix.job.arch }}" in - aarch64) - cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/ - cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ - ls -l /opt/artifacts/vcpkg/installed/lib/ - mkdir -p /vcpkg/installed/arm64-linux - ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib - ln -s /usr/include /vcpkg/installed/arm64-linux/include - export VCPKG_ROOT=/vcpkg - # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release - ;; - armv7) - cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ - cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ - mkdir -p /vcpkg/installed/arm-linux - ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib - ln -s /usr/include /vcpkg/installed/arm-linux/include - export VCPKG_ROOT=/vcpkg - # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release - ;; - esac - - - name: Upload Artifacts - uses: actions/upload-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: target/release/liblibrustdesk.so - - build-rustdesk-linux-arm: - needs: [build-rustdesk-lib-linux-arm] - name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ubuntu-20.04 # 20.04 has more performance on arm build - strategy: - fail-fast: false - matrix: - job: - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "", - } - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "appimage", - } - # - { - # arch: aarch64, - # target: aarch64-unknown-linux-gnu, - # os: ubuntu-18.04, # just for naming package, not running host - # use-cross: true, - # extra-build-features: "flatpak", - # } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Prepare env - run: | - sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools - mkdir -p ./target/release/ - - - name: Restore the rustdesk lib file - uses: actions/download-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: ./target/release/ - - - name: Download Flutter - shell: bash - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - pushd /opt - # clone repo and reset to flutter 3.7.0 - git clone https://github.com/sony/flutter-elinux.git || true - pushd flutter-elinux - # reset to flutter 3.7.0 - git fetch - git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 - popd - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk binary for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/flutter-elinux:/opt/flutter-elinux" - shell: /bin/bash - install: | - apt update -y - apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - pushd /workspace - # we use flutter-elinux to build our rustdesk - export PATH=/opt/flutter-elinux/bin:$PATH - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux. Run doctor to check if issues here. - flutter-elinux doctor -v - # Patch arm64 engine for flutter 3.6.0+ - flutter-elinux precache --linux - pushd /tmp - curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz - tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib - cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 - popd - # edit to corresponding arch - case ${{ matrix.job.arch }} in - aarch64) - sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py - sed -i "s/x64\/release/arm64\/release/g" ./build.py - ;; - armv7) - sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py - sed -i "s/x64\/release/arm\/release/g" ./build.py - ;; - esac - python3 ./build.py --flutter --hwcodec --skip-cargo - # rpm package - echo -e "start packaging fedora package" - pushd /workspace - case ${{ matrix.job.arch }} in - armv7) - sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec - sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter.spec - ;; - aarch64) - sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.spec - ;; - esac - HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb - pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} - mkdir -p /opt/artifacts/rpm - for name in rustdesk*??.rpm; do - mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" - done - # rpm suse package - echo -e "start packaging suse package" - pushd /workspace - case ${{ matrix.job.arch }} in - armv7) - sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec - sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter-suse.spec - ;; - aarch64) - sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter-suse.spec - ;; - esac - HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb - pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} - mkdir -p /opt/artifacts/rpm - for name in rustdesk*??.rpm; do - mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm" - done - - - name: Rename rustdesk - shell: bash - run: | - for name in rustdesk*??.deb; do - cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" - done - - - name: Publish debian package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Build appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - shell: bash - run: | - # set-up appimage-builder - pushd /tmp - wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage - chmod +x appimage-builder-x86_64.AppImage - sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder - popd - # run appimage-builder - pushd appimage - sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml - - - name: Publish appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage - - - name: Upload Artifact - uses: actions/upload-artifact@master - if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Patch archlinux PKGBUILD - if: ${{ matrix.job.extra-build-features == '' }} - run: | - sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD - case ${{ matrix.job.arch }} in - armv7) - sed -i "s/linux\/x64/linux\/arm/g" ./res/PKGBUILD - ;; - aarch64) - sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD - ;; - esac - - # Temporary disable for there is no many archlinux arm hosts - # - name: Build archlinux package - # if: ${{ matrix.job.extra-build-features == '' }} - # uses: vufa/arch-makepkg-action@master - # with: - # packages: > - # llvm - # clang - # libva - # libvdpau - # rust - # gstreamer - # unzip - # git - # cmake - # gcc - # curl - # wget - # yasm - # nasm - # zip - # make - # pkg-config - # clang - # gtk3 - # xdotool - # libxcb - # libxfixes - # alsa-lib - # pipewire - # python - # ttf-arphic-uming - # libappindicator-gtk3 - # scripts: | - # cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f - - # - name: Publish archlinux package - # if: ${{ matrix.job.extra-build-features == '' }} - # uses: softprops/action-gh-release@v1 - # with: - # prerelease: true - # tag_name: ${{ env.TAG_NAME }} - # files: | - # res/rustdesk*.zst - - - name: Publish fedora28/centos8 package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - /opt/artifacts/rpm/*.rpm - - build-rustdesk-linux-amd64: - needs: [build-rustdesk-lib-linux-amd64] - name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - job: - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-features: "", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-features: "flatpak", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-features: "appimage", - } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Prepare env - run: | - sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools - mkdir -p ./target/release/ - - - name: Restore the rustdesk lib file - uses: actions/download-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: ./target/release/ - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk binary for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - shell: /bin/bash - install: | - apt update -y - apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # Setup Flutter - pushd /opt - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz - tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz - ls -l . - export PATH=/opt/flutter/bin:$PATH - flutter doctor -v - pushd /workspace - python3 ./build.py --flutter --hwcodec --skip-cargo - # rpm package - pushd /workspace - case ${{ matrix.job.arch }} in - armv7) - sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec - ;; - esac - HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb - pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} - mkdir -p /opt/artifacts/rpm - for name in rustdesk*??.rpm; do - mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" - done - # rpm suse package - pushd /workspace - case ${{ matrix.job.arch }} in - armv7) - sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec - ;; - esac - HBB=`pwd` rpmbuild ./res/rpm-flutter-suse.spec -bb - pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} - mkdir -p /opt/artifacts/rpm - for name in rustdesk*??.rpm; do - mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm" - done - - - name: Rename rustdesk - shell: bash - run: | - for name in rustdesk*??.deb; do - # use cp to duplicate deb files to fit other packages. - cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" - done - - - name: Publish debian package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Upload Artifact - uses: actions/upload-artifact@master - if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Patch archlinux PKGBUILD - if: ${{ matrix.job.extra-build-features == '' }} - run: | - sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD - - - name: Build archlinux package - if: ${{ matrix.job.extra-build-features == '' }} - uses: vufa/arch-makepkg-action@master - with: - packages: > - llvm - clang - libva - libvdpau - rust - gstreamer - unzip - git - cmake - gcc - curl - wget - yasm - nasm - zip - make - pkg-config - clang - gtk3 - xdotool - libxcb - libxfixes - alsa-lib - pipewire - python - ttf-arphic-uming - libappindicator-gtk3 - scripts: | - cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f - - - name: Publish archlinux package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - res/rustdesk*.zst - - - name: Build appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - shell: bash - run: | - # set-up appimage-builder - pushd /tmp - wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage - chmod +x appimage-builder-x86_64.AppImage - sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder - popd - # run appimage-builder - pushd appimage - sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-x86_64.yml - - - name: Publish appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage - - - name: Publish fedora28/centos8 package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - /opt/artifacts/rpm/*.rpm - - # Temporary disable flatpak arm build - # - # build-flatpak-arm: - # name: Build Flatpak - # needs: [build-rustdesk-linux-arm] - # runs-on: ${{ matrix.job.os }} - # strategy: - # fail-fast: false - # matrix: - # job: - # # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, arch: arm64 } - # - { target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, arch: arm64 } - # steps: - # - name: Checkout source code - # uses: actions/checkout@v3 - - # - name: Download Binary - # uses: actions/download-artifact@master - # with: - # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - # path: . - - # - name: Rename Binary - # run: | - # mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb - - # - uses: Kingtous/run-on-arch-action@amd64-support - # name: Build rustdesk flatpak package for ${{ matrix.job.arch }} - # id: rpm - # with: - # arch: ${{ matrix.job.arch }} - # distro: ubuntu18.04 - # githubToken: ${{ github.token }} - # setup: | - # ls -l "${PWD}" - # dockerRunArgs: | - # --volume "${PWD}:/workspace" - # shell: /bin/bash - # install: | - # apt update -y - # apt install -y rpm - # run: | - # pushd /workspace - # # install - # apt update -y - # apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git - # # flatpak deps - # flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - # flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 - # flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 - # # package - # pushd flatpak - # git clone https://github.com/flathub/shared-modules.git --depth=1 - # flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json - # flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk - - # - name: Publish flatpak package - # uses: softprops/action-gh-release@v1 - # with: - # prerelease: true - # tag_name: ${{ env.TAG_NAME }} - # files: | - # flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak - - build-flatpak-amd64: - name: Build Flatpak - needs: [build-rustdesk-linux-amd64] - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - job: - - { target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, arch: x86_64 } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Download Binary - uses: actions/download-artifact@master - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - path: . - - - name: Rename Binary - run: | - mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk flatpak package for ${{ matrix.job.arch }} - id: rpm - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - shell: /bin/bash - install: | - apt update -y - apt install -y rpm git wget curl - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - pushd /workspace - # install - apt update -y - apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git - # flatpak deps - flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 - flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 - # package - pushd flatpak - git clone https://github.com/flathub/shared-modules.git --depth=1 - flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json - flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak com.rustdesk.RustDesk - - - name: Publish flatpak package - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak + run-flutter-nightly-build: + uses: ./.github/workflows/flutter-build.yml + secrets: inherit + with: + upload-artifact: true diff --git a/.github/workflows/history.yml b/.github/workflows/history.yml new file mode 100644 index 000000000..b1ca74721 --- /dev/null +++ b/.github/workflows/history.yml @@ -0,0 +1,370 @@ +name: Flutter Windows History Build + +on: [workflow_dispatch] + +env: + LLVM_VERSION: "10.0" + # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. + FLUTTER_VERSION: "3.0.5" + TAG_NAME: "tmp" + # vcpkg version: 2022.05.10 + # for multiarch gcc compatibility + VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" + VERSION: "1.2.0" + +jobs: + build-for-windows-2022-12-05: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + 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 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + with: + ref: '8d1254cf14b69f545c9cefa026c5eeb0e7dd3e7c' + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + 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/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "1.62" + target: ${{ matrix.job.target }} + override: true + components: rustfmt + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + run: | + dart pub global activate ffigen --version 5.0.1 + $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe + Push-Location .. + git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 + Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location + Pop-Location + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + shell: bash + + - name: Build rustdesk + run: python3 .\build.py --portable --hwcodec --flutter + + - name: Build self-extracted executable + shell: bash + run: | + pushd ./libs/portable + python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-2022-12-05-${{ matrix.job.target }}.exe + + - name: Publish Release + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./SignOutput/rustdesk-*.exe + + build-for-windows-2022-12-12: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + 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 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + with: + ref: '3dd43b79ec0409fc38103bed0c7eb0bc3cd993d5' + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + 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/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "1.62" + target: ${{ matrix.job.target }} + override: true + components: rustfmt + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + run: | + dart pub global activate ffigen --version 5.0.1 + $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe + Push-Location .. + git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 + Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location + Pop-Location + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + shell: bash + + - name: Build rustdesk + run: python3 .\build.py --portable --hwcodec --flutter + + - name: Build self-extracted executable + shell: bash + run: | + pushd ./libs/portable + python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-2022-12-12-${{ matrix.job.target }}.exe + + - name: Publish Release + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./SignOutput/rustdesk-*.exe + + build-for-windows-2022-12-19: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + 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 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + with: + ref: '1054715891c4e73ad9b164acec6dadecfc599a65' + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + 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/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "1.62" + target: ${{ matrix.job.target }} + override: true + components: rustfmt + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + run: | + dart pub global activate ffigen --version 5.0.1 + $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe + Push-Location .. + git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 + Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location + Pop-Location + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + shell: bash + + - name: Build rustdesk + run: python3 .\build.py --portable --hwcodec --flutter + + - name: Build self-extracted executable + shell: bash + run: | + pushd ./libs/portable + python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-2022-12-19-${{ matrix.job.target }}.exe + + - name: Publish Release + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./SignOutput/rustdesk-*.exe + + build-for-windows-2022-12-26: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + 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 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + with: + ref: 'b241925fe093dc4da804a5aac419375f4ca7653f' + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + 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/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "1.62" + target: ${{ matrix.job.target }} + override: true + components: rustfmt + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + run: | + dart pub global activate ffigen --version 5.0.1 + $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe + Push-Location .. + git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 + Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location + Pop-Location + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + shell: bash + + - name: Build rustdesk + run: python3 .\build.py --portable --hwcodec --flutter + + - name: Build self-extracted executable + shell: bash + run: | + pushd ./libs/portable + python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-2022-12-26-${{ matrix.job.target }}.exe + + - name: Publish Release + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./SignOutput/rustdesk-*.exe diff --git a/.github/workflows/vcpkg-deps-linux.yml b/.github/workflows/vcpkg-deps-linux.yml new file mode 100644 index 000000000..8eee01011 --- /dev/null +++ b/.github/workflows/vcpkg-deps-linux.yml @@ -0,0 +1,88 @@ +name: Build vcpkg dependencies for linux clients + +on: + workflow_call: + +jobs: + build-vcpkg-deps-linux: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { arch: armv7, os: ubuntu-20.04 } + - { arch: x86_64, os: ubuntu-20.04 } + - { arch: aarch64, os: ubuntu-20.04 } + steps: + - name: Create vcpkg artifacts folder + run: mkdir -p /opt/artifacts + + - name: Cache Vcpkg + id: cache-vcpkg + uses: actions/cache@v3 + with: + path: /opt/artifacts + key: vcpkg-${{ matrix.job.arch }} + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Run vcpkg install on ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "/opt/artifacts" + dockerRunArgs: | + --volume "/opt/artifacts:/artifacts" + shell: /bin/bash + install: | + apt update -y + case "${{ matrix.job.arch }}" in + x86_64) + # CMake 3.15+ + apt install -y gpg wget ca-certificates + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + apt update -y + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev + cmake --version + gcc -v + ;; + aarch64|armv7) + apt install -y curl zip unzip git + esac + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + case "${{ matrix.job.arch }}" in + x86_64) + export VCPKG_FORCE_SYSTEM_BINARIES=1 + pushd /artifacts + git clone https://github.com/microsoft/vcpkg.git || true + pushd vcpkg + git reset --hard ${{ env.VCPKG_COMMIT_ID }} + ./bootstrap-vcpkg.sh + ./vcpkg install libvpx libyuv opus + ;; + aarch64) + pushd /artifacts + rm -rf rustdesk_thirdparty_lib + git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1 + mkdir -p /artifacts/vcpkg/installed + mv ./rustdesk_thirdparty_lib/vcpkg/installed/arm64-linux /artifacts/vcpkg/installed/arm64-linux + ;; + armv7) + pushd /artifacts + rm -rf rustdesk_thirdparty_lib + git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1 + mkdir -p /artifacts/vcpkg/installed + mv ./rustdesk_thirdparty_lib/vcpkg/installed/arm-linux /artifacts/vcpkg/installed/arm-linux + ;; + esac + - name: Upload artifacts + uses: actions/upload-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: | + /opt/artifacts/vcpkg/installed \ No newline at end of file diff --git a/.gitignore b/.gitignore index a71c71a4e..ec6251084 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ rustdesk appimage/AppDir appimage/*.AppImage appimage/appimage-build +appimage/*.xz # flutter flutter/linux/build/** flutter/linux/cmake-build-debug/** @@ -38,6 +39,8 @@ flatpak/.flatpak-builder/shared-modules/** flatpak/.flatpak-builder/shared-modules/*.tar.xz flatpak/.flatpak-builder/debian-binary flatpak/build/** +flatpak/repo/** +flatpak/*.flatpak # bridge file lib/generated_bridge.dart # vscode devcontainer @@ -45,3 +48,5 @@ lib/generated_bridge.dart .vscode-server/ .ssh .devcontainer/.* +# build cache in examples +examples/**/target/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index b80b4d924..1133105ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,14 +65,14 @@ dependencies = [ [[package]] name = "alsa" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5915f52fe2cf65e83924d037b6c5290b7cee097c6b5c8700746e6168a343fd6b" +checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44" dependencies = [ "alsa-sys", "bitflags", "libc", - "nix 0.23.2", + "nix 0.24.3", ] [[package]] @@ -134,10 +134,50 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.69" +name = "anstream" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" +checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-wincon", + "concolor-override", + "concolor-query", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" + +[[package]] +name = "anstyle-parse" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-wincon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa" +dependencies = [ + "anstyle", + "windows-sys 0.45.0", +] + +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" [[package]] name = "arboard" @@ -208,22 +248,22 @@ dependencies = [ [[package]] name = "async-io" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock", "autocfg 1.1.0", + "cfg-if 1.0.0", "concurrent-queue", "futures-lite", - "libc", "log", "parking", "polling", + "rustix", "slab", "socket2 0.4.9", "waker-fn", - "windows-sys 0.42.0", ] [[package]] @@ -255,30 +295,30 @@ dependencies = [ [[package]] name = "async-recursion" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b015a331cc64ebd1774ba119538573603427eaace0a1950c423ab971f903796" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", - "syn 1.0.109", + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 2.0.11", ] [[package]] name = "async-task" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" [[package]] name = "async-trait" -version = "0.1.66" +version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", - "syn 1.0.109", + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 2.0.11", ] [[package]] @@ -302,7 +342,7 @@ dependencies = [ "glib-sys 0.16.3", "gobject-sys 0.16.3", "libc", - "system-deps 6.0.3", + "system-deps 6.0.4", ] [[package]] @@ -361,12 +401,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base-x" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" - [[package]] name = "base64" version = "0.21.0" @@ -388,34 +422,14 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "regex", "rustc-hash", "shlex", "which", ] -[[package]] -name = "bindgen" -version = "0.61.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a022e58a142a46fea340d68012b9201c094e93ec3d033a944a24f8fd4a4f09a" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2 1.0.51", - "quote 1.0.23", - "regex", - "rustc-hash", - "shlex", - "syn 1.0.109", -] - [[package]] name = "bindgen" version = "0.64.0" @@ -429,8 +443,8 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "regex", "rustc-hash", "shlex", @@ -542,7 +556,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" dependencies = [ - "serde 1.0.154", + "serde 1.0.159", ] [[package]] @@ -567,7 +581,7 @@ checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" dependencies = [ "glib-sys 0.16.3", "libc", - "system-deps 6.0.3", + "system-deps 6.0.4", ] [[package]] @@ -582,11 +596,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6031a462f977dd38968b6f23378356512feeace69cef817e1a4475108093cec3" +checksum = "c530edf18f37068ac2d977409ed5cd50d53d73bc653c7647b48eb78976ac9ae2" dependencies = [ - "serde 1.0.154", + "serde 1.0.159", ] [[package]] @@ -595,7 +609,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ - "serde 1.0.154", + "serde 1.0.159", ] [[package]] @@ -606,9 +620,9 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver 1.0.16", - "serde 1.0.154", - "serde_json 1.0.94", + "semver", + "serde 1.0.159", + "serde_json 1.0.95", ] [[package]] @@ -621,10 +635,10 @@ dependencies = [ "heck 0.4.1", "indexmap", "log", - "proc-macro2 1.0.51", - "quote 1.0.23", - "serde 1.0.154", - "serde_json 1.0.94", + "proc-macro2 1.0.54", + "quote 1.0.26", + "serde 1.0.159", + "serde_json 1.0.95", "syn 1.0.109", "tempfile", "toml 0.5.11", @@ -656,9 +670,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.11.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa" +checksum = "a35b255461940a32985c627ce82900867c61db1659764d3675ea81963f72a4c6" dependencies = [ "smallvec", ] @@ -677,9 +691,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", "js-sys", @@ -705,9 +719,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ed9a53e5d4d9c573ae844bfac6872b159cb1d1585a83b29e7a64b7eef7332a" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" dependencies = [ "glob", "libc", @@ -737,10 +751,8 @@ checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", - "clap_derive", "clap_lex 0.2.4", "indexmap", - "once_cell", "strsim 0.10.0", "termcolor", "textwrap 0.16.0", @@ -748,28 +760,38 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.8" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" +checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3" dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" +dependencies = [ + "anstream", + "anstyle", "bitflags", - "clap_lex 0.3.2", - "is-terminal", + "clap_lex 0.4.1", "strsim 0.10.0", - "termcolor", ] [[package]] name = "clap_derive" -version = "3.2.18" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" dependencies = [ "heck 0.4.1", - "proc-macro-error", - "proc-macro2 1.0.51", - "quote 1.0.23", - "syn 1.0.109", + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 2.0.11", ] [[package]] @@ -783,12 +805,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" -dependencies = [ - "os_str_bytes", -] +checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" [[package]] name = "clipboard" @@ -797,7 +816,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.154", + "serde 1.0.159", "serde_derive", "thiserror", ] @@ -824,9 +843,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.49" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" dependencies = [ "cc", ] @@ -849,9 +868,9 @@ dependencies = [ [[package]] name = "cocoa-foundation" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" +checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" dependencies = [ "bitflags", "block", @@ -899,6 +918,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "concolor-override" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f" + +[[package]] +name = "concolor-query" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" +dependencies = [ + "windows-sys 0.45.0", +] + [[package]] name = "concurrent-queue" version = "2.1.0" @@ -914,7 +948,7 @@ version = "0.4.0" source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" dependencies = [ "directories-next", - "serde 1.0.154", + "serde 1.0.159", "thiserror", "toml 0.5.11", ] @@ -955,6 +989,12 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" + [[package]] name = "core-foundation-sys" version = "0.7.0" @@ -1019,52 +1059,54 @@ dependencies = [ [[package]] name = "coreaudio-rs" -version = "0.10.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" +checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff" dependencies = [ "bitflags", + "core-foundation-sys 0.6.2", "coreaudio-sys", ] [[package]] name = "coreaudio-sys" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9444b94b8024feecc29e01a9706c69c1e26bfee480221c90764200cfd778fb" +checksum = "f034b2258e6c4ade2f73bf87b21047567fb913ee9550837c2316d139b0262b24" dependencies = [ - "bindgen 0.61.0", + "bindgen 0.64.0", ] [[package]] name = "cpal" -version = "0.14.2" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f342c1b63e185e9953584ff2199726bf53850d96610a310e3aca09e9405a2d0b" +checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" dependencies = [ "alsa", "core-foundation-sys 0.8.3", "coreaudio-rs", + "dasp_sample", "jni 0.19.0", "js-sys", "libc", - "mach", + "mach2", "ndk 0.7.0", "ndk-context", "oboe", "once_cell", "parking_lot 0.12.1", - "stdweb", - "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", - "windows 0.37.0", + "windows 0.46.0", ] [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" dependencies = [ "libc", ] @@ -1165,9 +1207,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "cxx" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a140f260e6f3f79013b8bfc65e7ce630c9ab4388c6a89c71e07226f49487b72" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" dependencies = [ "cc", "cxxbridge-flags", @@ -1177,34 +1219,34 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da6383f459341ea689374bf0a42979739dc421874f112ff26f829b8040b8e613" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" dependencies = [ "cc", "codespan-reporting", "once_cell", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "scratch", - "syn 1.0.109", + "syn 2.0.11", ] [[package]] name = "cxxbridge-flags" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90201c1a650e95ccff1c8c0bb5a343213bdd317c6e600a93075bca2eff54ec97" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" [[package]] name = "cxxbridge-macro" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", - "syn 1.0.109", + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 2.0.11", ] [[package]] @@ -1224,38 +1266,14 @@ dependencies = [ "zvariant", ] -[[package]] -name = "darling" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -dependencies = [ - "darling_core 0.10.2", - "darling_macro 0.10.2", -] - [[package]] name = "darling" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core 0.13.4", - "darling_macro 0.13.4", -] - -[[package]] -name = "darling_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2 1.0.51", - "quote 1.0.23", - "strsim 0.9.3", - "syn 1.0.109", + "darling_core", + "darling_macro", ] [[package]] @@ -1266,31 +1284,20 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "strsim 0.10.0", "syn 1.0.109", ] -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core 0.10.2", - "quote 1.0.23", - "syn 1.0.109", -] - [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core 0.13.4", - "quote 1.0.23", + "darling_core", + "quote 1.0.26", "syn 1.0.109", ] @@ -1456,14 +1463,19 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" [[package]] name = "default-net" -version = "0.12.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e349ed1e06fb344a7dd8b5a676375cf671b31e8900075dd2be816efc063a63" +checksum = "a4898b43aed56499fad6b294d15b3e76a51df68079bf492e5daae38ca084e003" dependencies = [ + "dlopen2", "libc", "memalloc", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "once_cell", "system-configuration", - "windows 0.30.0", + "windows 0.32.0", ] [[package]] @@ -1472,8 +1484,8 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "082a24a9967533dc5d743c602157637116fc1b52806d694a5a45e6f32567fcdd" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -1483,20 +1495,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", - "syn 1.0.109", -] - -[[package]] -name = "derive_setters" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1cf41b4580a37cca5ef2ada2cc43cf5d6be3983f4522e83010d67ab6925e84b" -dependencies = [ - "darling 0.10.2", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -1577,12 +1577,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "dispatch" version = "0.2.0" @@ -1610,6 +1604,29 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "dlopen2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b121caccfc363e4d9a4589528f3bef7c71b83c6ed01c8dc68cbeeb7fd29ec698" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi 0.3.9", +] + +[[package]] +name = "dlopen2_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a09ac8bb8c16a282264c379dffba707b9c998afc7506009137f3c6136888078" +dependencies = [ + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 1.0.109", +] + [[package]] name = "dlopen_derive" version = "0.1.4" @@ -1635,7 +1652,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" dependencies = [ "lazy_static", "regex", - "serde 1.0.154", + "serde 1.0.159", "strsim 0.10.0", ] @@ -1658,7 +1675,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.154", + "serde 1.0.159", "serde_derive", "thiserror", ] @@ -1685,7 +1702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e62abb876c07e4754fae5c14cafa77937841f01740637e17d78dc04352f32a5e" dependencies = [ "cc", - "rustc_version 0.4.0", + "rustc_version", "toml 0.5.11", "vswhom", "winreg 0.10.1", @@ -1710,7 +1727,7 @@ dependencies = [ "objc", "pkg-config", "rdev", - "serde 1.0.154", + "serde 1.0.159", "serde_derive", "tfc", "unicode-segmentation", @@ -1718,10 +1735,30 @@ dependencies = [ ] [[package]] -name = "enum-map" -version = "2.4.2" +name = "enum-iterator" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c25992259941eb7e57b936157961b217a4fc8597829ddef0596d6c3cd86e1a" +checksum = "706d9e7cf1c7664859d79cd524e4e53ea2b67ea03c98cc2870c5e539695d597e" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355f93763ef7b0ae1c43c4d8eccc9d5848d84ad1a1d8ce61c421d1ac85a19d05" +dependencies = [ + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 1.0.109", +] + +[[package]] +name = "enum-map" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "988f0d17a0fa38291e5f41f71ea8d46a5d5497b9054d5a759fae2cbb819f2356" dependencies = [ "enum-map-derive", ] @@ -1732,8 +1769,8 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4da76b3b6116d758c7ba93f7ec6a35d2e2cf24feda76c6e38a375f4d5c59f2" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -1744,8 +1781,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11f36e95862220b211a6e2aa5eca09b4fa391b13cd52ceb8035a24bf65a79de2" dependencies = [ "once_cell", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -1756,7 +1793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" dependencies = [ "enumflags2_derive", - "serde 1.0.154", + "serde 1.0.159", ] [[package]] @@ -1765,8 +1802,8 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -1823,24 +1860,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34a887c8df3ed90498c1c437ce21f211c8e27672921a8ffa293cb8d6d4caa9e" dependencies = [ "proc-macro-error", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "rustversion", "syn 1.0.109", "synstructure", ] -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi 0.3.9", -] - [[package]] name = "errno" version = "0.3.0" @@ -1890,17 +1916,17 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "exr" -version = "1.5.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8af5ef47e2ed89d23d0ecbc1b681b30390069de70260937877514377fc24feb" +checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4" dependencies = [ "bit_field", "flume", "half", "lebe", "miniz_oxide", + "rayon-core", "smallvec", - "threadpool", "zune-inflate", ] @@ -1915,9 +1941,9 @@ dependencies = [ [[package]] name = "fern" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd7b0849075e79ee9a1836df22c717d1eba30451796fdc631b04565dd11e2a" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" dependencies = [ "chrono", "colored", @@ -1931,7 +1957,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3cf3a800ff6e860c863ca6d4b16fd999db8b752819c1606884047b73e468535" dependencies = [ "memoffset 0.8.0", - "rustc_version 0.4.0", + "rustc_version", ] [[package]] @@ -1942,7 +1968,7 @@ checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "windows-sys 0.45.0", ] @@ -1984,14 +2010,14 @@ dependencies = [ "futures-sink", "nanorand", "pin-project", - "spin 0.9.5", + "spin 0.9.7", ] [[package]] name = "flutter_rust_bridge" -version = "1.68.0" +version = "1.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54f0d71ff30fc2ae7c18508b517488a89051d81e3bfc4d48d4a6cf54b5dab789" +checksum = "a85cf0e90d520425ac9bfca66e97b400782475d3091f4b75cadc210897c098c2" dependencies = [ "allo-isolate", "anyhow", @@ -2014,27 +2040,28 @@ dependencies = [ [[package]] name = "flutter_rust_bridge_codegen" -version = "1.68.0" +version = "1.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a75a72411f0c5b480e4671417f52780172053128cf87d5614a9757d7680a0" +checksum = "77f6545fe9767e82d3817b511753273b26903cd3658970cbed03a729b409a550" dependencies = [ "anyhow", "atty", "cargo_metadata", "cbindgen", "chrono", - "clap 3.2.23", + "clap 4.2.1", "convert_case", "delegate", + "enum-iterator", "enum_dispatch", "fern", "itertools 0.10.5", "lazy_static", "log", "pathdiff", - "quote 1.0.23", + "quote 1.0.26", "regex", - "serde 1.0.154", + "serde 1.0.159", "serde_yaml", "strum_macros 0.24.3", "syn 1.0.109", @@ -2046,9 +2073,9 @@ dependencies = [ [[package]] name = "flutter_rust_bridge_macros" -version = "1.68.0" +version = "1.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6187d1635afede47c23a9979f85c3dc57e3391eb9c690b7fe95715bf945da72" +checksum = "6a65b1cb0d5cc83085f9cbccbeadd35c4e90a67166fa7d72ef984ba9a960a222" [[package]] name = "fnv" @@ -2123,9 +2150,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" dependencies = [ "futures-channel", "futures-core", @@ -2138,9 +2165,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" dependencies = [ "futures-core", "futures-sink", @@ -2148,15 +2175,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" +checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" [[package]] name = "futures-executor" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" dependencies = [ "futures-core", "futures-task", @@ -2165,9 +2192,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" +checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" [[package]] name = "futures-lite" @@ -2186,32 +2213,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] [[package]] name = "futures-sink" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" +checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" [[package]] name = "futures-task" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" +checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" [[package]] name = "futures-util" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" dependencies = [ "futures-channel", "futures-core", @@ -2264,7 +2291,7 @@ dependencies = [ "glib-sys 0.16.3", "gobject-sys 0.16.3", "libc", - "system-deps 6.0.3", + "system-deps 6.0.4", ] [[package]] @@ -2281,7 +2308,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps 6.0.3", + "system-deps 6.0.4", ] [[package]] @@ -2295,7 +2322,7 @@ dependencies = [ "gobject-sys 0.16.3", "libc", "pkg-config", - "system-deps 6.0.3", + "system-deps 6.0.4", ] [[package]] @@ -2307,15 +2334,15 @@ dependencies = [ "gdk-sys", "glib-sys 0.16.3", "libc", - "system-deps 6.0.3", + "system-deps 6.0.4", "x11 2.21.0", ] [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -2346,9 +2373,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.11.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" dependencies = [ "color_quant", "weezl", @@ -2389,7 +2416,7 @@ dependencies = [ "glib-sys 0.16.3", "gobject-sys 0.16.3", "libc", - "system-deps 6.0.3", + "system-deps 6.0.4", "winapi 0.3.9", ] @@ -2425,7 +2452,7 @@ dependencies = [ "futures-task", "futures-util", "gio-sys", - "glib-macros 0.16.3", + "glib-macros 0.16.8", "glib-sys 0.16.3", "gobject-sys 0.16.3", "libc", @@ -2445,23 +2472,23 @@ dependencies = [ "itertools 0.9.0", "proc-macro-crate 0.1.5", "proc-macro-error", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] [[package]] name = "glib-macros" -version = "0.16.3" +version = "0.16.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" +checksum = "fb1a9325847aa46f1e96ffea37611b9d51fc4827e67f79e7de502a297560a67b" dependencies = [ "anyhow", "heck 0.4.1", "proc-macro-crate 1.3.1", "proc-macro-error", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -2482,7 +2509,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" dependencies = [ "libc", - "system-deps 6.0.3", + "system-deps 6.0.4", ] [[package]] @@ -2510,7 +2537,7 @@ checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" dependencies = [ "glib-sys 0.16.3", "libc", - "system-deps 6.0.3", + "system-deps 6.0.4", ] [[package]] @@ -2684,20 +2711,20 @@ dependencies = [ "gobject-sys 0.16.3", "libc", "pango-sys", - "system-deps 6.0.3", + "system-deps 6.0.4", ] [[package]] name = "gtk3-macros" -version = "0.16.0" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff" +checksum = "096eb63c6fedf03bafe65e5924595785eaf1bcb7200dac0f2cbe9c9738f05ad8" dependencies = [ "anyhow", "proc-macro-crate 1.3.1", "proc-macro-error", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -2749,6 +2776,7 @@ dependencies = [ "confy", "directories-next", "dirs-next", + "dlopen", "env_logger 0.10.0", "filetime", "flexi_logger", @@ -2765,16 +2793,16 @@ dependencies = [ "quinn", "rand 0.8.5", "regex", - "serde 1.0.154", + "serde 1.0.159", "serde_derive", - "serde_json 1.0.94", + "serde_json 1.0.95", "socket2 0.3.19", "sodiumoxide", "sysinfo", "tokio", "tokio-socks", "tokio-util", - "toml 0.7.2", + "toml 0.7.3", "winapi 0.3.9", "zstd", ] @@ -2878,16 +2906,16 @@ dependencies = [ "bindgen 0.59.2", "cc", "log", - "serde 1.0.154", + "serde 1.0.159", "serde_derive", - "serde_json 1.0.94", + "serde_json 1.0.95", ] [[package]] name = "hyper" -version = "0.14.24" +version = "0.14.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" +checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" dependencies = [ "bytes", "futures-channel", @@ -2922,16 +2950,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "0c17cc76786e99f8d2f055c11159e7f0091c42474dcc3189fbab96072e873e6d" dependencies = [ "android_system_properties", "core-foundation-sys 0.8.3", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi 0.3.9", + "windows 0.46.0", ] [[package]] @@ -2962,9 +2990,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.5" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" dependencies = [ "bytemuck", "byteorder", @@ -2975,7 +3003,7 @@ dependencies = [ "num-rational 0.4.1", "num-traits 0.2.15", "png", - "scoped_threadpool", + "qoi", "tiff", ] @@ -3002,15 +3030,15 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", ] [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg 1.1.0", "hashbrown", @@ -3050,10 +3078,11 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" dependencies = [ + "hermit-abi 0.3.1", "libc", "windows-sys 0.45.0", ] @@ -3069,15 +3098,15 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" [[package]] name = "is-terminal" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", @@ -3193,7 +3222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68" dependencies = [ "bitflags", - "serde 1.0.154", + "serde 1.0.159", "unicode-segmentation", ] @@ -3241,9 +3270,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.139" +version = "0.2.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" [[package]] name = "libdbus-sys" @@ -3378,9 +3407,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "cd550e73688e6d578f0ac2119e32b797a327631a42f9433e59d02e139c8df60d" [[package]] name = "lock_api" @@ -3412,10 +3441,10 @@ dependencies = [ ] [[package]] -name = "mach" -version = "0.3.2" +name = "mach2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" dependencies = [ "libc", ] @@ -3503,9 +3532,9 @@ dependencies = [ [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" @@ -3605,9 +3634,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee091608fe840d80c6b25e8f838964b264ee8e02e82ae0a38b2d900464d1582" +checksum = "1a3ef954ff22d2646c21ae64171b76d6e1202d214c6d3867305489d04a06db6a" dependencies = [ "cocoa", "crossbeam-channel", @@ -3705,10 +3734,10 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ - "darling 0.13.4", + "darling", "proc-macro-crate 1.3.1", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -3747,6 +3776,55 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "netlink-packet-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5cf0b54effda4b91615c40ff0fd12d0d4c9a6e0f5116874f03941792ff535a" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea993e32c77d87f01236c38f572ecb6c311d592e56a06262a007fd2a6e31253c" +dependencies = [ + "anyhow", + "bitflags", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror", +] + +[[package]] +name = "netlink-sys" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411" +dependencies = [ + "bytes", + "libc", + "log", +] + [[package]] name = "nix" version = "0.22.3" @@ -3854,8 +3932,8 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -3935,8 +4013,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -3990,12 +4068,12 @@ dependencies = [ [[package]] name = "oboe" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" +checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0" dependencies = [ - "jni 0.19.0", - "ndk 0.6.0", + "jni 0.20.0", + "ndk 0.7.0", "ndk-context", "num-derive", "num-traits 0.2.15", @@ -4004,9 +4082,9 @@ dependencies = [ [[package]] name = "oboe-sys" -version = "0.4.5" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3370abb7372ed744232c12954d920d1a40f1c4686de9e79e800021ef492294bd" +checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2" dependencies = [ "cc", ] @@ -4057,9 +4135,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.4.1" +version = "6.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" [[package]] name = "osascript" @@ -4067,9 +4145,9 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" dependencies = [ - "serde 1.0.154", + "serde 1.0.159", "serde_derive", - "serde_json 1.0.94", + "serde_json 1.0.95", ] [[package]] @@ -4078,6 +4156,38 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "pam" +version = "0.7.0" +source = "git+https://github.com/fufesou/pam#10da2cbbabe32cbc9de22a66abe44738e7ec0ea0" +dependencies = [ + "libc", + "pam-macros", + "pam-sys", + "users 0.10.0", +] + +[[package]] +name = "pam-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" +dependencies = [ + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 1.0.109", +] + +[[package]] +name = "pam-sys" +version = "1.0.0-alpha4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9dfd42858f6a6bb1081079fd9dc259ca3e2aaece6cb689fd36b1058046c969" +dependencies = [ + "bindgen 0.59.2", + "libc", +] + [[package]] name = "pango" version = "0.16.5" @@ -4101,7 +4211,7 @@ dependencies = [ "glib-sys 0.16.3", "gobject-sys 0.16.3", "libc", - "system-deps 6.0.3", + "system-deps 6.0.4", ] [[package]] @@ -4155,7 +4265,7 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi 0.3.9", ] @@ -4168,7 +4278,7 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "windows-sys 0.45.0", ] @@ -4250,8 +4360,8 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -4275,15 +4385,15 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "plist" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffac6a51110e97610dd3ac73e34a65b27e56a1e305df41bad1f616d8e1cb22f4" +checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" dependencies = [ "base64", "indexmap", "line-wrap", "quick-xml", - "serde 1.0.154", + "serde 1.0.159", "time 0.3.20", ] @@ -4362,8 +4472,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", "version_check", ] @@ -4374,8 +4484,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "version_check", ] @@ -4390,9 +4500,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" dependencies = [ "unicode-ident", ] @@ -4449,6 +4559,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quest" version = "0.3.0" @@ -4464,9 +4583,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.27.1" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" +checksum = "e5c1a97b1bc42b1d550bfb48d4262153fe400a12bab1511821736f7eac76d7e2" dependencies = [ "memchr", ] @@ -4491,9 +4610,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4ced82a24bb281af338b9e8f94429b6eca01b4e66d899f40031f074e74c9" +checksum = "67c10f662eee9c94ddd7135043e544f3c82fa839a1e7b865911331961b53186c" dependencies = [ "bytes", "rand 0.8.5", @@ -4532,11 +4651,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ - "proc-macro2 1.0.51", + "proc-macro2 1.0.54", ] [[package]] @@ -4721,7 +4840,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#25a99ce71ab42843ad253dd51e6a35e83e87a8a4" +source = "git+https://github.com/fufesou/rdev#f43a42fbedf1234a4bc132581790d63c9a2c8f92" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4769,6 +4888,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -4776,15 +4904,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", - "redox_syscall", + "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" -version = "1.7.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", @@ -4793,9 +4921,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "repng" @@ -4809,9 +4937,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" +checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" dependencies = [ "base64", "bytes", @@ -4832,8 +4960,8 @@ dependencies = [ "pin-project-lite", "rustls", "rustls-pemfile", - "serde 1.0.154", - "serde_json 1.0.94", + "serde 1.0.159", + "serde_json 1.0.95", "serde_urlencoded", "tokio", "tokio-rustls", @@ -4861,6 +4989,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "ringbuf" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ca10b9c9e53ac855a2d6953bce34cef6edbac32c4b13047a4d59d67299420a" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "rpassword" version = "2.1.0" @@ -4935,9 +5072,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" [[package]] name = "rustc-hash" @@ -4945,22 +5082,13 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.16", + "semver", ] [[package]] @@ -4977,12 +5105,13 @@ dependencies = [ "cfg-if 1.0.0", "chrono", "cidr-utils", - "clap 4.1.8", + "clap 4.2.1", "clipboard", "cocoa", "core-foundation 0.9.3", "core-graphics 0.22.3", "cpal", + "crossbeam-queue", "ctrlc", "dark-light", "dasp", @@ -4990,9 +5119,8 @@ dependencies = [ "dbus-crossroads", "default-net", "dispatch", - "dlopen", "enigo", - "errno 0.3.0", + "errno", "evdev", "flutter_rust_bridge", "flutter_rust_bridge_codegen", @@ -5005,6 +5133,7 @@ dependencies = [ "include_dir", "jni 0.19.0", "lazy_static", + "libloading", "libpulse-binding", "libpulse-simple-binding", "mac_address", @@ -5015,10 +5144,12 @@ dependencies = [ "objc", "objc_id", "os-version", + "pam", "parity-tokio-ipc", "rdev", "repng", "reqwest", + "ringbuf", "rpassword 7.2.0", "rubato", "runas", @@ -5026,19 +5157,19 @@ dependencies = [ "samplerate", "sciter-rs", "scrap", - "serde 1.0.154", + "serde 1.0.159", "serde_derive", - "serde_json 1.0.94", + "serde_json 1.0.95", "sha2", "shared_memory", "shutdown_hooks", - "simple_rc", "sys-locale", "system_shutdown", "tao", "tray-icon", "trayicon", "url", + "users 0.11.0", "uuid", "virtual_display", "whoami", @@ -5048,7 +5179,6 @@ dependencies = [ "winreg 0.10.1", "winres", "wol-rs", - "xrandr-parser", ] [[package]] @@ -5078,12 +5208,12 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.9" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" +checksum = "0e78cc525325c06b4a7ff02db283472f3c042b7ff0c391f96c6d5ac6f4f91b75" dependencies = [ "bitflags", - "errno 0.2.8", + "errno", "io-lifetimes", "libc", "linux-raw-sys", @@ -5185,12 +5315,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" -[[package]] -name = "scoped_threadpool" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" - [[package]] name = "scopeguard" version = "1.1.0" @@ -5219,8 +5343,8 @@ dependencies = [ "num_cpus", "quest", "repng", - "serde 1.0.154", - "serde_json 1.0.94", + "serde 1.0.159", + "serde_json 1.0.95", "target_build_utils", "tracing", "webm", @@ -5268,28 +5392,13 @@ dependencies = [ [[package]] name = "semver" -version = "0.9.0" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" dependencies = [ - "semver-parser", + "serde 1.0.159", ] -[[package]] -name = "semver" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" -dependencies = [ - "serde 1.0.154", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "serde" version = "0.9.15" @@ -5298,22 +5407,22 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" -version = "1.0.154" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e" +checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.154" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", - "syn 1.0.109", + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 2.0.11", ] [[package]] @@ -5330,24 +5439,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" dependencies = [ "itoa 1.0.6", "ryu", - "serde 1.0.154", + "serde 1.0.159", ] [[package]] name = "serde_repr" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395627de918015623b32e7669714206363a7fc00382bf477e72c1f7533e8eafc" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", - "syn 1.0.109", + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 2.0.11", ] [[package]] @@ -5356,7 +5465,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" dependencies = [ - "serde 1.0.154", + "serde 1.0.159", ] [[package]] @@ -5368,7 +5477,7 @@ dependencies = [ "form_urlencoded", "itoa 1.0.6", "ryu", - "serde 1.0.154", + "serde 1.0.159", ] [[package]] @@ -5379,19 +5488,10 @@ checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", - "serde 1.0.154", + "serde 1.0.159", "yaml-rust", ] -[[package]] -name = "sha1" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" -dependencies = [ - "sha1_smol", -] - [[package]] name = "sha1" version = "0.10.5" @@ -5403,12 +5503,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - [[package]] name = "sha2" version = "0.10.6" @@ -5472,20 +5566,9 @@ checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "simd-adler32" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14a5df39617d7c8558154693a1bb8157a4aab8179209540cc0b10e5dc24e0b18" - -[[package]] -name = "simple_rc" -version = "0.1.0" -dependencies = [ - "confy", - "hbb_common", - "serde 1.0.154", - "serde_derive", - "walkdir", -] +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" [[package]] name = "siphasher" @@ -5557,7 +5640,7 @@ dependencies = [ "ed25519", "libc", "libsodium-sys", - "serde 1.0.154", + "serde 1.0.159", ] [[package]] @@ -5568,9 +5651,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc" +checksum = "c0959fd6f767df20b231736396e4f602171e00d95205676286e79d4a4eb67bef" dependencies = [ "lock_api", ] @@ -5581,55 +5664,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stdweb" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version 0.2.3", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", - "serde 1.0.154", - "serde_derive", - "syn 1.0.109", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2 1.0.51", - "quote 1.0.23", - "serde 1.0.154", - "serde_derive", - "serde_json 1.0.94", - "sha1 0.6.1", - "syn 1.0.109", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "str-buf" version = "1.0.6" @@ -5648,12 +5682,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - [[package]] name = "strsim" version = "0.10.0" @@ -5679,8 +5707,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" dependencies = [ "heck 0.3.3", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -5691,8 +5719,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "rustversion", "syn 1.0.109", ] @@ -5714,8 +5742,19 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40" +dependencies = [ + "proc-macro2 1.0.54", + "quote 1.0.26", "unicode-ident", ] @@ -5725,8 +5764,8 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", "unicode-xid 0.2.4", ] @@ -5746,9 +5785,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.28.2" +version = "0.28.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e847e2de7a137c8c2cede5095872dbb00f4f9bf34d061347e36b43322acd56" +checksum = "b4c2f3ca6693feb29a89724516f016488e9aafc7f37264f898593ee4b942f31b" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.3", @@ -5797,14 +5836,14 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.0.3" +version = "6.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" +checksum = "555fc8147af6256f3931a36bb83ad0023240ce9cf2b319dec8236fd1f220b05f" dependencies = [ "cfg-expr", "heck 0.4.1", "pkg-config", - "toml 0.5.11", + "toml 0.7.3", "version-compare 0.1.1", ] @@ -5820,8 +5859,8 @@ dependencies = [ [[package]] name = "tao" -version = "0.17.0" -source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf" +version = "0.18.1" +source = "git+https://github.com/tauri-apps/tao?branch=muda#0c1417884161d165b8852fbf11be5492edef9fe7" dependencies = [ "bitflags", "cairo-rs", @@ -5865,11 +5904,11 @@ dependencies = [ [[package]] name = "tao-macros" -version = "0.1.0" -source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf" +version = "0.1.1" +source = "git+https://github.com/tauri-apps/tao?branch=muda#0c1417884161d165b8852fbf11be5492edef9fe7" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -5892,15 +5931,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if 1.0.0", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -5939,7 +5978,7 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/fufesou/The-Fat-Controller#1dba9a39c089ac9a7853b9dd5399c1d4aa3157d3" +source = "git+https://github.com/fufesou/The-Fat-Controller#9dd86151525fd010dc93f6bc9b6aedd1a75cc342" dependencies = [ "anyhow", "core-graphics 0.22.3", @@ -5950,22 +5989,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", - "syn 1.0.109", + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 2.0.11", ] [[package]] @@ -6006,7 +6045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ "itoa 1.0.6", - "serde 1.0.154", + "serde 1.0.159", "time-core", "time-macros", ] @@ -6043,14 +6082,13 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.26.0" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" dependencies = [ "autocfg 1.1.0", "bytes", "libc", - "memchr", "mio 0.8.6", "num_cpus", "parking_lot 0.12.1", @@ -6063,13 +6101,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", - "syn 1.0.109", + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 2.0.11", ] [[package]] @@ -6123,16 +6161,16 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ - "serde 1.0.154", + "serde 1.0.159", ] [[package]] name = "toml" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" dependencies = [ - "serde 1.0.154", + "serde 1.0.159", "serde_spanned", "toml_datetime", "toml_edit", @@ -6144,17 +6182,17 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" dependencies = [ - "serde 1.0.154", + "serde 1.0.159", ] [[package]] name = "toml_edit" -version = "0.19.4" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ "indexmap", - "serde 1.0.154", + "serde 1.0.159", "serde_spanned", "toml_datetime", "winnow", @@ -6190,8 +6228,8 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -6216,9 +6254,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87445e3a107818c17d87e8369db30a6fc25539bface8351efe2132b22e47dbc" +checksum = "405009de7f59e6dafff7afc04e1bff017d5e1531359bd05a1c52375b6f8b3716" dependencies = [ "cocoa", "core-graphics 0.22.3", @@ -6236,7 +6274,7 @@ dependencies = [ [[package]] name = "trayicon" version = "0.1.3-1" -source = "git+https://github.com/open-trade/trayicon-rs#8d9c4489287752cc5be4a35c103198f7111112f9" +source = "git+https://github.com/open-trade/trayicon-rs#35bd01963271b45a0b6a0f65f1ce03a5f35bc691" dependencies = [ "winapi 0.3.9", "winit", @@ -6275,9 +6313,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.11" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" @@ -6333,9 +6371,35 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde 1.0.154", + "serde 1.0.159", ] +[[package]] +name = "users" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.3.0" @@ -6375,7 +6439,6 @@ version = "0.1.0" dependencies = [ "hbb_common", "lazy_static", - "libloading", ] [[package]] @@ -6406,12 +6469,11 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" dependencies = [ "same-file", - "winapi 0.3.9", "winapi-util", ] @@ -6456,8 +6518,8 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", "wasm-bindgen-shared", ] @@ -6480,7 +6542,7 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ - "quote 1.0.23", + "quote 1.0.26", "wasm-bindgen-macro-support", ] @@ -6490,8 +6552,8 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", @@ -6560,8 +6622,8 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "xml-rs", ] @@ -6642,9 +6704,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45dbc71f0cdca27dc261a9bd37ddec174e4a0af2b900b890f378460f745426e3" +checksum = "2c70234412ca409cc04e864e89523cb0fc37f5e1344ebed5a3ebf4192b6b9f68" dependencies = [ "wasm-bindgen", "web-sys", @@ -6725,15 +6787,15 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b749ebd2304aa012c5992d11a25d07b406bdbe5f79d371cb7a918ce501a19eb0" +checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" dependencies = [ - "windows_aarch64_msvc 0.30.0", - "windows_i686_gnu 0.30.0", - "windows_i686_msvc 0.30.0", - "windows_x86_64_gnu 0.30.0", - "windows_x86_64_msvc 0.30.0", + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", ] [[package]] @@ -6749,19 +6811,6 @@ dependencies = [ "windows_x86_64_msvc 0.34.0", ] -[[package]] -name = "windows" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" -dependencies = [ - "windows_aarch64_msvc 0.37.0", - "windows_i686_gnu 0.37.0", - "windows_i686_msvc 0.37.0", - "windows_x86_64_gnu 0.37.0", - "windows_x86_64_msvc 0.37.0", -] - [[package]] name = "windows" version = "0.44.0" @@ -6773,14 +6822,23 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-implement" version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -6790,8 +6848,8 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] @@ -6827,12 +6885,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.1", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -6846,24 +6904,24 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.1", + "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_msvc" @@ -6873,9 +6931,9 @@ checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" [[package]] name = "windows_aarch64_msvc" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" [[package]] name = "windows_aarch64_msvc" @@ -6885,15 +6943,9 @@ checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" [[package]] name = "windows_aarch64_msvc" -version = "0.37.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_i686_gnu" @@ -6903,9 +6955,9 @@ checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" [[package]] name = "windows_i686_gnu" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" [[package]] name = "windows_i686_gnu" @@ -6915,15 +6967,9 @@ checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" [[package]] name = "windows_i686_gnu" -version = "0.37.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_msvc" @@ -6933,9 +6979,9 @@ checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" [[package]] name = "windows_i686_msvc" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" [[package]] name = "windows_i686_msvc" @@ -6945,15 +6991,9 @@ checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" [[package]] name = "windows_i686_msvc" -version = "0.37.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_x86_64_gnu" @@ -6963,9 +7003,9 @@ checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" [[package]] name = "windows_x86_64_gnu" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" [[package]] name = "windows_x86_64_gnu" @@ -6975,21 +7015,15 @@ checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" [[package]] name = "windows_x86_64_gnu" -version = "0.37.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_msvc" @@ -6999,9 +7033,9 @@ checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" [[package]] name = "windows_x86_64_msvc" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" [[package]] name = "windows_x86_64_msvc" @@ -7011,15 +7045,9 @@ checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" [[package]] name = "windows_x86_64_msvc" -version = "0.37.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "winit" @@ -7056,9 +7084,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee7b2c67f962bf5042bfd8b6a916178df33a26eec343ae064cb8e069f638fa6f" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" dependencies = [ "memchr", ] @@ -7182,16 +7210,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" -[[package]] -name = "xrandr-parser" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5af43ba661cee58bd86b9f81a899e45a15ac7f42fa4401340f73c0c2950030c1" -dependencies = [ - "derive_setters", - "serde 1.0.154", -] - [[package]] name = "yaml-rust" version = "0.4.5" @@ -7203,9 +7221,9 @@ dependencies = [ [[package]] name = "zbus" -version = "3.11.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20aae5dd5b051971cd2f49f9f3b860e57b2b495ba5ba254eaec42d34ede57e97" +checksum = "3dc29e76f558b2cb94190e8605ecfe77dd40f5df8c072951714b4b71a97f5848" dependencies = [ "async-broadcast", "async-executor", @@ -7228,9 +7246,9 @@ dependencies = [ "once_cell", "ordered-stream", "rand 0.8.5", - "serde 1.0.154", + "serde 1.0.159", "serde_repr", - "sha1 0.10.5", + "sha1", "static_assertions", "tracing", "uds_windows", @@ -7242,13 +7260,13 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.11.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9264b3a1bcf5503d4e0348b6e7efe1da58d4f92a913c15ed9e63b52de85faaa1" +checksum = "62a80fd82c011cd08459eaaf1fd83d3090c1b61e6d5284360074a7475af3a85d" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "regex", "syn 1.0.109", "zvariant_utils", @@ -7260,7 +7278,7 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" dependencies = [ - "serde 1.0.154", + "serde 1.0.159", "static_assertions", "zvariant", ] @@ -7296,9 +7314,9 @@ dependencies = [ [[package]] name = "zune-inflate" -version = "0.2.51" +version = "0.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01728b79fb9b7e28a8c11f715e1cd8dc2cda7416a007d66cac55cebb3a8ac6b" +checksum = "440a08fd59c6442e4b846ea9b10386c38307eae728b216e1ab2c305d1c9daaf8" dependencies = [ "simd-adler32", ] @@ -7312,7 +7330,7 @@ dependencies = [ "byteorder", "enumflags2", "libc", - "serde 1.0.154", + "serde 1.0.159", "static_assertions", "zvariant_derive", ] @@ -7324,8 +7342,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34c20260af4b28b3275d6676c7e2a6be0d4332e8e0aba4616d34007fd84e462a" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", "zvariant_utils", ] @@ -7336,7 +7354,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53b22993dbc4d128a17a3b6c92f1c63872dd67198537ee728d8b5d7c40640a8b" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.54", + "quote 1.0.26", "syn 1.0.109", ] diff --git a/Cargo.toml b/Cargo.toml index 7ad979f8c..784b83537 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ path = "src/naming.rs" inline = [] hbbs = [] cli = [] -with_rc = ["simple_rc"] flutter_texture_render = [] appimage = [] flatpak = [] @@ -30,6 +29,9 @@ flutter = ["flutter_rust_bridge"] default = ["use_dasp"] hwcodec = ["scrap/hwcodec"] mediacodec = ["scrap/mediacodec"] +linux_headless = ["pam", "users"] +virtual_display_driver = ["virtual_display"] +plugin_framework = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -57,21 +59,22 @@ rpassword = "7.0" base64 = "0.21" num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } -default-net = "0.12.0" +default-net = "0.14" wol-rs = "1.0" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.3" rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } -dlopen = "0.1" -hex = "0.4.3" - +crossbeam-queue = "0.3" +hex = "0.4" reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } -chrono = "0.4.23" -cidr-utils = "0.5.9" +chrono = "0.4" +cidr-utils = "0.5" +libloading = "0.7" [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] -cpal = "0.14" +cpal = "0.15" +ringbuf = "0.3" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] machine-uid = "0.2" @@ -91,10 +94,10 @@ winit = "0.26" winapi = { version = "0.3", features = ["winuser", "wincrypt"] } winreg = "0.10" windows-service = "0.4" -virtual_display = { path = "libs/virtual_display" } +virtual_display = { path = "libs/virtual_display", optional = true } impersonate_system = { git = "https://github.com/21pages/impersonate-system" } -shared_memory = "0.12.4" -shutdown_hooks = "0.1.0" +shared_memory = "0.12" +shutdown_hooks = "0.1" [target.'cfg(target_os = "macos")'.dependencies] objc = "0.2" @@ -102,10 +105,10 @@ cocoa = "0.24" dispatch = "0.2" core-foundation = "0.9" core-graphics = "0.22" -include_dir = "0.7.2" +include_dir = "0.7" dark-light = "1.0" -fruitbasket = "0.10.0" -objc_id = "0.1.1" +fruitbasket = "0.10" +objc_id = "0.1" [target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] tray-icon = "0.4" @@ -121,7 +124,8 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" -xrandr-parser = "0.3.0" +pam = { git="https://github.com/fufesou/pam", optional = true } +users = { version = "0.11.0", optional = true } [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" @@ -131,8 +135,8 @@ jni = "0.19" flutter_rust_bridge = "1.61.1" [workspace] -members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"] -exclude = ["vdi/host"] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"] +exclude = ["vdi/host", "examples/custom_plugin"] [package.metadata.winres] LegalCopyright = "Copyright © 2022 Purslane, Inc." @@ -146,7 +150,6 @@ winapi = { version = "0.3", features = [ "winnt" ] } [build-dependencies] cc = "1.0" hbb_common = { path = "libs/hbb_common" } -simple_rc = { path = "libs/simple_rc", optional = true } flutter_rust_bridge_codegen = "1.61.1" os-version = "0.2" @@ -157,7 +160,6 @@ hound = "3.5" name = "RustDesk" identifier = "com.carriez.rustdesk" icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"] -deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "curl", "libvdpau1", "libva2"] osx_minimum_system_version = "10.14" #https://github.com/johnthagen/min-sized-rust diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index f3cd8f568..87c854d5d 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -32,6 +32,9 @@ AppDir: - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted universe multiverse key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted + universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' include: - libc6 - libgtk-3-0 @@ -47,6 +50,9 @@ AppDir: - libva-x11-2 - libvdpau1 - libgstreamer-plugins-base1.0-0 + - libwayland-cursor0 + - libwayland-egl1 + - libpulse0 exclude: - humanity-icon-theme - hicolor-icon-theme diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 59dd5164f..8f72afd08 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -33,6 +33,8 @@ AppDir: - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted + universe multiverse - sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu bionic main include: @@ -50,6 +52,9 @@ AppDir: - libva-x11-2 - libvdpau1 - libgstreamer-plugins-base1.0-0 + - libwayland-cursor0 + - libwayland-egl1 + - libpulse0 exclude: - humanity-icon-theme - hicolor-icon-theme diff --git a/build.py b/build.py index 4a39f596d..6cb8c0e80 100755 --- a/build.py +++ b/build.py @@ -15,9 +15,21 @@ osx = platform.platform().startswith( 'Darwin') or platform.platform().startswith("macOS") hbb_name = 'rustdesk' + ('.exe' if windows else '') exe_path = 'target/release/' + hbb_name -flutter_win_target_dir = 'flutter/build/windows/runner/Release/' +if windows: + flutter_build_dir = 'build/windows/runner/Release/' +elif osx: + flutter_build_dir = 'build/macos/Build/Products/Release/' +else: + flutter_build_dir = 'build/linux/x64/release/bundle/' +flutter_build_dir_2 = f'flutter/{flutter_build_dir}' skip_cargo = False +def get_arch() -> str: + custom_arch = os.environ.get("ARCH") + if custom_arch is None: + return "amd64" + return custom_arch + def system2(cmd): err = os.system(cmd) if err != 0: @@ -35,11 +47,13 @@ def get_version(): def parse_rc_features(feature): available_features = { 'IddDriver': { - 'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64.zip', - 'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/checksum_md5', + 'platform': ['windows'], + 'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.3/RustDeskIddDriver_x64.zip', + 'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.3/checksum_md5', 'exclude': ['README.md', 'certmgr.exe', 'install_cert_runas_admin.bat'], }, 'PrivacyMode': { + 'platform': ['windows'], 'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1' '/TempTopMostWindow_x64_pic_en.zip', 'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5', @@ -49,16 +63,34 @@ def parse_rc_features(feature): apply_features = {} if not feature: feature = [] + + def platform_check(platforms): + if windows: + return 'windows' in platforms + elif osx: + return 'osx' in platforms + else: + return 'linux' in platforms + + def get_all_features(): + features = [] + for (feat, feat_info) in available_features.items(): + if platform_check(feat_info['platform']): + features.append(feat) + return features + if isinstance(feature, str) and feature.upper() == 'ALL': - return available_features + return get_all_features() elif isinstance(feature, list): - # force add PrivacyMode - feature.append('PrivacyMode') + if windows: + # force add PrivacyMode + feature.append('PrivacyMode') for feat in feature: if isinstance(feat, str) and feat.upper() == 'ALL': - return available_features + return get_all_features() if feat in available_features: - apply_features[feat] = available_features[feat] + if platform_check(available_features[feat]['platform']): + apply_features[feat] = available_features[feat] else: print(f'Unrecognized feature {feat}') return apply_features @@ -106,6 +138,10 @@ def make_parser(): action='store_true', help='Skip cargo build process, only flutter version + Linux supported currently' ) + parser.add_argument( + "--package", + type=str + ) return parser @@ -201,14 +237,12 @@ def download_extract_features(features, res_dir): print(f'{feat} extract end') -def get_rc_features(args): - flutter = args.flutter +def external_resources(flutter, args, res_dir): features = parse_rc_features(args.feature) if not features: - return [] + return print(f'Build with features {list(features.keys())}') - res_dir = 'resources' if os.path.isdir(res_dir) and not os.path.islink(res_dir): shutil.rmtree(res_dir) elif os.path.exists(res_dir): @@ -216,22 +250,19 @@ def get_rc_features(args): os.makedirs(res_dir, exist_ok=True) download_extract_features(features, res_dir) if flutter: - os.makedirs(flutter_win_target_dir, exist_ok=True) + os.makedirs(flutter_build_dir_2, exist_ok=True) for f in pathlib.Path(res_dir).iterdir(): print(f'{f}') if f.is_file(): - shutil.copy2(f, flutter_win_target_dir) + shutil.copy2(f, flutter_build_dir_2) else: - shutil.copytree(f, f'{flutter_win_target_dir}{f.stem}') - return [] - else: - return ['with_rc'] + shutil.copytree(f, f'{flutter_build_dir_2}{f.stem}') def get_features(args): features = ['inline'] if not args.flutter else [] if windows: - features.extend(get_rc_features(args)) + features.append('virtual_display_driver') if args.hwcodec: features.append('hwcodec') if args.flutter: @@ -251,13 +282,13 @@ def generate_control_file(version): content = """Package: rustdesk Version: %s -Architecture: amd64 -Maintainer: open-trade +Architecture: %s +Maintainer: rustdesk Homepage: https://rustdesk.com -Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0 +Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g Description: A remote control software. -""" % version +""" % (version, get_arch()) file = open(control_file_path, "w") file.write(content) file.close() @@ -277,12 +308,54 @@ def build_flutter_deb(version, features): system2('flutter build linux --release') system2('mkdir -p tmpdeb/usr/bin/') system2('mkdir -p tmpdeb/usr/lib/rustdesk') + system2('mkdir -p tmpdeb/etc/rustdesk/') + system2('mkdir -p tmpdeb/etc/pam.d/') system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') system2('mkdir -p tmpdeb/usr/share/applications/') system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') system2('rm tmpdeb/usr/bin/rustdesk || true') system2( - 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') + f'cp -r {flutter_build_dir}/* tmpdeb/usr/lib/rustdesk/') + system2( + 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') + system2( + 'cp ../res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') + system2( + 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + system2( + 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') + system2( + 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') + system2( + 'cp ../res/startwm.sh tmpdeb/etc/rustdesk/') + system2( + 'cp ../res/xorg.conf tmpdeb/etc/rustdesk/') + system2( + 'cp ../res/pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk') + system2( + "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") + + system2('mkdir -p tmpdeb/DEBIAN') + generate_control_file(version) + system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') + md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + system2('dpkg-deb -b tmpdeb rustdesk.deb;') + + system2('/bin/rm -rf tmpdeb/') + system2('/bin/rm -rf ../res/DEBIAN/control') + os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) + os.chdir("..") + +def build_deb_from_folder(version, binary_folder): + os.chdir('flutter') + system2('mkdir -p tmpdeb/usr/bin/') + system2('mkdir -p tmpdeb/usr/lib/rustdesk') + system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + system2('mkdir -p tmpdeb/usr/share/applications/') + system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') + system2('rm tmpdeb/usr/bin/rustdesk || true') + system2( + f'cp -r ../{binary_folder}/* tmpdeb/usr/lib/rustdesk/') system2( 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') system2( @@ -307,7 +380,6 @@ def build_flutter_deb(version, features): os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.chdir("..") - def build_flutter_dmg(version, features): if not skip_cargo: # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project @@ -329,7 +401,7 @@ def build_flutter_arch_manjaro(version, features): ffi_bindgen_function_refactor() os.chdir('flutter') system2('flutter build linux --release') - system2('strip build/linux/x64/release/bundle/lib/librustdesk.so') + system2(f'strip {flutter_build_dir}/lib/librustdesk.so') os.chdir('../res') system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f') @@ -344,11 +416,11 @@ def build_flutter_windows(version, features): system2('flutter build windows --release') os.chdir('..') shutil.copy2('target/release/deps/dylib_virtual_display.dll', - flutter_win_target_dir) + flutter_build_dir_2) os.chdir('libs/portable') system2('pip3 install -r requirements.txt') system2( - f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe') + f'python3 ./generate.py -f ../../{flutter_build_dir_2} -o . -e ../../{flutter_build_dir_2}/rustdesk.exe') os.chdir('../..') if os.path.exists('./rustdesk_portable.exe'): os.replace('./target/release/rustdesk-portable-packer.exe', @@ -381,6 +453,12 @@ def main(): if args.skip_cargo: skip_cargo = True portable = args.portable + package = args.package + if package: + build_deb_from_folder(version, package) + return + res_dir = 'resources' + external_resources(flutter, args, res_dir) if windows: # build virtual display dynamic library os.chdir('libs/virtual_display/dylib') @@ -401,7 +479,12 @@ def main(): else: print('Not signed') system2( - f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe') + f'cp -rf target/release/RustDesk.exe {res_dir}') + os.chdir('libs/portable') + system2('pip3 install -r requirements.txt') + system2( + f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe') + system2('mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..') elif os.path.isfile('/usr/bin/pacman'): # pacman -S -needed base-devel system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) @@ -506,12 +589,21 @@ def main(): 'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') system2( 'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') - system2('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') + os.system('mkdir -p tmpdeb/etc/rustdesk/') + os.system('cp -a res/startwm.sh tmpdeb/etc/rustdesk/') + os.system('mkdir -p tmpdeb/etc/X11/rustdesk/') + os.system('cp res/xorg.conf tmpdeb/etc/X11/rustdesk/') + os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/') + os.system('mkdir -p tmpdeb/etc/pam.d/') + os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk') system2('strip tmpdeb/usr/bin/rustdesk') system2('mkdir -p tmpdeb/usr/lib/rustdesk') system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + md5_file('etc/rustdesk/startwm.sh') + md5_file('etc/X11/rustdesk/xorg.conf') + md5_file('etc/pam.d/rustdesk') md5_file('usr/lib/rustdesk/libsciter-gtk.so') system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) diff --git a/build.rs b/build.rs index bf141e539..7ff8f9ef6 100644 --- a/build.rs +++ b/build.rs @@ -41,20 +41,6 @@ fn build_manifest() { } } -#[cfg(all(windows, feature = "with_rc"))] -fn build_rc_source() { - use simple_rc::{generate_with_conf, Config, ConfigItem}; - generate_with_conf(&Config { - outfile: "src/rc.rs".to_owned(), - confs: vec![ConfigItem { - inc: "resources".to_owned(), - exc: vec![], - suppressed_front: "resources".to_owned(), - }], - }) - .unwrap(); -} - fn install_oboe() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); if target_os != "android" { @@ -133,15 +119,15 @@ fn main() { gen_flutter_rust_bridge(); // return; // } - #[cfg(all(windows, feature = "with_rc"))] - build_rc_source(); #[cfg(all(windows, feature = "inline"))] build_manifest(); #[cfg(windows)] build_windows(); - #[cfg(target_os = "macos")] - build_mac(); - #[cfg(target_os = "macos")] - println!("cargo:rustc-link-lib=framework=ApplicationServices"); + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + if target_os == "macos" { + #[cfg(target_os = "macos")] + build_mac(); + println!("cargo:rustc-link-lib=framework=ApplicationServices"); + } println!("cargo:rerun-if-changed=build.rs"); } diff --git a/docs/CODE_OF_CONDUCT-PL.md b/docs/CODE_OF_CONDUCT-PL.md new file mode 100644 index 000000000..8aedf837d --- /dev/null +++ b/docs/CODE_OF_CONDUCT-PL.md @@ -0,0 +1,133 @@ + +# Kod postępowania Contributor Covenant Code of Conduct + +## Nasza przysięga + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Nasze standardy + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[info@rustdesk.com](mailto:info@rustdesk.com). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/docs/CONTRIBUTING-PL.md b/docs/CONTRIBUTING-PL.md new file mode 100644 index 000000000..8341692ba --- /dev/null +++ b/docs/CONTRIBUTING-PL.md @@ -0,0 +1,45 @@ +# Współtworzenie RustDesk + +RustDesk z zadowoleniem przyjmuje wkład od każdego. Oto wytyczne, jeśli chcesz nam pomóc: + +## Współtwórcy + +Contributions to RustDesk or its dependencies should be made in the form of GitHub +pull requests. Each pull request will be reviewed by a core contributor +(someone with permission to land patches) and either landed in the main tree or +given feedback for changes that would be required. All contributions should +follow this format, even those from core contributors. + +Should you wish to work on an issue, please claim it first by commenting on +the GitHub issue that you want to work on it. This is to prevent duplicated +efforts from contributors on the same issue. + +## Pull Request Checklist + +- Branch from the master branch and, if needed, rebase to the current master + branch before submitting your pull request. If it doesn't merge cleanly with + master you may be asked to rebase your changes. + +- Commits should be as small as possible, while ensuring that each commit is + correct independently (i.e., each commit should compile and pass tests). + +- Commits should be accompanied by a Developer Certificate of Origin + (http://developercertificate.org) sign-off, which indicates that you (and + your employer if applicable) agree to be bound by the terms of the + [project license](../LICENCE). In git, this is the `-s` option to `git commit` + +- If your patch is not getting reviewed or you need a specific person to review + it, you can @-reply a reviewer asking for a review in the pull request or a + comment, or you can ask for a review via [email](mailto:info@rustdesk.com). + +- Add tests relevant to the fixed bug or new feature. + +For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Kodeks postępowania + +[Kodeks postępowania](CODE_OF_CONDUCT-PL.md) + +## Komunikacja + +RustDesk contributors frequent the [Discord](https://discord.gg/nDceKgxnkV). diff --git a/docs/DEVCONTAINER-PL.md b/docs/DEVCONTAINER-PL.md new file mode 100644 index 000000000..0aae2b975 --- /dev/null +++ b/docs/DEVCONTAINER-PL.md @@ -0,0 +1,14 @@ + +Po uruchomieniu devcontainer w kontenerze docker, tworzony jest plik binarny linux w trybue debugowania. + +Obecnie devcontainer oferuje kompilowanie wersji dla linux i android w obu trybach - debugowania i wersji finalnej. + +Poniżej tabela poleceń do uruchomienia z głównego folderu do tworzenia wybranych kompilacji. + +Polecenie|Typ kompilacji|Tryb +-|-|-| +`.devcontainer/build.sh --debug linux`|Linux|debug +`.devcontainer/build.sh --release linux`|Linux|release +`.devcontainer/build.sh --debug android`|android-arm64|debug +`.devcontainer/build.sh --release android`|android-arm64|debug + diff --git a/docs/README-PL.md b/docs/README-PL.md index df8254f3d..fb5addb69 100644 --- a/docs/README-PL.md +++ b/docs/README-PL.md @@ -1,5 +1,5 @@

- RustDesk - Your remote desktop
+ RustDesk - Twój zdalny pulpit
SerweryKompilacjaDocker • @@ -13,27 +13,45 @@ Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](http [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego , [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). +Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego , [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) pomoc w uruchomieniu programu. +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -[**POBIERZ KOMPILACJE**](https://github.com/rustdesk/rustdesk/releases) +RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING-PL.md`](CONTRIBUTING-PL.md) pomoc w uruchomieniu programu. + +[**PYTANIA I ODPOWIEDZI (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**POBIERANIE BINARIÓW**](https://github.com/rustdesk/rustdesk/releases) + +[**WERSJE TESTOWE (NIGHTLY)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) ## Darmowe Serwery Publiczne Poniżej znajdują się serwery, z których można korzystać za darmo, może się to zmienić z upływem czasu. Jeśli nie znajdujesz się w pobliżu jednego z nich, Twoja prędkość połączenia może być niska. | Lokalizacja | Dostawca | Specyfikacja | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | -| Germany | Hetzner | 2 vCPU / 4GB RAM | -| Germany | Codext | 4 vCPU / 8GB RAM | -| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Korea Płd. (Seul) | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Niemcy | Hetzner | 2 vCPU / 4GB RAM | +| Niemcy | Codext | 4 vCPU / 8GB RAM | +| Finlandia (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | | USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | -| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | +| Ukraina (Kijów) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | + +## Konterner Programisty (Dev Container) + +[![Otwórz w Kontenerze programisty](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) + +Jeżeli masz zainstalowany VS Code i Docker, możesz kliknąć w powyższy link, aby rozpocząć. Kliknięcie spowoduje automatyczną instalację rozszrzenia Kontenera Programisty w VS Code (jeżeli wymagany), sklonuje kod źródłowy do kontenera, i przygotuje kontener do użycia. + +Więcej informacji w pliku [DEVCONTAINER-PL.md](docs/DEVCONTAINER-PL.md) for more info. ## Zależności -Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać bibliotekę dynamiczną sciter samodzielnie. +Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać samodzielnie bibliotekę sciter. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | @@ -43,7 +61,7 @@ Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobra - Przygotuj środowisko programistyczne Rust i środowisko programowania C++ -- Zainstaluj [vcpkg](https://github.com/microsoft/vcpkg), i ustaw `VCPKG_ROOT` env zmienną prawidłowo +- Zainstaluj [vcpkg](https://github.com/microsoft/vcpkg), i ustaw prawidłowo zmienną `VCPKG_ROOT` - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static - Linux/MacOS: vcpkg install libvpx libyuv opus @@ -58,6 +76,12 @@ Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobra sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake ``` +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +``` + ### Fedora 28 (CentOS 8) ```sh @@ -82,7 +106,7 @@ export VCPKG_ROOT=$HOME/vcpkg vcpkg/vcpkg install libvpx libyuv opus ``` -### Fix libvpx (For Fedora) +### Popraw libvpx (Dla Fedora) ```sh cd vcpkg/buildtrees/libvpx/src @@ -110,7 +134,31 @@ cargo run ### Zmień Wayland na X11 (Xorg) -RustDesk nie obsługuje Waylanda. Sprawdź [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) by skonfigurować Xorg jako domyślną sesję GNOME. +RustDesk nie obsługuje Waylanda. Sprawdź [tutaj](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), jak skonfigurować Xorg jako domyślną sesję GNOME. + +## Wspracie Wayland + +Wygląda na to, że Wayland nie wspiera żadnego API do wysyłania naciśnięć klawiszy do innych okien. Dlatego rustdesk używa API z niższego poziomu, urządzenia o nazwie `/dev/uinput` (poziom jądra Linux). + +Gdy po stronie kontrolowanej pracuje Wayland, musisz uruchomić program w następujący sposób: +```bash +# Start uinput service +$ sudo rustdesk --service +$ rustdesk +``` +**Uwaga**: Nagrywanie ekranu Wayland wykorzystuje różne interfejsy. RustDesk obecnie obsługuje tylko org.freedesktop.portal.ScreenCast. +```bash +$ dbus-send --session --print-reply \ + --dest=org.freedesktop.portal.Desktop \ + /org/freedesktop/portal/desktop \ + org.freedesktop.DBus.Properties.Get \ + string:org.freedesktop.portal.ScreenCast string:version +# Not support +Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” +# Support +method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 + variant uint32 4 +``` ## Jak kompilować za pomocą Dockera @@ -134,7 +182,7 @@ Zauważ, że pierwsza kompilacja może potrwać dłużej zanim zależności zost target/debug/rustdesk ``` -Lub, jeśli uruchamiasz plik wykonywalny wersji: +Lub jeśli uruchamiasz plik wykonywalny wersji: ```sh target/release/rustdesk @@ -144,16 +192,18 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru ## Struktura plików -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: kodek wideo, config, wrapper tcp/udp, protobuf, funkcje fs do transferu plików i kilka innych funkcji użytkowych +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: kodek wideo, konfiguracja, obsługa tcp/udp, protobuf, funkcje systemu plików do transferu plików i kilka innych funkcji użytkowych - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: przechwytywanie ekranu - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: specyficzne dla danej platformy sterowanie klawiaturą/myszą - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/schowek/wejście(input)/wideo oraz połączenia sieciowe -- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: uruchamia połączenie peer -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikacja z [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection -- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: specyficzny dla danej platformy kod +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: uruchamia połączenie bezpośrednie +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Komunikacja z [rustdesk-server](https://github.com/rustdesk/rustdesk-server), czekanie na bezpośrednie (odpytywanie TCP) lub przekazywane połączenie +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: kod specyficzny dla danej platformy +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: kod Flutter dla urządzeń mobilnych +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript dla Flutter - klient web -## Migawki(Snapshoty) +## Zrzuty ekranu ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/SECURITY-PL.md b/docs/SECURITY-PL.md new file mode 100644 index 000000000..0d4975ba0 --- /dev/null +++ b/docs/SECURITY-PL.md @@ -0,0 +1,9 @@ +# Polityka bezpieczeństwa + +## Zgłaszanie podatności + +Bardzo cenimy sobie bezpieczeństwo projektu. Zachęcamy wszystkich użytkowników do zgłaszania nam wszelkich wykrytych luk. +Jeżeli znajdziesz lukę w projekcie RustDesk, proszę zgłosić ją jak najszybciej wysyłając e-mail na adres info@rustdesk.com. + +W tym momencie, nie mamy uruchomionego programu nagradzania za wykryte błędy. Jesteśmy małym zespołem próbującym rozwiązywać duże problemy. +Prosimy o odpowidzialne zgłaszanie wszelkich podatności w zabezpieczeniach, abyśmy mogli kontynuować tworzenie bezpiecznej aplikacji dla całej społeczności. diff --git a/examples/custom_plugin/Cargo.toml b/examples/custom_plugin/Cargo.toml new file mode 100644 index 000000000..b9ee06ae7 --- /dev/null +++ b/examples/custom_plugin/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "custom_plugin" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "custom_plugin" +path = "src/lib.rs" +crate-type = ["cdylib"] + + +[features] +default = ["flutter"] +flutter = [] + +[dependencies] +lazy_static = "1.4.0" +rustdesk = { path = "../../", version = "1.2.0", features = ["flutter"]} + +[profile.release] +lto = true +codegen-units = 1 +panic = 'abort' +strip = true +#opt-level = 'z' # only have smaller size after strip +rpath = true \ No newline at end of file diff --git a/examples/custom_plugin/src/lib.rs b/examples/custom_plugin/src/lib.rs new file mode 100644 index 000000000..0b21f3fc8 --- /dev/null +++ b/examples/custom_plugin/src/lib.rs @@ -0,0 +1,30 @@ +use librustdesk::api::RustDeskApiTable; +/// This file demonstrates how to write a custom plugin for RustDesk. +use std::ffi::{c_char, c_int, CString}; + +lazy_static::lazy_static! { + pub static ref PLUGIN_NAME: CString = CString::new("A Template Rust Plugin").unwrap(); + pub static ref PLUGIN_ID: CString = CString::new("TemplatePlugin").unwrap(); + // Do your own logic based on the API provided by RustDesk. + pub static ref API: RustDeskApiTable = RustDeskApiTable::default(); +} + +#[no_mangle] +fn plugin_name() -> *const c_char { + return PLUGIN_NAME.as_ptr(); +} + +#[no_mangle] +fn plugin_id() -> *const c_char { + return PLUGIN_ID.as_ptr(); +} + +#[no_mangle] +fn plugin_init() -> c_int { + return 0 as _; +} + +#[no_mangle] +fn plugin_dispose() -> c_int { + return 0 as _; +} diff --git a/flutter/build_android_deps.sh b/flutter/build_android_deps.sh index a30abd154..2f876ca6e 100755 --- a/flutter/build_android_deps.sh +++ b/flutter/build_android_deps.sh @@ -58,13 +58,12 @@ function build { fi make clean ./configure --target=$LIBVPX_TARGET \ - --enable-pic --disable-vp8 \ + --enable-pic --disable-webm-io \ --disable-unit-tests \ --disable-examples \ --disable-libyuv \ --disable-postproc \ - --disable-vp8 \ --disable-tools \ --disable-docs \ --prefix=$PREFIX @@ -122,4 +121,4 @@ build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc # rm -rf build/libvpx -# rm -rf build/oboe \ No newline at end of file +# rm -rf build/oboe diff --git a/flutter/ios/Flutter/AppFrameworkInfo.plist b/flutter/ios/Flutter/AppFrameworkInfo.plist index 8d4492f97..9625e105d 100644 --- a/flutter/ios/Flutter/AppFrameworkInfo.plist +++ b/flutter/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/flutter/ios/Podfile b/flutter/ios/Podfile index 0df0a09f8..3f67abf6a 100644 --- a/flutter/ios/Podfile +++ b/flutter/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/flutter/ios/Podfile.lock b/flutter/ios/Podfile.lock index ff8c9282f..76d0bac73 100644 --- a/flutter/ios/Podfile.lock +++ b/flutter/ios/Podfile.lock @@ -1,194 +1,146 @@ PODS: - - device_info (0.0.1): + - device_info_plus (0.0.1): - Flutter - - Firebase/Analytics (8.15.0): - - Firebase/Core - - Firebase/Core (8.15.0): - - Firebase/CoreOnly - - FirebaseAnalytics (~> 8.15.0) - - Firebase/CoreOnly (8.15.0): - - FirebaseCore (= 8.15.0) - - firebase_analytics (9.1.8): - - Firebase/Analytics (= 8.15.0) - - firebase_core + - DKImagePickerController/Core (4.3.4): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.4) + - DKImagePickerController/PhotoGallery (4.3.4): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.4) + - DKPhotoGallery (0.0.17): + - DKPhotoGallery/Core (= 0.0.17) + - DKPhotoGallery/Model (= 0.0.17) + - DKPhotoGallery/Preview (= 0.0.17) + - DKPhotoGallery/Resource (= 0.0.17) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.17): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.17): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery - Flutter - - firebase_core (1.17.0): - - Firebase/CoreOnly (= 8.15.0) - - Flutter - - FirebaseAnalytics (8.15.0): - - FirebaseAnalytics/AdIdSupport (= 8.15.0) - - FirebaseCore (~> 8.0) - - FirebaseInstallations (~> 8.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - FirebaseAnalytics/AdIdSupport (8.15.0): - - FirebaseCore (~> 8.0) - - FirebaseInstallations (~> 8.0) - - GoogleAppMeasurement (= 8.15.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - FirebaseCore (8.15.0): - - FirebaseCoreDiagnostics (~> 8.0) - - GoogleUtilities/Environment (~> 7.7) - - GoogleUtilities/Logger (~> 7.7) - - FirebaseCoreDiagnostics (8.15.0): - - GoogleDataTransport (~> 9.1) - - GoogleUtilities/Environment (~> 7.7) - - GoogleUtilities/Logger (~> 7.7) - - nanopb (~> 2.30908.0) - - FirebaseInstallations (8.15.0): - - FirebaseCore (~> 8.0) - - GoogleUtilities/Environment (~> 7.7) - - GoogleUtilities/UserDefaults (~> 7.7) - - PromisesObjC (< 3.0, >= 1.2) - Flutter (1.0.0) - - GoogleAppMeasurement (8.15.0): - - GoogleAppMeasurement/AdIdSupport (= 8.15.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (8.15.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 8.15.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (8.15.0): - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - GoogleDataTransport (9.1.4): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.7.0): - - GoogleUtilities/Environment - - GoogleUtilities/Logger - - GoogleUtilities/Network - - GoogleUtilities/Environment (7.7.0): - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.7.0): - - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.7.0): - - GoogleUtilities/Logger - - GoogleUtilities/Network (7.7.0): - - GoogleUtilities/Logger - - "GoogleUtilities/NSData+zlib" - - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.7.0)" - - GoogleUtilities/Reachability (7.7.0): - - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.7.0): - - GoogleUtilities/Logger + - flutter_keyboard_visibility (0.0.1): + - Flutter + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) - image_picker_ios (0.0.1): - Flutter - MTBBarcodeScanner (5.0.11) - - nanopb (2.30908.0): - - nanopb/decode (= 2.30908.0) - - nanopb/encode (= 2.30908.0) - - nanopb/decode (2.30908.0) - - nanopb/encode (2.30908.0) - - package_info (0.0.1): + - package_info_plus (0.4.5): - Flutter - - path_provider_ios (0.0.1): + - path_provider_foundation (0.0.1): - Flutter - - PromisesObjC (2.1.0) + - FlutterMacOS - qr_code_scanner (0.2.0): - Flutter - MTBBarcodeScanner - - shared_preferences_ios (0.0.1): + - SDWebImage (5.15.5): + - SDWebImage/Core (= 5.15.5) + - SDWebImage/Core (5.15.5) + - sqflite (0.0.2): + - Flutter + - FMDB (>= 2.7.5) + - SwiftyGif (5.4.4) + - uni_links (0.0.1): - Flutter - url_launcher_ios (0.0.1): - Flutter + - video_player_avfoundation (0.0.1): + - Flutter - wakelock (0.0.1): - Flutter DEPENDENCIES: - - device_info (from `.symlinks/plugins/device_info/ios`) - - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - - package_info (from `.symlinks/plugins/package_info/ios`) - - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`) - - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) + - uni_links (from `.symlinks/plugins/uni_links/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) SPEC REPOS: trunk: - - Firebase - - FirebaseAnalytics - - FirebaseCore - - FirebaseCoreDiagnostics - - FirebaseInstallations - - GoogleAppMeasurement - - GoogleDataTransport - - GoogleUtilities + - DKImagePickerController + - DKPhotoGallery + - FMDB - MTBBarcodeScanner - - nanopb - - PromisesObjC + - SDWebImage + - SwiftyGif EXTERNAL SOURCES: - device_info: - :path: ".symlinks/plugins/device_info/ios" - firebase_analytics: - :path: ".symlinks/plugins/firebase_analytics/ios" - firebase_core: - :path: ".symlinks/plugins/firebase_core/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" - package_info: - :path: ".symlinks/plugins/package_info/ios" - path_provider_ios: - :path: ".symlinks/plugins/path_provider_ios/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/ios" qr_code_scanner: :path: ".symlinks/plugins/qr_code_scanner/ios" - shared_preferences_ios: - :path: ".symlinks/plugins/shared_preferences_ios/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" + uni_links: + :path: ".symlinks/plugins/uni_links/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/ios" wakelock: :path: ".symlinks/plugins/wakelock/ios" SPEC CHECKSUMS: - device_info: d7d233b645a32c40dfdc212de5cf646ca482f175 - Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d - firebase_analytics: 92d27947c7516981cabdc0acbb33cd0687bcda44 - firebase_core: aa1b92020533f5c23955e388c347c58fd64f8627 - FirebaseAnalytics: 7761cbadb00a717d8d0939363eb46041526474fa - FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1 - FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb - FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e - GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b - GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 + device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed + DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac + DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb - nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 - package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 - path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 - PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72 + package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e + path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e - shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad - url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe + sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f + uni_links: d97da20c7701486ba192624d99bffaaffcfc298a + url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2 + video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f -PODFILE CHECKSUM: a00077baecbb97321490c14848fceed3893ca92a +PODFILE CHECKSUM: c649b4e69a3086d323110011d04604e416ad0dcd -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.0 diff --git a/flutter/ios/Runner.xcodeproj/project.pbxproj b/flutter/ios/Runner.xcodeproj/project.pbxproj index 953dad47b..06ecbdd13 100644 --- a/flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/flutter/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -228,6 +228,7 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -264,6 +265,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -338,6 +340,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_ALLOWED = NO; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -351,7 +354,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -414,6 +417,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_ALLOWED = NO; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; @@ -433,7 +437,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -469,6 +473,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_ALLOWED = NO; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -482,7 +487,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/flutter/ios/Runner/AppDelegate.swift b/flutter/ios/Runner/AppDelegate.swift index 203cfff11..38ac2d8cd 100644 --- a/flutter/ios/Runner/AppDelegate.swift +++ b/flutter/ios/Runner/AppDelegate.swift @@ -13,9 +13,9 @@ import Flutter } public func dummyMethodToEnforceBundling() { - get_rgba(); - free_rgba(nil); - get_by_name("", ""); - set_by_name("", ""); +// get_rgba(); +// free_rgba(nil); +// get_by_name("", ""); +// set_by_name("", ""); } } diff --git a/flutter/ios/Runner/Info.plist b/flutter/ios/Runner/Info.plist index 3886d2456..873fe3751 100644 --- a/flutter/ios/Runner/Info.plist +++ b/flutter/ios/Runner/Info.plist @@ -49,5 +49,9 @@ This app needs camera access to scan QR codes NSPhotoLibraryUsageDescription This app needs photo library access to get QR codes from image + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/flutter/ios/Runner/ffi.h b/flutter/ios/Runner/ffi.h index 701ec4b09..755bf99fe 100644 --- a/flutter/ios/Runner/ffi.h +++ b/flutter/ios/Runner/ffi.h @@ -1,4 +1,4 @@ -void* get_rgba(); -void free_rgba(void*); -void set_by_name(const char*, const char*); -const char* get_by_name(const char*, const char*); +//void* get_rgba(); +//void free_rgba(void*); +//void set_by_name(const char*, const char*); +//const char* get_by_name(const char*, const char*); diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index f0fb00c2b..d64f2cde5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -43,6 +43,7 @@ final isIOS = Platform.isIOS; final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; var isWeb = false; var isWebDesktop = false; +var isMobile = isAndroid || isIOS; var version = ""; int androidVersion = 0; @@ -186,6 +187,71 @@ class MyTheme { static const Color button = Color(0xFF2C8CFF); static const Color hoverBorder = Color(0xFF999999); + // ListTile + static const ListTileThemeData listTileTheme = ListTileThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ); + + // Checkbox + static const CheckboxThemeData checkboxTheme = CheckboxThemeData( + splashRadius: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ); + + // TextButton + // Value is used to calculate "dialog.actionsPadding" + static const double mobileTextButtonPaddingLR = 20; + + // TextButton on mobile needs a fixed padding, otherwise small buttons + // like "OK" has a larger left/right padding. + static TextButtonThemeData mobileTextButtonTheme = TextButtonThemeData( + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric(horizontal: mobileTextButtonPaddingLR), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ); + + // Dialogs + static const double dialogPadding = 24; + + // padding bottom depends on content (some dialogs has no content) + static EdgeInsets dialogTitlePadding({bool content = true}) { + final double p = dialogPadding; + + return EdgeInsets.fromLTRB(p, p, p, content ? 0 : p); + } + + // padding bottom depends on actions (mobile has dialogs without actions) + static EdgeInsets dialogContentPadding({bool actions = true}) { + final double p = dialogPadding; + + return isDesktop + ? EdgeInsets.fromLTRB(p, p, p, actions ? (p - 4) : p) + : EdgeInsets.fromLTRB(p, p, p, actions ? (p / 2) : p); + } + + static EdgeInsets dialogActionsPadding() { + final double p = dialogPadding; + + return isDesktop + ? EdgeInsets.fromLTRB(p, 0, p, (p - 4)) + : EdgeInsets.fromLTRB(p, 0, (p - mobileTextButtonPaddingLR), (p / 2)); + } + + static EdgeInsets dialogButtonPadding = isDesktop + ? EdgeInsets.only(left: dialogPadding) + : EdgeInsets.only(left: dialogPadding / 3); + static ThemeData lightTheme = ThemeData( brightness: Brightness.light, hoverColor: Color.fromARGB(255, 224, 224, 224), @@ -236,7 +302,7 @@ class MyTheme { ), ), ) - : null, + : mobileTextButtonTheme, elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: MyTheme.accent, @@ -254,21 +320,8 @@ class MyTheme { ), ), ), - checkboxTheme: const CheckboxThemeData( - splashRadius: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(5), - ), - ), - ), - listTileTheme: ListTileThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(5), - ), - ), - ), + checkboxTheme: checkboxTheme, + listTileTheme: listTileTheme, menuBarTheme: MenuBarThemeData( style: MenuStyle(backgroundColor: MaterialStatePropertyAll(Colors.white))), @@ -334,7 +387,7 @@ class MyTheme { ), ), ) - : null, + : mobileTextButtonTheme, elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: MyTheme.accent, @@ -357,21 +410,8 @@ class MyTheme { ), ), ), - checkboxTheme: const CheckboxThemeData( - splashRadius: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(5), - ), - ), - ), - listTileTheme: ListTileThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(5), - ), - ), - ), + checkboxTheme: checkboxTheme, + listTileTheme: listTileTheme, menuBarTheme: MenuBarThemeData( style: MenuStyle( backgroundColor: MaterialStatePropertyAll(Color(0xFF121212)))), @@ -757,6 +797,7 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), child: Text( text, + textAlign: TextAlign.center, style: const TextStyle( decoration: TextDecoration.none, fontWeight: FontWeight.w300, @@ -771,6 +812,10 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { }); } +// TODO +// - Remove argument "contentPadding", no need for it, all should look the same. +// - Remove "required" for argument "content". See simple confirm dialog "delete peer", only title and actions are used. No need to "content: SizedBox.shrink()". +// - Make dead code alive, transform arguments "onSubmit" and "onCancel" into correspondenting buttons "ConfirmOkButton", "CancelButton". class CustomAlertDialog extends StatelessWidget { const CustomAlertDialog( {Key? key, @@ -798,8 +843,8 @@ class CustomAlertDialog extends StatelessWidget { Future.delayed(Duration.zero, () { if (!scopeNode.hasFocus) scopeNode.requestFocus(); }); - const double padding = 30; bool tabTapped = false; + return FocusScope( node: scopeNode, autofocus: true, @@ -824,22 +869,18 @@ class CustomAlertDialog extends StatelessWidget { return KeyEventResult.ignored; }, child: AlertDialog( - scrollable: true, - title: title, - titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0), - contentPadding: EdgeInsets.fromLTRB( - contentPadding ?? padding, - 25, - contentPadding ?? padding, - actions is List ? 10 : padding, - ), - content: ConstrainedBox( - constraints: contentBoxConstraints, - child: content, - ), - actions: actions, - actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding), - ), + scrollable: true, + title: title, + content: ConstrainedBox( + constraints: contentBoxConstraints, + child: content, + ), + actions: actions, + titlePadding: MyTheme.dialogTitlePadding(content: content != null), + contentPadding: + MyTheme.dialogContentPadding(actions: actions is List), + actionsPadding: MyTheme.dialogActionsPadding(), + buttonPadding: MyTheme.dialogButtonPadding), ); } } @@ -1115,38 +1156,23 @@ class AndroidPermissionManager { } } +// TODO move this to mobile/widgets. +// Used only for mobile, pages remote, settings, dialog +// TODO remove argument contentPadding, it’s not used, getToggle() has not RadioListTile getRadio( - String name, T toValue, T curValue, void Function(T?) onChange, + Widget title, T toValue, T curValue, ValueChanged? onChange, {EdgeInsetsGeometry? contentPadding}) { return RadioListTile( - contentPadding: contentPadding, + contentPadding: contentPadding ?? EdgeInsets.zero, + visualDensity: VisualDensity.compact, controlAffinity: ListTileControlAffinity.trailing, - title: Text(translate(name)), + title: title, value: toValue, groupValue: curValue, onChanged: onChange, - dense: true, ); } -CheckboxListTile getToggle( - String id, void Function(void Function()) setState, option, name, - {FFI? ffi}) { - final opt = bind.sessionGetToggleOptionSync(id: id, arg: option); - return CheckboxListTile( - value: opt, - onChanged: (v) { - setState(() { - bind.sessionToggleOption(id: id, value: option); - }); - if (option == "show-quality-monitor") { - (ffi ?? gFFI).qualityMonitorModel.checkShowQualityMonitor(id); - } - }, - dense: true, - title: Text(translate(name))); -} - /// find ffi, tag is Remote ID /// for session specific usage FFI ffi(String? tag) { @@ -1522,14 +1548,14 @@ bool checkArguments() { return false; } String? id = - kBootArgs.length < connectIndex + 1 ? null : kBootArgs[connectIndex + 1]; + kBootArgs.length <= connectIndex + 1 ? null : kBootArgs[connectIndex + 1]; String? password = - kBootArgs.length < connectIndex + 2 ? null : kBootArgs[connectIndex + 2]; + kBootArgs.length <= connectIndex + 2 ? null : kBootArgs[connectIndex + 2]; if (password != null && password.startsWith("--")) { password = null; } final switchUuidIndex = kBootArgs.indexOf("--switch_uuid"); - String? switchUuid = kBootArgs.length < switchUuidIndex + 1 + String? switchUuid = kBootArgs.length <= switchUuidIndex + 1 ? null : kBootArgs[switchUuidIndex + 1]; if (id != null) { @@ -1574,8 +1600,10 @@ bool callUniLinksUriHandler(Uri uri) { final peerId = uri.path.substring("/new/".length); var param = uri.queryParameters; String? switch_uuid = param["switch_uuid"]; + String? password = param["password"]; Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid); + rustDeskWinManager.newRemoteDesktop(peerId, + password: password, switch_uuid: switch_uuid); }); return true; } @@ -2007,3 +2035,12 @@ Widget futureBuilder( } }); } + +void onCopyFingerprint(String value) { + if (value.isNotEmpty) { + Clipboard.setData(ClipboardData(text: value)); + showToast('$value\n${translate("Copied")}'); + } else { + showToast(translate("no fingerprints")); + } +} diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index bc1a562b9..e4711ddf8 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -121,6 +121,29 @@ class ConnectionTypeState { Get.find(tag: tag(id)); } +class FingerprintState { + static String tag(String id) => 'fingerprint_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxString state = ''.obs; + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = ''; + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxString find(String id) => Get.find(tag: tag(id)); +} + class ShowRemoteCursorState { static String tag(String id) => 'show_remote_cursor_$id'; @@ -261,3 +284,25 @@ class PeerStringOption { static RxString find(String id, String opt) => Get.find(tag: tag(id, opt)); } + +initSharedStates(String id) { + PrivacyModeState.init(id); + BlockInputState.init(id); + CurrentDisplayState.init(id); + KeyboardEnabledState.init(id); + ShowRemoteCursorState.init(id); + RemoteCursorMovedState.init(id); + FingerprintState.init(id); + PeerBoolOption.init(id, 'zoom-cursor', () => false); +} + +removeSharedStates(String id) { + PrivacyModeState.delete(id); + BlockInputState.delete(id); + CurrentDisplayState.delete(id); + ShowRemoteCursorState.delete(id); + KeyboardEnabledState.delete(id); + RemoteCursorMovedState.delete(id); + FingerprintState.delete(id); + PeerBoolOption.delete(id, 'zoom-cursor'); +} diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index ff1a372d0..7228bb585 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -1,10 +1,20 @@ +import 'dart:async'; + +import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; import 'package:get/get.dart'; import '../../common.dart'; +import '../../models/model.dart'; import '../../models/platform_model.dart'; +void clientClose(String id, OverlayDialogManager dialogManager) { + msgBox(id, 'info', 'Close', 'Are you sure to close the connection?', '', + dialogManager); +} + abstract class ValidationRule { String get name; bool validate(String value); @@ -290,3 +300,972 @@ Future changeDirectAccessPort( }); return controller.text; } + +class DialogTextField extends StatelessWidget { + final String title; + final String? hintText; + final bool obscureText; + final String? errorText; + final String? helperText; + final Widget? prefixIcon; + final Widget? suffixIcon; + final TextEditingController controller; + final FocusNode? focusNode; + + static const kUsernameTitle = 'Username'; + static const kUsernameIcon = Icon(Icons.account_circle_outlined); + static const kPasswordTitle = 'Password'; + static const kPasswordIcon = Icon(Icons.lock_outline); + + DialogTextField( + {Key? key, + this.focusNode, + this.obscureText = false, + this.errorText, + this.helperText, + this.prefixIcon, + this.suffixIcon, + this.hintText, + required this.title, + required this.controller}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + labelText: title, + hintText: hintText, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + helperText: helperText, + helperMaxLines: 8, + errorText: errorText, + ), + controller: controller, + focusNode: focusNode, + autofocus: true, + obscureText: obscureText, + ), + ), + ], + ).paddingSymmetric(vertical: 4.0); + } +} + +class PasswordWidget extends StatefulWidget { + PasswordWidget({ + Key? key, + required this.controller, + this.autoFocus = true, + this.hintText, + this.errorText, + }) : super(key: key); + + final TextEditingController controller; + final bool autoFocus; + final String? hintText; + final String? errorText; + + @override + State createState() => _PasswordWidgetState(); +} + +class _PasswordWidgetState extends State { + bool _passwordVisible = false; + final _focusNode = FocusNode(); + Timer? _timer; + + @override + void initState() { + super.initState(); + if (widget.autoFocus) { + _timer = + Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus()); + } + } + + @override + void dispose() { + _timer?.cancel(); + _focusNode.unfocus(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DialogTextField( + title: translate(DialogTextField.kPasswordTitle), + hintText: translate(widget.hintText ?? 'Enter your password'), + controller: widget.controller, + prefixIcon: DialogTextField.kPasswordIcon, + suffixIcon: IconButton( + icon: Icon( + // Based on passwordVisible state choose the icon + _passwordVisible ? Icons.visibility : Icons.visibility_off, + color: MyTheme.lightTheme.primaryColor), + onPressed: () { + // Update the state i.e. toggle the state of passwordVisible variable + setState(() { + _passwordVisible = !_passwordVisible; + }); + }, + ), + obscureText: !_passwordVisible, + errorText: widget.errorText, + focusNode: _focusNode, + ); + } +} + +void wrongPasswordDialog( + String id, OverlayDialogManager dialogManager, type, title, text) { + dialogManager.dismissAll(); + dialogManager.show((setState, close) { + cancel() { + close(); + closeConnection(); + } + + submit() { + enterPasswordDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + onSubmit: submit, + onCancel: cancel, + actions: [ + dialogButton( + 'Cancel', + onPressed: cancel, + isOutline: true, + ), + dialogButton( + 'Retry', + onPressed: submit, + ), + ]); + }); +} + +void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { + await _connectDialog( + id, + dialogManager, + passwordController: TextEditingController(), + ); +} + +void enterUserLoginDialog(String id, OverlayDialogManager dialogManager) async { + await _connectDialog( + id, + dialogManager, + osUsernameController: TextEditingController(), + osPasswordController: TextEditingController(), + ); +} + +void enterUserLoginAndPasswordDialog( + String id, OverlayDialogManager dialogManager) async { + await _connectDialog( + id, + dialogManager, + osUsernameController: TextEditingController(), + osPasswordController: TextEditingController(), + passwordController: TextEditingController(), + ); +} + +_connectDialog( + String id, + OverlayDialogManager dialogManager, { + TextEditingController? osUsernameController, + TextEditingController? osPasswordController, + TextEditingController? passwordController, +}) async { + var rememberPassword = false; + if (passwordController != null) { + rememberPassword = await bind.sessionGetRemember(id: id) ?? false; + } + var rememberAccount = false; + if (osUsernameController != null) { + rememberAccount = await bind.sessionGetRemember(id: id) ?? false; + } + dialogManager.dismissAll(); + dialogManager.show((setState, close) { + cancel() { + close(); + closeConnection(); + } + + submit() { + final osUsername = osUsernameController?.text.trim() ?? ''; + final osPassword = osPasswordController?.text.trim() ?? ''; + final password = passwordController?.text.trim() ?? ''; + if (passwordController != null && password.isEmpty) return; + if (rememberAccount) { + bind.sessionPeerOption(id: id, name: 'os-username', value: osUsername); + bind.sessionPeerOption(id: id, name: 'os-password', value: osPassword); + } + gFFI.login( + osUsername, + osPassword, + id, + password, + rememberPassword, + ); + close(); + dialogManager.showLoading(translate('Logging in...'), + onCancel: closeConnection); + } + + descWidget(String text) { + return Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + text, + maxLines: 3, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 16), + ), + ), + Container( + height: 8, + ), + ], + ); + } + + rememberWidget( + String desc, + bool remember, + ValueChanged? onChanged, + ) { + return CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text(desc), + value: remember, + onChanged: onChanged, + ); + } + + osAccountWidget() { + if (osUsernameController == null || osPasswordController == null) { + return Offstage(); + } + return Column( + children: [ + descWidget(translate('login_linux_tip')), + DialogTextField( + title: translate(DialogTextField.kUsernameTitle), + controller: osUsernameController, + prefixIcon: DialogTextField.kUsernameIcon, + errorText: null, + ), + PasswordWidget( + controller: osPasswordController, + autoFocus: false, + ), + rememberWidget( + translate('remember_account_tip'), + rememberAccount, + (v) { + if (v != null) { + setState(() => rememberAccount = v); + } + }, + ), + ], + ); + } + + passwdWidget() { + if (passwordController == null) { + return Offstage(); + } + return Column( + children: [ + descWidget(translate('verify_rustdesk_password_tip')), + PasswordWidget( + controller: passwordController, + autoFocus: osUsernameController == null, + ), + rememberWidget( + translate('Remember password'), + rememberPassword, + (v) { + if (v != null) { + setState(() => rememberPassword = v); + } + }, + ), + ], + ); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('Password Required')).paddingOnly(left: 10), + ], + ), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + osAccountWidget(), + osUsernameController == null || passwordController == null + ? Offstage() + : Container(height: 12), + passwdWidget(), + ]), + actions: [ + dialogButton( + 'Cancel', + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + 'OK', + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: cancel, + ); + }); +} + +void showWaitUacDialog( + String id, OverlayDialogManager dialogManager, String type) { + dialogManager.dismissAll(); + dialogManager.show( + tag: '$id-wait-uac', + (setState, close) => CustomAlertDialog( + title: null, + content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'), + )); +} + +// Another username && password dialog? +void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) { + RxString groupValue = ''.obs; + RxString errUser = ''.obs; + RxString errPwd = ''.obs; + TextEditingController userController = TextEditingController(); + TextEditingController pwdController = TextEditingController(); + + void onRadioChanged(String? value) { + if (value != null) { + groupValue.value = value; + } + } + + const minTextStyle = TextStyle(fontSize: 14); + + var content = Obx(() => Column(children: [ + Row( + children: [ + Radio( + value: '', + groupValue: groupValue.value, + onChanged: onRadioChanged), + Expanded( + child: + Text(translate('Ask the remote user for authentication'))), + ], + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + translate( + 'Choose this if the remote account is administrator'), + style: TextStyle(fontSize: 13)) + .marginOnly(left: 40), + ).marginOnly(bottom: 15), + Row( + children: [ + Radio( + value: 'logon', + groupValue: groupValue.value, + onChanged: onRadioChanged), + Expanded( + child: Text(translate( + 'Transmit the username and password of administrator')), + ) + ], + ), + Row( + children: [ + Expanded( + flex: 1, + child: Text( + '${translate('Username')}:', + style: minTextStyle, + ).marginOnly(right: 10)), + Expanded( + flex: 3, + child: TextField( + controller: userController, + style: minTextStyle, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 15), + hintText: translate('eg: admin'), + errorText: errUser.isEmpty ? null : errUser.value), + onChanged: (s) { + if (s.isNotEmpty) { + errUser.value = ''; + } + }, + ), + ) + ], + ).marginOnly(left: 40), + Row( + children: [ + Expanded( + flex: 1, + child: Text( + '${translate('Password')}:', + style: minTextStyle, + ).marginOnly(right: 10)), + Expanded( + flex: 3, + child: TextField( + controller: pwdController, + obscureText: true, + style: minTextStyle, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 15), + errorText: errPwd.isEmpty ? null : errPwd.value), + onChanged: (s) { + if (s.isNotEmpty) { + errPwd.value = ''; + } + }, + ), + ), + ], + ).marginOnly(left: 40), + Align( + alignment: Alignment.centerLeft, + child: Text(translate('still_click_uac_tip'), + style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)) + .marginOnly(top: 20)), + ])); + + dialogManager.dismissAll(); + dialogManager.show(tag: '$id-request-elevation', (setState, close) { + void submit() { + if (groupValue.value == 'logon') { + if (userController.text.isEmpty) { + errUser.value = translate('Empty Username'); + return; + } + if (pwdController.text.isEmpty) { + errPwd.value = translate('Empty Password'); + return; + } + bind.sessionElevateWithLogon( + id: id, + username: userController.text, + password: pwdController.text); + } else { + bind.sessionElevateDirect(id: id); + } + } + + return CustomAlertDialog( + title: Text(translate('Request Elevation')), + content: content, + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showOnBlockDialog( + String id, + String type, + String title, + String text, + OverlayDialogManager dialogManager, +) { + if (dialogManager.existing('$id-wait-uac') || + dialogManager.existing('$id-request-elevation')) { + return; + } + dialogManager.show(tag: '$id-$type', (setState, close) { + void submit() { + close(); + showRequestElevationDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, + "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}"), + actions: [ + dialogButton('Wait', onPressed: close, isOutline: true), + dialogButton('Request Elevation', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showElevationError(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.show(tag: '$id-$type', (setState, close) { + void submit() { + close(); + showRequestElevationDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + actions: [ + dialogButton('Cancel', onPressed: () { + close(); + }, isOutline: true), + dialogButton('Retry', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showWaitAcceptDialog(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.dismissAll(); + dialogManager.show((setState, close) { + onCancel() { + closeConnection(); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + actions: [ + dialogButton('Cancel', onPressed: onCancel, isOutline: true), + ], + onCancel: onCancel, + ); + }); +} + +void showRestartRemoteDevice( + PeerInfo pi, String id, OverlayDialogManager dialogManager) async { + final res = + await dialogManager.show((setState, close) => CustomAlertDialog( + title: Row(children: [ + Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28), + Flexible( + child: Text(translate("Restart Remote Device")) + .paddingOnly(left: 10)), + ]), + content: Text( + "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: () => close(true), + ), + ], + onCancel: close, + onSubmit: () => close(true), + )); + if (res == true) bind.sessionRestartRemoteDevice(id: id); +} + +showSetOSPassword( + String id, + bool login, + OverlayDialogManager dialogManager, +) async { + final controller = TextEditingController(); + var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; + var autoLogin = await bind.sessionGetOption(id: id, arg: 'auto-login') != ''; + controller.text = password; + dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + bind.sessionPeerOption(id: id, name: 'os-password', value: text); + bind.sessionPeerOption( + id: id, name: 'auto-login', value: autoLogin ? 'Y' : ''); + if (text != '' && login) { + bind.sessionInputOsPassword(id: id, value: text); + } + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('OS Password')).paddingOnly(left: 10), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), + ), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +showSetOSAccount( + String id, + OverlayDialogManager dialogManager, +) async { + final usernameController = TextEditingController(); + final passwdController = TextEditingController(); + var username = await bind.sessionGetOption(id: id, arg: 'os-username') ?? ''; + var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; + usernameController.text = username; + passwdController.text = password; + dialogManager.show((setState, close) { + submit() { + final username = usernameController.text.trim(); + final password = usernameController.text.trim(); + bind.sessionPeerOption(id: id, name: 'os-username', value: username); + bind.sessionPeerOption(id: id, name: 'os-password', value: password); + close(); + } + + descWidget(String text) { + return Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + text, + maxLines: 3, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 16), + ), + ), + Container( + height: 8, + ), + ], + ); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('OS Account')).paddingOnly(left: 10), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + descWidget(translate("os_account_desk_tip")), + DialogTextField( + title: translate(DialogTextField.kUsernameTitle), + controller: usernameController, + prefixIcon: DialogTextField.kUsernameIcon, + errorText: null, + ), + PasswordWidget(controller: passwdController), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +showAuditDialog(String id, dialogManager) async { + final controller = TextEditingController(); + dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + if (text != '') { + bind.sessionSendNote(id: id, note: text); + } + close(); + } + + late final focusNode = FocusNode( + onKey: (FocusNode node, RawKeyEvent evt) { + if (evt.logicalKey.keyLabel == 'Enter') { + if (evt is RawKeyDownEvent) { + int pos = controller.selection.base.offset; + controller.text = + '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; + controller.selection = + TextSelection.fromPosition(TextPosition(offset: pos + 1)); + } + return KeyEventResult.handled; + } + if (evt.logicalKey.keyLabel == 'Esc') { + if (evt is RawKeyDownEvent) { + close(); + } + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + }, + ); + + return CustomAlertDialog( + title: Text(translate('Note')), + content: SizedBox( + width: 250, + height: 120, + child: TextField( + autofocus: true, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + decoration: const InputDecoration.collapsed( + hintText: 'input note here', + ), + maxLines: null, + maxLength: 256, + controller: controller, + focusNode: focusNode, + )), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit) + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showConfirmSwitchSidesDialog( + String id, OverlayDialogManager dialogManager) async { + dialogManager.show((setState, close) { + submit() async { + await bind.sessionSwitchSides(id: id); + closeConnection(id: id); + } + + return CustomAlertDialog( + content: msgboxContent('info', 'Switch Sides', + 'Please confirm if you want to share your desktop?'), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +customImageQualityDialog(String id, FFI ffi) async { + double qualityInitValue = 50; + double fpsInitValue = 30; + bool qualitySet = false; + bool fpsSet = false; + setCustomValues({double? quality, double? fps}) async { + if (quality != null) { + qualitySet = true; + await bind.sessionSetCustomImageQuality(id: id, value: quality.toInt()); + } + if (fps != null) { + fpsSet = true; + await bind.sessionSetCustomFps(id: id, fps: fps.toInt()); + } + if (!qualitySet) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: id, value: qualityInitValue.toInt()); + } + if (!fpsSet) { + fpsSet = true; + await bind.sessionSetCustomFps(id: id, fps: fpsInitValue.toInt()); + } + } + + final btnClose = dialogButton('Close', onPressed: () async { + await setCustomValues(); + ffi.dialogManager.dismissAll(); + }); + + // quality + final quality = await bind.sessionGetCustomImageQuality(id: id); + qualityInitValue = + quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; + const qualityMinValue = 10.0; + const qualityMaxValue = 100.0; + if (qualityInitValue < qualityMinValue) { + qualityInitValue = qualityMinValue; + } + if (qualityInitValue > qualityMaxValue) { + qualityInitValue = qualityMaxValue; + } + final RxDouble qualitySliderValue = RxDouble(qualityInitValue); + final debouncerQuality = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(quality: v); + }, + initialValue: qualityInitValue, + ); + final qualitySlider = Obx(() => Row( + children: [ + Expanded( + flex: 3, + child: Slider( + value: qualitySliderValue.value, + min: qualityMinValue, + max: qualityMaxValue, + divisions: 18, + onChanged: (double value) { + qualitySliderValue.value = value; + debouncerQuality.value = value; + }, + )), + Expanded( + flex: 1, + child: Text( + '${qualitySliderValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + Expanded( + flex: 2, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )), + ], + )); + // fps + final fpsOption = await bind.sessionGetOption(id: id, arg: 'custom-fps'); + fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; + if (fpsInitValue < 5 || fpsInitValue > 120) { + fpsInitValue = 30; + } + final RxDouble fpsSliderValue = RxDouble(fpsInitValue); + final debouncerFps = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(fps: v); + }, + initialValue: qualityInitValue, + ); + bool? direct; + try { + direct = + ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect; + } catch (_) {} + final fpsSlider = Offstage( + offstage: (await bind.mainIsUsingPublicServer() && direct != true) || + version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0, + child: Row( + children: [ + Expanded( + flex: 3, + child: Obx((() => Slider( + value: fpsSliderValue.value, + min: 5, + max: 120, + divisions: 23, + onChanged: (double value) { + fpsSliderValue.value = value; + debouncerFps.value = value; + }, + )))), + Expanded( + flex: 1, + child: Obx(() => Text( + '${fpsSliderValue.value.round()}', + style: const TextStyle(fontSize: 15), + ))), + Expanded( + flex: 2, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) + ], + ), + ); + + final content = Column( + children: [qualitySlider, fpsSlider], + ); + msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); +} diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 7b37c590d..3959c301c 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -9,6 +9,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; +import './dialog.dart'; class _IconOP extends StatelessWidget { final String icon; @@ -324,17 +325,16 @@ class LoginWidgetUserPass extends StatelessWidget { children: [ const SizedBox(height: 8.0), DialogTextField( - title: translate("Username"), + title: translate(DialogTextField.kUsernameTitle), controller: username, focusNode: userFocusNode, - prefixIcon: Icon(Icons.account_circle_outlined), + prefixIcon: DialogTextField.kUsernameIcon, errorText: usernameMsg), - DialogTextField( - title: translate("Password"), - obscureText: true, - controller: pass, - prefixIcon: Icon(Icons.lock_outline), - errorText: passMsg), + PasswordWidget( + controller: pass, + autoFocus: false, + errorText: passMsg, + ), Obx(() => CheckboxListTile( contentPadding: const EdgeInsets.all(0), dense: true, @@ -377,49 +377,6 @@ class LoginWidgetUserPass extends StatelessWidget { } } -class DialogTextField extends StatelessWidget { - final String title; - final bool obscureText; - final String? errorText; - final String? helperText; - final Widget? prefixIcon; - final TextEditingController controller; - final FocusNode? focusNode; - - DialogTextField( - {Key? key, - this.focusNode, - this.obscureText = false, - this.errorText, - this.helperText, - this.prefixIcon, - required this.title, - required this.controller}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: TextField( - decoration: InputDecoration( - labelText: title, - prefixIcon: prefixIcon, - helperText: helperText, - helperMaxLines: 8, - errorText: errorText), - controller: controller, - focusNode: focusNode, - autofocus: true, - obscureText: obscureText, - ), - ), - ], - ).paddingSymmetric(vertical: 4.0); - } -} - /// common login dialog for desktop /// call this directly Future loginDialog() async { diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index c67f0f7fb..8c44e7ee4 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -388,6 +388,15 @@ class BlockableOverlayState extends OverlayKeyState { _middleBlocked.value = blocked; } } + + void applyFfi(FFI ffi) { + ffi.dialogManager.setOverlayState(this); + ffi.chatModel.setOverlayState(this); + // make remote page penetrable automatically, effective for chat over remote + onMiddleBlockedClick = () { + setMiddleBlocked(false); + }; + } } class BlockableOverlay extends StatelessWidget { diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 2d36d9150..fa9ed29c0 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -261,7 +261,7 @@ class _PeerTabPageState extends State }, child: Icon( peerCardUiType.value == PeerUiType.grid - ? Icons.list_rounded + ? Icons.list : Icons.grid_view_rounded, size: 18, color: textColor, @@ -455,12 +455,12 @@ class _PeerSortDropdownState extends State { borderRadius: BorderRadius.circular(5), ); - final translated_text = - PeerSortType.values.map((e) => translate(e)).toList(); + final translated_text = { + for (var e in PeerSortType.values) e: translate(e) + }; final double max_width = - 50 + translated_text.map((e) => e.length).reduce(max) * 10; - + 50 + translated_text.values.map((e) => e.length).reduce(max) * 10; return Container( padding: EdgeInsets.all(4.0), decoration: deco, @@ -496,20 +496,20 @@ class _PeerSortDropdownState extends State { ), enabled: false, ), - ...translated_text + ...translated_text.entries .map>( - (String value) => DropdownMenuItem( - value: value, + (MapEntry entry) => DropdownMenuItem( + value: entry.key, child: Row( children: [ Icon( - value == peerSort.value + entry.key == peerSort.value ? Icons.radio_button_checked_rounded : Icons.radio_button_off_rounded, size: 18, ).paddingOnly(right: 12), Text( - value, + entry.value, overflow: TextOverflow.ellipsis, ), ], diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart new file mode 100644 index 000000000..451c7d1a5 --- /dev/null +++ b/flutter/lib/common/widgets/toolbar.dart @@ -0,0 +1,456 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; + +class TTextMenu { + final Widget child; + final VoidCallback onPressed; + Widget? trailingIcon; + bool divider; + TTextMenu( + {required this.child, + required this.onPressed, + this.trailingIcon, + this.divider = false}); +} + +class TRadioMenu { + final Widget child; + final T value; + final T groupValue; + final ValueChanged? onChanged; + + TRadioMenu( + {required this.child, + required this.value, + required this.groupValue, + required this.onChanged}); +} + +class TToggleMenu { + final Widget child; + final bool value; + final ValueChanged? onChanged; + TToggleMenu( + {required this.child, required this.value, required this.onChanged}); +} + +List toolbarControls(BuildContext context, String id, FFI ffi) { + final ffiModel = ffi.ffiModel; + final pi = ffiModel.pi; + final perms = ffiModel.permissions; + + List v = []; + // elevation + if (ffi.elevationModel.showRequestMenu) { + v.add( + TTextMenu( + child: Text(translate('Request Elevation')), + onPressed: () => showRequestElevationDialog(id, ffi.dialogManager)), + ); + } + // osAccount / osPassword + v.add( + TTextMenu( + child: Row(children: [ + Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')), + Offstage( + offstage: isDesktop, + child: + Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12)) + ]), + trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)), + onPressed: () => pi.is_headless + ? showSetOSAccount(id, ffi.dialogManager) + : showSetOSPassword(id, false, ffi.dialogManager)), + ); + // paste + if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) { + v.add(TTextMenu( + child: Text(translate('Paste')), + onPressed: () async { + ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + bind.sessionInputString(id: id, value: data.text ?? ""); + } + })); + } + // reset canvas + if (isMobile) { + v.add(TTextMenu( + child: Text(translate('Reset canvas')), + onPressed: () => ffi.cursorModel.reset())); + } + // transferFile + if (isDesktop) { + v.add( + TTextMenu( + child: Text(translate('Transfer File')), + onPressed: () => connect(context, id, isFileTransfer: true)), + ); + } + // tcpTunneling + if (isDesktop) { + v.add( + TTextMenu( + child: Text(translate('TCP Tunneling')), + onPressed: () => connect(context, id, isTcpTunneling: true)), + ); + } + // note + if (bind.sessionGetAuditServerSync(id: id, typ: "conn").isNotEmpty) { + v.add( + TTextMenu( + child: Text(translate('Note')), + onPressed: () => showAuditDialog(id, ffi.dialogManager)), + ); + } + // divider + if (isDesktop) { + v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true)); + } + // ctrlAltDel + if (!ffiModel.viewOnly && + ffiModel.keyboard && + (pi.platform == kPeerPlatformLinux || pi.sasEnabled)) { + v.add( + TTextMenu( + child: Text('${translate("Insert")} Ctrl + Alt + Del'), + onPressed: () => bind.sessionCtrlAltDel(id: id)), + ); + } + // restart + if (perms['restart'] != false && + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS)) { + v.add( + TTextMenu( + child: Text(translate('Restart Remote Device')), + onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager)), + ); + } + // insertLock + if (!ffiModel.viewOnly && ffi.ffiModel.keyboard) { + v.add( + TTextMenu( + child: Text(translate('Insert Lock')), + onPressed: () => bind.sessionLockScreen(id: id)), + ); + } + // blockUserInput + if (ffi.ffiModel.keyboard && + pi.platform == kPeerPlatformWindows) // privacy-mode != true ?? + { + v.add(TTextMenu( + child: Obx(() => Text(translate( + '${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))), + onPressed: () { + RxBool blockInput = BlockInputState.find(id); + bind.sessionToggleOption( + id: id, value: '${blockInput.value ? 'un' : ''}block-input'); + blockInput.value = !blockInput.value; + })); + } + // switchSides + if (isDesktop && + ffiModel.keyboard && + pi.platform != kPeerPlatformAndroid && + pi.platform != kPeerPlatformMacOS && + version_cmp(pi.version, '1.2.0') >= 0) { + v.add(TTextMenu( + child: Text(translate('Switch Sides')), + onPressed: () => showConfirmSwitchSidesDialog(id, ffi.dialogManager))); + } + // refresh + if (pi.version.isNotEmpty) { + v.add(TTextMenu( + child: Text(translate('Refresh')), + onPressed: () => bind.sessionRefresh(id: id))); + } + // record + var codecFormat = ffi.qualityMonitorModel.data.codecFormat; + if (!isDesktop && + (ffi.recordingModel.start || + (perms["recording"] != false && + (codecFormat == "VP8" || codecFormat == "VP9")))) { + v.add(TTextMenu( + child: Row( + children: [ + Text(translate(ffi.recordingModel.start + ? 'Stop session recording' + : 'Start session recording')), + Padding( + padding: EdgeInsets.only(left: 12), + child: Icon( + ffi.recordingModel.start + ? Icons.pause_circle_filled + : Icons.videocam_outlined, + color: MyTheme.accent), + ) + ], + ), + onPressed: () => ffi.recordingModel.toggle())); + } + // fingerprint + if (!isDesktop) { + v.add(TTextMenu( + child: Text(translate('Copy Fingerprint')), + onPressed: () => onCopyFingerprint(FingerprintState.find(id).value), + )); + } + return v; +} + +Future>> toolbarViewStyle( + BuildContext context, String id, FFI ffi) async { + final groupValue = await bind.sessionGetViewStyle(id: id) ?? ''; + void onChanged(String? value) async { + if (value == null) return; + bind + .sessionSetViewStyle(id: id, value: value) + .then((_) => ffi.canvasModel.updateViewStyle()); + } + + return [ + TRadioMenu( + child: Text(translate('Scale original')), + value: kRemoteViewStyleOriginal, + groupValue: groupValue, + onChanged: onChanged), + TRadioMenu( + child: Text(translate('Scale adaptive')), + value: kRemoteViewStyleAdaptive, + groupValue: groupValue, + onChanged: onChanged) + ]; +} + +Future>> toolbarImageQuality( + BuildContext context, String id, FFI ffi) async { + final groupValue = await bind.sessionGetImageQuality(id: id) ?? ''; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetImageQuality(id: id, value: value); + } + + return [ + TRadioMenu( + child: Text(translate('Good image quality')), + value: kRemoteImageQualityBest, + groupValue: groupValue, + onChanged: onChanged), + TRadioMenu( + child: Text(translate('Balanced')), + value: kRemoteImageQualityBalanced, + groupValue: groupValue, + onChanged: onChanged), + TRadioMenu( + child: Text(translate('Optimize reaction time')), + value: kRemoteImageQualityLow, + groupValue: groupValue, + onChanged: onChanged), + TRadioMenu( + child: Text(translate('Custom')), + value: kRemoteImageQualityCustom, + groupValue: groupValue, + onChanged: (value) { + onChanged(value); + customImageQualityDialog(id, ffi); + }, + ), + ]; +} + +Future>> toolbarCodec( + BuildContext context, String id, FFI ffi) async { + final alternativeCodecs = await bind.sessionAlternativeCodecs(id: id); + final groupValue = + await bind.sessionGetOption(id: id, arg: 'codec-preference') ?? ''; + final List codecs = []; + try { + final Map codecsJson = jsonDecode(alternativeCodecs); + final vp8 = codecsJson['vp8'] ?? false; + final h264 = codecsJson['h264'] ?? false; + final h265 = codecsJson['h265'] ?? false; + codecs.add(vp8); + codecs.add(h264); + codecs.add(h265); + } catch (e) { + debugPrint("Show Codec Preference err=$e"); + } + final visible = codecs.length == 3 && (codecs[0] || codecs[1] || codecs[2]); + if (!visible) return []; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionPeerOption( + id: id, name: 'codec-preference', value: value); + bind.sessionChangePreferCodec(id: id); + } + + TRadioMenu radio(String label, String value, bool enabled) { + return TRadioMenu( + child: Text(translate(label)), + value: value, + groupValue: groupValue, + onChanged: enabled ? onChanged : null); + } + + return [ + radio('Auto', 'auto', true), + if (isDesktop || codecs[0]) radio('VP8', 'vp8', codecs[0]), + radio('VP9', 'vp9', true), + if (isDesktop || codecs[1]) radio('H264', 'h264', codecs[1]), + if (isDesktop || codecs[2]) radio('H265', 'h265', codecs[2]), + ]; +} + +Future> toolbarDisplayToggle( + BuildContext context, String id, FFI ffi) async { + List v = []; + final ffiModel = ffi.ffiModel; + final pi = ffiModel.pi; + final perms = ffiModel.permissions; + + // show remote cursor + if (pi.platform != kPeerPlatformAndroid && + !ffi.canvasModel.cursorEmbedded && + !pi.is_wayland) { + final state = ShowRemoteCursorState.find(id); + final enabled = !ffiModel.viewOnly; + final option = 'show-remote-cursor'; + v.add(TToggleMenu( + child: Text(translate('Show remote cursor')), + value: state.value, + onChanged: enabled + ? (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: id, value: option); + state.value = + bind.sessionGetToggleOptionSync(id: id, arg: option); + } + : null)); + } + // zoom cursor + final viewStyle = await bind.sessionGetViewStyle(id: id) ?? ''; + if (!isMobile && + pi.platform != kPeerPlatformAndroid && + viewStyle != kRemoteViewStyleOriginal) { + final option = 'zoom-cursor'; + final peerState = PeerBoolOption.find(id, option); + v.add(TToggleMenu( + child: Text(translate('Zoom cursor')), + value: peerState.value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: id, value: option); + peerState.value = bind.sessionGetToggleOptionSync(id: id, arg: option); + }, + )); + } + // show quality monitor + final option = 'show-quality-monitor'; + v.add(TToggleMenu( + value: bind.sessionGetToggleOptionSync(id: id, arg: option), + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: id, value: option); + ffi.qualityMonitorModel.checkShowQualityMonitor(id); + }, + child: Text(translate('Show quality monitor')))); + // mute + if (perms['audio'] != false) { + final option = 'disable-audio'; + final value = bind.sessionGetToggleOptionSync(id: id, arg: option); + v.add(TToggleMenu( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: id, value: option); + }, + child: Text(translate('Mute')))); + } + // file copy and paste + if (Platform.isWindows && + pi.platform == kPeerPlatformWindows && + perms['file'] != false) { + final option = 'enable-file-transfer'; + final value = bind.sessionGetToggleOptionSync(id: id, arg: option); + v.add(TToggleMenu( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: id, value: option); + }, + child: Text(translate('Allow file copy and paste')))); + } + // disable clipboard + if (ffiModel.keyboard && perms['clipboard'] != false) { + final enabled = !ffiModel.viewOnly; + final option = 'disable-clipboard'; + var value = bind.sessionGetToggleOptionSync(id: id, arg: option); + if (ffiModel.viewOnly) value = true; + v.add(TToggleMenu( + value: value, + onChanged: enabled + ? (value) { + if (value == null) return; + bind.sessionToggleOption(id: id, value: option); + } + : null, + child: Text(translate('Disable clipboard')))); + } + // lock after session end + if (ffiModel.keyboard) { + final option = 'lock-after-session-end'; + final value = bind.sessionGetToggleOptionSync(id: id, arg: option); + v.add(TToggleMenu( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: id, value: option); + }, + child: Text(translate('Lock after session end')))); + } + // privacy mode + if (ffiModel.keyboard && pi.features.privacyMode) { + final option = 'privacy-mode'; + final rxValue = PrivacyModeState.find(id); + v.add(TToggleMenu( + value: rxValue.value, + onChanged: (value) { + if (value == null) return; + if (ffiModel.pi.currentDisplay != 0) { + msgBox(id, 'custom-nook-nocancel-hasclose', 'info', + 'Please switch to Display 1 first', '', ffi.dialogManager); + return; + } + bind.sessionToggleOption(id: id, value: option); + }, + child: Text(translate('Privacy mode')))); + } + // swap key + if (ffiModel.keyboard && + ((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) || + (!Platform.isMacOS && pi.platform == kPeerPlatformMacOS))) { + final option = 'allow_swap_key'; + final value = bind.sessionGetToggleOptionSync(id: id, arg: option); + v.add(TToggleMenu( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: id, value: option); + }, + child: Text(translate('Swap control-command key')))); + } + return v; +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index b6debbd8d..3f6bb7d16 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -19,6 +19,8 @@ import '../../common/widgets/peer_tab_page.dart'; import '../../models/platform_model.dart'; import '../widgets/button.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; + /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget { const ConnectionPage({Key? key}) : super(key: key); @@ -223,9 +225,7 @@ class _ConnectionPageState extends State children: [ Button( isOutline: true, - onTap: () { - onConnect(isFileTransfer: true); - }, + onTap: () => onConnect(isFileTransfer: true), text: "Transfer File", ), const SizedBox( diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 66ef83d31..c083421fd 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -301,7 +301,7 @@ class _GeneralState extends State<_General> { Widget audio(BuildContext context) { String getDefault() { - if (Platform.isWindows) return 'System Sound'; + if (Platform.isWindows) return translate('System Sound'); return ''; } @@ -322,7 +322,7 @@ class _GeneralState extends State<_General> { return futureBuilder(future: () async { List devices = (await bind.mainGetSoundInputs()).toList(); if (Platform.isWindows) { - devices.insert(0, 'System Sound'); + devices.insert(0, translate('System Sound')); } String current = await getValue(); return {'devices': devices, 'current': current}; @@ -415,7 +415,7 @@ class _GeneralState extends State<_General> { List keys = langsMap.keys.toList(); List values = langsMap.values.toList(); keys.insert(0, ''); - values.insert(0, 'Default'); + values.insert(0, translate('Default')); String currentKey = data['lang']!; if (!keys.contains(currentKey)) { currentKey = ''; @@ -1228,9 +1228,9 @@ class _DisplayState extends State<_Display> { children: [ Slider( value: fpsValue.value, - min: 10.0, + min: 5.0, max: 120.0, - divisions: 22, + divisions: 23, onChanged: (double value) async { fpsValue.value = value; await bind.mainSetUserDefaultOption( @@ -1258,9 +1258,6 @@ class _DisplayState extends State<_Display> { } Widget codec(BuildContext context) { - if (!bind.mainHasHwcodec()) { - return Offstage(); - } final key = 'codec-preference'; onChanged(String value) async { await bind.mainSetUserDefaultOption(key: key, value: value); @@ -1268,28 +1265,45 @@ class _DisplayState extends State<_Display> { } final groupValue = bind.mainGetUserDefaultOption(key: key); - + var hwRadios = []; + try { + final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings()); + final h264 = codecsJson['h264'] ?? false; + final h265 = codecsJson['h265'] ?? false; + if (h264) { + hwRadios.add(_Radio(context, + value: 'h264', + groupValue: groupValue, + label: 'H264', + onChanged: onChanged)); + } + if (h265) { + hwRadios.add(_Radio(context, + value: 'h265', + groupValue: groupValue, + label: 'H265', + onChanged: onChanged)); + } + } catch (e) { + debugPrint("failed to parse supported hwdecodings, err=$e"); + } return _Card(title: 'Default Codec', children: [ _Radio(context, value: 'auto', groupValue: groupValue, label: 'Auto', onChanged: onChanged), + _Radio(context, + value: 'vp8', + groupValue: groupValue, + label: 'VP8', + onChanged: onChanged), _Radio(context, value: 'vp9', groupValue: groupValue, label: 'VP9', onChanged: onChanged), - _Radio(context, - value: 'h264', - groupValue: groupValue, - label: 'H264', - onChanged: onChanged), - _Radio(context, - value: 'h265', - groupValue: groupValue, - label: 'H265', - onChanged: onChanged), + ...hwRadios, ]); } @@ -1376,11 +1390,18 @@ class _AboutState extends State<_About> { final license = await bind.mainGetLicense(); final version = await bind.mainGetVersion(); final buildDate = await bind.mainGetBuildDate(); - return {'license': license, 'version': version, 'buildDate': buildDate}; + final fingerprint = await bind.mainGetFingerprint(); + return { + 'license': license, + 'version': version, + 'buildDate': buildDate, + 'fingerprint': fingerprint + }; }(), hasData: (data) { final license = data['license'].toString(); final version = data['version'].toString(); final buildDate = data['buildDate'].toString(); + final fingerprint = data['fingerprint'].toString(); const linkStyle = TextStyle(decoration: TextDecoration.underline); final scrollController = ScrollController(); return DesktopScrollWrapper( @@ -1401,6 +1422,9 @@ class _AboutState extends State<_About> { SelectionArea( child: Text('${translate('Build Date')}: $buildDate') .marginSymmetric(vertical: 4.0)), + SelectionArea( + child: Text('${translate('Fingerprint')}: $fingerprint') + .marginSymmetric(vertical: 4.0)), InkWell( onTap: () { launchUrlString('https://rustdesk.com/privacy'); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 4a1a40242..a642e2590 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -23,7 +23,7 @@ class DesktopTabPage extends StatefulWidget { DesktopTabController tabController = Get.find(); tabController.add(TabInfo( key: kTabLabelSettingPage, - label: translate(kTabLabelSettingPage), + label: kTabLabelSettingPage, selectedIcon: Icons.build_sharp, unselectedIcon: Icons.build_outlined, page: DesktopSettingPage( @@ -46,7 +46,7 @@ class _DesktopTabPageState extends State { RemoteCountState.init(); tabController.add(TabInfo( key: kTabLabelHomePage, - label: translate(kTabLabelHomePage), + label: kTabLabelHomePage, selectedIcon: Icons.home_sharp, unselectedIcon: Icons.home_outlined, closable: false, diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 602dd5171..5588c577f 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -802,7 +802,7 @@ class _FileManagerViewState extends State { switchType: SwitchType.scheckbox, text: translate("Show Hidden Files"), getter: () async { - return controller.options.value.isWindows; + return controller.options.value.showHidden; }, setter: (bool v) async { controller.toggleShowHidden(); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index aea073ace..bd026874b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -17,7 +17,7 @@ import '../../consts.dart'; import '../../common/widgets/overlay.dart'; import '../../common/widgets/remote_input.dart'; import '../../common.dart'; -import '../../mobile/widgets/dialog.dart'; +import '../../common/widgets/dialog.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; @@ -77,15 +77,8 @@ class _RemotePageState extends State late FFI _ffi; void _initStates(String id) { - PrivacyModeState.init(id); - BlockInputState.init(id); - CurrentDisplayState.init(id); - KeyboardEnabledState.init(id); - ShowRemoteCursorState.init(id); - RemoteCursorMovedState.init(id); - final optZoomCursor = 'zoom-cursor'; - PeerBoolOption.init(id, optZoomCursor, () => false); - _zoomCursor = PeerBoolOption.find(id, optZoomCursor); + initSharedStates(id); + _zoomCursor = PeerBoolOption.find(id, 'zoom-cursor'); _showRemoteCursor = ShowRemoteCursorState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id); _remoteCursorMoved = RemoteCursorMovedState.find(id); @@ -93,15 +86,6 @@ class _RemotePageState extends State _textureId = RxInt(-1); } - void _removeStates(String id) { - PrivacyModeState.delete(id); - BlockInputState.delete(id); - CurrentDisplayState.delete(id); - ShowRemoteCursorState.delete(id); - KeyboardEnabledState.delete(id); - RemoteCursorMovedState.delete(id); - } - @override void initState() { super.initState(); @@ -158,12 +142,7 @@ class _RemotePageState extends State // _isCustomCursorInited = true; // } - _ffi.dialogManager.setOverlayState(_blockableOverlayState); - _ffi.chatModel.setOverlayState(_blockableOverlayState); - // make remote page penetrable automatically, effective for chat over remote - _blockableOverlayState.onMiddleBlockedClick = () { - _blockableOverlayState.setMiddleBlocked(false); - }; + _blockableOverlayState.applyFfi(_ffi); } @override @@ -222,7 +201,7 @@ class _RemotePageState extends State } Get.delete(tag: widget.id); super.dispose(); - _removeStates(widget.id); + removeSharedStates(widget.id); } Widget buildBody(BuildContext context) { @@ -310,7 +289,7 @@ class _RemotePageState extends State } void leaveView(PointerExitEvent evt) { - if (_ffi.ffiModel.keyboard()) { + if (_ffi.ffiModel.keyboard) { _ffi.inputModel.tryMoveEdgeOnExit(evt.position); } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index f7bb85a37..005c74522 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -4,6 +4,7 @@ import 'dart:ui' as ui; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/consts.dart'; @@ -158,20 +159,36 @@ class _ConnectionTabPageState extends State { ], ); } else { - final msgDirect = translate( - connectionType.direct.value == ConnectionType.strDirect - ? 'Direct Connection' - : 'Relay Connection'); - final msgSecure = translate( - connectionType.secure.value == ConnectionType.strSecure - ? 'Secure Connection' - : 'Insecure Connection'); + bool secure = + connectionType.secure.value == ConnectionType.strSecure; + bool direct = + connectionType.direct.value == ConnectionType.strDirect; + var msgConn; + if (secure && direct) { + msgConn = translate("Direct and encrypted connection"); + } else if (secure && !direct) { + msgConn = translate("Relayed and encrypted connection"); + } else if (!secure && direct) { + msgConn = translate("Direct and unencrypted connection"); + } else { + msgConn = translate("Relayed and unencrypted connection"); + } + var msgFingerprint = '${translate('Fingerprint')}:\n'; + var fingerprint = FingerprintState.find(key).value; + if (fingerprint.length > 5 * 8) { + var first = fingerprint.substring(0, 39); + var second = fingerprint.substring(40); + msgFingerprint += '$first\n$second'; + } else { + msgFingerprint += fingerprint; + } + final tab = Row( mainAxisAlignment: MainAxisAlignment.center, children: [ icon, Tooltip( - message: '$msgDirect\n$msgSecure', + message: '$msgConn\n$msgFingerprint', child: SvgPicture.asset( 'assets/${connectionType.secure.value}${connectionType.direct.value}.svg', width: themeConf.iconSize, @@ -259,7 +276,9 @@ class _ConnectionTabPageState extends State { ), ]); - if (!ffi.canvasModel.cursorEmbedded && !ffi.ffiModel.viewOnly) { + if (!ffi.canvasModel.cursorEmbedded && + !ffi.ffiModel.viewOnly && + !pi.is_wayland) { menu.add(MenuEntryDivider()); menu.add(RemoteMenuEntry.showRemoteCursor( key, @@ -283,6 +302,17 @@ class _ConnectionTabPageState extends State { } } + menu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Copy Fingerprint'), + style: style, + ), + proc: () => onCopyFingerprint(FingerprintState.find(key).value), + padding: padding, + dismissOnClicked: true, + dismissCallback: cancelFunc, + )); + return mod_menu.PopupMenu( items: menu .map((entry) => entry.build( diff --git a/flutter/lib/desktop/plugin/common.dart b/flutter/lib/desktop/plugin/common.dart new file mode 100644 index 000000000..b1b3dbfa0 --- /dev/null +++ b/flutter/lib/desktop/plugin/common.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +typedef PluginId = String; + +// ui location +const String kLocationHostMainDisplayOthers = + 'host|main|settings|display|others'; +const String kLocationClientRemoteToolbarDisplay = + 'client|remote|toolbar|display'; + +class MsgFromUi { + String remotePeerId; + String localPeerId; + String id; + String name; + String location; + String key; + String value; + String action; + + MsgFromUi({ + required this.remotePeerId, + required this.localPeerId, + required this.id, + required this.name, + required this.location, + required this.key, + required this.value, + required this.action, + }); + + Map toJson() { + return { + 'remote_peer_id': remotePeerId, + 'local_peer_id': localPeerId, + 'id': id, + 'name': name, + 'location': location, + 'key': key, + 'value': value, + 'action': action, + }; + } + + @override + String toString() { + return jsonEncode(toJson()); + } +} diff --git a/flutter/lib/desktop/plugin/desc.dart b/flutter/lib/desktop/plugin/desc.dart new file mode 100644 index 000000000..8c0de4625 --- /dev/null +++ b/flutter/lib/desktop/plugin/desc.dart @@ -0,0 +1,166 @@ +import 'dart:collection'; + +const String kValueTrue = '1'; +const String kValueFalse = '0'; + +class UiType { + String key; + String text; + String tooltip; + String action; + + UiType(this.key, this.text, this.tooltip, this.action); + + UiType.fromJson(Map json) + : key = json['key'] ?? '', + text = json['text'] ?? '', + tooltip = json['tooltip'] ?? '', + action = json['action'] ?? ''; + + static UiType? create(Map json) { + if (json['t'] == 'Button') { + return UiButton.fromJson(json['c']); + } else if (json['t'] == 'Checkbox') { + return UiCheckbox.fromJson(json['c']); + } else { + return null; + } + } +} + +class UiButton extends UiType { + String icon; + + UiButton( + {required String key, + required String text, + required this.icon, + required String tooltip, + required String action}) + : super(key, text, tooltip, action); + + UiButton.fromJson(Map json) + : icon = json['icon'] ?? '', + super.fromJson(json); +} + +class UiCheckbox extends UiType { + UiCheckbox( + {required String key, + required String text, + required String tooltip, + required String action}) + : super(key, text, tooltip, action); + + UiCheckbox.fromJson(Map json) : super.fromJson(json); +} + +class Location { + // location key: + // host|main|settings|display|others + // client|remote|toolbar|display + HashMap ui; + + Location(this.ui); + Location.fromJson(Map json) : ui = HashMap() { + json.forEach((key, value) { + var ui = UiType.create(value); + if (ui != null) { + this.ui[ui.key] = ui; + } + }); + } +} + +class ConfigItem { + String key; + String value; + String description; + String defaultValue; + + ConfigItem(this.key, this.value, this.defaultValue, this.description); + ConfigItem.fromJson(Map json) + : key = json['key'] ?? '', + value = json['value'] ?? '', + description = json['description'] ?? '', + defaultValue = json['default'] ?? ''; + + static String get trueValue => kValueTrue; + static String get falseValue => kValueFalse; + static bool isTrue(String value) => value == kValueTrue; + static bool isFalse(String value) => value == kValueFalse; +} + +class Config { + List local; + List peer; + + Config(this.local, this.peer); + Config.fromJson(Map json) + : local = (json['local'] as List) + .map((e) => ConfigItem.fromJson(e)) + .toList(), + peer = (json['peer'] as List) + .map((e) => ConfigItem.fromJson(e)) + .toList(); +} + +class Desc { + String id; + String name; + String version; + String description; + String author; + String home; + String license; + String published; + String released; + String github; + Location location; + Config config; + + Desc( + this.id, + this.name, + this.version, + this.description, + this.author, + this.home, + this.license, + this.published, + this.released, + this.github, + this.location, + this.config); + + Desc.fromJson(Map json) + : id = json['id'] ?? '', + name = json['name'] ?? '', + version = json['version'] ?? '', + description = json['description'] ?? '', + author = json['author'] ?? '', + home = json['home'] ?? '', + license = json['license'] ?? '', + published = json['published'] ?? '', + released = json['released'] ?? '', + github = json['github'] ?? '', + location = Location(HashMap.from(json['location'])), + config = Config( + (json['config'] as List) + .map((e) => ConfigItem.fromJson(e)) + .toList(), + (json['config'] as List) + .map((e) => ConfigItem.fromJson(e)) + .toList()); +} + +final mapPluginDesc = {}; + +void updateDesc(Map desc) { + Desc d = Desc.fromJson(desc); + mapPluginDesc[d.id] = d; +} + +Desc? getDesc(String id) { + return mapPluginDesc[id]; +} diff --git a/flutter/lib/desktop/plugin/event.dart b/flutter/lib/desktop/plugin/event.dart new file mode 100644 index 000000000..04d0ddf45 --- /dev/null +++ b/flutter/lib/desktop/plugin/event.dart @@ -0,0 +1,60 @@ +void handlePluginEvent( + Map evt, + String peer, + Function(Map e) handleMsgBox, +) { + // content + // + // { + // "t": "Option", + // "c": { + // "id": "id from RustDesk platform", + // "name": "Privacy Mode", + // "version": "v0.1.0", + // "location": "client|remote|toolbar|display", + // "key": "privacy-mode", + // "value": "1" + // } + // } + // + // { + // "t": "MsgBox", + // "c": { + // "type": "custom-nocancel", + // "title": "Privacy Mode", + // "text": "Failed unknown", + // "link": "" + // } + // } + // + if (evt['content']?['c'] == null) return; + final t = evt['content']?['t']; + if (t == 'Option') { + handleOptionEvent(evt['content']?['c'], peer); + } else if (t == 'MsgBox') { + handleMsgBox(evt['content']?['c']); + } +} + +void handleOptionEvent(Map evt, String peer) { + // content + // + // { + // "id": "id from RustDesk platform", + // "name": "Privacy Mode", + // "version": "v0.1.0", + // "location": "client|remote|toolbar|display", + // "key": "privacy-mode", + // "value": "1" + // } + // + final key = evt['key']; + final value = evt['value']; + if (key == 'privacy-mode') { + if (value == '1') { + // enable privacy mode + } else { + // disable privacy mode + } + } +} diff --git a/flutter/lib/desktop/plugin/model.dart b/flutter/lib/desktop/plugin/model.dart new file mode 100644 index 000000000..a823844aa --- /dev/null +++ b/flutter/lib/desktop/plugin/model.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import './common.dart'; +import './desc.dart'; + +final Map locationModels = {}; + +class PluginModel with ChangeNotifier { + final List uiList = []; + + void add(UiType ui) { + uiList.add(ui); + notifyListeners(); + } + + bool get isEmpty => uiList.isEmpty; +} + +class LocationModel with ChangeNotifier { + final Map pluginModels = {}; + + void add(PluginId id, UiType ui) { + if (pluginModels[id] != null) { + pluginModels[id]!.add(ui); + } else { + var model = PluginModel(); + model.add(ui); + pluginModels[id] = model; + notifyListeners(); + } + } + + bool get isEmpty => pluginModels.isEmpty; +} + +void addLocationUi(String location, PluginId id, UiType ui) { + locationModels[location]?.add(id, ui); +} + +LocationModel addLocation(String location) { + if (locationModels[location] == null) { + locationModels[location] = LocationModel(); + } + return locationModels[location]!; +} diff --git a/flutter/lib/desktop/plugin/widget.dart b/flutter/lib/desktop/plugin/widget.dart new file mode 100644 index 000000000..3f7b413ea --- /dev/null +++ b/flutter/lib/desktop/plugin/widget.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; + +import './desc.dart'; +import './model.dart'; +import './common.dart'; + +class LocationItem extends StatelessWidget { + final String peerId; + final FFI ffi; + final String location; + final LocationModel locationModel; + + LocationItem({ + Key? key, + required this.peerId, + required this.ffi, + required this.location, + required this.locationModel, + }) : super(key: key); + + bool get isEmpty => locationModel.isEmpty; + + static LocationItem createLocationItem( + String peerId, FFI ffi, String location) { + final model = addLocation(location); + return LocationItem( + peerId: peerId, + ffi: ffi, + location: location, + locationModel: model, + ); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: locationModel, + child: Consumer(builder: (context, model, child) { + return Column( + children: model.pluginModels.entries + .map((entry) => _buildPluginItem(entry.key, entry.value)) + .toList(), + ); + }), + ); + } + + Widget _buildPluginItem(PluginId id, PluginModel model) => PluginItem( + pluginId: id, + peerId: peerId, + ffi: ffi, + location: location, + pluginModel: model, + ); +} + +class PluginItem extends StatelessWidget { + final PluginId pluginId; + final String peerId; + final FFI ffi; + final String location; + final PluginModel pluginModel; + + PluginItem({ + Key? key, + required this.pluginId, + required this.peerId, + required this.ffi, + required this.location, + required this.pluginModel, + }) : super(key: key); + + bool get isEmpty => pluginModel.isEmpty; + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: pluginModel, + child: Consumer(builder: (context, model, child) { + return Column( + children: model.uiList.map((ui) => _buildItem(ui)).toList(), + ); + }), + ); + } + + // to-do: add plugin icon and tooltip + Widget _buildItem(UiType ui) { + switch (ui.runtimeType) { + case UiButton: + return _buildMenuButton(ui as UiButton); + case UiCheckbox: + return _buildCheckboxMenuButton(ui as UiCheckbox); + default: + return Container(); + } + } + + Uint8List _makeEvent( + String localPeerId, + String key, { + bool? v, + }) { + final event = MsgFromUi( + remotePeerId: peerId, + localPeerId: localPeerId, + id: pluginId, + name: getDesc(pluginId)?.name ?? '', + location: location, + key: key, + value: + v != null ? (v ? ConfigItem.trueValue : ConfigItem.falseValue) : '', + action: '', + ); + return Uint8List.fromList(event.toString().codeUnits); + } + + Widget _buildMenuButton(UiButton ui) { + return MenuButton( + onPressed: () { + () async { + final localPeerId = await bind.mainGetMyId(); + bind.pluginEvent( + id: pluginId, + event: _makeEvent(localPeerId, ui.key), + ); + }(); + }, + trailingIcon: Icon( + IconData(int.parse(ui.icon, radix: 16), fontFamily: 'MaterialIcons')), + // to-do: RustDesk translate or plugin translate ? + child: Text(ui.text), + ffi: ffi, + ); + } + + Widget _buildCheckboxMenuButton(UiCheckbox ui) { + final v = + bind.pluginGetSessionOption(id: pluginId, peer: peerId, key: ui.key); + if (v == null) { + // session or plugin not found + return Container(); + } + return CkbMenuButton( + value: ConfigItem.isTrue(v), + onChanged: (v) { + if (v != null) { + () async { + final localPeerId = await bind.mainGetMyId(); + bind.pluginEvent( + id: pluginId, + event: _makeEvent(localPeerId, ui.key, v: v), + ); + }(); + } + }, + // to-do: rustdesk translate or plugin translate ? + child: Text(ui.text), + ffi: ffi, + ); + } +} + +void handleReloading(Map evt, String peer) { + if (evt['id'] == null || evt['location'] == null) { + return; + } + final ui = UiType.fromJson(evt); + addLocationUi(evt['location']!, evt['id']!, ui); +} diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 30dc09a6e..f4895c785 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1,13 +1,15 @@ import 'dart:convert'; -import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/toolbar.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/desktop/plugin/widget.dart'; +import 'package:flutter_hbb/desktop/plugin/common.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -16,7 +18,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:window_size/window_size.dart' as window_size; import '../../common.dart'; -import '../../mobile/widgets/dialog.dart'; +import '../../common/widgets/dialog.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; @@ -31,7 +33,6 @@ class MenubarState { final kStoreKey = 'remoteMenubarState'; late RxBool show; late RxBool _pin; - RxString viewStyle = RxString(kRemoteViewStyleOriginal); MenubarState() { final s = bind.getLocalFlutterConfig(k: kStoreKey); @@ -317,6 +318,11 @@ class _RemoteMenubarState extends State { RxBool get show => widget.state.show; bool get pin => widget.state.pin; + PeerInfo get pi => widget.ffi.ffiModel.pi; + FfiModel get ffiModel => widget.ffi.ffiModel; + + triggerAutoHide() => _debouncerHide.value = _debouncerHide.value + 1; + @override initState() { super.initState(); @@ -329,7 +335,7 @@ class _RemoteMenubarState extends State { widget.onEnterOrLeaveImageSetter((enter) { if (enter) { - _debouncerHide.value = 0; + triggerAutoHide(); _isCursorOverImage = true; } else { _isCursorOverImage = false; @@ -364,7 +370,7 @@ class _RemoteMenubarState extends State { Widget _buildDraggableShowHide(BuildContext context) { return Obx(() { if (show.isTrue && _dragging.isFalse) { - _debouncerHide.value = 1; + triggerAutoHide(); } return Align( alignment: FractionalOffset(_fractionX.value, 0), @@ -393,8 +399,7 @@ class _RemoteMenubarState extends State { toolbarItems.add(_MobileActionMenu(ffi: widget.ffi)); } - if (PrivacyModeState.find(widget.id).isFalse && - stateGlobal.displaysCount.value > 1) { + if (PrivacyModeState.find(widget.id).isFalse && pi.displays.length > 1) { toolbarItems.add( bind.mainGetUserDefaultOption(key: 'show_monitors_toolbar') == 'Y' ? _MultiMonitorMenu(id: widget.id, ffi: widget.ffi) @@ -452,7 +457,7 @@ class _RemoteMenubarState extends State { return Theme.of(context).copyWith( menuButtonTheme: MenuButtonThemeData( style: ButtonStyle( - minimumSize: MaterialStatePropertyAll(Size(64, 36)), + minimumSize: MaterialStatePropertyAll(Size(64, 32)), textStyle: MaterialStatePropertyAll( TextStyle(fontWeight: FontWeight.normal), ), @@ -633,291 +638,17 @@ class _ControlMenu extends StatelessWidget { color: _MenubarTheme.blueColor, hoverColor: _MenubarTheme.hoverBlueColor, ffi: ffi, - menuChildren: [ - requestElevation(), - osPassword(), - transferFile(context), - tcpTunneling(context), - note(), - Divider(), - ctrlAltDel(), - restart(), - insertLock(), - blockUserInput(), - switchSides(), - refresh(), - ]); - } - - requestElevation() { - final visible = ffi.elevationModel.showRequestMenu; - if (!visible) return Offstage(); - return _MenuItemButton( - child: Text(translate('Request Elevation')), - ffi: ffi, - onPressed: () => showRequestElevationDialog(id, ffi.dialogManager)); - } - - osPassword() { - return _MenuItemButton( - child: Text(translate('OS Password')), - trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)), - ffi: ffi, - onPressed: () => _showSetOSPassword(id, false, ffi.dialogManager)); - } - - _showSetOSPassword( - String id, bool login, OverlayDialogManager dialogManager) async { - final controller = TextEditingController(); - var password = - await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; - var autoLogin = - await bind.sessionGetOption(id: id, arg: 'auto-login') != ''; - controller.text = password; - dialogManager.show((setState, close) { - submit() { - var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: 'os-password', value: text); - bind.sessionPeerOption( - id: id, name: 'auto-login', value: autoLogin ? 'Y' : ''); - if (text != '' && login) { - bind.sessionInputOsPassword(id: id, value: text); - } - close(); - } - - return CustomAlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.password_rounded, color: MyTheme.accent), - Text(translate('OS Password')).paddingOnly(left: 10), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Auto Login'), - ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, - ), - ], - ), - actions: [ - dialogButton( - "Cancel", - icon: Icon(Icons.close_rounded), - onPressed: close, - isOutline: true, - ), - dialogButton( - "OK", - icon: Icon(Icons.done_rounded), - onPressed: submit, - ), - ], - onSubmit: submit, - onCancel: close, - ); - }); - } - - transferFile(BuildContext context) { - return _MenuItemButton( - child: Text(translate('Transfer File')), - ffi: ffi, - onPressed: () => connect(context, id, isFileTransfer: true)); - } - - tcpTunneling(BuildContext context) { - return _MenuItemButton( - child: Text(translate('TCP Tunneling')), - ffi: ffi, - onPressed: () => connect(context, id, isTcpTunneling: true)); - } - - note() { - final auditServer = bind.sessionGetAuditServerSync(id: id, typ: "conn"); - final visible = auditServer.isNotEmpty; - if (!visible) return Offstage(); - return _MenuItemButton( - child: Text(translate('Note')), - ffi: ffi, - onPressed: () => _showAuditDialog(id, ffi.dialogManager), - ); - } - - _showAuditDialog(String id, dialogManager) async { - final controller = TextEditingController(); - dialogManager.show((setState, close) { - submit() { - var text = controller.text.trim(); - if (text != '') { - bind.sessionSendNote(id: id, note: text); - } - close(); - } - - late final focusNode = FocusNode( - onKey: (FocusNode node, RawKeyEvent evt) { - if (evt.logicalKey.keyLabel == 'Enter') { - if (evt is RawKeyDownEvent) { - int pos = controller.selection.base.offset; - controller.text = - '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; - controller.selection = - TextSelection.fromPosition(TextPosition(offset: pos + 1)); - } - return KeyEventResult.handled; - } - if (evt.logicalKey.keyLabel == 'Esc') { - if (evt is RawKeyDownEvent) { - close(); - } - return KeyEventResult.handled; + menuChildren: toolbarControls(context, id, ffi).map((e) { + if (e.divider) { + return Divider(); } else { - return KeyEventResult.ignored; + return MenuButton( + child: e.child, + onPressed: e.onPressed, + ffi: ffi, + trailingIcon: e.trailingIcon); } - }, - ); - - return CustomAlertDialog( - title: Text(translate('Note')), - content: SizedBox( - width: 250, - height: 120, - child: TextField( - autofocus: true, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.newline, - decoration: const InputDecoration.collapsed( - hintText: 'input note here', - ), - maxLines: null, - maxLength: 256, - controller: controller, - focusNode: focusNode, - )), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit) - ], - onSubmit: submit, - onCancel: close, - ); - }); - } - - ctrlAltDel() { - final perms = ffi.ffiModel.permissions; - final viewOnly = ffi.ffiModel.viewOnly; - final pi = ffi.ffiModel.pi; - final visible = !viewOnly && - perms['keyboard'] != false && - (pi.platform == kPeerPlatformLinux || pi.sasEnabled); - if (!visible) return Offstage(); - return _MenuItemButton( - child: Text('${translate("Insert")} Ctrl + Alt + Del'), - ffi: ffi, - onPressed: () => bind.sessionCtrlAltDel(id: id)); - } - - restart() { - final perms = ffi.ffiModel.permissions; - final pi = ffi.ffiModel.pi; - final visible = perms['restart'] != false && - (pi.platform == kPeerPlatformLinux || - pi.platform == kPeerPlatformWindows || - pi.platform == kPeerPlatformMacOS); - if (!visible) return Offstage(); - return _MenuItemButton( - child: Text(translate('Restart Remote Device')), - ffi: ffi, - onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager)); - } - - insertLock() { - final perms = ffi.ffiModel.permissions; - final viewOnly = ffi.ffiModel.viewOnly; - final visible = !viewOnly && perms['keyboard'] != false; - if (!visible) return Offstage(); - return _MenuItemButton( - child: Text(translate('Insert Lock')), - ffi: ffi, - onPressed: () => bind.sessionLockScreen(id: id)); - } - - blockUserInput() { - final perms = ffi.ffiModel.permissions; - final pi = ffi.ffiModel.pi; - final visible = - perms['keyboard'] != false && pi.platform == kPeerPlatformWindows; - if (!visible) return Offstage(); - return _MenuItemButton( - child: Obx(() => Text(translate( - '${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))), - ffi: ffi, - onPressed: () { - RxBool blockInput = BlockInputState.find(id); - bind.sessionToggleOption( - id: id, value: '${blockInput.value ? 'un' : ''}block-input'); - blockInput.value = !blockInput.value; - }); - } - - switchSides() { - final perms = ffi.ffiModel.permissions; - final pi = ffi.ffiModel.pi; - final visible = perms['keyboard'] != false && - pi.platform != kPeerPlatformAndroid && - pi.platform != kPeerPlatformMacOS && - version_cmp(pi.version, '1.2.0') >= 0; - if (!visible) return Offstage(); - return _MenuItemButton( - child: Text(translate('Switch Sides')), - ffi: ffi, - onPressed: () => _showConfirmSwitchSidesDialog(id, ffi.dialogManager)); - } - - void _showConfirmSwitchSidesDialog( - String id, OverlayDialogManager dialogManager) async { - dialogManager.show((setState, close) { - submit() async { - await bind.sessionSwitchSides(id: id); - closeConnection(id: id); - } - - return CustomAlertDialog( - content: msgboxContent('info', 'Switch Sides', - 'Please confirm if you want to share your desktop?'), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit), - ], - onSubmit: submit, - onCancel: close, - ); - }); - } - - refresh() { - final pi = ffi.ffiModel.pi; - final visible = pi.version.isNotEmpty; - if (!visible) return Offstage(); - return _MenuItemButton( - child: Text(translate('Refresh')), - ffi: ffi, - onPressed: () => bind.sessionRefresh(id: id)); + }).toList()); } } @@ -926,13 +657,19 @@ class _DisplayMenu extends StatefulWidget { final FFI ffi; final MenubarState state; final Function(bool) setFullscreen; + final LocationItem pluginItem; _DisplayMenu( {Key? key, required this.id, required this.ffi, required this.state, required this.setFullscreen}) - : super(key: key); + : pluginItem = LocationItem.createLocationItem( + id, + ffi, + kLocationClientRemoteToolbarDisplay, + ), + super(key: key); @override State<_DisplayMenu> createState() => _DisplayMenuState(); @@ -948,6 +685,9 @@ class _DisplayMenuState extends State<_DisplayMenu> { Map get perms => widget.ffi.ffiModel.permissions; PeerInfo get pi => widget.ffi.ffiModel.pi; + FfiModel get ffiModel => widget.ffi.ffiModel; + FFI get ffi => widget.ffi; + String get id => widget.id; @override Widget build(BuildContext context) { @@ -966,30 +706,27 @@ class _DisplayMenuState extends State<_DisplayMenu> { codec(), resolutions(), Divider(), - showRemoteCursor(), - zoomCursor(), - showQualityMonitor(), - mute(), - fileCopyAndPaste(), - disableClipboard(), - lockAfterSessionEnd(), - privacyMode(), - swapKey(), + toggles(), + widget.pluginItem, ]); } adjustWindow() { - final visible = _isWindowCanBeAdjusted(); - if (!visible) return Offstage(); - return Column( - children: [ - _MenuItemButton( - child: Text(translate('Adjust Window')), - onPressed: _doAdjustWindow, - ffi: widget.ffi), - Divider(), - ], - ); + return futureBuilder( + future: _isWindowCanBeAdjusted(), + hasData: (data) { + final visible = data as bool; + if (!visible) return Offstage(); + return Column( + children: [ + MenuButton( + child: Text(translate('Adjust Window')), + onPressed: _doAdjustWindow, + ffi: widget.ffi), + Divider(), + ], + ); + }); } _doAdjustWindow() async { @@ -1061,8 +798,9 @@ class _DisplayMenuState extends State<_DisplayMenu> { } } - _isWindowCanBeAdjusted() { - if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { + Future _isWindowCanBeAdjusted() async { + final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; + if (viewStyle != kRemoteViewStyleOriginal) { return false; } final remoteCount = RemoteCountState.find().value; @@ -1092,47 +830,34 @@ class _DisplayMenuState extends State<_DisplayMenu> { } viewStyle() { - return futureBuilder(future: () async { - final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; - widget.state.viewStyle.value = viewStyle; - return viewStyle; - }(), hasData: (data) { - final groupValue = data as String; - onChanged(String? value) async { - if (value == null) return; - await bind.sessionSetViewStyle(id: widget.id, value: value); - widget.state.viewStyle.value = value; - widget.ffi.canvasModel.updateViewStyle(); - } - - return Column(children: [ - _RadioMenuButton( - child: Text(translate('Scale original')), - value: kRemoteViewStyleOriginal, - groupValue: groupValue, - onChanged: onChanged, - ffi: widget.ffi, - ), - _RadioMenuButton( - child: Text(translate('Scale adaptive')), - value: kRemoteViewStyleAdaptive, - groupValue: groupValue, - onChanged: onChanged, - ffi: widget.ffi, - ), - Divider(), - ]); - }); + return futureBuilder( + future: toolbarViewStyle(context, widget.id, widget.ffi), + hasData: (data) { + final v = data as List>; + return Column(children: [ + ...v + .map((e) => RdoMenuButton( + value: e.value, + groupValue: e.groupValue, + onChanged: e.onChanged, + child: e.child, + ffi: ffi)) + .toList(), + Divider(), + ]); + }); } scrollStyle() { - final visible = widget.state.viewStyle.value == kRemoteViewStyleOriginal; - if (!visible) return Offstage(); return futureBuilder(future: () async { + final viewStyle = await bind.sessionGetViewStyle(id: id) ?? ''; + final visible = viewStyle == kRemoteViewStyleOriginal; final scrollStyle = await bind.sessionGetScrollStyle(id: widget.id) ?? ''; - return scrollStyle; + return {'visible': visible, 'scrollStyle': scrollStyle}; }(), hasData: (data) { - final groupValue = data as String; + final visible = data['visible'] as bool; + if (!visible) return Offstage(); + final groupValue = data['scrollStyle'] as String; onChange(String? value) async { if (value == null) return; await bind.sessionSetScrollStyle(id: widget.id, value: value); @@ -1141,14 +866,14 @@ class _DisplayMenuState extends State<_DisplayMenu> { final enabled = widget.ffi.canvasModel.imageOverflow.value; return Column(children: [ - _RadioMenuButton( + RdoMenuButton( child: Text(translate('ScrollAuto')), value: kRemoteScrollStyleAuto, groupValue: groupValue, onChanged: enabled ? (value) => onChange(value) : null, ffi: widget.ffi, ), - _RadioMenuButton( + RdoMenuButton( child: Text(translate('Scrollbar')), value: kRemoteScrollStyleBar, groupValue: groupValue, @@ -1161,269 +886,51 @@ class _DisplayMenuState extends State<_DisplayMenu> { } imageQuality() { - return futureBuilder(future: () async { - final imageQuality = - await bind.sessionGetImageQuality(id: widget.id) ?? ''; - return imageQuality; - }(), hasData: (data) { - final groupValue = data as String; - onChanged(String? value) async { - if (value == null) return; - await bind.sessionSetImageQuality(id: widget.id, value: value); - } - - return _SubmenuButton( - ffi: widget.ffi, - child: Text(translate('Image Quality')), - menuChildren: [ - _RadioMenuButton( - child: Text(translate('Good image quality')), - value: kRemoteImageQualityBest, - groupValue: groupValue, - onChanged: onChanged, + return futureBuilder( + future: toolbarImageQuality(context, widget.id, widget.ffi), + hasData: (data) { + final v = data as List>; + return _SubmenuButton( ffi: widget.ffi, - ), - _RadioMenuButton( - child: Text(translate('Balanced')), - value: kRemoteImageQualityBalanced, - groupValue: groupValue, - onChanged: onChanged, - ffi: widget.ffi, - ), - _RadioMenuButton( - child: Text(translate('Optimize reaction time')), - value: kRemoteImageQualityLow, - groupValue: groupValue, - onChanged: onChanged, - ffi: widget.ffi, - ), - _RadioMenuButton( - child: Text(translate('Custom')), - value: kRemoteImageQualityCustom, - groupValue: groupValue, - onChanged: (value) { - onChanged(value); - _customImageQualityDialog(); - }, - ffi: widget.ffi, - ), - ], - ); - }); - } - - _customImageQualityDialog() async { - double qualityInitValue = 50; - double fpsInitValue = 30; - bool qualitySet = false; - bool fpsSet = false; - setCustomValues({double? quality, double? fps}) async { - if (quality != null) { - qualitySet = true; - await bind.sessionSetCustomImageQuality( - id: widget.id, value: quality.toInt()); - } - if (fps != null) { - fpsSet = true; - await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt()); - } - if (!qualitySet) { - qualitySet = true; - await bind.sessionSetCustomImageQuality( - id: widget.id, value: qualityInitValue.toInt()); - } - if (!fpsSet) { - fpsSet = true; - await bind.sessionSetCustomFps( - id: widget.id, fps: fpsInitValue.toInt()); - } - } - - final btnClose = dialogButton('Close', onPressed: () async { - await setCustomValues(); - widget.ffi.dialogManager.dismissAll(); - }); - - // quality - final quality = await bind.sessionGetCustomImageQuality(id: widget.id); - qualityInitValue = - quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; - const qualityMinValue = 10.0; - const qualityMaxValue = 100.0; - if (qualityInitValue < qualityMinValue) { - qualityInitValue = qualityMinValue; - } - if (qualityInitValue > qualityMaxValue) { - qualityInitValue = qualityMaxValue; - } - final RxDouble qualitySliderValue = RxDouble(qualityInitValue); - final debouncerQuality = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(quality: v); - }, - initialValue: qualityInitValue, - ); - final qualitySlider = Obx(() => Row( - children: [ - Slider( - value: qualitySliderValue.value, - min: qualityMinValue, - max: qualityMaxValue, - divisions: 18, - onChanged: (double value) { - qualitySliderValue.value = value; - debouncerQuality.value = value; - }, - ), - SizedBox( - width: 40, - child: Text( - '${qualitySliderValue.value.round()}%', - style: const TextStyle(fontSize: 15), - )), - SizedBox( - width: 50, - child: Text( - translate('Bitrate'), - style: const TextStyle(fontSize: 15), - )) - ], - )); - // fps - final fpsOption = - await bind.sessionGetOption(id: widget.id, arg: 'custom-fps'); - fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; - if (fpsInitValue < 10 || fpsInitValue > 120) { - fpsInitValue = 30; - } - final RxDouble fpsSliderValue = RxDouble(fpsInitValue); - final debouncerFps = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(fps: v); - }, - initialValue: qualityInitValue, - ); - bool? direct; - try { - direct = ConnectionTypeState.find(widget.id).direct.value == - ConnectionType.strDirect; - } catch (_) {} - final fpsSlider = Offstage( - offstage: (await bind.mainIsUsingPublicServer() && direct != true) || - version_cmp(pi.version, '1.2.0') < 0, - child: Row( - children: [ - Obx((() => Slider( - value: fpsSliderValue.value, - min: 10, - max: 120, - divisions: 22, - onChanged: (double value) { - fpsSliderValue.value = value; - debouncerFps.value = value; - }, - ))), - SizedBox( - width: 40, - child: Obx(() => Text( - '${fpsSliderValue.value.round()}', - style: const TextStyle(fontSize: 15), - ))), - SizedBox( - width: 50, - child: Text( - translate('FPS'), - style: const TextStyle(fontSize: 15), - )) - ], - ), - ); - - final content = Column( - children: [qualitySlider, fpsSlider], - ); - msgBoxCommon( - widget.ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); + child: Text(translate('Image Quality')), + menuChildren: v + .map((e) => RdoMenuButton( + value: e.value, + groupValue: e.groupValue, + onChanged: e.onChanged, + child: e.child, + ffi: ffi)) + .toList(), + ); + }); } codec() { - return futureBuilder(future: () async { - final supportedHwcodec = - await bind.sessionSupportedHwcodec(id: widget.id); - final codecPreference = - await bind.sessionGetOption(id: widget.id, arg: 'codec-preference') ?? - ''; - return { - 'supportedHwcodec': supportedHwcodec, - 'codecPreference': codecPreference - }; - }(), hasData: (data) { - final List codecs = []; - try { - final Map codecsJson = jsonDecode(data['supportedHwcodec']); - final h264 = codecsJson['h264'] ?? false; - final h265 = codecsJson['h265'] ?? false; - codecs.add(h264); - codecs.add(h265); - } catch (e) { - debugPrint("Show Codec Preference err=$e"); - } - final visible = bind.mainHasHwcodec() && - codecs.length == 2 && - (codecs[0] || codecs[1]); - if (!visible) return Offstage(); - final groupValue = data['codecPreference'] as String; - onChanged(String? value) async { - if (value == null) return; - await bind.sessionPeerOption( - id: widget.id, name: 'codec-preference', value: value); - bind.sessionChangePreferCodec(id: widget.id); - } + return futureBuilder( + future: toolbarCodec(context, id, ffi), + hasData: (data) { + final v = data as List>; + if (v.isEmpty) return Offstage(); - return _SubmenuButton( - ffi: widget.ffi, - child: Text(translate('Codec')), - menuChildren: [ - _RadioMenuButton( - child: Text(translate('Auto')), - value: 'auto', - groupValue: groupValue, - onChanged: onChanged, + return _SubmenuButton( ffi: widget.ffi, - ), - _RadioMenuButton( - child: Text(translate('VP9')), - value: 'vp9', - groupValue: groupValue, - onChanged: onChanged, - ffi: widget.ffi, - ), - _RadioMenuButton( - child: Text(translate('H264')), - value: 'h264', - groupValue: groupValue, - onChanged: codecs[0] ? onChanged : null, - ffi: widget.ffi, - ), - _RadioMenuButton( - child: Text(translate('H265')), - value: 'h265', - groupValue: groupValue, - onChanged: codecs[1] ? onChanged : null, - ffi: widget.ffi, - ), - ]); - }); + child: Text(translate('Codec')), + menuChildren: v + .map((e) => RdoMenuButton( + value: e.value, + groupValue: e.groupValue, + onChanged: e.onChanged, + child: e.child, + ffi: ffi)) + .toList()); + }); } resolutions() { - final resolutions = widget.ffi.ffiModel.pi.resolutions; - final visible = widget.ffi.ffiModel.permissions["keyboard"] != false && - resolutions.length > 1; + final resolutions = pi.resolutions; + final visible = ffiModel.keyboard && resolutions.length > 1; if (!visible) return Offstage(); - final display = widget.ffi.ffiModel.display; + final display = ffiModel.display; final groupValue = "${display.width}x${display.height}"; onChanged(String? value) async { if (value == null) return; @@ -1435,9 +942,9 @@ class _DisplayMenuState extends State<_DisplayMenu> { await bind.sessionChangeResolution( id: widget.id, width: w, height: h); Future.delayed(Duration(seconds: 3), () async { - final display = widget.ffi.ffiModel.display; + final display = ffiModel.display; if (w == display.width && h == display.height) { - if (_isWindowCanBeAdjusted()) { + if (await _isWindowCanBeAdjusted()) { _doAdjustWindow(); } } @@ -1449,7 +956,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { return _SubmenuButton( ffi: widget.ffi, menuChildren: resolutions - .map((e) => _RadioMenuButton( + .map((e) => RdoMenuButton( value: '${e.width}x${e.height}', groupValue: groupValue, onChanged: onChanged, @@ -1459,171 +966,21 @@ class _DisplayMenuState extends State<_DisplayMenu> { child: Text(translate("Resolution"))); } - showRemoteCursor() { - if (widget.ffi.ffiModel.pi.platform == kPeerPlatformAndroid) { - return Offstage(); - } - final ffiModel = widget.ffi.ffiModel; - final visible = !widget.ffi.canvasModel.cursorEmbedded; - if (!visible) return Offstage(); - final enabled = !ffiModel.viewOnly; - final state = ShowRemoteCursorState.find(widget.id); - final option = 'show-remote-cursor'; - return _CheckboxMenuButton( - value: state.value, - onChanged: enabled - ? (value) async { - if (value == null) return; - await bind.sessionToggleOption(id: widget.id, value: option); - state.value = - bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - } - : null, - ffi: widget.ffi, - child: Text(translate('Show remote cursor'))); - } - - zoomCursor() { - if (widget.ffi.ffiModel.pi.platform == kPeerPlatformAndroid) { - return Offstage(); - } - final visible = widget.state.viewStyle.value != kRemoteViewStyleOriginal; - if (!visible) return Offstage(); - final option = 'zoom-cursor'; - final peerState = PeerBoolOption.find(widget.id, option); - return _CheckboxMenuButton( - value: peerState.value, - onChanged: (value) async { - if (value == null) return; - await bind.sessionToggleOption(id: widget.id, value: option); - peerState.value = - bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - }, - ffi: widget.ffi, - child: Text(translate('Zoom cursor'))); - } - - showQualityMonitor() { - final option = 'show-quality-monitor'; - final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - return _CheckboxMenuButton( - value: value, - onChanged: (value) async { - if (value == null) return; - await bind.sessionToggleOption(id: widget.id, value: option); - widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); - }, - ffi: widget.ffi, - child: Text(translate('Show quality monitor'))); - } - - mute() { - final visible = perms['audio'] != false; - if (!visible) return Offstage(); - final option = 'disable-audio'; - final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - return _CheckboxMenuButton( - value: value, - onChanged: (value) { - if (value == null) return; - bind.sessionToggleOption(id: widget.id, value: option); - }, - ffi: widget.ffi, - child: Text(translate('Mute'))); - } - - fileCopyAndPaste() { - final visible = Platform.isWindows && - pi.platform == kPeerPlatformWindows && - perms['file'] != false; - if (!visible) return Offstage(); - final option = 'enable-file-transfer'; - final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - return _CheckboxMenuButton( - value: value, - onChanged: (value) { - if (value == null) return; - bind.sessionToggleOption(id: widget.id, value: option); - }, - ffi: widget.ffi, - child: Text(translate('Allow file copy and paste'))); - } - - disableClipboard() { - final ffiModel = widget.ffi.ffiModel; - final visible = perms['keyboard'] != false && perms['clipboard'] != false; - if (!visible) return Offstage(); - final enabled = !ffiModel.viewOnly; - final option = 'disable-clipboard'; - var value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - if (ffiModel.viewOnly) value = true; - return _CheckboxMenuButton( - value: value, - onChanged: enabled - ? (value) { - if (value == null) return; - bind.sessionToggleOption(id: widget.id, value: option); - } - : null, - ffi: widget.ffi, - child: Text(translate('Disable clipboard'))); - } - - lockAfterSessionEnd() { - final visible = perms['keyboard'] != false; - if (!visible) return Offstage(); - final option = 'lock-after-session-end'; - final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - return _CheckboxMenuButton( - value: value, - onChanged: (value) { - if (value == null) return; - bind.sessionToggleOption(id: widget.id, value: option); - }, - ffi: widget.ffi, - child: Text(translate('Lock after session end'))); - } - - privacyMode() { - bool visible = perms['keyboard'] != false && pi.features.privacyMode; - if (!visible) return Offstage(); - final option = 'privacy-mode'; - final rxValue = PrivacyModeState.find(widget.id); - return _CheckboxMenuButton( - value: rxValue.value, - onChanged: (value) { - if (value == null) return; - if (widget.ffi.ffiModel.pi.currentDisplay != 0) { - msgBox( - widget.id, - 'custom-nook-nocancel-hasclose', - 'info', - 'Please switch to Display 1 first', - '', - widget.ffi.dialogManager); - return; - } - bind.sessionToggleOption(id: widget.id, value: option); - }, - ffi: widget.ffi, - child: Text(translate('Privacy mode'))); - } - - swapKey() { - final visible = perms['keyboard'] != false && - ((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) || - (!Platform.isMacOS && pi.platform == kPeerPlatformMacOS)); - if (!visible) return Offstage(); - final option = 'allow_swap_key'; - final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - return _CheckboxMenuButton( - value: value, - onChanged: (value) { - if (value == null) return; - bind.sessionToggleOption(id: widget.id, value: option); - }, - ffi: widget.ffi, - child: Text(translate('Swap control-command key'))); + toggles() { + return futureBuilder( + future: toolbarDisplayToggle(context, id, ffi), + hasData: (data) { + final v = data as List; + if (v.isEmpty) return Offstage(); + return Column( + children: v + .map((e) => CkbMenuButton( + value: e.value, + onChanged: e.onChanged, + child: e.child, + ffi: ffi)) + .toList()); + }); } } @@ -1641,15 +998,17 @@ class _KeyboardMenu extends StatelessWidget { @override Widget build(BuildContext context) { var ffiModel = Provider.of(context); - if (ffiModel.permissions['keyboard'] == false) return Offstage(); + if (!ffiModel.keyboard) return Offstage(); + String? modeOnly; if (stateGlobal.grabKeyboard) { if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) { bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode); + modeOnly = _kKeyMapMode; } else if (bind.sessionIsKeyboardModeSupported( id: id, mode: _kKeyLegacyMode)) { bind.sessionSetKeyboardMode(id: id, value: _kKeyLegacyMode); + modeOnly = _kKeyLegacyMode; } - return Offstage(); } return _IconSubmenuButton( tooltip: 'Keyboard Settings', @@ -1658,14 +1017,14 @@ class _KeyboardMenu extends StatelessWidget { color: _MenubarTheme.blueColor, hoverColor: _MenubarTheme.hoverBlueColor, menuChildren: [ - mode(), + mode(modeOnly), localKeyboardType(), Divider(), view_mode(), ]); } - mode() { + mode(String? modeOnly) { return futureBuilder(future: () async { return await bind.sessionGetKeyboardMode(id: id) ?? _kKeyLegacyMode; }(), hasData: (data) { @@ -1675,7 +1034,7 @@ class _KeyboardMenu extends StatelessWidget { KeyboardModeMenu(key: _kKeyMapMode, menu: 'Map mode'), KeyboardModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'), ]; - List<_RadioMenuButton> list = []; + List list = []; final enabled = !ffi.ffiModel.viewOnly; onChanged(String? value) async { if (value == null) return; @@ -1683,24 +1042,28 @@ class _KeyboardMenu extends StatelessWidget { } for (KeyboardModeMenu mode in modes) { - if (bind.sessionIsKeyboardModeSupported(id: id, mode: mode.key)) { - if (mode.key == _kKeyTranslateMode) { - if (Platform.isLinux) { - continue; - } - } - var text = translate(mode.menu); - if (mode.key == _kKeyTranslateMode) { - text = '$text beta'; - } - list.add(_RadioMenuButton( - child: Text(text), - value: mode.key, - groupValue: groupValue, - onChanged: enabled ? onChanged : null, - ffi: ffi, - )); + if (modeOnly != null && mode.key != modeOnly) { + continue; + } else if (!bind.sessionIsKeyboardModeSupported( + id: id, mode: mode.key)) { + continue; } + + if (pi.is_wayland && mode.key != _kKeyMapMode) { + continue; + } + + var text = translate(mode.menu); + if (mode.key == _kKeyTranslateMode) { + text = '$text beta'; + } + list.add(RdoMenuButton( + child: Text(text), + value: mode.key, + groupValue: groupValue, + onChanged: enabled ? onChanged : null, + ffi: ffi, + )); } return Column(children: list); }); @@ -1714,7 +1077,7 @@ class _KeyboardMenu extends StatelessWidget { return Column( children: [ Divider(), - _MenuItemButton( + MenuButton( child: Text( '${translate('Local keyboard type')}: ${KBLayoutType.value}'), trailingIcon: const Icon(Icons.settings), @@ -1729,9 +1092,8 @@ class _KeyboardMenu extends StatelessWidget { view_mode() { final ffiModel = ffi.ffiModel; - final enabled = version_cmp(pi.version, '1.2.0') >= 0 && - ffiModel.permissions["keyboard"] != false; - return _CheckboxMenuButton( + final enabled = version_cmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard; + return CkbMenuButton( value: ffiModel.viewOnly, onChanged: enabled ? (value) async { @@ -1775,7 +1137,7 @@ class _ChatMenuState extends State<_ChatMenu> { } textChat() { - return _MenuItemButton( + return MenuButton( child: Text(translate('Text chat')), ffi: widget.ffi, onPressed: () { @@ -1794,7 +1156,7 @@ class _ChatMenuState extends State<_ChatMenu> { } voiceCall() { - return _MenuItemButton( + return MenuButton( child: Text(translate('Voice call')), ffi: widget.ffi, onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), @@ -1846,19 +1208,22 @@ class _RecordMenu extends StatelessWidget { @override Widget build(BuildContext context) { var ffi = Provider.of(context); - final visible = ffi.permissions['recording'] != false; + var recordingModel = Provider.of(context); + final visible = + recordingModel.start || ffi.permissions['recording'] != false; if (!visible) return Offstage(); - return Consumer( - builder: (context, value, child) => _IconMenuButton( - assetName: 'assets/rec.svg', - tooltip: - value.start ? 'Stop session recording' : 'Start session recording', - onPressed: () => value.toggle(), - color: value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor, - hoverColor: value.start - ? _MenubarTheme.hoverRedColor - : _MenubarTheme.hoverBlueColor, - ), + return _IconMenuButton( + assetName: 'assets/rec.svg', + tooltip: recordingModel.start + ? 'Stop session recording' + : 'Start session recording', + onPressed: () => recordingModel.toggle(), + color: recordingModel.start + ? _MenubarTheme.redColor + : _MenubarTheme.blueColor, + hoverColor: recordingModel.start + ? _MenubarTheme.hoverRedColor + : _MenubarTheme.hoverBlueColor, ); } } @@ -2046,12 +1411,12 @@ class _SubmenuButton extends StatelessWidget { } } -class _MenuItemButton extends StatelessWidget { +class MenuButton extends StatelessWidget { final VoidCallback? onPressed; final Widget? trailingIcon; final Widget? child; final FFI ffi; - _MenuItemButton( + MenuButton( {Key? key, this.onPressed, this.trailingIcon, @@ -2074,12 +1439,12 @@ class _MenuItemButton extends StatelessWidget { } } -class _CheckboxMenuButton extends StatelessWidget { +class CkbMenuButton extends StatelessWidget { final bool? value; final ValueChanged? onChanged; final Widget? child; final FFI ffi; - const _CheckboxMenuButton( + const CkbMenuButton( {Key? key, required this.value, required this.onChanged, @@ -2103,13 +1468,13 @@ class _CheckboxMenuButton extends StatelessWidget { } } -class _RadioMenuButton extends StatelessWidget { +class RdoMenuButton extends StatelessWidget { final T value; final T? groupValue; final ValueChanged? onChanged; final Widget? child; final FFI ffi; - const _RadioMenuButton( + const RdoMenuButton( {Key? key, required this.value, required this.groupValue, @@ -2253,10 +1618,10 @@ class _MultiMonitorMenu extends StatelessWidget { final FFI ffi; const _MultiMonitorMenu({ - super.key, + Key? key, required this.id, required this.ffi, - }); + }) : super(key: key); @override Widget build(BuildContext context) { diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 4211911ff..2186e879e 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -869,7 +869,7 @@ class _TabState extends State<_Tab> with RestorationMixin { return ConstrainedBox( constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200), child: Text( - widget.label.value, + translate(widget.label.value), textAlign: TextAlign.center, style: TextStyle( color: isSelected diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index f0a9a938f..27987d776 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -134,10 +134,8 @@ void runMainApp(bool startService) async { await restoreWindowPosition(WindowType.Main); // Check the startup argument, if we successfully handle the argument, we keep the main window hidden. final handledByUniLinks = await initUniLinks(); - final handledByCli = checkArguments(); - debugPrint( - "handled by uni links: $handledByUniLinks, handled by cli: $handledByCli"); - if (handledByUniLinks || handledByCli) { + debugPrint("handled by uni links: $handledByUniLinks"); + if (handledByUniLinks || checkArguments()) { windowManager.hide(); } else { windowManager.show(); @@ -250,6 +248,7 @@ void hideCmWindow() { windowManager.setOpacity(0); windowManager.waitUntilReadyToShow(windowOptions, () async { bind.mainHideDocker(); + await windowManager.minimize(); await windowManager.hide(); }); } diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index c6ba42d31..30e0661f6 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -8,7 +8,7 @@ import 'package:toggle_switch/toggle_switch.dart'; import 'package:wakelock/wakelock.dart'; import '../../common.dart'; -import '../widgets/dialog.dart'; +import '../../common/widgets/dialog.dart'; class FileManagerPage extends StatefulWidget { FileManagerPage({Key? key, required this.id}) : super(key: key); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 951d63faf..32c736dba 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -4,22 +4,25 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/common/widgets/toolbar.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:get/get.dart'; import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; import '../../common.dart'; import '../../common/widgets/overlay.dart'; +import '../../common/widgets/dialog.dart'; import '../../common/widgets/remote_input.dart'; import '../../models/input_model.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../utils/image.dart'; -import '../widgets/dialog.dart'; import '../widgets/gestures.dart'; final initText = '\1' * 1024; @@ -42,6 +45,8 @@ class _RemotePageState extends State { double _mouseScrollIntegral = 0; // mouse scroll speed controller Orientation? _currentOrientation; + final _blockableOverlayState = BlockableOverlayState(); + final keyboardVisibilityController = KeyboardVisibilityController(); late final StreamSubscription keyboardSubscription; final FocusNode _mobileFocusNode = FocusNode(); @@ -66,6 +71,8 @@ class _RemotePageState extends State { gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id); keyboardSubscription = keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged); + _blockableOverlayState.applyFfi(gFFI); + initSharedStates(widget.id); } @override @@ -82,6 +89,7 @@ class _RemotePageState extends State { overlays: SystemUiOverlay.values); Wakelock.disable(); keyboardSubscription.cancel(); + removeSharedStates(widget.id); super.dispose(); } @@ -540,128 +548,21 @@ class _RemotePageState extends State { final size = MediaQuery.of(context).size; final x = 120.0; final y = size.height; - final more = >[]; - final pi = gFFI.ffiModel.pi; - final perms = gFFI.ffiModel.permissions; - if (pi.version.isNotEmpty) { - more.add(PopupMenuItem( - child: Text(translate('Refresh')), value: 'refresh')); - } - more.add(PopupMenuItem( - child: Row( - children: ([ - Text(translate('OS Password')), - TextButton( - style: flatButtonStyle, - onPressed: () { - showSetOSPassword(id, false, gFFI.dialogManager); - }, - child: Icon(Icons.edit, color: MyTheme.accent), - ) - ])), - value: 'enter_os_password')); - if (!isWebDesktop) { - if (perms['keyboard'] != false && perms['clipboard'] != false) { - more.add(PopupMenuItem( - child: Text(translate('Paste')), value: 'paste')); - } - more.add(PopupMenuItem( - child: Text(translate('Reset canvas')), value: 'reset_canvas')); - } - if (perms['keyboard'] != false) { - // * Currently mobile does not enable map mode - // more.add(PopupMenuItem( - // child: Text(translate('Physical Keyboard Input Mode')), - // value: 'input-mode')); - if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { - more.add(PopupMenuItem( - child: Text('${translate('Insert')} Ctrl + Alt + Del'), - value: 'cad')); - } - more.add(PopupMenuItem( - child: Text(translate('Insert Lock')), value: 'lock')); - if (pi.platform == kPeerPlatformWindows && - await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') != - true) { - more.add(PopupMenuItem( - child: Text(translate( - '${gFFI.ffiModel.inputBlocked ? 'Unb' : 'B'}lock user input')), - value: 'block-input')); - } - } - if (perms["restart"] != false && - (pi.platform == kPeerPlatformLinux || - pi.platform == kPeerPlatformWindows || - pi.platform == kPeerPlatformMacOS)) { - more.add(PopupMenuItem( - child: Text(translate('Restart Remote Device')), value: 'restart')); - } - // Currently only support VP9 - if (gFFI.recordingModel.start || - (perms["recording"] != false && - gFFI.qualityMonitorModel.data.codecFormat == "VP9")) { - more.add(PopupMenuItem( - child: Row( - children: [ - Text(translate(gFFI.recordingModel.start - ? 'Stop session recording' - : 'Start session recording')), - Padding( - padding: EdgeInsets.only(left: 12), - child: Icon( - gFFI.recordingModel.start - ? Icons.pause_circle_filled - : Icons.videocam_outlined, - color: MyTheme.accent), - ) - ], - ), - value: 'record')); - } + final menus = toolbarControls(context, id, gFFI); + final more = menus + .asMap() + .entries + .map((e) => PopupMenuItem(child: e.value.child, value: e.key)) + .toList(); () async { - var value = await showMenu( + var index = await showMenu( context: context, position: RelativeRect.fromLTRB(x, y, x, y), items: more, elevation: 8, ); - if (value == 'cad') { - bind.sessionCtrlAltDel(id: widget.id); - // * Currently mobile does not enable map mode - // } else if (value == 'input-mode') { - // changePhysicalKeyboardInputMode(); - } else if (value == 'lock') { - bind.sessionLockScreen(id: widget.id); - } else if (value == 'block-input') { - bind.sessionToggleOption( - id: widget.id, - value: '${gFFI.ffiModel.inputBlocked ? 'un' : ''}block-input'); - gFFI.ffiModel.inputBlocked = !gFFI.ffiModel.inputBlocked; - } else if (value == 'refresh') { - bind.sessionRefresh(id: widget.id); - } else if (value == 'paste') { - () async { - ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null && data.text != null) { - bind.sessionInputString(id: widget.id, value: data.text ?? ""); - } - }(); - } else if (value == 'enter_os_password') { - // FIXME: - // null means no session of id - // empty string means no password - var password = await bind.sessionGetOption(id: id, arg: 'os-password'); - if (password != null) { - bind.sessionInputOsPassword(id: widget.id, value: password); - } else { - showSetOSPassword(id, true, gFFI.dialogManager); - } - } else if (value == 'reset_canvas') { - gFFI.cursorModel.reset(); - } else if (value == 'restart') { - showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); - } else if (value == 'record') { - gFFI.recordingModel.toggle(); + if (index != null && index < menus.length) { + menus[index].onPressed.call(); } }(); } @@ -695,10 +596,8 @@ class _RemotePageState extends State { // return CustomAlertDialog( // title: Text(translate('Physical Keyboard Input Mode')), // content: Column(mainAxisSize: MainAxisSize.min, children: [ - // getRadio('Legacy mode', 'legacy', current, setMode, - // contentPadding: EdgeInsets.zero), - // getRadio('Map mode', 'map', current, setMode, - // contentPadding: EdgeInsets.zero), + // getRadio('Legacy mode', 'legacy', current, setMode), + // getRadio('Map mode', 'map', current, setMode), // ])); // }, clickMaskDismiss: true); // } @@ -918,14 +817,6 @@ class CursorPaint extends StatelessWidget { void showOptions( BuildContext context, String id, OverlayDialogManager dialogManager) async { - String quality = - await bind.sessionGetImageQuality(id: id) ?? kRemoteImageQualityBalanced; - if (quality == '') quality = kRemoteImageQualityBalanced; - String codec = - await bind.sessionGetOption(id: id, arg: 'codec-preference') ?? 'auto'; - if (codec == '') codec = 'auto'; - String viewStyle = await bind.sessionGetViewStyle(id: id) ?? ''; - var displays = []; final pi = gFFI.ffiModel.pi; final image = gFFI.ffiModel.getConnectionImage(); @@ -968,155 +859,65 @@ void showOptions( if (displays.isNotEmpty) { displays.add(const Divider(color: MyTheme.border)); } - final perms = gFFI.ffiModel.permissions; - final hasHwcodec = bind.mainHasHwcodec(); - final List codecs = []; - if (hasHwcodec) { - try { - final Map codecsJson = - jsonDecode(await bind.sessionSupportedHwcodec(id: id)); - final h264 = codecsJson['h264'] ?? false; - final h265 = codecsJson['h265'] ?? false; - codecs.add(h264); - codecs.add(h265); - } catch (e) { - debugPrint("Show Codec Preference err=$e"); - } - } + + List> viewStyleRadios = + await toolbarViewStyle(context, id, gFFI); + List> imageQualityRadios = + await toolbarImageQuality(context, id, gFFI); + List> codecRadios = await toolbarCodec(context, id, gFFI); + List displayToggles = + await toolbarDisplayToggle(context, id, gFFI); dialogManager.show((setState, close) { - final more = []; - if (perms['audio'] != false) { - more.add(getToggle(id, setState, 'disable-audio', 'Mute')); - } - if (perms['keyboard'] != false) { - if (perms['clipboard'] != false) { - more.add( - getToggle(id, setState, 'disable-clipboard', 'Disable clipboard')); - } - more.add(getToggle( - id, setState, 'lock-after-session-end', 'Lock after session end')); - if (pi.platform == kPeerPlatformWindows) { - more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode')); - } - } - setQuality(String? value) { - if (value == null) return; - setState(() { - quality = value; - bind.sessionSetImageQuality(id: id, value: value); - }); - } - - setViewStyle(String? value) { - if (value == null) return; - setState(() { - viewStyle = value; - bind - .sessionSetViewStyle(id: id, value: value) - .then((_) => gFFI.canvasModel.updateViewStyle()); - }); - } - - setCodec(String? value) { - if (value == null) return; - setState(() { - codec = value; - bind - .sessionPeerOption(id: id, name: "codec-preference", value: value) - .then((_) => bind.sessionChangePreferCodec(id: id)); - }); - } - + var viewStyle = + (viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs; + var imageQuality = + (imageQualityRadios.isNotEmpty ? imageQualityRadios[0].groupValue : '') + .obs; + var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs; final radios = [ - getRadio( - 'Scale original', kRemoteViewStyleOriginal, viewStyle, setViewStyle), - getRadio( - 'Scale adaptive', kRemoteViewStyleAdaptive, viewStyle, setViewStyle), + for (var e in viewStyleRadios) + Obx(() => getRadio(e.child, e.value, viewStyle.value, (v) { + e.onChanged?.call(v); + if (v != null) viewStyle.value = v; + })), const Divider(color: MyTheme.border), - getRadio( - 'Good image quality', kRemoteImageQualityBest, quality, setQuality), - getRadio('Balanced', kRemoteImageQualityBalanced, quality, setQuality), - getRadio('Optimize reaction time', kRemoteImageQualityLow, quality, - setQuality), - const Divider(color: MyTheme.border) + for (var e in imageQualityRadios) + Obx(() => getRadio(e.child, e.value, imageQuality.value, (v) { + e.onChanged?.call(v); + if (v != null) imageQuality.value = v; + })), + const Divider(color: MyTheme.border), + for (var e in codecRadios) + Obx(() => getRadio(e.child, e.value, codec.value, (v) { + e.onChanged?.call(v); + if (v != null) codec.value = v; + })), + if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border), ]; - - if (hasHwcodec && codecs.length == 2 && (codecs[0] || codecs[1])) { - radios.addAll([ - getRadio(translate('Auto'), 'auto', codec, setCodec), - getRadio('VP9', 'vp9', codec, setCodec), - ]); - if (codecs[0]) { - radios.add(getRadio('H264', 'h264', codec, setCodec)); - } - if (codecs[1]) { - radios.add(getRadio('H265', 'h265', codec, setCodec)); - } - radios.add(const Divider(color: MyTheme.border)); - } - - final toggles = [ - getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'), - ]; - if (!gFFI.canvasModel.cursorEmbedded) { - toggles.insert(0, - getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor')); - } + final rxToggleValues = displayToggles.map((e) => e.value.obs).toList(); + final toggles = displayToggles + .asMap() + .entries + .map((e) => Obx(() => CheckboxListTile( + contentPadding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + value: rxToggleValues[e.key].value, + onChanged: (v) { + e.value.onChanged?.call(v); + if (v != null) rxToggleValues[e.key].value = v; + }, + title: e.value.child))) + .toList(); return CustomAlertDialog( content: Column( mainAxisSize: MainAxisSize.min, - children: displays + radios + toggles + more), - contentPadding: 0, + children: displays + radios + toggles), ); }, clickMaskDismiss: true, backDismiss: true); } -void showSetOSPassword( - String id, bool login, OverlayDialogManager dialogManager) async { - final controller = TextEditingController(); - var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? ""; - var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; - controller.text = password; - dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate('OS Password')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Auto Login'), - ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, - ), - ]), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton( - 'OK', - onPressed: () { - var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: "os-password", value: text); - bind.sessionPeerOption( - id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); - if (text != "" && login) { - bind.sessionInputOsPassword(id: id, value: text); - } - close(); - }, - ), - ]); - }); -} - void sendPrompt(bool isMac, String key) { final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl; if (isMac) { diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index c19601956..103c3e9fd 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:settings_ui/settings_ui.dart'; @@ -45,6 +46,7 @@ class _SettingsState extends State with WidgetsBindingObserver { var _autoRecordIncomingSession = false; var _localIP = ""; var _directAccessPort = ""; + var _fingerprint = ""; @override void initState() { @@ -135,6 +137,12 @@ class _SettingsState extends State with WidgetsBindingObserver { _directAccessPort = directAccessPort; } + final fingerprint = await bind.mainGetFingerprint(); + if (_fingerprint != fingerprint) { + update = true; + _fingerprint = fingerprint; + } + if (update) { setState(() {}); } @@ -462,6 +470,14 @@ class _SettingsState extends State with WidgetsBindingObserver { )), ), leading: Icon(Icons.info)), + SettingsTile.navigation( + onPressed: (context) => onCopyFingerprint(_fingerprint), + title: Text(translate("Fingerprint")), + value: Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Text(_fingerprint), + ), + leading: Icon(Icons.fingerprint)), ], ), ], @@ -502,19 +518,18 @@ void showLanguageSettings(OverlayDialogManager dialogManager) async { } return CustomAlertDialog( - title: SizedBox.shrink(), - content: Column( - children: [ - getRadio('Default', '', lang, setLang), - Divider(color: MyTheme.border), - ] + - langs.map((e) { - final key = e[0] as String; - final name = e[1] as String; - return getRadio(name, key, lang, setLang); - }).toList(), - ), - actions: []); + content: Column( + children: [ + getRadio(Text(translate('Default')), '', lang, setLang), + Divider(color: MyTheme.border), + ] + + langs.map((e) { + final key = e[0] as String; + final name = e[1] as String; + return getRadio(Text(translate(name)), key, lang, setLang); + }).toList(), + ), + ); }, backDismiss: true, clickMaskDismiss: true); } catch (e) { // @@ -536,14 +551,14 @@ void showThemeSettings(OverlayDialogManager dialogManager) async { } return CustomAlertDialog( - title: SizedBox.shrink(), - contentPadding: 10, - content: Column(children: [ - getRadio('Light', ThemeMode.light, themeMode, setTheme), - getRadio('Dark', ThemeMode.dark, themeMode, setTheme), - getRadio('Follow System', ThemeMode.system, themeMode, setTheme) - ]), - actions: []); + content: Column(children: [ + getRadio( + Text(translate('Light')), ThemeMode.light, themeMode, setTheme), + getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode, setTheme), + getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode, + setTheme) + ]), + ); }, backDismiss: true, clickMaskDismiss: true); } diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index b14401795..3f22cee7c 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -4,51 +4,16 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../common.dart'; -import '../../models/model.dart'; import '../../models/platform_model.dart'; -void clientClose(String id, OverlayDialogManager dialogManager) { - msgBox(id, 'info', 'Close', 'Are you sure to close the connection?', '', - dialogManager); -} - -void showSuccess() { +void _showSuccess() { showToast(translate("Successful")); } -void showError() { +void _showError() { showToast(translate("Error")); } -void showRestartRemoteDevice( - PeerInfo pi, String id, OverlayDialogManager dialogManager) async { - final res = - await dialogManager.show((setState, close) => CustomAlertDialog( - title: Row(children: [ - Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28), - Text(translate("Restart Remote Device")).paddingOnly(left: 10), - ]), - content: Text( - "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), - actions: [ - dialogButton( - "Cancel", - icon: Icon(Icons.close_rounded), - onPressed: close, - isOutline: true, - ), - dialogButton( - "OK", - icon: Icon(Icons.done_rounded), - onPressed: () => close(true), - ), - ], - onCancel: close, - onSubmit: () => close(true), - )); - if (res == true) bind.sessionRestartRemoteDevice(id: id); -} - void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { final pw = await bind.mainGetPermanentPassword(); final p0 = TextEditingController(text: pw); @@ -61,10 +26,10 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { dialogManager.showLoading(translate("Waiting")); if (await gFFI.serverModel.setPermanentPassword(p0.text)) { dialogManager.dismissAll(); - showSuccess(); + _showSuccess(); } else { dialogManager.dismissAll(); - showError(); + _showError(); } } @@ -157,117 +122,29 @@ void setTemporaryPasswordLengthDialog( bind.mainUpdateTemporaryPassword(); Future.delayed(Duration(milliseconds: 200), () { close(); - showSuccess(); + _showSuccess(); }); } return CustomAlertDialog( title: Text(translate("Set one-time password length")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: - lengths.map((e) => getRadio(e, e, length, setLength)).toList()), - actions: [], - contentPadding: 14, + content: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: lengths + .map( + (value) => Row( + children: [ + Text(value), + Radio( + value: value, groupValue: length, onChanged: setLength), + ], + ), + ) + .toList()), ); }, backDismiss: true, clickMaskDismiss: true); } -void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { - final controller = TextEditingController(); - var remember = await bind.sessionGetRemember(id: id) ?? false; - dialogManager.dismissAll(); - dialogManager.show((setState, close) { - cancel() { - close(); - closeConnection(); - } - - submit() { - var text = controller.text.trim(); - if (text == '') return; - gFFI.login(id, text, remember); - close(); - dialogManager.showLoading(translate('Logging in...'), - onCancel: closeConnection); - } - - return CustomAlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.password_rounded, color: MyTheme.accent), - Text(translate('Password Required')).paddingOnly(left: 10), - ], - ), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Remember password'), - ), - value: remember, - onChanged: (v) { - if (v != null) { - setState(() => remember = v); - } - }, - ), - ]), - actions: [ - dialogButton( - 'Cancel', - icon: Icon(Icons.close_rounded), - onPressed: cancel, - isOutline: true, - ), - dialogButton( - 'OK', - icon: Icon(Icons.done_rounded), - onPressed: submit, - ), - ], - onSubmit: submit, - onCancel: cancel, - ); - }); -} - -void wrongPasswordDialog( - String id, OverlayDialogManager dialogManager, type, title, text) { - dialogManager.dismissAll(); - dialogManager.show((setState, close) { - cancel() { - close(); - closeConnection(); - } - - submit() { - enterPasswordDialog(id, dialogManager); - } - - return CustomAlertDialog( - title: null, - content: msgboxContent(type, title, text), - onSubmit: submit, - onCancel: cancel, - actions: [ - dialogButton( - 'Cancel', - onPressed: cancel, - isOutline: true, - ), - dialogButton( - 'Retry', - onPressed: submit, - ), - ]); - }); -} - void showServerSettingsWithValue( ServerConfig serverConfig, OverlayDialogManager dialogManager) async { Map oldOptions = jsonDecode(await bind.mainGetOptions()); @@ -393,232 +270,6 @@ void showServerSettingsWithValue( }); } -void showWaitUacDialog( - String id, OverlayDialogManager dialogManager, String type) { - dialogManager.dismissAll(); - dialogManager.show( - tag: '$id-wait-uac', - (setState, close) => CustomAlertDialog( - title: null, - content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'), - )); -} - -void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) { - RxString groupValue = ''.obs; - RxString errUser = ''.obs; - RxString errPwd = ''.obs; - TextEditingController userController = TextEditingController(); - TextEditingController pwdController = TextEditingController(); - - void onRadioChanged(String? value) { - if (value != null) { - groupValue.value = value; - } - } - - const minTextStyle = TextStyle(fontSize: 14); - - var content = Obx(() => Column(children: [ - Row( - children: [ - Radio( - value: '', - groupValue: groupValue.value, - onChanged: onRadioChanged), - Expanded( - child: - Text(translate('Ask the remote user for authentication'))), - ], - ), - Align( - alignment: Alignment.centerLeft, - child: Text( - translate( - 'Choose this if the remote account is administrator'), - style: TextStyle(fontSize: 13)) - .marginOnly(left: 40), - ).marginOnly(bottom: 15), - Row( - children: [ - Radio( - value: 'logon', - groupValue: groupValue.value, - onChanged: onRadioChanged), - Expanded( - child: Text(translate( - 'Transmit the username and password of administrator')), - ) - ], - ), - Row( - children: [ - Expanded( - flex: 1, - child: Text( - '${translate('Username')}:', - style: minTextStyle, - ).marginOnly(right: 10)), - Expanded( - flex: 3, - child: TextField( - controller: userController, - style: minTextStyle, - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.symmetric(vertical: 15), - hintText: translate('eg: admin'), - errorText: errUser.isEmpty ? null : errUser.value), - onChanged: (s) { - if (s.isNotEmpty) { - errUser.value = ''; - } - }, - ), - ) - ], - ).marginOnly(left: 40), - Row( - children: [ - Expanded( - flex: 1, - child: Text( - '${translate('Password')}:', - style: minTextStyle, - ).marginOnly(right: 10)), - Expanded( - flex: 3, - child: TextField( - controller: pwdController, - obscureText: true, - style: minTextStyle, - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.symmetric(vertical: 15), - errorText: errPwd.isEmpty ? null : errPwd.value), - onChanged: (s) { - if (s.isNotEmpty) { - errPwd.value = ''; - } - }, - ), - ), - ], - ).marginOnly(left: 40), - Align( - alignment: Alignment.centerLeft, - child: Text(translate('still_click_uac_tip'), - style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)) - .marginOnly(top: 20)), - ])); - - dialogManager.dismissAll(); - dialogManager.show(tag: '$id-request-elevation', (setState, close) { - void submit() { - if (groupValue.value == 'logon') { - if (userController.text.isEmpty) { - errUser.value = translate('Empty Username'); - return; - } - if (pwdController.text.isEmpty) { - errPwd.value = translate('Empty Password'); - return; - } - bind.sessionElevateWithLogon( - id: id, - username: userController.text, - password: pwdController.text); - } else { - bind.sessionElevateDirect(id: id); - } - } - - return CustomAlertDialog( - title: Text(translate('Request Elevation')), - content: content, - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit), - ], - onSubmit: submit, - onCancel: close, - ); - }); -} - -void showOnBlockDialog( - String id, - String type, - String title, - String text, - OverlayDialogManager dialogManager, -) { - if (dialogManager.existing('$id-wait-uac') || - dialogManager.existing('$id-request-elevation')) { - return; - } - dialogManager.show(tag: '$id-$type', (setState, close) { - void submit() { - close(); - showRequestElevationDialog(id, dialogManager); - } - - return CustomAlertDialog( - title: null, - content: msgboxContent(type, title, - "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}"), - actions: [ - dialogButton('Wait', onPressed: close, isOutline: true), - dialogButton('Request Elevation', onPressed: submit), - ], - onSubmit: submit, - onCancel: close, - ); - }); -} - -void showElevationError(String id, String type, String title, String text, - OverlayDialogManager dialogManager) { - dialogManager.show(tag: '$id-$type', (setState, close) { - void submit() { - close(); - showRequestElevationDialog(id, dialogManager); - } - - return CustomAlertDialog( - title: null, - content: msgboxContent(type, title, text), - actions: [ - dialogButton('Cancel', onPressed: () { - close(); - }, isOutline: true), - dialogButton('Retry', onPressed: submit), - ], - onSubmit: submit, - onCancel: close, - ); - }); -} - -void showWaitAcceptDialog(String id, String type, String title, String text, - OverlayDialogManager dialogManager) { - dialogManager.dismissAll(); - dialogManager.show((setState, close) { - onCancel() { - closeConnection(); - } - - return CustomAlertDialog( - title: null, - content: msgboxContent(type, title, text), - actions: [ - dialogButton('Cancel', onPressed: onCancel, isOutline: true), - ], - onCancel: onCancel, - ); - }); -} - Future validateAsync(String value) async { value = value.trim(); if (value.isEmpty) { @@ -627,62 +278,3 @@ Future validateAsync(String value) async { final res = await bind.mainTestIfValidServer(server: value); return res.isEmpty ? null : res; } - -class PasswordWidget extends StatefulWidget { - PasswordWidget({Key? key, required this.controller, this.autoFocus = true}) - : super(key: key); - - final TextEditingController controller; - final bool autoFocus; - - @override - State createState() => _PasswordWidgetState(); -} - -class _PasswordWidgetState extends State { - bool _passwordVisible = false; - final _focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - if (widget.autoFocus) { - Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus()); - } - } - - @override - void dispose() { - _focusNode.unfocus(); - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return TextField( - focusNode: _focusNode, - controller: widget.controller, - obscureText: !_passwordVisible, - //This will obscure text dynamically - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Password'), - hintText: translate('Enter your password'), - // Here is key idea - suffixIcon: IconButton( - icon: Icon( - // Based on passwordVisible state choose the icon - _passwordVisible ? Icons.visibility : Icons.visibility_off, - color: MyTheme.lightTheme.primaryColor), - onPressed: () { - // Update the state i.e. toggle the state of passwordVisible variable - setState(() { - _passwordVisible = !_passwordVisible; - }); - }, - ), - ), - ); - } -} diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 175c8424b..133343297 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -27,7 +27,9 @@ class AbModel { abError.value = ""; final api = "${await bind.mainGetApiServer()}/api/ab/get"; try { - final resp = await http.post(Uri.parse(api), headers: getHttpHeaders()); + var authHeaders = getHttpHeaders(); + authHeaders['Content-Type'] = "application/json"; + final resp = await http.post(Uri.parse(api), headers: authHeaders); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 9366d5b46..ff0faf7a4 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -45,8 +45,12 @@ class InputModel { var command = false; // trackpad - var trackpadScrollDistance = Offset.zero; + final _trackpadSpeed = 0.02; + var _trackpadLastDelta = Offset.zero; + var _trackpadScrollUnsent = Offset.zero; + var _stopFling = true; Timer? _flingTimer; + final _flingBaseDelay = 10; // mouse final isPhysicalMouse = false.obs; @@ -55,10 +59,12 @@ class InputModel { get id => parent.target?.id ?? ""; + bool get keyboardPerm => parent.target!.ffiModel.keyboard; + InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { - if (!stateGlobal.grabKeyboard) { + if (isDesktop && !stateGlobal.grabKeyboard) { return KeyEventResult.handled; } @@ -111,44 +117,41 @@ class InputModel { } void mapKeyboardMode(RawKeyEvent e) { - int scanCode; - int keyCode; + int positionCode = -1; + int platformCode = -1; bool down; if (e.data is RawKeyEventDataMacOs) { RawKeyEventDataMacOs newData = e.data as RawKeyEventDataMacOs; - scanCode = newData.keyCode; - keyCode = newData.keyCode; + positionCode = newData.keyCode; + platformCode = newData.keyCode; } else if (e.data is RawKeyEventDataWindows) { RawKeyEventDataWindows newData = e.data as RawKeyEventDataWindows; - scanCode = newData.scanCode; - keyCode = newData.keyCode; + positionCode = newData.scanCode; + platformCode = newData.keyCode; } else if (e.data is RawKeyEventDataLinux) { RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux; // scanCode and keyCode of RawKeyEventDataLinux are incorrect. // 1. scanCode means keycode // 2. keyCode means keysym - scanCode = 0; - keyCode = newData.scanCode; + positionCode = newData.scanCode; + platformCode = newData.keyCode; } else if (e.data is RawKeyEventDataAndroid) { RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid; - scanCode = newData.scanCode + 8; - keyCode = newData.keyCode; - } else { - scanCode = -1; - keyCode = -1; - } + positionCode = newData.scanCode + 8; + platformCode = newData.keyCode; + } else {} if (e is RawKeyDownEvent) { down = true; } else { down = false; } - inputRawKey(e.character ?? '', keyCode, scanCode, down); + inputRawKey(e.character ?? '', platformCode, positionCode, down); } /// Send raw Key Event - void inputRawKey(String name, int keyCode, int scanCode, bool down) { + void inputRawKey(String name, int platformCode, int positionCode, bool down) { const capslock = 1; const numlock = 2; const scrolllock = 3; @@ -168,8 +171,8 @@ class InputModel { bind.sessionHandleFlutterKeyEvent( id: id, name: name, - keycode: keyCode, - scancode: scanCode, + platformCode: platformCode, + positionCode: positionCode, lockModes: lockModes, downOrUp: down); } @@ -199,7 +202,7 @@ class InputModel { /// [down] indicates the key's state(down or up). /// [press] indicates a click event(down and up). void inputKey(String name, {bool? down, bool? press}) { - if (!parent.target!.ffiModel.keyboard()) return; + if (!keyboardPerm) return; bind.sessionInputKey( id: id, name: name, @@ -282,7 +285,7 @@ class InputModel { /// Send mouse press event. void sendMouse(String type, MouseButtons button) { - if (!parent.target!.ffiModel.keyboard()) return; + if (!keyboardPerm) return; bind.sessionSendMouse( id: id, msg: json.encode(modify({'type': type, 'buttons': button.value}))); @@ -299,7 +302,7 @@ class InputModel { /// Send mouse movement event with distance in [x] and [y]. void moveMouse(double x, double y) { - if (!parent.target!.ffiModel.keyboard()) return; + if (!keyboardPerm) return; var x2 = x.toInt(); var y2 = y.toInt(); bind.sessionSendMouse( @@ -307,6 +310,7 @@ class InputModel { } void onPointHoverImage(PointerHoverEvent e) { + _stopFling = true; if (e.kind != ui.PointerDeviceKind.mouse) return; if (!isPhysicalMouse.value) { isPhysicalMouse.value = true; @@ -324,17 +328,33 @@ class InputModel { } } - void onPointerPanZoomStart(PointerPanZoomStartEvent e) {} + void onPointerPanZoomStart(PointerPanZoomStartEvent e) { + _stopFling = true; + } // https://docs.flutter.dev/release/breaking-changes/trackpad-gestures // TODO(support zoom in/out) void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) { var delta = e.panDelta; - trackpadScrollDistance += delta; + _trackpadLastDelta = delta; + _trackpadScrollUnsent += (delta * _trackpadSpeed); + var x = _trackpadScrollUnsent.dx.truncate(); + var y = _trackpadScrollUnsent.dy.truncate(); + _trackpadScrollUnsent -= Offset(_trackpadScrollUnsent.dx - x.toDouble(), + _trackpadScrollUnsent.dy - y.toDouble()); + + if (x == 0 && y == 0) { + x = delta.dx > 1 ? 1 : (delta.dx < -1 ? -1 : 0); + y = delta.dy > 1 ? 1 : (delta.dy < -1 ? -1 : 0); + if (x.abs() > y.abs()) { + y = 0; + } else { + x = 0; + } + } + bind.sessionSendMouse( - id: id, - msg: - '{"type": "trackpad", "x": "${delta.dx.toInt()}", "y": "${delta.dy.toInt()}"}'); + id: id, msg: '{"type": "trackpad", "x": "$x", "y": "$y"}'); } // Simple simulation for fling. @@ -357,18 +377,68 @@ class InputModel { }); } - void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { - var x = _signOrZero(trackpadScrollDistance.dx); - var y = _signOrZero(trackpadScrollDistance.dy); - var dx = trackpadScrollDistance.dx.abs() ~/ 40; - var dy = trackpadScrollDistance.dy.abs() ~/ 40; - _scheduleFling(x, y, dx, dy); + void _scheduleFling2(double x, double y, int delay) { + if ((x == 0 && y == 0) || _stopFling) { + return; + } - trackpadScrollDistance = Offset.zero; + _flingTimer = Timer(Duration(milliseconds: delay), () { + if (_stopFling) { + return; + } + + final d = 0.95; + x *= d; + y *= d; + final dx0 = x * _trackpadSpeed * 2; + final dy0 = y * _trackpadSpeed * 2; + + // Try set delta (x,y) and delay. + var dx = dx0.toInt(); + var dy = dy0.toInt(); + var delay = _flingBaseDelay; + + // Try set min delta (x,y), and increase delay. + if (dx == 0 && dy == 0) { + final thr = 25; + var vx = thr; + var vy = thr; + if (dx0 != 0) { + vx = 1.0 ~/ dx0.abs(); + } + if (dy0 != 0) { + vy = 1.0 ~/ dy0.abs(); + } + if (vx < vy && vx < thr) { + delay *= vx; + dx = dx0 > 0 ? 1 : (dx0 < 0 ? -1 : 0); + } else if (vy < thr) { + delay *= vy; + dy = dy0 > 0 ? 1 : (dy0 < 0 ? -1 : 0); + } + } + + if (dx == 0 && dy == 0) { + return; + } + + bind.sessionSendMouse( + id: id, msg: '{"type": "trackpad", "x": "$dx", "y": "$dy"}'); + _scheduleFling2(x, y, delay); + }); + } + + void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { + _stopFling = false; + _trackpadScrollUnsent = Offset.zero; + _scheduleFling2( + _trackpadLastDelta.dx, _trackpadLastDelta.dy, _flingBaseDelay); + _trackpadLastDelta = Offset.zero; } void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage"); + _stopFling = true; if (e.kind != ui.PointerDeviceKind.mouse) { if (isPhysicalMouse.value) { isPhysicalMouse.value = false; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1cc059ce0..406783c2e 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -16,6 +16,9 @@ import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/plugin/event.dart'; +import 'package:flutter_hbb/desktop/plugin/desc.dart'; +import 'package:flutter_hbb/desktop/plugin/widget.dart'; import 'package:flutter_hbb/common/shared_state.dart'; import 'package:tuple/tuple.dart'; import 'package:image/image.dart' as img2; @@ -25,7 +28,7 @@ import 'package:get/get.dart'; import '../common.dart'; import '../utils/image.dart' as img; -import '../mobile/widgets/dialog.dart'; +import '../common/widgets/dialog.dart'; import 'input_model.dart'; import 'platform_model.dart'; @@ -93,7 +96,7 @@ class FfiModel with ChangeNotifier { notifyListeners(); } - bool keyboard() => _permissions['keyboard'] != false; + bool get keyboard => _permissions['keyboard'] != false; clear() { _pi = PeerInfo(); @@ -224,6 +227,15 @@ class FfiModel with ChangeNotifier { parent.target?.chatModel.onVoiceCallIncoming(); } else if (name == "update_voice_call_state") { parent.target?.serverModel.updateVoiceCallState(evt); + } else if (name == "fingerprint") { + FingerprintState.find(peerId).value = evt['fingerprint'] ?? ''; + } else if (name == "plugin_desc") { + updateDesc(evt); + } else if (name == "plugin_event") { + handlePluginEvent( + evt, peerId, (Map e) => handleMsgBox(e, peerId)); + } else if (name == "plugin_reload") { + handleReloading(evt, peerId); } else { debugPrint("Unknown event name: $name"); } @@ -293,6 +305,11 @@ class FfiModel with ChangeNotifier { wrongPasswordDialog(id, dialogManager, type, title, text); } else if (type == 'input-password') { enterPasswordDialog(id, dialogManager); + } else if (type == 'session-login' || type == 'session-re-login') { + enterUserLoginDialog(id, dialogManager); + } else if (type == 'session-login-password' || + type == 'session-login-password') { + enterUserLoginAndPasswordDialog(id, dialogManager); } else if (type == 'restarting') { showMsgBox(id, type, title, text, link, false, dialogManager, hasCancel: false); @@ -452,6 +469,16 @@ class FfiModel with ChangeNotifier { setViewOnly(peerId, bind.sessionGetToggleOptionSync(id: peerId, arg: 'view-only')); } + if (connType == ConnType.defaultConn) { + final platform_additions = evt['platform_additions']; + if (platform_additions != null && platform_additions != '') { + try { + _pi.platform_additions = json.decode(platform_additions); + } catch (e) { + debugPrint('Failed to decode platform_additions $e'); + } + } + } notifyListeners(); } @@ -525,11 +552,18 @@ class FfiModel with ChangeNotifier { void setViewOnly(String id, bool value) { if (version_cmp(_pi.version, '1.2.0') < 0) return; - if (value) { - ShowRemoteCursorState.find(id).value = value; - } else { - ShowRemoteCursorState.find(id).value = - bind.sessionGetToggleOptionSync(id: id, arg: 'show-remote-cursor'); + // tmp fix for https://github.com/rustdesk/rustdesk/pull/3706#issuecomment-1481242389 + // because below rx not used in mobile version, so not initialized, below code will cause crash + // current our flutter code quality is fucking shit now. !!!!!!!!!!!!!!!! + try { + if (value) { + ShowRemoteCursorState.find(id).value = value; + } else { + ShowRemoteCursorState.find(id).value = + bind.sessionGetToggleOptionSync(id: id, arg: 'show-remote-cursor'); + } + } catch (e) { + // } if (_viewOnly != value) { _viewOnly = value; @@ -554,7 +588,13 @@ class ImageModel with ChangeNotifier { addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb); onRgba(Uint8List rgba) { - if (_waitForImage[id]!) { + final waitforImage = _waitForImage[id]; + if (waitforImage == null) { + debugPrint('Exception, peer $id not found for waiting image'); + return; + } + + if (waitforImage == true) { _waitForImage[id] = false; parent.target?.dialogManager.dismissAll(); if (isDesktop) { @@ -878,7 +918,7 @@ class CanvasModel with ChangeNotifier { } // If keyboard is not permitted, do not move cursor when mouse is moving. - if (parent.target != null && parent.target!.ffiModel.keyboard()) { + if (parent.target != null && parent.target!.ffiModel.keyboard) { // Draw cursor if is not desktop. if (!isDesktop) { parent.target!.cursorModel.moveLocal(x, y); @@ -1542,6 +1582,7 @@ class FFI { id = 'pf_$id'; } else { chatModel.resetClientMode(); + connType = ConnType.defaultConn; canvasModel.id = id; imageModel.id = id; cursorModel.id = id; @@ -1602,8 +1643,14 @@ class FFI { } /// Login with [password], choose if the client should [remember] it. - void login(String id, String password, bool remember) { - bind.sessionLogin(id: id, password: password, remember: remember); + void login(String osUsername, String osPassword, String id, String password, + bool remember) { + bind.sessionLogin( + id: id, + osUsername: osUsername, + osPassword: osPassword, + password: password, + remember: remember); } /// Close the remote session. @@ -1687,6 +1734,10 @@ class PeerInfo { List displays = []; Features features = Features(); List resolutions = []; + Map platform_additions = {}; + + bool get is_wayland => platform_additions['is_wayland'] == true; + bool get is_headless => platform_additions['headless'] == true; } const canvasKey = 'canvas'; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 6bd19d01a..1edf94527 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1236,10 +1236,10 @@ packages: dependency: "direct main" description: name: texture_rgba_renderer - sha256: "52bc9f217b7b07a760ee837d5a17329ad1f78ae8ed1e3fa612c6f1bed3c77f79" + sha256: cb048abdd800468ca40749ca10d1db9d1e6a055d1cde6234c05191293f0c7d61 url: "https://pub.dev" source: hosted - version: "0.0.13" + version: "0.0.16" timing: dependency: transitive description: @@ -1571,5 +1571,5 @@ packages: source: hosted version: "0.1.1" sdks: - dart: ">=2.18.0 <4.0.0" + dart: ">=2.18.0 <3.0.0" flutter: ">=3.3.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 5b98e85c0..596f565ff 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -92,7 +92,7 @@ dependencies: password_strength: ^0.2.0 flutter_launcher_icons: ^0.11.0 flutter_keyboard_visibility: ^5.4.0 - texture_rgba_renderer: ^0.0.13 + texture_rgba_renderer: ^0.0.16 percent_indicator: ^4.2.2 dropdown_button2: ^2.0.0 diff --git a/flutter/web/js/gen_js_from_hbb.py b/flutter/web/js/gen_js_from_hbb.py index 8ee553b35..cfa95ffe0 100755 --- a/flutter/web/js/gen_js_from_hbb.py +++ b/flutter/web/js/gen_js_from_hbb.py @@ -59,7 +59,7 @@ def main(): b = toks[1].replace('ControlKey(ControlKey::', '').replace("Chr('", '').replace("' as _)),", '').replace(')),', '') KEY_MAP[0] += ' "%s": "%s",\n'%(a, b) print() - print('export function checkIfRetry(msgtype: string, title: string, text: string) {') + print('export function checkIfRetry(msgtype: string, title: string, text: string, retry_for_relay: boolean) {') print(' return %s'%check_if_retry[0].replace('to_lowercase', 'toLowerCase').replace('contains', 'indexOf').replace('!', '').replace('")', '") < 0')) print(';}') print() diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index e7dc69ab1..c082236e3 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -115,7 +115,7 @@ impl Enigo { impl Default for Enigo { fn default() -> Self { - let is_x11 = "x11" == hbb_common::platform::linux::get_display_server(); + let is_x11 = hbb_common::platform::linux::is_x11_or_headless(); Self { is_x11, tfc: if is_x11 { diff --git a/libs/enigo/src/win/keycodes.rs b/libs/enigo/src/win/keycodes.rs index ea35685c5..500582bf0 100644 --- a/libs/enigo/src/win/keycodes.rs +++ b/libs/enigo/src/win/keycodes.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] // https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731 // // JP/KR mapping https://github.com/TigerVNC/tigervnc/blob/1a008c1380305648ab50f1d99e73439747e9d61d/vncviewer/win32.c#L267 diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index 115cb9789..f183e94ae 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -156,7 +156,7 @@ impl MouseControllable for Enigo { match button { MouseButton::Back => XBUTTON1 as _, MouseButton::Forward => XBUTTON2 as _, - _ => 0, + _ => 0, }, 0, 0, @@ -186,7 +186,7 @@ impl MouseControllable for Enigo { match button { MouseButton::Back => XBUTTON1 as _, MouseButton::Forward => XBUTTON2 as _, - _ => 0, + _ => 0, }, 0, 0, @@ -215,7 +215,7 @@ impl KeyboardControllable for Enigo { fn as_mut_any(&mut self) -> &mut dyn std::any::Any { self } - + fn key_sequence(&mut self, sequence: &str) { let mut buffer = [0; 2]; @@ -247,15 +247,51 @@ impl KeyboardControllable for Enigo { } fn key_down(&mut self, key: Key) -> crate::ResultType { - let code = self.key_to_keycode(key); - if code == 0 || code == 65535 { - return Err("".into()); - } - let res = keybd_event(0, code, 0); - if res == 0 { - let err = get_error(); - if !err.is_empty() { - return Err(err.into()); + match &key { + Key::Layout(c) => { + // to-do: dup code + // https://github.com/rustdesk/rustdesk/blob/1bc0dd791ed8344997024dc46626bd2ca7df73d2/src/server/input_service.rs#L1348 + let code = self.get_layoutdependent_keycode(*c); + if code as u16 != 0xFFFF { + let vk = code & 0x00FF; + let flag = code >> 8; + let modifiers = [Key::Shift, Key::Control, Key::Alt]; + let mod_len = modifiers.len(); + for pos in 0..mod_len { + if flag & (0x0001 << pos) != 0 { + self.key_down(modifiers[pos])?; + } + } + + let res = keybd_event(0, vk, 0); + let err = if res == 0 { get_error() } else { "".to_owned() }; + + for pos in 0..mod_len { + let rpos = mod_len - 1 - pos; + if flag & (0x0001 << rpos) != 0 { + self.key_up(modifiers[pos]); + } + } + + if !err.is_empty() { + return Err(err.into()); + } + } else { + return Err(format!("Failed to get keycode of {}", c).into()); + } + } + _ => { + let code = self.key_to_keycode(key); + if code == 0 || code == 65535 { + return Err("".into()); + } + let res = keybd_event(0, code, 0); + if res == 0 { + let err = get_error(); + if !err.is_empty() { + return Err(err.into()); + } + } } } Ok(()) @@ -323,9 +359,6 @@ impl Enigo { } fn key_to_keycode(&self, key: Key) -> u16 { - unsafe { - LAYOUT = std::ptr::null_mut(); - } // do not use the codes from crate winapi they're // wrongly typed with i32 instead of i16 use the // ones provided by win/keycodes.rs that are prefixed @@ -411,30 +444,24 @@ impl Enigo { Key::RightAlt => EVK_RMENU, Key::Raw(raw_keycode) => raw_keycode, - Key::Layout(c) => self.get_layoutdependent_keycode(c.to_string()), Key::Super | Key::Command | Key::Windows | Key::Meta => EVK_LWIN, + Key::Layout(..) => { + // unreachable + 0 + } } } - fn get_layoutdependent_keycode(&self, string: String) -> u16 { - // get the first char from the string ignore the rest - // ensure its not a multybyte char - if let Some(chr) = string.chars().nth(0) { - // NOTE VkKeyScanW uses the current keyboard LAYOUT - // to specify a LAYOUT use VkKeyScanExW and GetKeyboardLayout - // or load one with LoadKeyboardLayoutW - let current_window_thread_id = - unsafe { GetWindowThreadProcessId(GetForegroundWindow(), std::ptr::null_mut()) }; - unsafe { LAYOUT = GetKeyboardLayout(current_window_thread_id) }; - let keycode_and_shiftstate = unsafe { VkKeyScanExW(chr as _, LAYOUT) }; - if keycode_and_shiftstate == (EVK_DECIMAL as i16) && chr == '.' { - // a workaround of italian keyboard shift + '.' issue - EVK_PERIOD as _ - } else { - keycode_and_shiftstate as _ - } - } else { - 0 + fn get_layoutdependent_keycode(&self, chr: char) -> u16 { + unsafe { + LAYOUT = std::ptr::null_mut(); } + // NOTE VkKeyScanW uses the current keyboard LAYOUT + // to specify a LAYOUT use VkKeyScanExW and GetKeyboardLayout + // or load one with LoadKeyboardLayoutW + let current_window_thread_id = + unsafe { GetWindowThreadProcessId(GetForegroundWindow(), std::ptr::null_mut()) }; + unsafe { LAYOUT = GetKeyboardLayout(current_window_thread_id) }; + unsafe { VkKeyScanExW(chr as _, LAYOUT) as _ } } } diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index ed9ec73be..4ce9ef37e 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -34,6 +34,7 @@ tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } chrono = "0.4" backtrace = "0.3" libc = "0.2" +dlopen = "0.1" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index ed704f91e..1f323b7a9 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -24,8 +24,8 @@ message VideoFrame { YUV yuv = 8; EncodedVideoFrames h264s = 10; EncodedVideoFrames h265s = 11; + EncodedVideoFrames vp8s = 12; } - int64 timestamp = 9; } message IdPk { @@ -53,6 +53,11 @@ message FileTransfer { bool show_hidden = 2; } +message OSLogin { + string username = 1; + string password = 2; +} + message LoginRequest { string username = 1; bytes password = 2; @@ -66,6 +71,7 @@ message LoginRequest { bool video_ack_required = 9; uint64 session_id = 10; string version = 11; + OSLogin os_login = 12; } message ChatMessage { string text = 1; } @@ -77,6 +83,7 @@ message Features { message SupportedEncoding { bool h264 = 1; bool h265 = 2; + bool vp8 = 3; } message PeerInfo { @@ -91,6 +98,8 @@ message PeerInfo { Features features = 9; SupportedEncoding encoding = 10; SupportedResolutions resolutions = 11; + // Use JSON's key-value format which is friendly for peer to handle. + string platform_additions = 12; } message LoginResponse { @@ -202,11 +211,13 @@ message KeyEvent { bool press = 2; oneof union { ControlKey control_key = 3; - // high word, sym key code. win: virtual-key code, linux: keysym ?, macos: - // low word, position key code. win: scancode, linux: key code, macos: key code + // position key code. win: scancode, linux: key code, macos: key code uint32 chr = 4; uint32 unicode = 5; string seq = 6; + // high word. virtual keycode + // low word. unicode + uint32 win2win_hotkey = 7; } repeated ControlKey modifiers = 8; KeyboardMode mode = 9; @@ -456,18 +467,20 @@ enum ImageQuality { Best = 4; } -message VideoCodecState { +message SupportedDecoding { enum PreferCodec { Auto = 0; - VPX = 1; + VP9 = 1; H264 = 2; H265 = 3; + VP8 = 4; } - int32 score_vpx = 1; - int32 score_h264 = 2; - int32 score_h265 = 3; + int32 ability_vp9 = 1; + int32 ability_h264 = 2; + int32 ability_h265 = 3; PreferCodec prefer = 4; + int32 ability_vp8 = 5; } message OptionMessage { @@ -485,7 +498,7 @@ message OptionMessage { BoolOption disable_audio = 7; BoolOption disable_clipboard = 8; BoolOption enable_file_transfer = 9; - VideoCodecState video_codec_state = 10; + SupportedDecoding supported_decoding = 10; int32 custom_fps = 11; BoolOption disable_keyboard = 12; } @@ -511,7 +524,6 @@ message AudioFormat { message AudioFrame { bytes data = 1; - int64 timestamp = 2; } // Notify peer to show message box. @@ -588,6 +600,17 @@ message SwitchSidesResponse { message SwitchBack {} +message PluginRequest { + string id = 1; + bytes content = 2; +} + +message PluginResponse { + string id = 1; + string name = 2; + string msg = 3; +} + message Misc { oneof union { ChatMessage chat_message = 4; @@ -609,6 +632,8 @@ message Misc { SwitchSidesRequest switch_sides_request = 21; SwitchBack switch_back = 22; Resolution change_resolution = 24; + PluginRequest plugin_request = 25; + PluginResponse plugin_response = 26; } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 0624af672..369920982 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -64,11 +64,15 @@ lazy_static::lazy_static! { pub static ref APP_HOME_DIR: Arc> = Default::default(); } -// #[cfg(any(target_os = "android", target_os = "ios"))] +pub const LINK_DOCS_HOME: &str = "https://rustdesk.com/docs/en/"; +pub const LINK_DOCS_X11_REQUIRED: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; +pub const LINK_HEADLESS_LINUX_SUPPORT: &str = + "https://github.com/rustdesk/rustdesk/wiki/Headless-Linux-Support"; lazy_static::lazy_static! { pub static ref HELPER_URL: HashMap<&'static str, &'static str> = HashMap::from([ - ("rustdesk docs home", "https://rustdesk.com/docs/en/"), - ("rustdesk docs x11-required", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), + ("rustdesk docs home", LINK_DOCS_HOME), + ("rustdesk docs x11-required", LINK_DOCS_X11_REQUIRED), + ("rustdesk x11 headless", LINK_HEADLESS_LINUX_SUPPORT), ]); } @@ -297,6 +301,7 @@ pub struct TransferSerde { pub read_jobs: Vec, } +#[cfg(not(any(target_os = "android", target_os = "ios")))] fn patch(path: PathBuf) -> PathBuf { if let Some(_tmp) = path.to_str() { #[cfg(windows)] @@ -914,15 +919,13 @@ impl PeerConfig { decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); config.password = password; store = store || store2; - if let Some(v) = config.options.get_mut("rdp_password") { - let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION); - *v = password; - store = store || store2; - } - if let Some(v) = config.options.get_mut("os-password") { - let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION); - *v = password; - store = store || store2; + for opt in ["rdp_password", "os-username", "os-password"] { + if let Some(v) = config.options.get_mut(opt) { + let (encrypted, _, store2) = + decrypt_str_or_original(v, PASSWORD_ENC_VERSION); + *v = encrypted; + store = store || store2; + } } if store { config.store(id); @@ -940,12 +943,11 @@ impl PeerConfig { let _lock = CONFIG.read().unwrap(); let mut config = self.clone(); config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); - if let Some(v) = config.options.get_mut("rdp_password") { - *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION) + for opt in ["rdp_password", "os-username", "os-password"] { + if let Some(v) = config.options.get_mut(opt) { + *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION) + } } - if let Some(v) = config.options.get_mut("os-password") { - *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION) - }; if let Err(err) = store_path(Self::path(id), config) { log::error!("Failed to store config: {}", err); } @@ -1359,9 +1361,9 @@ impl UserDefaultConfig { "view_style" => self.get_string(key, "original", vec!["adaptive"]), "scroll_style" => self.get_string(key, "scrollauto", vec!["scrollbar"]), "image_quality" => self.get_string(key, "balanced", vec!["best", "low", "custom"]), - "codec-preference" => self.get_string(key, "auto", vec!["vp9", "h264", "h265"]), + "codec-preference" => self.get_string(key, "auto", vec!["vp8", "vp9", "h264", "h265"]), "custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 100.0), - "custom-fps" => self.get_double_string(key, 30.0, 10.0, 120.0), + "custom-fps" => self.get_double_string(key, 30.0, 5.0, 120.0), _ => self .options .get(key) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 799093d24..7d841613d 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -44,6 +44,8 @@ pub use libc; pub mod keyboard; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use sysinfo; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use dlopen; #[cfg(feature = "quic")] pub type Stream = quic::Connection; diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index f6133415a..89c96799d 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -5,6 +5,9 @@ lazy_static::lazy_static! { pub static ref DISTRO: Distro = Distro::new(); } +pub const DISPLAY_SERVER_WAYLAND: &str = "wayland"; +pub const DISPLAY_SERVER_X11: &str = "x11"; + pub struct Distro { pub name: String, pub version_id: String, @@ -12,23 +15,41 @@ pub struct Distro { impl Distro { fn new() -> Self { - let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release".to_owned()) + let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release") + .unwrap_or_default() + .trim() + .trim_matches('"') + .to_string(); + let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release") .unwrap_or_default() .trim() .trim_matches('"') .to_string(); - let version_id = - run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release".to_owned()) - .unwrap_or_default() - .trim() - .trim_matches('"') - .to_string(); Self { name, version_id } } } +#[inline] +pub fn is_gdm_user(username: &str) -> bool { + username == "gdm" + // || username == "lightgdm" +} + +#[inline] +pub fn is_desktop_wayland() -> bool { + get_display_server() == DISPLAY_SERVER_WAYLAND +} + +#[inline] +pub fn is_x11_or_headless() -> bool { + !is_desktop_wayland() +} + +// -1 +const INVALID_SESSION: &str = "4294967295"; + pub fn get_display_server() -> String { - let mut session = get_values_of_seat0([0].to_vec())[0].clone(); + let mut session = get_values_of_seat0(&[0])[0].clone(); if session.is_empty() { // loginctl has not given the expected output. try something else. if let Ok(sid) = std::env::var("XDG_SESSION_ID") { @@ -36,14 +57,20 @@ pub fn get_display_server() -> String { session = sid; } if session.is_empty() { - session = run_cmds("cat /proc/self/sessionid".to_owned()).unwrap_or_default(); + session = run_cmds("cat /proc/self/sessionid").unwrap_or_default(); + if session == INVALID_SESSION { + session = "".to_owned(); + } } } - - get_display_server_of_session(&session) + if session.is_empty() { + "".to_owned() + } else { + get_display_server_of_session(&session) + } } -fn get_display_server_of_session(session: &str) -> String { +pub fn get_display_server_of_session(session: &str) -> String { let mut display_server = if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "Type", session])) // Check session type of the session @@ -61,7 +88,7 @@ fn get_display_server_of_session(session: &str) -> String { .replace("TTY=", "") .trim_end() .into(); - if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{tty}.\\\\+Xorg\"")) + if let Ok(xorg_results) = run_cmds(&format!("ps -e | grep \"{tty}.\\\\+Xorg\"")) // And check if Xorg is running on that tty { if xorg_results.trim_end() != "" { @@ -87,44 +114,68 @@ fn get_display_server_of_session(session: &str) -> String { display_server.to_lowercase() } -pub fn get_values_of_seat0(indices: Vec) -> Vec { +#[inline] +fn line_values(indices: &[usize], line: &str) -> Vec { + indices + .into_iter() + .map(|idx| line.split_whitespace().nth(*idx).unwrap_or("").to_owned()) + .collect::>() +} + +#[inline] +pub fn get_values_of_seat0(indices: &[usize]) -> Vec { + _get_values_of_seat0(indices, true) +} + +#[inline] +pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec { + _get_values_of_seat0(indices, false) +} + +fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec { if let Ok(output) = run_loginctl(None) { for line in String::from_utf8_lossy(&output.stdout).lines() { if line.contains("seat0") { if let Some(sid) = line.split_whitespace().next() { if is_active(sid) { - return indices - .into_iter() - .map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned()) - .collect::>(); + if ignore_gdm_wayland { + if is_gdm_user(line.split_whitespace().nth(2).unwrap_or("")) + && get_display_server_of_session(sid) == DISPLAY_SERVER_WAYLAND + { + continue; + } + } + return line_values(indices, line); } } } } - } - // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 - if let Ok(output) = run_loginctl(None) { + // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 for line in String::from_utf8_lossy(&output.stdout).lines() { if let Some(sid) = line.split_whitespace().next() { - let d = get_display_server_of_session(sid); - if is_active(sid) && d != "tty" { - return indices - .into_iter() - .map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned()) - .collect::>(); + if is_active(sid) { + let d = get_display_server_of_session(sid); + if ignore_gdm_wayland { + if is_gdm_user(line.split_whitespace().nth(2).unwrap_or("")) + && d == DISPLAY_SERVER_WAYLAND + { + continue; + } + } + if d == "tty" { + continue; + } + return line_values(indices, line); } } } } - return indices - .iter() - .map(|_x| "".to_owned()) - .collect::>(); + line_values(indices, "") } -fn is_active(sid: &str) -> bool { +pub fn is_active(sid: &str) -> bool { if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) { String::from_utf8_lossy(&output.stdout).contains("active") } else { @@ -132,9 +183,9 @@ fn is_active(sid: &str) -> bool { } } -pub fn run_cmds(cmds: String) -> ResultType { +pub fn run_cmds(cmds: &str) -> ResultType { let output = std::process::Command::new("sh") - .args(vec!["-c", &cmds]) + .args(vec!["-c", cmds]) .output()?; Ok(String::from_utf8_lossy(&output.stdout).to_string()) } diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index c37c8aaf5..137868e12 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -4,11 +4,15 @@ pub mod linux; #[cfg(target_os = "macos")] pub mod macos; +#[cfg(not(debug_assertions))] use crate::{config::Config, log}; +#[cfg(not(debug_assertions))] use std::process::exit; +#[cfg(not(debug_assertions))] static mut GLOBAL_CALLBACK: Option> = None; +#[cfg(not(debug_assertions))] extern "C" fn breakdown_signal_handler(sig: i32) { let mut stack = vec![]; backtrace::trace(|frame| { @@ -29,6 +33,16 @@ extern "C" fn breakdown_signal_handler(sig: i32) { info = "Always use software rendering will be set.".to_string(); log::info!("{}", info); } + if stack.iter().any(|s| { + s.to_lowercase().contains("nvidia") + || s.to_lowercase().contains("amf") + || s.to_lowercase().contains("mfx") + || s.contains("cuProfilerStop") + }) { + Config::set_option("enable-hwcodec".to_string(), "N".to_string()); + info = "Perhaps hwcodec causing the crash, disable it first".to_string(); + log::info!("{}", info); + } log::error!( "Got signal {} and exit. stack:\n{}", sig, @@ -51,6 +65,7 @@ extern "C" fn breakdown_signal_handler(sig: i32) { exit(0); } +#[cfg(not(debug_assertions))] pub fn register_breakdown_handler(callback: T) where T: Fn() + 'static, diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs index bb0d071a2..55a82f8f7 100644 --- a/libs/hbb_common/src/udp.rs +++ b/libs/hbb_common/src/udp.rs @@ -32,7 +32,7 @@ fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result 0 { socket.set_recv_buffer_size(buf_size).ok(); } - log::info!( + log::debug!( "Receive buf size of udp {}: {:?}", addr, socket.recv_buffer_size() diff --git a/libs/scrap/build.rs b/libs/scrap/build.rs index 8939cd214..b0d800545 100644 --- a/libs/scrap/build.rs +++ b/libs/scrap/build.rs @@ -15,7 +15,7 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf { let mut target = if target_os == "macos" { if target_arch == "x64" { "x64-osx".to_owned() - } else if target_arch == "arm64"{ + } else if target_arch == "arm64" { "arm64-osx".to_owned() } else { format!("{}-{}", target_arch, target_os) diff --git a/libs/scrap/examples/benchmark.rs b/libs/scrap/examples/benchmark.rs index 003830f95..ba8dec9f2 100644 --- a/libs/scrap/examples/benchmark.rs +++ b/libs/scrap/examples/benchmark.rs @@ -3,7 +3,8 @@ use hbb_common::env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV}; use scrap::{ codec::{EncoderApi, EncoderCfg}, Capturer, Display, TraitCapturer, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, - VpxVideoCodecId, STRIDE_ALIGN, + VpxVideoCodecId::{self, *}, + STRIDE_ALIGN, }; use std::{io::Write, time::Instant}; @@ -49,7 +50,7 @@ fn main() { "benchmark {}x{} bitrate:{}k hw_pixfmt:{:?}", width, height, bitrate_k, args.flag_hw_pixfmt ); - test_vp9(&yuvs, width, height, bitrate_k, yuv_count); + [VP8, VP9].map(|c| test_vpx(c, &yuvs, width, height, bitrate_k, yuv_count)); #[cfg(feature = "hwcodec")] { use hwcodec::AVPixelFormat; @@ -57,7 +58,7 @@ fn main() { Pixfmt::I420 => AVPixelFormat::AV_PIX_FMT_YUV420P, Pixfmt::NV12 => AVPixelFormat::AV_PIX_FMT_NV12, }; - let yuvs = hw::vp9_yuv_to_hw_yuv(yuvs, width, height, hw_pixfmt); + let yuvs = hw::vpx_yuv_to_hw_yuv(yuvs, width, height, hw_pixfmt); hw::test(&yuvs, width, height, bitrate_k, yuv_count, hw_pixfmt); } } @@ -87,13 +88,20 @@ fn capture_yuv(yuv_count: usize) -> (Vec>, usize, usize) { } } -fn test_vp9(yuvs: &Vec>, width: usize, height: usize, bitrate_k: usize, yuv_count: usize) { +fn test_vpx( + codec_id: VpxVideoCodecId, + yuvs: &Vec>, + width: usize, + height: usize, + bitrate_k: usize, + yuv_count: usize, +) { let config = EncoderCfg::VPX(VpxEncoderConfig { width: width as _, height: height as _, timebase: [1, 1000], bitrate: bitrate_k as _, - codec: VpxVideoCodecId::VP9, + codec: codec_id, num_threads: (num_cpus::get() / 2) as _, }); let mut encoder = VpxEncoder::new(config).unwrap(); @@ -104,35 +112,43 @@ fn test_vp9(yuvs: &Vec>, width: usize, height: usize, bitrate_k: usize, .unwrap(); let _ = encoder.flush().unwrap(); } - println!("vp9 encode: {:?}", start.elapsed() / yuv_count as _); + println!( + "{:?} encode: {:?}", + codec_id, + start.elapsed() / yuv_count as _ + ); // prepare data separately - let mut vp9s = vec![]; + let mut vpxs = vec![]; let start = Instant::now(); for yuv in yuvs { for ref frame in encoder .encode(start.elapsed().as_millis() as _, yuv, STRIDE_ALIGN) .unwrap() { - vp9s.push(frame.data.to_vec()); + vpxs.push(frame.data.to_vec()); } for ref frame in encoder.flush().unwrap() { - vp9s.push(frame.data.to_vec()); + vpxs.push(frame.data.to_vec()); } } - assert_eq!(vp9s.len(), yuv_count); + assert_eq!(vpxs.len(), yuv_count); let mut decoder = VpxDecoder::new(VpxDecoderConfig { - codec: VpxVideoCodecId::VP9, + codec: codec_id, num_threads: (num_cpus::get() / 2) as _, }) .unwrap(); let start = Instant::now(); - for vp9 in vp9s { - let _ = decoder.decode(&vp9); + for vpx in vpxs { + let _ = decoder.decode(&vpx); let _ = decoder.flush(); } - println!("vp9 decode: {:?}", start.elapsed() / yuv_count as _); + println!( + "{:?} decode: {:?}", + codec_id, + start.elapsed() / yuv_count as _ + ); } #[cfg(feature = "hwcodec")] @@ -267,7 +283,7 @@ mod hw { Some(info.clone()) == best.h264 || Some(info.clone()) == best.h265 } - pub fn vp9_yuv_to_hw_yuv( + pub fn vpx_yuv_to_hw_yuv( yuvs: Vec>, width: usize, height: usize, diff --git a/libs/scrap/examples/capture_mag.rs b/libs/scrap/examples/capture_mag.rs index 81b2d8573..047020899 100644 --- a/libs/scrap/examples/capture_mag.rs +++ b/libs/scrap/examples/capture_mag.rs @@ -3,9 +3,9 @@ extern crate scrap; use std::fs::File; +use scrap::{i420_to_rgb, Display}; #[cfg(windows)] use scrap::{CapturerMag, TraitCapturer}; -use scrap::{i420_to_rgb, Display}; fn main() { let n = Display::all().unwrap().len(); diff --git a/libs/scrap/examples/record-screen.rs b/libs/scrap/examples/record-screen.rs index 5df97838e..10ad66e09 100644 --- a/libs/scrap/examples/record-screen.rs +++ b/libs/scrap/examples/record-screen.rs @@ -18,7 +18,7 @@ use webm::mux; use webm::mux::Track; use scrap::vpxcodec as vpx_encode; -use scrap::{TraitCapturer, Capturer, Display, STRIDE_ALIGN}; +use scrap::{Capturer, Display, TraitCapturer, STRIDE_ALIGN}; const USAGE: &'static str = " Simple WebM screen capture. diff --git a/libs/scrap/src/common/android.rs b/libs/scrap/src/common/android.rs index 8daf8e4bb..36d6a8a9b 100644 --- a/libs/scrap/src/common/android.rs +++ b/libs/scrap/src/common/android.rs @@ -50,6 +50,7 @@ impl crate::TraitCapturer for Capturer { pub enum Frame<'a> { RAW(&'a [u8]), + VP8(&'a [u8]), VP9(&'a [u8]), Empty, } diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 9e4b6fce4..a4b7c27f8 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -1,7 +1,6 @@ -use std::ops::{Deref, DerefMut}; -#[cfg(feature = "hwcodec")] use std::{ collections::HashMap, + ops::{Deref, DerefMut}, sync::{Arc, Mutex}, }; @@ -11,30 +10,31 @@ use crate::hwcodec::*; use crate::mediacodec::{ MediaCodecDecoder, MediaCodecDecoders, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT, }; -use crate::{vpxcodec::*, ImageFormat}; +use crate::{vpxcodec::*, CodecName, ImageFormat}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::sysinfo::{System, SystemExt}; use hbb_common::{ anyhow::anyhow, + config::PeerConfig, log, - message_proto::{video_frame, EncodedVideoFrames, Message, VideoCodecState}, + message_proto::{ + supported_decoding::PreferCodec, video_frame, EncodedVideoFrames, Message, + SupportedDecoding, SupportedEncoding, + }, ResultType, }; #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] -use hbb_common::{ - config::{Config2, PeerConfig}, - lazy_static, - message_proto::video_codec_state::PreferCodec, -}; +use hbb_common::{config::Config2, lazy_static}; -#[cfg(feature = "hwcodec")] lazy_static::lazy_static! { - static ref PEER_DECODER_STATES: Arc>> = Default::default(); + static ref PEER_DECODINGS: Arc>> = Default::default(); + static ref CODEC_NAME: Arc> = Arc::new(Mutex::new(CodecName::VP9)); } -const SCORE_VPX: i32 = 90; #[derive(Debug, Clone)] pub struct HwEncoderConfig { - pub codec_name: String, + pub name: String, pub width: usize, pub height: usize, pub bitrate: i32, @@ -58,10 +58,6 @@ pub trait EncoderApi { fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()>; } -pub struct DecoderCfg { - pub vpx: VpxDecoderConfig, -} - pub struct Encoder { pub codec: Box, } @@ -81,7 +77,8 @@ impl DerefMut for Encoder { } pub struct Decoder { - vpx: VpxDecoder, + vp8: VpxDecoder, + vp9: VpxDecoder, #[cfg(feature = "hwcodec")] hw: HwDecoders, #[cfg(feature = "hwcodec")] @@ -91,10 +88,10 @@ pub struct Decoder { } #[derive(Debug, Clone)] -pub enum EncoderUpdate { - State(VideoCodecState), +pub enum EncodingUpdate { + New(SupportedDecoding), Remove, - DisableHwIfNotExist, + NewOnlyVP9, } impl Encoder { @@ -111,7 +108,8 @@ impl Encoder { codec: Box::new(hw), }), Err(e) => { - check_config_process(true); + check_config_process(); + *CODEC_NAME.lock().unwrap() = CodecName::VP9; Err(e) } }, @@ -120,172 +118,158 @@ impl Encoder { } } - // TODO - pub fn update_video_encoder(id: i32, update: EncoderUpdate) { + pub fn update(id: i32, update: EncodingUpdate) { + let mut decodings = PEER_DECODINGS.lock().unwrap(); + match update { + EncodingUpdate::New(decoding) => { + decodings.insert(id, decoding); + } + EncodingUpdate::Remove => { + decodings.remove(&id); + } + EncodingUpdate::NewOnlyVP9 => { + decodings.insert( + id, + SupportedDecoding { + ability_vp9: 1, + ..Default::default() + }, + ); + } + } + + let vp8_useable = decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_vp8 > 0); + #[allow(unused_mut)] + let mut h264_name = None; + #[allow(unused_mut)] + let mut h265_name = None; #[cfg(feature = "hwcodec")] { - let mut states = PEER_DECODER_STATES.lock().unwrap(); - match update { - EncoderUpdate::State(state) => { - states.insert(id, state); - } - EncoderUpdate::Remove => { - states.remove(&id); - } - EncoderUpdate::DisableHwIfNotExist => { - if !states.contains_key(&id) { - states.insert(id, VideoCodecState::default()); - } - } - } - let name = HwEncoder::current_name(); - if states.len() > 0 { + if enable_hwcodec_option() { let best = HwEncoder::best(); - let enabled_h264 = best.h264.is_some() - && states.len() > 0 - && states.iter().all(|(_, s)| s.score_h264 > 0); - let enabled_h265 = best.h265.is_some() - && states.len() > 0 - && states.iter().all(|(_, s)| s.score_h265 > 0); - - // Preference first - let mut preference = PreferCodec::Auto; - let preferences: Vec<_> = states - .iter() - .filter(|(_, s)| { - s.prefer == PreferCodec::VPX.into() - || s.prefer == PreferCodec::H264.into() && enabled_h264 - || s.prefer == PreferCodec::H265.into() && enabled_h265 - }) - .map(|(_, s)| s.prefer) - .collect(); - if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) { - preference = preferences[0].enum_value_or(PreferCodec::Auto); + let h264_useable = + decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_h264 > 0); + let h265_useable = + decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_h265 > 0); + if h264_useable { + h264_name = best.h264.map_or(None, |c| Some(c.name)); } - - match preference { - PreferCodec::VPX => *name.lock().unwrap() = None, - PreferCodec::H264 => { - *name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name)) - } - PreferCodec::H265 => { - *name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name)) - } - PreferCodec::Auto => { - // score encoder - let mut score_vpx = SCORE_VPX; - let mut score_h264 = best.h264.as_ref().map_or(0, |c| c.score); - let mut score_h265 = best.h265.as_ref().map_or(0, |c| c.score); - - // score decoder - score_vpx += states.iter().map(|s| s.1.score_vpx).sum::(); - if enabled_h264 { - score_h264 += states.iter().map(|s| s.1.score_h264).sum::(); - } - if enabled_h265 { - score_h265 += states.iter().map(|s| s.1.score_h265).sum::(); - } - - if enabled_h265 && score_h265 >= score_vpx && score_h265 >= score_h264 { - *name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name)); - } else if enabled_h264 - && score_h264 >= score_vpx - && score_h264 >= score_h265 - { - *name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name)); - } else { - *name.lock().unwrap() = None; - } - } + if h265_useable { + h265_name = best.h265.map_or(None, |c| Some(c.name)); } - - log::info!( - "connection count:{}, used preference:{:?}, encoder:{:?}", - states.len(), - preference, - name.lock().unwrap() - ) - } else { - *name.lock().unwrap() = None; } } - #[cfg(not(feature = "hwcodec"))] - { - let _ = id; - let _ = update; + + let mut name = CODEC_NAME.lock().unwrap(); + let mut preference = PreferCodec::Auto; + let preferences: Vec<_> = decodings + .iter() + .filter(|(_, s)| { + s.prefer == PreferCodec::VP9.into() + || s.prefer == PreferCodec::VP8.into() && vp8_useable + || s.prefer == PreferCodec::H264.into() && h264_name.is_some() + || s.prefer == PreferCodec::H265.into() && h265_name.is_some() + }) + .map(|(_, s)| s.prefer) + .collect(); + if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) { + preference = preferences[0].enum_value_or(PreferCodec::Auto); } - } - #[inline] - pub fn current_hw_encoder_name() -> Option { - #[cfg(feature = "hwcodec")] - if enable_hwcodec_option() { - return HwEncoder::current_name().lock().unwrap().clone(); - } else { - return None; + + #[allow(unused_mut)] + let mut auto_codec = CodecName::VP9; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if vp8_useable && System::new_all().total_memory() <= 4 * 1024 * 1024 * 1024 { + // 4 Gb + auto_codec = CodecName::VP8 } - #[cfg(not(feature = "hwcodec"))] - return None; + + match preference { + PreferCodec::VP8 => *name = CodecName::VP8, + PreferCodec::VP9 => *name = CodecName::VP9, + PreferCodec::H264 => *name = h264_name.map_or(auto_codec, |c| CodecName::H264(c)), + PreferCodec::H265 => *name = h265_name.map_or(auto_codec, |c| CodecName::H265(c)), + PreferCodec::Auto => *name = auto_codec, + } + + log::info!( + "connection count:{}, used preference:{:?}, encoder:{:?}", + decodings.len(), + preference, + *name + ) } - pub fn supported_encoding() -> (bool, bool) { + #[inline] + pub fn negotiated_codec() -> CodecName { + CODEC_NAME.lock().unwrap().clone() + } + + pub fn supported_encoding() -> SupportedEncoding { + #[allow(unused_mut)] + let mut encoding = SupportedEncoding { + vp8: true, + ..Default::default() + }; #[cfg(feature = "hwcodec")] if enable_hwcodec_option() { let best = HwEncoder::best(); - ( - best.h264.as_ref().map_or(false, |c| c.score > 0), - best.h265.as_ref().map_or(false, |c| c.score > 0), - ) - } else { - (false, false) + encoding.h264 = best.h264.is_some(); + encoding.h265 = best.h265.is_some(); } - #[cfg(not(feature = "hwcodec"))] - (false, false) + encoding } } impl Decoder { - pub fn video_codec_state(_id: &str) -> VideoCodecState { + pub fn supported_decodings(id_for_perfer: Option<&str>) -> SupportedDecoding { + #[allow(unused_mut)] + let mut decoding = SupportedDecoding { + ability_vp8: 1, + ability_vp9: 1, + prefer: id_for_perfer + .map_or(PreferCodec::Auto, |id| Self::codec_preference(id)) + .into(), + ..Default::default() + }; #[cfg(feature = "hwcodec")] if enable_hwcodec_option() { let best = HwDecoder::best(); - return VideoCodecState { - score_vpx: SCORE_VPX, - score_h264: best.h264.map_or(0, |c| c.score), - score_h265: best.h265.map_or(0, |c| c.score), - prefer: Self::codec_preference(_id).into(), - ..Default::default() - }; + decoding.ability_h264 = if best.h264.is_some() { 1 } else { 0 }; + decoding.ability_h265 = if best.h265.is_some() { 1 } else { 0 }; } #[cfg(feature = "mediacodec")] if enable_hwcodec_option() { - let score_h264 = if H264_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) { - 92 - } else { - 0 - }; - let score_h265 = if H265_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) { - 94 - } else { - 0 - }; - return VideoCodecState { - score_vpx: SCORE_VPX, - score_h264, - score_h265, - prefer: Self::codec_preference(_id).into(), - ..Default::default() - }; - } - VideoCodecState { - score_vpx: SCORE_VPX, - ..Default::default() + decoding.ability_h264 = + if H264_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) { + 1 + } else { + 0 + }; + decoding.ability_h265 = + if H265_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) { + 1 + } else { + 0 + }; } + decoding } - pub fn new(config: DecoderCfg) -> Decoder { - let vpx = VpxDecoder::new(config.vpx).unwrap(); + pub fn new() -> Decoder { + let vp8 = VpxDecoder::new(VpxDecoderConfig { + codec: VpxVideoCodecId::VP8, + num_threads: (num_cpus::get() / 2) as _, + }) + .unwrap(); + let vp9 = VpxDecoder::new(VpxDecoderConfig { + codec: VpxVideoCodecId::VP9, + num_threads: (num_cpus::get() / 2) as _, + }) + .unwrap(); Decoder { - vpx, + vp8, + vp9, #[cfg(feature = "hwcodec")] hw: if enable_hwcodec_option() { HwDecoder::new_decoders() @@ -310,8 +294,11 @@ impl Decoder { rgb: &mut Vec, ) -> ResultType { match frame { + video_frame::Union::Vp8s(vp8s) => { + Decoder::handle_vpxs_video_frame(&mut self.vp8, vp8s, fmt, rgb) + } video_frame::Union::Vp9s(vp9s) => { - Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, fmt, rgb) + Decoder::handle_vpxs_video_frame(&mut self.vp9, vp9s, fmt, rgb) } #[cfg(feature = "hwcodec")] video_frame::Union::H264s(h264s) => { @@ -349,15 +336,15 @@ impl Decoder { } } - fn handle_vp9s_video_frame( + fn handle_vpxs_video_frame( decoder: &mut VpxDecoder, - vp9s: &EncodedVideoFrames, + vpxs: &EncodedVideoFrames, fmt: (ImageFormat, usize), rgb: &mut Vec, ) -> ResultType { let mut last_frame = Image::new(); - for vp9 in vp9s.frames.iter() { - for frame in decoder.decode(&vp9.data)? { + for vpx in vpxs.frames.iter() { + for frame in decoder.decode(&vpx.data)? { drop(last_frame); last_frame = frame; } @@ -408,14 +395,15 @@ impl Decoder { return Ok(false); } - #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] fn codec_preference(id: &str) -> PreferCodec { let codec = PeerConfig::load(id) .options .get("codec-preference") .map_or("".to_owned(), |c| c.to_owned()); - if codec == "vp9" { - PreferCodec::VPX + if codec == "vp8" { + PreferCodec::VP8 + } else if codec == "vp9" { + PreferCodec::VP9 } else if codec == "h264" { PreferCodec::H264 } else if codec == "h265" { diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 3c86af94e..8daa6551c 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -3,10 +3,11 @@ use crate::{ hw, ImageFormat, HW_STRIDE_ALIGN, }; use hbb_common::{ + allow_err, anyhow::{anyhow, Context}, bytes::Bytes, config::HwCodecConfig, - get_time, lazy_static, log, + log, message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}, ResultType, }; @@ -18,18 +19,13 @@ use hwcodec::{ Quality::{self, *}, RateControl::{self, *}, }; -use std::sync::{Arc, Mutex}; - -lazy_static::lazy_static! { - static ref HW_ENCODER_NAME: Arc>> = Default::default(); -} const CFG_KEY_ENCODER: &str = "bestHwEncoders"; const CFG_KEY_DECODER: &str = "bestHwDecoders"; const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_YUV420P; pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30]; -const DEFAULT_GOP: i32 = 60; +const DEFAULT_GOP: i32 = i32::MAX; const DEFAULT_HW_QUALITY: Quality = Quality_Default; const DEFAULT_RC: RateControl = RC_DEFAULT; @@ -48,7 +44,7 @@ impl EncoderApi for HwEncoder { match cfg { EncoderCfg::HW(config) => { let ctx = EncodeContext { - name: config.codec_name.clone(), + name: config.name.clone(), width: config.width as _, height: config.height as _, pixfmt: DEFAULT_PIXFMT, @@ -59,12 +55,12 @@ impl EncoderApi for HwEncoder { quality: DEFAULT_HW_QUALITY, rc: DEFAULT_RC, }; - let format = match Encoder::format_from_name(config.codec_name.clone()) { + let format = match Encoder::format_from_name(config.name.clone()) { Ok(format) => format, Err(_) => { return Err(anyhow!(format!( "failed to get format from name:{}", - config.codec_name + config.name ))) } }; @@ -107,7 +103,6 @@ impl EncoderApi for HwEncoder { DataFormat::H264 => vf.set_h264s(frames), DataFormat::H265 => vf.set_h265s(frames), } - vf.timestamp = get_time(); msg_out.set_video_frame(vf); Ok(msg_out) } else { @@ -133,10 +128,6 @@ impl HwEncoder { }) } - pub fn current_name() -> Arc>> { - HW_ENCODER_NAME.clone() - } - pub fn encode(&mut self, bgra: &[u8]) -> ResultType> { match self.pixfmt { AVPixelFormat::AV_PIX_FMT_YUV420P => hw::hw_bgra_to_i420( @@ -208,7 +199,7 @@ impl HwDecoder { } } if fail { - check_config_process(true); + check_config_process(); } HwDecoders { h264, h265 } } @@ -332,13 +323,11 @@ pub fn check_config() { log::error!("Failed to serialize codec info"); } -pub fn check_config_process(force_reset: bool) { +pub fn check_config_process() { use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; std::thread::spawn(move || { - if force_reset { - HwCodecConfig::remove(); - } + HwCodecConfig::remove(); if let Ok(exe) = std::env::current_exe() { if let Some(file_name) = exe.file_name().to_owned() { let s = System::new_all(); @@ -353,7 +342,21 @@ pub fn check_config_process(force_reset: bool) { let second = 3; std::thread::sleep(std::time::Duration::from_secs(second)); // kill: Different platforms have different results - child.kill().ok(); + allow_err!(child.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + match child.try_wait() { + Ok(Some(status)) => log::info!("Check hwcodec config, exit with: {status}"), + Ok(None) => { + log::info!( + "Check hwcodec config, status not ready yet, let's really wait" + ); + let res = child.wait(); + log::info!("Check hwcodec config, wait result: {res:?}"); + } + Err(e) => { + log::error!("Check hwcodec config, error attempting to wait: {e}") + } + } HwCodecConfig::refresh(); } } diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 9e15cf084..292f371c6 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -1,4 +1,5 @@ pub use self::vpxcodec::*; +use hbb_common::message_proto::{video_frame, VideoFrame}; cfg_if! { if #[cfg(quartz)] { @@ -63,6 +64,9 @@ pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> pub trait TraitCapturer { fn set_use_yuv(&mut self, use_yuv: bool); + + // We doesn't support + #[cfg(not(any(target_os = "ios")))] fn frame<'a>(&'a mut self, timeout: std::time::Duration) -> std::io::Result>; #[cfg(windows)] @@ -74,7 +78,7 @@ pub trait TraitCapturer { #[cfg(x11)] #[inline] pub fn is_x11() -> bool { - "x11" == hbb_common::platform::linux::get_display_server() + hbb_common::platform::linux::is_x11_or_headless() } #[cfg(x11)] @@ -92,3 +96,55 @@ pub fn is_cursor_embedded() -> bool { pub fn is_cursor_embedded() -> bool { false } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CodecName { + VP8, + VP9, + H264(String), + H265(String), +} + +#[derive(PartialEq, Debug, Clone)] +pub enum CodecFormat { + VP8, + VP9, + H264, + H265, + Unknown, +} + +impl From<&VideoFrame> for CodecFormat { + fn from(it: &VideoFrame) -> Self { + match it.union { + Some(video_frame::Union::Vp8s(_)) => CodecFormat::VP8, + Some(video_frame::Union::Vp9s(_)) => CodecFormat::VP9, + Some(video_frame::Union::H264s(_)) => CodecFormat::H264, + Some(video_frame::Union::H265s(_)) => CodecFormat::H265, + _ => CodecFormat::Unknown, + } + } +} + +impl From<&CodecName> for CodecFormat { + fn from(value: &CodecName) -> Self { + match value { + CodecName::VP8 => Self::VP8, + CodecName::VP9 => Self::VP9, + CodecName::H264(_) => Self::H264, + CodecName::H265(_) => Self::H265, + } + } +} + +impl ToString for CodecFormat { + fn to_string(&self) -> String { + match self { + CodecFormat::VP8 => "VP8".into(), + CodecFormat::VP9 => "VP9".into(), + CodecFormat::H264 => "H264".into(), + CodecFormat::H265 => "H265".into(), + CodecFormat::Unknown => "Unknow".into(), + } + } +} diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index 9f38f2d6a..9de70ae14 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -1,3 +1,4 @@ +use crate::CodecFormat; #[cfg(feature = "hwcodec")] use hbb_common::anyhow::anyhow; use hbb_common::{ @@ -21,13 +22,6 @@ use webm::mux::{self, Segment, Track, VideoTrack, Writer}; const MIN_SECS: u64 = 1; -#[derive(Debug, Clone, PartialEq)] -pub enum RecordCodecID { - VP9, - H264, - H265, -} - #[derive(Debug, Clone)] pub struct RecorderContext { pub server: bool, @@ -36,7 +30,7 @@ pub struct RecorderContext { pub filename: String, pub width: usize, pub height: usize, - pub codec_id: RecordCodecID, + pub format: CodecFormat, pub tx: Option>, } @@ -55,8 +49,9 @@ impl RecorderContext { } let file = if self.server { "s" } else { "c" }.to_string() + &self.id.clone() - + &chrono::Local::now().format("_%Y%m%d%H%M%S").to_string() - + if self.codec_id == RecordCodecID::VP9 { + + &chrono::Local::now().format("_%Y%m%d%H%M%S_").to_string() + + &self.format.to_string() + + if self.format == CodecFormat::VP9 || self.format == CodecFormat::VP8 { ".webm" } else { ".mp4" @@ -107,8 +102,8 @@ impl DerefMut for Recorder { impl Recorder { pub fn new(mut ctx: RecorderContext) -> ResultType { ctx.set_filename()?; - let recorder = match ctx.codec_id { - RecordCodecID::VP9 => Recorder { + let recorder = match ctx.format { + CodecFormat::VP8 | CodecFormat::VP9 => Recorder { inner: Box::new(WebmRecorder::new(ctx.clone())?), ctx, }, @@ -126,8 +121,8 @@ impl Recorder { fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> { ctx.set_filename()?; - self.inner = match ctx.codec_id { - RecordCodecID::VP9 => Box::new(WebmRecorder::new(ctx.clone())?), + self.inner = match ctx.format { + CodecFormat::VP8 | CodecFormat::VP9 => Box::new(WebmRecorder::new(ctx.clone())?), #[cfg(feature = "hwcodec")] _ => Box::new(HwRecorder::new(ctx.clone())?), #[cfg(not(feature = "hwcodec"))] @@ -148,10 +143,19 @@ impl Recorder { pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> { match frame { - video_frame::Union::Vp9s(vp9s) => { - if self.ctx.codec_id != RecordCodecID::VP9 { + video_frame::Union::Vp8s(vp8s) => { + if self.ctx.format != CodecFormat::VP8 { self.change(RecorderContext { - codec_id: RecordCodecID::VP9, + format: CodecFormat::VP8, + ..self.ctx.clone() + })?; + } + vp8s.frames.iter().map(|f| self.write_video(f)).count(); + } + video_frame::Union::Vp9s(vp9s) => { + if self.ctx.format != CodecFormat::VP9 { + self.change(RecorderContext { + format: CodecFormat::VP9, ..self.ctx.clone() })?; } @@ -159,25 +163,25 @@ impl Recorder { } #[cfg(feature = "hwcodec")] video_frame::Union::H264s(h264s) => { - if self.ctx.codec_id != RecordCodecID::H264 { + if self.ctx.format != CodecFormat::H264 { self.change(RecorderContext { - codec_id: RecordCodecID::H264, + format: CodecFormat::H264, ..self.ctx.clone() })?; } - if self.ctx.codec_id == RecordCodecID::H264 { + if self.ctx.format == CodecFormat::H264 { h264s.frames.iter().map(|f| self.write_video(f)).count(); } } #[cfg(feature = "hwcodec")] video_frame::Union::H265s(h265s) => { - if self.ctx.codec_id != RecordCodecID::H265 { + if self.ctx.format != CodecFormat::H265 { self.change(RecorderContext { - codec_id: RecordCodecID::H265, + format: CodecFormat::H265, ..self.ctx.clone() })?; } - if self.ctx.codec_id == RecordCodecID::H265 { + if self.ctx.format == CodecFormat::H265 { h265s.frames.iter().map(|f| self.write_video(f)).count(); } } @@ -221,7 +225,11 @@ impl RecorderApi for WebmRecorder { ctx.width as _, ctx.height as _, None, - mux::VideoCodecId::VP9, + if ctx.format == CodecFormat::VP9 { + mux::VideoCodecId::VP9 + } else { + mux::VideoCodecId::VP8 + }, ); Ok(WebmRecorder { vt, @@ -279,7 +287,7 @@ impl RecorderApi for HwRecorder { filename: ctx.filename.clone(), width: ctx.width, height: ctx.height, - is265: ctx.codec_id == RecordCodecID::H265, + is265: ctx.format == CodecFormat::H265, framerate: crate::hwcodec::DEFAULT_TIME_BASE[1] as _, }) .map_err(|_| anyhow!("Failed to create hardware muxer"))?; diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs index b91871c8f..4820ea171 100644 --- a/libs/scrap/src/common/vpxcodec.rs +++ b/libs/scrap/src/common/vpxcodec.rs @@ -4,7 +4,7 @@ use hbb_common::anyhow::{anyhow, Context}; use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}; -use hbb_common::{get_time, ResultType}; +use hbb_common::ResultType; use crate::STRIDE_ALIGN; use crate::{codec::EncoderApi, ImageFormat}; @@ -30,6 +30,7 @@ pub struct VpxEncoder { ctx: vpx_codec_ctx_t, width: usize, height: usize, + id: VpxVideoCodecId, } pub struct VpxDecoder { @@ -97,15 +98,10 @@ impl EncoderApi for VpxEncoder { { match cfg { crate::codec::EncoderCfg::VPX(config) => { - let i; - if cfg!(feature = "VP8") { - i = match config.codec { - VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()), - VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()), - }; - } else { - i = call_vpx_ptr!(vpx_codec_vp9_cx()); - } + let i = match config.codec { + VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()), + VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()), + }; let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0)); @@ -187,12 +183,17 @@ impl EncoderApi for VpxEncoder { VP9E_SET_TILE_COLUMNS as _, 4 as c_int )); + } else if config.codec == VpxVideoCodecId::VP8 { + // https://github.com/webmproject/libvpx/blob/972149cafeb71d6f08df89e91a0130d6a38c4b15/vpx/vp8cx.h#L172 + // https://groups.google.com/a/webmproject.org/g/webm-discuss/c/DJhSrmfQ61M + call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, 12,)); } Ok(Self { ctx, width: config.width as _, height: config.height as _, + id: config.codec, }) } _ => Err(anyhow!("encoder type mismatch")), @@ -213,7 +214,7 @@ impl EncoderApi for VpxEncoder { // to-do: flush periodically, e.g. 1 second if frames.len() > 0 { - Ok(VpxEncoder::create_msg(frames)) + Ok(VpxEncoder::create_msg(self.id, frames)) } else { Err(anyhow!("no valid frame")) } @@ -280,14 +281,17 @@ impl VpxEncoder { } #[inline] - fn create_msg(vp9s: Vec) -> Message { + pub fn create_msg(codec_id: VpxVideoCodecId, frames: Vec) -> Message { let mut msg_out = Message::new(); let mut vf = VideoFrame::new(); - vf.set_vp9s(EncodedVideoFrames { - frames: vp9s.into(), + let vpxs = EncodedVideoFrames { + frames: frames.into(), ..Default::default() - }); - vf.timestamp = get_time(); + }; + match codec_id { + VpxVideoCodecId::VP8 => vf.set_vp8s(vpxs), + VpxVideoCodecId::VP9 => vf.set_vp9s(vpxs), + } msg_out.set_video_frame(vf); msg_out } @@ -383,15 +387,10 @@ impl VpxDecoder { pub fn new(config: VpxDecoderConfig) -> Result { // This is sound because `vpx_codec_ctx` is a repr(C) struct without any field that can // cause UB if uninitialized. - let i; - if cfg!(feature = "VP8") { - i = match config.codec { - VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()), - VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()), - }; - } else { - i = call_vpx_ptr!(vpx_codec_vp9_dx()); - } + let i = match config.codec { + VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_dx()), + VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_dx()), + }; let mut ctx = Default::default(); let cfg = vpx_codec_dec_cfg_t { threads: if config.num_threads == 0 { diff --git a/libs/scrap/src/wayland.rs b/libs/scrap/src/wayland.rs index 82b219306..a5c4a3903 100644 --- a/libs/scrap/src/wayland.rs +++ b/libs/scrap/src/wayland.rs @@ -1,3 +1,3 @@ +pub mod capturable; pub mod pipewire; mod pipewire_dbus; -pub mod capturable; diff --git a/libs/simple_rc/Cargo.toml b/libs/simple_rc/Cargo.toml deleted file mode 100644 index 89304524d..000000000 --- a/libs/simple_rc/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "simple_rc" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -serde_derive = "1.0" -serde = "1.0" -walkdir = "2" -confy = { git = "https://github.com/open-trade/confy" } -hbb_common = { path = "../hbb_common" } diff --git a/libs/simple_rc/examples/generate.rs b/libs/simple_rc/examples/generate.rs deleted file mode 100644 index 2de39961a..000000000 --- a/libs/simple_rc/examples/generate.rs +++ /dev/null @@ -1,23 +0,0 @@ -extern crate simple_rc; - -use simple_rc::*; - -fn main() { - { - const CONF_FILE: &str = "simple_rc.toml"; - generate(CONF_FILE).unwrap(); - } - - { - generate_with_conf(&Config { - outfile: "src/rc.rs".to_owned(), - confs: vec![ConfigItem { - inc: "D:/projects/windows/RustDeskTempTopMostWindow/x64/Release/xxx".to_owned(), - // exc: vec!["*.dll".to_owned(), "*.exe".to_owned()], - exc: vec![], - suppressed_front: "D:/projects/windows".to_owned(), - }], - }) - .unwrap(); - } -} diff --git a/libs/simple_rc/simple_rc.toml b/libs/simple_rc/simple_rc.toml deleted file mode 100644 index bef976967..000000000 --- a/libs/simple_rc/simple_rc.toml +++ /dev/null @@ -1,12 +0,0 @@ -# The output source file -outfile = "src/rc.rs" - -# The resource config list. -[[confs]] -# The file or director to integrate. -inc = "D:/projects/windows/RustDeskTempTopMostWindow/x64/Release/xxx" -# The exclusions. -exc = ["*.dll", "*.exe"] -# The front path that will ignore for extracting. -# The following config will make base output path to be "RustDeskTempTopMostWindow/x64/Release/xxx". -suppressed_front = "D:/projects/windows" diff --git a/libs/simple_rc/src/lib.rs b/libs/simple_rc/src/lib.rs deleted file mode 100644 index e59e0493f..000000000 --- a/libs/simple_rc/src/lib.rs +++ /dev/null @@ -1,208 +0,0 @@ -use hbb_common::{bail, ResultType}; -use serde_derive::{Deserialize, Serialize}; -use std::{collections::HashMap, fs::File, io::prelude::*, path::Path}; -use walkdir::WalkDir; - -//mod rc; - -#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)] -pub struct ConfigItem { - // include directory or file - pub inc: String, - // exclude files - pub exc: Vec, - // out_path = origin_path - suppressed_front - pub suppressed_front: String, -} - -#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)] -pub struct Config { - // output source file - pub outfile: String, - // config items - pub confs: Vec, -} - -pub fn get_outin_files<'a>(item: &'a ConfigItem) -> ResultType> { - let mut outin_filemap = HashMap::new(); - - for entry in WalkDir::new(&item.inc).follow_links(true) { - let path = entry?.into_path(); - if path.is_file() { - let mut exclude = false; - for excfile in item.exc.iter() { - if excfile.starts_with("*.") { - if let Some(ext) = path.extension().and_then(|x| x.to_str()) { - if excfile.ends_with(&format!(".{}", ext)) { - exclude = true; - break; - } - } - } else { - if path.ends_with(Path::new(excfile)) { - exclude = true; - break; - } - } - } - if exclude { - continue; - } - - let mut suppressed_front = item.suppressed_front.clone(); - if !suppressed_front.is_empty() && suppressed_front.ends_with('/') { - suppressed_front.push('/'); - } - let outpath = path.strip_prefix(Path::new(&suppressed_front))?; - let outfile = if outpath.is_absolute() { - match outpath - .file_name() - .and_then(|f| f.to_str()) - .map(|f| f.to_string()) - { - None => { - bail!("Failed to get filename of {}", outpath.display()); - } - Some(s) => s, - } - } else { - match outpath.to_str() { - None => { - bail!("Failed to convert {} to string", outpath.display()); - } - // Simple replace \ to / here. - // A better way is to use lib [path-slash](https://github.com/rhysd/path-slash) - Some(s) => s.to_string().replace("\\", "/"), - } - }; - let infile = match path.canonicalize()?.to_str() { - None => { - bail!("Failed to get file path of {}", path.display()); - } - Some(s) => s.to_string(), - }; - if let Some(_) = outin_filemap.insert(outfile.clone(), infile) { - bail!("outfile {} is set before", outfile); - } - } - } - - Ok(outin_filemap) -} - -pub fn generate(conf_file: &str) -> ResultType<()> { - let conf = confy::load_path(conf_file)?; - generate_with_conf(&conf)?; - Ok(()) -} - -pub fn generate_with_conf<'a>(conf: &'a Config) -> ResultType<()> { - let mut outfile = File::create(&conf.outfile)?; - - outfile.write( - br##"use hbb_common::{bail, ResultType}; -use std::{ - fs::{self, File}, - io::prelude::*, - path::Path, -}; - -"##, - )?; - - outfile.write(b"#[allow(dead_code)]\n")?; - outfile.write(b"pub fn extract_resources(root_path: &str) -> ResultType<()> {\n")?; - outfile.write(b" let mut resources: Vec<(&str, &[u8])> = Vec::new();\n")?; - - let mut outin_files = HashMap::new(); - for item in conf.confs.iter() { - for (o, i) in get_outin_files(item)?.into_iter() { - if let Some(_) = outin_files.insert(o.clone(), i) { - bail!("outfile {} is set before", o); - } - } - } - - let mut count = 1; - for (o, i) in outin_files.iter() { - let mut infile = File::open(&i)?; - let mut buffer = Vec::::new(); - infile.read_to_end(&mut buffer)?; - - let var_outfile = format!("outfile_{}", count); - let var_outdata = format!("outdata_{}", count); - - write!(outfile, " let {} = \"{}\";\n", var_outfile, o)?; - write!(outfile, " let {}: &[u8] = &[\n ", var_outdata)?; - - let mut line_num = 20; - for v in buffer { - if line_num == 0 { - write!(outfile, "\n ")?; - line_num = 20; - } - write!(outfile, "{:#04x}, ", v)?; - line_num -= 1; - } - write!(outfile, "\n ];\n")?; - - write!( - outfile, - " resources.push(({}, &{}));\n", - var_outfile, var_outdata - )?; - - count += 1; - } - - outfile.write(b" do_extract(root_path, resources)?;\n")?; - outfile.write(b" Ok(())\n")?; - outfile.write(b"}\n")?; - - outfile.write( - br##" -#[allow(dead_code)] -fn do_extract(root_path: &str, resources: Vec<(&str, &[u8])>) -> ResultType<()> { - let mut root_path = root_path.replace("\\", "/"); - if !root_path.ends_with('/') { - root_path.push('/'); - } - let root_path = Path::new(&root_path); - for (outfile, data) in resources { - let outfile_path = root_path.join(outfile); - match outfile_path.parent().and_then(|p| p.to_str()) { - None => { - bail!("Failed to get parent of {}", outfile_path.display()); - } - Some(p) => { - fs::create_dir_all(p)?; - let mut of = File::create(outfile_path)?; - of.write_all(data)?; - of.flush()?; - } - } - } - Ok(()) -} -"##, - )?; - - outfile.flush()?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } - - // #[test] - // fn test_extract() { - // use super::*; - // rc::extract_resources("D:").unwrap(); - // } -} diff --git a/libs/virtual_display/Cargo.toml b/libs/virtual_display/Cargo.toml index c700bd12a..2559ffbc0 100644 --- a/libs/virtual_display/Cargo.toml +++ b/libs/virtual_display/Cargo.toml @@ -7,5 +7,4 @@ edition = "2021" [dependencies] lazy_static = "1.4" -libloading = "0.7" -hbb_common = { path = "../hbb_common" } +hbb_common = { path = "../hbb_common" } \ No newline at end of file diff --git a/libs/virtual_display/dylib/src/lib.rs b/libs/virtual_display/dylib/src/lib.rs index 4a95e3461..3b83d297c 100644 --- a/libs/virtual_display/dylib/src/lib.rs +++ b/libs/virtual_display/dylib/src/lib.rs @@ -2,18 +2,21 @@ pub mod win10; use hbb_common::{bail, lazy_static, ResultType}; -use std::{path::Path, sync::Mutex}; +use std::path::Path; +#[cfg(windows)] +use std::sync::Mutex; + +#[cfg(windows)] lazy_static::lazy_static! { // If device is uninstalled though "Device Manager" Window. // Rustdesk is unable to handle device any more... static ref H_SW_DEVICE: Mutex = Mutex::new(0); - static ref MONITOR_PLUGIN: Mutex> = Mutex::new(Vec::new()); } #[no_mangle] #[cfg(windows)] -pub fn get_dirver_install_path() -> &'static str { +pub fn get_driver_install_path() -> &'static str { win10::DRIVER_INSTALL_PATH } @@ -137,68 +140,48 @@ pub fn close_device() { unsafe { win10::idd::DeviceClose(*H_SW_DEVICE.lock().unwrap() as win10::idd::HSWDEVICE); *H_SW_DEVICE.lock().unwrap() = 0; - MONITOR_PLUGIN.lock().unwrap().clear(); } } #[no_mangle] -pub fn plug_in_monitor() -> ResultType<()> { +pub fn plug_in_monitor(_monitor_index: u32, _edid: u32, _retries: u32) -> ResultType<()> { #[cfg(windows)] unsafe { - let monitor_index = 0 as u32; - let mut plug_in_monitors = MONITOR_PLUGIN.lock().unwrap(); - for i in 0..plug_in_monitors.len() { - if let Some(d) = plug_in_monitors.get(i) { - if *d == monitor_index { - return Ok(()); - } - }; - } - if win10::idd::MonitorPlugIn(monitor_index, 0, 30) == win10::idd::FALSE { - bail!("{}", win10::get_last_msg()?); - } - (*plug_in_monitors).push(monitor_index); - } - Ok(()) -} - -#[no_mangle] -pub fn plug_out_monitor() -> ResultType<()> { - #[cfg(windows)] - unsafe { - let monitor_index = 0 as u32; - if win10::idd::MonitorPlugOut(monitor_index) == win10::idd::FALSE { - bail!("{}", win10::get_last_msg()?); - } - let mut plug_in_monitors = MONITOR_PLUGIN.lock().unwrap(); - for i in 0..plug_in_monitors.len() { - if let Some(d) = plug_in_monitors.get(i) { - if *d == monitor_index { - plug_in_monitors.remove(i); - break; - } - }; - } - } - Ok(()) -} - -#[no_mangle] -pub fn update_monitor_modes() -> ResultType<()> { - #[cfg(windows)] - unsafe { - let monitor_index = 0 as u32; - let mut modes = vec![win10::idd::MonitorMode { - width: 1920, - height: 1080, - sync: 60, - }]; - if win10::idd::FALSE - == win10::idd::MonitorModesUpdate( - monitor_index as win10::idd::UINT, - modes.len() as win10::idd::UINT, - modes.as_mut_ptr(), - ) + if win10::idd::MonitorPlugIn(_monitor_index as _, _edid as _, _retries as _) + == win10::idd::FALSE + { + bail!("{}", win10::get_last_msg()?); + } + } + Ok(()) +} + +#[no_mangle] +pub fn plug_out_monitor(_monitor_index: u32) -> ResultType<()> { + #[cfg(windows)] + unsafe { + if win10::idd::MonitorPlugOut(_monitor_index) == win10::idd::FALSE { + bail!("{}", win10::get_last_msg()?); + } + } + Ok(()) +} + +#[cfg(windows)] +type PMonitorMode = win10::idd::PMonitorMode; +#[cfg(not(windows))] +type PMonitorMode = *mut std::ffi::c_void; + +#[no_mangle] +pub fn update_monitor_modes( + _monitor_index: u32, + _mode_count: u32, + _modes: PMonitorMode, +) -> ResultType<()> { + #[cfg(windows)] + unsafe { + if win10::idd::FALSE + == win10::idd::MonitorModesUpdate(_monitor_index as _, _mode_count as _, _modes) { bail!("{}", win10::get_last_msg()?); } diff --git a/libs/virtual_display/dylib/src/win10/IddController.c b/libs/virtual_display/dylib/src/win10/IddController.c index c1faccfc2..6c240657a 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.c +++ b/libs/virtual_display/dylib/src/win10/IddController.c @@ -196,7 +196,7 @@ BOOL DeviceCreate(PHSWDEVICE hSwDevice) } if (created == TRUE) { - SetLastMsg("Device is created before, please uninstall it first\n"); + SetLastMsg("Device is already created, please destroy it first\n"); if (g_printMsg) { printf(g_lastMsg); @@ -288,7 +288,7 @@ BOOL MonitorPlugIn(UINT index, UINT edid, INT retries) if (retries < 0) { - SetLastMsg("invalid tries %d\n", retries); + SetLastMsg("Invalid tries %d\n", retries); if (g_printMsg) { printf(g_lastMsg); diff --git a/libs/virtual_display/examples/virtual_display_1.rs b/libs/virtual_display/examples/virtual_display_1.rs index 31fdbe06e..e5e1ae554 100644 --- a/libs/virtual_display/examples/virtual_display_1.rs +++ b/libs/virtual_display/examples/virtual_display_1.rs @@ -3,7 +3,7 @@ use virtual_display; fn prompt_input() -> u8 { println!("Press key execute:"); - println!(" 1. 'x' 1. exit"); + println!(" 1. 'q' 1. quit"); println!(" 2. 'i' 2. install or update driver"); println!(" 3. 'u' 3. uninstall driver"); println!(" 4. 'c' 4. create device"); @@ -18,18 +18,18 @@ fn prompt_input() -> u8 { .unwrap_or(0) } -fn plug_in() { +fn plug_in(monitor_index: u32) { println!("Plug in monitor begin"); - if let Err(e) = virtual_display::plug_in_monitor() { + if let Err(e) = virtual_display::plug_in_monitor(monitor_index as _) { println!("{}", e); } else { println!("Plug in monitor done"); } } -fn plug_out() { +fn plug_out(monitor_index: u32) { println!("Plug out monitor begin"); - if let Err(e) = virtual_display::plug_out_monitor() { + if let Err(e) = virtual_display::plug_out_monitor(monitor_index as _) { println!("{}", e); } else { println!("Plug out monitor done"); @@ -38,8 +38,9 @@ fn plug_out() { fn main() { loop { - match prompt_input() as char { - 'x' => break, + let chr = prompt_input(); + match chr as char { + 'q' => break, 'i' => { println!("Install or update driver begin"); let mut reboot_required = false; @@ -81,8 +82,12 @@ fn main() { virtual_display::close_device(); println!("Close device done"); } - '1' => plug_in(), - '4' => plug_out(), + '1' => plug_in(0), + '2' => plug_in(1), + '3' => plug_in(2), + '4' => plug_out(0), + '5' => plug_out(1), + '6' => plug_out(2), _ => {} } } diff --git a/libs/virtual_display/src/lib.rs b/libs/virtual_display/src/lib.rs index cd9402c69..578ffa2e9 100644 --- a/libs/virtual_display/src/lib.rs +++ b/libs/virtual_display/src/lib.rs @@ -1,12 +1,96 @@ -use hbb_common::{bail, ResultType}; -use std::sync::{Arc, Mutex}; +use hbb_common::{anyhow, dlopen::symbor::Library, log, ResultType}; +use std::{ + collections::HashSet, + sync::{Arc, Mutex}, +}; const LIB_NAME_VIRTUAL_DISPLAY: &str = "dylib_virtual_display"; +pub type DWORD = ::std::os::raw::c_ulong; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct _MonitorMode { + pub width: DWORD, + pub height: DWORD, + pub sync: DWORD, +} +pub type MonitorMode = _MonitorMode; +pub type PMonitorMode = *mut MonitorMode; + +pub type GetDriverInstallPath = fn() -> &'static str; +pub type IsDeviceCreated = fn() -> bool; +pub type CloseDevice = fn(); +pub type DownLoadDriver = fn() -> ResultType<()>; +pub type CreateDevice = fn() -> ResultType<()>; +pub type InstallUpdateDriver = fn(&mut bool) -> ResultType<()>; +pub type UninstallDriver = fn(&mut bool) -> ResultType<()>; +pub type PlugInMonitor = fn(u32) -> ResultType<()>; +pub type PlugOutMonitor = fn(u32) -> ResultType<()>; +pub type UpdateMonitorModes = fn(u32, u32, PMonitorMode) -> ResultType<()>; + +macro_rules! make_lib_wrapper { + ($($field:ident : $tp:ty),+) => { + struct LibWrapper { + _lib: Option, + $($field: Option<$tp>),+ + } + + impl LibWrapper { + fn new() -> Self { + let lib = match Library::open(get_lib_name()) { + Ok(lib) => Some(lib), + Err(e) => { + log::warn!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e); + None + } + }; + + $(let $field = if let Some(lib) = &lib { + match unsafe { lib.symbol::<$tp>(stringify!($field)) } { + Ok(m) => { + log::info!("method found {}", stringify!($field)); + Some(*m) + }, + Err(e) => { + log::warn!("Failed to load func {}, {}", stringify!($field), e); + None + } + } + } else { + None + };)+ + + Self { + _lib: lib, + $( $field ),+ + } + } + } + + impl Default for LibWrapper { + fn default() -> Self { + Self::new() + } + } + } +} + +make_lib_wrapper!( + get_driver_install_path: GetDriverInstallPath, + is_device_created: IsDeviceCreated, + close_device: CloseDevice, + download_driver: DownLoadDriver, + create_device: CreateDevice, + install_update_driver: InstallUpdateDriver, + uninstall_driver: UninstallDriver, + plug_in_monitor: PlugInMonitor, + plug_out_monitor: PlugOutMonitor, + update_monitor_modes: UpdateMonitorModes +); + lazy_static::lazy_static! { - static ref LIB_VIRTUAL_DISPLAY: Arc>> = { - Arc::new(Mutex::new(unsafe { libloading::Library::new(get_lib_name()) })) - }; + static ref LIB_WRAPPER: Arc> = Default::default(); + static ref MONITOR_INDICES: Mutex> = Mutex::new(HashSet::new()); } #[cfg(target_os = "windows")] @@ -24,102 +108,90 @@ fn get_lib_name() -> String { format!("lib{}.dylib", LIB_NAME_VIRTUAL_DISPLAY) } -fn try_reload_lib() { - let mut lock = LIB_VIRTUAL_DISPLAY.lock().unwrap(); - if lock.is_err() { - *lock = unsafe { libloading::Library::new(get_lib_name()) }; - } -} - #[cfg(windows)] -pub fn get_dirver_install_path() -> ResultType<&'static str> { - try_reload_lib(); - match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { - Ok(lib) => unsafe { - match lib.get:: &'static str>>(b"get_dirver_install_path") { - Ok(func) => Ok(func()), - Err(e) => bail!("Failed to load func get_dirver_install_path, {}", e), - } - }, - Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), - } +pub fn get_driver_install_path() -> Option<&'static str> { + Some(LIB_WRAPPER.lock().unwrap().get_driver_install_path?()) } pub fn is_device_created() -> bool { - try_reload_lib(); - match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { - Ok(lib) => unsafe { - match lib.get:: bool>>(b"is_device_created") { - Ok(func) => func(), - Err(..) => false, - } - }, - Err(..) => false, - } + LIB_WRAPPER + .lock() + .unwrap() + .is_device_created + .map(|f| f()) + .unwrap_or(false) } pub fn close_device() { - try_reload_lib(); - match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { - Ok(lib) => unsafe { - match lib.get::>(b"close_device") { - Ok(func) => func(), - Err(..) => {} - } - }, - Err(..) => {} - } + let _r = LIB_WRAPPER.lock().unwrap().close_device.map(|f| f()); } -macro_rules! def_func_result { - ($func:ident, $name: tt) => { - pub fn $func() -> ResultType<()> { - try_reload_lib(); - match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { - Ok(lib) => unsafe { - match lib.get:: ResultType<()>>>($name.as_bytes()) { - Ok(func) => func(), - Err(e) => bail!("Failed to load func {}, {}", $name, e), - } - }, - Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), - } - } - }; +pub fn download_driver() -> ResultType<()> { + LIB_WRAPPER + .lock() + .unwrap() + .download_driver + .ok_or(anyhow::Error::msg("download_driver method not found"))?() +} + +pub fn create_device() -> ResultType<()> { + LIB_WRAPPER + .lock() + .unwrap() + .create_device + .ok_or(anyhow::Error::msg("create_device method not found"))?() } pub fn install_update_driver(reboot_required: &mut bool) -> ResultType<()> { - try_reload_lib(); - match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { - Ok(lib) => unsafe { - match lib.get:: ResultType<()>>>( - b"install_update_driver", - ) { - Ok(func) => func(reboot_required), - Err(e) => bail!("Failed to load func install_update_driver, {}", e), - } - }, - Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), - } + LIB_WRAPPER + .lock() + .unwrap() + .install_update_driver + .ok_or(anyhow::Error::msg("install_update_driver method not found"))?(reboot_required) } pub fn uninstall_driver(reboot_required: &mut bool) -> ResultType<()> { - try_reload_lib(); - match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { - Ok(lib) => unsafe { - match lib - .get:: ResultType<()>>>(b"uninstall_driver") - { - Ok(func) => func(reboot_required), - Err(e) => bail!("Failed to load func uninstall_driver, {}", e), - } - }, - Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), - } + LIB_WRAPPER + .lock() + .unwrap() + .uninstall_driver + .ok_or(anyhow::Error::msg("uninstall_driver method not found"))?(reboot_required) } -def_func_result!(download_driver, "download_driver"); -def_func_result!(create_device, "create_device"); -def_func_result!(plug_in_monitor, "plug_in_monitor"); -def_func_result!(plug_out_monitor, "plug_out_monitor"); -def_func_result!(update_monitor_modes, "update_monitor_modes"); +#[cfg(windows)] +pub fn plug_in_monitor(monitor_index: u32) -> ResultType<()> { + let mut lock = MONITOR_INDICES.lock().unwrap(); + if lock.contains(&monitor_index) { + return Ok(()); + } + let f = LIB_WRAPPER + .lock() + .unwrap() + .plug_in_monitor + .ok_or(anyhow::Error::msg("plug_in_monitor method not found"))?; + f(monitor_index)?; + lock.insert(monitor_index); + Ok(()) +} + +#[cfg(windows)] +pub fn plug_out_monitor(monitor_index: u32) -> ResultType<()> { + let f = LIB_WRAPPER + .lock() + .unwrap() + .plug_out_monitor + .ok_or(anyhow::Error::msg("plug_out_monitor method not found"))?; + f(monitor_index)?; + MONITOR_INDICES.lock().unwrap().remove(&monitor_index); + Ok(()) +} + +#[cfg(windows)] +pub fn update_monitor_modes(monitor_index: u32, modes: &[MonitorMode]) -> ResultType<()> { + let f = LIB_WRAPPER + .lock() + .unwrap() + .update_monitor_modes + .ok_or(anyhow::Error::msg("update_monitor_modes method not found"))?; + f(monitor_index, modes.len() as _, modes.as_ptr() as _) +} diff --git a/res/PKGBUILD b/res/PKGBUILD index cff61516f..559f1c025 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -7,7 +7,7 @@ arch=('x86_64') url="" license=('AGPL-3.0') groups=() -depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'curl' 'libva' 'libvdpau' 'libappindicator-gtk3') +depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'curl' 'libva' 'libvdpau' 'libappindicator-gtk3' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire') makedepends=() checkdepends=() optdepends=() diff --git a/res/pam.d/rustdesk.debian b/res/pam.d/rustdesk.debian new file mode 100644 index 000000000..789ce8f7c --- /dev/null +++ b/res/pam.d/rustdesk.debian @@ -0,0 +1,5 @@ +#%PAM-1.0 +@include common-auth +@include common-account +@include common-session +@include common-password diff --git a/res/pam.d/rustdesk.suse b/res/pam.d/rustdesk.suse new file mode 100644 index 000000000..a7c7836ce --- /dev/null +++ b/res/pam.d/rustdesk.suse @@ -0,0 +1,5 @@ +#%PAM-1.0 +auth include common-auth +account include common-account +session include common-session +password include common-password diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 77c28a94e..cdee3d901 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -3,7 +3,7 @@ Version: 1.2.0 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libappindicator-gtk3 libvdpau1 libva2 +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libappindicator-gtk3 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) %description diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 6124cbb70..d81bb4b89 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -3,7 +3,7 @@ Version: 1.2.0 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator-gtk3 libvdpau libva +Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator-gtk3 libvdpau libva pam gstreamer1-plugins-base Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) %description diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec index 5d03d9c8a..496e47b19 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -3,7 +3,7 @@ Version: 1.1.9 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire %description The best open-source remote desktop client software, written in Rust. diff --git a/res/rpm.spec b/res/rpm.spec index b2a3e27e1..988b8270b 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -3,7 +3,7 @@ Version: 1.2.0 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator libvdpau1 libva2 +Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator libvdpau1 libva2 pam gstreamer1-plugins-base %description The best open-source remote desktop client software, written in Rust. diff --git a/res/startwm.sh b/res/startwm.sh new file mode 100755 index 000000000..7cdaf07ce --- /dev/null +++ b/res/startwm.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +# This script is derived from https://github.com/neutrinolabs/xrdp/sesman/startwm.sh. + +# +# This script is an example. You might need to edit this script +# depending on your distro if it doesn't work for you. +# +# Uncomment the following line for debug: +# exec xterm + + +# Execution sequence for interactive login shell - pseudocode +# +# IF /etc/profile is readable THEN +# execute ~/.bash_profile +# END IF +# IF ~/.bash_profile is readable THEN +# execute ~/.bash_profile +# ELSE +# IF ~/.bash_login is readable THEN +# execute ~/.bash_login +# ELSE +# IF ~/.profile is readable THEN +# execute ~/.profile +# END IF +# END IF +# END IF +pre_start() +{ + if [ -r /etc/profile ]; then + . /etc/profile + fi + if [ -r ~/.bash_profile ]; then + . ~/.bash_profile + else + if [ -r ~/.bash_login ]; then + . ~/.bash_login + else + if [ -r ~/.profile ]; then + . ~/.profile + fi + fi + fi + return 0 +} + +# When loging out from the interactive shell, the execution sequence is: +# +# IF ~/.bash_logout exists THEN +# execute ~/.bash_logout +# END IF +post_start() +{ + if [ -r ~/.bash_logout ]; then + . ~/.bash_logout + fi + return 0 +} + +#start the window manager +wm_start() +{ + if [ -r /etc/default/locale ]; then + . /etc/default/locale + export LANG LANGUAGE + fi + + # debian + if [ -r /etc/X11/Xsession ]; then + pre_start + . /etc/X11/Xsession + post_start + exit 0 + fi + + # alpine + # Don't use /etc/X11/xinit/Xsession - it doesn't work + if [ -f /etc/alpine-release ]; then + if [ -f /etc/X11/xinit/xinitrc ]; then + pre_start + /etc/X11/xinit/xinitrc + post_start + else + echo "** xinit package isn't installed" >&2 + exit 1 + fi + fi + + # el + if [ -r /etc/X11/xinit/Xsession ]; then + pre_start + . /etc/X11/xinit/Xsession + post_start + exit 0 + fi + + # suse + if [ -r /etc/X11/xdm/Xsession ]; then + # since the following script run a user login shell, + # do not execute the pseudo login shell scripts + . /etc/X11/xdm/Xsession + exit 0 + elif [ -r /usr/etc/X11/xdm/Xsession ]; then + . /usr/etc/X11/xdm/Xsession + exit 0 + fi + + pre_start + xterm + post_start +} + +#. /etc/environment +#export PATH=$PATH +#export LANG=$LANG + +# change PATH to be what your environment needs usually what is in +# /etc/environment +#PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games" +#export PATH=$PATH + +# for PATH and LANG from /etc/environment +# pam will auto process the environment file if /etc/pam.d/xrdp-sesman +# includes +# auth required pam_env.so readenv=1 + +wm_start + +exit 1 diff --git a/res/xorg.conf b/res/xorg.conf new file mode 100644 index 000000000..fe1539995 --- /dev/null +++ b/res/xorg.conf @@ -0,0 +1,30 @@ +Section "Monitor" + Identifier "Dummy Monitor" + + # Default HorizSync 31.50 - 48.00 kHz + HorizSync 5.0 - 150.0 + # Default VertRefresh 50.00 - 70.00 Hz + VertRefresh 5.0 - 100.0 + + # Taken from https://www.xpra.org/xorg.conf + Modeline "1920x1080" 23.53 1920 1952 2040 2072 1080 1106 1108 1135 + Modeline "1280x720" 27.41 1280 1312 1416 1448 720 737 740 757 +EndSection + +Section "Device" + Identifier "Dummy VideoCard" + Driver "dummy" + # Default VideoRam 4096 + # (1920 * 1080 * 4) / 1024 = 8100 + VideoRam 8100 +EndSection + +Section "Screen" + Identifier "Dummy Screen" + Device "Dummy VideoCard" + Monitor "Dummy Monitor" + SubSection "Display" + Depth 24 + Modes "1920x1080" "1280x720" + EndSubSection +EndSection \ No newline at end of file diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 000000000..a0ea5df1c --- /dev/null +++ b/src/api.rs @@ -0,0 +1,87 @@ +use std::ffi::{c_char}; + +use crate::{ + flutter::{FlutterHandler, SESSIONS}, + plugins::PLUGIN_REGISTRAR, + ui_session_interface::Session, +}; + +// API provided by RustDesk. +pub type LoadPluginFunc = fn(*const c_char) -> i32; +pub type UnloadPluginFunc = fn(*const c_char) -> i32; +pub type AddSessionFunc = fn(session_id: String) -> bool; +pub type RemoveSessionFunc = fn(session_id: &String) -> bool; +pub type AddSessionHookFunc = fn(session_id: String, key: String, hook: SessionHook) -> bool; +pub type RemoveSessionHookFunc = fn(session_id: String, key: &String) -> bool; + +/// Hooks for session. +#[derive(Clone)] +pub enum SessionHook { + OnSessionRgba(fn(String, Vec) -> Vec), +} + +// #[repr(C)] +pub struct RustDeskApiTable { + pub(crate) load_plugin: LoadPluginFunc, + pub(crate) unload_plugin: UnloadPluginFunc, + pub add_session: AddSessionFunc, + pub remove_session: RemoveSessionFunc, + pub add_session_hook: AddSessionHookFunc, + pub remove_session_hook: RemoveSessionHookFunc, +} + +fn load_plugin(path: *const c_char) -> i32 { + PLUGIN_REGISTRAR.load_plugin(path) +} + +fn unload_plugin(path: *const c_char) -> i32 { + PLUGIN_REGISTRAR.unload_plugin(path) +} + +fn add_session(session_id: String) -> bool { + // let mut sessions = SESSIONS.write().unwrap(); + // if sessions.contains_key(&session.id) { + // return false; + // } + // let _ = sessions.insert(session.id.to_owned(), session); + // true + false +} + +fn remove_session(session_id: &String) -> bool { + let mut sessions = SESSIONS.write().unwrap(); + if !sessions.contains_key(session_id) { + return false; + } + let _ = sessions.remove(session_id); + true +} + +fn add_session_hook(session_id: String, key: String, hook: SessionHook) -> bool { + let sessions = SESSIONS.read().unwrap(); + if let Some(session) = sessions.get(&session_id) { + return session.add_session_hook(key, hook); + } + false +} + +fn remove_session_hook(session_id: String, key: &String) -> bool { + let sessions = SESSIONS.read().unwrap(); + if let Some(session) = sessions.get(&session_id) { + return session.remove_session_hook(key); + } + false +} + +impl Default for RustDeskApiTable { + fn default() -> Self { + Self { + load_plugin, + unload_plugin, + add_session, + remove_session, + add_session_hook, + remove_session_hook, + } + } +} diff --git a/src/cli.rs b/src/cli.rs index 40ab21188..13e70987b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -48,18 +48,24 @@ impl Interface for Session { } fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { - if msgtype == "input-password" { - self.sender - .send(Data::Login((self.password.clone(), true))) - .ok(); - } else if msgtype == "re-input-password" { - log::error!("{}: {}", title, text); - let pass = rpassword::prompt_password("Enter password: ").unwrap(); - self.sender.send(Data::Login((pass, true))).ok(); - } else if msgtype.contains("error") { - log::error!("{}: {}: {}", msgtype, title, text); - } else { - log::info!("{}: {}: {}", msgtype, title, text); + match msgtype { + "input-password" => { + self.sender + .send(Data::Login((self.password.clone(), true))) + .ok(); + } + "re-input-password" => { + log::error!("{}: {}", title, text); + let password = rpassword::prompt_password("Enter password: ").unwrap(); + let login_data = Data::Login((password, true)); + self.sender.send(login_data).ok(); + } + msg if msg.contains("error") => { + log::error!("{}: {}: {}", msgtype, title, text); + } + _ => { + log::info!("{}: {}: {}", msgtype, title, text); + } } } @@ -79,8 +85,8 @@ impl Interface for Session { handle_hash(self.lc.clone(), &pass, hash, self, peer).await; } - async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { - handle_login_from_ui(self.lc.clone(), password, remember, peer).await; + async fn handle_login_from_ui(&mut self, os_username: String, os_password: String, password: String, remember: bool, peer: &mut Stream) { + handle_login_from_ui(self.lc.clone(), os_username, os_password, password, remember, peer).await; } async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { diff --git a/src/client.rs b/src/client.rs index 0cd90e1f3..f37719641 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,12 @@ use std::{ collections::HashMap, net::SocketAddr, - ops::{Deref, Not}, + ops::Deref, str::FromStr, - sync::{mpsc, Arc, Mutex, RwLock}, + sync::{ + atomic::{AtomicUsize, Ordering}, + mpsc, Arc, Mutex, RwLock, + }, }; pub use async_trait::async_trait; @@ -13,11 +16,15 @@ use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; +use crossbeam_queue::ArrayQueue; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; +#[cfg(not(any(target_os = "android", target_os = "linux")))] +use ringbuf::{ring_buffer::RbBase, Rb}; use sha2::{Digest, Sha256}; use uuid::Uuid; pub use file_trait::FileManager; +#[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::tokio::sync::mpsc::UnboundedSender; use hbb_common::{ @@ -39,24 +46,20 @@ use hbb_common::{ tokio::time::Duration, AddrMangle, ResultType, Stream, }; -pub use helper::LatencyController; pub use helper::*; use scrap::{ - codec::{Decoder, DecoderCfg}, + codec::Decoder, record::{Recorder, RecorderContext}, - ImageFormat, VpxDecoderConfig, VpxVideoCodecId, + ImageFormat, }; -use crate::{ - common::{self, is_keyboard_mode_supported}, - server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, -}; +use crate::common::{self, is_keyboard_mode_supported}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::{ - common::{check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, - ui_session_interface::SessionPermissionConfig, -}; +use crate::common::{check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +#[cfg(not(feature = "flutter"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::ui_session_interface::SessionPermissionConfig; pub use super::lang::*; @@ -66,6 +69,32 @@ pub mod io_loop; pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); +pub const VIDEO_QUEUE_SIZE: usize = 120; + +#[cfg(all(target_os = "linux", feature = "linux_headless"))] +#[cfg(not(any(feature = "flatpak", feature = "appimage")))] +pub const LOGIN_MSG_DESKTOP_NOT_INITED: &str = "Desktop env is not inited"; +pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY: &str = "Desktop session not ready"; +pub const LOGIN_MSG_DESKTOP_XSESSION_FAILED: &str = "Desktop xsession failed"; +pub const LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER: &str = "Desktop session another user login"; +pub const LOGIN_MSG_DESKTOP_XORG_NOT_FOUND: &str = "Desktop xorg not found"; +// ls /usr/share/xsessions/ +pub const LOGIN_MSG_DESKTOP_NO_DESKTOP: &str = "Desktop none"; +pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY: &str = + "Desktop session not ready, password empty"; +pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG: &str = + "Desktop session not ready, password wrong"; +pub const LOGIN_MSG_PASSWORD_EMPTY: &str = "Empty Password"; +pub const LOGIN_MSG_PASSWORD_WRONG: &str = "Wrong Password"; +pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access"; +pub const LOGIN_MSG_OFFLINE: &str = "Offline"; +#[cfg(target_os = "linux")] +pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; +#[cfg(target_os = "linux")] +pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = + "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."; +pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; +pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; /// Client of the remote desktop. pub struct Client; @@ -88,6 +117,12 @@ lazy_static::lazy_static! { static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_old_clipboard_text() -> &'static Arc> { + &OLD_CLIPBOARD_TEXT +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn get_key_state(key: enigo::Key) -> bool { use enigo::KeyboardControllable; @@ -130,6 +165,7 @@ impl OboePlayer { } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn is_null(&self) -> bool { self.raw.is_null() } @@ -165,7 +201,7 @@ impl Client { token: &str, conn_type: ConnType, interface: impl Interface, - ) -> ResultType<(Stream, bool)> { + ) -> ResultType<(Stream, bool, Option>)> { match Self::_start(peer, key, token, conn_type, interface).await { Err(err) => { let err_str = err.to_string(); @@ -186,7 +222,7 @@ impl Client { token: &str, conn_type: ConnType, interface: impl Interface, - ) -> ResultType<(Stream, bool)> { + ) -> ResultType<(Stream, bool, Option>)> { // to-do: remember the port for each peer, so that we can retry easier if hbb_common::is_ip_str(peer) { return Ok(( @@ -196,6 +232,7 @@ impl Client { ) .await?, true, + None, )); } // Allow connect to {domain}:{port} @@ -203,6 +240,7 @@ impl Client { return Ok(( socket_client::connect_tcp(peer, RENDEZVOUS_TIMEOUT).await?, true, + None, )); } let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await; @@ -298,7 +336,7 @@ impl Client { my_addr.is_ipv4(), ) .await?; - Self::secure_connection( + let pk = Self::secure_connection( peer, signed_id_pk, key, @@ -307,7 +345,7 @@ impl Client { interface, ) .await?; - return Ok((conn, false)); + return Ok((conn, false, pk)); } _ => { log::error!("Unexpected protobuf msg received: {:?}", msg_in); @@ -368,7 +406,7 @@ impl Client { token: &str, conn_type: ConnType, interface: impl Interface, - ) -> ResultType<(Stream, bool)> { + ) -> ResultType<(Stream, bool, Option>)> { let direct_failures = PeerConfig::load(peer_id).direct_failures; let mut connect_timeout = 0; const MIN: u64 = 1000; @@ -438,8 +476,9 @@ impl Client { } let mut conn = conn?; log::info!("{:?} used to establish connection", start.elapsed()); - Self::secure_connection(peer_id, signed_id_pk, key, &mut conn, direct, interface).await?; - Ok((conn, direct)) + let pk = Self::secure_connection(peer_id, signed_id_pk, key, &mut conn, direct, interface) + .await?; + Ok((conn, direct, pk)) } /// Establish secure connection with the server. @@ -450,17 +489,19 @@ impl Client { conn: &mut Stream, direct: bool, interface: impl Interface, - ) -> ResultType<()> { + ) -> ResultType>> { let rs_pk = get_rs_pk(if key.is_empty() { hbb_common::config::RS_PUB_KEY } else { key }); let mut sign_pk = None; + let mut option_pk = None; if !signed_id_pk.is_empty() && rs_pk.is_some() { if let Ok((id, pk)) = decode_id_pk(&signed_id_pk, &rs_pk.unwrap()) { if id == peer_id { sign_pk = Some(sign::PublicKey(pk)); + option_pk = Some(pk.to_vec()); } } if sign_pk.is_none() { @@ -472,7 +513,7 @@ impl Client { None => { // send an empty message out in case server is setting up secure and waiting for first message conn.send(&Message::new()).await?; - return Ok(()); + return Ok(option_pk); } }; match timeout(READ_TIMEOUT, conn.next()).await? { @@ -525,7 +566,7 @@ impl Client { bail!("Reset by the peer"); } } - Ok(()) + Ok(option_pk) } /// Request a relay connection to the server. @@ -628,8 +669,13 @@ impl Client { TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; } + // `try_start_clipboard` is called by all session when connection is established. (When handling peer info). + // This function only create one thread with a loop, the loop is shared by all sessions. + // After all sessions are end, the loop exists. + // + // If clipboard update is detected, the text will be sent to all sessions by `send_text_clipboard_msg`. #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn try_start_clipboard(_conf_tx: Option<(SessionPermissionConfig, UnboundedSender)>) { + fn try_start_clipboard(_ctx: Option) { let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); if clipboard_lock.running { return; @@ -656,9 +702,9 @@ impl Client { #[cfg(feature = "flutter")] crate::flutter::send_text_clipboard_msg(msg); #[cfg(not(feature = "flutter"))] - if let Some((cfg, tx)) = &_conf_tx { - if cfg.is_text_clipboard_required() { - let _ = tx.send(Data::Message(msg)); + if let Some(ctx) = &_ctx { + if ctx.cfg.is_text_clipboard_required() { + let _ = ctx.tx.send(Data::Message(msg)); } } } @@ -672,6 +718,7 @@ impl Client { } } + #[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] fn get_current_text_clipboard_msg() -> Option { let txt = &*OLD_CLIPBOARD_TEXT.lock().unwrap(); @@ -702,24 +749,28 @@ pub struct AudioHandler { #[cfg(target_os = "linux")] simple: Option, #[cfg(not(any(target_os = "android", target_os = "linux")))] - audio_buffer: Arc>>, + audio_buffer: AudioBuffer, sample_rate: (u32, u32), #[cfg(not(any(target_os = "android", target_os = "linux")))] audio_stream: Option>, channels: u16, - latency_controller: Arc>, - ignore_count: i32, + #[cfg(not(any(target_os = "android", target_os = "linux")))] + ready: Arc>, +} + +#[cfg(not(any(target_os = "android", target_os = "linux")))] +struct AudioBuffer(pub Arc>>); + +#[cfg(not(any(target_os = "android", target_os = "linux")))] +impl Default for AudioBuffer { + fn default() -> Self { + Self(Arc::new(std::sync::Mutex::new( + ringbuf::HeapRb::::new(48000 * 2), // 48000hz, 2 channel, 1 second + ))) + } } impl AudioHandler { - /// Create a new audio handler. - pub fn new(latency_controller: Arc>) -> Self { - AudioHandler { - latency_controller, - ..Default::default() - } - } - /// Start the audio playback. #[cfg(target_os = "linux")] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { @@ -778,9 +829,17 @@ impl AudioHandler { let mut config: StreamConfig = config.into(); config.channels = format0.channels as _; match sample_format { - cpal::SampleFormat::F32 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::I8 => self.build_output_stream::(&config, &device)?, cpal::SampleFormat::I16 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::I32 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::I64 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::U8 => self.build_output_stream::(&config, &device)?, cpal::SampleFormat::U16 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::U32 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::U64 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::F32 => self.build_output_stream::(&config, &device)?, + cpal::SampleFormat::F64 => self.build_output_stream::(&config, &device)?, + f => bail!("unsupported audio format: {:?}", f), } self.sample_rate = (format0.sample_rate, config.sample_rate.0); Ok(()) @@ -802,26 +861,10 @@ impl AudioHandler { } /// Handle audio frame and play it. + #[inline] pub fn handle_frame(&mut self, frame: AudioFrame) { - if frame.timestamp != 0 { - if self - .latency_controller - .lock() - .unwrap() - .check_audio(frame.timestamp) - .not() - { - self.ignore_count += 1; - if self.ignore_count == 100 { - self.ignore_count = 0; - log::debug!("100 audio frames are ignored"); - } - return; - } - } - #[cfg(not(any(target_os = "android", target_os = "linux")))] - if self.audio_stream.is_none() { + if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() { return; } #[cfg(target_os = "linux")] @@ -841,11 +884,7 @@ impl AudioHandler { { let sample_rate0 = self.sample_rate.0; let sample_rate = self.sample_rate.1; - let audio_buffer = self.audio_buffer.clone(); - // avoiding memory overflow if audio_buffer consumer side has problem - if audio_buffer.lock().unwrap().len() as u32 > sample_rate * 120 { - *audio_buffer.lock().unwrap() = Default::default(); - } + let audio_buffer = self.audio_buffer.0.clone(); if sample_rate != sample_rate0 { let buffer = crate::resample_channels( &buffer[0..n], @@ -853,12 +892,12 @@ impl AudioHandler { sample_rate, channels, ); - audio_buffer.lock().unwrap().extend(buffer); + audio_buffer.lock().unwrap().push_slice_overwrite(&buffer); } else { audio_buffer .lock() .unwrap() - .extend(buffer[0..n].iter().cloned()); + .push_slice_overwrite(&buffer[0..n]); } } #[cfg(target_os = "android")] @@ -877,7 +916,7 @@ impl AudioHandler { /// Build audio output stream for current device. #[cfg(not(any(target_os = "android", target_os = "linux")))] - fn build_output_stream( + fn build_output_stream>( &mut self, config: &StreamConfig, device: &Device, @@ -886,24 +925,33 @@ impl AudioHandler { // too many errors, will improve later log::trace!("an error occurred on stream: {}", err); }; - let audio_buffer = self.audio_buffer.clone(); + let audio_buffer = self.audio_buffer.0.clone(); + let ready = self.ready.clone(); + let timeout = None; let stream = device.build_output_stream( config, move |data: &mut [T], _: &_| { + if !*ready.lock().unwrap() { + *ready.lock().unwrap() = true; + } let mut lock = audio_buffer.lock().unwrap(); let mut n = data.len(); - if lock.len() < n { - n = lock.len(); + if lock.occupied_len() < n { + n = lock.occupied_len(); } - let mut input = lock.drain(0..n); + let mut elems = vec![0.0f32; n]; + lock.pop_slice(&mut elems); + drop(lock); + let mut input = elems.into_iter(); for sample in data.iter_mut() { *sample = match input.next() { - Some(x) => T::from(&x), - _ => T::from(&0.), + Some(x) => T::from_sample(x), + _ => T::from_sample(0.), }; } }, err_fn, + timeout, )?; stream.play()?; self.audio_stream = Some(Box::new(stream)); @@ -914,7 +962,6 @@ impl AudioHandler { /// Video handler for the [`Client`]. pub struct VideoHandler { decoder: Decoder, - latency_controller: Arc>, pub rgb: Vec, recorder: Arc>>, record: bool, @@ -922,15 +969,9 @@ pub struct VideoHandler { impl VideoHandler { /// Create a new video handler. - pub fn new(latency_controller: Arc>) -> Self { + pub fn new() -> Self { VideoHandler { - decoder: Decoder::new(DecoderCfg { - vpx: VpxDecoderConfig { - codec: VpxVideoCodecId::VP9, - num_threads: (num_cpus::get() / 2) as _, - }, - }), - latency_controller, + decoder: Decoder::new(), rgb: Default::default(), recorder: Default::default(), record: false, @@ -938,14 +979,8 @@ impl VideoHandler { } /// Handle a new video frame. + #[inline] pub fn handle_frame(&mut self, vf: VideoFrame) -> ResultType { - if vf.timestamp != 0 { - // Update the latency controller with the latest timestamp. - self.latency_controller - .lock() - .unwrap() - .update_video(vf.timestamp); - } match &vf.union { Some(frame) => { let res = self.decoder.handle_video_frame( @@ -968,12 +1003,7 @@ impl VideoHandler { /// Reset the decoder. pub fn reset(&mut self) { - self.decoder = Decoder::new(DecoderCfg { - vpx: VpxDecoderConfig { - codec: VpxVideoCodecId::VP9, - num_threads: 1, - }, - }); + self.decoder = Decoder::new(); } /// Start or stop screen record. @@ -987,13 +1017,14 @@ impl VideoHandler { filename: "".to_owned(), width: w as _, height: h as _, - codec_id: scrap::record::RecordCodecID::VP9, + format: scrap::CodecFormat::VP9, tx: None, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); } else { self.recorder = Default::default(); } + self.record = start; } } @@ -1012,7 +1043,7 @@ pub struct LoginConfigHandler { pub conn_id: i32, features: Option, session_id: u64, - pub supported_encoding: Option<(bool, bool)>, + pub supported_encoding: SupportedEncoding, pub restarting_remote_device: bool, pub force_relay: bool, pub direct: Option, @@ -1060,7 +1091,7 @@ impl LoginConfigHandler { self.remember = !config.password.is_empty(); self.config = config; self.session_id = rand::random(); - self.supported_encoding = None; + self.supported_encoding = Default::default(); self.restarting_remote_device = false; self.force_relay = !self.get_option("force-always-relay").is_empty() || force_relay; self.direct = None; @@ -1314,11 +1345,12 @@ impl LoginConfigHandler { config.custom_image_quality[0] }; msg.custom_image_quality = quality << 8; + #[cfg(feature = "flutter")] + if let Some(custom_fps) = self.options.get("custom-fps") { + msg.custom_fps = custom_fps.parse().unwrap_or(30); + } n += 1; } - if let Some(custom_fps) = self.options.get("custom-fps") { - msg.custom_fps = custom_fps.parse().unwrap_or(30); - } let view_only = self.get_toggle_option("view-only"); if view_only { msg.disable_keyboard = BoolOption::Yes.into(); @@ -1344,8 +1376,8 @@ impl LoginConfigHandler { msg.disable_clipboard = BoolOption::Yes.into(); n += 1; } - let state = Decoder::video_codec_state(&self.id); - msg.video_codec_state = hbb_common::protobuf::MessageField::some(state); + msg.supported_decoding = + hbb_common::protobuf::MessageField::some(Decoder::supported_decodings(Some(&self.id))); n += 1; if n > 0 { @@ -1578,10 +1610,7 @@ impl LoginConfigHandler { self.conn_id = pi.conn_id; // no matter if change, for update file time self.save_config(config); - #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] - { - self.supported_encoding = Some((pi.encoding.h264, pi.encoding.h265)); - } + self.supported_encoding = pi.encoding.clone().unwrap_or_default(); } pub fn get_remote_dir(&self) -> String { @@ -1604,7 +1633,12 @@ impl LoginConfigHandler { } /// Create a [`Message`] for login. - fn create_login_msg(&self, password: Vec) -> Message { + fn create_login_msg( + &self, + os_username: String, + os_password: String, + password: Vec, + ) -> Message { #[cfg(any(target_os = "android", target_os = "ios"))] let my_id = Config::get_id_or(crate::common::DEVICE_ID.lock().unwrap().clone()); #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1617,6 +1651,12 @@ impl LoginConfigHandler { option: self.get_option_message(true).into(), session_id: self.session_id, version: crate::VERSION.to_string(), + os_login: Some(OSLogin { + username: os_username, + password: os_password, + ..Default::default() + }) + .into(), ..Default::default() }; match self.conn_type { @@ -1639,10 +1679,10 @@ impl LoginConfigHandler { } pub fn change_prefer_codec(&self) -> Message { - let state = scrap::codec::Decoder::video_codec_state(&self.id); + let decoding = scrap::codec::Decoder::supported_decodings(Some(&self.id)); let mut misc = Misc::new(); misc.set_option(OptionMessage { - video_codec_state: hbb_common::protobuf::MessageField::some(state), + supported_decoding: hbb_common::protobuf::MessageField::some(decoding), ..Default::default() }); let mut msg_out = Message::new(); @@ -1674,8 +1714,9 @@ impl LoginConfigHandler { /// Media data. pub enum MediaData { - VideoFrame(VideoFrame), - AudioFrame(AudioFrame), + VideoQueue, + VideoFrame(Box), + AudioFrame(Box), AudioFormat(AudioFormat), Reset, RecordScreen(bool, i32, i32, String), @@ -1689,24 +1730,64 @@ pub type MediaSender = mpsc::Sender; /// # Arguments /// /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. -pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) +pub fn start_video_audio_threads( + video_callback: F, +) -> ( + MediaSender, + MediaSender, + Arc>, + Arc, +) where F: 'static + FnMut(&mut Vec) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); + let video_queue = Arc::new(ArrayQueue::::new(VIDEO_QUEUE_SIZE)); + let video_queue_cloned = video_queue.clone(); let mut video_callback = video_callback; - - let latency_controller = LatencyController::new(); - let latency_controller_cl = latency_controller.clone(); + let mut duration = std::time::Duration::ZERO; + let mut count = 0; + let fps = Arc::new(AtomicUsize::new(0)); + let decode_fps = fps.clone(); + let mut skip_beginning = 0; std::thread::spawn(move || { - let mut video_handler = VideoHandler::new(latency_controller); + let mut video_handler = VideoHandler::new(); loop { if let Ok(data) = video_receiver.recv() { match data { - MediaData::VideoFrame(vf) => { + MediaData::VideoFrame(_) | MediaData::VideoQueue => { + let vf = if let MediaData::VideoFrame(vf) = data { + *vf + } else { + if let Some(vf) = video_queue.pop() { + vf + } else { + continue; + } + }; + let start = std::time::Instant::now(); if let Ok(true) = video_handler.handle_frame(vf) { video_callback(&mut video_handler.rgb); + // fps calculation + // The first frame will be very slow + if skip_beginning < 5 { + skip_beginning += 1; + continue; + } + duration += start.elapsed(); + count += 1; + if count % 10 == 0 { + fps.store( + (count * 1000 / duration.as_millis()) as usize, + Ordering::Relaxed, + ); + } + // Clear to get real-time fps + if count > 300 { + count = 0; + duration = Duration::ZERO; + } } } MediaData::Reset => { @@ -1723,24 +1804,21 @@ where } log::info!("Video decoder loop exits"); }); - let audio_sender = start_audio_thread(Some(latency_controller_cl)); - return (video_sender, audio_sender); + let audio_sender = start_audio_thread(); + return (video_sender, audio_sender, video_queue_cloned, decode_fps); } /// Start an audio thread /// Return a audio [`MediaSender`] -pub fn start_audio_thread( - latency_controller: Option>>, -) -> MediaSender { - let latency_controller = latency_controller.unwrap_or(LatencyController::new()); +pub fn start_audio_thread() -> MediaSender { let (audio_sender, audio_receiver) = mpsc::channel::(); std::thread::spawn(move || { - let mut audio_handler = AudioHandler::new(latency_controller); + let mut audio_handler = AudioHandler::default(); loop { if let Ok(data) = audio_receiver.recv() { match data { MediaData::AudioFrame(af) => { - audio_handler.handle_frame(af); + audio_handler.handle_frame(*af); } MediaData::AudioFormat(f) => { log::debug!("recved audio format, sample rate={}", f.sample_rate); @@ -1908,6 +1986,71 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) { interface.send(Data::Message(msg_out)); } +#[derive(Copy, Clone)] +struct LoginErrorMsgBox { + msgtype: &'static str, + title: &'static str, + text: &'static str, + link: &'static str, + try_again: bool, +} + +lazy_static::lazy_static! { + static ref LOGIN_ERROR_MAP: Arc> = { + use hbb_common::config::LINK_HEADLESS_LINUX_SUPPORT; + let map = HashMap::from([(LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LoginErrorMsgBox{ + msgtype: "session-login", + title: "", + text: "", + link: "", + try_again: true, + }), (LOGIN_MSG_DESKTOP_XSESSION_FAILED, LoginErrorMsgBox{ + msgtype: "session-re-login", + title: "", + text: "", + link: "", + try_again: true, + }), (LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER, LoginErrorMsgBox{ + msgtype: "info-nocancel", + title: "another_user_login_title_tip", + text: "another_user_login_text_tip", + link: "", + try_again: false, + }), (LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, LoginErrorMsgBox{ + msgtype: "info-nocancel", + title: "xorg_not_found_title_tip", + text: "xorg_not_found_text_tip", + link: LINK_HEADLESS_LINUX_SUPPORT, + try_again: true, + }), (LOGIN_MSG_DESKTOP_NO_DESKTOP, LoginErrorMsgBox{ + msgtype: "info-nocancel", + title: "no_desktop_title_tip", + text: "no_desktop_text_tip", + link: LINK_HEADLESS_LINUX_SUPPORT, + try_again: true, + }), (LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY, LoginErrorMsgBox{ + msgtype: "session-login-password", + title: "", + text: "", + link: "", + try_again: true, + }), (LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG, LoginErrorMsgBox{ + msgtype: "session-login-re-password", + title: "", + text: "", + link: "", + try_again: true, + }), (LOGIN_MSG_NO_PASSWORD_ACCESS, LoginErrorMsgBox{ + msgtype: "wait-remote-accept-nook", + title: "Prompt", + text: "Please wait for the remote side to accept your session request...", + link: "", + try_again: true, + })]); + Arc::new(map) + }; +} + /// Handle login error. /// Return true if the password is wrong, return false if there's an actual error. pub fn handle_login_error( @@ -1915,19 +2058,27 @@ pub fn handle_login_error( err: &str, interface: &impl Interface, ) -> bool { - if err == "Wrong Password" { + if err == LOGIN_MSG_PASSWORD_EMPTY { + lc.write().unwrap().password = Default::default(); + interface.msgbox("input-password", "Password Required", "", ""); + true + } else if err == LOGIN_MSG_PASSWORD_WRONG { lc.write().unwrap().password = Default::default(); interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); true - } else if err == "No Password Access" { - lc.write().unwrap().password = Default::default(); - interface.msgbox( - "wait-remote-accept-nook", - "Prompt", - "Please wait for the remote side to accept your session request...", - "", - ); - true + } else if LOGIN_ERROR_MAP.contains_key(err) { + if let Some(msgbox_info) = LOGIN_ERROR_MAP.get(err) { + interface.msgbox( + msgbox_info.msgtype, + msgbox_info.title, + msgbox_info.text, + msgbox_info.link, + ); + msgbox_info.try_again + } else { + // unreachable! + false + } } else { if err.contains(SCRAP_X11_REQUIRED) { interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL); @@ -1975,16 +2126,21 @@ pub async fn handle_hash( if password.is_empty() { password = lc.read().unwrap().config.password.clone(); } - if password.is_empty() { + let password = if password.is_empty() { // login without password, the remote side can click accept - send_login(lc.clone(), Vec::new(), peer).await; interface.msgbox("input-password", "Password Required", "", ""); + Vec::new() } else { let mut hasher = Sha256::new(); hasher.update(&password); hasher.update(&hash.challenge); - send_login(lc.clone(), hasher.finalize()[..].into(), peer).await; - } + hasher.finalize()[..].into() + }; + + let os_username = lc.read().unwrap().get_option("os-username"); + let os_password = lc.read().unwrap().get_option("os-password"); + + send_login(lc.clone(), os_username, os_password, password, peer).await; lc.write().unwrap().hash = hash; } @@ -1993,10 +2149,21 @@ pub async fn handle_hash( /// # Arguments /// /// * `lc` - Login config. +/// * `os_username` - OS username. +/// * `os_password` - OS password. /// * `password` - Password. /// * `peer` - [`Stream`] for communicating with peer. -async fn send_login(lc: Arc>, password: Vec, peer: &mut Stream) { - let msg_out = lc.read().unwrap().create_login_msg(password); +async fn send_login( + lc: Arc>, + os_username: String, + os_password: String, + password: Vec, + peer: &mut Stream, +) { + let msg_out = lc + .read() + .unwrap() + .create_login_msg(os_username, os_password, password); allow_err!(peer.send(&msg_out).await); } @@ -2005,25 +2172,40 @@ async fn send_login(lc: Arc>, password: Vec, peer /// # Arguments /// /// * `lc` - Login config. +/// * `os_username` - OS username. +/// * `os_password` - OS password. /// * `password` - Password. /// * `remember` - Whether to remember password. /// * `peer` - [`Stream`] for communicating with peer. pub async fn handle_login_from_ui( lc: Arc>, + os_username: String, + os_password: String, password: String, remember: bool, peer: &mut Stream, ) { - let mut hasher = Sha256::new(); - hasher.update(password); - hasher.update(&lc.read().unwrap().hash.salt); - let res = hasher.finalize(); - lc.write().unwrap().remember = remember; - lc.write().unwrap().password = res[..].into(); + let mut hash_password = if password.is_empty() { + let mut password2 = lc.read().unwrap().password.clone(); + if password2.is_empty() { + password2 = lc.read().unwrap().config.password.clone(); + } + password2 + } else { + let mut hasher = Sha256::new(); + hasher.update(password); + hasher.update(&lc.read().unwrap().hash.salt); + let res = hasher.finalize(); + lc.write().unwrap().remember = remember; + lc.write().unwrap().password = res[..].into(); + res[..].into() + }; let mut hasher2 = Sha256::new(); - hasher2.update(&res[..]); + hasher2.update(&hash_password[..]); hasher2.update(&lc.read().unwrap().hash.challenge); - send_login(lc.clone(), hasher2.finalize()[..].into(), peer).await; + hash_password = hasher2.finalize()[..].to_vec(); + + send_login(lc.clone(), os_username, os_password, hash_password, peer).await; } async fn send_switch_login_request( @@ -2037,7 +2219,7 @@ async fn send_switch_login_request( lr: hbb_common::protobuf::MessageField::some( lc.read() .unwrap() - .create_login_msg(vec![]) + .create_login_msg("".to_owned(), "".to_owned(), vec![]) .login_request() .to_owned(), ), @@ -2058,7 +2240,14 @@ pub trait Interface: Send + Clone + 'static + Sized { self.msgbox("error", "Error", err, ""); } async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream); - async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream); + async fn handle_login_from_ui( + &mut self, + os_username: String, + os_password: String, + password: String, + remember: bool, + peer: &mut Stream, + ); async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); fn get_login_config_handler(&self) -> Arc>; @@ -2078,7 +2267,7 @@ pub trait Interface: Send + Clone + 'static + Sized { #[derive(Clone)] pub enum Data { Close, - Login((String, bool)), + Login((String, String, String, bool)), Message(Message), SendFiles((i32, String, String, i32, bool, bool)), RemoveDirAll((i32, String, bool, bool)), @@ -2285,3 +2474,14 @@ fn decode_id_pk(signed: &[u8], key: &sign::PublicKey) -> ResultType<(String, [u8 bail!("Wrong public length"); } } + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub(crate) struct ClientClipboardContext; + +#[cfg(not(feature = "flutter"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub(crate) struct ClientClipboardContext { + pub cfg: SessionPermissionConfig, + pub tx: UnboundedSender, +} diff --git a/src/client/helper.rs b/src/client/helper.rs index 20acd811a..61844d908 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -1,111 +1,8 @@ -use std::{ - sync::{Arc, Mutex}, - time::Instant, -}; - use hbb_common::{ - log, - message_proto::{video_frame, VideoFrame, Message, VoiceCallRequest, VoiceCallResponse}, get_time, + get_time, + message_proto::{Message, VoiceCallRequest, VoiceCallResponse}, }; - -const MAX_LATENCY: i64 = 500; -const MIN_LATENCY: i64 = 100; - -/// Latency controller for syncing audio with the video stream. -/// Only sync the audio to video, not the other way around. -#[derive(Debug)] -pub struct LatencyController { - last_video_remote_ts: i64, // generated on remote device - update_time: Instant, - allow_audio: bool, - audio_only: bool -} - -impl Default for LatencyController { - fn default() -> Self { - Self { - last_video_remote_ts: Default::default(), - update_time: Instant::now(), - allow_audio: Default::default(), - audio_only: false - } - } -} - -impl LatencyController { - /// Create a new latency controller. - pub fn new() -> Arc> { - Arc::new(Mutex::new(LatencyController::default())) - } - - /// Set whether this [LatencyController] should be working in audio only mode. - pub fn set_audio_only(&mut self, only: bool) { - self.audio_only = only; - } - - /// Update the latency controller with the latest video timestamp. - pub fn update_video(&mut self, timestamp: i64) { - self.last_video_remote_ts = timestamp; - self.update_time = Instant::now(); - } - - /// Check if the audio should be played based on the current latency. - pub fn check_audio(&mut self, timestamp: i64) -> bool { - // Compute audio latency. - let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; - let latency = if self.audio_only { - expected - } else { - expected - timestamp - }; - // Set MAX and MIN, avoid fixing too frequently. - if self.allow_audio { - if latency.abs() > MAX_LATENCY { - log::debug!("LATENCY > {}ms cut off, latency:{}", MAX_LATENCY, latency); - self.allow_audio = false; - } - } else { - if latency.abs() < MIN_LATENCY { - log::debug!("LATENCY < {}ms resume, latency:{}", MIN_LATENCY, latency); - self.allow_audio = true; - } - } - // No video frame here, which means the update time is not up to date. - // We manually update the time here. - self.update_time = Instant::now(); - self.allow_audio - } -} - -#[derive(PartialEq, Debug, Clone)] -pub enum CodecFormat { - VP9, - H264, - H265, - Unknown, -} - -impl From<&VideoFrame> for CodecFormat { - fn from(it: &VideoFrame) -> Self { - match it.union { - Some(video_frame::Union::Vp9s(_)) => CodecFormat::VP9, - Some(video_frame::Union::H264s(_)) => CodecFormat::H264, - Some(video_frame::Union::H265s(_)) => CodecFormat::H265, - _ => CodecFormat::Unknown, - } - } -} - -impl ToString for CodecFormat { - fn to_string(&self) -> String { - match self { - CodecFormat::VP9 => "VP9".into(), - CodecFormat::H264 => "H264".into(), - CodecFormat::H265 => "H265".into(), - CodecFormat::Unknown => "Unknow".into(), - } - } -} +use scrap::CodecFormat; #[derive(Debug, Default)] pub struct QualityStatus { @@ -135,4 +32,4 @@ pub fn new_voice_call_response(request_timestamp: i64, accepted: bool) -> Messag let mut msg = Message::new(); msg.set_voice_call_response(resp); msg -} \ No newline at end of file +} diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 989c7c95b..bbcd74477 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,10 +1,13 @@ use std::collections::HashMap; use std::num::NonZeroI64; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; +use crossbeam_queue::ArrayQueue; use hbb_common::config::{PeerConfig, TransferSerde}; use hbb_common::fs::{ can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, @@ -13,6 +16,8 @@ use hbb_common::fs::{ use hbb_common::message_proto::permission_info::Permission; use hbb_common::protobuf::Message as _; use hbb_common::rendezvous_proto::ConnType; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::sleep; use hbb_common::tokio::sync::mpsc::error::TryRecvError; #[cfg(windows)] use hbb_common::tokio::sync::Mutex as TokioMutex; @@ -21,22 +26,23 @@ use hbb_common::tokio::{ sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::{allow_err, get_time, message_proto::*, sleep}; -use hbb_common::{fs, log, Stream}; +use hbb_common::{allow_err, fs, get_time, log, message_proto::*, Stream}; +use scrap::CodecFormat; use crate::client::{ - new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, - SEC30, + new_voice_call_request, Client, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::update_clipboard; +use crate::common::{self, update_clipboard}; use crate::common::{get_default_sound_input, set_sound_input}; use crate::ui_session_interface::{InvokeUiSession, Session}; -use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; +#[cfg(not(any(target_os = "ios")))] +use crate::{audio_service, ConnInner, CLIENT_SERVER}; use crate::{client::Data, client::Interface}; pub struct Remote { handler: Session, + video_queue: Arc>, video_sender: MediaSender, audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, @@ -44,7 +50,6 @@ pub struct Remote { // Stop sending local audio to remote client. stop_voice_call_sender: Option>, voice_call_request_timestamp: Option, - old_clipboard: Arc>, read_jobs: Vec, write_jobs: Vec, remove_jobs: HashMap, @@ -57,24 +62,28 @@ pub struct Remote { frame_count: Arc, video_format: CodecFormat, elevation_requested: bool, + fps_control: FpsControl, + decode_fps: Arc, } impl Remote { pub fn new( handler: Session, + video_queue: Arc>, video_sender: MediaSender, audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, frame_count: Arc, + decode_fps: Arc, ) -> Self { Self { handler, + video_queue, video_sender, audio_sender, receiver, sender, - old_clipboard: Default::default(), read_jobs: Vec::new(), write_jobs: Vec::new(), remove_jobs: Default::default(), @@ -89,6 +98,8 @@ impl Remote { stop_voice_call_sender: None, voice_call_request_timestamp: None, elevation_requested: false, + fps_control: Default::default(), + decode_fps, } } @@ -110,9 +121,11 @@ impl Remote { ) .await { - Ok((mut peer, direct)) => { + Ok((mut peer, direct, pk)) => { self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.set_connection_info(direct, false); + self.handler + .set_fingerprint(crate::common::pk_to_fingerprint(pk.unwrap_or_default())); // just build for now #[cfg(not(windows))] @@ -136,6 +149,7 @@ impl Remote { let mut rx_clip_client = rx_clip_client_lock.lock().await; let mut status_timer = time::interval(Duration::new(1, 0)); + let mut fps_instant = Instant::now(); loop { tokio::select! { @@ -213,9 +227,18 @@ impl Remote { } } _ = status_timer.tick() => { - let speed = self.data_count.swap(0, Ordering::Relaxed); + self.fps_control(); + let elapsed = fps_instant.elapsed().as_millis(); + if elapsed < 1000 { + continue; + } + fps_instant = Instant::now(); + let mut speed = self.data_count.swap(0, Ordering::Relaxed); + speed = speed * 1000 / elapsed as usize; let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); - let fps = self.frame_count.swap(0, Ordering::Relaxed) as _; + let mut fps = self.frame_count.swap(0, Ordering::Relaxed) as _; + // Correcting the inaccuracy of status_timer + fps = fps * 1000 / elapsed as i32; self.handler.update_quality_status(QualityStatus { speed:Some(speed), fps:Some(fps), @@ -283,62 +306,69 @@ impl Remote { if let Some(device) = default_sound_device { set_sound_input(device); } - // Create a channel to receive error or closed message - let (tx, rx) = std::sync::mpsc::channel(); - let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); - // Create a stand-alone inner, add subscribe to audio service - let conn_id = CLIENT_SERVER.write().unwrap().get_new_id(); - let client_conn_inner = ConnInner::new(conn_id.clone(), Some(tx_audio_data), None); - // now we subscribe - CLIENT_SERVER.write().unwrap().subscribe( - audio_service::NAME, - client_conn_inner.clone(), - true, - ); - let tx_audio = self.sender.clone(); - std::thread::spawn(move || { - loop { - // check if client is closed - match rx.try_recv() { - Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { - log::debug!("Exit voice call audio service of client"); - // unsubscribe - CLIENT_SERVER.write().unwrap().subscribe( - audio_service::NAME, - client_conn_inner, - false, - ); - break; - } - _ => {} - } - match rx_audio_data.try_recv() { - Ok((_instant, msg)) => match &msg.union { - Some(message::Union::AudioFrame(frame)) => { - let mut msg = Message::new(); - msg.set_audio_frame(frame.clone()); - tx_audio.send(Data::Message(msg)).ok(); - log::debug!("send audio frame {}", frame.timestamp); - } - Some(message::Union::Misc(misc)) => { - let mut msg = Message::new(); - msg.set_misc(misc.clone()); - tx_audio.send(Data::Message(msg)).ok(); - log::debug!("send audio misc {:?}", misc.audio_format()); + // iOS does not have this server. + #[cfg(not(any(target_os = "ios")))] + { + // Create a channel to receive error or closed message + let (tx, rx) = std::sync::mpsc::channel(); + let (tx_audio_data, mut rx_audio_data) = + hbb_common::tokio::sync::mpsc::unbounded_channel(); + // Create a stand-alone inner, add subscribe to audio service + let conn_id = CLIENT_SERVER.write().unwrap().get_new_id(); + let client_conn_inner = ConnInner::new(conn_id.clone(), Some(tx_audio_data), None); + // now we subscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner.clone(), + true, + ); + let tx_audio = self.sender.clone(); + std::thread::spawn(move || { + loop { + // check if client is closed + match rx.try_recv() { + Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { + log::debug!("Exit voice call audio service of client"); + // unsubscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner, + false, + ); + break; } _ => {} - }, - Err(err) => { - if err == TryRecvError::Empty { - // ignore - } else { - log::debug!("Failed to record local audio channel: {}", err); + } + match rx_audio_data.try_recv() { + Ok((_instant, msg)) => match &msg.union { + Some(message::Union::AudioFrame(frame)) => { + let mut msg = Message::new(); + msg.set_audio_frame(frame.clone()); + tx_audio.send(Data::Message(msg)).ok(); + } + Some(message::Union::Misc(misc)) => { + let mut msg = Message::new(); + msg.set_misc(misc.clone()); + tx_audio.send(Data::Message(msg)).ok(); + } + _ => {} + }, + Err(err) => { + if err == TryRecvError::Empty { + // ignore + } else { + log::debug!("Failed to record local audio channel: {}", err); + } } } } - } - }); - Some(tx) + }); + return Some(tx); + } + #[cfg(target_os = "ios")] + { + None + } } async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { @@ -351,9 +381,9 @@ impl Remote { allow_err!(peer.send(&msg).await); return false; } - Data::Login((password, remember)) => { + Data::Login((os_username, os_password, password, remember)) => { self.handler - .handle_login_from_ui(password, remember, peer) + .handle_login_from_ui(os_username, os_password, password, remember, peer) .await; } Data::ToggleClipboardFile => { @@ -807,6 +837,63 @@ impl Remote { } } + fn contains_key_frame(vf: &VideoFrame) -> bool { + use video_frame::Union::*; + match &vf.union { + Some(vf) => match vf { + Vp8s(f) | Vp9s(f) | H264s(f) | H265s(f) => f.frames.iter().any(|e| e.key), + _ => false, + }, + None => false, + } + } + #[inline] + fn fps_control(&mut self) { + let len = self.video_queue.len(); + let ctl = &mut self.fps_control; + // Current full speed decoding fps + let decode_fps = self.decode_fps.load(std::sync::atomic::Ordering::Relaxed); + // 500ms + let debounce = if decode_fps > 10 { decode_fps / 2 } else { 5 }; + if len < debounce || decode_fps == 0 { + return; + } + // First setting , or the length of the queue still increases after setting, or exceed the size of the last setting again + if ctl.set_times < 10 // enough + && (ctl.set_times == 0 + || (len > ctl.last_queue_size && ctl.last_set_instant.elapsed().as_secs() > 30)) + { + // 80% fps to ensure decoding is faster than encoding + let mut custom_fps = decode_fps as i32 * 4 / 5; + if custom_fps < 1 { + custom_fps = 1; + } + // send custom fps + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + custom_fps, + ..Default::default() + }); + let mut msg = Message::new(); + msg.set_misc(misc); + self.sender.send(Data::Message(msg)).ok(); + ctl.last_queue_size = len; + ctl.set_times += 1; + ctl.last_set_instant = Instant::now(); + } + // send refresh + if ctl.refresh_times < 10 // enough + && (len > self.video_queue.capacity() / 2 + && (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 30)) + { + // Refresh causes client set_display, left frames cause flickering. + while let Some(_) = self.video_queue.pop() {} + self.handler.refresh_video(); + ctl.refresh_times += 1; + ctl.last_refresh_instant = Instant::now(); + } + } + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { @@ -825,7 +912,15 @@ impl Remote { ..Default::default() }) }; - self.video_sender.send(MediaData::VideoFrame(vf)).ok(); + if Self::contains_key_frame(&vf) { + while let Some(_) = self.video_queue.pop() {} + self.video_sender + .send(MediaData::VideoFrame(Box::new(vf))) + .ok(); + } else { + self.video_queue.force_push(vf); + self.video_sender.send(MediaData::VideoQueue).ok(); + } } Some(message::Union::Hash(hash)) => { self.handler @@ -842,30 +937,30 @@ impl Remote { self.handler.handle_peer_info(pi); self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { - let sender = self.sender.clone(); - let permission_config = self.handler.get_permission_config(); - #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] Client::try_start_clipboard(None); #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] - Client::try_start_clipboard(Some(( - permission_config.clone(), - sender.clone(), - ))); + Client::try_start_clipboard(Some( + crate::client::ClientClipboardContext { + cfg: self.handler.get_permission_config(), + tx: self.sender.clone(), + }, + )); #[cfg(not(any(target_os = "android", target_os = "ios")))] - tokio::spawn(async move { - // due to clipboard service interval time - sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; - if permission_config.is_text_clipboard_required() { - if let Some(msg_out) = Client::get_current_text_clipboard_msg() - { + if let Some(msg_out) = Client::get_current_text_clipboard_msg() { + let sender = self.sender.clone(); + let permission_config = self.handler.get_permission_config(); + tokio::spawn(async move { + // due to clipboard service interval time + sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; + if permission_config.is_text_clipboard_required() { sender.send(Data::Message(msg_out)).ok(); } - } - }); + }); + } } if self.handler.is_file_transfer() { @@ -886,7 +981,7 @@ impl Remote { Some(message::Union::Clipboard(cb)) => { if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] - update_clipboard(cb, Some(&self.old_clipboard)); + update_clipboard(cb, Some(&crate::client::get_old_clipboard_text())); #[cfg(any(target_os = "android", target_os = "ios"))] { let content = if cb.compress { @@ -1203,6 +1298,22 @@ impl Remote { #[cfg(feature = "flutter")] self.handler.switch_back(&self.handler.id); } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::PluginRequest(p)) => { + allow_err!(crate::plugin::handle_server_event(&p.id, &p.content)); + // to-do: show message box on UI when error occurs? + } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::PluginResponse(p)) => { + let name = if p.name.is_empty() { + "plugin".to_string() + } else { + p.name + }; + self.handler.msgbox("custom-nocancel", &name, &p.msg, ""); + } _ => {} }, Some(message::Union::TestDelay(t)) => { @@ -1210,7 +1321,9 @@ impl Remote { } Some(message::Union::AudioFrame(frame)) => { if !self.handler.lc.read().unwrap().disable_audio.v { - self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); + self.audio_sender + .send(MediaData::AudioFrame(Box::new(frame))) + .ok(); } } Some(message::Union::FileAction(action)) => match action.union { @@ -1223,6 +1336,7 @@ impl Remote { }, Some(message::Union::MessageBox(msgbox)) => { let mut link = msgbox.link; + // Links from the remote side must be verified. if !link.starts_with("rustdesk://") { if let Some(v) = hbb_common::config::HELPER_URL.get(&link as &str) { link = v.to_string(); @@ -1456,3 +1570,23 @@ impl RemoveJob { } } } + +struct FpsControl { + last_queue_size: usize, + set_times: usize, + refresh_times: usize, + last_set_instant: Instant, + last_refresh_instant: Instant, +} + +impl Default for FpsControl { + fn default() -> Self { + Self { + last_queue_size: Default::default(), + set_times: Default::default(), + refresh_times: Default::default(), + last_set_instant: Instant::now(), + last_refresh_instant: Instant::now(), + } + } +} diff --git a/src/common.rs b/src/common.rs index 7bdfd9012..d66938cf7 100644 --- a/src/common.rs +++ b/src/common.rs @@ -755,7 +755,7 @@ lazy_static::lazy_static! { #[cfg(target_os = "linux")] lazy_static::lazy_static! { - pub static ref IS_X11: bool = "x11" == hbb_common::platform::linux::get_display_server(); + pub static ref IS_X11: bool = hbb_common::platform::linux::is_x11_or_headless(); } pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { @@ -782,6 +782,7 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin /// 1. Try to send the url scheme from ipc. /// 2. If failed to send the url scheme, we open a new main window to handle this url scheme. pub fn handle_url_scheme(url: String) { + #[cfg(not(target_os = "ios"))] if let Err(err) = crate::ipc::send_url_scheme(url.clone()) { log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); let _ = crate::run_me(vec![url]); @@ -801,6 +802,9 @@ pub fn decode64>(input: T) -> Result, base64::DecodeError } pub async fn get_key(sync: bool) -> String { + #[cfg(target_os = "ios")] + let mut key = Config::get_option("key"); + #[cfg(not(target_os = "ios"))] let mut key = if sync { Config::get_option("key") } else { @@ -818,3 +822,35 @@ pub async fn get_key(sync: bool) -> String { } key } + +pub fn is_peer_version_ge(v: &str) -> bool { + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = crate::ui::CUR_SESSION.lock().unwrap().as_ref() { + return session.get_peer_version() >= hbb_common::get_version_number(v); + } + + #[cfg(feature = "flutter")] + if let Some(session) = crate::flutter::SESSIONS + .read() + .unwrap() + .get(&*crate::flutter::CUR_SESSION_ID.read().unwrap()) + { + return session.get_peer_version() >= hbb_common::get_version_number(v); + } + + false +} + +pub fn pk_to_fingerprint(pk: Vec) -> String { + let s: String = pk.iter().map(|u| format!("{:02x}", u)).collect(); + s.chars() + .enumerate() + .map(|(i, c)| { + if i > 0 && i % 4 == 0 { + format!(" {}", c) + } else { + format!("{}", c) + } + }) + .collect() +} diff --git a/src/core_main.rs b/src/core_main.rs index 2c209bd07..c5193b566 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,5 +1,8 @@ +#[cfg(not(debug_assertions))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::platform::breakdown_callback; use hbb_common::log; +#[cfg(not(debug_assertions))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::platform::register_breakdown_handler; @@ -8,6 +11,7 @@ use hbb_common::platform::register_breakdown_handler; /// [Note] /// If it returns [`None`], then the process will terminate, and flutter gui will not be started. /// If it returns [`Some`], then the process will continue, and flutter gui will be started. +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn core_main() -> Option> { let mut args = Vec::new(); let mut flutter_args = Vec::new(); @@ -38,6 +42,7 @@ pub fn core_main() -> Option> { } i += 1; } + #[cfg(not(debug_assertions))] #[cfg(not(any(target_os = "android", target_os = "ios")))] register_breakdown_handler(breakdown_callback); #[cfg(target_os = "linux")] @@ -140,10 +145,6 @@ pub fn core_main() -> Option> { args.len() > 1, )); return None; - } else if args[0] == "--extract" { - #[cfg(feature = "with_rc")] - hbb_common::allow_err!(crate::rc::extract_resources(&args[1])); - return None; } else if args[0] == "--install-cert" { #[cfg(windows)] hbb_common::allow_err!(crate::platform::windows::install_cert(&args[1])); @@ -223,6 +224,11 @@ pub fn core_main() -> Option> { // call connection manager to establish connections // meanwhile, return true to call flutter window to show control panel crate::ui_interface::start_option_status_sync(); + } else if args[0] == "--cm-no-ui" { + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::flutter::connection_manager::start_cm_no_ui(); + return None; } } //_async_logger_holder.map(|x| x.flush()); diff --git a/src/flutter.rs b/src/flutter.rs index 089daff5c..02c6b16b1 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -3,18 +3,19 @@ use crate::{ flutter_ffi::EventToUI, ui_session_interface::{io_loop, InvokeUiSession, Session}, }; -#[cfg(feature = "flutter_texture_render")] -use dlopen::{ - symbor::{Library, Symbol}, - Error as LibError, -}; use flutter_rust_bridge::StreamSink; -#[cfg(feature = "flutter_texture_render")] -use hbb_common::libc::c_void; use hbb_common::{ bail, config::LocalConfig, get_version_number, log, message_proto::*, rendezvous_proto::ConnType, ResultType, }; +#[cfg(feature = "flutter_texture_render")] +use hbb_common::{ + dlopen::{ + symbor::{Library, Symbol}, + Error as LibError, + }, + libc::c_void, +}; use serde_json::json; #[cfg(not(feature = "flutter_texture_render"))] @@ -28,20 +29,20 @@ use std::{ /// tag "main" for [Desktop Main Page] and [Mobile (Client and Server)] (the mobile don't need multiple windows, only one global event stream is needed) /// tag "cm" only for [Desktop CM Page] -pub(super) const APP_TYPE_MAIN: &str = "main"; +pub(crate) const APP_TYPE_MAIN: &str = "main"; #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub(super) const APP_TYPE_CM: &str = "cm"; +pub(crate) const APP_TYPE_CM: &str = "cm"; #[cfg(any(target_os = "android", target_os = "ios"))] -pub(super) const APP_TYPE_CM: &str = "main"; +pub(crate) const APP_TYPE_CM: &str = "main"; -pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; -pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; -pub(super) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward"; +pub(crate) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; +pub(crate) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; +pub(crate) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward"; lazy_static::lazy_static! { - pub static ref CUR_SESSION_ID: RwLock = Default::default(); - pub static ref SESSIONS: RwLock>> = Default::default(); - pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel + pub(crate) static ref CUR_SESSION_ID: RwLock = Default::default(); + pub(crate) static ref SESSIONS: RwLock>> = Default::default(); + static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } #[cfg(all(target_os = "windows", feature = "flutter_texture_render"))] @@ -145,6 +146,8 @@ pub struct FlutterHandler { notify_rendered: Arc>, renderer: Arc>, peer_info: Arc>, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + hooks: Arc>>, } #[cfg(not(feature = "flutter_texture_render"))] @@ -156,6 +159,8 @@ pub struct FlutterHandler { pub rgba: Arc>>, pub rgba_valid: Arc, peer_info: Arc>, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + hooks: Arc>>, } #[cfg(feature = "flutter_texture_render")] @@ -218,7 +223,7 @@ impl VideoRenderer { } pub fn on_rgba(&self, rgba: &Vec) { - if self.ptr == usize::default() { + if self.ptr == usize::default() || self.width == 0 || self.height == 0 { return; } if let Some(func) = &self.on_rgba_func { @@ -244,17 +249,21 @@ impl FlutterHandler { /// /// * `name` - The name of the event. /// * `event` - Fields of the event content. - fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { + pub fn push_event(&self, name: &str, event: Vec<(&str, &str)>) -> Option { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); h.insert("name", name); let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); - if let Some(stream) = &*self.event_stream.read().unwrap() { - stream.add(EventToUI::Event(out)); - } + Some( + self.event_stream + .read() + .unwrap() + .as_ref()? + .add(EventToUI::Event(out)), + ) } - pub fn close_event_stream(&mut self) { + pub(crate) fn close_event_stream(&mut self) { let mut stream_lock = self.event_stream.write().unwrap(); if let Some(stream) = &*stream_lock { stream.add(EventToUI::Event("close".to_owned())); @@ -276,6 +285,28 @@ impl FlutterHandler { serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub(crate) fn add_session_hook(&self, key: String, hook: crate::api::SessionHook) -> bool { + let mut hooks = self.hooks.write().unwrap(); + if hooks.contains_key(&key) { + // Already has the hook with this key. + return false; + } + let _ = hooks.insert(key, hook); + true + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub(crate) fn remove_session_hook(&self, key: &String) -> bool { + let mut hooks = self.hooks.write().unwrap(); + if !hooks.contains_key(key) { + // The hook with this key does not found. + return false; + } + let _ = hooks.remove(key); + true + } + #[inline] #[cfg(feature = "flutter_texture_render")] pub fn register_texture(&mut self, ptr: usize) { @@ -364,6 +395,10 @@ impl InvokeUiSession for FlutterHandler { ); } + fn set_fingerprint(&self, fingerprint: String) { + self.push_event("fingerprint", vec![("fingerprint", &fingerprint)]); + } + fn job_error(&self, id: i32, err: String, file_num: i32) { self.push_event( "job_error", @@ -509,6 +544,7 @@ impl InvokeUiSession for FlutterHandler { ("features", &features), ("current_display", &pi.current_display.to_string()), ("resolutions", &resolutions), + ("platform_additions", &pi.platform_additions), ], ); } @@ -599,7 +635,7 @@ impl InvokeUiSession for FlutterHandler { } fn on_voice_call_closed(&self, reason: &str) { - self.push_event("on_voice_call_closed", [("reason", reason)].into()) + let _res = self.push_event("on_voice_call_closed", [("reason", reason)].into()); } fn on_voice_call_waiting(&self) { @@ -814,8 +850,20 @@ pub mod connection_manager { } } + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn start_cm_no_ui() { + start_listen_ipc(false); + } + + #[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn start_listen_ipc_thread() { + start_listen_ipc(true); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn start_listen_ipc(new_thread: bool) { use crate::ui_cm_interface::{start_ipc, ConnectionManager}; #[cfg(target_os = "linux")] @@ -824,7 +872,11 @@ pub mod connection_manager { let cm = ConnectionManager { ui_handler: FlutterHandler {}, }; - std::thread::spawn(move || start_ipc(cm)); + if new_thread { + std::thread::spawn(move || start_ipc(cm)); + } else { + start_ipc(cm); + } } #[cfg(target_os = "android")] @@ -949,6 +1001,7 @@ pub fn session_next_rgba(id: *const char) { } } +#[inline] #[no_mangle] #[cfg(feature = "flutter_texture_render")] pub fn session_register_texture(id: *const char, ptr: usize) { @@ -960,6 +1013,35 @@ pub fn session_register_texture(id: *const char, ptr: usize) { } } +#[inline] #[no_mangle] #[cfg(not(feature = "flutter_texture_render"))] pub fn session_register_texture(_id: *const char, _ptr: usize) {} + +#[inline] +pub fn push_session_event(peer: &str, name: &str, event: Vec<(&str, &str)>) -> Option { + SESSIONS.read().unwrap().get(peer)?.push_event(name, event) +} + +#[inline] +pub fn push_global_event(channel: &str, event: String) -> Option { + Some(GLOBAL_EVENT_STREAM.read().unwrap().get(channel)?.add(event)) +} + +pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { + if let Some(_) = GLOBAL_EVENT_STREAM + .write() + .unwrap() + .insert(app_type.clone(), s) + { + log::warn!( + "Global event stream of type {} is started before, but now removed", + app_type + ); + } + Ok(()) +} + +pub fn stop_global_event_stream(app_type: String) { + let _ = GLOBAL_EVENT_STREAM.write().unwrap().remove(&app_type); +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a3e8c38f2..b5cc669ab 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -9,6 +9,9 @@ use crate::{ ui_interface::{self, *}, }; use flutter_rust_bridge::{StreamSink, SyncReturn}; +#[cfg(feature = "plugin_framework")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::allow_err; use hbb_common::{ config::{self, LocalConfig, PeerConfig, PeerInfoSerde, ONLINE}, fs, log, @@ -48,32 +51,20 @@ fn initialize(app_dir: &str) { } } +#[inline] +pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { + super::flutter::start_global_event_stream(s, app_type) +} + +#[inline] +pub fn stop_global_event_stream(app_type: String) { + super::flutter::stop_global_event_stream(app_type) +} pub enum EventToUI { Event(String), Rgba, } -pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { - if let Some(_) = flutter::GLOBAL_EVENT_STREAM - .write() - .unwrap() - .insert(app_type.clone(), s) - { - log::warn!( - "Global event stream of type {} is started before, but now removed", - app_type - ); - } - Ok(()) -} - -pub fn stop_global_event_stream(app_type: String) { - let _ = flutter::GLOBAL_EVENT_STREAM - .write() - .unwrap() - .remove(&app_type); -} - pub fn host_stop_system_key_propagate(_stopped: bool) { #[cfg(windows)] crate::platform::windows::stop_system_key_propagate(_stopped); @@ -136,9 +127,15 @@ pub fn session_get_option(id: String, arg: String) -> Option { } } -pub fn session_login(id: String, password: String, remember: bool) { +pub fn session_login( + id: String, + os_username: String, + os_password: String, + password: String, + remember: bool, +) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.login(password, remember); + session.login(os_username, os_password, password, remember); } } @@ -168,14 +165,12 @@ pub fn session_reconnect(id: String, force_relay: bool) { } pub fn session_toggle_option(id: String, value: String) { - let mut is_found = false; if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { - is_found = true; log::warn!("toggle option {}", &value); session.toggle_option(value.clone()); } #[cfg(not(any(target_os = "android", target_os = "ios")))] - if is_found && value == "disable-clipboard" { + if SESSIONS.read().unwrap().get(&id).is_some() && value == "disable-clipboard" { crate::flutter::update_text_clipboard_required(); } } @@ -328,20 +323,26 @@ pub fn session_switch_display(id: String, value: i32) { pub fn session_handle_flutter_key_event( id: String, name: String, - keycode: i32, - scancode: i32, + platform_code: i32, + position_code: i32, lock_modes: i32, down_or_up: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.handle_flutter_key_event(&name, keycode, scancode, lock_modes, down_or_up); + session.handle_flutter_key_event( + &name, + platform_code, + position_code, + lock_modes, + down_or_up, + ); } } -pub fn session_enter_or_leave(id: String, enter: bool) { +pub fn session_enter_or_leave(_id: String, _enter: bool) { #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - if enter { + if let Some(session) = SESSIONS.read().unwrap().get(&_id) { + if _enter { session.enter(); } else { session.leave(); @@ -735,20 +736,18 @@ pub fn main_load_recent_peers() { .drain(..) .map(|(id, _, p)| peer_to_map(id, p)) .collect(); - if let Some(s) = flutter::GLOBAL_EVENT_STREAM - .read() - .unwrap() - .get(flutter::APP_TYPE_MAIN) - { - let data = HashMap::from([ - ("name", "load_recent_peers".to_owned()), - ( - "peers", - serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), - ), - ]); - s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); - }; + + let data = HashMap::from([ + ("name", "load_recent_peers".to_owned()), + ( + "peers", + serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), + ), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); } } @@ -786,38 +785,33 @@ pub fn main_load_fav_peers() { } }) .collect(); - if let Some(s) = flutter::GLOBAL_EVENT_STREAM - .read() - .unwrap() - .get(flutter::APP_TYPE_MAIN) - { - let data = HashMap::from([ - ("name", "load_fav_peers".to_owned()), - ( - "peers", - serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), - ), - ]); - s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); - }; + + let data = HashMap::from([ + ("name", "load_fav_peers".to_owned()), + ( + "peers", + serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), + ), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); } } pub fn main_load_lan_peers() { - if let Some(s) = flutter::GLOBAL_EVENT_STREAM - .read() - .unwrap() - .get(flutter::APP_TYPE_MAIN) - { - let data = HashMap::from([ - ("name", "load_lan_peers".to_owned()), - ( - "peers", - serde_json::to_string(&get_lan_peers()).unwrap_or_default(), - ), - ]); - s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); - }; + let data = HashMap::from([ + ("name", "load_lan_peers".to_owned()), + ( + "peers", + serde_json::to_string(&get_lan_peers()).unwrap_or_default(), + ), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); } pub fn main_remove_discovered(id: String) { @@ -831,20 +825,21 @@ fn main_broadcast_message(data: &HashMap<&str, &str>) { flutter::APP_TYPE_DESKTOP_PORT_FORWARD, ]; + let event = serde_json::ser::to_string(&data).unwrap_or("".to_owned()); for app in apps { - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(app) { - s.add(serde_json::ser::to_string(data).unwrap_or("".to_owned())); - }; + let _res = flutter::push_global_event(app, event.clone()); } } pub fn main_change_theme(dark: String) { main_broadcast_message(&HashMap::from([("name", "theme"), ("dark", &dark)])); + #[cfg(not(any(target_os = "ios")))] send_to_cm(&crate::ipc::Data::Theme(dark)); } pub fn main_change_language(lang: String) { main_broadcast_message(&HashMap::from([("name", "language"), ("lang", &lang)])); + #[cfg(not(any(target_os = "ios")))] send_to_cm(&crate::ipc::Data::Language(lang)); } @@ -931,6 +926,10 @@ pub fn main_get_permanent_password() -> String { ui_interface::permanent_password() } +pub fn main_get_fingerprint() -> String { + get_fingerprint() +} + pub fn main_get_online_statue() -> i64 { ONLINE.lock().unwrap().values().max().unwrap_or(&0).clone() } @@ -971,6 +970,13 @@ pub fn main_has_hwcodec() -> SyncReturn { SyncReturn(has_hwcodec()) } +pub fn main_supported_hwdecodings() -> SyncReturn { + let decoding = supported_hwdecodings(); + let msg = HashMap::from([("h264", decoding.0), ("h265", decoding.1)]); + + SyncReturn(serde_json::ser::to_string(&msg).unwrap_or("".to_owned())) +} + pub fn main_is_root() -> bool { is_root() } @@ -1056,10 +1062,10 @@ pub fn session_send_note(id: String, note: String) { } } -pub fn session_supported_hwcodec(id: String) -> String { +pub fn session_alternative_codecs(id: String) -> String { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - let (h264, h265) = session.supported_hwcodec(); - let msg = HashMap::from([("h264", h264), ("h265", h265)]); + let (vp8, h264, h265) = session.alternative_codecs(); + let msg = HashMap::from([("vp8", vp8), ("h264", h264), ("h265", h265)]); serde_json::ser::to_string(&msg).unwrap_or("".to_owned()) } else { String::new() @@ -1127,6 +1133,8 @@ pub fn main_get_mouse_time() -> f64 { } pub fn main_wol(id: String) { + // TODO: move send_wol outside. + #[cfg(not(any(target_os = "ios")))] crate::lan::send_wol(id) } @@ -1136,10 +1144,12 @@ pub fn main_create_shortcut(_id: String) { } pub fn cm_send_chat(conn_id: i32, msg: String) { + #[cfg(not(any(target_os = "ios")))] crate::ui_cm_interface::send_chat(conn_id, msg); } pub fn cm_login_res(conn_id: i32, res: bool) { + #[cfg(not(any(target_os = "ios")))] if res { crate::ui_cm_interface::authorize(conn_id); } else { @@ -1148,22 +1158,29 @@ pub fn cm_login_res(conn_id: i32, res: bool) { } pub fn cm_close_connection(conn_id: i32) { + #[cfg(not(any(target_os = "ios")))] crate::ui_cm_interface::close(conn_id); } pub fn cm_remove_disconnected_connection(conn_id: i32) { + #[cfg(not(any(target_os = "ios")))] crate::ui_cm_interface::remove(conn_id); } pub fn cm_check_click_time(conn_id: i32) { + #[cfg(not(any(target_os = "ios")))] crate::ui_cm_interface::check_click_time(conn_id) } pub fn cm_get_click_time() -> f64 { - crate::ui_cm_interface::get_click_time() as _ + #[cfg(not(any(target_os = "ios")))] + return crate::ui_cm_interface::get_click_time() as _; + #[cfg(any(target_os = "ios"))] + return 0 as _; } pub fn cm_switch_permission(conn_id: i32, name: String, enabled: bool) { + #[cfg(not(any(target_os = "ios")))] crate::ui_cm_interface::switch_permission(conn_id, name, enabled) } @@ -1172,10 +1189,12 @@ pub fn cm_can_elevate() -> SyncReturn { } pub fn cm_elevate_portable(conn_id: i32) { + #[cfg(not(any(target_os = "ios")))] crate::ui_cm_interface::elevate_portable(conn_id); } pub fn cm_switch_back(conn_id: i32) { + #[cfg(not(any(target_os = "ios")))] crate::ui_cm_interface::switch_back(conn_id); } @@ -1196,21 +1215,19 @@ unsafe extern "C" fn translate(name: *const c_char, locale: *const c_char) -> *c } fn handle_query_onlines(onlines: Vec, offlines: Vec) { - if let Some(s) = flutter::GLOBAL_EVENT_STREAM - .read() - .unwrap() - .get(flutter::APP_TYPE_MAIN) - { - let data = HashMap::from([ - ("name", "callback_query_onlines".to_owned()), - ("onlines", onlines.join(",")), - ("offlines", offlines.join(",")), - ]); - s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); - }; + let data = HashMap::from([ + ("name", "callback_query_onlines".to_owned()), + ("onlines", onlines.join(",")), + ("offlines", offlines.join(",")), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); } pub fn query_onlines(ids: Vec) { + #[cfg(not(any(target_os = "ios")))] crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) } @@ -1379,6 +1396,67 @@ pub fn send_url_scheme(_url: String) { std::thread::spawn(move || crate::handle_url_scheme(_url)); } +#[inline] +pub fn plugin_event(_id: String, _event: Vec) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + allow_err!(crate::plugin::handle_ui_event(&_id, &_event)); + } +} + +#[inline] +pub fn plugin_get_session_option(_id: String, _peer: String, _key: String) -> SyncReturn> { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + return SyncReturn(crate::plugin::PeerConfig::get(&_id, &_peer, &_key)); + } + #[cfg(any( + not(feature = "plugin_framework"), + target_os = "android", + target_os = "ios" + ))] + { + return SyncReturn(None); + } +} + +#[inline] +pub fn plugin_set_session_option(_id: String, _peer: String, _key: String, _value: String) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + crate::plugin::PeerConfig::set(&_id, &_peer, &_key, &_value); + } +} + +#[inline] +pub fn plugin_get_local_option(_id: String, _key: String) -> SyncReturn> { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + return SyncReturn(crate::plugin::LocalConfig::get(&_id, &_key)); + } + #[cfg(any( + not(feature = "plugin_framework"), + target_os = "android", + target_os = "ios" + ))] + { + return SyncReturn(None); + } +} + +#[inline] +pub fn plugin_set_local_option(_id: String, _key: String, _value: String) { + #[cfg(feature = "plugin_framework")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + crate::plugin::LocalConfig::set(&_id, &_key, &_value); + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; @@ -1405,7 +1483,7 @@ pub mod server_side { #[no_mangle] pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startService( - env: JNIEnv, + _env: JNIEnv, _class: JClass, ) { log::debug!("startService from jvm"); diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index a060d6a20..cd7f95208 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -6,24 +6,28 @@ use hbb_common::{ }; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; - +#[cfg(not(any(target_os = "ios")))] use crate::Connection; const TIME_HEARTBEAT: Duration = Duration::from_secs(30); const TIME_CONN: Duration = Duration::from_secs(3); +#[cfg(not(any(target_os = "ios")))] lazy_static::lazy_static! { static ref SENDER : Mutex>> = Mutex::new(start_hbbs_sync()); } +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn start() { let _sender = SENDER.lock().unwrap(); } +#[cfg(not(target_os = "ios"))] pub fn signal_receiver() -> broadcast::Receiver> { SENDER.lock().unwrap().subscribe() } +#[cfg(not(any(target_os = "ios")))] fn start_hbbs_sync() -> broadcast::Sender> { let (tx, _rx) = broadcast::channel::>(16); std::thread::spawn(move || start_hbbs_sync_async()); @@ -36,6 +40,7 @@ pub struct StrategyOptions { pub extra: HashMap, } +#[cfg(not(any(target_os = "ios")))] #[tokio::main(flavor = "current_thread")] async fn start_hbbs_sync_async() { tokio::spawn(async move { diff --git a/src/ipc.rs b/src/ipc.rs index 5f415c6e8..a3222dd02 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -383,6 +383,12 @@ async fn handle(data: Data, stream: &mut Connection) { )); } else if name == "rendezvous_servers" { value = Some(Config::get_rendezvous_servers().join(",")); + } else if name == "fingerprint" { + value = if Config::get_key_confirmed() { + Some(crate::common::pk_to_fingerprint(Config::get_key_pair().1)) + } else { + None + }; } else { value = None; } @@ -690,6 +696,12 @@ pub fn get_permanent_password() -> String { } } +pub fn get_fingerprint() -> String { + get_config("fingerprint") + .unwrap_or_default() + .unwrap_or_default() +} + pub fn set_permanent_password(v: String) -> ResultType<()> { Config::set_permanent_password(&v); set_config("permanent-password", v) diff --git a/src/keyboard.rs b/src/keyboard.rs index 42cff9ce0..e6e9da35a 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1,22 +1,24 @@ -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::client::get_key_state; -use crate::common::GrabState; #[cfg(feature = "flutter")] use crate::flutter::{CUR_SESSION_ID, SESSIONS}; #[cfg(target_os = "windows")] -use crate::platform::windows::get_char_by_vk; +use crate::platform::windows::{get_char_from_vk, get_unicode_from_vk}; #[cfg(not(any(feature = "flutter", feature = "cli")))] use crate::ui::CUR_SESSION; #[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::{client::get_key_state, common::GrabState}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::log; use hbb_common::message_proto::*; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use rdev::KeyCode; use rdev::{Event, EventType, Key}; #[cfg(any(target_os = "windows", target_os = "macos"))] use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use std::time::SystemTime; use std::{ collections::{HashMap, HashSet}, sync::{Arc, Mutex}, - time::SystemTime, }; #[cfg(windows)] @@ -71,6 +73,7 @@ pub mod client { super::start_grab_loop(); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn change_grab_status(state: GrabState) { match state { GrabState::Ready => {} @@ -177,6 +180,7 @@ pub mod client { } #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn lock_screen() { send_key_event(&event_lock_screen()); } @@ -196,6 +200,7 @@ pub mod client { } #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn ctrl_alt_del() { send_key_event(&event_ctrl_alt_del()); } @@ -227,8 +232,8 @@ pub fn start_grab_loop() { } let mut _keyboard_mode = KeyboardMode::Map; - let _scan_code = event.scan_code; - let _code = event.code; + let _scan_code = event.position_code; + let _code = event.platform_code as KeyCode; let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { _keyboard_mode = client::process_event(&event, None); if is_press { @@ -264,7 +269,7 @@ pub fn start_grab_loop() { #[cfg(target_os = "macos")] unsafe { - if _code as u32 == rdev::kVK_Option { + if _code == rdev::kVK_Option { IS_LEFT_OPTION_DOWN = is_press; } } @@ -316,6 +321,7 @@ pub fn is_long_press(event: &Event) -> bool { return false; } +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn release_remote_keys() { // todo!: client quit suddenly, how to release keys? let to_release = TO_RELEASE.lock().unwrap().clone(); @@ -333,19 +339,117 @@ pub fn get_keyboard_mode_enum() -> KeyboardMode { match client::get_keyboard_mode().as_str() { "map" => KeyboardMode::Map, "translate" => KeyboardMode::Translate, - _ => KeyboardMode::Legacy, + "legacy" => KeyboardMode::Legacy, + _ => { + // Set "map" as default mode if version >= 1.2.0. + if crate::is_peer_version_ge("1.2.0") { + KeyboardMode::Map + } else { + KeyboardMode::Legacy + } + } } } +#[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] -fn add_numlock_capslock_with_lock_modes(key_event: &mut KeyEvent, lock_modes: i32) { +pub fn is_modifier(key: &rdev::Key) -> bool { + matches!( + key, + Key::ShiftLeft + | Key::ShiftRight + | Key::ControlLeft + | Key::ControlRight + | Key::MetaLeft + | Key::MetaRight + | Key::Alt + | Key::AltGr + ) +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn is_numpad_rdev_key(key: &rdev::Key) -> bool { + matches!( + key, + Key::Kp0 + | Key::Kp1 + | Key::Kp2 + | Key::Kp3 + | Key::Kp4 + | Key::Kp5 + | Key::Kp6 + | Key::Kp7 + | Key::Kp8 + | Key::Kp9 + | Key::KpMinus + | Key::KpMultiply + | Key::KpDivide + | Key::KpPlus + | Key::KpDecimal + ) +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn is_letter_rdev_key(key: &rdev::Key) -> bool { + matches!( + key, + Key::KeyA + | Key::KeyB + | Key::KeyC + | Key::KeyD + | Key::KeyE + | Key::KeyF + | Key::KeyG + | Key::KeyH + | Key::KeyI + | Key::KeyJ + | Key::KeyK + | Key::KeyL + | Key::KeyM + | Key::KeyN + | Key::KeyO + | Key::KeyP + | Key::KeyQ + | Key::KeyR + | Key::KeyS + | Key::KeyT + | Key::KeyU + | Key::KeyV + | Key::KeyW + | Key::KeyX + | Key::KeyY + | Key::KeyZ + ) +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn is_numpad_key(event: &Event) -> bool { + matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if is_numpad_rdev_key(&key)) +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn is_letter_key(event: &Event) -> bool { + matches!(event.event_type, EventType::KeyPress(key) | EventType::KeyRelease(key) if is_letter_rdev_key(&key)) +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn parse_add_lock_modes_modifiers( + key_event: &mut KeyEvent, + lock_modes: i32, + is_numpad_key: bool, + is_letter_key: bool, +) { const CAPS_LOCK: i32 = 1; const NUM_LOCK: i32 = 2; // const SCROLL_LOCK: i32 = 3; - if lock_modes & (1 << CAPS_LOCK) != 0 { + if is_letter_key && (lock_modes & (1 << CAPS_LOCK) != 0) { key_event.modifiers.push(ControlKey::CapsLock.into()); } - if lock_modes & (1 << NUM_LOCK) != 0 { + if is_numpad_key && lock_modes & (1 << NUM_LOCK) != 0 { key_event.modifiers.push(ControlKey::NumLock.into()); } // if lock_modes & (1 << SCROLL_LOCK) != 0 { @@ -354,11 +458,11 @@ fn add_numlock_capslock_with_lock_modes(key_event: &mut KeyEvent, lock_modes: i3 } #[cfg(not(any(target_os = "android", target_os = "ios")))] -fn add_numlock_capslock_status(key_event: &mut KeyEvent) { - if get_key_state(enigo::Key::CapsLock) { +fn add_lock_modes_modifiers(key_event: &mut KeyEvent, is_numpad_key: bool, is_letter_key: bool) { + if is_letter_key && get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } - if get_key_state(enigo::Key::NumLock) { + if is_numpad_key && get_key_state(enigo::Key::NumLock) { key_event.modifiers.push(ControlKey::NumLock.into()); } } @@ -405,7 +509,7 @@ fn update_modifiers_state(event: &Event) { pub fn event_to_key_events( event: &Event, keyboard_mode: KeyboardMode, - lock_modes: Option, + _lock_modes: Option, ) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -424,7 +528,12 @@ pub fn event_to_key_events( peer.retain(|c| !c.is_whitespace()); key_event.mode = keyboard_mode.into(); - let mut key_events = match keyboard_mode { + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let mut key_events; + #[cfg(any(target_os = "android", target_os = "ios"))] + let key_events; + key_events = match keyboard_mode { KeyboardMode::Map => match map_keyboard_mode(peer.as_str(), event, key_event) { Some(event) => [event].to_vec(), None => Vec::new(), @@ -442,26 +551,30 @@ pub fn event_to_key_events( } }; - if keyboard_mode != KeyboardMode::Translate { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let is_numpad_key = is_numpad_key(&event); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if keyboard_mode != KeyboardMode::Translate || is_numpad_key { + let is_letter_key = is_letter_key(&event); for key_event in &mut key_events { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(lock_modes) = lock_modes { - add_numlock_capslock_with_lock_modes(key_event, lock_modes); + if let Some(lock_modes) = _lock_modes { + parse_add_lock_modes_modifiers(key_event, lock_modes, is_numpad_key, is_letter_key); } else { - add_numlock_capslock_status(key_event); + add_lock_modes_modifiers(key_event, is_numpad_key, is_letter_key); } } } key_events } +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn event_type_to_event(event_type: EventType) -> Event { Event { event_type, time: SystemTime::now(), unicode: None, - code: 0, - scan_code: 0, + platform_code: 0, + position_code: 0, } } @@ -546,7 +659,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec> 8) == 0xE0 { + if (event.position_code >> 8) == 0xE0 { unsafe { IS_ALT_GR = true; } @@ -725,7 +838,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec Option { +pub fn map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Option { match event.event_type { EventType::KeyPress(..) => { key_event.down = true; @@ -737,56 +850,58 @@ pub fn map_keyboard_mode(peer: &str, event: &Event, mut key_event: KeyEvent) -> }; #[cfg(target_os = "windows")] - let keycode = match peer { + let keycode = match _peer { OS_LOWER_WINDOWS => { // https://github.com/rustdesk/rustdesk/issues/1371 // Filter scancodes that are greater than 255 and the hight word is not 0xE0. - if event.scan_code > 255 && (event.scan_code >> 8) != 0xE0 { + if event.position_code > 255 && (event.position_code >> 8) != 0xE0 { return None; } - event.scan_code + event.position_code } OS_LOWER_MACOS => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { - rdev::win_scancode_to_macos_iso_code(event.scan_code)? + rdev::win_scancode_to_macos_iso_code(event.position_code)? } else { - rdev::win_scancode_to_macos_code(event.scan_code)? + rdev::win_scancode_to_macos_code(event.position_code)? } } - _ => rdev::win_scancode_to_linux_code(event.scan_code)?, + _ => rdev::win_scancode_to_linux_code(event.position_code)?, }; #[cfg(target_os = "macos")] - let keycode = match peer { - OS_LOWER_WINDOWS => rdev::macos_code_to_win_scancode(event.code as _)?, - OS_LOWER_MACOS => event.code as _, - _ => rdev::macos_code_to_linux_code(event.code as _)?, + let keycode = match _peer { + OS_LOWER_WINDOWS => rdev::macos_code_to_win_scancode(event.platform_code as _)?, + OS_LOWER_MACOS => event.platform_code as _, + _ => rdev::macos_code_to_linux_code(event.platform_code as _)?, }; #[cfg(target_os = "linux")] - let keycode = match peer { - OS_LOWER_WINDOWS => rdev::linux_code_to_win_scancode(event.code as _)?, + let keycode = match _peer { + OS_LOWER_WINDOWS => rdev::linux_code_to_win_scancode(event.position_code as _)?, OS_LOWER_MACOS => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { - rdev::linux_code_to_macos_iso_code(event.code as _)? + rdev::linux_code_to_macos_iso_code(event.position_code as _)? } else { - rdev::linux_code_to_macos_code(event.code as _)? + rdev::linux_code_to_macos_code(event.position_code as _)? } } - _ => event.code as _, + _ => event.position_code as _, }; #[cfg(any(target_os = "android", target_os = "ios"))] let keycode = 0; - key_event.set_chr(keycode); + key_event.set_chr(keycode as _); Some(key_event) } -fn try_fill_unicode(event: &Event, key_event: &KeyEvent, events: &mut Vec) { +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn try_fill_unicode(_peer: &str, event: &Event, key_event: &KeyEvent, events: &mut Vec) { match &event.unicode { Some(unicode_info) => { if let Some(name) = &unicode_info.name { if name.len() > 0 { let mut evt = key_event.clone(); evt.set_seq(name.to_string()); + evt.down = true; events.push(evt); } } @@ -794,17 +909,50 @@ fn try_fill_unicode(event: &Event, key_event: &KeyEvent, events: &mut Vec { #[cfg(target_os = "windows")] - if is_hot_key_modifiers_down() && unsafe { !IS_0X021D_DOWN } { - if let Some(chr) = get_char_by_vk(event.code as u32) { - let mut evt = key_event.clone(); - evt.set_seq(chr.to_string()); - events.push(evt); + if _peer == OS_LOWER_LINUX { + if is_hot_key_modifiers_down() && unsafe { !IS_0X021D_DOWN } { + if let Some(chr) = get_char_from_vk(event.platform_code as u32) { + let mut evt = key_event.clone(); + evt.set_seq(chr.to_string()); + evt.down = true; + events.push(evt); + } } } } } } +#[cfg(target_os = "windows")] +fn try_file_win2win_hotkey( + peer: &str, + event: &Event, + key_event: &KeyEvent, + events: &mut Vec, +) { + if peer == OS_LOWER_WINDOWS && is_hot_key_modifiers_down() && unsafe { !IS_0X021D_DOWN } { + let mut down = false; + let win2win_hotkey = match event.event_type { + EventType::KeyPress(..) => { + down = true; + if let Some(unicode) = get_unicode_from_vk(event.platform_code as u32) { + Some((unicode as u32 & 0x0000FFFF) | (event.platform_code << 16)) + } else { + None + } + } + EventType::KeyRelease(..) => Some(event.platform_code << 16), + _ => None, + }; + if let Some(code) = win2win_hotkey { + let mut evt = key_event.clone(); + evt.set_win2win_hotkey(code); + evt.down = down; + events.push(evt); + } + } +} + #[cfg(target_os = "windows")] fn is_hot_key_modifiers_down() -> bool { if rdev::get_modifier(Key::ControlLeft) || rdev::get_modifier(Key::ControlRight) { @@ -820,21 +968,33 @@ fn is_hot_key_modifiers_down() -> bool { } #[inline] -#[cfg(target_os = "windows")] -pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option { - let mut key_event = map_keyboard_mode(peer, event, key_event)?; - key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); - Some(key_event) +#[cfg(any(target_os = "linux", target_os = "windows"))] +fn is_altgr(event: &Event) -> bool { + #[cfg(target_os = "linux")] + if event.platform_code == 0xFE03 { + true + } else { + false + } + + #[cfg(target_os = "windows")] + if unsafe { IS_0X021D_DOWN } && event.position_code == 0xE038 { + true + } else { + false + } } #[inline] -#[cfg(not(target_os = "windows"))] -pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option { - map_keyboard_mode(peer, event, key_event) +#[cfg(any(target_os = "linux", target_os = "windows"))] +fn is_press(event: &Event) -> bool { + matches!(event.event_type, EventType::KeyPress(_)) } +// https://github.com/fufesou/rustdesk/wiki/Keyboard-mode----Translate-Mode pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -> Vec { let mut events: Vec = Vec::new(); + if let Some(unicode_info) = &event.unicode { if unicode_info.is_dead { #[cfg(target_os = "macos")] @@ -849,30 +1009,39 @@ pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) - } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if is_numpad_key(&event) { + if let Some(evt) = map_keyboard_mode(peer, event, key_event) { + events.push(evt); + } + return events; + } + #[cfg(target_os = "macos")] // ignore right option key - if event.code as u32 == rdev::kVK_RightOption { + if event.platform_code == rdev::kVK_RightOption as u32 { + return events; + } + + #[cfg(any(target_os = "linux", target_os = "windows"))] + if is_altgr(event) { return events; } #[cfg(target_os = "windows")] - unsafe { - if event.scan_code == 0x021D { - return events; - } - - if IS_0X021D_DOWN { - if event.scan_code == 0xE038 { - return events; - } - } + if event.position_code == 0x021D { + return events; } #[cfg(target_os = "windows")] - if matches!(event.event_type, EventType::KeyPress(_)) { - try_fill_unicode(event, &key_event, &mut events); + try_file_win2win_hotkey(peer, event, &key_event, &mut events); + + #[cfg(any(target_os = "linux", target_os = "windows"))] + if events.is_empty() && is_press(event) { + try_fill_unicode(peer, event, &key_event, &mut events); } + // If AltGr is down, no need to send events other than unicode. #[cfg(target_os = "windows")] unsafe { if IS_0X021D_DOWN { @@ -880,18 +1049,25 @@ pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) - } } - #[cfg(target_os = "linux")] - try_fill_unicode(event, &key_event, &mut events); - #[cfg(target_os = "macos")] if !unsafe { IS_LEFT_OPTION_DOWN } { - try_fill_unicode(event, &key_event, &mut events); + try_fill_unicode(peer, event, &key_event, &mut events); } if events.is_empty() { - if let Some(evt) = translate_key_code(peer, event, key_event) { + if let Some(evt) = map_keyboard_mode(peer, event, key_event) { events.push(evt); } } events } + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn keycode_to_rdev_key(keycode: u32) -> Key { + #[cfg(target_os = "windows")] + return rdev::win_key_from_scancode(keycode); + #[cfg(target_os = "linux")] + return rdev::linux_key_from_code(keycode); + #[cfg(target_os = "macos")] + return rdev::macos_key_from_code(keycode.try_into().unwrap_or_default()); +} diff --git a/src/lan.rs b/src/lan.rs index 5b52ee1a9..15914965b 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -1,7 +1,9 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::config::Config; use hbb_common::{ allow_err, anyhow::bail, - config::{self, Config, RENDEZVOUS_PORT}, + config::{self, RENDEZVOUS_PORT}, log, protobuf::Message as _, rendezvous_proto::*, @@ -11,6 +13,7 @@ use hbb_common::{ }, ResultType, }; + use std::{ collections::{HashMap, HashSet}, net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs, UdpSocket}, @@ -19,6 +22,7 @@ use std::{ type Message = RendezvousMessage; +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub(super) fn start_listening() -> ResultType<()> { let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port())); let socket = std::net::UdpSocket::bind(addr)?; @@ -98,9 +102,9 @@ fn get_broadcast_port() -> u16 { (RENDEZVOUS_PORT + 3) as _ } -fn get_mac(ip: &IpAddr) -> String { +fn get_mac(_ip: &IpAddr) -> String { #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Ok(mac) = get_mac_by_ip(ip) { + if let Ok(mac) = get_mac_by_ip(_ip) { mac.to_string() } else { "".to_owned() @@ -119,6 +123,7 @@ fn get_all_ipv4s() -> ResultType> { Ok(ipv4s) } +#[cfg(not(any(target_os = "android", target_os = "ios")))] fn get_mac_by_ip(ip: &IpAddr) -> ResultType { for interface in default_net::get_interfaces() { match ip { diff --git a/src/lang.rs b/src/lang.rs index 972510325..6de810f23 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -5,12 +5,12 @@ mod cn; mod cs; mod da; mod de; +mod el; mod en; mod eo; mod es; mod fa; mod fr; -mod el; mod hu; mod id; mod it; @@ -32,6 +32,7 @@ mod tr; mod tw; mod ua; mod vn; +mod lt; pub const LANGS: &[(&str, &str)] = &[ ("en", "English"), @@ -66,6 +67,7 @@ pub const LANGS: &[(&str, &str)] = &[ ("th", "ภาษาไทย"), ("sl", "Slovenščina"), ("ro", "Română"), + ("lt", "Lietuvių"), ]; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -129,6 +131,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "th" => th::T.deref(), "sl" => sl::T.deref(), "ro" => ro::T.deref(), + "lt" => lt::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { diff --git a/src/lang/ca.rs b/src/lang/ca.rs index c77d73944..49fd2fb89 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 14dc440ae..2daa44c64 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "此文件与对方的一致"), ("show_monitors_tip", ""), ("View Mode", "浏览模式"), + ("login_linux_tip", "登录被控端的 Linux 账户,才能启用 X 桌面"), + ("verify_rustdesk_password_tip", "验证 RustDesk 密码"), + ("remember_account_tip", "记住此账户"), + ("os_account_desk_tip", "在无显示器的环境下,此账户用于登录被控系统,并启用桌面"), + ("OS Account", "系统账户"), + ("another_user_login_title_tip", "其他用户已登录"), + ("another_user_login_text_tip", "断开"), + ("xorg_not_found_title_tip", "Xorg 未安装"), + ("xorg_not_found_text_tip", "请安装 Xorg"), + ("no_desktop_title_tip", "desktop 未安装"), + ("no_desktop_text_tip", "请安装 desktop"), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", "指纹"), + ("Copy Fingerprint", "复制指纹"), + ("no fingerprints", "没有指纹"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 5e8bdba5f..3bbd3e9eb 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 542543155..d22cac717 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 58ea77d6d..5112a8d35 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -113,7 +113,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Finished", "Fertiggestellt"), ("Speed", "Geschwindigkeit"), ("Custom Image Quality", "Benutzerdefinierte Bildqualität"), - ("Privacy mode", "Datenschutz-Modus"), + ("Privacy mode", "Datenschutzmodus"), ("Block user input", "Benutzereingaben blockieren"), ("Unblock user input", "Benutzereingaben freigeben"), ("Adjust Window", "Fenster anpassen"), @@ -123,8 +123,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scrollbar", "Scroll-Leiste"), ("ScrollAuto", "Automatisch scrollen"), ("Good image quality", "Hohe Bildqualität"), - ("Balanced", "Ausgeglichen"), - ("Optimize reaction time", "Geschwindigkeit"), + ("Balanced", "Ausgeglichene Bildqualität"), + ("Optimize reaction time", "Reaktionszeit optimieren"), ("Custom", "Benutzerdefiniert"), ("Show remote cursor", "Entfernten Cursor anzeigen"), ("Show quality monitor", "Qualitätsüberwachung anzeigen"), @@ -181,7 +181,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnect", "Verbindung trennen"), ("Allow using keyboard and mouse", "Verwendung von Maus und Tastatur zulassen"), ("Allow using clipboard", "Verwendung der Zwischenablage zulassen"), - ("Allow hearing sound", "System-Audio übertragen"), + ("Allow hearing sound", "Soundübertragung zulassen"), ("Allow file copy and paste", "Kopieren und Einfügen von Dateien zulassen"), ("Connected", "Verbunden"), ("Direct and encrypted connection", "Direkte und verschlüsselte Verbindung"), @@ -218,7 +218,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), ("Verify", "Überprüfen"), - ("Remember me", "Login speichern"), + ("Remember me", "Login merken"), ("Trust this device", "Diesem Gerät vertrauen"), ("Verification code", "Verifizierungscode"), ("verification_tip", "Es wurde ein neues Gerät erkannt und ein Verifizierungscode an die registrierte E-Mail-Adresse gesendet. Geben Sie den Verifizierungscode ein, um sich erneut anzumelden."), @@ -278,13 +278,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Input Control", "Eingabesteuerung"), ("Audio Capture", "Audioaufnahme"), ("File Connection", "Dateiverbindung"), - ("Screen Connection", "Bildschirmanschluss"), + ("Screen Connection", "Bildschirmverbindung"), ("Do you accept?", "Verbindung zulassen?"), ("Open System Setting", "Systemeinstellung öffnen"), ("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"), ("android_input_permission_tip1", "Damit ein entferntes Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."), ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen Sie \"Installierte Dienste\" und schalten Sie den Dienst \"RustDesk Input\" ein."), - ("android_new_connection_tip", "möchte ihr Gerät steuern."), + ("android_new_connection_tip", "möchte Ihr Gerät steuern."), ("android_service_will_start_tip", "Durch das Aktivieren der Bildschirmfreigabe wird der Dienst automatisch gestartet, sodass andere Geräte dieses Android-Gerät steuern können."), ("android_stop_service_tip", "Durch das Deaktivieren des Dienstes werden automatisch alle hergestellten Verbindungen getrennt."), ("android_version_audio_tip", "Ihre Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher, falls möglich."), @@ -315,7 +315,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start the screen sharing service on boot, requires special permissions", "Bildschirmfreigabedienst beim Booten starten, erfordert zusätzliche Berechtigungen"), ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", "Kompatibilitätsmodus"), - ("Map mode", "Kartenmodus"), + ("Map mode", "Zuordnungsmodus"), ("Translate mode", "Übersetzungsmodus"), ("Use permanent password", "Permanentes Passwort verwenden"), ("Use both passwords", "Beide Passwörter verwenden"), @@ -357,7 +357,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Audio", "Audio aktivieren"), ("Unlock Network Settings", "Netzwerkeinstellungen entsperren"), ("Server", "Server"), - ("Direct IP Access", "Direkter IP-Zugriff"), + ("Direct IP Access", "Direkter IP-Zugang"), ("Proxy", "Proxy"), ("Apply", "Anwenden"), ("Disconnect all devices?", "Alle Geräte trennen?"), @@ -420,7 +420,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), ("Always use software rendering", "Software-Rendering immer verwenden"), - ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk die Berechtigung \"Input Monitoring\" erteilen."), + ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk die Berechtigung \"Eingabeüberwachung\" erteilen."), ("config_microphone", "Um aus der Ferne sprechen zu können, müssen Sie RustDesk die Berechtigung \"Audio aufzeichnen\" erteilen."), ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), ("Wait", "Warten"), @@ -428,7 +428,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ask the remote user for authentication", "Den entfernten Benutzer zur Authentifizierung auffordern"), ("Choose this if the remote account is administrator", "Wählen Sie dies, wenn das entfernte Konto Administrator ist."), ("Transmit the username and password of administrator", "Übermitteln Sie den Benutzernamen und das Passwort des Administrators"), - ("still_click_uac_tip", "Der entfernte Benutzer muss immer noch im UAC-Fenster von RustDesk auf OK klicken."), + ("still_click_uac_tip", "Der entfernte Benutzer muss immer noch im UAC-Fenster von RustDesk auf \"Ja\" klicken."), ("Request Elevation", "Erhöhte Rechte anfordern"), ("wait_accept_uac_tip", "Bitte warten Sie, bis der entfernte Benutzer den UAC-Dialog akzeptiert hat."), ("Elevate successfully", "Erhöhung der Rechte erfolgreich"), @@ -477,8 +477,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Username", "Leerer Benutzername"), ("Empty Password", "Leeres Passwort"), ("Me", "Ich"), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), + ("identical_file_tip", "Diese Datei ist identisch mit der Datei der Gegenstelle."), + ("show_monitors_tip", "Monitore in der Symbolleiste anzeigen"), + ("View Mode", "Ansichtsmodus"), + ("login_linux_tip", "Sie müssen sich an einem entfernten Linux-Konto anmelden, um eine X-Desktop-Sitzung zu eröffnen."), + ("verify_rustdesk_password_tip", "RustDesk-Passwort bestätigen"), + ("remember_account_tip", "Dieses Konto merken"), + ("os_account_desk_tip", "Dieses Konto wird verwendet, um sich beim entfernten Betriebssystem anzumelden und die Desktop-Sitzung im Headless-Modus zu aktivieren."), + ("OS Account", "Betriebssystem-Konto"), + ("another_user_login_title_tip", "Ein anderer Benutzer ist bereits angemeldet."), + ("another_user_login_text_tip", "Trennen"), + ("xorg_not_found_title_tip", "Xorg nicht gefunden."), + ("xorg_not_found_text_tip", "Bitte installieren Sie Xorg."), + ("no_desktop_title_tip", "Es ist kein Desktop verfügbar."), + ("no_desktop_text_tip", "Bitte installieren Sie den GNOME-Desktop."), + ("No need to elevate", "Erhöhung der Rechte nicht erforderlich"), + ("System Sound", "Systemsound"), + ("Default", "Systemstandard"), + ("New RDP", "Neue RDP-Verbindung"), + ("Fingerprint", "Fingerabdruck"), + ("Copy Fingerprint", "Fingerabdruck kopieren"), + ("no fingerprints", "Keine Fingerabdrücke"), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 6d8a77d20..b071de54d 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Το αρχείο είναι πανομοιότυπο με αυτό του άλλου υπολογιστή."), ("show_monitors_tip", "Εμφάνιση οθονών στη γραμμή εργαλείων"), ("View Mode", "Λειτουργία προβολής"), + ("login_linux_tip", "Απαιτείται είσοδος σε απομακρυσμένο λογαριασμό Linux για την ενεργοποίηση του περιβάλλον εργασίας Χ."), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index eb09dacd2..b20035dc9 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -53,6 +53,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_lan_tip", "Oh no, it looks like we haven't discovered any peers yet."), ("empty_address_book_tip", "Oh dear, it appears that there are currently no peers listed in your address book."), ("identical_file_tip", "This file is identical with the peer's one."), - ("show_monitors_tip", "Show monitors in toolbar") + ("show_monitors_tip", "Show monitors in toolbar."), + ("enter_rustdesk_passwd_tip", "Enter RustDesk password"), + ("remember_rustdesk_passwd_tip", "Remember RustDesk password"), + ("login_linux_tip", "You need to login to remote Linux account to enable a X desktop session"), + ("verify_rustdesk_password_tip", "Verify RustDesk password"), + ("remember_account_tip", "Remember this account"), + ("os_account_desk_tip", "This account is used to login the remote OS and enable the desktop session in headless"), + ("another_user_login_title_tip", "Another user already logged in"), + ("another_user_login_text_tip", "Disconnect"), + ("xorg_not_found_title_tip", "Xorg not found"), + ("xorg_not_found_text_tip", "Please install Xorg"), + ("no_desktop_title_tip", "No desktop is avaliable"), + ("no_desktop_text_tip", "Please install GNOME desktop"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 6d697528d..3916afc01 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ed53d22bd..4f6b6e6b1 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -397,7 +397,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "Este PC"), ("or", "o"), ("Continue with", "Continuar con"), - ("Elevate", "Elevar"), + ("Elevate", "Elevar privilegios"), ("Zoom cursor", "Ampliar cursor"), ("Accept sessions via password", "Aceptar sesiones a través de contraseña"), ("Accept sessions via click", "Aceptar sesiones a través de clic"), @@ -422,16 +422,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", "Usar siempre renderizado por software"), ("config_input", "Para controlar el escritorio remoto con el teclado necesitas dar a RustDesk permisos de \"Monitorización de entrada\"."), ("config_microphone", "Para poder hablar de forma remota necesitas darle a RustDesk permisos de \"Grabar Audio\"."), - ("request_elevation_tip", "También puedes solicitar elevación si hay alguien en el lado remoto."), + ("request_elevation_tip", "También puedes solicitar elevación de privilegios si hay alguien en el lado remoto."), ("Wait", "Esperar"), - ("Elevation Error", "Error de elevación"), + ("Elevation Error", "Error de elevación de privilegios"), ("Ask the remote user for authentication", "Pida autenticación al usuario remoto"), ("Choose this if the remote account is administrator", "Elegir si la cuenta remota es de administrador"), ("Transmit the username and password of administrator", "Transmitir usuario y contraseña del administrador"), ("still_click_uac_tip", "Aún se necesita que el usuario remoto haga click en OK en la ventana UAC del RusDesk en ejecución."), - ("Request Elevation", "Solicitar Elevación"), + ("Request Elevation", "Solicitar Elevación de privilegios"), ("wait_accept_uac_tip", "Por favor espere a que el usuario remoto acepte el diálogo UAC."), - ("Elevate successfully", "Elevar con éxito"), + ("Elevate successfully", "Elevación de privilegios exitosa"), ("uppercase", "mayúsculas"), ("lowercase", "minúsculas"), ("digit", "dígito"), @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Este archivo es idéntico al del par."), ("show_monitors_tip", "Mostrar monitores en la barra de herramientas"), ("View Mode", "Modo Vista"), + ("login_linux_tip", "Necesitas iniciar sesión con la cueneta del Linux remoto para activar una sesión de escritorio X"), + ("verify_rustdesk_password_tip", "Verificar la contraseña de RustDesk"), + ("remember_account_tip", "Recordar esta cuenta"), + ("os_account_desk_tip", "Esta cueneta se usa para iniciar sesión en el sistema operativo remoto y habilitar la sesión de escritorio en headless."), + ("OS Account", "Cuenta del SO"), + ("another_user_login_title_tip", "Otro usuario ya ha iniciado sesión"), + ("another_user_login_text_tip", "Desconectar"), + ("xorg_not_found_title_tip", "Xorg no hallado"), + ("xorg_not_found_text_tip", "Por favor, instala Xorg"), + ("no_desktop_title_tip", "No hay escritorio disponible"), + ("no_desktop_text_tip", "Por favor, instala GNOME Desktop"), + ("No need to elevate", "No es necesario elevar privilegios"), + ("System Sound", ""), + ("Default", ""), + ("New RDP", "Nuevo RDP"), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index ffdf22a50..0bc9c3bc8 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -191,7 +191,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter Remote ID", "شناسه از راه دور را وارد کنید"), ("Enter your password", "زمر عبور خود را وارد کنید"), ("Logging in...", "...در حال ورود"), - ("Enable RDP session sharing", "اشتراک گذاری جلسه RDP را فعال کنید"), + ("Enable RDP session sharing", "را فعال کنید RDP اشتراک گذاری جلسه"), ("Auto Login", "ورود خودکار"), ("Enable Direct IP Access", "را فعال کنید IP دسترسی مستقیم"), ("Rename", "تغییر نام"), @@ -240,7 +240,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove from Favorites", "از علاقه مندی ها حذف شود"), ("Empty", "موردی وجود ندارد"), ("Invalid folder name", "نام پوشه نامعتبر است"), - ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5 Proxy", "Socks5 پروکسی"), ("Hostname", "نام هاست"), ("Discovered", "پیدا شده"), ("install_daemon_tip", "برای شروع در هنگام راه اندازی، باید سرویس سیستم را نصب کنید"), @@ -353,9 +353,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Light", "روشن"), ("Follow System", "پیروی از سیستم"), ("Enable hardware codec", "فعال سازی کدک سخت افزاری"), - ("Unlock Security Settings", "آنلاک شدن تنظیمات امنیتی"), + ("Unlock Security Settings", "دسترسی کامل به تنظیمات امنیتی"), ("Enable Audio", "فعال شدن صدا"), - ("Unlock Network Settings", "آنلاک شدن تنظیمات شبکه"), + ("Unlock Network Settings", "دسترسی کامل به تنظیمات شبکه"), ("Server", "سرور"), ("Direct IP Access", "IP دسترسی مستقیم به"), ("Proxy", "پروکسی"), @@ -479,6 +479,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Me", "من"), ("identical_file_tip", "این فایل با فایل همتا یکسان است."), ("show_monitors_tip", "نمایش مانیتورها در نوار ابزار"), - ("View Mode", ""), + ("View Mode", "حالت مشاهده"), + ("login_linux_tip", "برای فعال کردن دسکتاپ X، باید به حساب لینوکس راه دور وارد شوید"), + ("verify_rustdesk_password_tip", "رمز عبور RustDesk را تأیید کنید"), + ("remember_account_tip", "این حساب را به خاطر بسپارید"), + ("os_account_desk_tip", "این حساب برای ورود به سیستم عامل راه دور و فعال کردن جلسه دسکتاپ در هدلس استفاده می شود"), + ("OS Account", "حساب کاربری سیستم عامل"), + ("another_user_login_title_tip", "کاربر دیگری قبلاً وارد شده است"), + ("another_user_login_text_tip", "قطع شدن"), + ("xorg_not_found_title_tip", "پیدا نشد Xorg"), + ("xorg_not_found_text_tip", "لطفا Xorg را نصب کنید"), + ("no_desktop_title_tip", "هیچ دسکتاپی در دسترس نیست"), + ("no_desktop_text_tip", "لطفا دسکتاپ گنوم را نصب کنید"), + ("No need to elevate", "نیازی به ارتقاء نیست"), + ("System Sound", "صدای سیستم"), + ("Default", "پیش فرض"), + ("New RDP", "ریموت جدید"), + ("Fingerprint", "اثر انگشت"), + ("Copy Fingerprint", "کپی کردن اثر انگشت"), + ("no fingerprints", "بدون اثر انگشت"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6aa7f12f6..1850ff2f6 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -213,7 +213,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fermé manuellement par le pair"), ("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"), ("Run without install", "Exécuter sans installer"), - ("Connect via relay", ""), + ("Connect via relay", "Connexion via relais"), ("Always connect via relay", "Forcer la connexion relais"), ("whitelist_tip", "Seule une IP de la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), @@ -288,8 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "L'activation de la capture d'écran démarrera automatiquement le service, permettant à d'autres appareils de demander une connexion à partir de cet appareil."), ("android_stop_service_tip", "La fermeture du service fermera automatiquement toutes les connexions établies."), ("android_version_audio_tip", "La version actuelle d'Android ne prend pas en charge la capture audio, veuillez passer à Android 10 ou supérieur."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_start_service_tip", "Appuyez sur [Démarrer le service] ou activez l'autorisation [Capture d'écran] pour démarrer le service de partage d'écran."), + ("android_permission_may_not_change_tip", "Les autorisations pour les connexions établies peuvent ne pas être prisent en compte instantanément ou avant la reconnection."), ("Account", "Compte"), ("Overwrite", "Écraser"), ("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"), @@ -311,8 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Gardez le service RustDesk en arrière plan"), ("Ignore Battery Optimizations", "Ignorer les optimisations batterie"), ("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"), - ("Start on Boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), + ("Start on Boot", "Lancer au démarrage"), + ("Start the screen sharing service on boot, requires special permissions", "Lancer le service de partage d'écran au démarrage, nécessite des autorisations spéciales"), ("Connection not allowed", "Connexion non autorisée"), ("Legacy mode", "Mode hérité"), ("Map mode", ""), @@ -348,7 +348,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Security", "Sécurité"), ("Theme", "Thème"), ("Dark Theme", "Thème somble"), - ("Light Theme", ""), + ("Light Theme", "Thème clair"), ("Dark", "Sombre"), ("Light", "Clair"), ("Follow System", "Suivi système"), @@ -421,7 +421,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), ("Always use software rendering", "Utiliser toujours le rendu logiciel"), ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."), - ("config_microphone", ""), + ("config_microphone", "Pour discuter à distance, vous devez accorder à RustDesk les autorisations « Enregistrer l'audio »."), ("request_elevation_tip", "Vous pouvez également demander une augmentation des privilèges s'il y a quelqu'un du côté distant."), ("Wait", "En cours"), ("Elevation Error", "Erreur d'augmentation des privilèges"), @@ -451,34 +451,52 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), - ("Set one-time password length", ""), - ("idd_driver_tip", ""), - ("confirm_idd_driver_tip", ""), - ("RDP Settings", ""), - ("Sort by", ""), - ("New Connection", ""), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), - ("Your Device", ""), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", ""), - ("Empty Username", ""), - ("Empty Password", ""), - ("Me", ""), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), + ("Voice call", "Appel voix"), + ("Text chat", "Conversation textuelfle"), + ("Stop voice call", "Stopper l'appel voix"), + ("relay_hint_tip", "Il se peut qu'il ne doit pas possible de se connecter directement, vous pouvez essayer de vous connecter via un relais. \nEn outre, si vous souhaitez utiliser directement le relais, vous pouvez ajouter le suffixe \"/r\" à l'ID ou sélectionner l'option \"Toujours se connecter via le relais\" dans la fiche pair."), + ("Reconnect", "Se reconnecter"), + ("Codec", "Codec"), + ("Resolution", "Résolution"), + ("No transfers in progress", "Pas de transfert en cours"), + ("Set one-time password length", "Définir la longueur du mot de passe à usage unique"), + ("idd_driver_tip", "Installez le pilote d'affichage virtuel pour être utilisé lorsque vous n'avez pas d'affichage physique."), + ("confirm_idd_driver_tip", "L'option d'installation du pilote d'affichage virtuel est cochée. Notez qu'un certificat de test sera installé pour faire confiance au pilote d'affichage virtuel. Ce certificat de test ne sera utilisé que pour faire confiance aux pilotes Rustdesk."), + ("RDP Settings", "Configuration RDP"), + ("Sort by", "Trier par"), + ("New Connection", "Nouvelle connexion"), + ("Restore", "Restaurer"), + ("Minimize", "Minimiser"), + ("Maximize", "Maximiser"), + ("Your Device", "Votre appareil"), + ("empty_recent_tip", "Oups, pas de sessions récentes!\nIl est temps d'en prévoir une nouvelle."), + ("empty_favorite_tip", "Vous n'avez pas encore de pairs favoris?\nTrouvons quelqu'un avec qui vous connecter et ajoutez-le à vos favoris!"), + ("empty_lan_tip", "Oh non, il semble que nous n'ayons pas encore de pairs découverts."), + ("empty_address_book_tip", "Ouh là là! il semble qu'il n'y ait actuellement aucun pair répertorié dans votre carnet d'adresses."), + ("eg: admin", "ex: admin"), + ("Empty Username", "Nom d'utilisation non spécifié"), + ("Empty Password", "Mot de passe non spécifié"), + ("Me", "Moi"), + ("identical_file_tip", "Ce fichier est identique à celui du pair."), + ("show_monitors_tip", "Afficher les moniteurs dans la barre d'outils."), + ("View Mode", "Mode vue"), + ("login_linux_tip", "Se connecter au compte Linux distant"), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 474b3a70d..513c07e69 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 3bdef56bb..76acc5a99 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 7dc3b70ef..7612bac00 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -2,18 +2,18 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Stato"), - ("Your Desktop", "Il tuo desktop"), - ("desk_tip", "Puoi accedere al tuo desktop usando l'ID e la password riportati qui."), + ("Your Desktop", "Questo desktop"), + ("desk_tip", "Puoi accedere a questo desktop usando l'ID e la password indicati qui sotto."), ("Password", "Password"), ("Ready", "Pronto"), - ("Established", "Stabilito"), - ("connecting_status", "Connessione alla rete RustDesk in corso..."), + ("Established", "Stabilita"), + ("connecting_status", "Connessione alla rete RustDesk..."), ("Enable Service", "Abilita servizio"), ("Start Service", "Avvia servizio"), ("Service is running", "Il servizio è in esecuzione"), ("Service is not running", "Il servizio non è in esecuzione"), - ("not_ready_status", "Non pronto. Verifica la tua connessione"), - ("Control Remote Desktop", "Controlla un desktop remoto"), + ("not_ready_status", "Non pronto. Verifica la connessione"), + ("Control Remote Desktop", "Controlla desktop remoto"), ("Transfer File", "Trasferisci file"), ("Connect", "Connetti"), ("Recent Sessions", "Sessioni recenti"), @@ -22,38 +22,38 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("TCP Tunneling", "Tunnel TCP"), ("Remove", "Rimuovi"), ("Refresh random password", "Nuova password casuale"), - ("Set your own password", "Imposta la tua password"), + ("Set your own password", "Imposta la password"), ("Enable Keyboard/Mouse", "Abilita tastiera/mouse"), ("Enable Clipboard", "Abilita appunti"), ("Enable File Transfer", "Abilita trasferimento file"), ("Enable TCP Tunneling", "Abilita tunnel TCP"), ("IP Whitelisting", "IP autorizzati"), ("ID/Relay Server", "Server ID/Relay"), - ("Import Server Config", "Importa configurazione Server"), - ("Export Server Config", "Esporta configurazione Server"), - ("Import server configuration successfully", "Configurazione Server importata con successo"), - ("Export server configuration successfully", "Configurazione Server esportata con successo"), - ("Invalid server configuration", "Configurazione Server non valida"), + ("Import Server Config", "Importa configurazione server dagli appunti"), + ("Export Server Config", "Esporta configurazione server negli appunti"), + ("Import server configuration successfully", "Configurazione server importata completata"), + ("Export server configuration successfully", "Configurazione Server esportata completata"), + ("Invalid server configuration", "Configurazione server non valida"), ("Clipboard is empty", "Gli appunti sono vuoti"), ("Stop service", "Arresta servizio"), ("Change ID", "Cambia ID"), - ("Your new ID", "Il tuo nuovo ID"), - ("length %min% to %max%", "da lunghezza %min% a %max%"), + ("Your new ID", "Il nuovo ID"), + ("length %min% to %max%", "lunghezza da %min% a %max%"), ("starts with a letter", "inizia con una lettera"), ("allowed characters", "caratteri consentiti"), - ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (underscore). Il primo carattere deve essere a-z o A-Z. La lunghezza deve essere fra 6 e 16 caratteri."), - ("Website", "Sito web"), - ("About", "Informazioni"), - ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), + ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (sottolineato).\nIl primo carattere deve essere a-z o A-Z.\nLa lunghezza deve essere fra 6 e 16 caratteri."), + ("Website", "Sito web programma"), + ("About", "Info programma"), + ("Slogan_tip", "Realizzato con il cuore in questo mondo caotico!"), ("Privacy Statement", "Informativa sulla privacy"), - ("Mute", "Silenzia"), - ("Build Date", "Data della build"), + ("Mute", "Audio off"), + ("Build Date", "Data build"), ("Version", "Versione"), ("Home", "Home"), - ("Audio Input", "Input audio"), + ("Audio Input", "Ingresso audio"), ("Enhancements", "Miglioramenti"), - ("Hardware Codec", "Codifica Hardware"), - ("Adaptive Bitrate", "Bitrate Adattivo"), + ("Hardware Codec", "Codec hardware"), + ("Adaptive Bitrate", "Bitrate adattivo"), ("ID Server", "ID server"), ("Relay Server", "Server relay"), ("API Server", "Server API"), @@ -68,65 +68,65 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Close", "Chiudi"), ("Retry", "Riprova"), ("OK", "OK"), - ("Password Required", "Password Richiesta"), - ("Please enter your password", "Inserisci la tua password"), + ("Password Required", "Password richiesta"), + ("Please enter your password", "Inserisci la password"), ("Remember password", "Ricorda password"), - ("Wrong Password", "Password Errata"), + ("Wrong Password", "Password errata"), ("Do you want to enter again?", "Vuoi riprovare?"), ("Connection Error", "Errore di connessione"), ("Error", "Errore"), ("Reset by the peer", "Reimpostata dal peer"), ("Connecting...", "Connessione..."), - ("Connection in progress. Please wait.", "Connessione in corso. Attendi."), - ("Please try 1 minute later", "Per favore riprova fra 1 minuto"), - ("Login Error", "Errore Login"), - ("Successful", "Successo"), + ("Connection in progress. Please wait.", "Connessione in corso..."), + ("Please try 1 minute later", "Riprova fra 1 minuto"), + ("Login Error", "Errore accesso"), + ("Successful", "Completato"), ("Connected, waiting for image...", "Connesso, in attesa dell'immagine..."), ("Name", "Nome"), ("Type", "Tipo"), ("Modified", "Modificato"), ("Size", "Dimensione"), - ("Show Hidden Files", "Mostra file nascosti"), + ("Show Hidden Files", "Visualizza file nascosti"), ("Receive", "Ricevi"), ("Send", "Invia"), ("Refresh File", "Aggiorna file"), ("Local", "Locale"), - ("Remote", "Remote"), + ("Remote", "Remoto"), ("Remote Computer", "Computer remoto"), ("Local Computer", "Computer locale"), - ("Confirm Delete", "Conferma cancellazione"), - ("Delete", "Eliminare"), + ("Confirm Delete", "Conferma eliminazione"), + ("Delete", "Elimina"), ("Properties", "Proprietà"), ("Multi Select", "Selezione multipla"), ("Select All", "Seleziona tutto"), ("Unselect All", "Deseleziona tutto"), - ("Empty Directory", "Directory vuota"), - ("Not an empty directory", "Non una directory vuota"), - ("Are you sure you want to delete this file?", "Vuoi davvero eliminare questo file?"), - ("Are you sure you want to delete this empty directory?", "Sei sicuro di voler eliminare questa directory vuota?"), - ("Are you sure you want to delete the file of this directory?", "Sei sicuro di voler eliminare il file di questa directory?"), + ("Empty Directory", "Cartella vuota"), + ("Not an empty directory", "Non è una cartella vuota"), + ("Are you sure you want to delete this file?", "Sei sicuro di voler eliminare questo file?"), + ("Are you sure you want to delete this empty directory?", "Sei sicuro di voler eliminare questa cartella vuota?"), + ("Are you sure you want to delete the file of this directory?", "Sei sicuro di voler eliminare il file di questa cartella?"), ("Do this for all conflicts", "Ricorca questa scelta per tutti i conflitti"), ("This is irreversible!", "Questo è irreversibile!"), - ("Deleting", "Cancellazione di"), + ("Deleting", "Eliminazione di"), ("files", "file"), ("Waiting", "In attesa"), - ("Finished", "Terminato"), + ("Finished", "Completato"), ("Speed", "Velocità"), ("Custom Image Quality", "Qualità immagine personalizzata"), ("Privacy mode", "Modalità privacy"), - ("Block user input", "Blocca l'input dell'utente"), - ("Unblock user input", "Sbloccare l'input dell'utente"), - ("Adjust Window", "Adatta la finestra"), + ("Block user input", "Blocca input utente"), + ("Unblock user input", "Sblocca input utente"), + ("Adjust Window", "Adatta finestra"), ("Original", "Originale"), ("Shrink", "Restringi"), ("Stretch", "Allarga"), - ("Scrollbar", "Barra di scorrimento"), + ("Scrollbar", "Barra scorrimento"), ("ScrollAuto", "Scorri automaticamente"), - ("Good image quality", "Qualità immagine migliore"), - ("Balanced", "Bilanciato"), - ("Optimize reaction time", "Ottimizza il tempo di reazione"), - ("Custom", "Personalizzato"), - ("Show remote cursor", "Mostra il cursore remoto"), + ("Good image quality", "Qualità immagine buona"), + ("Balanced", "Bilanciata qualità/velocità"), + ("Optimize reaction time", "Ottimizza tempo reazione"), + ("Custom", "Qualità personalizzata"), + ("Show remote cursor", "Visualizza cursore remoto"), ("Show quality monitor", "Visualizza qualità video"), ("Disable clipboard", "Disabilita appunti"), ("Lock after session end", "Blocca al termine della sessione"), @@ -144,25 +144,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to connect via relay server", "Errore di connessione tramite il server relay"), ("Failed to make direct connection to remote desktop", "Impossibile connettersi direttamente al desktop remoto"), ("Set Password", "Imposta password"), - ("OS Password", "Password del sistema operativo"), - ("install_tip", "A causa del Controllo Account Utente, RustDesk potrebbe non funzionare correttamente come desktop remoto. Per evitare questo problema, fai click sul tasto qui sotto per installare RustDesk a livello di sistema."), - ("Click to upgrade", "Fai click per aggiornare"), - ("Click to download", "Cliquez per scaricare"), - ("Click to update", "Fare clic per aggiornare"), + ("OS Password", "Password sistema operativo"), + ("install_tip", "A causa del controllo account uUtente (UAC), RustDesk potrebbe non funzionare correttamente come desktop remoto.\nPer evitare questo problema, fai clic sul tasto qui sotto per installare RustDesk a livello di sistema."), + ("Click to upgrade", "Aggiorna"), + ("Click to download", "Download"), + ("Click to update", "Aggiorna"), ("Configure", "Configura"), - ("config_acc", "Per controllare il tuo desktop dall'esterno, devi fornire a RustDesk il permesso \"Accessibilità\"."), - ("config_screen", "Per controllare il tuo desktop dall'esterno, devi fornire a RustDesk il permesso \"Registrazione schermo\"."), + ("config_acc", "Per controllare il desktop dall'esterno, devi fornire a RustDesk il permesso 'Accessibilità'."), + ("config_screen", "Per controllare il desktop dall'esterno, devi fornire a RustDesk il permesso 'Registrazione schermo'."), ("Installing ...", "Installazione ..."), ("Install", "Installa"), ("Installation", "Installazione"), - ("Installation Path", "Percorso di installazione"), - ("Create start menu shortcuts", "Crea i collegamenti nel menu di avvio"), + ("Installation Path", "Percorso installazione"), + ("Create start menu shortcuts", "Crea i collegamenti nel menu Start"), ("Create desktop icon", "Crea un'icona sul desktop"), ("agreement_tip", "Avviando l'installazione, accetti i termini del contratto di licenza."), ("Accept and Install", "Accetta e installa"), - ("End-user license agreement", "Contratto di licenza con l'utente finale"), + ("End-user license agreement", "Contratto di licenza utente finale"), ("Generating ...", "Generazione ..."), - ("Your installation is lower version.", "La tua installazione non è aggiornata."), + ("Your installation is lower version.", "Questa installazione non è aggiornata."), ("not_close_tcp_tip", "Non chiudere questa finestra mentre stai usando il tunnel"), ("Listening ...", "In ascolto ..."), ("Remote Host", "Host remoto"), @@ -172,9 +172,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Porta locale"), ("Local Address", "Indirizzo locale"), ("Change Local Port", "Cambia porta locale"), - ("setup_server_tip", "Per una connessione più veloce, configura un tuo server"), - ("Too short, at least 6 characters.", "Troppo breve, almeno 6 caratteri"), - ("The confirmation is not identical.", "La conferma non corrisponde"), + ("setup_server_tip", "Per una connessione più veloce, configura uno specifico server"), + ("Too short, at least 6 characters.", "Troppo corta, almeno 6 caratteri"), + ("The confirmation is not identical.", "La password di conferma non corrisponde"), ("Permissions", "Permessi"), ("Accept", "Accetta"), ("Dismiss", "Rifiuta"), @@ -189,7 +189,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Direct and unencrypted connection", "Connessione diretta e non cifrata"), ("Relayed and unencrypted connection", "Connessione tramite relay e non cifrata"), ("Enter Remote ID", "Inserisci l'ID remoto"), - ("Enter your password", "Inserisci la tua password"), + ("Enter your password", "Inserisci la password"), ("Logging in...", "Autenticazione..."), ("Enable RDP session sharing", "Abilita la condivisione della sessione RDP"), ("Auto Login", "Accesso automatico"), @@ -197,31 +197,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Rename", "Rinomina"), ("Space", "Spazio"), ("Create Desktop Shortcut", "Crea collegamento sul desktop"), - ("Change Path", "Cambia percorso"), + ("Change Path", "Modifica percorso"), ("Create Folder", "Crea cartella"), ("Please enter the folder name", "Inserisci il nome della cartella"), ("Fix it", "Risolvi"), ("Warning", "Avviso"), - ("Login screen using Wayland is not supported", "La schermata di accesso non è supportata utilizzando Wayland"), + ("Login screen using Wayland is not supported", "La schermata di accesso non è supportata usando Wayland"), ("Reboot required", "Riavvio necessario"), ("Unsupported display server", "Display server non supportato"), - ("x11 expected", "x11 necessario"), + ("x11 expected", "necessario xll"), ("Port", "Porta"), ("Settings", "Impostazioni"), ("Username", "Nome utente"), - ("Invalid port", "Numero di porta non valido"), + ("Invalid port", "Numero porta non valido"), ("Closed manually by the peer", "Chiuso manualmente dal peer"), ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), ("Run without install", "Esegui senza installare"), ("Connect via relay", "Collegati tramite relay"), ("Always connect via relay", "Collegati sempre tramite relay"), - ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), + ("whitelist_tip", "Possono connettersi a questo desktop solo gli indirizzi IP autorizzati"), ("Login", "Accedi"), ("Verify", "Verifica"), ("Remember me", "Ricordami"), ("Trust this device", "Registra questo dispositivo come attendibile"), ("Verification code", "Codice di verifica"), - ("verification_tip", "È stato rilevato un nuovo dispositivo e un codice di verifica è stato inviato all'indirizzo e-mail registrato; inserire il codice di verifica per continuare l'accesso."), + ("verification_tip", "È stato rilevato un nuovo dispositivo ed è stato inviato un codice di verifica all'indirizzo email registrato.\nPer continuare l'accesso inserisci il codice di verifica."), ("Logout", "Esci"), ("Tags", "Tag"), ("Search ID", "Cerca ID"), @@ -243,10 +243,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Socks5 Proxy", "Proxy Socks5"), ("Hostname", "Nome host"), ("Discovered", "Rilevati"), - ("install_daemon_tip", "Per avviarsi all'accensione, è necessario installare il servizio di sistema."), + ("install_daemon_tip", "Per avviare il programma all'accensione, è necessario installarlo come servizio di sistema."), ("Remote ID", "ID remoto"), ("Paste", "Incolla"), - ("Paste here?", "Incolla qui?"), + ("Paste here?", "Incollare qui?"), ("Are you sure to close the connection?", "Sei sicuro di voler chiudere la connessione?"), ("Download new version", "Scarica nuova versione"), ("Touch mode", "Modalità tocco"), @@ -264,9 +264,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Two-Finger Move", "Movimento con due dita"), ("Canvas Move", "Sposta tela"), ("Pinch to Zoom", "Pizzica per zoomare"), - ("Canvas Zoom", "Zoom canvas"), - ("Reset canvas", "Ripristina canvas"), - ("No permission of file transfer", "Nessun permesso di trasferimento di file"), + ("Canvas Zoom", "Zoom tela"), + ("Reset canvas", "Ripristina tela"), + ("No permission of file transfer", "Nessun permesso per il trasferimento file"), ("Note", "Nota"), ("Connection", "Connessione"), ("Share Screen", "Condividi schermo"), @@ -275,68 +275,68 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("items", "Oggetti"), ("Selected", "Selezionato"), ("Screen Capture", "Cattura schermo"), - ("Input Control", "Controllo di input"), + ("Input Control", "Controllo input"), ("Audio Capture", "Acquisizione audio"), ("File Connection", "Connessione file"), ("Screen Connection", "Connessione schermo"), ("Do you accept?", "Accetti?"), ("Open System Setting", "Apri impostazioni di sistema"), - ("How to get Android input permission?", "Come ottenere l'autorizzazione di input su Android?"), - ("android_input_permission_tip1", "Affinché un dispositivo remoto possa controllare il tuo dispositivo Android tramite mouse o tocco, devi consentire a RustDesk di utilizzare il servizio \"Accessibilità\"."), - ("android_input_permission_tip2", "Vai alla pagina delle impostazioni di sistema che si aprirà di seguito, trova e accedi a [Servizi installati], attiva il servizio [RustDesk Input]."), - ("android_new_connection_tip", "È stata ricevuta una nuova richiesta di controllo per il dispositivo corrente."), + ("How to get Android input permission?", "Come ottenere l'autorizzazione input in Android?"), + ("android_input_permission_tip1", "Affinché un dispositivo remoto possa controllare un dispositivo Android tramite mouse o tocco, devi consentire a RustDesk di usare il servizio 'Accessibilità'."), + ("android_input_permission_tip2", "Vai nella pagina delle impostazioni di sistema che si aprirà di seguito, trova e accedi a [Servizi installati], attiva il servizio [RustDesk Input]."), + ("android_new_connection_tip", "È stata ricevuta una nuova richiesta di controllo per il dispositivo attuale."), ("android_service_will_start_tip", "L'attivazione di Cattura schermo avvierà automaticamente il servizio, consentendo ad altri dispositivi di richiedere una connessione da questo dispositivo."), ("android_stop_service_tip", "La chiusura del servizio chiuderà automaticamente tutte le connessioni stabilite."), - ("android_version_audio_tip", "L'attuale versione di Android non supporta l'acquisizione audio, esegui l'upgrade ad Android 10 o versioni successive."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_version_audio_tip", "L'attuale versione di Android non supporta l'acquisizione audio, esegui l'aggiornamento ad Android 10 o versioni successive."), + ("android_start_service_tip", "Per avviare il servizio di condivisione dello schermo seleziona [Avvia servizio] o abilita l'autorizzazione [Cattura schermo]."), + ("android_permission_may_not_change_tip", "Le autorizzazioni per le connessioni stabilite non possono essere modificate istantaneamente fino alla riconnessione."), ("Account", "Account"), ("Overwrite", "Sovrascrivi"), - ("This file exists, skip or overwrite this file?", "Questo file esiste, saltare o sovrascrivere questo file?"), + ("This file exists, skip or overwrite this file?", "Questo file esiste, vuoi ignorarlo o sovrascrivere questo file?"), ("Quit", "Esci"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Aiuto"), ("Failed", "Fallito"), - ("Succeeded", "Successo"), - ("Someone turns on privacy mode, exit", "Qualcuno attiva la modalità privacy, esci"), + ("Succeeded", "Completato"), + ("Someone turns on privacy mode, exit", "Qualcuno ha attivato la modalità privacy, uscita"), ("Unsupported", "Non supportato"), ("Peer denied", "Peer negato"), - ("Please install plugins", "Si prega di installare i plugin"), - ("Peer exit", "Uscita tra pari"), + ("Please install plugins", "Installa i plugin"), + ("Peer exit", "Uscita peer"), ("Failed to turn off", "Impossibile spegnere"), ("Turned off", "Spegni"), ("In privacy mode", "In modalità privacy"), - ("Out privacy mode", "Fuori modalità privacy"), - ("Language", "Linguaggio"), + ("Out privacy mode", "Uscita dalla modalità privacy"), + ("Language", "Lingua"), ("Keep RustDesk background service", "Mantieni il servizio di RustDesk in background"), ("Ignore Battery Optimizations", "Ignora le ottimizzazioni della batteria"), - ("android_open_battery_optimizations_tip", "Se si desidera disabilitare questa funzione, andare nelle impostazioni dell'applicazione RustDesk, aprire la sezione [Batteria] e deselezionare [Senza restrizioni]."), + ("android_open_battery_optimizations_tip", "Se vuoi disabilitare questa funzione, vai nelle impostazioni dell'applicazione RustDesk, apri la sezione 'Batteria' e deseleziona 'Senza restrizioni'."), ("Start on Boot", "Avvia all'accensione"), - ("Start the screen sharing service on boot, requires special permissions", "L'avvio del servizio di condivisione dello schermo all'accensione, richiede autorizzazioni speciali"), + ("Start the screen sharing service on boot, requires special permissions", "L'avvio del servizio di condivisione dello schermo all'accensione richiede autorizzazioni speciali"), ("Connection not allowed", "Connessione non consentita"), ("Legacy mode", "Modalità legacy"), ("Map mode", "Modalità mappa"), - ("Translate mode", "Modalità di traduzione"), + ("Translate mode", "Modalità traduzione"), ("Use permanent password", "Usa password permanente"), - ("Use both passwords", "Usa entrambe le password"), + ("Use both passwords", "Usa password monouso e permanente"), ("Set permanent password", "Imposta password permanente"), ("Enable Remote Restart", "Abilita riavvio da remoto"), ("Allow remote restart", "Consenti riavvio da remoto"), ("Restart Remote Device", "Riavvia dispositivo remoto"), ("Are you sure you want to restart", "Sei sicuro di voler riavviare?"), ("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"), - ("remote_restarting_tip", "Riavviare il dispositivo remoto"), + ("remote_restarting_tip", "Riavvia il dispositivo remoto"), ("Copied", "Copiato"), ("Exit Fullscreen", "Esci dalla modalità schermo intero"), ("Fullscreen", "A schermo intero"), ("Mobile Actions", "Azioni mobili"), ("Select Monitor", "Seleziona schermo"), - ("Control Actions", "Azioni di controllo"), - ("Display Settings", "Impostazioni di visualizzazione"), + ("Control Actions", "Azioni controllo"), + ("Display Settings", "Impostazioni visualizzazione"), ("Ratio", "Rapporto"), - ("Image Quality", "Qualità dell'immagine"), - ("Scroll Style", "Stile di scorrimento"), - ("Show Menubar", "Mostra la barra dei menu"), + ("Image Quality", "Qualità immagine"), + ("Scroll Style", "Stile scorrimento"), + ("Show Menubar", "Visualizza barra menu"), ("Hide Menubar", "nascondi la barra dei menu"), ("Direct Connection", "Connessione diretta"), ("Relay Connection", "Connessione relay"), @@ -347,91 +347,91 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("General", "Generale"), ("Security", "Sicurezza"), ("Theme", "Tema"), - ("Dark Theme", "Tema Scuro"), - ("Light Theme", ""), + ("Dark Theme", "Tema scuro"), + ("Light Theme", "Tema chiaro"), ("Dark", "Scuro"), ("Light", "Chiaro"), - ("Follow System", "Segui il sistema"), + ("Follow System", "Sistema"), ("Enable hardware codec", "Abilita codec hardware"), - ("Unlock Security Settings", "Sblocca impostazioni di sicurezza"), + ("Unlock Security Settings", "Sblocca impostazioni sicurezza"), ("Enable Audio", "Abilita audio"), ("Unlock Network Settings", "Sblocca impostazioni di rete"), ("Server", "Server"), ("Direct IP Access", "Accesso IP diretto"), ("Proxy", "Proxy"), ("Apply", "Applica"), - ("Disconnect all devices?", "Disconnettere tutti i dispositivi?"), - ("Clear", "Ripulisci"), - ("Audio Input Device", "Dispositivo di input audio"), + ("Disconnect all devices?", "Vuoi disconnettere tutti i dispositivi?"), + ("Clear", "Azzera"), + ("Audio Input Device", "Dispositivo ingresso audio"), ("Deny remote access", "Nega accesso remoto"), - ("Use IP Whitelisting", "Utilizza la whitelist di IP"), + ("Use IP Whitelisting", "Usa elenco IP autorizzati"), ("Network", "Rete"), ("Enable RDP", "Abilita RDP"), - ("Pin menubar", "Blocca la barra dei menu"), - ("Unpin menubar", "Sblocca la barra dei menu"), + ("Pin menubar", "Blocca barra menu"), + ("Unpin menubar", "Sblocca barra menu"), ("Recording", "Registrazione"), ("Directory", "Cartella"), ("Automatically record incoming sessions", "Registra automaticamente le sessioni in entrata"), - ("Change", "Cambia"), - ("Start session recording", "Inizia registrazione della sessione"), - ("Stop session recording", "Ferma registrazione della sessione"), - ("Enable Recording Session", "Abilita registrazione della sessione"), - ("Allow recording session", "Permetti di registrare la sessione"), - ("Enable LAN Discovery", "Abilita il rilevamento della LAN"), - ("Deny LAN Discovery", "Nega il rilevamento della LAN"), + ("Change", "Modifica"), + ("Start session recording", "Inizia registrazione sessione"), + ("Stop session recording", "Ferma registrazione sessione"), + ("Enable Recording Session", "Abilita registrazione sessione"), + ("Allow recording session", "Permetti registrazione sessione"), + ("Enable LAN Discovery", "Abilita rilevamento LAN"), + ("Deny LAN Discovery", "Nega rilevamento LAN"), ("Write a message", "Scrivi un messaggio"), - ("Prompt", "Richiede"), + ("Prompt", "Richiedi"), ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), - ("elevated_foreground_window_tip", "La finestra corrente del desktop remoto richiede privilegi più elevati per funzionare, quindi non è in grado di utilizzare temporaneamente il mouse e la tastiera. È possibile chiedere all'utente remoto di ridurre a icona la finestra corrente o di fare clic sul pulsante di elevazione nella finestra di gestione della connessione. Per evitare questo problema, si consiglia di installare il software sul dispositivo remoto."), + ("elevated_foreground_window_tip", "La finestra attuale del desktop remoto richiede per funzionare privilegi più elevati, quindi non è possibile usare temporaneamente il mouse e la tastiera.\nÈ possibile chiedere all'utente remoto di ridurre a icona la finestra attuale o di selezionare il pulsante di elevazione nella finestra di gestione della connessione.\nPer evitare questo problema, ti consigliamo di installare il software nel dispositivo remoto."), ("Disconnected", "Disconnesso"), ("Other", "Altro"), ("Confirm before closing multiple tabs", "Conferma prima di chiudere più schede"), ("Keyboard Settings", "Impostazioni tastiera"), ("Full Access", "Accesso completo"), ("Screen Share", "Condivisione dello schermo"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland richiede Ubuntu 21.04 o successiva."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland richiede una versione superiore della distribuzione Linux. Prova X11 desktop o cambia il tuo sistema operativo."), - ("JumpLink", "View"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland richiede Ubuntu 21.04 o versione successiva."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland richiede una versione superiore della distribuzione Linux.\nProva X11 desktop o cambia il sistema operativo."), + ("JumpLink", "Vai a"), ("Please Select the screen to be shared(Operate on the peer side).", "Seleziona lo schermo da condividere (opera sul lato peer)."), - ("Show RustDesk", "Mostra RustDesk"), + ("Show RustDesk", "Visualizza RustDesk"), ("This PC", "Questo PC"), ("or", "O"), ("Continue with", "Continua con"), ("Elevate", "Eleva"), ("Zoom cursor", "Cursore zoom"), ("Accept sessions via password", "Accetta sessioni via password"), - ("Accept sessions via click", "Accetta sessioni via click"), + ("Accept sessions via click", "Accetta sessioni via clic"), ("Accept sessions via both", "Accetta sessioni con entrambi"), - ("Please wait for the remote side to accept your session request...", "Attendere che il lato remoto accetti la richiesta di sessione..."), + ("Please wait for the remote side to accept your session request...", "Attendi che il dispositivo remoto accetti la richiesta di sessione..."), ("One-time Password", "Password monouso"), ("Use one-time password", "Usa password monouso"), ("One-time password length", "Lunghezza password monouso"), - ("Request access to your device", "Richiedi l'accesso al tuo dispositivo"), + ("Request access to your device", "Richiedi l'accesso al dispositivo"), ("Hide connection management window", "Nascondi la finestra di gestione delle connessioni"), ("hide_cm_tip", "Permetti di nascondere solo se si accettano sessioni con password permanente"), - ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, utilizza X11 se necessiti di un accesso stabile."), + ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, se vuoi un accesso stabile usa X11."), ("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"), ("Skipped", "Saltato"), ("Add to Address Book", "Aggiungi alla rubrica"), ("Group", "Gruppo"), ("Search", "Cerca"), - ("Closed manually by web console", "Chiudi manualmente dalla console Web"), - ("Local keyboard type", "Tipo di tastiera locale"), + ("Closed manually by web console", "Chiudi manualmente dalla console web"), + ("Local keyboard type", "Tipo tastiera locale"), ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), - ("software_render_tip", "Se si dispone di una scheda grafica Nvidia e la finestra remota si chiude immediatamente dopo la connessione, l'installazione del driver nouveau e la scelta di utilizzare il rendering software possono aiutare. È necessario un riavvio del software."), - ("Always use software rendering", "Usa sempre il render Software"), - ("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk \"Monitoraggio dell'input\"."), - ("config_microphone", "Per poter chiamare, è necessario concedere l'autorizzazione a RustDesk \"Registra audio\"."), - ("request_elevation_tip", "È possibile richiedere l'elevazione se c'è qualcuno sul lato remoto."), + ("software_render_tip", "Se disponi di una scheda grafica Nvidia e la finestra remota si chiude immediatamente dopo la connessione, l'installazione del driver aggiornato e la scelta di usare il rendering software possono aiutare.\nÈ necessario un riavvio del programma."), + ("Always use software rendering", "Usa sempre il render software"), + ("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk 'Monitoraggio input'."), + ("config_microphone", "Per poter chiamare, è necessario concedere l'autorizzazione a RustDesk 'Registra audio'."), + ("request_elevation_tip", "Se c'è qualcuno nel lato remoto è possibile richiedere l'elevazione."), ("Wait", "Attendi"), ("Elevation Error", "Errore durante l'elevazione dei diritti"), - ("Ask the remote user for authentication", "Chiedere l'autenticazione all'utente remoto"), - ("Choose this if the remote account is administrator", "Scegliere questa opzione se l'account remoto è amministratore"), - ("Transmit the username and password of administrator", "Trasmettere il nome utente e la password dell'amministratore"), - ("still_click_uac_tip", "Richiede ancora che l'utente remoto faccia clic su OK nella finestra UAC dell'esecuzione di RustDesk."), + ("Ask the remote user for authentication", "Chiedi l'autenticazione all'utente remoto"), + ("Choose this if the remote account is administrator", "Scegli questa opzione se l'account remoto è amministratore"), + ("Transmit the username and password of administrator", "Trasmetti il nome utente e la password dell'amministratore"), + ("still_click_uac_tip", "Richiedi ancora che l'utente remoto faccia clic su OK nella finestra UAC dell'esecuzione di RustDesk."), ("Request Elevation", "Richiedi elevazione dei diritti"), - ("wait_accept_uac_tip", "Attendere che l'utente remoto accetti la finestra di dialogo UAC."), - ("Elevate successfully", "Elevazione dei diritti effettuata con successo"), + ("wait_accept_uac_tip", "Attendi che l'utente remoto accetti la finestra di dialogo UAC."), + ("Elevate successfully", "Elevazione dei diritti effettuata correttamente"), ("uppercase", "Maiuscola"), ("lowercase", "Minuscola"), ("digit", "Numero"), @@ -441,44 +441,62 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Media"), ("Strong", "Forte"), ("Switch Sides", "Cambia lato"), - ("Please confirm if you want to share your desktop?", "Vuoi condividere il tuo desktop?"), + ("Please confirm if you want to share your desktop?", "Vuoi condividere il desktop?"), ("Display", "Visualizzazione"), - ("Default View Style", "Stile Visualizzazione Predefinito"), - ("Default Scroll Style", "Stile Scorrimento Predefinito"), - ("Default Image Quality", "Qualità Immagine Predefinita"), - ("Default Codec", "Codec Predefinito"), + ("Default View Style", "Stile visualizzazione predefinito"), + ("Default Scroll Style", "Stile scorrimento predefinito"), + ("Default Image Quality", "Qualità immagine predefinita"), + ("Default Codec", "Codec predefinito"), ("Bitrate", "Bitrate"), ("FPS", "FPS"), - ("Auto", "Auto"), - ("Other Default Options", "Altre Opzioni Predefinite"), + ("Auto", "Automatico"), + ("Other Default Options", "Altre opzioni predefinite"), ("Voice call", "Chiamata vocale"), ("Text chat", "Chat testuale"), - ("Stop voice call", "Interrompi la chiamata vocale"), - ("relay_hint_tip", "Se non è possibile connettersi direttamente, si può provare a farlo tramite relay.\nInoltre, se si desidera utilizzare il relay al primo tentativo, è possibile aggiungere il suffisso \"/r\" all'ID o selezionare l'opzione \"Collegati sempre tramite relay\" nella scheda peer."), + ("Stop voice call", "Interrompi chiamata vocale"), + ("relay_hint_tip", "Se non è possibile connettersi direttamente, puoi provare a farlo tramite relay.\nInoltre, se si vuoi usare il relay al primo tentativo, è possibile aggiungere all'ID il suffisso '/r\' o selezionare nella scheda peer l'opzione 'Collegati sempre tramite relay'."), ("Reconnect", "Riconnetti"), ("Codec", "Codec"), ("Resolution", "Risoluzione"), ("No transfers in progress", "Nessun trasferimento in corso"), - ("Set one-time password length", "Imposta la lunghezza della password monouso"), - ("idd_driver_tip", "Installa il driver per lo schermo virtuale che sarà utilizzato quando non si dispone di schermi fisici."), - ("confirm_idd_driver_tip", "L'opzione per installare il driver per lo schermo virtuale è selezionata. Nota che un certificato di test sarà installato per l'attendibilità del driver dello schermo virtuale. Questo certificato di test verrà utilizzato solo per l'attendibilità dei driver di RustDesk."), + ("Set one-time password length", "Imposta lunghezza password monouso"), + ("idd_driver_tip", "Installa driver schermo virtuale che verrà usato quando non sono disponibili schermi fisici."), + ("confirm_idd_driver_tip", "È stata selezionata l'opzione per installare il driver schermo virtuale.\nNota che verrà installato un certificato di test per l'attendibilità del driver dello schermo virtuale.\nQuesto certificato di test verrà utilizzato solo per l'attendibilità dei driver di RustDesk."), ("RDP Settings", "Impostazioni RDP"), ("Sort by", "Ordina per"), ("New Connection", "Nuova connessione"), ("Restore", "Ripristina"), ("Minimize", "Minimizza"), ("Maximize", "Massimizza"), - ("Your Device", "Il tuo dispositivo"), - ("empty_recent_tip", "Oops, non c'è nessuna sessione recente!\nTempo di pianificarne una."), - ("empty_favorite_tip", "Ancora nessun peer?\nTrova qualcuno con cui connetterti e aggiungilo ai tuoi preferiti!"), - ("empty_lan_tip", "Oh no, sembra proprio che non abbiamo ancora rilevato nessun peer."), - ("empty_address_book_tip", "Oh diamine, sembra che per ora non ci siano peer nella tua rubrica."), + ("Your Device", "Questo dispositivo"), + ("empty_recent_tip", "Non c'è nessuna sessione recente!\nPianificane una."), + ("empty_favorite_tip", "Ancora nessun peer?\nTrova qualcuno con cui connetterti e aggiungilo ai preferiti!"), + ("empty_lan_tip", "Sembra proprio che non sia stato rilevato nessun peer."), + ("empty_address_book_tip", "Sembra che per ora nella rubrica non ci siano peer."), ("eg: admin", "es: admin"), - ("Empty Username", "Nome Utente Vuoto"), - ("Empty Password", "Password Vuota"), + ("Empty Username", "Nome utente vuoto"), + ("Empty Password", "Password vuota"), ("Me", "Io"), ("identical_file_tip", "Questo file è identico a quello del peer."), - ("show_monitors_tip", "Mostra schermi nella barra degli strumenti"), - ("View Mode", ""), + ("show_monitors_tip", "Visualizza schermi nella barra strumenti"), + ("View Mode", "Modalità visualizzazione"), + ("login_linux_tip", "Accedi all'account Linux remoto"), + ("verify_rustdesk_password_tip", "Conferma password RustDesk"), + ("remember_account_tip", "Ricorda questo account"), + ("os_account_desk_tip", "Questo account viene usato per accedere al sistema operativo remoto e attivare la sessione desktop in modalità non presidiata."), + ("OS Account", "Account sistema operativo"), + ("another_user_login_title_tip", "È già loggato un altro utente."), + ("another_user_login_text_tip", "Separato"), + ("xorg_not_found_title_tip", "Xorg non trovato."), + ("xorg_not_found_text_tip", "Installa Xorg."), + ("no_desktop_title_tip", "Non c'è nessun desktop disponibile."), + ("no_desktop_text_tip", "Installa il desktop GNOME."), + ("No need to elevate", "Elevazione dei privilegi non richiesta"), + ("System Sound", "Dispositivo audio sistema"), + ("Default", "Predefinita"), + ("New RDP", "Nuovo RDP"), + ("Fingerprint", "Firma digitale"), + ("Copy Fingerprint", "Copia firma digitale"), + ("no fingerprints", "Nessuna firma digitale"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 925ccebde..835a8e1ce 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 89c65a332..1bd6bb4a4 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 0ec4d099b..3006abaa9 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs new file mode 100644 index 000000000..7e8ae803d --- /dev/null +++ b/src/lang/lt.rs @@ -0,0 +1,502 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Būsena"), + ("Your Desktop", "Jūsų darbalaukis"), + ("desk_tip", "Jūsų darbalaukis pasiekiamas naudojant šį ID ir slaptažodį"), + ("Password", "Slaptažodis"), + ("Ready", "Pasiruošęs"), + ("Established", "Įsteigta"), + ("connecting_status", "Prisijungiama prie RustDesk tinklo..."), + ("Enable Service", "Įgalinti paslaugą"), + ("Start Service", "Pradėti paslaugą"), + ("Service is running", "Paslauga veikia"), + ("Service is not running", "Paslauga neveikia"), + ("not_ready_status", "Neprisijungęs. Patikrinkite ryšį."), + ("Control Remote Desktop", "Nuotolinio darbalaukio valdymas"), + ("Transfer File", "Perkelti failą"), + ("Connect", "Prisijungti"), + ("Recent Sessions", "Seansų istorija"), + ("Address Book", "Adresų knyga"), + ("Confirmation", "Patvirtinimas"), + ("TCP Tunneling", "TCP tuneliavimas"), + ("Remove", "Pašalinti"), + ("Refresh random password", "Atnaujinti atsitiktinį slaptažodį"), + ("Set your own password", "Nustatykite savo slaptažodį"), + ("Enable Keyboard/Mouse", "Įgalinti klaviatūrą/pelę"), + ("Enable Clipboard", "Įgalinti iškarpinę"), + ("Enable File Transfer", "Įgalinti failų perdavimą"), + ("Enable TCP Tunneling", "Įgalinti TCP tuneliavimą"), + ("IP Whitelisting", "IP baltasis sąrašas"), + ("ID/Relay Server", "ID / perdavimo serveris"), + ("Import Server Config", "Importuoti serverio konfigūraciją"), + ("Export Server Config", "Eksportuoti serverio konfigūraciją"), + ("Import server configuration successfully", "Sėkmingai importuoti serverio konfigūraciją"), + ("Export server configuration successfully", "Sėkmingai eksportuoti serverio konfigūraciją"), + ("Invalid server configuration", "Netinkama serverio konfigūracija"), + ("Clipboard is empty", "Iškarpinė tuščia"), + ("Stop service", "Sustabdyti paslaugą"), + ("Change ID", "Keisti ID"), + ("Your new ID", "Jūsų naujasis ID"), + ("length %min% to %max%", "ilgis %min% iki %max%"), + ("starts with a letter", "prasideda raide"), + ("allowed characters", "leistini simboliai"), + ("id_change_tip", "Leidžiami tik simboliai a–z, A–Z, 0–9 ir _ (pabraukimas). Pirmoji raidė turi būti a-z, A-Z. Ilgis nuo 6 iki 16."), + ("Website", "Interneto svetainė"), + ("About", "Apie"), + ("Slogan_tip", "Sukurta su siela šiame beprotiškame pasaulyje!"), + ("Privacy Statement", "Privatumo pareiškimas"), + ("Mute", "Nutildyti"), + ("Build Date", "Sukūrimo data"), + ("Version", "Versija"), + ("Home", "Namai"), + ("Audio Input", "Garso įvestis"), + ("Enhancements", "Patobulinimai"), + ("Hardware Codec", "Aparatinės įrangos paspartinimas"), + ("Adaptive Bitrate", "Adaptyvusis pralaidumas"), + ("ID Server", "ID serveris"), + ("Relay Server", "Perdavimo serveris"), + ("API Server", "API serveris"), + ("invalid_http", "Turi prasidėti http:// arba https://"), + ("Invalid IP", "Netinkamas IP"), + ("Invalid format", "Neteisingas formatas"), + ("server_not_support", "Serveris dar nepalaikomas"), + ("Not available", "Nepasiekiamas"), + ("Too frequent", "Per dažnai"), + ("Cancel", "Atšaukti"), + ("Skip", "Praleisti"), + ("Close", "Uždaryti"), + ("Retry", "Bandykite dar kartą"), + ("OK", "GERAI"), + ("Password Required", "Reikalingas slaptažodis"), + ("Please enter your password", "Prašome įvesti savo slaptažodį"), + ("Remember password", "Prisiminti slaptažodį"), + ("Wrong Password", "Neteisingas slaptažodis"), + ("Do you want to enter again?", "Ar norite įeiti dar kartą?"), + ("Connection Error", "Ryšio klaida"), + ("Error", "Klaida"), + ("Reset by the peer", "Atmetė nuotolinis kompiuteris"), + ("Connecting...", "Jungiamasi..."), + ("Connection in progress. Please wait.", "Jungiamasi. Palaukite."), + ("Please try 1 minute later", "Prašome pabandyti po 1 minutės"), + ("Login Error", "Prisijungimo klaida"), + ("Successful", "Sėkmingai"), + ("Connected, waiting for image...", "Prisijungta, laukiama vaizdo..."), + ("Name", "Vardas"), + ("Type", "Tipas"), + ("Modified", "Pakeista"), + ("Size", "Dydis"), + ("Show Hidden Files", "Rodyti paslėptus failus"), + ("Receive", "Gauti"), + ("Send", "Siųsti"), + ("Refresh File", "Atnaujinti failą"), + ("Local", "Vietinis"), + ("Remote", "Nuotolinis"), + ("Remote Computer", "Nuotolinis kompiuteris"), + ("Local Computer", "Šis kompiuteris"), + ("Confirm Delete", "Patvirtinti ištrynimą"), + ("Delete", "Ištrinti"), + ("Properties", "Ypatybės"), + ("Multi Select", "Keli pasirinkimas"), + ("Select All", "Pasirinkti viską"), + ("Unselect All", "Atšaukti visų pasirinkimą"), + ("Empty Directory", "Tuščias katalogas"), + ("Not an empty directory", "Ne tuščias katalogas"), + ("Are you sure you want to delete this file?", "Ar tikrai norite ištrinti šį failą?"), + ("Are you sure you want to delete this empty directory?", "Ar tikrai norite ištrinti šį tuščią katalogą?"), + ("Are you sure you want to delete the file of this directory?", "Ar tikrai norite ištrinti šio katalogo failą?"), + ("Do this for all conflicts", "Taikyti visiems konfliktams"), + ("This is irreversible!", "Tai negrįžtama!"), + ("Deleting", "Ištrinama"), + ("files", "failai"), + ("Waiting", "Laukiu"), + ("Finished", "Baigta"), + ("Speed", "Greitis"), + ("Custom Image Quality", "Tinkinta vaizdo kokybė"), + ("Privacy mode", "Privatumo režimas"), + ("Block user input", "Blokuoti naudotojo įvestį"), + ("Unblock user input", "Atblokuoti naudotojo įvestį"), + ("Adjust Window", "Koreguoti langą"), + ("Original", "Originalas"), + ("Shrink", "Susitraukti"), + ("Stretch", "Ištempti"), + ("Scrollbar", "Slinkties juosta"), + ("ScrollAuto", "Automatinis slinkimas"), + ("Good image quality", "Gera vaizdo kokybė"), + ("Balanced", "Subalansuotas"), + ("Optimize reaction time", "Optimizuoti reakcijos laiką"), + ("Custom", "Tinkintas"), + ("Show remote cursor", "Rodyti nuotolinį žymeklį"), + ("Show quality monitor", "Rodyti kokybės monitorių"), + ("Disable clipboard", "Išjungti mainų sritį"), + ("Lock after session end", "Užrakinti pasibaigus seansui"), + ("Insert", "Įdėti"), + ("Insert Lock", "Įterpti užraktą"), + ("Refresh", "Atnaujinti"), + ("ID does not exist", "ID neegzistuoja"), + ("Failed to connect to rendezvous server", "Nepavyko prisijungti prie susitikimo serverio"), + ("Please try later", "Prašome pabandyti vėliau"), + ("Remote desktop is offline", "Nuotolinis darbalaukis neprisijungęs"), + ("Key mismatch", "Raktų neatitikimas"), + ("Timeout", "Laikas baigėsi"), + ("Failed to connect to relay server", "Nepavyko prisijungti prie perdavimo serverio"), + ("Failed to connect via rendezvous server", "Nepavyko prisijungti per susitikimo serverį"), + ("Failed to connect via relay server", "Nepavyko prisijungti per perdavimo serverį"), + ("Failed to make direct connection to remote desktop", "Nepavyko tiesiogiai prisijungti prie nuotolinio darbalaukio"), + ("Set Password", "Nustatyti slaptažodį"), + ("OS Password", "OS slaptažodis"), + ("install_tip", "Kai kuriais atvejais UAC gali priversti RustDesk netinkamai veikti nuotoliniame pagrindiniame kompiuteryje. Norėdami apeiti UAC, spustelėkite toliau esantį mygtuką, kad įdiegtumėte RustDesk į savo kompiuterį."), + ("Click to upgrade", "Spustelėkite, jei norite atnaujinti"), + ("Click to download", "Spustelėkite norėdami atsisiųsti"), + ("Click to update", "Spustelėkite norėdami atnaujinti"), + ("Configure", "Konfigūruoti"), + ("config_acc", "Norėdami nuotoliniu būdu valdyti darbalaukį, turite suteikti RustDesk \"prieigos\" leidimus"), + ("config_screen", "Norėdami nuotoliniu būdu pasiekti darbalaukį, turite suteikti RustDesk leidimus \"ekrano kopija\""), + ("Installing ...", "Diegiama ..."), + ("Install", "Diegti"), + ("Installation", "Įdiegimas"), + ("Installation Path", "Įdiegimo kelias"), + ("Create start menu shortcuts", "Sukurti pradžios meniu sparčiuosius klavišus"), + ("Create desktop icon", "Sukurti darbalaukio piktogramą"), + ("agreement_tip", "Pradėdami diegimą sutinkate su licencijos sutarties sąlygomis"), + ("Accept and Install", "Priimti ir įdiegti"), + ("End-user license agreement", "Galutinio vartotojo licencijos sutartis"), + ("Generating ...", "Generuojamas..."), + ("Your installation is lower version.", "Jūsų įdiegta versija senesnė."), + ("not_close_tcp_tip", "Naudodami tunelį neuždarykite šio lango"), + ("Listening ...", "Laukimas..."), + ("Remote Host", "Nuotolinis pagrindinis kompiuteris"), + ("Remote Port", "Nuotolinis prievadas"), + ("Action", "Veiksmas"), + ("Add", "Papildyti"), + ("Local Port", "Vietinis prievadas"), + ("Local Address", "Vietinis adresas"), + ("Change Local Port", "Keisti vietinį prievadą"), + ("setup_server_tip", "Kad ryšys būtų greitesnis, nustatykite savo serverį"), + ("Too short, at least 6 characters.", "Per trumpas, mažiausiai 6 simboliai."), + ("The confirmation is not identical.", "Patvirtinimas nėra tapatus."), + ("Permissions", "Leidimai"), + ("Accept", "Priimti"), + ("Dismiss", "Atmesti"), + ("Disconnect", "Atjungti"), + ("Allow using keyboard and mouse", "Leisti naudoti klaviatūrą ir pelę"), + ("Allow using clipboard", "Leisti naudoti mainų sritį"), + ("Allow hearing sound", "Leisti girdėti kompiuterio garsą"), + ("Allow file copy and paste", "Leisti kopijuoti ir įklijuoti failus"), + ("Connected", "Prisijungta"), + ("Direct and encrypted connection", "Tiesioginis ir šifruotas ryšys"), + ("Relayed and encrypted connection", "Perduotas ir šifruotas ryšys"), + ("Direct and unencrypted connection", "Tiesioginis ir nešifruotas ryšys"), + ("Relayed and unencrypted connection", "Perduotas ir nešifruotas ryšys"), + ("Enter Remote ID", "Įveskite nuotolinio ID"), + ("Enter your password", "Įveskite savo slaptažodį"), + ("Logging in...", "Prisijungiama..."), + ("Enable RDP session sharing", "Įgalinti RDP seansų bendrinimą"), + ("Auto Login", "Automatinis prisijungimas"), + ("Enable Direct IP Access", "Įgalinti tiesioginę IP prieigą"), + ("Rename", "Pervardyti"), + ("Space", "Erdvė"), + ("Create Desktop Shortcut", "Sukurti nuorodą darbalaukyje"), + ("Change Path", "Keisti kelią"), + ("Create Folder", "Sukurti aplanką"), + ("Please enter the folder name", "Įveskite aplanko pavadinimą"), + ("Fix it", "Pataisyk tai"), + ("Warning", "Įspėjimas"), + ("Login screen using Wayland is not supported", "Prisijungimo ekranas naudojant Wayland nepalaikomas"), + ("Reboot required", "Reikia paleisti iš naujo"), + ("Unsupported display server", "Nepalaikomas rodymo serveris"), + ("x11 expected", "reikalingas x11"), + ("Port", "Prievadas"), + ("Settings", "Nustatymai"), + ("Username", "Vartotojo vardas"), + ("Invalid port", "Netinkamas prievadas"), + ("Closed manually by the peer", "Partneris atmetė prašymą prisijungti"), + ("Enable remote configuration modification", "Įgalinti nuotolinį konfigūracijos modifikavimą"), + ("Run without install", "Vykdyti be diegimo"), + ("Connect via relay", "Prisijungti per relę"), + ("Always connect via relay", "Visada prisijunkite per relę"), + ("whitelist_tip", "Mane gali pasiekti tik baltajame sąraše esantys IP adresai"), + ("Login", "Prisijungti"), + ("Verify", "Patvirtinti"), + ("Remember me", "Prisimink mane"), + ("Trust this device", "Pasitikėk šiuo įrenginiu"), + ("Verification code", "Patvirtinimo kodas"), + ("verification_tip", "Aptiktas naujas įrenginys ir registruotu el. pašto adresu išsiųstas patvirtinimo kodas. Įveskite jį norėdami tęsti prisijungimą."), + ("Logout", "Atsijungti"), + ("Tags", "Žymos"), + ("Search ID", "Paieškos ID"), + ("whitelist_sep", "Atskirti kableliu, kabliataškiu, tarpu arba nauja eilute"), + ("Add ID", "Pridėti ID"), + ("Add Tag", "Pridėti žymą"), + ("Unselect all tags", "Atšaukti visų žymų pasirinkimą"), + ("Network error", "Tinklo klaida"), + ("Username missed", "Prarastas vartotojo vardas"), + ("Password missed", "Slaptažodis praleistas"), + ("Wrong credentials", "Klaidingi kredencialai"), + ("Edit Tag", "Redaguoti žymą"), + ("Unremember Password", "Nebeprisiminti slaptažodžio"), + ("Favorites", "Parankiniai"), + ("Add to Favorites", "Įtraukti į parankinius"), + ("Remove from Favorites", "Pašalinti iš parankinių"), + ("Empty", "Tuščia"), + ("Invalid folder name", "Neteisingas aplanko pavadinimas"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Pagrindinio kompiuterio pavadinimas"), + ("Discovered", "Aptikta tinkle"), + ("install_daemon_tip", "Norėdami, kad RustDesk startuotų automatiškai, turite ją įdiegti"), + ("Remote ID", "Nuotolinis ID"), + ("Paste", "Įklijuoti"), + ("Paste here?", "Įklijuoti čia?"), + ("Are you sure to close the connection?", "Ar tikrai norite atsijungti?"), + ("Download new version", "Atsisiųsti naują versiją"), + ("Touch mode", "Palietimo režimas"), + ("Mouse mode", "Pelės režimas"), + ("One-Finger Tap", "Palietimas vienu pirštu"), + ("Left Mouse", "Kairysis pelės kl."), + ("One-Long Tap", "Vienas palietimas"), + ("Two-Finger Tap", "Palietimas dviem pirštais"), + ("Right Mouse", "Dešinysis pelės kl."), + ("One-Finger Move", "Vieno piršto judesys"), + ("Double Tap & Move", "Dukart palieskite ir perkelkite"), + ("Mouse Drag", "Pelės vilkimas"), + ("Three-Finger vertically", "Trys pirštai vertikaliai"), + ("Mouse Wheel", "Pelės ratukas"), + ("Two-Finger Move", "Dviejų pirštų judesys"), + ("Canvas Move", "Drobės perkėlimas"), + ("Pinch to Zoom", "Suimkite, kad padidintumėte"), + ("Canvas Zoom", "Drobės mastelis"), + ("Reset canvas", "Atstatyti drobę"), + ("No permission of file transfer", "Nėra leidimo perkelti failus"), + ("Note", "Pastaba"), + ("Connection", "Ryšys"), + ("Share Screen", "Bendrinti ekraną"), + ("Chat", "Pokalbis"), + ("Total", "Iš viso"), + ("items", "elementai"), + ("Selected", "Pasirinkta"), + ("Screen Capture", "Ekrano nuotrauka"), + ("Input Control", "Įvesties valdymas"), + ("Audio Capture", "Garso fiksavimas"), + ("File Connection", "Failo ryšys"), + ("Screen Connection", "Ekrano jungtis"), + ("Do you accept?", "Ar sutinki?"), + ("Open System Setting", "Atviros sistemos nustatymas"), + ("How to get Android input permission?", "Kaip gauti Android įvesties leidimą?"), + ("android_input_permission_tip1", "Kad nuotolinis įrenginys galėtų valdyti Android įrenginį pele arba liesti, turite leisti RustDesk naudoti \"Prieinamumo\" paslaugą."), + ("android_input_permission_tip2", "Eikite į kitą sistemos nustatymų puslapį, suraskite \"Įdiegtos paslaugos\" ir įgalinkite \"RustDesk įvestis\" paslaugą."), + ("android_new_connection_tip", "Gauta nauja užklausa tvarkyti dabartinį įrenginį."), + ("android_service_will_start_tip", "Įgalinus ekrano fiksavimo paslaugą, kiti įrenginiai gali pateikti užklausą prisijungti prie to įrenginio."), + ("android_stop_service_tip", "Uždarius paslaugą automatiškai bus uždaryti visi užmegzti ryšiai."), + ("android_version_audio_tip", "Dabartinė Android versija nepalaiko garso įrašymo, atnaujinkite į Android 10 ar naujesnę versiją."), + ("android_start_service_tip", "Spustelėkite [Paleisti paslaugą] arba įjunkite [Fiksuoti ekraną], kad paleistumėte ekrano bendrinimo paslaugą."), + ("android_permission_may_not_change_tip", "Užmegztų ryšių leidimų keisti negalima, reikia prisijungti iš naujo."), + ("Account", "Paskyra"), + ("Overwrite", "Perrašyti"), + ("This file exists, skip or overwrite this file?", "Šis failas egzistuoja, praleisti arba perrašyti šį failą?"), + ("Quit", "Išeiti"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/"), + ("Help", "Pagalba"), + ("Failed", "Nepavyko"), + ("Succeeded", "Pavyko"), + ("Someone turns on privacy mode, exit", "Kažkas įjungė privatumo režimą, išeiti"), + ("Unsupported", "Nepalaikomas"), + ("Peer denied", "Atšaukė"), + ("Please install plugins", "Įdiekite papildinius"), + ("Peer exit", "Nuotolinis mazgas neveikia"), + ("Failed to turn off", "Nepavyko išjungti"), + ("Turned off", "Išjungti"), + ("In privacy mode", "Privatumo režimas"), + ("Out privacy mode", "Išėjimas iš privatumo režimo"), + ("Language", "Kalba"), + ("Keep RustDesk background service", "Palikti RustDesk fonine paslauga"), + ("Ignore Battery Optimizations", "Ignoruoti akumuliatoriaus optimizavimą"), + ("android_open_battery_optimizations_tip", "Eikite į kitą nustatymų puslapį"), + ("Start on Boot", "Pradėti paleidžiant"), + ("Start the screen sharing service on boot, requires special permissions", "Paleiskite ekrano bendrinimo paslaugą įkrovos metu, reikia specialių leidimų"), + ("Connection not allowed", "Ryšys neleidžiamas"), + ("Legacy mode", "Senasis režimas"), + ("Map mode", "Žemėlapio režimas"), + ("Translate mode", "Vertimo režimas"), + ("Use permanent password", "Naudoti nuolatinį slaptažodį"), + ("Use both passwords", "Naudoti abu slaptažodžius"), + ("Set permanent password", "Nustatyti nuolatinį slaptažodį"), + ("Enable Remote Restart", "Įgalinti nuotolinį paleidimą iš naujo"), + ("Allow remote restart", "Leisti nuotolinio kompiuterio paleidimą iš naujo"), + ("Restart Remote Device", "Paleisti nuotolinį kompiuterį iš naujo"), + ("Are you sure you want to restart", "Ar tikrai norite paleisti iš naujo?"), + ("Restarting Remote Device", "Nuotolinio įrenginio paleidimas iš naujo"), + ("remote_restarting_tip", "Nuotolinis įrenginys paleidžiamas iš naujo. Uždarykite šį pranešimą ir po kurio laiko vėl prisijunkite naudodami nuolatinį slaptažodį."), + ("Copied", "Nukopijuota"), + ("Exit Fullscreen", "Išeiti iš pilno ekrano"), + ("Fullscreen", "Per visą ekraną"), + ("Mobile Actions", "Veiksmai mobiliesiems"), + ("Select Monitor", "Pasirinkite monitorių"), + ("Control Actions", "Valdymo veiksmai"), + ("Display Settings", "Ekrano nustatymai"), + ("Ratio", "Santykis"), + ("Image Quality", "Vaizdo kokybė"), + ("Scroll Style", "Slinkimo stilius"), + ("Show Menubar", "Rodyti meniu juostą"), + ("Hide Menubar", "Slėpti meniu juostą"), + ("Direct Connection", "Tiesioginis ryšys"), + ("Relay Connection", "Tarpinė jungtis"), + ("Secure Connection", "Saugus ryšys"), + ("Insecure Connection", "Nesaugus ryšys"), + ("Scale original", "Pakeisti originalų mastelį"), + ("Scale adaptive", "Pritaikomas mastelis"), + ("General", "Bendra"), + ("Security", "Sauga"), + ("Theme", "Tema"), + ("Dark Theme", "Tamsioji tema"), + ("Light Theme", "Šviesi tema"), + ("Dark", "Tamsi"), + ("Light", "Šviesi"), + ("Follow System", "Kaip sistemos"), + ("Enable hardware codec", "Įgalinti"), + ("Unlock Security Settings", "Atrakinti saugos nustatymus"), + ("Enable Audio", "Įgalinti garsą"), + ("Unlock Network Settings", "Atrakinti tinklo nustatymus"), + ("Server", "Serveris"), + ("Direct IP Access", "Tiesioginė IP prieiga"), + ("Proxy", "Tarpinis serveris"), + ("Apply", "Taikyti"), + ("Disconnect all devices?", "Atjungti visus įrenginius?"), + ("Clear", "Išvalyti"), + ("Audio Input Device", "Garso įvestis"), + ("Deny remote access", "Uždrausti nuotolinę prieigą"), + ("Use IP Whitelisting", "Naudoti patikimą IP sąrašą"), + ("Network", "Tinklas"), + ("Enable RDP", "Įgalinti RDP"), + ("Pin menubar", "Prisegti meniu juostą"), + ("Unpin menubar", "Atsegti meniu juostą"), + ("Recording", "Įrašymas"), + ("Directory", "Katalogas"), + ("Automatically record incoming sessions", "Automatiškai įrašyti įeinančius seansus"), + ("Change", "Keisti"), + ("Start session recording", "Pradėti seanso įrašinėjimą"), + ("Stop session recording", "Sustabdyti seanso įrašinėjimą"), + ("Enable Recording Session", "Įgalinti seanso įrašinėjimą"), + ("Allow recording session", "Leisti seanso įrašinėjimą"), + ("Enable LAN Discovery", "Įgalinti LAN aptikimą"), + ("Deny LAN Discovery", "Neleisti LAN aptikimo"), + ("Write a message", "Rašyti žinutę"), + ("Prompt", "Užuomina"), + ("Please wait for confirmation of UAC...", "Palaukite UAC patvirtinimo..."), + ("elevated_foreground_window_tip", "Dabartinis nuotolinio darbalaukio langas reikalauja didesnių privilegijų, todėl laikinai neįmanoma naudoti pelės ir klaviatūros. Galite paprašyti nuotolinio vartotojo sumažinti dabartinį langą arba spustelėti aukščio mygtuką ryšio valdymo lange. Norint išvengti šios problemos ateityje, rekomenduojama programinę įrangą įdiegti nuotoliniame įrenginyje."), + ("Disconnected", "Atsijungęs"), + ("Other", "Kita"), + ("Confirm before closing multiple tabs", "Patvirtinti prieš uždarant kelis skirtukus"), + ("Keyboard Settings", "Klaviatūros nustatymai"), + ("Full Access", "Pilna prieiga"), + ("Screen Share", "Ekrano bendrinimas"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland reikalauja Ubuntu 21.04 arba naujesnės versijos."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland reikalinga naujesnės Linux Distro versijos. Išbandykite X11 darbalaukį arba pakeiskite OS."), + ("JumpLink", "Peržiūra"), + ("Please Select the screen to be shared(Operate on the peer side).", "Prašome pasirinkti ekraną, kurį norite bendrinti (veikiantį kitoje pusėje)."), + ("Show RustDesk", "Rodyti RustDesk"), + ("This PC", "Šis kompiuteris"), + ("or", "arba"), + ("Continue with", "Tęsti su"), + ("Elevate", "Pakelti"), + ("Zoom cursor", "Mastelio keitimo žymeklis"), + ("Accept sessions via password", "Priimti seansus naudojant slaptažodį"), + ("Accept sessions via click", "Priimti seansus spustelėjus"), + ("Accept sessions via both", "Priimti seansus abiem variantais"), + ("Please wait for the remote side to accept your session request...", "Palaukite, kol nuotolinė pusė priims jūsų seanso užklausą..."), + ("One-time Password", "Vienkartinis slaptažodis"), + ("Use one-time password", "Naudoti vienkartinį slaptažodį"), + ("One-time password length", "Vienkartinio slaptažodžio ilgis"), + ("Request access to your device", "Prašo leidimo valdyti jūsų įrenginį"), + ("Hide connection management window", "Slėpti ryšio valdymo langą"), + ("hide_cm_tip", "Leisti paslėpti didžiąją ir mažąją raidę, jei priimamos slaptažodžio sesijos arba naudojamas nuolatinis slaptažodis"), + ("wayland_experiment_tip", "Wayland palaikymas yra eksperimentinis, naudokite X11, jei jums reikalingas automatinis prisijungimas."), + ("Right click to select tabs", "Dešiniuoju pelės mygtuku spustelėkite, kad pasirinktumėte skirtukus"), + ("Skipped", "Praleisti"), + ("Add to Address Book", "Pridėti prie adresų knygos"), + ("Group", "Grupė"), + ("Search", "Paieška"), + ("Closed manually by web console", "Uždaryta rankiniu būdu naudojant žiniatinklio konsolę"), + ("Local keyboard type", "Vietinės klaviatūros tipas"), + ("Select local keyboard type", "Pasirinkite vietinės klaviatūros tipą"), + ("software_render_tip", "Jei turite Nvidia vaizdo plokštę ir nuotolinis langas iškart užsidaro prisijungus, gali padėti „Nouveau“ tvarkyklės įdiegimas ir programinės įrangos atvaizdavimo pasirinkimas. Būtina paleisti iš naujo."), + ("Always use software rendering", "Visada naudoti programinį spartintuvą"), + ("config_input", "Norėdami valdyti nuotolinį darbalaukį naudodami klaviatūrą, turite suteikti RustDesk leidimus \"Įvesties monitoringas\"."), + ("config_microphone", "Norėdami kalbėtis su nuotoline puse, turite suteikti RustDesk leidimą \"Įrašyti garsą\"."), + ("request_elevation_tip", "Taip pat galite prašyti tesių suteikimo, jeigu kas nors yra nuotolinėje pusėje."), + ("Wait", "Laukti"), + ("Elevation Error", "Teisių suteikimo klaida"), + ("Ask the remote user for authentication", "Klauskite nuotolinio vartotojo autentifikavimo"), + ("Choose this if the remote account is administrator", "Pasirinkite tai, jei nuotolinė paskyra yra administratorius"), + ("Transmit the username and password of administrator", "Persiųsti administratoriaus vartotojo vardą ir slaptažodį"), + ("still_click_uac_tip", "Vis tiek reikia, kad nuotolinis vartotojas paleidžiant RustDesk UAC lange paspaustų \"OK\"."), + ("Request Elevation", "Prašyti teisių"), + ("wait_accept_uac_tip", "Palaukite, kol nuotolinis vartotojas patvirtins UAC užklausą."), + ("Elevate successfully", "Teisės suteiktos"), + ("uppercase", "didžiosios raidės"), + ("lowercase", "mažosios raidės"), + ("digit", "skaitmuo"), + ("special character", "specialusis simbolis"), + ("length>=8", "ilgis>=8"), + ("Weak", "Silpnas"), + ("Medium", "Vidutinis"), + ("Strong", "Stiprus"), + ("Switch Sides", "Perjungti puses"), + ("Please confirm if you want to share your desktop?", "Prašome patvirtinti, jeigu norite bendrinti darbalaukį?"), + ("Display", "Ekranas"), + ("Default View Style", "Numatytasis peržiūros stilius"), + ("Default Scroll Style", "Numatytasis slinkties stilius"), + ("Default Image Quality", "Numatytoji vaizdo kokybė"), + ("Default Codec", "Numatytasis kodekas"), + ("Bitrate", "Sparta"), + ("FPS", "FPS"), + ("Auto", "Automatinis"), + ("Other Default Options", "Kitos numatytosios parinktys"), + ("Voice call", "Balso skambutis"), + ("Text chat", "Tekstinis pokalbis"), + ("Stop voice call", "Sustabdyti balso skambutį"), + ("relay_hint_tip", "Tiesioginis ryšys gali būti neįmanomas. Tokiu atveju galite pabandyti prisijungti per perdavimo serverį. \nArba, jei norite iš karto naudoti perdavimo serverį, prie ID galite pridėti priesagą \"/r\" arba nuotolinio pagrindinio kompiuterio nustatymuose įgalinti \"Visada prisijungti per relę\"."), + ("Reconnect", "Prisijungti iš naujo"), + ("Codec", "Kodekas"), + ("Resolution", "Rezoliucija"), + ("No transfers in progress", "Nevyksta jokių perdavimų"), + ("Set one-time password length", "Nustatyti vienkartinio slaptažodžio ilgį"), + ("idd_driver_tip", "Įdiekite virtualaus ekrano tvarkyklę (naudojama, kai nėra fizinių ekranų)"), + ("confirm_idd_driver_tip", "Įjungta virtualaus ekrano tvarkyklės diegimo funkcija. Atminkite, kad bus įdiegtas bandomasis sertifikatas, kad būtų galima pasitikėti tvarkykle. Šis sertifikatas bus naudojamas tik pasitikėjimui Rustdesk tvarkyklėmis patikrinti."), + ("RDP Settings", "RDP nustatymai"), + ("Sort by", "Rūšiuoti pagal"), + ("New Connection", "Naujas ryšys"), + ("Restore", "Atkurti"), + ("Minimize", "Sumažinti"), + ("Maximize", "Padidinti"), + ("Your Device", "Jūsų įrenginys"), + ("empty_recent_tip", "Nėra paskutinių seansų!\nLaikas suplanuoti naują."), + ("empty_favorite_tip", "Dar neturite parankinių nuotolinių seansų."), + ("empty_lan_tip", "Nuotolinių mazgų nerasta."), + ("empty_address_book_tip", "Adresų knygelėje nėra nuotolinių kompiuterių."), + ("eg: admin", "pvz.: administratorius"), + ("Empty Username", "Tuščias naudotojo vardas"), + ("Empty Password", "Tuščias slaptažodis"), + ("Me", "Aš"), + ("identical_file_tip", "Failas yra identiškas nuotoliniame kompiuteryje esančiam failui."), + ("show_monitors_tip", "Rodyti monitorius įrankių juostoje"), + ("View Mode", "Peržiūros režimas"), + ("login_linux_tip", "Norėdami įjungti X darbalaukio seansą, turite būti prisijungę prie nuotolinės Linux paskyros."), + ("verify_rustdesk_password_tip", "Įveskite kliento RustDesk slaptažodį"), + ("remember_account_tip", "Prisiminti šią paskyrą"), + ("os_account_desk_tip", "Ši paskyra naudojama norint prisijungti prie nuotolinės OS ir įgalinti darbalaukio seansą režimu headless"), + ("OS Account", "OS paskyra"), + ("another_user_login_title_tip", "Kitas vartotojas jau yra prisijungęs"), + ("another_user_login_text_tip", "Atjungti"), + ("xorg_not_found_title_tip", "Xorg nerastas"), + ("xorg_not_found_text_tip", "Prašom įdiegti Xorg"), + ("no_desktop_title_tip", "Nėra pasiekiamų nuotolinių darbalaukių"), + ("no_desktop_text_tip", "Prašom įdiegti GNOME Desktop"), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/pl.rs b/src/lang/pl.rs index aabf14fc7..b350f1f45 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -37,10 +37,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schowek jest pusty"), ("Stop service", "Zatrzymaj usługę"), ("Change ID", "Zmień ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Twój nowy ID"), + ("length %min% to %max%", "o długości od %min% do %max%"), + ("starts with a letter", "rozpoczyna się literą"), + ("allowed characters", "dozwolone znaki"), ("id_change_tip", "Nowy ID może być złożony z małych i dużych liter a-zA-z, cyfry 0-9 oraz _ (podkreślenie). Pierwszym znakiem powinna być litera a-zA-Z, a całe ID powinno składać się z 6 do 16 znaków."), ("Website", "Strona internetowa"), ("About", "O aplikacji"), @@ -213,7 +213,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Połączenie zakończone ręcznie przez peer"), ("Enable remote configuration modification", "Włącz zdalną modyfikację konfiguracji"), ("Run without install", "Uruchom bez instalacji"), - ("Connect via relay", ""), + ("Connect via relay", "Połącz bezpośrednio"), ("Always connect via relay", "Zawsze łącz pośrednio"), ("whitelist_tip", "Zezwalaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("Login", "Zaloguj"), @@ -240,7 +240,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove from Favorites", "Usuń z ulubionych"), ("Empty", "Pusto"), ("Invalid folder name", "Nieprawidłowa nazwa folderu"), - ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5 Proxy", "Proxy Socks5"), ("Hostname", "Nazwa hosta"), ("Discovered", "Wykryte"), ("install_daemon_tip", "Podpowiedź instalacji daemona"), @@ -288,8 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Włączenie opcji „Przechwytywanie ekranu” spowoduje automatyczne uruchomienie usługi, umożliwiając innym urządzeniom żądanie połączenia z Twoim urządzeniem."), ("android_stop_service_tip", "Zamknięcie usługi spowoduje automatyczne zamknięcie wszystkich nawiązanych połączeń."), ("android_version_audio_tip", "Bieżąca wersja systemu Android nie obsługuje przechwytywania dźwięku, zaktualizuj system do wersji Android 10 lub nowszej."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_start_service_tip", "Kliknij [Uruchom serwis] lub włącz uprawnienia [Zrzuty ekranu], aby uruchomić usługę udostępniania ekranu."), + ("android_permission_may_not_change_tip", "Uprawnienia do nawiązanych połączeń nie mogą być zmieniane automatycznie, dopiero po ponownym połączeniu."), ("Account", "Konto"), ("Overwrite", "Nadpisz"), ("This file exists, skip or overwrite this file?", "Ten plik istnieje, pominąć czy nadpisać ten plik?"), @@ -348,7 +348,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Security", "Zabezpieczenia"), ("Theme", "Motyw"), ("Dark Theme", "Ciemny motyw"), - ("Light Theme", ""), + ("Light Theme", "Jasny motyw"), ("Dark", "Ciemny"), ("Light", "Jasny"), ("Follow System", "Zgodny z systemem"), @@ -408,7 +408,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time password length", "Długość hasła jednorazowego"), ("Request access to your device", "Żądanie dostępu do Twojego urządzenia"), ("Hide connection management window", "Ukryj okno zarządzania połączeniem"), - ("hide_cm_tip", "Pozwalaj na ukrycie tylko gdy akceptujesz sesje za pośrednictwem hasła i używasz hasła permanentnego"), + ("hide_cm_tip", "Pozwalaj na ukrycie tylko, gdy akceptujesz sesje za pośrednictwem hasła i używasz hasła permanentnego"), ("wayland_experiment_tip", "Wsparcie dla Wayland jest niekompletne, użyj X11 jeżeli chcesz korzystać z dostępu nienadzorowanego"), ("Right click to select tabs", "Kliknij prawym przyciskiem myszy by wybrać zakładkę"), ("Skipped", "Pominięte"), @@ -421,7 +421,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Jeżeli posiadasz kartę graficzną Nvidia i okno zamyka się natychmiast po nawiązaniu połączenia, instalacja sterownika nouveau i wybór renderowania programowego mogą pomóc. Restart aplikacji jest wymagany."), ("Always use software rendering", "Zawsze używaj renderowania programowego"), ("config_input", "By kontrolować zdalne urządzenie przy pomocy klawiatury, musisz udzielić aplikacji RustDesk uprawnień do \"Urządzeń Wejściowych\"."), - ("config_microphone", ""), + ("config_microphone", "Aby umożliwić zdalne rozmowy należy przyznać RuskDesk uprawnienia do \"Nagrań audio\"."), ("request_elevation_tip", "Możesz poprosić o podniesienie uprawnień jeżeli ktoś posiada dostęp do zdalnego urządzenia."), ("Wait", "Czekaj"), ("Elevation Error", "Błąd przy podnoszeniu uprawnień"), @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default View Style", "Domyślny styl wyświetlania"), ("Default Scroll Style", "Domyślny styl przewijania"), ("Default Image Quality", "Domyślna jakość obrazu"), - ("Default Codec", "Dokyślny kodek"), + ("Default Codec", "Domyślny kodek"), ("Bitrate", "Bitrate"), ("FPS", "FPS"), ("Auto", "Auto"), @@ -459,26 +459,44 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Codec", "Kodek"), ("Resolution", "Rozdzielczość"), ("No transfers in progress", "Brak transferów w toku"), - ("Set one-time password length", ""), - ("idd_driver_tip", ""), - ("confirm_idd_driver_tip", ""), - ("RDP Settings", ""), - ("Sort by", ""), - ("New Connection", ""), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), - ("Your Device", ""), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", ""), - ("Empty Username", ""), - ("Empty Password", ""), - ("Me", ""), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), + ("Set one-time password length", "Ustaw długość jednorazowego hasła"), + ("idd_driver_tip", "Zainstaluj sterownik wirtualnego wyświetlacza, który jest używany, gdy nie masz fizycznych monitorów."), + ("confirm_idd_driver_tip", "Opcja instalacji sterownika wirtualnego wyświetlacza jest zaznaczona. Pamiętaj, że zostanie zainstalowany testowy certyfikat, aby zaufać wirtualnemu sterownikowi. Ten certyfikat będzie używany tylko do weryfikacji sterowników RustDesk."), + ("RDP Settings", "Ustawienia RDP"), + ("Sort by", "Sortuj wg"), + ("New Connection", "Nowe połączenie"), + ("Restore", "Przywróć"), + ("Minimize", "Minimalizuj"), + ("Maximize", "Maksymalizuj"), + ("Your Device", "Twoje urządzenie"), + ("empty_recent_tip", "Ups, żadnych nowych sesji!\nCzas zaplanować nową."), + ("empty_favorite_tip", "Brak ulubionych?\nZnajdźmy kogoś, z kim możesz się połączyć i dodaj Go do ulubionych!"), + ("empty_lan_tip", "Ojej, wygląda na to, że nie odkryliśmy żadnych urządzeń z RustDesk w Twojej sieci."), + ("empty_address_book_tip", "Ojej, wygląda na to, że nie ma żadnych wpisów w Twojej książce adresowej."), + ("eg: admin", "np. admin"), + ("Empty Username", "Pusty użytkownik"), + ("Empty Password", "Puste hasło"), + ("Me", "Ja"), + ("identical_file_tip", "Ten plik jest identyczny z plikiem na drugim komputerze."), + ("show_monitors_tip", "Pokaż monitory w zasobniku"), + ("View Mode", "Tryb widoku"), + ("login_linux_tip", "Zaloguj do zdalnego konta Linux"), + ("verify_rustdesk_password_tip", "Weryfikuj hasło RustDesk"), + ("remember_account_tip", "Zapamiętaj to konto"), + ("os_account_desk_tip", "To konto jest używane do logowania do zdalnych systemów i włącza bezobsługowe sesje pulpitu"), + ("OS Account", "Konto systemowe"), + ("another_user_login_title_tip", "Inny użytkownik jest już zalogowany"), + ("another_user_login_text_tip", "Rozłącz"), + ("xorg_not_found_title_tip", "Nie znaleziono Xorg"), + ("xorg_not_found_text_tip", "Proszę zainstalować Xorg"), + ("no_desktop_title_tip", "Żaden pulpit nie jest dostępny"), + ("no_desktop_text_tip", "Proszę zainstalować pulpit GNOME"), + ("No need to elevate", "Podniesienie uprawnień nie jest wymagane"), + ("System Sound", "Dźwięk Systemowy"), + ("Default", "Domyślne"), + ("New RDP", "Nowe RDP"), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 25561d43a..6814d8dcb 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 6a6df5078..4a4a6401d 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 886f27bd4..d2a4cb265 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 0dd1c6fe9..f9a375ff5 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -460,7 +460,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resolution", "Разрешение"), ("No transfers in progress", "Передача не осуществляется"), ("Set one-time password length", "Установить длину одноразового пароля"), - ("idd_driver_tip", "Установите драйвер виртуального дисплея, который используется при отсутствии физических дисплеев."), + ("idd_driver_tip", "Установить драйвер виртуального дисплея (используется при отсутствии физических дисплеев)"), ("confirm_idd_driver_tip", "Включена функция установки драйвера виртуального дисплея. Обратите внимание, что для доверия к драйверу будет установлен тестовый сертификат. Этот сертификат будет использоваться только для подтверждения доверия драйверам Rustdesk."), ("RDP Settings", "Настройки RDP"), ("Sort by", "Сортировка"), @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Файл идентичен файлу на удалённом узле."), ("show_monitors_tip", "Показывать мониторы на панели инструментов"), ("View Mode", "Режим просмотра"), + ("login_linux_tip", "Чтобы включить сеанс рабочего стола X, необходимо войти в удалённый аккаунт Linux."), + ("verify_rustdesk_password_tip", "Подтвердить пароль RustDesk"), + ("remember_account_tip", "Запомнить этот аккаунт"), + ("os_account_desk_tip", "Этот аккаунт используется для входа в удалённую ОС и включения сеанса рабочего стола в режиме headless"), + ("OS Account", "Аккаунт ОС"), + ("another_user_login_title_tip", "Другой пользователь уже вошёл в систему"), + ("another_user_login_text_tip", "Отключить"), + ("xorg_not_found_title_tip", "Xorg не найден"), + ("xorg_not_found_text_tip", "Установите Xorg"), + ("no_desktop_title_tip", "Нет доступных рабочих столов"), + ("no_desktop_text_tip", "Установите GNOME Desktop"), + ("No need to elevate", "Повышение прав не требуется"), + ("System Sound", "Системный звук"), + ("Default", "По умолчанию"), + ("New RDP", "Новый RDP"), + ("Fingerprint", "Отпечаток"), + ("Copy Fingerprint", "Копировать отпечаток"), + ("no fingerprints", "отпечатки отсутствуют"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index ceed59b43..51f12c102 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 14e50fc1d..52f1381c8 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 142074539..86aae1150 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 7ea384def..554bd7589 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 31db07b30..69fb3abb8 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 343d6c1ea..9d567c9b4 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a05f8ff88..172677cc3 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index e00877118..9907141a3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index d562cdfb4..f9337827a 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -7,37 +7,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Password", "密碼"), ("Ready", "就緒"), ("Established", "已建立"), - ("connecting_status", "正在連接至 RustDesk 網路..."), + ("connecting_status", "正在連線到 RustDesk 網路 ..."), ("Enable Service", "啟用服務"), ("Start Service", "啟動服務"), - ("Service is running", "服務正在運行"), + ("Service is running", "服務正在執行"), ("Service is not running", "服務尚未執行"), - ("not_ready_status", "尚未就緒。請檢查您的網路連線"), + ("not_ready_status", "尚未就緒,請檢查您的網路連線。"), ("Control Remote Desktop", "控制遠端桌面"), ("Transfer File", "傳輸檔案"), - ("Connect", "連接"), + ("Connect", "連線"), ("Recent Sessions", "近期的工作階段"), ("Address Book", "通訊錄"), ("Confirmation", "確認"), ("TCP Tunneling", "TCP 通道"), ("Remove", "移除"), ("Refresh random password", "重新產生隨機密碼"), - ("Set your own password", "自行設置密碼"), - ("Enable Keyboard/Mouse", "啟用鍵盤/滑鼠"), + ("Set your own password", "自行設定密碼"), + ("Enable Keyboard/Mouse", "啟用鍵盤和滑鼠"), ("Enable Clipboard", "啟用剪貼簿"), ("Enable File Transfer", "啟用檔案傳輸"), ("Enable TCP Tunneling", "啟用 TCP 通道"), ("IP Whitelisting", "IP 白名單"), - ("ID/Relay Server", "ID/轉送伺服器"), + ("ID/Relay Server", "ID / 轉送伺服器"), ("Import Server Config", "匯入伺服器設定"), - ("Export Server Config", "導出服務器配置"), + ("Export Server Config", "匯出伺服器設定"), ("Import server configuration successfully", "匯入伺服器設定成功"), - ("Export server configuration successfully", "導出服務器配置信息成功"), + ("Export server configuration successfully", "匯出伺服器設定成功"), ("Invalid server configuration", "無效的伺服器設定"), ("Clipboard is empty", "剪貼簿是空的"), ("Stop service", "停止服務"), ("Change ID", "更改 ID"), - ("Your new ID", "你的新 ID"), + ("Your new ID", "您的新 ID"), ("length %min% to %max%", "長度在 %min% 與 %max% 之間"), ("starts with a letter", "以字母開頭"), ("allowed characters", "使用允許的字元"), @@ -45,22 +45,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Website", "網站"), ("About", "關於"), ("Slogan_tip", ""), - ("Privacy Statement", "隱私聲明"), + ("Privacy Statement", "隱私權聲明"), ("Mute", "靜音"), - ("Build Date", "建構日期"), + ("Build Date", "構建日期"), ("Version", "版本"), - ("Home", "主頁"), + ("Home", "首頁"), ("Audio Input", "音訊輸入"), ("Enhancements", "增強功能"), - ("Hardware Codec", "硬件編解碼"), - ("Adaptive Bitrate", "自適應碼率"), + ("Hardware Codec", "硬體編解碼器"), + ("Adaptive Bitrate", "自適應位元速率"), ("ID Server", "ID 伺服器"), ("Relay Server", "轉送伺服器"), ("API Server", "API 伺服器"), ("invalid_http", "開頭必須為 http:// 或 https://"), ("Invalid IP", "IP 無效"), ("Invalid format", "格式無效"), - ("server_not_support", "服務器暫不支持"), + ("server_not_support", "伺服器暫不支持"), ("Not available", "無法使用"), ("Too frequent", "修改過於頻繁,請稍後再試。"), ("Cancel", "取消"), @@ -75,13 +75,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you want to enter again?", "您要重新輸入嗎?"), ("Connection Error", "連線錯誤"), ("Error", "錯誤"), - ("Reset by the peer", "對方重置了連線"), - ("Connecting...", "正在連接..."), - ("Connection in progress. Please wait.", "正在連接,請稍候。"), + ("Reset by the peer", "對方重設了連線"), + ("Connecting...", "正在連線 ..."), + ("Connection in progress. Please wait.", "正在連線,請稍候。"), ("Please try 1 minute later", "請於 1 分鐘後再試"), ("Login Error", "登入錯誤"), ("Successful", "成功"), - ("Connected, waiting for image...", "已連線,等待畫面傳輸..."), + ("Connected, waiting for image...", "已連線,等待畫面傳輸 ..."), ("Name", "名稱"), ("Type", "類型"), ("Modified", "修改時間"), @@ -89,7 +89,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show Hidden Files", "顯示隱藏檔案"), ("Receive", "接收"), ("Send", "傳送"), - ("Refresh File", "刷新文件"), + ("Refresh File", "重新整理檔案"), ("Local", "本地"), ("Remote", "遠端"), ("Remote Computer", "遠端電腦"), @@ -100,19 +100,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Multi Select", "多選"), ("Select All", "全選"), ("Unselect All", "取消全選"), - ("Empty Directory", "空文件夾"), - ("Not an empty directory", "不是一個空文件夾"), + ("Empty Directory", "空資料夾"), + ("Not an empty directory", "不是一個空資料夾"), ("Are you sure you want to delete this file?", "您確定要刪除此檔案嗎?"), - ("Are you sure you want to delete this empty directory?", "您確定要刪除此空目錄嗎?"), - ("Are you sure you want to delete the file of this directory?", "您確定要刪除此目錄中的檔案嗎?"), + ("Are you sure you want to delete this empty directory?", "您確定要刪除此空資料夾嗎?"), + ("Are you sure you want to delete the file of this directory?", "您確定要刪除此資料夾中的檔案嗎?"), ("Do this for all conflicts", "套用到其他衝突"), ("This is irreversible!", "此操作不可逆!"), - ("Deleting", "正在刪除"), + ("Deleting", "正在刪除 ..."), ("files", "檔案"), - ("Waiting", "正在等候..."), + ("Waiting", "正在等候 ..."), ("Finished", "已完成"), ("Speed", "速度"), - ("Custom Image Quality", "自訂圖片品質"), + ("Custom Image Quality", "自訂畫面品質"), ("Privacy mode", "隱私模式"), ("Block user input", "封鎖使用者輸入"), ("Unblock user input", "取消封鎖使用者輸入"), @@ -122,10 +122,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stretch", "延展"), ("Scrollbar", "滾動條"), ("ScrollAuto", "自動滾動"), - ("Good image quality", "畫面品質良好"), + ("Good image quality", "最佳化畫面品質"), ("Balanced", "平衡"), - ("Optimize reaction time", "回應速度最佳化"), - ("Custom", "自定義"), + ("Optimize reaction time", "最佳化反應時間"), + ("Custom", "自訂"), ("Show remote cursor", "顯示遠端游標"), ("Show quality monitor", "顯示質量監測"), ("Disable clipboard", "停用剪貼簿"), @@ -134,71 +134,71 @@ 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", "無法連接至 rendezvous 伺服器"), + ("Failed to connect to rendezvous server", "無法連線到 rendezvous 伺服器"), ("Please try later", "請稍候再試"), - ("Remote desktop is offline", "遠端電腦離線"), + ("Remote desktop is offline", "遠端桌面已離線"), ("Key mismatch", "金鑰不符"), ("Timeout", "逾時"), - ("Failed to connect to relay server", "無法連接至轉送伺服器"), - ("Failed to connect via rendezvous server", "無法透過 rendezvous 伺服器連接"), - ("Failed to connect via relay server", "無法透過轉送伺服器連接"), - ("Failed to make direct connection to remote desktop", "無法直接連線至遠端電腦"), - ("Set Password", "設置密碼"), + ("Failed to connect to relay server", "無法連線到轉送伺服器"), + ("Failed to connect via rendezvous server", "無法透過 rendezvous 伺服器連線"), + ("Failed to connect via relay server", "無法透過轉送伺服器連線"), + ("Failed to make direct connection to remote desktop", "無法直接連線到遠端桌面"), + ("Set Password", "設定密碼"), ("OS Password", "作業系統密碼"), - ("install_tip", "UAC 會導致 RustDesk 在某些情況下無法正常以遠端電腦運作。若要避開 UAC,請點擊下方按鈕將 RustDesk 安裝到系統中。"), + ("install_tip", "UAC 會導致 RustDesk 在某些情況下無法正常以遠端電腦執行。若要避開 UAC,請點擊下方按鈕將 RustDesk 安裝到系統中。"), ("Click to upgrade", "點擊以升級"), ("Click to download", "點擊以下載"), ("Click to update", "點擊以更新"), ("Configure", "設定"), - ("config_acc", "您需要授予 RustDesk 「協助工具」 權限才能遠端存取電腦。"), - ("config_screen", "您需要授予 RustDesk 「畫面錄製」 權限才能遠端存取電腦。"), - ("Installing ...", "正在安裝..."), + ("config_acc", "您需要授予 RustDesk「協助工具」權限才能存取遠端電腦。"), + ("config_screen", "您需要授予 RustDesk「畫面錄製」權限才能存取遠端電腦。"), + ("Installing ...", "正在安裝 ..."), ("Install", "安裝"), ("Installation", "安裝"), ("Installation Path", "安裝路徑"), - ("Create start menu shortcuts", "建立開始選單捷徑"), - ("Create desktop icon", "建立桌面圖示"), + ("Create start menu shortcuts", "新增開始功能表捷徑"), + ("Create desktop icon", "新增桌面捷徑"), ("agreement_tip", "開始安裝即表示接受許可協議"), ("Accept and Install", "接受並安裝"), ("End-user license agreement", "使用者授權合約"), ("Generating ...", "正在產生 ..."), - ("Your installation is lower version.", "您的安裝版本過舊。"), + ("Your installation is lower version.", "您安裝的版本過舊。"), ("not_close_tcp_tip", "使用通道時請不要關閉此視窗"), - ("Listening ...", "正在等待通道連接..."), + ("Listening ...", "正在等待通道連線 ..."), ("Remote Host", "遠端主機"), - ("Remote Port", "遠端連接埠"), + ("Remote Port", "遠端連線端口"), ("Action", "操作"), ("Add", "新增"), - ("Local Port", "本機連接埠"), + ("Local Port", "本機連線端口"), ("Local Address", "本機地址"), - ("Change Local Port", "修改本機連接埠"), - ("setup_server_tip", "若您需要更快的連接速度,可以選擇自行建立伺服器"), - ("Too short, at least 6 characters.", "過短,至少需 6 個字元。"), + ("Change Local Port", "修改本機連線端口"), + ("setup_server_tip", "若您需要更快的連線速度,可以選擇自行建立伺服器"), + ("Too short, at least 6 characters.", "過短,至少需要 6 個字元。"), ("The confirmation is not identical.", "兩次輸入不相符"), ("Permissions", "權限"), ("Accept", "接受"), ("Dismiss", "關閉"), - ("Disconnect", "斷開連線"), + ("Disconnect", "中斷連線"), ("Allow using keyboard and mouse", "允許使用鍵盤和滑鼠"), ("Allow using clipboard", "允許使用剪貼簿"), ("Allow hearing sound", "允許分享音訊"), - ("Allow file copy and paste", "允許文件複製和粘貼"), - ("Connected", "已連接"), + ("Allow file copy and paste", "允許檔案複製和貼上"), + ("Connected", "已連線"), ("Direct and encrypted connection", "加密直接連線"), ("Relayed and encrypted connection", "加密轉送連線"), ("Direct and unencrypted connection", "未加密直接連線"), ("Relayed and unencrypted connection", "未加密轉送連線"), ("Enter Remote ID", "輸入遠端 ID"), ("Enter your password", "輸入您的密碼"), - ("Logging in...", "正在登入..."), + ("Logging in...", "正在登入 ..."), ("Enable RDP session sharing", "啟用 RDP 工作階段共享"), ("Auto Login", "自動登入 (鎖定將在設定關閉後套用)"), ("Enable Direct IP Access", "允許 IP 直接存取"), ("Rename", "重新命名"), ("Space", "空白"), - ("Create Desktop Shortcut", "建立桌面捷徑"), + ("Create Desktop Shortcut", "新增桌面捷徑"), ("Change Path", "更改路徑"), - ("Create Folder", "建立資料夾"), + ("Create Folder", "新增資料夾"), ("Please enter the folder name", "請輸入資料夾名稱"), ("Fix it", "修復"), ("Warning", "警告"), @@ -209,9 +209,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Port", "端口"), ("Settings", "設定"), ("Username", "使用者名稱"), - ("Invalid port", "連接埠無效"), - ("Closed manually by the peer", "由對方手動關閉"), - ("Enable remote configuration modification", "啟用遠端更改設定"), + ("Invalid port", "連線端口無效"), + ("Closed manually by the peer", "遠端使用者關閉了工作階段"), + ("Enable remote configuration modification", "允許遠端使用者更改設定"), ("Run without install", "跳過安裝直接執行"), ("Connect via relay", "中繼連線"), ("Always connect via relay", "一律透過轉送連線"), @@ -219,9 +219,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Login", "登入"), ("Verify", "驗證"), ("Remember me", "記住我"), - ("Trust this device", "信任此設備"), + ("Trust this device", "信任此裝置"), ("Verification code", "驗證碼"), - ("verification_tip", "檢測到新設備登錄,已向註冊郵箱發送了登入驗證碼,請輸入驗證碼繼續登錄"), + ("verification_tip", "檢測到新裝置登入,已向註冊電子信箱發送了登入驗證碼,請輸入驗證碼以繼續登入"), ("Logout", "登出"), ("Tags", "標籤"), ("Search ID", "搜尋 ID"), @@ -235,18 +235,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wrong credentials", "提供的登入資訊有誤"), ("Edit Tag", "編輯標籤"), ("Unremember Password", "忘掉密碼"), - ("Favorites", "收藏"), - ("Add to Favorites", "加入到收藏"), - ("Remove from Favorites", "從收藏中刪除"), + ("Favorites", "我的最愛"), + ("Add to Favorites", "新增到我的最愛"), + ("Remove from Favorites", "從我的最愛中刪除"), ("Empty", "空空如也"), ("Invalid folder name", "資料夾名稱無效"), ("Socks5 Proxy", "Socks5 代理"), ("Hostname", "主機名稱"), - ("Discovered", "已發現"), - ("install_daemon_tip", "為了開機啟動,請安裝系統服務。"), + ("Discovered", "已探索"), + ("install_daemon_tip", "為了能夠開機時自動啟動,請先安裝系統服務。"), ("Remote ID", "遠端 ID"), ("Paste", "貼上"), - ("Paste here?", "貼上到這裡?"), + ("Paste here?", "貼上到這裡?"), ("Are you sure to close the connection?", "您確定要關閉連線嗎?"), ("Download new version", "下載新版本"), ("Touch mode", "觸控模式"), @@ -265,12 +265,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Canvas Move", "移動畫布"), ("Pinch to Zoom", "雙指縮放"), ("Canvas Zoom", "縮放畫布"), - ("Reset canvas", "重置畫布"), - ("No permission of file transfer", "無文件傳輸權限"), + ("Reset canvas", "重設畫布"), + ("No permission of file transfer", "沒有檔案傳輸權限"), ("Note", "備註"), - ("Connection", "連接"), - ("Share Screen", "共享畫面"), - ("Chat", "聊天消息"), + ("Connection", "連線"), + ("Share Screen", "共享螢幕畫面"), + ("Chat", "聊天訊息"), ("Total", "總計"), ("items", "個項目"), ("Selected", "已選擇"), @@ -280,26 +280,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("File Connection", "檔案連線"), ("Screen Connection", "畫面連線"), ("Do you accept?", "是否接受?"), - ("Open System Setting", "打開系統設定"), + ("Open System Setting", "開啟系統設定"), ("How to get Android input permission?", "如何獲取 Android 的輸入權限?"), - ("android_input_permission_tip1", "取得輸入權限後可以讓遠端裝置通過滑鼠控制此 Android 裝置"), - ("android_input_permission_tip2", "請在接下來的系統設定頁面中,找到並進入 「已安裝的服務」 頁面,並將 「RustDesk Input」 服務開啟"), - ("android_new_connection_tip", "收到新的連接控制請求,對方想要控制您目前的設備"), - ("android_service_will_start_tip", "開啟畫面錄製權限將自動開啟服務,允許其他裝置向此裝置請求建立連接。"), - ("android_stop_service_tip", "關閉服務將自動關閉所有已建立的連接。"), - ("android_version_audio_tip", "目前的 Android 版本不支持音訊錄製,請升級至 Android 10 或以上版本。"), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", "對於已經建立的連接,權限可能不會立即發生改變,除非重新建立連接。"), - ("Account", "賬戶"), - ("Overwrite", "覆寫"), - ("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要跳過或是覆寫此檔案嗎?"), + ("android_input_permission_tip1", "取得輸入權限後可以讓遠端裝置透過滑鼠控制此 Android 裝置"), + ("android_input_permission_tip2", "請在接下來的系統設定頁面中,找到並進入「已安裝的服務」頁面,並將「RustDesk Input」服務開啟"), + ("android_new_connection_tip", "收到新的連線控制請求,對方想要控制您目前的裝置"), + ("android_service_will_start_tip", "開啟畫面錄製權限將自動開啟服務,允許其他裝置向此裝置請求建立連線。"), + ("android_stop_service_tip", "關閉服務將自動關閉所有已建立的連線。"), + ("android_version_audio_tip", "目前的 Android 版本不支援音訊錄製,請升級到 Android 10 或以上版本。"), + ("android_start_service_tip", "點擊「啟動服務」或啟用「螢幕錄製」權限,以啟動螢幕共享服務。"), + ("android_permission_may_not_change_tip", "對於已經建立的連線,權限可能不會立即發生改變,除非重新建立連線。"), + ("Account", "帳號"), + ("Overwrite", "取代"), + ("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要略過或是取代此檔案嗎?"), ("Quit", "退出"), ("doc_mac_permission", "https://rustdesk.com/docs/zh-tw/manual/mac/#啟用權限"), ("Help", "幫助"), ("Failed", "失敗"), ("Succeeded", "成功"), - ("Someone turns on privacy mode, exit", "其他用戶開啟隱私模式,退出"), - ("Unsupported", "不支持"), + ("Someone turns on privacy mode, exit", "其他使用者開啟隱私模式,退出"), + ("Unsupported", "不支援"), ("Peer denied", "被控端拒絕"), ("Please install plugins", "請安裝插件"), ("Peer exit", "被控端退出"), @@ -308,177 +308,195 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("In privacy mode", "開啟隱私模式"), ("Out privacy mode", "退出隱私模式"), ("Language", "語言"), - ("Keep RustDesk background service", "保持RustDesk後台服務"), - ("Ignore Battery Optimizations", "忽略電池優化"), - ("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的RustDesk應用設置頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"), - ("Start on Boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", "對方不允許連接"), + ("Keep RustDesk background service", "保持 RustDesk 後台服務"), + ("Ignore Battery Optimizations", "忽略電池最佳化"), + ("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的 RustDesk 應用設定頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"), + ("Start on Boot", "開機自動啟動"), + ("Start the screen sharing service on boot, requires special permissions", "開機自動啟動螢幕共享服務,此功能需要一些特殊權限。"), + ("Connection not allowed", "對方不允許連線"), ("Legacy mode", "傳統模式"), - ("Map mode", "1:1傳輸"), + ("Map mode", "1:1 傳輸模式"), ("Translate mode", "翻譯模式"), ("Use permanent password", "使用固定密碼"), ("Use both passwords", "同時使用兩種密碼"), ("Set permanent password", "設定固定密碼"), - ("Enable Remote Restart", "允許遠程重啓"), - ("Allow remote restart", "允許遠程重啓"), - ("Restart Remote Device", "重啓遠程電腦"), - ("Are you sure you want to restart", "确定要重启"), - ("Restarting Remote Device", "正在重啓遠程設備"), - ("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"), + ("Enable Remote Restart", "啟用遠端重新啟動"), + ("Allow remote restart", "允許遠端重新啟動"), + ("Restart Remote Device", "重新啟動遠端電腦"), + ("Are you sure you want to restart", "確定要重新啟動"), + ("Restarting Remote Device", "正在重新啟動遠端裝置"), + ("remote_restarting_tip", "遠端裝置正在重新啟動,請關閉當前提示框,並在一段時間後使用永久密碼重新連線"), ("Copied", "已複製"), - ("Exit Fullscreen", "退出全屏"), - ("Fullscreen", "全屏"), - ("Mobile Actions", "移動端操作"), - ("Select Monitor", "選擇監視器"), + ("Exit Fullscreen", "退出全螢幕"), + ("Fullscreen", "全螢幕"), + ("Mobile Actions", "手機操作"), + ("Select Monitor", "選擇顯示器"), ("Control Actions", "控制操作"), - ("Display Settings", "顯示設置"), + ("Display Settings", "顯示設定"), ("Ratio", "比例"), ("Image Quality", "畫質"), ("Scroll Style", "滾動樣式"), - ("Show Menubar", "顯示菜單欄"), - ("Hide Menubar", "隱藏菜單欄"), - ("Direct Connection", "直接連接"), - ("Relay Connection", "中繼連接"), - ("Secure Connection", "安全連接"), - ("Insecure Connection", "非安全連接"), + ("Show Menubar", "顯示選單欄"), + ("Hide Menubar", "隱藏選單欄"), + ("Direct Connection", "直接連線"), + ("Relay Connection", "中繼連線"), + ("Secure Connection", "安全連線"), + ("Insecure Connection", "非安全連線"), ("Scale original", "原始尺寸"), - ("Scale adaptive", "適應窗口"), - ("General", "常規"), + ("Scale adaptive", "適應視窗"), + ("General", "通用"), ("Security", "安全"), ("Theme", "主題"), - ("Dark Theme", "暗黑主題"), + ("Dark Theme", "黑暗主題"), ("Light Theme", "明亮主題"), ("Dark", "黑暗"), ("Light", "明亮"), ("Follow System", "跟隨系統"), - ("Enable hardware codec", "使用硬件編解碼"), - ("Unlock Security Settings", "解鎖安全設置"), - ("Enable Audio", "允許傳輸音頻"), - ("Unlock Network Settings", "解鎖網絡設置"), - ("Server", "服務器"), - ("Direct IP Access", "IP直接訪問"), + ("Enable hardware codec", "使用硬體編解碼器"), + ("Unlock Security Settings", "解鎖安全設定"), + ("Enable Audio", "允許傳輸音訊"), + ("Unlock Network Settings", "解鎖網路設定"), + ("Server", "伺服器"), + ("Direct IP Access", "IP 直接連線"), ("Proxy", "代理"), ("Apply", "應用"), - ("Disconnect all devices?", "斷開所有遠程連接?"), + ("Disconnect all devices?", "中斷所有遠端連線?"), ("Clear", "清空"), - ("Audio Input Device", "音頻輸入設備"), - ("Deny remote access", "拒絕遠程訪問"), - ("Use IP Whitelisting", "只允許白名單上的IP訪問"), - ("Network", "網絡"), - ("Enable RDP", "允許RDP訪問"), - ("Pin menubar", "固定菜單欄"), - ("Unpin menubar", "取消固定菜單欄"), - ("Recording", "錄屏"), - ("Directory", "目錄"), - ("Automatically record incoming sessions", "自動錄製來訪會話"), + ("Audio Input Device", "音訊輸入裝置"), + ("Deny remote access", "拒絕遠端存取"), + ("Use IP Whitelisting", "只允許白名單上的 IP 進行連線"), + ("Network", "網路"), + ("Enable RDP", "允許 RDP 訪問"), + ("Pin menubar", "固定選單欄"), + ("Unpin menubar", "取消固定選單欄"), + ("Recording", "錄製"), + ("Directory", "路徑"), + ("Automatically record incoming sessions", "自動錄製連入的工作階段"), ("Change", "變更"), - ("Start session recording", "開始錄屏"), - ("Stop session recording", "結束錄屏"), - ("Enable Recording Session", "允許錄製會話"), - ("Allow recording session", "允許錄製會話"), - ("Enable LAN Discovery", "允許局域網發現"), - ("Deny LAN Discovery", "拒絕局域網發現"), - ("Write a message", "輸入聊天消息"), + ("Start session recording", "開始錄影"), + ("Stop session recording", "停止錄影"), + ("Enable Recording Session", "啟用錄製工作階段"), + ("Allow recording session", "允許錄製工作階段"), + ("Enable LAN Discovery", "允許區域網路探索"), + ("Deny LAN Discovery", "拒絕區域網路探索"), + ("Write a message", "輸入聊天訊息"), ("Prompt", "提示"), - ("Please wait for confirmation of UAC...", "請等待對方確認UAC"), - ("elevated_foreground_window_tip", "遠端桌面的當前窗口需要更高的權限才能操作, 暫時無法使用鼠標鍵盤, 可以請求對方最小化當前窗口, 或者在連接管理窗口點擊提升。為避免這個問題,建議在遠端設備上安裝本軟件。"), - ("Disconnected", "會話已結束"), + ("Please wait for confirmation of UAC...", "請等待對方確認 UAC ..."), + ("elevated_foreground_window_tip", "目前的遠端桌面視窗需要更高的權限才能繼續操作,暫時無法使用滑鼠、鍵盤,可以請求對方最小化目前視窗,或者在連線管理視窗點擊提升權限。為了避免這個問題,建議在遠端裝置上安裝本軟體。"), + ("Disconnected", "斷開連線"), ("Other", "其他"), - ("Confirm before closing multiple tabs", "關閉多個分頁前跟我確認"), - ("Keyboard Settings", "鍵盤設置"), + ("Confirm before closing multiple tabs", "關閉多個分頁前詢問我"), + ("Keyboard Settings", "鍵盤設定"), ("Full Access", "完全訪問"), - ("Screen Share", "僅共享屏幕"), + ("Screen Share", "僅分享螢幕畫面"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 發行版。 請嘗試 X11 桌面或更改您的操作系統。"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 Linux 發行版。請嘗試使用 X11 桌面或更改您的作業系統。"), ("JumpLink", "查看"), - ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的畫面(在對端操作)。"), + ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的螢幕畫面(在對端操作)。"), ("Show RustDesk", "顯示 RustDesk"), ("This PC", "此電腦"), ("or", "或"), - ("Continue with", "使用"), - ("Elevate", "提權"), + ("Continue with", "繼續"), + ("Elevate", "提升權限"), ("Zoom cursor", "縮放游標"), - ("Accept sessions via password", "只允許密碼訪問"), - ("Accept sessions via click", "只允許點擊訪問"), - ("Accept sessions via both", "允許密碼或點擊訪問"), - ("Please wait for the remote side to accept your session request...", "請等待對方接受你的連接..."), + ("Accept sessions via password", "只允許透過輸入密碼進行連線"), + ("Accept sessions via click", "只允許透過點擊接受進行連線"), + ("Accept sessions via both", "允許輸入密碼或點擊接受進行連線"), + ("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求 ..."), ("One-time Password", "一次性密碼"), ("Use one-time password", "使用一次性密碼"), ("One-time password length", "一次性密碼長度"), - ("Request access to your device", "請求訪問你的設備"), - ("Hide connection management window", "隱藏連接管理窗口"), - ("hide_cm_tip", "在只允許密碼連接並且只用固定密碼的情況下才允許隱藏"), - ("wayland_experiment_tip", "Wayland 支持處於實驗階段,如果你需要使用無人值守訪問,請使用 X11。"), - ("Right click to select tabs", "右鍵選擇選項卡"), + ("Request access to your device", "請求訪問您的裝置"), + ("Hide connection management window", "隱藏連線管理視窗"), + ("hide_cm_tip", "在只允許密碼連線並且只用固定密碼的情況下才允許隱藏"), + ("wayland_experiment_tip", "Wayland 支援處於實驗階段,如果您需要使用無人值守訪問,請使用 X11。"), + ("Right click to select tabs", "右鍵選擇分頁"), ("Skipped", "已略過"), - ("Add to Address Book", "添加到地址簿"), - ("Group", "小組"), - ("Search", "搜索"), - ("Closed manually by web console", "被web控制台手動關閉"), + ("Add to Address Book", "新增到通訊錄"), + ("Group", "群組"), + ("Search", "搜尋"), + ("Closed manually by web console", "被 Web 控制台手動關閉"), ("Local keyboard type", "本地鍵盤類型"), ("Select local keyboard type", "請選擇本地鍵盤類型"), - ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), - ("Always use software rendering", "使用軟件渲染"), - ("config_input", "為了能夠通過鍵盤控制遠程桌面, 請給予 RustDesk \"輸入監控\" 權限。"), - ("config_microphone", "為了支持通過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"), - ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), + ("software_render_tip", "如果您使用 NVIDIA 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體渲染可能會有幫助。重新啟動軟體後生效。"), + ("Always use software rendering", "使用軟體渲染"), + ("config_input", "為了能夠透過鍵盤控制遠端桌面,請給予 RustDesk \"輸入監控\" 權限。"), + ("config_microphone", "為了支援透過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"), + ("request_elevation_tip", "如果遠端使用者可以操作電腦,也可以請求提升權限。"), ("Wait", "等待"), - ("Elevation Error", "提權失敗"), - ("Ask the remote user for authentication", "請求遠端用戶授權"), - ("Choose this if the remote account is administrator", "當對面電腦是管理員賬號時選擇該選項"), - ("Transmit the username and password of administrator", "發送管理員賬號的用戶名密碼"), - ("still_click_uac_tip", "依然需要被控端用戶在UAC窗口點擊確認。"), - ("Request Elevation", "請求提權"), - ("wait_accept_uac_tip", "請等待遠端用戶確認UAC對話框。"), - ("Elevate successfully", "提權成功"), + ("Elevation Error", "權限提升失敗"), + ("Ask the remote user for authentication", "請求遠端使用者進行身分驗證"), + ("Choose this if the remote account is administrator", "當遠端使用者帳戶是管理員時,請選擇此選項"), + ("Transmit the username and password of administrator", "發送管理員的使用者名稱和密碼"), + ("still_click_uac_tip", "依然需要遠端使用者在 UAC 視窗點擊確認。"), + ("Request Elevation", "請求權限提升"), + ("wait_accept_uac_tip", "請等待遠端使用者確認 UAC 對話框。"), + ("Elevate successfully", "權限提升成功"), ("uppercase", "大寫字母"), ("lowercase", "小寫字母"), ("digit", "數字"), - ("special character", "特殊字符"), - ("length>=8", "長度不小於8"), + ("special character", "特殊字元"), + ("length>=8", "長度不能小於 8"), ("Weak", "弱"), ("Medium", "中"), ("Strong", "強"), - ("Switch Sides", "反轉訪問方向"), - ("Please confirm if you want to share your desktop?", "請確認是否要讓對方訪問你的桌面?"), + ("Switch Sides", "反轉存取方向"), + ("Please confirm if you want to share your desktop?", "請確認是否要讓對方存取您的桌面?"), ("Display", "顯示"), - ("Default View Style", "默認顯示方式"), - ("Default Scroll Style", "默認滾動方式"), - ("Default Image Quality", "默認圖像質量"), - ("Default Codec", "默認編解碼"), - ("Bitrate", "波特率"), + ("Default View Style", "預設顯示方式"), + ("Default Scroll Style", "預設滾動方式"), + ("Default Image Quality", "預設圖像質量"), + ("Default Codec", "預設編解碼器"), + ("Bitrate", "位元速率"), ("FPS", "幀率"), ("Auto", "自動"), - ("Other Default Options", "其它默認選項"), + ("Other Default Options", "其他預設選項"), ("Voice call", "語音通話"), ("Text chat", "文字聊天"), - ("Stop voice call", "停止語音聊天"), - ("relay_hint_tip", "可能無法直連,可以嘗試中繼連接。 \n另外,如果想直接使用中繼連接,可以在ID後面添加/r,或者在卡片選項裡選擇強制走中繼連接。"), - ("Reconnect", "重連"), - ("Codec", "編解碼"), - ("Resolution", "分辨率"), - ("No transfers in progress", ""), - ("Set one-time password length", ""), - ("idd_driver_tip", ""), - ("confirm_idd_driver_tip", ""), - ("RDP Settings", ""), - ("Sort by", ""), - ("New Connection", ""), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), - ("Your Device", ""), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", ""), - ("Empty Username", ""), - ("Empty Password", ""), - ("Me", ""), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), + ("Stop voice call", "停止語音通話"), + ("relay_hint_tip", "可能無法直接連線,可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,可以在 ID 後面新增/r,或者在卡片選項裡選擇強制走中繼連線。"), + ("Reconnect", "重新連線"), + ("Codec", "編解碼器"), + ("Resolution", "解析度"), + ("No transfers in progress", "沒有正在進行的傳輸"), + ("Set one-time password length", "設定一次性密碼長度"), + ("idd_driver_tip", "安裝虛擬顯示器驅動程式,以便在沒有連接顯示器的情況下啟動虛擬顯示器進行控制。"), + ("confirm_idd_driver_tip", "安裝虛擬顯示器驅動程式的選項已勾選。請注意,測試證書將被安裝以信任虛擬顯示器驅動。測試證書僅會用於信任 RustDesk 的驅動程式。"), + ("RDP Settings", "RDP 設定"), + ("Sort by", "排序方式"), + ("New Connection", "新連線"), + ("Restore", "還原"), + ("Minimize", "最小化"), + ("Maximize", "最大化"), + ("Your Device", "您的裝置"), + ("empty_recent_tip", "空空如也"), + ("empty_favorite_tip", "空空如也"), + ("empty_lan_tip", "空空如也"), + ("empty_address_book_tip", "空空如也"), + ("eg: admin", "例如:admin"), + ("Empty Username", "空使用者帳號"), + ("Empty Password", "空密碼"), + ("Me", "我"), + ("identical_file_tip", "此檔案與對方的檔案一致"), + ("show_monitors_tip", "在工具列中顯示顯示器"), ("View Mode", "瀏覽模式"), + ("login_linux_tip", "需要登入到遠端 Linux 使用者帳戶才能啟用 X 介面"), + ("verify_rustdesk_password_tip", "驗證 RustDesk 密碼"), + ("remember_account_tip", "記住此使用者帳戶"), + ("os_account_desk_tip", "此使用者帳戶將用於登入遠端作業系統並啟用無頭模式的桌面連線"), + ("OS Account", "作業系統使用者帳戶"), + ("another_user_login_title_tip", "另一個使用者已經登入"), + ("another_user_login_text_tip", "斷開連線"), + ("xorg_not_found_title_tip", "未找到 Xorg"), + ("xorg_not_found_text_tip", "請安裝 Xorg"), + ("no_desktop_title_tip", "沒有可用的桌面"), + ("no_desktop_text_tip", "請安裝 GNOME 桌面"), + ("No need to elevate", "不需要提升權限"), + ("System Sound", ""), + ("Default", ""), + ("New RDP", "新的 RDP"), + ("Fingerprint", "指紋"), + ("Copy Fingerprint", "複製指紋"), + ("no fingerprints", "沒有指紋"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 7abfd4188..6f460c1df 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -12,7 +12,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start Service", "Запустити службу"), ("Service is running", "Служба працює"), ("Service is not running", "Служба не запущена"), - ("not_ready_status", "Не готово. Будь ласка, перевірте підключення"), + ("not_ready_status", "Не готово. Будь ласка, перевірте ваше з'єднання"), ("Control Remote Desktop", "Керування віддаленою стільницею"), ("Transfer File", "Передати файл"), ("Connect", "Підключитися"), @@ -32,24 +32,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Import Server Config", "Імпортувати конфігурацію сервера"), ("Export Server Config", "Експортувати конфігурацію сервера"), ("Import server configuration successfully", "Конфігурацію сервера успішно імпортовано"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Конфігурацію сервера успішно експортовано"), ("Invalid server configuration", "Недійсна конфігурація сервера"), ("Clipboard is empty", "Буфер обміну порожній"), ("Stop service", "Зупинити службу"), ("Change ID", "Змінити ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), - ("id_change_tip", "Допускаються тільки символи a-z, A-Z, 0-9 і _ (підкреслення). Перша буква повинна бути a-z, A-Z. Довжина від 6 до 16"), + ("Your new ID", "Ваш новий ID"), + ("length %min% to %max%", "від %min% до %max% символів"), + ("starts with a letter", "починається з літери"), + ("allowed characters", "дозволені символи"), + ("id_change_tip", "Допускаються лише символи a-z, A-Z, 0-9 і _ (підкреслення). Першою повинна бути літера a-z, A-Z. В межах від 6 до 16 символів"), ("Website", "Веб-сайт"), ("About", "Про RustDesk"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), ("Privacy Statement", "Декларація про конфіденційність"), ("Mute", "Вимкнути звук"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Дата збірки"), + ("Version", "Версія"), + ("Home", "Домівка"), ("Audio Input", "Аудіовхід"), ("Enhancements", "Покращення"), ("Hardware Codec", "Апаратний кодек"), @@ -60,7 +60,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("invalid_http", "Повинен починатися з http:// або https://"), ("Invalid IP", "Невірна IP-адреса"), ("Invalid format", "Невірний формат"), - ("server_not_support", "Поки не підтримується сервером"), + ("server_not_support", "Наразі не підтримується сервером"), ("Not available", "Недоступно"), ("Too frequent", "Занадто часто"), ("Cancel", "Скасувати"), @@ -154,16 +154,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("config_screen", "Для віддаленого доступу до стільниці ви повинні надати RustDesk права для \"запису екрану\""), ("Installing ...", "Встановлюється..."), ("Install", "Встановити"), - ("Installation", "Установка"), + ("Installation", "Встановлення"), ("Installation Path", "Шлях встановлення"), ("Create start menu shortcuts", "Створити ярлики меню \"Пуск\""), ("Create desktop icon", "Створити значок на стільниці"), - ("agreement_tip", "Починаючи установку, ви приймаєте умови ліцензійної угоди"), + ("agreement_tip", "Починаючи встановлення, ви приймаєте умови ліцензійної угоди"), ("Accept and Install", "Прийняти та встановити"), ("End-user license agreement", "Ліцензійна угода з кінцевим користувачем"), ("Generating ...", "Генерація..."), - ("Your installation is lower version.", "Ваша установка більш ранньої версії"), - ("not_close_tcp_tip", "Не закривати це вікно під час використання тунелю"), + ("Your installation is lower version.", "У вас встановлена більш рання версія"), + ("not_close_tcp_tip", "Не закривайте це вікно під час використання тунелю"), ("Listening ...", "Очікуємо ..."), ("Remote Host", "Віддалена машина"), ("Remote Port", "Віддалений порт"), @@ -212,16 +212,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid port", "Неправильний порт"), ("Closed manually by the peer", "Закрито вузлом вручну"), ("Enable remote configuration modification", "Дозволити віддалену зміну конфігурації"), - ("Run without install", "Запустити без установки"), - ("Connect via relay", ""), + ("Run without install", "Запустити без встановлення"), + ("Connect via relay", "Підключитися через ретрансляційний сервер"), ("Always connect via relay", "Завжди підключатися через ретрансляційний сервер"), ("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"), ("Login", "Увійти"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Підтвердити"), + ("Remember me", "Запам'ятати мене"), + ("Trust this device", "Довірений пристрій"), + ("Verification code", "Код підтвердження"), + ("verification_tip", "Виявлено новий пристрій, код підтвердження надіслано на зареєстровану email-адресу, введіть код підтвердження для продовження авторизації."), ("Logout", "Вийти"), ("Tags", "Ключові слова"), ("Search ID", "Пошук за ID"), @@ -311,7 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Зберегти фонову службу RustDesk"), ("Ignore Battery Optimizations", "Ігнорувати оптимізацію батареї"), ("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"), - ("Start on Boot", ""), + ("Start on Boot", "Автозапуск"), ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Підключення не дозволено"), ("Legacy mode", "Застарілий режим"), @@ -326,7 +326,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Ви впевнені, що хочете виконати перезапуск?"), ("Restarting Remote Device", "Перезавантаження віддаленого пристрою"), ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення та через деякий час перепідключіться, використовуючи постійний пароль."), - ("Copied", ""), + ("Copied", "Скопійовано"), ("Exit Fullscreen", "Вийти з повноекранного режиму"), ("Fullscreen", "Повноекранний"), ("Mobile Actions", "Мобільні дії"), @@ -348,7 +348,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Security", "Безпека"), ("Theme", "Тема"), ("Dark Theme", "Темна тема"), - ("Light Theme", ""), + ("Light Theme", "Світла тема"), ("Dark", "Темна"), ("Light", "Світла"), ("Follow System", "Як у системі"), @@ -385,7 +385,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_foreground_window_tip", "Поточне вікно віддаленої стільниці потребує розширених прав для роботи, тому наразі неможливо використати мишу та клавіатуру. Ви можете запропонувати віддаленому користувачу згорнути поточне вікно чи натиснути кнопку розширення прав у вікні керування з'єднаннями. Для уникнення цієї проблеми, рекомендується встановити програму на віддаленому пристрої"), ("Disconnected", "Відключено"), ("Other", "Інше"), - ("Confirm before closing multiple tabs", ""), + ("Confirm before closing multiple tabs", "Підтверджувати перед закриттям кількох вкладок"), ("Keyboard Settings", "Налаштування клавіатури"), ("Full Access", "Повний доступ"), ("Screen Share", "Демонстрація екрану"), @@ -411,19 +411,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Дозволено приховати лише якщо сеанс підтверджується постійним паролем"), ("wayland_experiment_tip", "Підтримка Wayland на експериментальній стадії, будь ласка, використовуйте X11, якщо необхідний автоматичний доступ."), ("Right click to select tabs", "Правий клік для вибору вкладки"), - ("Skipped", ""), + ("Skipped", "Пропущено"), ("Add to Address Book", "Додати IP до Адресної книги"), ("Group", "Група"), ("Search", "Пошук"), - ("Closed manually by web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Closed manually by web console", "Закрито вручну з веб-консолі"), + ("Local keyboard type", "Тип локальної клавіатури"), + ("Select local keyboard type", "Оберіть тип локальної клавіатури"), ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), ("config_microphone", ""), ("request_elevation_tip", ""), - ("Wait", ""), + ("Wait", "Зачекайте"), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -432,53 +432,71 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("uppercase", "верхній регістр"), + ("lowercase", "нижній регістр"), + ("digit", "цифра"), + ("special character", "спецсимвол"), + ("length>=8", "довжина>=8"), + ("Weak", "Слабкий"), + ("Medium", "Середній"), + ("Strong", "Сильний"), ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Please confirm if you want to share your desktop?", "Будь ласка, пітвердіть дозвіл на спільне використання стільниці"), + ("Display", "Екран"), + ("Default View Style", "Типовий стиль перегляду"), + ("Default Scroll Style", "Типовий стиль гортання"), + ("Default Image Quality", "Типова якість зображення"), + ("Default Codec", "Типовий кодек"), + ("Bitrate", "Бітрейт"), + ("FPS", "FPS"), + ("Auto", "Авто"), + ("Other Default Options", "Інші типові параметри"), + ("Voice call", "Голосовий дзвінок"), + ("Text chat", "Текстовий чат"), + ("Stop voice call", "Покласти слухавку"), ("relay_hint_tip", ""), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), + ("Reconnect", "Перепідключитися"), + ("Codec", "Кодек"), + ("Resolution", "Роздільна здатність"), ("No transfers in progress", ""), ("Set one-time password length", ""), ("idd_driver_tip", ""), ("confirm_idd_driver_tip", ""), - ("RDP Settings", ""), - ("Sort by", ""), - ("New Connection", ""), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), - ("Your Device", ""), + ("RDP Settings", "Налаштування RDP"), + ("Sort by", "Сортувати за"), + ("New Connection", "Нове з'єднання"), + ("Restore", "Відновити"), + ("Minimize", "Згорнути"), + ("Maximize", "Розгорнути"), + ("Your Device", "Вам пристрій"), ("empty_recent_tip", ""), ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), + ("eg: admin", "напр. admin"), ("Empty Username", ""), ("Empty Password", ""), - ("Me", ""), + ("Me", "Я"), ("identical_file_tip", ""), ("show_monitors_tip", ""), - ("View Mode", ""), + ("View Mode", "Режим перегляду"), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", "Користувач ОС"), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 918106363..75d78fe72 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -480,5 +480,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ("No need to elevate", ""), + ("System Sound", ""), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", ""), + ("no fingerprints", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 5dcd6389c..ec70d1179 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ +mod keyboard; #[cfg(not(any(target_os = "ios")))] /// cbindgen:ignore pub mod platform; -mod keyboard; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service}; #[cfg(not(any(target_os = "ios")))] @@ -20,7 +20,12 @@ pub use self::rendezvous_mediator::*; pub mod common; #[cfg(not(any(target_os = "ios")))] pub mod ipc; -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] +#[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" +)))] pub mod ui; mod version; pub use version::*; @@ -43,6 +48,17 @@ mod license; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod port_forward; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(any(feature = "flutter"))] +pub mod api; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(any(feature = "flutter"))] +pub mod plugins; + +#[cfg(all(feature = "flutter", feature = "plugin_framework"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod plugin; + mod tray; mod ui_cm_interface; @@ -54,7 +70,8 @@ mod hbbs_http; #[cfg(windows)] pub mod clipboard_file; -#[cfg(all(windows, feature = "with_rc"))] -pub mod rc; -#[cfg(target_os = "windows")] -pub mod win_privacy; +#[cfg(windows)] +pub mod privacy_win_mag; + +#[cfg(all(windows, feature = "virtual_display_driver"))] +pub mod virtual_display_manager; diff --git a/src/license.rs b/src/license.rs index 8875d2b64..7d4270052 100644 --- a/src/license.rs +++ b/src/license.rs @@ -37,6 +37,11 @@ pub fn get_license_from_string(s: &str) -> ResultType { s }; if s.contains("host=") { + let s = if s.contains("#") { + &s[0..s.find("#").unwrap_or(s.len())] + } else { + s + }; let strs: Vec<&str> = s.split("host=").collect(); if strs.len() == 2 { let strs2: Vec<&str> = strs[1].split(",key=").collect(); diff --git a/src/main.rs b/src/main.rs index 3759f6056..24e708c52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ - #![cfg_attr( - all(not(debug_assertions), target_os = "windows"), - windows_subsystem = "windows" - )] +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] use librustdesk::*; diff --git a/src/naming.rs b/src/naming.rs index 53e675d92..7a8d0cecc 100644 --- a/src/naming.rs +++ b/src/naming.rs @@ -4,26 +4,13 @@ use hbb_common::ResultType; use license::*; fn gen_name(lic: &License) -> ResultType { - let tmp = serde_json::to_vec::(lic)?; - let tmp = URL_SAFE_NO_PAD.encode(&tmp); - let tmp: String = tmp.chars().rev().collect(); - Ok(tmp) + let tmp = URL_SAFE_NO_PAD.encode(&serde_json::to_vec(lic)?); + Ok(tmp.chars().rev().collect()) } fn main() { - let mut args = Vec::new(); - let mut i = 0; - for arg in std::env::args() { - if i > 0 { - args.push(arg); - } - i += 1; - } - let api = if args.len() < 3 { - "".to_owned() - } else { - args[2].clone() - }; + let args: Vec<_> = std::env::args().skip(1).collect(); + let api = args.get(2).cloned().unwrap_or_default(); if args.len() >= 2 { println!( "rustdesk-licensed-{}.exe", diff --git a/src/platform/linux.rs b/src/platform/linux.rs index bde2a5c58..33ebcc82c 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,12 +1,12 @@ use super::{CursorData, ResultType}; +use desktop::Desktop; pub use hbb_common::platform::linux::*; use hbb_common::{ - allow_err, - anyhow::anyhow, - bail, + allow_err, bail, libc::{c_char, c_int, c_long, c_void}, log, message_proto::Resolution, + regex::{Captures, Regex}, }; use std::{ cell::RefCell, @@ -18,7 +18,6 @@ use std::{ }, time::{Duration, Instant}, }; -use xrandr_parser::Parser; type Xdo = *const c_void; @@ -66,6 +65,11 @@ pub struct xcb_xfixes_get_cursor_image { pub pixels: *const c_long, } +#[inline] +fn sleep_millis(millis: u64) { + std::thread::sleep(Duration::from_millis(millis)); +} + pub fn get_cursor_pos() -> Option<(i32, i32)> { let mut res = None; XDO.with(|xdo| { @@ -192,7 +196,7 @@ fn start_server(user: Option<(String, String)>, server: &mut Option) { fn stop_server(server: &mut Option) { if let Some(mut ps) = server.take() { allow_err!(ps.kill()); - std::thread::sleep(Duration::from_millis(30)); + sleep_millis(30); match ps.try_wait() { Ok(Some(_status)) => {} Ok(None) => { @@ -203,82 +207,80 @@ fn stop_server(server: &mut Option) { } } -fn set_x11_env(uid: &str) { - log::info!("uid of seat0: {}", uid); - let gdm = format!("/run/user/{}/gdm/Xauthority", uid); - let mut auth = get_env_tries("XAUTHORITY", uid, 10); - // auth is another user's when uid = 0, https://github.com/rustdesk/rustdesk/issues/2468 - if auth.is_empty() || uid == "0" { - auth = if Path::new(&gdm).exists() { - gdm - } else { - let username = get_active_username(); - if username == "root" { - format!("/{}/.Xauthority", username) - } else { - let tmp = format!("/home/{}/.Xauthority", username); - if Path::new(&tmp).exists() { - tmp - } else { - format!("/var/lib/{}/.Xauthority", username) - } - } - }; +fn set_x11_env(desktop: &Desktop) { + log::info!("DISPLAY: {}", desktop.display); + log::info!("XAUTHORITY: {}", desktop.xauth); + if !desktop.display.is_empty() { + std::env::set_var("DISPLAY", &desktop.display); } - let mut d = get_env("DISPLAY", uid); - if d.is_empty() { - d = get_display(); + if !desktop.xauth.is_empty() { + std::env::set_var("XAUTHORITY", &desktop.xauth); } - if d.is_empty() { - d = ":0".to_owned(); - } - d = d.replace(&whoami::hostname(), "").replace("localhost", ""); - log::info!("DISPLAY: {}", d); - log::info!("XAUTHORITY: {}", auth); - std::env::set_var("XAUTHORITY", auth); - std::env::set_var("DISPLAY", d); } +#[inline] fn stop_rustdesk_servers() { - let _ = run_cmds(format!( + let _ = run_cmds(&format!( r##"ps -ef | grep -E 'rustdesk +--server' | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, )); } +#[inline] +fn stop_subprocess() { + let _ = run_cmds(&format!( + r##"ps -ef | grep '/etc/rustdesk/xorg.conf' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, + )); + let _ = run_cmds(&format!( + r##"ps -ef | grep -E 'rustdesk +--cm-no-ui' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, + )); +} + fn should_start_server( try_x11: bool, uid: &mut String, - cur_uid: String, + desktop: &Desktop, cm0: &mut bool, last_restart: &mut Instant, server: &mut Option, ) -> bool { let cm = get_cm(); let mut start_new = false; - if cur_uid != *uid && !cur_uid.is_empty() { - *uid = cur_uid; + let mut should_kill = false; + + if desktop.is_headless() { + if !uid.is_empty() { + // From having a monitor to not having a monitor. + *uid = "".to_owned(); + should_kill = true; + } + } else if desktop.uid != *uid && !desktop.uid.is_empty() { + *uid = desktop.uid.clone(); if try_x11 { - set_x11_env(&uid); + set_x11_env(&desktop); } - if let Some(ps) = server.as_mut() { - allow_err!(ps.kill()); - std::thread::sleep(Duration::from_millis(30)); - *last_restart = Instant::now(); - } - } else if !cm + should_kill = true; + } + + if !should_kill + && !cm && ((*cm0 && last_restart.elapsed().as_secs() > 60) || last_restart.elapsed().as_secs() > 3600) { // restart server if new connections all closed, or every one hour, // as a workaround to resolve "SpotUdp" (dns resolve) // and x server get displays failure issue + should_kill = true; + log::info!("restart server"); + } + + if should_kill { if let Some(ps) = server.as_mut() { allow_err!(ps.kill()); - std::thread::sleep(Duration::from_millis(30)); + sleep_millis(30); *last_restart = Instant::now(); - log::info!("restart server"); } } + if let Some(ps) = server.as_mut() { match ps.try_wait() { Ok(Some(_)) => { @@ -298,15 +300,18 @@ fn should_start_server( // stop_rustdesk_servers() is just a temp solution here. fn force_stop_server() { stop_rustdesk_servers(); - std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); + sleep_millis(super::SERVICE_INTERVAL); } pub fn start_os_service() { stop_rustdesk_servers(); + stop_subprocess(); start_uinput_service(); let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); + let mut desktop = Desktop::default(); + let mut sid = "".to_owned(); let mut uid = "".to_owned(); let mut server: Option = None; let mut user_server: Option = None; @@ -319,62 +324,63 @@ pub fn start_os_service() { let mut cm0 = false; let mut last_restart = Instant::now(); while running.load(Ordering::SeqCst) { - let (cur_uid, cur_user) = get_active_user_id_name(); + desktop.refresh(); - // for fixing https://github.com/rustdesk/rustdesk/issues/3129 to avoid too much dbus calling, - // though duplicate logic here with should_start_server - if !(cur_uid != *uid && !cur_uid.is_empty()) { - let cm = get_cm(); - if !(!cm - && ((cm0 && last_restart.elapsed().as_secs() > 60) - || last_restart.elapsed().as_secs() > 3600)) - { - std::thread::sleep(Duration::from_millis(500)); - continue; - } - } - - let is_wayland = current_is_wayland(); - - if cur_user == "root" || !is_wayland { + // Duplicate logic here with should_start_server + // Login wayland will try to start a headless --server. + if desktop.username == "root" || !desktop.is_wayland() || desktop.is_login_wayland() { // try kill subprocess "--server" stop_server(&mut user_server); // try start subprocess "--server" if should_start_server( true, &mut uid, - cur_uid, + &desktop, &mut cm0, &mut last_restart, &mut server, ) { + stop_subprocess(); force_stop_server(); start_server(None, &mut server); } - } else if cur_user != "" { - if cur_user != "gdm" { - // try kill subprocess "--server" - stop_server(&mut server); + } else if desktop.username != "" { + // try kill subprocess "--server" + stop_server(&mut server); - // try start subprocess "--server" - if should_start_server( - false, - &mut uid, - cur_uid.clone(), - &mut cm0, - &mut last_restart, + // try start subprocess "--server" + if should_start_server( + false, + &mut uid, + &desktop, + &mut cm0, + &mut last_restart, + &mut user_server, + ) { + stop_subprocess(); + force_stop_server(); + start_server( + Some((desktop.uid.clone(), desktop.username.clone())), &mut user_server, - ) { - force_stop_server(); - start_server(Some((cur_uid, cur_user)), &mut user_server); - } + ); } } else { force_stop_server(); stop_server(&mut user_server); stop_server(&mut server); } - std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); + + let keeps_headless = sid.is_empty() && desktop.is_headless(); + let keeps_session = sid == desktop.sid; + if keeps_headless || keeps_session { + // for fixing https://github.com/rustdesk/rustdesk/issues/3129 to avoid too much dbus calling, + sleep_millis(500); + } else { + sleep_millis(super::SERVICE_INTERVAL); + } + if !desktop.is_headless() { + sid = desktop.sid.clone(); + } } if let Some(ps) = user_server.take().as_mut() { @@ -386,13 +392,15 @@ pub fn start_os_service() { log::info!("Exit"); } +#[inline] pub fn get_active_user_id_name() -> (String, String) { - let vec_id_name = get_values_of_seat0([1, 2].to_vec()); + let vec_id_name = get_values_of_seat0(&[1, 2]); (vec_id_name[0].clone(), vec_id_name[1].clone()) } +#[inline] pub fn get_active_userid() -> String { - get_values_of_seat0([1].to_vec())[0].clone() + get_values_of_seat0(&[1])[0].clone() } fn get_cm() -> bool { @@ -411,45 +419,6 @@ fn get_cm() -> bool { false } -fn get_display() -> String { - let user = get_active_username(); - log::debug!("w {}", &user); - if let Ok(output) = Command::new("w").arg(&user).output() { - for line in String::from_utf8_lossy(&output.stdout).lines() { - log::debug!(" {}", line); - let mut iter = line.split_whitespace(); - let b = iter.nth(2); - if let Some(b) = b { - if b.starts_with(":") { - return b.to_owned(); - } - } - } - } - // above not work for gdm user - log::debug!("ls -l /tmp/.X11-unix/"); - let mut last = "".to_owned(); - if let Ok(output) = Command::new("ls") - .args(vec!["-l", "/tmp/.X11-unix/"]) - .output() - { - for line in String::from_utf8_lossy(&output.stdout).lines() { - log::debug!(" {}", line); - let mut iter = line.split_whitespace(); - let user_field = iter.nth(2); - if let Some(x) = iter.last() { - if x.starts_with("X") { - last = x.replace("X", ":").to_owned(); - if user_field == Some(&user) { - return last; - } - } - } - } - } - last -} - pub fn is_login_wayland() -> bool { if let Ok(contents) = std::fs::read_to_string("/etc/gdm3/custom.conf") { contents.contains("#WaylandEnable=false") || contents.contains("WaylandEnable=true") @@ -460,9 +429,9 @@ pub fn is_login_wayland() -> bool { } } +#[inline] pub fn current_is_wayland() -> bool { - let dtype = get_display_server(); - return "wayland" == dtype && unsafe { UNMODIFIED }; + return is_desktop_wayland() && unsafe { UNMODIFIED }; } // to-do: test the other display manager @@ -475,8 +444,9 @@ fn _get_display_manager() -> String { "gdm3".to_owned() } +#[inline] pub fn get_active_username() -> String { - get_values_of_seat0([2].to_vec())[0].clone() + get_values_of_seat0(&[2])[0].clone() } pub fn get_active_user_home() -> Option { @@ -490,6 +460,14 @@ pub fn get_active_user_home() -> Option { None } +pub fn get_env_var(k: &str) -> String { + match std::env::var(k) { + Ok(v) => v, + Err(_e) => "".to_owned(), + } +} + +// Headless is enabled, always return true. pub fn is_prelogin() -> bool { let n = get_active_userid().len(); n < 4 && n > 1 @@ -500,7 +478,7 @@ pub fn is_root() -> bool { } fn is_opensuse() -> bool { - if let Ok(res) = run_cmds("cat /etc/os-release | grep opensuse".to_owned()) { + if let Ok(res) = run_cmds("cat /etc/os-release | grep opensuse") { if !res.is_empty() { return true; } @@ -514,6 +492,9 @@ pub fn run_as_user(arg: Vec<&str>, user: Option<(String, String)>) -> ResultType None => get_active_user_id_name(), }; let cmd = std::env::current_exe()?; + if uid.is_empty() { + bail!("No valid uid"); + } let xdg = &format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str; let mut args = vec![xdg, "-u", &username, cmd.to_str().unwrap_or("")]; args.append(&mut arg.clone()); @@ -599,21 +580,31 @@ pub fn is_installed() -> bool { true } -fn get_env_tries(name: &str, uid: &str, n: usize) -> String { +pub(super) fn get_env_tries(name: &str, uid: &str, process: &str, n: usize) -> String { for _ in 0..n { - let x = get_env(name, uid); + let x = get_env(name, uid, process); if !x.is_empty() { return x; } - std::thread::sleep(Duration::from_millis(300)); + sleep_millis(300); } "".to_owned() } -fn get_env(name: &str, uid: &str) -> String { - let cmd = format!("ps -u {} -o pid= | xargs -I__ cat /proc/__/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", uid, name, name); - log::debug!("Run: {}", &cmd); - if let Ok(x) = run_cmds(cmd) { +#[inline] +fn get_env(name: &str, uid: &str, process: &str) -> String { + let cmd = format!("ps -u {} -f | grep '{}' | grep -v 'grep' | tail -1 | awk '{{print $2}}' | xargs -I__ cat /proc/__/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", uid, process, name, name); + if let Ok(x) = run_cmds(&cmd) { + x.trim_end().to_string() + } else { + "".to_owned() + } +} + +#[inline] +fn get_env_from_pid(name: &str, pid: &str) -> String { + let cmd = format!("cat /proc/{}/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", pid, name, name); + if let Ok(x) = run_cmds(&cmd) { x.trim_end().to_string() } else { "".to_owned() @@ -671,44 +662,99 @@ pub fn get_double_click_time() -> u32 { } } +#[inline] +fn get_width_height_from_captures<'t>(caps: &Captures<'t>) -> Option<(i32, i32)> { + match (caps.name("width"), caps.name("height")) { + (Some(width), Some(height)) => { + match ( + width.as_str().parse::(), + height.as_str().parse::(), + ) { + (Ok(width), Ok(height)) => { + return Some((width, height)); + } + _ => {} + } + } + _ => {} + } + None +} + +#[inline] +fn get_xrandr_conn_pat(name: &str) -> String { + format!( + r"{}\s+connected.+?(?P\d+)x(?P\d+)\+(?P\d+)\+(?P\d+).*?\n", + name + ) +} + pub fn resolutions(name: &str) -> Vec { + let resolutions_pat = r"(?P(\s*\d+x\d+\s+\d+.*\n)+)"; + let connected_pat = get_xrandr_conn_pat(name); let mut v = vec![]; - let mut parser = Parser::new(); - if parser.parse().is_ok() { - if let Ok(connector) = parser.get_connector(name) { - if let Ok(resolutions) = &connector.available_resolutions() { - for r in resolutions { - if let Ok(width) = r.horizontal.parse::() { - if let Ok(height) = r.vertical.parse::() { - let resolution = Resolution { - width, - height, - ..Default::default() - }; - if !v.contains(&resolution) { - v.push(resolution); + if let Ok(re) = Regex::new(&format!("{}{}", connected_pat, resolutions_pat)) { + match run_cmds("xrandr --query | tr -s ' '") { + Ok(xrandr_output) => { + // There'are different kinds of xrandr output. + /* + 1. + Screen 0: minimum 320 x 175, current 1920 x 1080, maximum 1920 x 1080 + default connected 1920x1080+0+0 0mm x 0mm + 1920x1080 10.00* + 1280x720 25.00 + 1680x1050 60.00 + Virtual2 disconnected (normal left inverted right x axis y axis) + Virtual3 disconnected (normal left inverted right x axis y axis) + + XWAYLAND0 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm + Virtual1 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm + HDMI-0 connected (normal left inverted right x axis y axis) + + rdp0 connected primary 1920x1080+0+0 0mm x 0mm + */ + if let Some(caps) = re.captures(&xrandr_output) { + if let Some(resolutions) = caps.name("resolutions") { + let resolution_pat = + r"\s*(?P\d+)x(?P\d+)\s+(?P(\d+\.\d+[* ]*)+)\s*\n"; + let resolution_re = Regex::new(&format!(r"{}", resolution_pat)).unwrap(); + for resolution_caps in resolution_re.captures_iter(resolutions.as_str()) { + if let Some((width, height)) = + get_width_height_from_captures(&resolution_caps) + { + let resolution = Resolution { + width, + height, + ..Default::default() + }; + if !v.contains(&resolution) { + v.push(resolution); + } } } } } } + Err(e) => log::error!("Failed to run xrandr query, {}", e), } } + v } pub fn current_resolution(name: &str) -> ResultType { - let mut parser = Parser::new(); - parser.parse().map_err(|e| anyhow!(e))?; - let connector = parser.get_connector(name).map_err(|e| anyhow!(e))?; - let r = connector.current_resolution(); - let width = r.horizontal.parse::()?; - let height = r.vertical.parse::()?; - Ok(Resolution { - width, - height, - ..Default::default() - }) + let xrandr_output = run_cmds("xrandr --query | tr -s ' '")?; + let re = Regex::new(&get_xrandr_conn_pat(name))?; + if let Some(caps) = re.captures(&xrandr_output) { + if let Some((width, height)) = get_width_height_from_captures(&caps) { + return Ok(Resolution { + width, + height, + ..Default::default() + }); + } + } + bail!("Failed to find current resolution for {}", name); } pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { @@ -722,3 +768,190 @@ pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType< .spawn()?; Ok(()) } + +mod desktop { + use super::*; + + pub const XFCE4_PANEL: &str = "xfce4-panel"; + pub const GNOME_SESSION_BINARY: &str = "gnome-session-binary"; + + #[derive(Debug, Clone, Default)] + pub struct Desktop { + pub sid: String, + pub username: String, + pub uid: String, + pub protocal: String, + pub display: String, + pub xauth: String, + pub is_rustdesk_subprocess: bool, + } + + impl Desktop { + #[inline] + pub fn is_wayland(&self) -> bool { + self.protocal == DISPLAY_SERVER_WAYLAND + } + + #[inline] + pub fn is_login_wayland(&self) -> bool { + super::is_gdm_user(&self.username) && self.protocal == DISPLAY_SERVER_WAYLAND + } + + #[inline] + pub fn is_headless(&self) -> bool { + self.sid.is_empty() || self.is_rustdesk_subprocess + } + + fn get_display(&mut self) { + self.display = get_env_tries("DISPLAY", &self.uid, GNOME_SESSION_BINARY, 10); + if self.display.is_empty() { + self.display = get_env_tries("DISPLAY", &self.uid, XFCE4_PANEL, 10); + } + if self.display.is_empty() { + self.display = Self::get_display_by_user(&self.username); + } + if self.display.is_empty() { + self.display = ":0".to_owned(); + } + self.display = self + .display + .replace(&whoami::hostname(), "") + .replace("localhost", ""); + } + + fn get_xauth_from_xorg(&mut self) { + if let Ok(output) = run_cmds(&format!( + "ps -u {} -f | grep 'Xorg' | grep -v 'grep'", + &self.uid + )) { + for line in output.lines() { + let mut auth_found = false; + for v in line.split_whitespace() { + if v == "-auth" { + auth_found = true; + } else if auth_found { + if std::path::Path::new(v).is_absolute() { + self.xauth = v.to_string(); + } else { + if let Some(pid) = line.split_whitespace().nth(1) { + let home_dir = get_env_from_pid("HOME", pid); + if home_dir.is_empty() { + self.xauth = format!("/home/{}/{}", self.username, v); + } else { + self.xauth = format!("{}/{}", home_dir, v); + } + } else { + // unreachable! + } + } + return; + } + } + } + } + } + + fn get_xauth(&mut self) { + self.xauth = get_env_tries("XAUTHORITY", &self.uid, GNOME_SESSION_BINARY, 10); + if self.xauth.is_empty() { + get_env_tries("XAUTHORITY", &self.uid, XFCE4_PANEL, 10); + } + if self.xauth.is_empty() { + self.get_xauth_from_xorg(); + } + + let gdm = format!("/run/user/{}/gdm/Xauthority", self.uid); + if self.xauth.is_empty() { + self.xauth = if std::path::Path::new(&gdm).exists() { + gdm + } else { + let username = &self.username; + if username == "root" { + format!("/{}/.Xauthority", username) + } else { + let tmp = format!("/home/{}/.Xauthority", username); + if std::path::Path::new(&tmp).exists() { + tmp + } else { + format!("/var/lib/{}/.Xauthority", username) + } + } + }; + } + } + + fn get_display_by_user(user: &str) -> String { + // log::debug!("w {}", &user); + if let Ok(output) = std::process::Command::new("w").arg(&user).output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + let mut iter = line.split_whitespace(); + let b = iter.nth(2); + if let Some(b) = b { + if b.starts_with(":") { + return b.to_owned(); + } + } + } + } + // above not work for gdm user + //log::debug!("ls -l /tmp/.X11-unix/"); + let mut last = "".to_owned(); + if let Ok(output) = std::process::Command::new("ls") + .args(vec!["-l", "/tmp/.X11-unix/"]) + .output() + { + for line in String::from_utf8_lossy(&output.stdout).lines() { + let mut iter = line.split_whitespace(); + let user_field = iter.nth(2); + if let Some(x) = iter.last() { + if x.starts_with("X") { + last = x.replace("X", ":").to_owned(); + if user_field == Some(&user) { + return last; + } + } + } + } + } + last + } + + fn set_is_subprocess(&mut self) { + self.is_rustdesk_subprocess = false; + let cmd = "ps -ef | grep 'rustdesk/xorg.conf' | grep -v grep | wc -l"; + if let Ok(res) = run_cmds(cmd) { + if res.trim() != "0" { + self.is_rustdesk_subprocess = true; + } + } + } + + pub fn refresh(&mut self) { + if !self.sid.is_empty() && is_active(&self.sid) { + return; + } + + let seat0_values = get_values_of_seat0(&[0, 1, 2]); + if seat0_values[0].is_empty() { + *self = Self::default(); + self.is_rustdesk_subprocess = false; + return; + } + + self.sid = seat0_values[0].clone(); + self.uid = seat0_values[1].clone(); + self.username = seat0_values[2].clone(); + self.protocal = get_display_server_of_session(&self.sid).into(); + if self.is_login_wayland() { + self.display = "".to_owned(); + self.xauth = "".to_owned(); + self.is_rustdesk_subprocess = false; + return; + } + + self.get_display(); + self.get_xauth(); + self.set_is_subprocess(); + } + } +} diff --git a/src/platform/linux_desktop_manager.rs b/src/platform/linux_desktop_manager.rs new file mode 100644 index 000000000..fe60964cc --- /dev/null +++ b/src/platform/linux_desktop_manager.rs @@ -0,0 +1,733 @@ +use super::{linux::*, ResultType}; +use crate::client::{ + LOGIN_MSG_DESKTOP_NO_DESKTOP, LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER, + LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, + LOGIN_MSG_DESKTOP_XSESSION_FAILED, +}; +use hbb_common::{allow_err, bail, log, rand::prelude::*, tokio::time}; +use pam; +use std::{ + collections::HashMap, + os::unix::process::CommandExt, + path::Path, + process::{Child, Command}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{sync_channel, SyncSender}, + Arc, Mutex, + }, + time::{Duration, Instant}, +}; +use users::{get_user_by_name, os::unix::UserExt, User}; + +lazy_static::lazy_static! { + static ref DESKTOP_RUNNING: Arc = Arc::new(AtomicBool::new(false)); + static ref DESKTOP_MANAGER: Arc>> = Arc::new(Mutex::new(None)); +} + +#[derive(Debug)] +struct DesktopManager { + seat0_username: String, + seat0_display_server: String, + child_username: String, + child_exit: Arc, + is_child_running: Arc, +} + +fn check_desktop_manager() { + let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); + if let Some(desktop_manager) = &mut (*desktop_manager) { + if desktop_manager.is_child_running.load(Ordering::SeqCst) { + return; + } + desktop_manager.child_exit.store(true, Ordering::SeqCst); + } +} + +// --server process +pub fn start_xdesktop() { + std::thread::spawn(|| { + *DESKTOP_MANAGER.lock().unwrap() = Some(DesktopManager::new()); + + let interval = time::Duration::from_millis(super::SERVICE_INTERVAL); + DESKTOP_RUNNING.store(true, Ordering::SeqCst); + while DESKTOP_RUNNING.load(Ordering::SeqCst) { + check_desktop_manager(); + std::thread::sleep(interval); + } + log::info!("xdesktop child thread exit"); + }); +} + +pub fn stop_xdesktop() { + DESKTOP_RUNNING.store(false, Ordering::SeqCst); + *DESKTOP_MANAGER.lock().unwrap() = None; +} + +fn detect_headless() -> Option<&'static str> { + match run_cmds(&format!("which {}", DesktopManager::get_xorg())) { + Ok(output) => { + if output.trim().is_empty() { + return Some(LOGIN_MSG_DESKTOP_XORG_NOT_FOUND); + } + } + _ => { + return Some(LOGIN_MSG_DESKTOP_XORG_NOT_FOUND); + } + } + + match run_cmds("ls /usr/share/xsessions/") { + Ok(output) => { + if output.trim().is_empty() { + return Some(LOGIN_MSG_DESKTOP_NO_DESKTOP); + } + } + _ => { + return Some(LOGIN_MSG_DESKTOP_NO_DESKTOP); + } + } + + None +} + +pub fn try_start_desktop(_username: &str, _passsword: &str) -> String { + if _username.is_empty() { + let username = get_username(); + if username.is_empty() { + if let Some(msg) = detect_headless() { + msg + } else { + LOGIN_MSG_DESKTOP_SESSION_NOT_READY + } + } else { + "" + } + .to_owned() + } else { + let username = get_username(); + if username == _username { + // No need to verify password here. + return "".to_owned(); + } + + if let Some(msg) = detect_headless() { + return msg.to_owned(); + } + + match try_start_x_session(_username, _passsword) { + Ok((username, x11_ready)) => { + if x11_ready { + if _username != username { + LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER.to_owned() + } else { + "".to_owned() + } + } else { + LOGIN_MSG_DESKTOP_SESSION_NOT_READY.to_owned() + } + } + Err(e) => { + log::error!("Failed to start xsession {}", e); + LOGIN_MSG_DESKTOP_XSESSION_FAILED.to_owned() + } + } + } +} + +fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bool)> { + let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); + if let Some(desktop_manager) = &mut (*desktop_manager) { + if let Some(seat0_username) = desktop_manager.get_supported_display_seat0_username() { + return Ok((seat0_username, true)); + } + + let _ = desktop_manager.try_start_x_session(username, password)?; + log::debug!( + "try_start_x_session, username: {}, {:?}", + &username, + &desktop_manager + ); + Ok(( + desktop_manager.child_username.clone(), + desktop_manager.is_running(), + )) + } else { + bail!(crate::client::LOGIN_MSG_DESKTOP_NOT_INITED); + } +} + +#[inline] +pub fn is_headless() -> bool { + DESKTOP_MANAGER + .lock() + .unwrap() + .as_ref() + .map_or(false, |manager| { + manager.get_supported_display_seat0_username().is_none() + }) +} + +pub fn get_username() -> String { + match &*DESKTOP_MANAGER.lock().unwrap() { + Some(manager) => { + if let Some(seat0_username) = manager.get_supported_display_seat0_username() { + seat0_username + } else { + if manager.is_running() && !manager.child_username.is_empty() { + manager.child_username.clone() + } else { + "".to_owned() + } + } + } + None => "".to_owned(), + } +} + +impl Drop for DesktopManager { + fn drop(&mut self) { + self.stop_children(); + } +} + +impl DesktopManager { + fn fatal_exit() { + std::process::exit(0); + } + + pub fn new() -> Self { + let mut seat0_username = "".to_owned(); + let mut seat0_display_server = "".to_owned(); + let seat0_values = get_values_of_seat0(&[0, 2]); + if !seat0_values[0].is_empty() { + seat0_username = seat0_values[1].clone(); + seat0_display_server = get_display_server_of_session(&seat0_values[0]); + } + Self { + seat0_username, + seat0_display_server, + child_username: "".to_owned(), + child_exit: Arc::new(AtomicBool::new(true)), + is_child_running: Arc::new(AtomicBool::new(false)), + } + } + + fn get_supported_display_seat0_username(&self) -> Option { + if is_gdm_user(&self.seat0_username) && self.seat0_display_server == DISPLAY_SERVER_WAYLAND + { + None + } else if self.seat0_username.is_empty() { + None + } else { + Some(self.seat0_username.clone()) + } + } + + #[inline] + fn get_xauth() -> String { + let xauth = get_env_var("XAUTHORITY"); + if xauth.is_empty() { + "/tmp/.Xauthority".to_owned() + } else { + xauth + } + } + + #[inline] + fn is_running(&self) -> bool { + self.is_child_running.load(Ordering::SeqCst) + } + + fn try_start_x_session(&mut self, username: &str, password: &str) -> ResultType<()> { + match get_user_by_name(username) { + Some(userinfo) => { + let mut client = pam::Client::with_password(pam_get_service_name())?; + client + .conversation_mut() + .set_credentials(username, password); + match client.authenticate() { + Ok(_) => { + if self.is_running() { + return Ok(()); + } + + match self.start_x_session(&userinfo, username, password) { + Ok(_) => { + log::info!("Succeeded to start x11"); + self.child_username = username.to_string(); + Ok(()) + } + Err(e) => { + bail!("failed to start x session, {}", e); + } + } + } + Err(e) => { + bail!("failed to check user pass for {}, {}", username, e); + } + } + } + None => { + bail!("failed to get userinfo of {}", username); + } + } + } + + // The logic mainly fron https://github.com/neutrinolabs/xrdp/blob/34fe9b60ebaea59e8814bbc3ca5383cabaa1b869/sesman/session.c#L334. + fn get_avail_display() -> ResultType { + let display_range = 0..51; + for i in display_range.clone() { + if Self::is_x_server_running(i) { + continue; + } + return Ok(i); + } + bail!("No avaliable display found in range {:?}", display_range) + } + + #[inline] + fn is_x_server_running(display: u32) -> bool { + Path::new(&format!("/tmp/.X11-unix/X{}", display)).exists() + || Path::new(&format!("/tmp/.X{}-lock", display)).exists() + } + + fn start_x_session( + &mut self, + userinfo: &User, + username: &str, + password: &str, + ) -> ResultType<()> { + self.stop_children(); + + let display_num = Self::get_avail_display()?; + // "xServer_ip:display_num.screen_num" + + let uid = userinfo.uid(); + let gid = userinfo.primary_group_id(); + let envs = HashMap::from([ + ("SHELL", userinfo.shell().to_string_lossy().to_string()), + ("PATH", "/sbin:/bin:/usr/bin:/usr/local/bin".to_owned()), + ("USER", username.to_string()), + ("UID", userinfo.uid().to_string()), + ("HOME", userinfo.home_dir().to_string_lossy().to_string()), + ( + "XDG_RUNTIME_DIR", + format!("/run/user/{}", userinfo.uid().to_string()), + ), + // ("DISPLAY", self.display.clone()), + // ("XAUTHORITY", self.xauth.clone()), + // (ENV_DESKTOP_PROTOCAL, XProtocal::X11.to_string()), + ]); + self.child_exit.store(false, Ordering::SeqCst); + let is_child_running = self.is_child_running.clone(); + + let (tx_res, rx_res) = sync_channel(1); + let password = password.to_string(); + let username = username.to_string(); + // start x11 + std::thread::spawn(move || { + match Self::start_x_session_thread( + tx_res.clone(), + is_child_running, + uid, + gid, + display_num, + username, + password, + envs, + ) { + Ok(_) => {} + Err(e) => { + log::error!("Failed to start x session thread"); + allow_err!(tx_res.send(format!("Failed to start x session thread, {}", e))); + } + } + }); + + // wait x11 + match rx_res.recv_timeout(Duration::from_millis(10_000)) { + Ok(res) => { + if res == "" { + Ok(()) + } else { + bail!(res) + } + } + Err(e) => { + bail!("Failed to recv x11 result {}", e) + } + } + } + + #[inline] + fn display_from_num(num: u32) -> String { + format!(":{num}") + } + + fn start_x_session_thread( + tx_res: SyncSender, + is_child_running: Arc, + uid: u32, + gid: u32, + display_num: u32, + username: String, + password: String, + envs: HashMap<&str, String>, + ) -> ResultType<()> { + let mut client = pam::Client::with_password(pam_get_service_name())?; + client + .conversation_mut() + .set_credentials(&username, &password); + client.authenticate()?; + + client.set_item(pam::PamItemType::TTY, &Self::display_from_num(display_num))?; + client.open_session()?; + + // fixme: FreeBSD kernel needs to login here. + // see: https://github.com/neutrinolabs/xrdp/blob/a64573b596b5fb07ca3a51590c5308d621f7214e/sesman/session.c#L556 + + let (child_xorg, child_wm) = Self::start_x11(uid, gid, username, display_num, &envs)?; + is_child_running.store(true, Ordering::SeqCst); + + log::info!("Start xorg and wm done, notify and wait xtop x11"); + allow_err!(tx_res.send("".to_owned())); + + Self::wait_stop_x11(child_xorg, child_wm); + log::info!("Wait x11 stop done"); + Ok(()) + } + + fn wait_xorg_exit(child_xorg: &mut Child) -> ResultType { + if let Ok(_) = child_xorg.kill() { + for _ in 0..3 { + match child_xorg.try_wait() { + Ok(Some(status)) => return Ok(format!("Xorg exit with {}", status)), + Ok(None) => {} + Err(e) => { + // fatal error + log::error!("Failed to wait xorg process, {}", e); + bail!("Failed to wait xorg process, {}", e) + } + } + std::thread::sleep(std::time::Duration::from_millis(1_000)); + } + log::error!("Failed to wait xorg process, not exit"); + bail!("Failed to wait xorg process, not exit") + } else { + Ok("Xorg is already exited".to_owned()) + } + } + + fn add_xauth_cookie( + file: &str, + display: &str, + uid: u32, + gid: u32, + envs: &HashMap<&str, String>, + ) -> ResultType<()> { + let randstr = (0..16) + .map(|_| format!("{:02x}", random::())) + .collect::(); + let output = Command::new("xauth") + .uid(uid) + .gid(gid) + .envs(envs) + .args(vec!["-q", "-f", file, "add", display, ".", &randstr]) + .output()?; + // xauth run success, even the following error occurs. + // Ok(Output { status: ExitStatus(unix_wait_status(0)), stdout: "", stderr: "xauth: file .Xauthority does not exist\n" }) + let errmsg = String::from_utf8_lossy(&output.stderr).to_string(); + if !errmsg.is_empty() { + if !errmsg.contains("does not exist") { + bail!("Failed to launch xauth, {}", errmsg) + } + } + Ok(()) + } + + fn wait_x_server_running(pid: u32, display_num: u32, max_wait_secs: u64) -> ResultType<()> { + let wait_begin = Instant::now(); + loop { + if run_cmds(&format!("ls /proc/{}", pid))?.is_empty() { + bail!("X server exit"); + } + + if Self::is_x_server_running(display_num) { + return Ok(()); + } + if wait_begin.elapsed().as_secs() > max_wait_secs { + bail!("Failed to wait xserver after {} seconds", max_wait_secs); + } + std::thread::sleep(Duration::from_millis(300)); + } + } + + fn start_x11( + uid: u32, + gid: u32, + username: String, + display_num: u32, + envs: &HashMap<&str, String>, + ) -> ResultType<(Child, Child)> { + log::debug!("envs of user {}: {:?}", &username, &envs); + + let xauth = Self::get_xauth(); + let display = Self::display_from_num(display_num); + + Self::add_xauth_cookie(&xauth, &display, uid, gid, &envs)?; + + // Start Xorg + let mut child_xorg = Self::start_x_server(&xauth, &display, uid, gid, &envs)?; + + log::info!("xorg started, wait 10 secs to ensuer x server is running"); + + let max_wait_secs = 10; + // wait x server running + if let Err(e) = Self::wait_x_server_running(child_xorg.id(), display_num, max_wait_secs) { + match Self::wait_xorg_exit(&mut child_xorg) { + Ok(msg) => log::info!("{}", msg), + Err(e) => { + log::error!("{}", e); + Self::fatal_exit(); + } + } + bail!(e) + } + + log::info!( + "xorg is running, start x window manager with DISPLAY: {}, XAUTHORITY: {}", + &display, + &xauth + ); + + std::env::set_var("DISPLAY", &display); + std::env::set_var("XAUTHORITY", &xauth); + // start window manager (startwm.sh) + let child_wm = match Self::start_x_window_manager(uid, gid, &envs) { + Ok(c) => c, + Err(e) => { + match Self::wait_xorg_exit(&mut child_xorg) { + Ok(msg) => log::info!("{}", msg), + Err(e) => { + log::error!("{}", e); + Self::fatal_exit(); + } + } + bail!(e) + } + }; + log::info!("x window manager is started"); + + Ok((child_xorg, child_wm)) + } + + fn try_wait_x11_child_exit(child_xorg: &mut Child, child_wm: &mut Child) -> bool { + match child_xorg.try_wait() { + Ok(Some(status)) => { + log::info!("Xorg exit with {}", status); + return true; + } + Ok(None) => {} + Err(e) => log::error!("Failed to wait xorg process, {}", e), + } + + match child_wm.try_wait() { + Ok(Some(status)) => { + // Logout may result "wm exit with signal: 11 (SIGSEGV) (core dumped)" + log::info!("wm exit with {}", status); + return true; + } + Ok(None) => {} + Err(e) => log::error!("Failed to wait xorg process, {}", e), + } + false + } + + fn wait_x11_children_exit(child_xorg: &mut Child, child_wm: &mut Child) { + log::debug!("Try kill child process xorg"); + if let Ok(_) = child_xorg.kill() { + let mut exited = false; + for _ in 0..2 { + match child_xorg.try_wait() { + Ok(Some(status)) => { + log::info!("Xorg exit with {}", status); + exited = true; + break; + } + Ok(None) => {} + Err(e) => { + log::error!("Failed to wait xorg process, {}", e); + Self::fatal_exit(); + } + } + std::thread::sleep(std::time::Duration::from_millis(1_000)); + } + if !exited { + log::error!("Failed to wait child xorg, after kill()"); + // try kill -9? + } + } + log::debug!("Try kill child process wm"); + if let Ok(_) = child_wm.kill() { + let mut exited = false; + for _ in 0..2 { + match child_wm.try_wait() { + Ok(Some(status)) => { + // Logout may result "wm exit with signal: 11 (SIGSEGV) (core dumped)" + log::info!("wm exit with {}", status); + exited = true; + } + Ok(None) => {} + Err(e) => { + log::error!("Failed to wait wm process, {}", e); + Self::fatal_exit(); + } + } + std::thread::sleep(std::time::Duration::from_millis(1_000)); + } + if !exited { + log::error!("Failed to wait child xorg, after kill()"); + // try kill -9? + } + } + } + + fn try_wait_stop_x11(child_xorg: &mut Child, child_wm: &mut Child) -> bool { + let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); + let mut exited = true; + if let Some(desktop_manager) = &mut (*desktop_manager) { + if desktop_manager.child_exit.load(Ordering::SeqCst) { + exited = true; + } else { + exited = Self::try_wait_x11_child_exit(child_xorg, child_wm); + } + if exited { + log::debug!("Wait x11 children exiting"); + Self::wait_x11_children_exit(child_xorg, child_wm); + desktop_manager + .is_child_running + .store(false, Ordering::SeqCst); + desktop_manager.child_exit.store(true, Ordering::SeqCst); + } + } + exited + } + + fn wait_stop_x11(mut child_xorg: Child, mut child_wm: Child) { + loop { + if Self::try_wait_stop_x11(&mut child_xorg, &mut child_wm) { + break; + } + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); + } + } + + fn get_xorg() -> &'static str { + // Fedora 26 or later + let xorg = "/usr/libexec/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Debian 9 or later + let xorg = "/usr/lib/xorg/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Ubuntu 16.04 or later + let xorg = "/usr/lib/xorg/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Arch Linux + let xorg = "/usr/lib/xorg-server/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Arch Linux + let xorg = "/usr/lib/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // CentOS 7 /usr/bin/Xorg or param=Xorg + + log::warn!("Failed to find xorg, use default Xorg.\n Please add \"allowed_users=anybody\" to \"/etc/X11/Xwrapper.config\"."); + "Xorg" + } + + fn start_x_server( + xauth: &str, + display: &str, + uid: u32, + gid: u32, + envs: &HashMap<&str, String>, + ) -> ResultType { + let xorg = Self::get_xorg(); + log::info!("Use xorg: {}", &xorg); + match Command::new(xorg) + .envs(envs) + .uid(uid) + .gid(gid) + .args(vec![ + "-noreset", + "+extension", + "GLX", + "+extension", + "RANDR", + "+extension", + "RENDER", + //"-logfile", + //"/tmp/RustDesk_xorg.log", + "-config", + "/etc/rustdesk/xorg.conf", + "-auth", + xauth, + display, + ]) + .spawn() + { + Ok(c) => Ok(c), + Err(e) => { + bail!("Failed to start Xorg with display {}, {}", display, e); + } + } + } + + fn start_x_window_manager( + uid: u32, + gid: u32, + envs: &HashMap<&str, String>, + ) -> ResultType { + match Command::new("/etc/rustdesk/startwm.sh") + .envs(envs) + .uid(uid) + .gid(gid) + .spawn() + { + Ok(c) => Ok(c), + Err(e) => { + bail!("Failed to start window manager, {}", e); + } + } + } + + fn stop_children(&mut self) { + self.child_exit.store(true, Ordering::SeqCst); + for _i in 1..10 { + if !self.is_child_running.load(Ordering::SeqCst) { + break; + } + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); + } + if self.is_child_running.load(Ordering::SeqCst) { + log::warn!("xdesktop child is still running!"); + } + } +} + +fn pam_get_service_name() -> &'static str { + if Path::new("/etc/pam.d/rustdesk").is_file() { + "rustdesk" + } else { + "gdm" + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 2ef39e92a..e382b0b13 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -17,9 +17,13 @@ pub mod delegate; #[cfg(target_os = "linux")] pub mod linux; +#[cfg(all(target_os = "linux", feature = "linux_headless"))] +#[cfg(not(any(feature = "flatpak", feature = "appimage")))] +pub mod linux_desktop_manager; + #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::{message_proto::CursorData, ResultType}; -#[cfg(not(target_os = "macos"))] +#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))] const SERVICE_INTERVAL: u64 = 300; pub fn is_xfce() -> bool { @@ -35,7 +39,9 @@ pub fn is_xfce() -> bool { pub fn breakdown_callback() { #[cfg(target_os = "linux")] - crate::input_service::clear_remapped_keycode() + crate::input_service::clear_remapped_keycode(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::input_service::release_device_modifiers(); } // Android diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 379cd29fb..8fe900d11 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,7 +1,10 @@ use super::{CursorData, ResultType}; use crate::common::PORTABLE_APPNAME_RUNTIME_ENV_KEY; -use crate::ipc; -use crate::license::*; +use crate::{ + ipc, + license::*, + privacy_win_mag::{self, WIN_MAG_INJECTED_PROCESS_EXE}, +}; use hbb_common::{ allow_err, bail, config::{self, Config}, @@ -9,16 +12,17 @@ use hbb_common::{ message_proto::Resolution, sleep, timeout, tokio, }; -use std::io::prelude::*; -use std::ptr::null_mut; use std::{ + collections::HashMap, ffi::OsString, - fs, io, mem, + fs, io, + io::prelude::*, + mem, os::windows::process::CommandExt, - path::PathBuf, + path::*, + ptr::null_mut, sync::{Arc, Mutex}, time::{Duration, Instant}, - collections::HashMap }; use winapi::{ ctypes::c_void, @@ -717,7 +721,7 @@ pub fn is_share_rdp() -> bool { } pub fn set_share_rdp(enable: bool) { - let (subkey, _, _, _) = get_install_info(); + let (subkey, _, _, _, _) = get_install_info(); let cmd = format!( "reg add {} /f /v share_rdp /t REG_SZ /d \"{}\"", subkey, @@ -810,11 +814,11 @@ fn get_valid_subkey() -> String { return get_subkey(&app_name, false); } -pub fn get_install_info() -> (String, String, String, String) { +pub fn get_install_info() -> (String, String, String, String, String) { get_install_info_with_subkey(get_valid_subkey()) } -fn get_default_install_info() -> (String, String, String, String) { +fn get_default_install_info() -> (String, String, String, String, String) { get_install_info_with_subkey(get_subkey(&crate::get_app_name(), false)) } @@ -837,8 +841,8 @@ fn get_default_install_path() -> String { pub fn check_update_broker_process() -> ResultType<()> { // let (_, path, _, _) = get_install_info(); - let process_exe = crate::win_privacy::INJECTED_PROCESS_EXE; - let origin_process_exe = crate::win_privacy::ORIGIN_PROCESS_EXE; + let process_exe = privacy_win_mag::INJECTED_PROCESS_EXE; + let origin_process_exe = privacy_win_mag::ORIGIN_PROCESS_EXE; let exe_file = std::env::current_exe()?; if exe_file.parent().is_none() { @@ -883,7 +887,7 @@ pub fn check_update_broker_process() -> ResultType<()> { Ok(()) } -fn get_install_info_with_subkey(subkey: String) -> (String, String, String, String) { +fn get_install_info_with_subkey(subkey: String) -> (String, String, String, String, String) { let mut path = get_reg_of(&subkey, "InstallLocation"); if path.is_empty() { path = get_default_install_path(); @@ -894,43 +898,59 @@ fn get_install_info_with_subkey(subkey: String) -> (String, String, String, Stri crate::get_app_name() ); let exe = format!("{}\\{}.exe", path, crate::get_app_name()); - (subkey, path, start_menu, exe) + let dll = format!("{}\\sciter.dll", path); + (subkey, path, start_menu, exe, dll) } -pub fn copy_exe_cmd(src_exe: &str, _exe: &str, path: &str) -> String { +pub fn copy_raw_cmd(src_raw: &str, _raw: &str, _path: &str) -> String { #[cfg(feature = "flutter")] - let main_exe = format!( + let main_raw = format!( "XCOPY \"{}\" \"{}\" /Y /E /H /C /I /K /R /Z", - PathBuf::from(src_exe) + PathBuf::from(src_raw) .parent() .unwrap() .to_string_lossy() .to_string(), - path + _path ); #[cfg(not(feature = "flutter"))] - let main_exe = format!( - "copy /Y \"{src_exe}\" \"{exe}\"", - src_exe = src_exe, - exe = _exe + let main_raw = format!( + "copy /Y \"{src_raw}\" \"{raw}\"", + src_raw = src_raw, + raw = _raw ); + return main_raw; +} - return format!( +pub fn copy_exe_cmd(src_exe: &str, exe: &str, path: &str) -> String { + let main_exe = copy_raw_cmd(src_exe, exe, path); + format!( " {main_exe} copy /Y \"{ORIGIN_PROCESS_EXE}\" \"{path}\\{broker_exe}\" - \"{src_exe}\" --extract \"{path}\" ", main_exe = main_exe, path = path, - ORIGIN_PROCESS_EXE = crate::win_privacy::ORIGIN_PROCESS_EXE, - broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, - ); + ORIGIN_PROCESS_EXE = privacy_win_mag::ORIGIN_PROCESS_EXE, + broker_exe = privacy_win_mag::INJECTED_PROCESS_EXE, + ) } pub fn update_me() -> ResultType<()> { - let (_, path, _, exe) = get_install_info(); + let (_, path, _, exe, _dll) = get_install_info(); let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); + #[cfg(not(feature = "flutter"))] + let src_dll = std::env::current_exe()? + .parent() + .unwrap_or(&Path::new(&get_default_install_path())) + .join("sciter.dll") + .to_str() + .unwrap_or("") + .to_owned(); + #[cfg(feature = "flutter")] + let copy_dll = "".to_string(); + #[cfg(not(feature = "flutter"))] + let copy_dll = copy_raw_cmd(&src_dll, &_dll, &path); let cmds = format!( " chcp 65001 @@ -938,11 +958,13 @@ pub fn update_me() -> ResultType<()> { taskkill /F /IM {broker_exe} taskkill /F /IM {app_name}.exe /FI \"PID ne {cur_pid}\" {copy_exe} + {copy_dll} sc start {app_name} {lic} ", copy_exe = copy_exe_cmd(&src_exe, &exe, &path), - broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, + copy_dll = copy_dll, + broker_exe = WIN_MAG_INJECTED_PROCESS_EXE, app_name = crate::get_app_name(), lic = register_licence(), cur_pid = get_current_pid(), @@ -980,12 +1002,14 @@ fn get_after_install(exe: &str) -> String { pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> ResultType<()> { let uninstall_str = get_uninstall(false); let mut path = path.trim_end_matches('\\').to_owned(); - let (subkey, _path, start_menu, exe) = get_default_install_info(); + let (subkey, _path, start_menu, exe, dll) = get_default_install_info(); let mut exe = exe; + let mut _dll = dll; if path.is_empty() { path = _path; } else { exe = exe.replace(&_path, &path); + _dll = _dll.replace(&_path, &path); } let mut version_major = "0"; let mut version_minor = "0"; @@ -1109,6 +1133,18 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} app_name = crate::get_app_name(), ); let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_string(); + #[cfg(not(feature = "flutter"))] + let src_dll = std::env::current_exe()? + .parent() + .unwrap_or(&Path::new(&get_default_install_path())) + .join("sciter.dll") + .to_str() + .unwrap_or("") + .to_owned(); + #[cfg(feature = "flutter")] + let copy_dll = "".to_string(); + #[cfg(not(feature = "flutter"))] + let copy_dll = copy_raw_cmd(&src_dll, &_dll, &path); let install_cert = if options.contains("driverCert") { format!("\"{}\" --install-cert \"RustDeskIddDriver.cer\"", src_exe) @@ -1122,6 +1158,7 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} chcp 65001 md \"{path}\" {copy_exe} +{copy_dll} reg add {subkey} /f reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\" reg add {subkey} /f /v DisplayName /t REG_SZ /d \"{app_name}\" @@ -1181,6 +1218,7 @@ sc delete {app_name} &dels }, copy_exe = copy_exe_cmd(&src_exe, &exe, &path), + copy_dll = copy_dll ); run_cmds(cmds, debug, "install")?; std::thread::sleep(std::time::Duration::from_millis(2000)); @@ -1193,7 +1231,7 @@ sc delete {app_name} } pub fn run_after_install() -> ResultType<()> { - let (_, _, _, exe) = get_install_info(); + let (_, _, _, exe, _) = get_install_info(); run_cmds(get_after_install(&exe), true, "after_install") } @@ -1220,14 +1258,14 @@ fn get_before_uninstall(kill_self: bool) -> String { netsh advfirewall firewall delete rule name=\"{app_name} Service\" ", app_name = app_name, - broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, + broker_exe = WIN_MAG_INJECTED_PROCESS_EXE, ext = ext, filter = filter, ) } fn get_uninstall(kill_self: bool) -> String { - let (subkey, path, start_menu, _) = get_install_info(); + let (subkey, path, start_menu, _, _) = get_install_info(); format!( " {before_uninstall} @@ -1331,7 +1369,7 @@ pub fn is_installed() -> bool { service::ServiceAccess, service_manager::{ServiceManager, ServiceManagerAccess}, }; - let (_, _, _, exe) = get_install_info(); + let (_, _, _, exe, _) = get_install_info(); if !std::fs::metadata(exe).is_ok() { return false; } @@ -1347,7 +1385,7 @@ pub fn is_installed() -> bool { } pub fn get_installed_version() -> String { - let (_, _, _, exe) = get_install_info(); + let (_, _, _, exe, _) = get_install_info(); if let Ok(output) = std::process::Command::new(exe).arg("--version").output() { for line in String::from_utf8_lossy(&output.stdout).lines() { return line.to_owned(); @@ -1357,7 +1395,7 @@ pub fn get_installed_version() -> String { } fn get_reg(name: &str) -> String { - let (subkey, _, _, _) = get_install_info(); + let (subkey, _, _, _, _) = get_install_info(); get_reg_of(&subkey, name) } @@ -1408,7 +1446,7 @@ pub fn bootstrap() { } fn register_licence() -> String { - let (subkey, _, _, _) = get_install_info(); + let (subkey, _, _, _, _) = get_install_info(); if let Ok(lic) = get_license_from_exe_name() { format!( " @@ -1765,14 +1803,17 @@ pub fn send_message_to_hnwd( pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> ResultType<()> { let last_error_table = HashMap::from([ - (ERROR_LOGON_FAILURE, "The user name or password is incorrect."), - (ERROR_ACCESS_DENIED, "Access is denied.") + ( + ERROR_LOGON_FAILURE, + "The user name or password is incorrect.", + ), + (ERROR_ACCESS_DENIED, "Access is denied."), ]); unsafe { let user_split = user.split("\\").collect::>(); let wuser = wide_string(user_split.get(1).unwrap_or(&user)); - let wpc = wide_string(user_split.get(0).unwrap_or(&"")); + let wpc = wide_string(user_split.get(0).unwrap_or(&"")); let wpwd = wide_string(pwd); let cmd = if arg.is_empty() { format!("\"{}\"", exe) @@ -1804,7 +1845,7 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> { let last_error = GetLastError(); bail!( - "CreateProcessWithLogonW failed : \"{}\", errno={}", + "CreateProcessWithLogonW failed : \"{}\", errno={}", last_error_table .get(&last_error) .unwrap_or(&"Unknown error"), @@ -2091,7 +2132,25 @@ mod cert { } } -pub fn get_char_by_vk(vk: u32) -> Option { +#[inline] +pub fn get_char_from_vk(vk: u32) -> Option { + get_char_from_unicode(get_unicode_from_vk(vk)?) +} + +pub fn get_char_from_unicode(unicode: u16) -> Option { + let buff = [unicode]; + if let Some(chr) = String::from_utf16(&buff[..1]).ok()?.chars().next() { + if chr.is_control() { + return None; + } else { + Some(chr) + } + } else { + None + } +} + +pub fn get_unicode_from_vk(vk: u32) -> Option { const BUF_LEN: i32 = 32; let mut buff = [0_u16; BUF_LEN as usize]; let buff_ptr = buff.as_mut_ptr(); @@ -2116,24 +2175,20 @@ pub fn get_char_by_vk(vk: u32) -> Option { ToUnicodeEx(vk, 0x00, &state as _, buff_ptr, BUF_LEN, 0, layout) }; if len == 1 { - if let Some(chr) = String::from_utf16(&buff[..len as usize]) - .ok()? - .chars() - .next() - { - if chr.is_control() { - return None; - } else { - Some(chr) - } - } else { - None - } + Some(buff[0]) } else { None } } +pub fn is_process_consent_running() -> ResultType { + let output = std::process::Command::new("cmd") + .args(&["/C", "tasklist | findstr consent.exe"]) + .creation_flags(CREATE_NO_WINDOW) + .output()?; + Ok(output.status.success() && !output.stdout.is_empty()) +} + #[cfg(test)] mod tests { use super::*; @@ -2151,10 +2206,10 @@ mod tests { } #[test] - fn test_get_char_by_vk() { - let chr = get_char_by_vk(0x41); // VK_A + fn test_get_unicode_char_by_vk() { + let chr = get_char_from_vk(0x41); // VK_A assert_eq!(chr, Some('a')); - let chr = get_char_by_vk(VK_ESCAPE as u32); // VK_ESC + let chr = get_char_from_vk(VK_ESCAPE as u32); // VK_ESC assert_eq!(chr, None) } } diff --git a/src/plugin/callback_msg.rs b/src/plugin/callback_msg.rs new file mode 100644 index 000000000..4d02db287 --- /dev/null +++ b/src/plugin/callback_msg.rs @@ -0,0 +1,111 @@ +use super::cstr_to_string; +use crate::flutter::{self, APP_TYPE_CM, APP_TYPE_MAIN, SESSIONS}; +use hbb_common::{lazy_static, log, message_proto::PluginRequest}; +use serde_json; +use std::{ + collections::HashMap, + ffi::{c_char, c_void}, + sync::Arc, +}; + +const MSG_TO_PEER_TARGET: &str = "peer"; +const MSG_TO_UI_TARGET: &str = "ui"; + +#[allow(dead_code)] +const MSG_TO_UI_FLUTTER_CHANNEL_MAIN: u16 = 0x01 << 0; +#[allow(dead_code)] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +const MSG_TO_UI_FLUTTER_CHANNEL_CM: u16 = 0x01 << 1; +#[cfg(any(target_os = "android", target_os = "ios"))] +const MSG_TO_UI_FLUTTER_CHANNEL_CM: u16 = 0x01; +const MSG_TO_UI_FLUTTER_CHANNEL_REMOTE: u16 = 0x01 << 2; +#[allow(dead_code)] +const MSG_TO_UI_FLUTTER_CHANNEL_TRANSFER: u16 = 0x01 << 3; +#[allow(dead_code)] +const MSG_TO_UI_FLUTTER_CHANNEL_FORWARD: u16 = 0x01 << 4; + +lazy_static::lazy_static! { + static ref MSG_TO_UI_FLUTTER_CHANNELS: Arc> = { + let channels = HashMap::from([ + (MSG_TO_UI_FLUTTER_CHANNEL_MAIN, APP_TYPE_MAIN.to_string()), + (MSG_TO_UI_FLUTTER_CHANNEL_CM, APP_TYPE_CM.to_string()), + ]); + Arc::new(channels) + }; +} + +/// Callback to send message to peer or ui. +/// peer, target, id are utf8 strings(null terminated). +/// +/// peer: The peer id. +/// target: "peer" or "ui". +/// id: The id of this plugin. +/// content: The content. +/// len: The length of the content. +pub fn callback_msg( + peer: *const c_char, + target: *const c_char, + id: *const c_char, + content: *const c_void, + len: usize, +) { + macro_rules! callback_msg_field { + ($field: ident) => { + let $field = match cstr_to_string($field) { + Err(e) => { + log::error!("Failed to convert {} to string, {}", stringify!($field), e); + return; + } + Ok(v) => v, + }; + }; + } + callback_msg_field!(peer); + callback_msg_field!(target); + callback_msg_field!(id); + + match &target as _ { + MSG_TO_PEER_TARGET => { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&peer) { + let content_slice = + unsafe { std::slice::from_raw_parts(content as *const u8, len) }; + let content_vec = Vec::from(content_slice); + let request = PluginRequest { + id, + content: bytes::Bytes::from(content_vec), + ..Default::default() + }; + session.send_plugin_request(request); + } + } + MSG_TO_UI_TARGET => { + let content_slice = unsafe { std::slice::from_raw_parts(content as *const u8, len) }; + let channel = u16::from_be_bytes([content_slice[0], content_slice[1]]); + let content = std::string::String::from_utf8(content_slice[2..].to_vec()) + .unwrap_or("".to_string()); + let mut m = HashMap::new(); + m.insert("name", "plugin_event"); + m.insert("peer", &peer); + m.insert("content", &content); + let event = serde_json::to_string(&m).unwrap_or("".to_string()); + for (k, v) in MSG_TO_UI_FLUTTER_CHANNELS.iter() { + if channel & k != 0 { + let _res = flutter::push_global_event(v as _, event.clone()); + } + } + if channel & MSG_TO_UI_FLUTTER_CHANNEL_REMOTE != 0 + || channel & MSG_TO_UI_FLUTTER_CHANNEL_TRANSFER != 0 + || channel & MSG_TO_UI_FLUTTER_CHANNEL_FORWARD != 0 + { + let _res = flutter::push_session_event( + &peer, + "plugin_event", + vec![("peer", &peer), ("content", &content)], + ); + } + } + _ => { + log::error!("Unknown target {}", target); + } + } +} diff --git a/src/plugin/config.rs b/src/plugin/config.rs new file mode 100644 index 000000000..d86053649 --- /dev/null +++ b/src/plugin/config.rs @@ -0,0 +1,196 @@ +use super::desc::ConfigItem; +use hbb_common::{bail, config::Config as HbbConfig, lazy_static, ResultType}; +use serde_derive::{Deserialize, Serialize}; +use std::{ + ops::{Deref, DerefMut}, + sync::{Arc, Mutex}, + {collections::HashMap, path::PathBuf}, +}; + +lazy_static::lazy_static! { + static ref CONFIG_LOCAL: Arc>> = Default::default(); + static ref CONFIG_LOCAL_ITEMS: Arc>>> = Default::default(); + static ref CONFIG_PEERS: Arc>> = Default::default(); + static ref CONFIG_PEER_ITEMS: Arc>>> = Default::default(); +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct LocalConfig(HashMap); +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct PeerConfig(HashMap); +type PeersConfig = HashMap; + +#[inline] +fn path_plugins(id: &str) -> PathBuf { + HbbConfig::path("plugins").join(id) +} + +impl Deref for LocalConfig { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for LocalConfig { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Deref for PeerConfig { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for PeerConfig { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl LocalConfig { + #[inline] + fn path(id: &str) -> PathBuf { + path_plugins(id).join("local.toml") + } + + #[inline] + pub fn load(id: &str) { + let mut conf = hbb_common::config::load_path::(Self::path(id)); + if let Some(items) = CONFIG_LOCAL_ITEMS.lock().unwrap().get(id) { + for item in items { + if !conf.contains_key(&item.key) { + conf.insert(item.key.to_owned(), item.default.to_owned()); + } + } + } + CONFIG_LOCAL.lock().unwrap().insert(id.to_owned(), conf); + } + + #[inline] + fn save(id: &str) -> ResultType<()> { + match CONFIG_LOCAL.lock().unwrap().get(id) { + Some(config) => hbb_common::config::store_path(Self::path(id), config), + None => bail!("No such plugin {}", id), + } + } + + #[inline] + pub fn get(id: &str, key: &str) -> Option { + if let Some(conf) = CONFIG_LOCAL.lock().unwrap().get(id) { + return conf.get(key).map(|s| s.to_owned()); + } + Self::load(id); + CONFIG_LOCAL + .lock() + .unwrap() + .get(id)? + .get(key) + .map(|s| s.to_owned()) + } + + #[inline] + pub fn set(id: &str, key: &str, value: &str) -> ResultType<()> { + match CONFIG_LOCAL.lock().unwrap().get_mut(id) { + Some(config) => { + config.insert(key.to_owned(), value.to_owned()); + hbb_common::config::store_path(Self::path(id), config) + } + None => bail!("No such plugin {}", id), + } + } +} + +impl PeerConfig { + #[inline] + fn path(id: &str, peer: &str) -> PathBuf { + path_plugins(id) + .join("peers") + .join(format!("{}.toml", peer)) + } + + #[inline] + pub fn load(id: &str, peer: &str) { + let mut conf = hbb_common::config::load_path::(Self::path(id, peer)); + if let Some(items) = CONFIG_PEER_ITEMS.lock().unwrap().get(id) { + for item in items { + if !conf.contains_key(&item.key) { + conf.insert(item.key.to_owned(), item.default.to_owned()); + } + } + } + match CONFIG_PEERS.lock().unwrap().get_mut(id) { + Some(peers) => { + peers.insert(peer.to_owned(), conf); + } + None => { + let mut peers = HashMap::new(); + peers.insert(peer.to_owned(), conf); + CONFIG_PEERS.lock().unwrap().insert(id.to_owned(), peers); + } + } + } + + #[inline] + fn save(id: &str, peer: &str) -> ResultType<()> { + match CONFIG_PEERS.lock().unwrap().get(id) { + Some(peers) => match peers.get(peer) { + Some(config) => hbb_common::config::store_path(Self::path(id, peer), config), + None => bail!("No such peer {}", peer), + }, + None => bail!("No such plugin {}", id), + } + } + + #[inline] + pub fn get(id: &str, peer: &str, key: &str) -> Option { + if let Some(peers) = CONFIG_PEERS.lock().unwrap().get(id) { + if let Some(conf) = peers.get(peer) { + return conf.get(key).map(|s| s.to_owned()); + } + } + Self::load(id, peer); + CONFIG_PEERS + .lock() + .unwrap() + .get(id)? + .get(peer)? + .get(key) + .map(|s| s.to_owned()) + } + + #[inline] + pub fn set(id: &str, peer: &str, key: &str, value: &str) -> ResultType<()> { + match CONFIG_PEERS.lock().unwrap().get_mut(id) { + Some(peers) => match peers.get_mut(peer) { + Some(config) => { + config.insert(key.to_owned(), value.to_owned()); + hbb_common::config::store_path(Self::path(id, peer), config) + } + None => bail!("No such peer {}", peer), + }, + None => bail!("No such plugin {}", id), + } + } +} + +#[inline] +pub(super) fn set_local_items(id: &str, items: &Vec) { + CONFIG_LOCAL_ITEMS + .lock() + .unwrap() + .insert(id.to_owned(), items.clone()); +} + +#[inline] +pub(super) fn set_peer_items(id: &str, items: &Vec) { + CONFIG_PEER_ITEMS + .lock() + .unwrap() + .insert(id.to_owned(), items.clone()); +} diff --git a/src/plugin/desc.rs b/src/plugin/desc.rs new file mode 100644 index 000000000..94a137570 --- /dev/null +++ b/src/plugin/desc.rs @@ -0,0 +1,119 @@ +use hbb_common::ResultType; +use serde_derive::{Deserialize, Serialize}; +use serde_json; +use std::collections::HashMap; +use std::ffi::{c_char, CStr}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct UiButton { + key: String, + text: String, + icon: String, // icon can be int in flutter, but string in other ui framework. And it is flexible to use string. + tooltip: String, + action: String, // The action to be triggered when the button is clicked. +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UiCheckbox { + key: String, + text: String, + tooltip: String, + action: String, // The action to be triggered when the checkbox is checked or unchecked. +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "t", content = "c")] +pub enum UiType { + Button(UiButton), + Checkbox(UiCheckbox), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Location { + pub ui: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigItem { + pub key: String, + pub value: String, + pub default: String, + pub description: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Config { + pub local: Vec, + pub peer: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Desc { + id: String, + name: String, + version: String, + description: String, + author: String, + home: String, + license: String, + published: String, + released: String, + github: String, + location: Location, + config: Config, +} + +impl Desc { + pub fn from_cstr(s: *const c_char) -> ResultType { + let s = unsafe { CStr::from_ptr(s) }; + Ok(serde_json::from_str(s.to_str()?)?) + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn version(&self) -> &str { + &self.version + } + + pub fn description(&self) -> &str { + &self.description + } + + pub fn author(&self) -> &str { + &self.author + } + + pub fn home(&self) -> &str { + &self.home + } + + pub fn license(&self) -> &str { + &self.license + } + + pub fn published(&self) -> &str { + &self.published + } + + pub fn released(&self) -> &str { + &self.released + } + + pub fn github(&self) -> &str { + &self.github + } + + pub fn location(&self) -> &Location { + &self.location + } + + pub fn config(&self) -> &Config { + &self.config + } +} diff --git a/src/plugin/errno.rs b/src/plugin/errno.rs new file mode 100644 index 000000000..db580c0bd --- /dev/null +++ b/src/plugin/errno.rs @@ -0,0 +1,28 @@ +#![allow(dead_code)] + +pub const ERR_SUCCESS: i32 = 0; + +// ====================================================== +// errors that will be handled by RustDesk + +pub const ERR_RUSTDESK_HANDLE_BASE: i32 = 10000; + +// not loaded +pub const ERR_PLUGIN_LOAD: i32 = 10001; +// not initialized +pub const ERR_PLUGIN_MSG_CB: i32 = 10101; +// invalid +pub const ERR_CALL_INVALID_METHOD: i32 = 10201; +pub const ERR_CALL_NOT_SUPPORTED_METHOD: i32 = 10202; +// failed on calling +pub const ERR_CALL_INVALID_ARGS: i32 = 10301; +pub const ERR_PEER_ID_MISMATCH: i32 = 10302; + +// ====================================================== +// errors that should be handled by the plugin + +pub const ERR_PLUGIN_HANDLE_BASE: i32 = 20000; + +pub const EER_CALL_FAILED: i32 = 200021; +pub const ERR_PEER_ON_FAILED: i32 = 30012; +pub const ERR_PEER_OFF_FAILED: i32 = 30012; diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs new file mode 100644 index 000000000..c7ff2c4af --- /dev/null +++ b/src/plugin/mod.rs @@ -0,0 +1,34 @@ +use hbb_common::ResultType; +use std::ffi::{c_char, c_void, CStr}; + +mod callback_msg; +mod config; +pub mod desc; +mod errno; +mod plugins; + +pub use plugins::{ + handle_client_event, handle_server_event, handle_ui_event, load_plugin, load_plugins, + reload_plugin, unload_plugin, +}; + +pub use config::{LocalConfig, PeerConfig}; + +#[inline] +fn cstr_to_string(cstr: *const c_char) -> ResultType { + Ok(String::from_utf8(unsafe { + CStr::from_ptr(cstr).to_bytes().to_vec() + })?) +} + +#[inline] +fn get_code_msg_from_ret(ret: *const c_void) -> (i32, String) { + assert!(ret.is_null() == false); + let code_bytes = unsafe { std::slice::from_raw_parts(ret as *const u8, 4) }; + let code = i32::from_le_bytes([code_bytes[0], code_bytes[1], code_bytes[2], code_bytes[3]]); + let msg = unsafe { CStr::from_ptr((ret as *const u8).add(4) as _) } + .to_str() + .unwrap_or("") + .to_string(); + (code, msg) +} diff --git a/src/plugin/plugins.rs b/src/plugin/plugins.rs new file mode 100644 index 000000000..e05940cd1 --- /dev/null +++ b/src/plugin/plugins.rs @@ -0,0 +1,315 @@ +use std::{ + collections::HashMap, + ffi::{c_char, c_void}, + path::Path, + sync::{Arc, RwLock}, +}; + +use super::{callback_msg, desc::Desc, errno::*, get_code_msg_from_ret}; +use crate::flutter; +use hbb_common::{ + bail, + dlopen::symbor::Library, + lazy_static, libc, log, + message_proto::{Message, Misc, PluginResponse}, + ResultType, +}; + +const METHOD_HANDLE_UI: &[u8; 10] = b"handle_ui\0"; +const METHOD_HANDLE_PEER: &[u8; 12] = b"handle_peer\0"; + +lazy_static::lazy_static! { + pub static ref PLUGINS: Arc>> = Default::default(); +} + +/// Initialize the plugins. +/// +/// Return null ptr if success. +/// Return the error message if failed. `i32-String` without dash, i32 is a signed little-endian number, the String is utf8 string. +/// The plugin allocate memory with `libc::malloc` and return the pointer. +pub type PluginFuncInit = fn() -> *const c_void; +/// Reset the plugin. +/// +/// Return null ptr if success. +/// Return the error message if failed. `i32-String` without dash, i32 is a signed little-endian number, the String is utf8 string. +/// The plugin allocate memory with `libc::malloc` and return the pointer. +pub type PluginFuncReset = fn() -> *const c_void; +/// Clear the plugin. +/// +/// Return null ptr if success. +/// Return the error message if failed. `i32-String` without dash, i32 is a signed little-endian number, the String is utf8 string. +/// The plugin allocate memory with `libc::malloc` and return the pointer. +pub type PluginFuncClear = fn() -> *const c_void; +/// Get the description of the plugin. +/// Return the description. The plugin allocate memory with `libc::malloc` and return the pointer. +pub type PluginFuncDesc = fn() -> *const c_char; +/// Callback to send message to peer or ui. +/// peer, target, id are utf8 strings(null terminated). +/// +/// peer: The peer id. +/// target: "peer" or "ui". +/// id: The id of this plugin. +/// content: The content. +/// len: The length of the content. +type PluginFuncCallbackMsg = fn( + peer: *const c_char, + target: *const c_char, + id: *const c_char, + content: *const c_void, + len: usize, +); +pub type PluginFuncSetCallbackMsg = fn(PluginFuncCallbackMsg); +/// The main function of the plugin. +/// method: The method. "handle_ui" or "handle_peer" +/// args: The arguments. +/// +/// Return null ptr if success. +/// Return the error message if failed. `i32-String` without dash, i32 is a signed little-endian number, the String is utf8 string. +/// The plugin allocate memory with `libc::malloc` and return the pointer. +pub type PluginFuncCall = + fn(method: *const c_char, args: *const c_void, len: usize) -> *const c_void; + +macro_rules! make_plugin { + ($($field:ident : $tp:ty),+) => { + pub struct Plugin { + _lib: Library, + path: String, + desc: Option, + $($field: $tp),+ + } + + impl Plugin { + fn new(path: &str) -> ResultType { + let lib = match Library::open(path) { + Ok(lib) => lib, + Err(e) => { + bail!("Failed to load library {}, {}", path, e); + } + }; + + $(let $field = match unsafe { lib.symbol::<$tp>(stringify!($field)) } { + Ok(m) => { + log::info!("method found {}", stringify!($field)); + *m + }, + Err(e) => { + bail!("Failed to load {} func {}, {}", path, stringify!($field), e); + } + } + ;)+ + + Ok(Self { + _lib: lib, + path: path.to_string(), + desc: None, + $( $field ),+ + }) + } + } + } +} + +make_plugin!( + fn_init: PluginFuncInit, + fn_reset: PluginFuncReset, + fn_clear: PluginFuncClear, + fn_desc: PluginFuncDesc, + fn_set_cb_msg: PluginFuncSetCallbackMsg, + fn_call: PluginFuncCall +); + +pub fn load_plugins>(dir: P) -> ResultType<()> { + for entry in std::fs::read_dir(dir)? { + match entry { + Ok(entry) => { + let path = entry.path(); + if path.is_file() { + let path = path.to_str().unwrap_or(""); + if path.ends_with(".so") { + if let Err(e) = load_plugin(path) { + log::error!("{e}"); + } + } + } + } + Err(e) => { + log::error!("Failed to read dir entry, {}", e); + } + } + } + Ok(()) +} + +pub fn unload_plugin(id: &str) { + if let Some(plugin) = PLUGINS.write().unwrap().remove(id) { + let _ret = (plugin.fn_clear)(); + } +} + +pub fn reload_plugin(id: &str) -> ResultType<()> { + let path = match PLUGINS.read().unwrap().get(id) { + Some(plugin) => plugin.path.clone(), + None => bail!("Plugin {} not found", id), + }; + unload_plugin(id); + load_plugin(&path) +} + +pub fn load_plugin(path: &str) -> ResultType<()> { + let mut plugin = Plugin::new(path)?; + let desc = (plugin.fn_desc)(); + let desc_res = Desc::from_cstr(desc); + unsafe { + libc::free(desc as _); + } + let desc = desc_res?; + let id = desc.id().to_string(); + // to-do validate plugin + // to-do check the plugin id (make sure it does not use another plugin's id) + (plugin.fn_set_cb_msg)(callback_msg::callback_msg); + update_ui_plugin_desc(&desc); + update_config(&desc); + reload_ui(&desc); + plugin.desc = Some(desc); + PLUGINS.write().unwrap().insert(id, plugin); + Ok(()) +} + +fn handle_event(method: &[u8], id: &str, event: &[u8]) -> ResultType<()> { + match PLUGINS.read().unwrap().get(id) { + Some(plugin) => { + let ret = (plugin.fn_call)(method.as_ptr() as _, event.as_ptr() as _, event.len()); + if ret.is_null() { + Ok(()) + } else { + let (code, msg) = get_code_msg_from_ret(ret); + unsafe { + libc::free(ret as _); + } + bail!( + "Failed to handle plugin event, id: {}, method: {}, code: {}, msg: {}", + id, + std::string::String::from_utf8(method.to_vec()).unwrap_or_default(), + code, + msg + ); + } + } + None => bail!("Plugin {} not found", id), + } +} + +#[inline] +pub fn handle_ui_event(id: &str, event: &[u8]) -> ResultType<()> { + handle_event(METHOD_HANDLE_UI, id, event) +} + +#[inline] +pub fn handle_server_event(id: &str, event: &[u8]) -> ResultType<()> { + handle_event(METHOD_HANDLE_PEER, id, event) +} + +#[inline] +pub fn handle_client_event(id: &str, event: &[u8]) -> Option { + match PLUGINS.read().unwrap().get(id) { + Some(plugin) => { + let ret = (plugin.fn_call)( + METHOD_HANDLE_PEER.as_ptr() as _, + event.as_ptr() as _, + event.len(), + ); + if ret.is_null() { + None + } else { + let (code, msg) = get_code_msg_from_ret(ret); + unsafe { + libc::free(ret as _); + } + if code > ERR_RUSTDESK_HANDLE_BASE && code < ERR_PLUGIN_HANDLE_BASE { + let name = plugin.desc.as_ref().unwrap().name(); + match code { + ERR_CALL_NOT_SUPPORTED_METHOD => Some(make_plugin_response( + id, + name, + "plugin method is not supported", + )), + ERR_CALL_INVALID_ARGS => Some(make_plugin_response( + id, + name, + "plugin arguments is invalid", + )), + _ => Some(make_plugin_response(id, name, &msg)), + } + } else { + log::error!( + "Failed to handle client event, code: {}, msg: {}", + code, + msg + ); + None + } + } + } + None => Some(make_plugin_response(id, "", "plugin not found")), + } +} + +fn make_plugin_response(id: &str, name: &str, msg: &str) -> Message { + let mut misc = Misc::new(); + misc.set_plugin_response(PluginResponse { + id: id.to_owned(), + name: name.to_owned(), + msg: msg.to_owned(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out +} + +fn update_config(desc: &Desc) { + super::config::set_local_items(desc.id(), &desc.config().local); + super::config::set_peer_items(desc.id(), &desc.config().peer); +} + +fn reload_ui(desc: &Desc) { + for (location, ui) in desc.location().ui.iter() { + let v: Vec<&str> = location.split('|').collect(); + // The first element is the "client" or "host". + // The second element is the "main", "remote", "cm", "file transfer", "port forward". + if v.len() >= 2 { + let available_channels = vec![ + flutter::APP_TYPE_MAIN, + flutter::APP_TYPE_DESKTOP_REMOTE, + flutter::APP_TYPE_CM, + flutter::APP_TYPE_DESKTOP_FILE_TRANSFER, + flutter::APP_TYPE_DESKTOP_PORT_FORWARD, + ]; + if available_channels.contains(&v[1]) { + if let Ok(ui) = serde_json::to_string(&ui) { + let mut m = HashMap::new(); + m.insert("name", "plugin_reload"); + m.insert("id", desc.id()); + m.insert("location", &location); + m.insert("ui", &ui); + flutter::push_global_event(v[1], serde_json::to_string(&m).unwrap()); + } + } + } + } +} + +fn update_ui_plugin_desc(desc: &Desc) { + // This function is rarely used. There's no need to care about serialization efficiency here. + if let Ok(desc_str) = serde_json::to_string(desc) { + let mut m = HashMap::new(); + m.insert("name", "plugin_desc"); + m.insert("desc", &desc_str); + flutter::push_global_event(flutter::APP_TYPE_MAIN, serde_json::to_string(&m).unwrap()); + flutter::push_global_event( + flutter::APP_TYPE_DESKTOP_REMOTE, + serde_json::to_string(&m).unwrap(), + ); + flutter::push_global_event(flutter::APP_TYPE_CM, serde_json::to_string(&m).unwrap()); + } +} diff --git a/src/plugins.rs b/src/plugins.rs new file mode 100644 index 000000000..dfbdc7753 --- /dev/null +++ b/src/plugins.rs @@ -0,0 +1,201 @@ +use std::{ + collections::HashMap, + ffi::{c_char, CStr}, + sync::{Arc, RwLock}, +}; + +use hbb_common::{ + anyhow::Error, + log::{debug, error}, +}; +use lazy_static::lazy_static; +use libloading::{Library, Symbol}; + +lazy_static! { + pub static ref PLUGIN_REGISTRAR: Arc> = + Arc::new(PluginRegistar::::default()); +} +// API needed to be implemented by plugins. +pub type PluginInitFunc = fn() -> i32; +// API needed to be implemented by plugins. +pub type PluginIdFunc = fn() -> *const c_char; +// API needed to be implemented by plugins. +pub type PluginNameFunc = fn() -> *const c_char; +// API needed to be implemented by plugins. +pub type PluginDisposeFunc = fn() -> i32; + +pub trait Plugin { + // Return: the unique ID which identifies this plugin. + fn plugin_id(&self) -> String; + // Return: the name which is human-readable. + fn plugin_name(&self) -> String; + // Return: the virtual table of the plugin. + fn plugin_vt(&self) -> &RustDeskPluginTable; +} + +#[repr(C)] +#[derive(Default, Clone)] +pub struct RustDeskPluginTable { + pub init: Option, // NonNull + pub dispose: Option, // NonNull +} + +pub struct PluginImpl { + vt: RustDeskPluginTable, + pub id: String, + pub name: String, + _inner: Option, +} + +impl Default for PluginImpl { + fn default() -> Self { + Self { + _inner: None, + vt: Default::default(), + id: Default::default(), + name: Default::default(), + } + } +} + +impl Plugin for PluginImpl { + fn plugin_id(&self) -> String { + self.id.to_owned() + } + + fn plugin_name(&self) -> String { + self.name.to_owned() + } + + fn plugin_vt(&self) -> &RustDeskPluginTable { + &self.vt + } +} + +#[derive(Default, Clone)] +pub struct PluginRegistar { + plugins: Arc>>, +} + +impl PluginRegistar

{ + pub fn load_plugin(&self, path: *const c_char) -> i32 { + let p = unsafe { CStr::from_ptr(path) }; + let lib_path = p.to_str().unwrap_or("").to_owned(); + let lib = unsafe { libloading::Library::new(lib_path.as_str()) }; + match lib { + Ok(lib) => match lib.try_into() { + Ok(plugin) => { + let plugin: PluginImpl = plugin; + // try to initialize this plugin + if let Some(init) = plugin.plugin_vt().init { + let init_ret = init(); + if init_ret != 0 { + error!( + "Error when initializing the plugin {} with error code {}.", + plugin.name, init_ret + ); + return init_ret; + } + } + PLUGIN_REGISTRAR + .plugins + .write() + .unwrap() + .insert(lib_path, plugin); + return 0; + } + Err(err) => { + eprintln!("Load plugin failed: {}", err); + } + }, + Err(err) => { + eprintln!("Load plugin failed: {}", err); + } + } + -1 + } + + pub fn unload_plugin(&self, path: *const c_char) -> i32 { + let p = unsafe { CStr::from_ptr(path) }; + let lib_path = p.to_str().unwrap_or("").to_owned(); + match PLUGIN_REGISTRAR.plugins.write().unwrap().remove(&lib_path) { + Some(plugin) => { + if let Some(dispose) = plugin.plugin_vt().dispose { + return dispose(); + } + 0 + } + None => -1, + } + } +} + +impl TryFrom for PluginImpl { + type Error = Error; + + fn try_from(library: Library) -> Result { + let init: Symbol = unsafe { library.get(b"plugin_init")? }; + let dispose: Symbol = unsafe { library.get(b"plugin_dispose")? }; + let id_func: Symbol = unsafe { library.get(b"plugin_id")? }; + let id_string = unsafe { + std::ffi::CStr::from_ptr(id_func()) + .to_str() + .unwrap_or("") + .to_owned() + }; + let name_func: Symbol = unsafe { library.get(b"plugin_name")? }; + let name_string = unsafe { + std::ffi::CStr::from_ptr(name_func()) + .to_str() + .unwrap_or("") + .to_owned() + }; + debug!( + "Successfully loaded the plugin called {} with id {}.", + name_string, id_string + ); + Ok(Self { + vt: RustDeskPluginTable { + init: Some(*init), + dispose: Some(*dispose), + }, + id: id_string, + name: name_string, + _inner: Some(library), + }) + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_plugin() { + use std::io::Write; + let mut cmd = std::process::Command::new("cargo"); + cmd.current_dir("./examples/custom_plugin"); + // Strip this shared library. + cmd.env("RUSTFLAGS", "-C link-arg=-s"); + cmd.arg("build"); + // Spawn the compiler process. + let mut child = cmd.spawn().unwrap(); + // Wait for the compiler to finish. + let status = child.wait().unwrap(); + assert!(status.success()); + // Load the library. + let lib = unsafe { + Library::new("./examples/custom_plugin/target/debug/libcustom_plugin.so").unwrap() + }; + let plugin: PluginImpl = lib.try_into().unwrap(); + assert!(plugin._inner.is_some()); + assert!(plugin.name == "A Template Rust Plugin"); + assert!(plugin.id == "TemplatePlugin"); + println!( + "plugin vt size: {}", + std::mem::size_of::() + ); + assert!(PLUGIN_REGISTRAR + .plugins + .write() + .unwrap() + .insert("test".to_owned(), plugin) + .is_none()); +} diff --git a/src/port_forward.rs b/src/port_forward.rs index f50f40db8..028875fa3 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -154,7 +154,8 @@ async fn connect_and_login_2( } else { ConnType::PORT_FORWARD }; - let (mut stream, direct) = Client::start(id, key, token, conn_type, interface.clone()).await?; + let (mut stream, direct, _pk) = + Client::start(id, key, token, conn_type, interface.clone()).await?; let mut interface = interface; let mut buffer = Vec::new(); let mut received = false; @@ -199,8 +200,8 @@ async fn connect_and_login_2( }, d = ui_receiver.recv() => { match d { - Some(Data::Login((password, remember))) => { - interface.handle_login_from_ui(password, remember, &mut stream).await; + Some(Data::Login((os_username, os_password, password, remember))) => { + interface.handle_login_from_ui(os_username, os_password, password, remember, &mut stream).await; } _ => {} } diff --git a/src/win_privacy.rs b/src/privacy_win_mag.rs similarity index 96% rename from src/win_privacy.rs rename to src/privacy_win_mag.rs index 9944bf262..fe0ee4f69 100644 --- a/src/win_privacy.rs +++ b/src/privacy_win_mag.rs @@ -5,7 +5,6 @@ use crate::{ use hbb_common::{allow_err, bail, lazy_static, log, tokio, ResultType}; use std::{ ffi::CString, - os::windows::process::CommandExt, sync::Mutex, time::{Duration, Instant}, }; @@ -25,18 +24,22 @@ use winapi::{ CreateProcessAsUserW, GetCurrentThreadId, QueueUserAPC, ResumeThread, PROCESS_INFORMATION, STARTUPINFOW, }, - winbase::{ - WTSGetActiveConsoleSessionId, CREATE_NO_WINDOW, CREATE_SUSPENDED, DETACHED_PROCESS, - }, + winbase::{WTSGetActiveConsoleSessionId, CREATE_SUSPENDED, DETACHED_PROCESS}, winnt::{MEM_COMMIT, PAGE_READWRITE}, winuser::*, }, }; pub const ORIGIN_PROCESS_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe"; -pub const INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe"; +pub const WIN_MAG_INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe"; +pub const INJECTED_PROCESS_EXE: &'static str = WIN_MAG_INJECTED_PROCESS_EXE; pub const PRIVACY_WINDOW_NAME: &'static str = "RustDeskPrivacyWindow"; +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"; + pub const GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT: u32 = 2; pub const GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS: u32 = 4; @@ -317,14 +320,6 @@ fn wait_find_privacy_hwnd(msecs: u128) -> ResultType { } } -pub fn is_process_consent_running() -> ResultType { - let output = std::process::Command::new("cmd") - .args(&["/C", "tasklist | findstr consent.exe"]) - .creation_flags(CREATE_NO_WINDOW) - .output()?; - Ok(output.status.success() && !output.stdout.is_empty()) -} - #[tokio::main(flavor = "current_thread")] async fn set_privacy_mode_state( conn_id: i32, diff --git a/src/rc.rs b/src/rc.rs deleted file mode 100644 index ef86caaa3..000000000 --- a/src/rc.rs +++ /dev/null @@ -1,38 +0,0 @@ -use hbb_common::{bail, ResultType}; -use std::{ - fs::{self, File}, - io::prelude::*, - path::Path, -}; - -#[allow(dead_code)] -pub fn extract_resources(root_path: &str) -> ResultType<()> { - let mut resources: Vec<(&str, &[u8])> = Vec::new(); - resources.push((outfile_4, &outdata_4)); - do_extract(root_path, resources)?; - Ok(()) -} - -#[allow(dead_code)] -fn do_extract(root_path: &str, resources: Vec<(&str, &[u8])>) -> ResultType<()> { - let mut root_path = root_path.replace("\\", "/"); - if !root_path.ends_with('/') { - root_path.push('/'); - } - let root_path = Path::new(&root_path); - for (outfile, data) in resources { - let outfile_path = root_path.join(outfile); - match outfile_path.parent().and_then(|p| p.to_str()) { - None => { - bail!("Failed to get parent of {}", outfile_path.display()); - } - Some(p) => { - fs::create_dir_all(p)?; - let mut of = File::create(outfile_path)?; - of.write_all(data)?; - of.flush()?; - } - } - } - Ok(()) -} diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 8b7dae1ba..b6422e9a6 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -72,6 +72,9 @@ impl RendezvousMediator { allow_err!(super::lan::start_listening()); }); } + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + crate::platform::linux_desktop_manager::start_xdesktop(); loop { Config::reset_online(); if Config::get_option("stop-service").is_empty() { @@ -96,6 +99,11 @@ impl RendezvousMediator { } sleep(1.).await; } + // It should be better to call stop_xdesktop. + // But for server, it also is Ok without calling this method. + // #[cfg(all(target_os = "linux", feature = "linux_headless"))] + // #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + // crate::platform::linux_desktop_manager::stop_xdesktop(); } pub async fn start(server: ServerPtr, host: String) -> ResultType<()> { diff --git a/src/server.rs b/src/server.rs index 1f174ef03..6a0371757 100644 --- a/src/server.rs +++ b/src/server.rs @@ -365,7 +365,7 @@ pub fn check_zombie() { /// Otherwise, client will check if there's already a server and start one if not. #[cfg(any(target_os = "android", target_os = "ios"))] #[tokio::main] -pub async fn start_server(is_server: bool) { +pub async fn start_server(_is_server: bool) { crate::RendezvousMediator::start_all().await; } @@ -389,7 +389,7 @@ pub async fn start_server(is_server: bool) { use std::sync::Once; static ONCE: Once = Once::new(); ONCE.call_once(|| { - scrap::hwcodec::check_config_process(false); + scrap::hwcodec::check_config_process(); }) } @@ -451,17 +451,13 @@ pub async fn start_ipc_url_server() { Ok(Some(data)) => match data { #[cfg(feature = "flutter")] Data::UrlLink(url) => { - if let Some(stream) = crate::flutter::GLOBAL_EVENT_STREAM - .read() - .unwrap() - .get(crate::flutter::APP_TYPE_MAIN) - { - let mut m = HashMap::new(); - m.insert("name", "on_url_scheme_received"); - m.insert("url", url.as_str()); - stream.add(serde_json::to_string(&m).unwrap()); - } else { - log::warn!("No main window app found!"); + let mut m = HashMap::new(); + m.insert("name", "on_url_scheme_received"); + m.insert("url", url.as_str()); + let event = serde_json::to_string(&m).unwrap(); + match crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, event) { + None => log::warn!("No main window app found!"), + Some(..) => {} } } _ => { diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs index c7a720ded..c2b1d80f9 100644 --- a/src/server/audio_service.rs +++ b/src/server/audio_service.rs @@ -13,7 +13,6 @@ // https://github.com/krruzic/pulsectl use super::*; -use hbb_common::get_time; use magnum_opus::{Application::*, Channels::*, Encoder}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -65,6 +64,7 @@ mod pa_impl { ))) .await ); + #[cfg(target_os = "linux")] let zero_audio_frame: Vec = vec![0.; AUDIO_DATA_SIZE_U8 / 4]; while sp.ok() && !RESTARTING.load(Ordering::SeqCst) { sp.snapshot(|sps| { @@ -104,11 +104,12 @@ mod cpal_impl { use super::*; use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, - Device, Host, SupportedStreamConfig, + BufferSize, Device, Host, InputCallbackInfo, StreamConfig, SupportedStreamConfig, }; lazy_static::lazy_static! { static ref HOST: Host = cpal::default_host(); + static ref INPUT_BUFFER: Arc>> = Default::default(); } #[derive(Default)] @@ -213,12 +214,9 @@ mod cpal_impl { } fn play(sp: &GenericService) -> ResultType<(Box, Arc)> { + use cpal::SampleFormat::*; let (device, config) = get_device()?; let sp = sp.clone(); - let err_fn = move |err| { - // too many UnknownErrno, will improve later - log::trace!("an error occurred on stream: {}", err); - }; // Sample rate must be one of 8000, 12000, 16000, 24000, or 48000. let sample_rate_0 = config.sample_rate().0; let sample_rate = if sample_rate_0 < 12000 { @@ -232,6 +230,40 @@ mod cpal_impl { } else { 48000 }; + let stream = match config.sample_format() { + I8 => build_input_stream::(device, &config, sp, sample_rate)?, + I16 => build_input_stream::(device, &config, sp, sample_rate)?, + I32 => build_input_stream::(device, &config, sp, sample_rate)?, + I64 => build_input_stream::(device, &config, sp, sample_rate)?, + U8 => build_input_stream::(device, &config, sp, sample_rate)?, + U16 => build_input_stream::(device, &config, sp, sample_rate)?, + U32 => build_input_stream::(device, &config, sp, sample_rate)?, + U64 => build_input_stream::(device, &config, sp, sample_rate)?, + F32 => build_input_stream::(device, &config, sp, sample_rate)?, + F64 => build_input_stream::(device, &config, sp, sample_rate)?, + f => bail!("unsupported audio format: {:?}", f), + }; + stream.play()?; + Ok(( + Box::new(stream), + Arc::new(create_format_msg(sample_rate, config.channels())), + )) + } + + fn build_input_stream( + device: cpal::Device, + config: &cpal::SupportedStreamConfig, + sp: GenericService, + sample_rate: u32, + ) -> ResultType + where + T: cpal::SizedSample + dasp::sample::ToSample, + { + let err_fn = move |err| { + // too many UnknownErrno, will improve later + log::trace!("an error occurred on stream: {}", err); + }; + let sample_rate_0 = config.sample_rate().0; log::debug!("Audio sample rate : {}", sample_rate); unsafe { AUDIO_ZERO_COUNT = 0; @@ -242,57 +274,38 @@ mod cpal_impl { LowDelay, )?; let channels = config.channels(); - let stream = match config.sample_format() { - cpal::SampleFormat::F32 => device.build_input_stream( - &config.into(), - move |data, _: &_| { - send( - data, - sample_rate_0, - sample_rate, - channels, - &mut encoder, - &sp, - ); - }, - err_fn, - )?, - cpal::SampleFormat::I16 => device.build_input_stream( - &config.into(), - move |data: &[i16], _: &_| { - let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect(); - send( - &buffer, - sample_rate_0, - sample_rate, - channels, - &mut encoder, - &sp, - ); - }, - err_fn, - )?, - cpal::SampleFormat::U16 => device.build_input_stream( - &config.into(), - move |data: &[u16], _: &_| { - let buffer: Vec<_> = data.iter().map(|s| cpal::Sample::to_f32(s)).collect(); - send( - &buffer, - sample_rate_0, - sample_rate, - channels, - &mut encoder, - &sp, - ); - }, - err_fn, - )?, + // https://www.opus-codec.org/docs/html_api/group__opusencoder.html#gace941e4ef26ed844879fde342ffbe546 + // https://chromium.googlesource.com/chromium/deps/opus/+/1.1.1/include/opus.h + let encode_len = sample_rate as usize * channels as usize / 100; // 10 ms + INPUT_BUFFER.lock().unwrap().clear(); + let timeout = None; + let stream_config = StreamConfig { + channels, + sample_rate: config.sample_rate(), + buffer_size: BufferSize::Default, }; - stream.play()?; - Ok(( - Box::new(stream), - Arc::new(create_format_msg(sample_rate, channels)), - )) + let stream = device.build_input_stream( + &stream_config, + move |data: &[T], _: &InputCallbackInfo| { + let buffer: Vec = data.iter().map(|s| T::to_sample(*s)).collect(); + let mut lock = INPUT_BUFFER.lock().unwrap(); + lock.extend(buffer); + while lock.len() >= encode_len { + let frame: Vec = lock.drain(0..encode_len).collect(); + send( + &frame, + sample_rate_0, + sample_rate, + channels, + &mut encoder, + &sp, + ); + } + }, + err_fn, + timeout, + )?; + Ok(stream) } } @@ -349,7 +362,6 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { let mut msg_out = Message::new(); msg_out.set_audio_frame(AudioFrame { data: data.into(), - timestamp: get_time(), ..Default::default() }); sp.send(msg_out); @@ -369,7 +381,6 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { let mut msg_out = Message::new(); msg_out.set_audio_frame(AudioFrame { data: data.into(), - timestamp: get_time(), ..Default::default() }); sp.send(msg_out); diff --git a/src/server/connection.rs b/src/server/connection.rs index 5e6fbe50e..44e995a3a 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -3,12 +3,14 @@ use super::{input_service::*, *}; use crate::clipboard_file::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::update_clipboard; +#[cfg(all(target_os = "linux", feature = "linux_headless"))] +#[cfg(not(any(feature = "flatpak", feature = "appimage")))] +use crate::platform::linux_desktop_manager; #[cfg(windows)] use crate::portable_service::client as portable_client; use crate::{ client::{ - new_voice_call_request, new_voice_call_response, start_audio_thread, LatencyController, - MediaData, MediaSender, + new_voice_call_request, new_voice_call_response, start_audio_thread, MediaData, MediaSender, }, common::{get_default_sound_input, set_sound_input}, video_service, @@ -17,6 +19,9 @@ use crate::{ use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; use cidr_utils::cidr::IpCidr; +#[cfg(all(target_os = "linux", feature = "linux_headless"))] +#[cfg(not(any(feature = "flatpak", feature = "appimage")))] +use hbb_common::platform::linux::run_cmds; use hbb_common::{ config::Config, fs, @@ -46,6 +51,9 @@ use std::{ #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use std::collections::HashSet; + pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; lazy_static::lazy_static! { @@ -65,7 +73,9 @@ pub struct ConnInner { } enum MessageInput { + #[cfg(not(any(target_os = "android", target_os = "ios")))] Mouse((MouseEvent, i32)), + #[cfg(not(any(target_os = "android", target_os = "ios")))] Key((KeyEvent, bool)), BlockOn, BlockOff, @@ -126,10 +136,19 @@ pub struct Connection { #[cfg(windows)] portable: PortableState, from_switch: bool, + #[cfg(not(any(target_os = "android", target_os = "ios")))] origin_resolution: HashMap, voice_call_request_timestamp: Option, audio_input_device_before_voice_call: Option, options_in_login: Option, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pressed_modifiers: HashSet, + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + rx_cm_stream_ready: mpsc::Receiver<()>, + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + tx_desktop_ready: mpsc::Sender<()>, } impl ConnInner { @@ -188,9 +207,14 @@ impl Connection { let (tx_to_cm, rx_to_cm) = mpsc::unbounded_channel::(); let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); - let (tx_input, rx_input) = std_mpsc::channel(); + let (tx_input, _rx_input) = std_mpsc::channel(); let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let (_tx_desktop_ready, rx_desktop_ready) = mpsc::channel(1); + #[cfg(not(any(target_os = "android", target_os = "ios")))] let tx_cloned = tx.clone(); let mut conn = Self { inner: ConnInner { @@ -234,15 +258,26 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, + #[cfg(not(any(target_os = "android", target_os = "ios")))] origin_resolution: Default::default(), audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, options_in_login: None, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pressed_modifiers: Default::default(), + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + rx_cm_stream_ready: _rx_cm_stream_ready, + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + tx_desktop_ready: _tx_desktop_ready, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { - if let Err(err) = start_ipc(rx_to_cm, tx_from_cm).await { + if let Err(err) = + start_ipc(rx_to_cm, tx_from_cm, rx_desktop_ready, tx_cm_stream_ready).await + { log::error!("ipc to connection manager exit: {}", err); } }); @@ -283,7 +318,7 @@ impl Connection { ); #[cfg(not(any(target_os = "android", target_os = "ios")))] - std::thread::spawn(move || Self::handle_input(rx_input, tx_cloned)); + std::thread::spawn(move || Self::handle_input(_rx_input, tx_cloned)); let mut second_timer = time::interval(Duration::from_secs(1)); loop { @@ -529,7 +564,7 @@ impl Connection { let _ = privacy_mode::turn_off_privacy(0); } video_service::notify_video_frame_fetched(id, None); - scrap::codec::Encoder::update_video_encoder(id, scrap::codec::EncoderUpdate::Remove); + scrap::codec::Encoder::update(id, scrap::codec::EncodingUpdate::Remove); video_service::VIDEO_QOS.lock().unwrap().reset(); if conn.authorized { password::update_temporary_password(); @@ -843,17 +878,25 @@ impl Connection { pi.hostname = DEVICE_NAME.lock().unwrap().clone(); pi.platform = "Android".into(); } - #[cfg(feature = "hwcodec")] + #[cfg(target_os = "linux")] { - let (h264, h265) = scrap::codec::Encoder::supported_encoding(); - pi.encoding = Some(SupportedEncoding { - h264, - h265, - ..Default::default() - }) - .into(); + let mut platform_additions = serde_json::Map::new(); + if crate::platform::current_is_wayland() { + platform_additions.insert("is_wayland".into(), json!(true)); + } + #[cfg(feature = "linux_headless")] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + if linux_desktop_manager::is_headless() { + platform_additions.insert("headless".into(), json!(true)); + } + if !platform_additions.is_empty() { + pi.platform_additions = + serde_json::to_string(&platform_additions).unwrap_or("".into()); + } } + pi.encoding = Some(scrap::codec::Encoder::supported_encoding()).into(); + if self.port_forward_socket.is_some() { let mut msg_out = Message::new(); res.set_peer_info(pi); @@ -864,9 +907,11 @@ impl Connection { #[cfg(target_os = "linux")] if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() { let dtype = crate::platform::linux::get_display_server(); - if dtype != "x11" && dtype != "wayland" { + if dtype != crate::platform::linux::DISPLAY_SERVER_X11 + && dtype != crate::platform::linux::DISPLAY_SERVER_WAYLAND + { res.set_error(format!( - "Unsupported display server type {}, x11 or wayland expected", + "Unsupported display server type \"{}\", x11 or wayland expected", dtype )); let mut msg_out = Message::new(); @@ -1031,11 +1076,13 @@ impl Connection { } #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn input_mouse(&self, msg: MouseEvent, conn_id: i32) { self.tx_input.send(MessageInput::Mouse((msg, conn_id))).ok(); } #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn input_key(&self, msg: KeyEvent, press: bool) { self.tx_input.send(MessageInput::Key((msg, press))).ok(); } @@ -1126,21 +1173,21 @@ impl Connection { self.lr = lr.clone(); if let Some(o) = lr.option.as_ref() { self.options_in_login = Some(o.clone()); - if let Some(q) = o.video_codec_state.clone().take() { - scrap::codec::Encoder::update_video_encoder( + if let Some(q) = o.supported_decoding.clone().take() { + scrap::codec::Encoder::update( self.inner.id(), - scrap::codec::EncoderUpdate::State(q), + scrap::codec::EncodingUpdate::New(q), ); } else { - scrap::codec::Encoder::update_video_encoder( + scrap::codec::Encoder::update( self.inner.id(), - scrap::codec::EncoderUpdate::DisableHwIfNotExist, + scrap::codec::EncodingUpdate::NewOnlyVP9, ); } } else { - scrap::codec::Encoder::update_video_encoder( + scrap::codec::Encoder::update( self.inner.id(), - scrap::codec::EncoderUpdate::DisableHwIfNotExist, + scrap::codec::EncodingUpdate::NewOnlyVP9, ); } self.video_ack_required = lr.video_ack_required; @@ -1204,8 +1251,35 @@ impl Connection { } _ => {} } + + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + let desktop_err = match lr.os_login.as_ref() { + Some(os_login) => { + linux_desktop_manager::try_start_desktop(&os_login.username, &os_login.password) + } + None => linux_desktop_manager::try_start_desktop("", ""), + }; + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + let is_headless = linux_desktop_manager::is_headless(); + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + let wait_ipc_timeout = 10_000; + + // If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password. + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + if !desktop_err.is_empty() + && desktop_err != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY + { + self.send_login_error(desktop_err).await; + return true; + } + if !hbb_common::is_ipv4_str(&lr.username) && lr.username != Config::get_id() { - self.send_login_error("Offline").await; + self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) + .await; } else if password::approve_mode() == ApproveMode::Click || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { @@ -1213,7 +1287,8 @@ impl Connection { if hbb_common::get_version_number(&lr.version) >= hbb_common::get_version_number("1.2.0") { - self.send_login_error("No Password Access").await; + self.send_login_error(crate::client::LOGIN_MSG_NO_PASSWORD_ACCESS) + .await; } return true; } else if password::approve_mode() == ApproveMode::Password @@ -1222,12 +1297,42 @@ impl Connection { self.send_login_error("Connection not allowed").await; return false; } else if self.is_recent_session() { - self.try_start_cm(lr.my_id, lr.my_name, true); - self.send_logon_response().await; - if self.port_forward_socket.is_some() { - return false; + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + if desktop_err.is_empty() { + #[cfg(target_os = "linux")] + if is_headless { + self.tx_desktop_ready.send(()).await.ok(); + let _res = timeout(wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await; + } + self.try_start_cm(lr.my_id, lr.my_name, true); + self.send_logon_response().await; + if self.port_forward_socket.is_some() { + return false; + } + } else { + self.send_login_error(desktop_err).await; + } + #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] + { + self.try_start_cm(lr.my_id, lr.my_name, true); + self.send_logon_response().await; + if self.port_forward_socket.is_some() { + return false; + } } } else if lr.password.is_empty() { + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + if desktop_err.is_empty() { + self.try_start_cm(lr.my_id, lr.my_name, false); + } else { + self.send_login_error( + crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY, + ) + .await; + } + #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] self.try_start_cm(lr.my_id, lr.my_name, false); } else { let mut failure = LOGIN_FAILURES @@ -1269,16 +1374,52 @@ impl Connection { .lock() .unwrap() .insert(self.ip.clone(), failure); - self.send_login_error("Wrong Password").await; - self.try_start_cm(lr.my_id, lr.my_name, false); + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + if desktop_err.is_empty() { + self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) + .await; + self.try_start_cm(lr.my_id, lr.my_name, false); + } else { + self.send_login_error( + crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG, + ) + .await; + } + #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] + { + self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) + .await; + self.try_start_cm(lr.my_id, lr.my_name, false); + } } else { if failure.0 != 0 { LOGIN_FAILURES.lock().unwrap().remove(&self.ip); } - self.try_start_cm(lr.my_id, lr.my_name, true); - self.send_logon_response().await; - if self.port_forward_socket.is_some() { - return false; + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + if desktop_err.is_empty() { + #[cfg(target_os = "linux")] + if is_headless { + self.tx_desktop_ready.send(()).await.ok(); + let _res = + timeout(wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await; + } + self.send_logon_response().await; + self.try_start_cm(lr.my_id, lr.my_name, true); + if self.port_forward_socket.is_some() { + return false; + } + } else { + self.send_login_error(desktop_err).await; + } + #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] + { + self.send_logon_response().await; + self.try_start_cm(lr.my_id, lr.my_name, true); + if self.port_forward_socket.is_some() { + return false; + } } } } @@ -1331,8 +1472,10 @@ impl Connection { self.input_mouse(me, self.inner.id()); } } + #[cfg(any(target_os = "android", target_os = "ios"))] + Some(message::Union::KeyEvent(..)) => {} + #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(message::Union::KeyEvent(me)) => { - #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.peer_keyboard_enabled() { if is_enter(&me) { CLICK_TIME.store(get_time(), Ordering::SeqCst); @@ -1345,6 +1488,30 @@ impl Connection { } else { me.press }; + + let key = match me.mode.enum_value_or_default() { + KeyboardMode::Map => { + Some(crate::keyboard::keycode_to_rdev_key(me.chr())) + } + KeyboardMode::Translate => { + if let Some(key_event::Union::Chr(code)) = me.union { + Some(crate::keyboard::keycode_to_rdev_key(code & 0x0000FFFF)) + } else { + None + } + } + _ => None, + } + .filter(crate::keyboard::is_modifier); + + if let Some(key) = key { + if is_press { + self.pressed_modifiers.insert(key); + } else { + self.pressed_modifiers.remove(&key); + } + } + if is_press { match me.union { Some(key_event::Union::Unicode(_)) @@ -1360,11 +1527,11 @@ impl Connection { } } } - Some(message::Union::Clipboard(cb)) => + Some(message::Union::Clipboard(_cb)) => { #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.clipboard { - update_clipboard(cb, None); + update_clipboard(_cb, None); } } Some(message::Union::Cliprdr(_clip)) => { @@ -1601,11 +1768,7 @@ impl Connection { if !self.disable_audio { // Drop the audio sender previously. drop(std::mem::replace(&mut self.audio_sender, None)); - // Start a audio thread to play the audio sent by peer. - let latency_controller = LatencyController::new(); - // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. - latency_controller.lock().unwrap().set_audio_only(true); - self.audio_sender = Some(start_audio_thread(Some(latency_controller))); + self.audio_sender = Some(start_audio_thread()); allow_err!(self .audio_sender .as_ref() @@ -1647,12 +1810,19 @@ impl Connection { } } } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::PluginRequest(p)) => { + if let Some(msg) = crate::plugin::handle_client_event(&p.id, &p.content) { + self.send(msg).await; + } + } _ => {} }, Some(message::Union::AudioFrame(frame)) => { if !self.disable_audio { if let Some(sender) = &self.audio_sender { - allow_err!(sender.send(MediaData::AudioFrame(frame))); + allow_err!(sender.send(MediaData::AudioFrame(Box::new(frame)))); } else { log::warn!( "Processing audio frame without the voice call audio sender." @@ -1741,11 +1911,8 @@ impl Connection { .unwrap() .update_user_fps(o.custom_fps as _); } - if let Some(q) = o.video_codec_state.clone().take() { - scrap::codec::Encoder::update_video_encoder( - self.inner.id(), - scrap::codec::EncoderUpdate::State(q), - ); + if let Some(q) = o.supported_decoding.clone().take() { + scrap::codec::Encoder::update(self.inner.id(), scrap::codec::EncodingUpdate::New(q)); } if let Ok(q) = o.lock_after_session_end.enum_value() { if q != BoolOption::NotSet { @@ -2006,6 +2173,14 @@ impl Connection { }) .count(); } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn release_pressed_modifiers(&mut self) { + for modifier in self.pressed_modifiers.iter() { + rdev::simulate(&rdev::EventType::KeyRelease(*modifier)).ok(); + } + self.pressed_modifiers.clear(); + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -2019,6 +2194,8 @@ pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { async fn start_ipc( mut rx_to_cm: mpsc::UnboundedReceiver, tx_from_cm: mpsc::UnboundedSender, + mut _rx_desktop_ready: mpsc::Receiver<()>, + tx_stream_ready: mpsc::Sender<()>, ) -> ResultType<()> { loop { if !crate::platform::is_prelogin() { @@ -2034,6 +2211,39 @@ async fn start_ipc( if password::hide_cm() { args.push("--hide"); }; + + #[cfg(target_os = "linux")] + #[cfg(not(feature = "linux_headless"))] + let user = None; + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(any(feature = "flatpak", feature = "appimage"))] + let user = None; + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + let mut user = None; + // Cm run as user, wait until desktop session is ready. + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + #[cfg(not(any(feature = "flatpak", feature = "appimage")))] + if linux_desktop_manager::is_headless() { + let mut username = linux_desktop_manager::get_username(); + loop { + if !username.is_empty() { + break; + } + let _res = timeout(1_000, _rx_desktop_ready.recv()).await; + username = linux_desktop_manager::get_username(); + } + let uid = { + let output = run_cmds(&format!("id -u {}", &username))?; + let output = output.trim(); + if output.is_empty() || !output.parse::().is_ok() { + bail!("Invalid username {}", &username); + } + output.to_string() + }; + user = Some((uid, username)); + args = vec!["--cm-no-ui"]; + } let run_done; if crate::platform::is_root() { let mut res = Ok(None); @@ -2046,7 +2256,7 @@ async fn start_ipc( #[cfg(target_os = "linux")] { log::debug!("Start cm"); - res = crate::platform::run_as_user(args.clone(), None); + res = crate::platform::run_as_user(args.clone(), user.clone()); } if res.is_ok() { break; @@ -2078,6 +2288,8 @@ async fn start_ipc( bail!("Failed to connect to connection manager"); } } + + let _res = tx_stream_ready.send(()).await; let mut stream = stream.unwrap(); loop { tokio::select! { @@ -2135,13 +2347,13 @@ fn try_activate_screen() { mod privacy_mode { use super::*; + #[cfg(windows)] + use crate::privacy_win_mag; pub(super) fn turn_off_privacy(_conn_id: i32) -> Message { #[cfg(windows)] { - use crate::win_privacy::*; - - let res = turn_off_privacy(_conn_id, None); + let res = privacy_win_mag::turn_off_privacy(_conn_id, None); match res { Ok(_) => crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOffSucceeded, @@ -2163,7 +2375,7 @@ mod privacy_mode { pub(super) fn turn_on_privacy(_conn_id: i32) -> ResultType { #[cfg(windows)] { - let plugin_exist = crate::win_privacy::turn_on_privacy(_conn_id)?; + let plugin_exist = privacy_win_mag::turn_on_privacy(_conn_id)?; Ok(plugin_exist) } #[cfg(not(windows))] @@ -2203,3 +2415,10 @@ impl Default for PortableState { } } } + +impl Drop for Connection { + fn drop(&mut self) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.release_pressed_modifiers(); + } +} diff --git a/src/server/dbus.rs b/src/server/dbus.rs index 081db3e8f..b01b11c9d 100644 --- a/src/server/dbus.rs +++ b/src/server/dbus.rs @@ -71,22 +71,18 @@ fn handle_client_message(builder: &mut IfaceBuilder<()>) { move |_, _, (_uni_links,): (String,)| { #[cfg(feature = "flutter")] { - use crate::flutter::{self, APP_TYPE_MAIN}; - - if let Some(stream) = flutter::GLOBAL_EVENT_STREAM - .write() - .unwrap() - .get(APP_TYPE_MAIN) - { - let data = HashMap::from([ - ("name", "new_connection"), - ("uni_links", _uni_links.as_str()), - ]); - if !stream.add(serde_json::ser::to_string(&data).unwrap_or("".to_string())) { - log::error!("failed to add dbus message to flutter global dbus stream."); + use crate::flutter; + let data = HashMap::from([ + ("name", "new_connection"), + ("uni_links", _uni_links.as_str()), + ]); + let event = serde_json::ser::to_string(&data).unwrap_or("".to_string()); + match crate::flutter::push_global_event(flutter::APP_TYPE_MAIN, event) { + None => log::error!("failed to find main event stream"), + Some(false) => { + log::error!("failed to add dbus message to flutter global dbus stream.") } - } else { - log::error!("failed to find main event stream"); + Some(true) => {} } } return Ok((DBUS_METHOD_RETURN_SUCCESS.to_string(),)); diff --git a/src/server/input_service.rs b/src/server/input_service.rs index bb9ba167c..41423d436 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -5,16 +5,15 @@ use crate::common::IS_X11; use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; use hbb_common::{config::COMPRESS_LEVEL, get_time, protobuf::EnumOrUnknown}; -use rdev::{self, EventType, Key as RdevKey, RawKey}; +use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey}; #[cfg(target_os = "macos")] use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; -use std::time::Duration; use std::{ convert::TryFrom, ops::Sub, sync::atomic::{AtomicBool, Ordering}, thread, - time::{self, Instant}, + time::{self, Duration, Instant}, }; const INVALID_CURSOR_POS: i32 = i32::MIN; @@ -113,6 +112,119 @@ impl Subscriber for MouseCursorSub { } } +#[cfg(any(target_os = "windows", target_os = "linux"))] +struct LockModesHandler { + caps_lock_changed: bool, + num_lock_changed: bool, +} + +#[cfg(target_os = "macos")] +struct LockModesHandler; + +impl LockModesHandler { + #[inline] + fn is_modifier_enabled(key_event: &KeyEvent, modifier: ControlKey) -> bool { + key_event.modifiers.contains(&modifier.into()) + } + + #[inline] + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + fn new_handler(key_event: &KeyEvent, _is_numpad_key: bool) -> Self { + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + Self::new(key_event, _is_numpad_key) + } + #[cfg(target_os = "macos")] + { + Self::new(key_event) + } + } + + #[cfg(any(target_os = "windows", target_os = "linux"))] + fn new(key_event: &KeyEvent, is_numpad_key: bool) -> Self { + let mut en = ENIGO.lock().unwrap(); + let event_caps_enabled = Self::is_modifier_enabled(key_event, ControlKey::CapsLock); + let local_caps_enabled = en.get_key_state(enigo::Key::CapsLock); + let caps_lock_changed = event_caps_enabled != local_caps_enabled; + if caps_lock_changed { + en.key_click(enigo::Key::CapsLock); + } + + let mut num_lock_changed = false; + if is_numpad_key { + let local_num_enabled = en.get_key_state(enigo::Key::NumLock); + let event_num_enabled = Self::is_modifier_enabled(key_event, ControlKey::NumLock); + num_lock_changed = event_num_enabled != local_num_enabled; + } else if is_legacy_mode(key_event) { + #[cfg(target_os = "windows")] + { + num_lock_changed = + should_disable_numlock(key_event) && en.get_key_state(enigo::Key::NumLock); + } + } + if num_lock_changed { + en.key_click(enigo::Key::NumLock); + } + + Self { + caps_lock_changed, + num_lock_changed, + } + } + + #[cfg(target_os = "macos")] + fn new(key_event: &KeyEvent) -> Self { + let event_caps_enabled = Self::is_modifier_enabled(key_event, ControlKey::CapsLock); + // Do not use the following code to detect `local_caps_enabled`. + // Because the state of get_key_state will not affect simuation of `VIRTUAL_INPUT_STATE` in this file. + // + // let local_caps_enabled = VirtualInput::get_key_state( + // CGEventSourceStateID::CombinedSessionState, + // rdev::kVK_CapsLock, + // ); + let local_caps_enabled = unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + VIRTUAL_INPUT_STATE + .as_ref() + .map_or(false, |input| input.capslock_down) + }; + if event_caps_enabled && !local_caps_enabled { + press_capslock(); + } else if !event_caps_enabled && local_caps_enabled { + release_capslock(); + } + + Self {} + } +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +impl Drop for LockModesHandler { + fn drop(&mut self) { + let mut en = ENIGO.lock().unwrap(); + if self.caps_lock_changed { + en.key_click(enigo::Key::CapsLock); + } + if self.num_lock_changed { + en.key_click(enigo::Key::NumLock); + } + } +} + +#[inline] +#[cfg(target_os = "windows")] +fn should_disable_numlock(evt: &KeyEvent) -> bool { + // disable numlock if press home etc when numlock is on, + // because we will get numpad value (7,8,9 etc) if not + match (&evt.union, evt.mode.enum_value_or(KeyboardMode::Legacy)) { + (Some(key_event::Union::ControlKey(ck)), KeyboardMode::Legacy) => { + return NUMPAD_KEY_MAP.contains_key(&ck.value()); + } + _ => {} + } + false +} + pub const NAME_CURSOR: &'static str = "mouse_cursor"; pub const NAME_POS: &'static str = "mouse_pos"; pub type MouseCursorService = ServiceTmpl; @@ -268,10 +380,33 @@ lazy_static::lazy_static! { static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); } +#[cfg(target_os = "macos")] +struct VirtualInputState { + virtual_input: VirtualInput, + capslock_down: bool, +} + +#[cfg(target_os = "macos")] +impl VirtualInputState { + fn new() -> Option { + VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session) + .map(|virtual_input| Self { + virtual_input, + capslock_down: false, + }) + .ok() + } + + #[inline] + fn simulate(&self, event_type: &EventType) -> ResultType<()> { + Ok(self.virtual_input.simulate(&event_type)?) + } +} + #[cfg(target_os = "macos")] static mut VIRTUAL_INPUT_MTX: Mutex<()> = Mutex::new(()); #[cfg(target_os = "macos")] -static mut VIRTUAL_INPUT: Option = None; +static mut VIRTUAL_INPUT_STATE: Option = None; // First call set_uinput() will create keyboard and mouse clients. // The clients are ipc connections that must live shorter than tokio runtime. @@ -345,6 +480,12 @@ fn is_pressed(key: &Key, en: &mut Enigo) -> bool { get_modifier_state(key.clone(), en) } +#[inline] +#[cfg(target_os = "macos")] +fn key_sleep() { + std::thread::sleep(Duration::from_millis(20)); +} + #[inline] fn get_modifier_state(key: Key, en: &mut Enigo) -> bool { // https://github.com/rustdesk/rustdesk/issues/332 @@ -413,6 +554,7 @@ pub fn fix_key_down_timeout_at_exit() { log::info!("fix_key_down_timeout_at_exit"); } +#[inline] #[cfg(target_os = "linux")] pub fn clear_remapped_keycode() { ENIGO.lock().unwrap().tfc_clear_remapped(); @@ -440,6 +582,24 @@ fn record_key_to_key(record_key: u64) -> Option { } } +pub fn release_device_modifiers() { + let mut en = ENIGO.lock().unwrap(); + for modifier in [ + Key::Shift, + Key::Control, + Key::Alt, + Key::Meta, + Key::RightShift, + Key::RightControl, + Key::RightAlt, + Key::RWin, + ] { + if get_modifier_state(modifier, &mut en) { + en.key_up(modifier); + } + } +} + #[inline] fn release_record_key(record_key: KeysDown) { let func = move || match record_key { @@ -746,7 +906,7 @@ pub fn handle_key(evt: &KeyEvent) { // having GUI, run main GUI thread, otherwise crash let evt = evt.clone(); QUEUE.exec_async(move || handle_key_(&evt)); - std::thread::sleep(Duration::from_millis(20)); + key_sleep(); return; } #[cfg(windows)] @@ -754,7 +914,7 @@ pub fn handle_key(evt: &KeyEvent) { #[cfg(not(windows))] handle_key_(evt); #[cfg(target_os = "macos")] - std::thread::sleep(Duration::from_millis(20)); + key_sleep(); } #[cfg(target_os = "macos")] @@ -762,8 +922,7 @@ pub fn handle_key(evt: &KeyEvent) { fn reset_input() { unsafe { let _lock = VIRTUAL_INPUT_MTX.lock(); - VIRTUAL_INPUT = - VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session).ok(); + VIRTUAL_INPUT_STATE = VirtualInputState::new(); } } @@ -776,7 +935,7 @@ pub fn reset_input_ondisconn() { } } -fn sim_rdev_rawkey_position(code: u32, keydown: bool) { +fn sim_rdev_rawkey_position(code: KeyCode, keydown: bool) { #[cfg(target_os = "windows")] let rawkey = RawKey::ScanCode(code); #[cfg(target_os = "linux")] @@ -810,13 +969,43 @@ fn sim_rdev_rawkey_virtual(code: u32, keydown: bool) { simulate_(&event_type); } -#[cfg(target_os = "macos")] #[inline] +#[cfg(target_os = "macos")] fn simulate_(event_type: &EventType) { unsafe { let _lock = VIRTUAL_INPUT_MTX.lock(); - if let Some(virtual_input) = &VIRTUAL_INPUT { - let _ = virtual_input.simulate(&event_type); + if let Some(input) = &VIRTUAL_INPUT_STATE { + let _ = input.simulate(&event_type); + } + } +} + +#[inline] +#[cfg(target_os = "macos")] +fn press_capslock() { + let caps_key = RdevKey::RawKey(rdev::RawKey::MacVirtualKeycode(rdev::kVK_CapsLock)); + unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + if let Some(input) = &mut VIRTUAL_INPUT_STATE { + if input.simulate(&EventType::KeyPress(caps_key)).is_ok() { + input.capslock_down = true; + key_sleep(); + } + } + } +} + +#[cfg(target_os = "macos")] +#[inline] +fn release_capslock() { + let caps_key = RdevKey::RawKey(rdev::RawKey::MacVirtualKeycode(rdev::kVK_CapsLock)); + unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + if let Some(input) = &mut VIRTUAL_INPUT_STATE { + if input.simulate(&EventType::KeyRelease(caps_key)).is_ok() { + input.capslock_down = false; + key_sleep(); + } } } } @@ -832,14 +1021,6 @@ fn simulate_(event_type: &EventType) { } } -fn is_modifier_in_key_event(control_key: ControlKey, key_event: &KeyEvent) -> bool { - key_event - .modifiers - .iter() - .position(|&m| m == control_key.into()) - .is_some() -} - #[inline] fn control_key_value_to_key(value: i32) -> Option { KEY_MAP.get(&value).and_then(|k| Some(*k)) @@ -850,87 +1031,6 @@ fn char_value_to_key(value: u32) -> Key { Key::Layout(std::char::from_u32(value).unwrap_or('\0')) } -fn is_not_same_status(client_locking: bool, remote_locking: bool) -> bool { - client_locking != remote_locking -} - -#[cfg(target_os = "windows")] -fn has_numpad_key(key_event: &KeyEvent) -> bool { - key_event - .modifiers - .iter() - .filter(|&&ck| NUMPAD_KEY_MAP.get(&ck.value()).is_some()) - .count() - != 0 -} - -#[cfg(target_os = "windows")] -fn is_rdev_numpad_key(key_event: &KeyEvent) -> bool { - let code = key_event.chr(); - let key = rdev::get_win_key(code, 0); - match key { - RdevKey::Home - | RdevKey::UpArrow - | RdevKey::PageUp - | RdevKey::LeftArrow - | RdevKey::RightArrow - | RdevKey::End - | RdevKey::DownArrow - | RdevKey::PageDown - | RdevKey::Insert - | RdevKey::Delete => true, - _ => false, - } -} - -#[cfg(target_os = "windows")] -fn is_numlock_disabled(key_event: &KeyEvent) -> bool { - // disable numlock if press home etc when numlock is on, - // because we will get numpad value (7,8,9 etc) if not - match key_event.mode.unwrap() { - KeyboardMode::Map => is_rdev_numpad_key(key_event), - _ => has_numpad_key(key_event), - } -} - -fn click_capslock(en: &mut Enigo) { - #[cfg(not(targe_os = "macos"))] - en.key_click(enigo::Key::CapsLock); - #[cfg(target_os = "macos")] - let _ = en.key_down(enigo::Key::CapsLock); -} - -fn click_numlock(_en: &mut Enigo) { - // without numlock in macos - #[cfg(not(target_os = "macos"))] - _en.key_click(enigo::Key::NumLock); -} - -fn sync_numlock_capslock_status(key_event: &KeyEvent) { - let mut en = ENIGO.lock().unwrap(); - - let client_caps_locking = is_modifier_in_key_event(ControlKey::CapsLock, key_event); - let client_num_locking = is_modifier_in_key_event(ControlKey::NumLock, key_event); - let remote_caps_locking = en.get_key_state(enigo::Key::CapsLock); - let remote_num_locking = en.get_key_state(enigo::Key::NumLock); - - let need_click_capslock = is_not_same_status(client_caps_locking, remote_caps_locking); - let need_click_numlock = is_not_same_status(client_num_locking, remote_num_locking); - - #[cfg(not(target_os = "windows"))] - let disable_numlock = false; - #[cfg(target_os = "windows")] - let disable_numlock = is_numlock_disabled(key_event); - - if need_click_capslock { - click_capslock(&mut en); - } - - if need_click_numlock && !disable_numlock { - click_numlock(&mut en); - } -} - fn map_keyboard_mode(evt: &KeyEvent) { #[cfg(windows)] crate::platform::windows::try_change_desktop(); @@ -949,7 +1049,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { return; } - sim_rdev_rawkey_position(evt.chr(), evt.down); + sim_rdev_rawkey_position(evt.chr() as _, evt.down); } #[cfg(target_os = "macos")] @@ -1085,7 +1185,9 @@ fn is_function_key(ck: &EnumOrUnknown) -> bool { }); res = true; } else if ck.value() == ControlKey::LockScreen.value() { - lock_screen_2(); + std::thread::spawn(|| { + lock_screen_2(); + }); res = true; } return res; @@ -1127,7 +1229,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { fn translate_process_code(code: u32, down: bool) { crate::platform::windows::try_change_desktop(); match code >> 16 { - 0 => sim_rdev_rawkey_position(code, down), + 0 => sim_rdev_rawkey_position(code as _, down), vk_code => sim_rdev_rawkey_virtual(vk_code, down), }; } @@ -1143,40 +1245,169 @@ fn translate_keyboard_mode(evt: &KeyEvent) { // remote: Shift + 1 => 1 let mut en = ENIGO.lock().unwrap(); - #[cfg(target_os = "linux")] + #[cfg(target_os = "macos")] + en.key_sequence(seq); + #[cfg(any(target_os = "linux", target_os = "windows"))] { - simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); - simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + if get_modifier_state(Key::Shift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); + } + if get_modifier_state(Key::RightShift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + } for chr in seq.chars() { + #[cfg(target_os = "windows")] + rdev::simulate_char(chr, true).ok(); + #[cfg(target_os = "linux")] en.key_click(Key::Layout(chr)); } } - #[cfg(not(target_os = "linux"))] - en.key_sequence(seq); } Some(key_event::Union::Chr(..)) => { #[cfg(target_os = "windows")] translate_process_code(evt.chr(), evt.down); #[cfg(not(target_os = "windows"))] - sim_rdev_rawkey_position(evt.chr(), evt.down); + sim_rdev_rawkey_position(evt.chr() as _, evt.down); } Some(key_event::Union::Unicode(..)) => { // Do not handle unicode for now. } + #[cfg(target_os = "windows")] + Some(key_event::Union::Win2winHotkey(code)) => { + simulate_win2win_hotkey(*code, evt.down); + } _ => { log::debug!("Unreachable. Unexpected key event {:?}", &evt); } } } +#[cfg(target_os = "windows")] +fn simulate_win2win_hotkey(code: u32, down: bool) { + let unicode: u16 = (code & 0x0000FFFF) as u16; + if down { + if rdev::simulate_key_unicode(unicode, false).is_ok() { + return; + } + } + + let keycode: u16 = ((code >> 16) & 0x0000FFFF) as u16; + let scan = rdev::vk_to_scancode(keycode as _); + allow_err!(rdev::simulate_code(None, Some(scan), down)); +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +fn skip_led_sync_control_key(_key: &ControlKey) -> bool { + false +} + +// LockModesHandler should not be created when single meta is pressing and releasing. +// Because the drop function may insert "CapsLock Click" and "NumLock Click", which breaks single meta click. +// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1496936687 +// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1500415822 +// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1500773473 +#[cfg(any(target_os = "windows", target_os = "linux"))] +fn skip_led_sync_control_key(key: &ControlKey) -> bool { + matches!( + key, + ControlKey::Control + | ControlKey::RControl + | ControlKey::Meta + | ControlKey::Shift + | ControlKey::RShift + | ControlKey::Alt + | ControlKey::RAlt + | ControlKey::Tab + | ControlKey::Return + ) +} + +#[inline] +#[cfg(any(target_os = "windows", target_os = "linux"))] +fn is_numpad_control_key(key: &ControlKey) -> bool { + matches!( + key, + ControlKey::Numpad0 + | ControlKey::Numpad1 + | ControlKey::Numpad2 + | ControlKey::Numpad3 + | ControlKey::Numpad4 + | ControlKey::Numpad5 + | ControlKey::Numpad6 + | ControlKey::Numpad7 + | ControlKey::Numpad8 + | ControlKey::Numpad9 + | ControlKey::NumpadEnter + ) +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +fn skip_led_sync_rdev_key(_key: &RdevKey) -> bool { + false +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +fn skip_led_sync_rdev_key(key: &RdevKey) -> bool { + matches!( + key, + RdevKey::ControlLeft + | RdevKey::ControlRight + | RdevKey::MetaLeft + | RdevKey::MetaRight + | RdevKey::ShiftLeft + | RdevKey::ShiftRight + | RdevKey::Alt + | RdevKey::AltGr + | RdevKey::Tab + | RdevKey::Return + ) +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn is_legacy_mode(evt: &KeyEvent) -> bool { + evt.mode.enum_value_or(KeyboardMode::Legacy) == KeyboardMode::Legacy +} + pub fn handle_key_(evt: &KeyEvent) { if EXITING.load(Ordering::SeqCst) { return; } - if evt.down { - sync_numlock_capslock_status(evt) - } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let mut _lock_mode_handler = None; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + match &evt.union { + Some(key_event::Union::Unicode(..)) | Some(key_event::Union::Seq(..)) => { + _lock_mode_handler = Some(LockModesHandler::new_handler(&evt, false)); + } + Some(key_event::Union::ControlKey(ck)) => { + let key = ck.enum_value_or(ControlKey::Unknown); + if !skip_led_sync_control_key(&key) { + #[cfg(target_os = "macos")] + let is_numpad_key = false; + #[cfg(any(target_os = "windows", target_os = "linux"))] + let is_numpad_key = is_numpad_control_key(&key); + _lock_mode_handler = Some(LockModesHandler::new_handler(&evt, is_numpad_key)); + } + } + Some(key_event::Union::Chr(code)) => { + if is_legacy_mode(&evt) { + _lock_mode_handler = Some(LockModesHandler::new_handler(evt, false)); + } else { + let key = crate::keyboard::keycode_to_rdev_key(*code); + if !skip_led_sync_rdev_key(&key) { + #[cfg(target_os = "macos")] + let is_numpad_key = false; + #[cfg(any(target_os = "windows", target_os = "linux"))] + let is_numpad_key = crate::keyboard::is_numpad_rdev_key(&key); + _lock_mode_handler = Some(LockModesHandler::new_handler(evt, is_numpad_key)); + } + } + } + _ => {} + }; + match evt.mode.unwrap() { KeyboardMode::Map => { map_keyboard_mode(evt); diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index d9acc4152..6cff2b9dd 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -479,12 +479,11 @@ pub mod client { )?); shutdown_hooks::add_shutdown_hook(drop_portable_service_shared_memory); } - let mut option = SHMEM.lock().unwrap(); - let shmem = option.as_mut().unwrap(); - unsafe { - libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + if let Some(shmem) = SHMEM.lock().unwrap().as_mut() { + unsafe { + libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + } } - drop(option); match para { StartPara::Direct => { if let Err(e) = crate::platform::run_background( @@ -544,8 +543,11 @@ pub mod client { } pub extern "C" fn drop_portable_service_shared_memory() { - log::info!("drop shared memory"); - *SHMEM.lock().unwrap() = None; + let mut lock = SHMEM.lock().unwrap(); + if lock.is_some() { + *lock = None; + log::info!("drop shared memory"); + } } pub fn set_quick_support(v: bool) { @@ -560,17 +562,18 @@ pub mod client { Self: Sized, { let mut option = SHMEM.lock().unwrap(); - let shmem = option.as_mut().unwrap(); - Self::set_para( - shmem, - CapturerPara { - current_display, - use_yuv, - use_yuv_set: false, - timeout_ms: 33, - }, - ); - shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE)); + if let Some(shmem) = option.as_mut() { + Self::set_para( + shmem, + CapturerPara { + current_display, + use_yuv, + use_yuv_set: false, + timeout_ms: 33, + }, + ); + shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE)); + } CapturerPortable {} } @@ -587,25 +590,29 @@ pub mod client { impl TraitCapturer for CapturerPortable { fn set_use_yuv(&mut self, use_yuv: bool) { let mut option = SHMEM.lock().unwrap(); - let shmem = option.as_mut().unwrap(); - unsafe { - let para_ptr = shmem.as_ptr().add(ADDR_CAPTURER_PARA); - let para = para_ptr as *const CapturerPara; - Self::set_para( - shmem, - CapturerPara { - current_display: (*para).current_display, - use_yuv, - use_yuv_set: true, - timeout_ms: (*para).timeout_ms, - }, - ); + if let Some(shmem) = option.as_mut() { + unsafe { + let para_ptr = shmem.as_ptr().add(ADDR_CAPTURER_PARA); + let para = para_ptr as *const CapturerPara; + Self::set_para( + shmem, + CapturerPara { + current_display: (*para).current_display, + use_yuv, + use_yuv_set: true, + timeout_ms: (*para).timeout_ms, + }, + ); + } } } fn frame<'a>(&'a mut self, timeout: Duration) -> std::io::Result> { - let mut option = SHMEM.lock().unwrap(); - let shmem = option.as_mut().unwrap(); + let mut lock = SHMEM.lock().unwrap(); + let shmem = lock.as_mut().ok_or(std::io::Error::new( + std::io::ErrorKind::Other, + "shmem dropped".to_string(), + ))?; unsafe { let base = shmem.as_ptr(); let para_ptr = base.add(ADDR_CAPTURER_PARA); @@ -801,7 +808,10 @@ pub mod client { pub fn get_cursor_info(pci: PCURSORINFO) -> BOOL { if RUNNING.lock().unwrap().clone() { - get_cursor_info_(&mut SHMEM.lock().unwrap().as_mut().unwrap(), pci) + let mut option = SHMEM.lock().unwrap(); + option + .as_mut() + .map_or(FALSE, |sheme| get_cursor_info_(sheme, pci)) } else { unsafe { winuser::GetCursorInfo(pci) } } diff --git a/src/server/service.rs b/src/server/service.rs index 9cc1e860c..fe038f3c0 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -221,6 +221,7 @@ impl> ServiceTmpl { thread::sleep(interval - elapsed); } } + log::info!("Service {} exit", sp.name()); }); self.0.write().unwrap().handle = Some(thread); } @@ -256,6 +257,7 @@ impl> ServiceTmpl { } thread::sleep(time::Duration::from_millis(HIBERNATE_TIMEOUT)); } + log::info!("Service {} exit", sp.name()); }); self.0.write().unwrap().handle = Some(thread); } diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index 47bf49707..d53053691 100644 --- a/src/server/video_qos.rs +++ b/src/server/video_qos.rs @@ -1,7 +1,7 @@ use super::*; use std::time::Duration; pub const FPS: u8 = 30; -pub const MIN_FPS: u8 = 10; +pub const MIN_FPS: u8 = 1; pub const MAX_FPS: u8 = 120; trait Percent { fn as_percent(&self) -> u32; @@ -221,7 +221,9 @@ impl VideoQoS { } pub fn reset(&mut self) { - *self = Default::default(); + self.fps = FPS; + self.user_fps = FPS; + self.updated = true; } pub fn check_abr_config(&mut self) -> bool { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index affb5eb17..f7a6625ec 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -19,6 +19,10 @@ // https://slhck.info/video/2017/03/01/rate-control.html use super::{video_qos::VideoQoS, *}; +#[cfg(all(windows, feature = "virtual_display_driver"))] +use crate::virtual_display_manager; +#[cfg(windows)] +use crate::{platform::windows::is_process_consent_running, privacy_win_mag}; #[cfg(windows)] use hbb_common::get_version_number; use hbb_common::tokio::sync::{ @@ -31,7 +35,7 @@ use scrap::{ codec::{Encoder, EncoderCfg, HwEncoderConfig}, record::{Recorder, RecorderContext}, vpxcodec::{VpxEncoderConfig, VpxVideoCodecId}, - Display, TraitCapturer, + CodecName, Display, TraitCapturer, }; #[cfg(windows)] use std::sync::Once; @@ -41,14 +45,6 @@ use std::{ ops::{Deref, DerefMut}, time::{self, Duration, Instant}, }; -#[cfg(windows)] -use virtual_display; - -pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; -pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = - "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."; -pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; -pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; pub const NAME: &'static str = "video"; @@ -208,8 +204,6 @@ fn create_capturer( if privacy_mode_id > 0 { #[cfg(windows)] { - use crate::win_privacy::*; - match scrap::CapturerMag::new( display.origin(), display.width(), @@ -220,7 +214,7 @@ fn create_capturer( let mut ok = false; let check_begin = Instant::now(); while check_begin.elapsed().as_secs() < 5 { - match c1.exclude("", PRIVACY_WINDOW_NAME) { + match c1.exclude("", privacy_win_mag::PRIVACY_WINDOW_NAME) { Ok(false) => { ok = false; std::thread::sleep(std::time::Duration::from_millis(500)); @@ -229,7 +223,7 @@ fn create_capturer( bail!( "Failed to exclude privacy window {} - {}, err: {}", "", - PRIVACY_WINDOW_NAME, + privacy_win_mag::PRIVACY_WINDOW_NAME, e ); } @@ -243,7 +237,7 @@ fn create_capturer( bail!( "Failed to exclude privacy window {} - {} ", "", - PRIVACY_WINDOW_NAME + privacy_win_mag::PRIVACY_WINDOW_NAME ); } log::debug!("Create magnifier capture for {}", privacy_mode_id); @@ -275,18 +269,12 @@ fn create_capturer( }; } -#[cfg(windows)] +// to-do: do not close if in privacy mode. +#[cfg(all(windows, feature = "virtual_display_driver"))] fn ensure_close_virtual_device() -> ResultType<()> { let num_displays = Display::all()?.len(); - if num_displays == 0 { - // Device may sometimes be uninstalled by user in "Device Manager" Window. - // Closing device will clear the instance data. - virtual_display::close_device(); - } else if num_displays > 1 { - // Try close device, if display device changed. - if virtual_display::is_device_created() { - virtual_display::close_device(); - } + if num_displays > 1 { + virtual_display_manager::plug_out_headless(); } Ok(()) } @@ -309,11 +297,11 @@ pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> bool { fn check_uac_switch(privacy_mode_id: i32, capturer_privacy_mode_id: i32) -> ResultType<()> { if capturer_privacy_mode_id != 0 { if privacy_mode_id != capturer_privacy_mode_id { - if !crate::win_privacy::is_process_consent_running()? { + if !is_process_consent_running()? { bail!("consent.exe is running"); } } - if crate::win_privacy::is_process_consent_running()? { + if is_process_consent_running()? { bail!("consent.exe is running"); } } @@ -374,7 +362,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType Option> { } } -fn check_displays_changed() -> Option { +fn check_get_displays_changed_msg() -> Option { let displays = check_displays_new()?; let (current, displays) = get_displays_2(&displays); let mut pi = PeerInfo { @@ -447,7 +435,7 @@ fn check_displays_changed() -> Option { } fn run(sp: GenericService) -> ResultType<()> { - #[cfg(windows)] + #[cfg(all(windows, feature = "virtual_display_driver"))] ensure_close_virtual_device()?; // ensure_inited() is needed because release_resource() may be called. @@ -468,21 +456,29 @@ fn run(sp: GenericService) -> ResultType<()> { drop(video_qos); log::info!("init bitrate={}, abr enabled:{}", bitrate, abr); - let encoder_cfg = match Encoder::current_hw_encoder_name() { - Some(codec_name) => EncoderCfg::HW(HwEncoderConfig { - codec_name, - width: c.width, - height: c.height, - bitrate: bitrate as _, - }), - None => EncoderCfg::VPX(VpxEncoderConfig { - width: c.width as _, - height: c.height as _, - timebase: [1, 1000], // Output timestamp precision - bitrate, - codec: VpxVideoCodecId::VP9, - num_threads: (num_cpus::get() / 2) as _, - }), + let encoder_cfg = match Encoder::negotiated_codec() { + scrap::CodecName::H264(name) | scrap::CodecName::H265(name) => { + EncoderCfg::HW(HwEncoderConfig { + name, + width: c.width, + height: c.height, + bitrate: bitrate as _, + }) + } + name @ (scrap::CodecName::VP8 | scrap::CodecName::VP9) => { + EncoderCfg::VPX(VpxEncoderConfig { + width: c.width as _, + height: c.height as _, + timebase: [1, 1000], // Output timestamp precision + bitrate, + codec: if name == scrap::CodecName::VP8 { + VpxVideoCodecId::VP8 + } else { + VpxVideoCodecId::VP9 + }, + num_threads: (num_cpus::get() / 2) as _, + }) + } }; let mut encoder; @@ -526,7 +522,7 @@ fn run(sp: GenericService) -> ResultType<()> { let mut try_gdi = 1; #[cfg(windows)] log::info!("gdi: {}", c.is_gdi()); - let codec_name = Encoder::current_hw_encoder_name(); + let codec_name = Encoder::negotiated_codec(); let recorder = get_recorder(c.width, c.height, &codec_name); #[cfg(windows)] start_uac_elevation_check(); @@ -539,7 +535,7 @@ fn run(sp: GenericService) -> ResultType<()> { check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?; let mut video_qos = VIDEO_QOS.lock().unwrap(); - if video_qos.check_if_updated() { + if video_qos.check_if_updated() && video_qos.target_bitrate > 0 { log::debug!( "qos is updated, target_bitrate:{}, fps:{}", video_qos.target_bitrate, @@ -557,7 +553,7 @@ fn run(sp: GenericService) -> ResultType<()> { *SWITCH.lock().unwrap() = true; bail!("SWITCH"); } - if codec_name != Encoder::current_hw_encoder_name() { + if codec_name != Encoder::negotiated_codec() { bail!("SWITCH"); } #[cfg(windows)] @@ -585,11 +581,8 @@ fn run(sp: GenericService) -> ResultType<()> { bail!("SWITCH"); } - if let Some(msg_out) = check_displays_changed() { + if let Some(msg_out) = check_get_displays_changed_msg() { sp.send(msg_out); - } - - if c.ndisplay != get_display_num() { log::info!("Displays changed"); *SWITCH.lock().unwrap() = true; bail!("SWITCH"); @@ -606,8 +599,14 @@ fn run(sp: GenericService) -> ResultType<()> { let time = now - start; let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; match frame { + scrap::Frame::VP8(data) => { + let send_conn_ids = + handle_one_frame_encoded(VpxVideoCodecId::VP8, &sp, data, ms)?; + frame_controller.set_send(now, send_conn_ids); + } scrap::Frame::VP9(data) => { - let send_conn_ids = handle_one_frame_encoded(&sp, data, ms)?; + let send_conn_ids = + handle_one_frame_encoded(VpxVideoCodecId::VP9, &sp, data, ms)?; frame_controller.set_send(now, send_conn_ids); } scrap::Frame::RAW(data) => { @@ -720,12 +719,11 @@ fn run(sp: GenericService) -> ResultType<()> { fn get_recorder( width: usize, height: usize, - codec_name: &Option, + codec_name: &CodecName, ) -> Arc>> { #[cfg(not(target_os = "ios"))] let recorder = if !Config::get_option("allow-auto-record-incoming").is_empty() { use crate::hbbs_http::record_upload; - use scrap::record::RecordCodecID::*; let tx = if record_upload::is_enable() { let (tx, rx) = std::sync::mpsc::channel(); @@ -734,16 +732,6 @@ fn get_recorder( } else { None }; - let codec_id = match codec_name { - Some(name) => { - if name.contains("264") { - H264 - } else { - H265 - } - } - None => VP9, - }; Recorder::new(RecorderContext { server: true, id: Config::get_id(), @@ -751,7 +739,7 @@ fn get_recorder( filename: "".to_owned(), width, height, - codec_id, + format: codec_name.into(), tx, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) @@ -778,20 +766,6 @@ fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> Resu Ok(()) } -#[inline] -#[cfg(any(target_os = "android", target_os = "ios"))] -fn create_msg(vp9s: Vec) -> Message { - let mut msg_out = Message::new(); - let mut vf = VideoFrame::new(); - vf.set_vp9s(EncodedVideoFrames { - frames: vp9s.into(), - ..Default::default() - }); - vf.timestamp = hbb_common::get_time(); - msg_out.set_video_frame(vf); - msg_out -} - #[inline] fn handle_one_frame( sp: &GenericService, @@ -824,6 +798,7 @@ fn handle_one_frame( #[inline] #[cfg(any(target_os = "android", target_os = "ios"))] pub fn handle_one_frame_encoded( + codec: VpxVideoCodecId, sp: &GenericService, frame: &[u8], ms: i64, @@ -835,32 +810,16 @@ pub fn handle_one_frame_encoded( } Ok(()) })?; - let mut send_conn_ids: HashSet = Default::default(); - let vp9_frame = EncodedVideoFrame { + let vpx_frame = EncodedVideoFrame { data: frame.to_vec().into(), key: true, pts: ms, ..Default::default() }; - send_conn_ids = sp.send_video_frame(create_msg(vec![vp9_frame])); + let send_conn_ids = sp.send_video_frame(scrap::VpxEncoder::create_msg(codec, vec![vpx_frame])); Ok(send_conn_ids) } -fn get_display_num() -> usize { - #[cfg(target_os = "linux")] - { - if !scrap::is_x11() { - return if let Ok(n) = super::wayland::get_display_num() { - n - } else { - 0 - }; - } - } - - LAST_SYNC_DISPLAYS.read().unwrap().len() -} - pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { let mut displays = Vec::new(); let mut primary = 0; @@ -951,37 +910,24 @@ pub async fn switch_to_primary() { } #[inline] -#[cfg(not(windows))] +#[cfg(not(all(windows, feature = "virtual_display_driver")))] fn try_get_displays() -> ResultType> { Ok(Display::all()?) } -#[cfg(windows)] +#[cfg(all(windows, feature = "virtual_display_driver"))] fn try_get_displays() -> ResultType> { let mut displays = Display::all()?; if displays.len() == 0 { log::debug!("no displays, create virtual display"); - // Try plugin monitor - if !virtual_display::is_device_created() { - if let Err(e) = virtual_display::create_device() { - log::debug!("Create device failed {}", e); - } + if let Err(e) = virtual_display_manager::plug_in_headless() { + log::error!("plug in headless failed {}", e); + } else { + displays = Display::all()?; } - if virtual_display::is_device_created() { - if let Err(e) = virtual_display::plug_in_monitor() { - log::debug!("Plug in monitor failed {}", e); - } else { - if let Err(e) = virtual_display::update_monitor_modes() { - log::debug!("Update monitor modes failed {}", e); - } - } - } - displays = Display::all()?; } else if displays.len() > 1 { // If more than one displays exists, close RustDeskVirtualDisplay - if virtual_display::is_device_created() { - virtual_display::close_device() - } + let _res = virtual_display_manager::plug_in_headless(); } Ok(displays) } @@ -1020,7 +966,7 @@ fn start_uac_elevation_check() { if !crate::platform::is_installed() && !crate::platform::is_root() { std::thread::spawn(|| loop { std::thread::sleep(std::time::Duration::from_secs(1)); - if let Ok(uac) = crate::win_privacy::is_process_consent_running() { + if let Ok(uac) = is_process_consent_running() { *IS_UAC_RUNNING.lock().unwrap() = uac; } if !crate::platform::is_elevated(None).unwrap_or(false) { diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 954f1ed1d..10b93afce 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -3,7 +3,7 @@ use hbb_common::{allow_err, platform::linux::DISTRO}; use scrap::{is_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer}; use std::io; -use super::video_service::{ +use crate::client::{ SCRAP_OTHER_VERSION_OR_X11_REQUIRED, SCRAP_UBUNTU_HIGHER_REQUIRED, SCRAP_X11_REQUIRED, }; @@ -230,19 +230,6 @@ pub(super) fn get_primary() -> ResultType { } } -pub(super) fn get_display_num() -> ResultType { - let addr = *CAP_DISPLAY_INFO.read().unwrap(); - if addr != 0 { - let cap_display_info: *const CapDisplayInfo = addr as _; - unsafe { - let cap_display_info = &*cap_display_info; - Ok(cap_display_info.num) - } - } else { - bail!("Failed to get capturer display info"); - } -} - #[allow(dead_code)] pub(super) fn release_resource() { if scrap::is_x11() { diff --git a/src/ui.rs b/src/ui.rs index f7419cd34..d08e82a36 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -54,6 +54,18 @@ pub fn start(args: &mut [String]) { let dir = "/usr"; sciter::set_library(&(prefix + dir + "/lib/rustdesk/libsciter-gtk.so")).ok(); } + #[cfg(windows)] + // Check if there is a sciter.dll nearby. + if let Ok(exe) = std::env::current_exe() { + if let Some(parent) = exe.parent() { + let sciter_dll_path = parent.join("sciter.dll"); + if sciter_dll_path.exists() { + // Try to set the sciter dll. + let p = sciter_dll_path.to_string_lossy().to_string(); + log::debug!("Found dll:{}, \n {:?}", p, sciter::set_library(&p)); + } + } + } // https://github.com/c-smile/sciter-sdk/blob/master/include/sciter-x-types.h // https://github.com/rustdesk/rustdesk/issues/132#issuecomment-886069737 #[cfg(windows)] @@ -458,6 +470,10 @@ impl UI { get_version() } + fn get_fingerprint(&self) -> String { + get_fingerprint() + } + fn get_app_name(&self) -> String { get_app_name() } @@ -637,6 +653,7 @@ impl sciter::EventHandler for UI { fn get_software_update_url(); fn get_new_version(); fn get_version(); + fn get_fingerprint(); fn update_me(String); fn show_run_without_install(); fn run_without_install(); diff --git a/src/ui/common.css b/src/ui/common.css index 0fb9afcb1..9845ff104 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -142,6 +142,10 @@ div.password input { font-size: 1.2em; } +div.username input { + font-size: 1.2em; +} + svg { background: none; } diff --git a/src/ui/common.tis b/src/ui/common.tis index b6723b131..92e704052 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -252,7 +252,7 @@ function msgbox(type, title, content, link="", callback=null, height=180, width= view.close(); return; } - handler.login(res.password, res.remember); + handler.login("", "", res.password, res.remember); if (!is_port_forward) { // Specially handling file transfer for no permission hanging issue (including 60ms // timer in setPermission. @@ -262,6 +262,30 @@ function msgbox(type, title, content, link="", callback=null, height=180, width= else msgbox("connecting", "Connecting...", "Logging in..."); } }; + } else if (type == "session-login" || type == "session-re-login") { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.login(res.osusername, res.ospassword, "", false); + if (!is_port_forward) { + if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in..."); + else msgbox("connecting", "Connecting...", "Logging in..."); + } + }; + } else if (type.indexOf("session-login") >= 0) { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.login(res.osusername, res.ospassword, res.password, res.remember); + if (!is_port_forward) { + if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in..."); + else msgbox("connecting", "Connecting...", "Logging in..."); + } + }; } else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) { callback = function() { view.close(); } } else if (type == 'wait-remote-accept-nook') { diff --git a/src/ui/header.tis b/src/ui/header.tis index 257ba417e..af4f1e349 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -161,8 +161,8 @@ class Header: Reactor.Component { } function renderDisplayPop() { - var codecs = handler.supported_hwcodec(); - var show_codec = handler.has_hwcodec() && (codecs[0] || codecs[1]); + var codecs = handler.alternative_codecs(); + var show_codec = codecs[0] || codecs[1] || codecs[2]; var cursor_embedded = false; if ((pi.displays || []).length > 0) { @@ -186,9 +186,10 @@ class Header: Reactor.Component { {show_codec ?

  • {svg_checkmark}Auto
  • + {codecs[0] ?
  • {svg_checkmark}VP8
  • : ""}
  • {svg_checkmark}VP9
  • - {codecs[0] ?
  • {svg_checkmark}H264
  • : ""} - {codecs[1] ?
  • {svg_checkmark}H265
  • : ""} + {codecs[1] ?
  • {svg_checkmark}H264
  • : ""} + {codecs[2] ?
  • {svg_checkmark}H265
  • : ""}
    : ""}
    {!cursor_embedded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • } diff --git a/src/ui/index.tis b/src/ui/index.tis index 0e2247070..011b895d2 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -311,7 +311,7 @@ class MyIdMenu: Reactor.Component {
  • {svg_checkmark}{translate("Enable Service")}
  • {handler.is_rdp_service_open() ? : ""} - {false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connected via relay')}
  • } + {false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } {handler.is_ok_change_id() ?
    : ""} {username ?
  • {translate('Logout')} ({username})
  • : @@ -364,6 +364,7 @@ class MyIdMenu: Reactor.Component { var name = handler.get_app_name(); msgbox("custom-nocancel-nook-hasclose", translate("About") + " " + name, "
    \
    Version: " + handler.get_version() + " \ +
    Fingerprint: " + handler.get_fingerprint() + " \
    Privacy Statement
    \
    Website
    \
    Copyright © 2022 Purslane Ltd.\ diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index d5c60d95c..2099a8e7b 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -32,7 +32,7 @@ class MsgboxComponent: Reactor.Component { } function getIcon(color) { - if (this.type == "input-password") { + if (this.type == "input-password" || this.type == "session-login" || this.type == "session-login-password") { return ; } if (this.type == "connecting") { @@ -41,7 +41,7 @@ class MsgboxComponent: Reactor.Component { if (this.type == "success") { return ; } - if (this.type.indexOf("error") >= 0 || this.type == "re-input-password") { + if (this.type.indexOf("error") >= 0 || this.type == "re-input-password" || this.type == "session-re-login" || this.type == "session-login-re-password") { return ; } return null; @@ -56,11 +56,36 @@ class MsgboxComponent: Reactor.Component {
    ; } + function getInputUserPasswordContent() { + return
    +
    {translate("OS Username")}
    +
    +
    {translate("OS Password")}
    + +
    +
    ; + } + + function getXsessionPasswordContent() { + return
    +
    {translate("OS Username")}
    +
    +
    {translate("OS Password")}
    + +
    {translate('Please enter your password')}
    + +
    {translate('Remember password')}
    +
    ; + } + function getContent() { if (this.type == "input-password") { return this.getInputPasswordContent(); - } - if (this.type == "custom-os-password") { + } else if (this.type == "session-login") { + return this.getInputUserPasswordContent(); + } else if (this.type == "session-login-password") { + return this.getXsessionPasswordContent(); + } else if (this.type == "custom-os-password") { var ts = this.auto_login ? { checked: true } : {}; return
    @@ -71,13 +96,13 @@ class MsgboxComponent: Reactor.Component { } function getColor() { - if (this.type == "input-password" || this.type == "custom-os-password") { + if (this.type == "input-password" || this.type == "custom-os-password" || this.type == "session-login" || this.type == "session-login-password") { return "#AD448E"; } if (this.type == "success") { return "#32bea6"; } - if (this.type.indexOf("error") >= 0 || this.type == "re-input-password") { + if (this.type.indexOf("error") >= 0 || this.type == "re-input-password" || this.type == "session-re-login" || this.type == "session-login-re-password") { return "#e04f5f"; } return "#2C8CFF"; @@ -177,6 +202,16 @@ class MsgboxComponent: Reactor.Component { this.update(); return; } + if (this.type == "session-re-login") { + this.type = "session-login"; + this.update(); + return; + } + if (this.type == "session-login-re-password") { + this.type = "session-login-password"; + this.update(); + return; + } var values = this.getValues(); if (this.callback) { var self = this; @@ -238,6 +273,21 @@ class MsgboxComponent: Reactor.Component { return; } } + if (this.type == "session-login") { + values.osusername = (values.osusername || "").trim(); + values.ospassword = (values.ospassword || "").trim(); + if (!values.osusername || !values.ospassword) { + return; + } + } + if (this.type == "session-login-password") { + values.password = (values.password || "").trim(); + values.osusername = (values.osusername || "").trim(); + values.ospassword = (values.ospassword || "").trim(); + if (!values.osusername || !values.ospassword || !values.password) { + return; + } + } return values; } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 68decf955..c161cddf3 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -20,7 +20,6 @@ use hbb_common::{ use crate::{ client::*, - ui_interface::has_hwcodec, ui_session_interface::{InvokeUiSession, Session}, }; @@ -145,6 +144,8 @@ impl InvokeUiSession for SciterHandler { self.call("setConnectionType", &make_args!(is_secured, direct)); } + fn set_fingerprint(&self, _fingerprint: String) {} + fn job_error(&self, id: i32, err: String, file_num: i32) { self.call("jobError", &make_args!(id, err, file_num)); } @@ -197,7 +198,14 @@ impl InvokeUiSession for SciterHandler { self.call("confirmDeleteFiles", &make_args!(id, i, name)); } - fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool, is_identical: bool) { + fn override_file_confirm( + &self, + id: i32, + file_num: i32, + to: String, + is_upload: bool, + is_identical: bool, + ) { self.call( "overrideFileConfirm", &make_args!(id, file_num, to, is_upload, is_identical), @@ -398,7 +406,7 @@ impl sciter::EventHandler for SciterSession { fn is_file_transfer(); fn is_port_forward(); fn is_rdp(); - fn login(String, bool); + fn login(String, String, String, bool); fn new_rdp(); fn send_mouse(i32, i32, i32, bool, bool, bool, bool); fn enter(); @@ -451,8 +459,7 @@ impl sciter::EventHandler for SciterSession { fn set_write_override(i32, i32, bool, bool, bool); fn get_keyboard_mode(); fn save_keyboard_mode(String); - fn has_hwcodec(); - fn supported_hwcodec(); + fn alternative_codecs(); fn change_prefer_codec(); fn restart_remote_device(); fn request_voice_call(); @@ -504,10 +511,6 @@ impl SciterSession { v } - fn has_hwcodec(&self) -> bool { - has_hwcodec() - } - pub fn t(&self, name: String) -> String { crate::client::translate(name) } @@ -516,9 +519,10 @@ impl SciterSession { super::get_icon() } - fn supported_hwcodec(&self) -> Value { - let (h264, h265) = self.0.supported_hwcodec(); + fn alternative_codecs(&self) -> Value { + let (vp8, h264, h265) = self.0.alternative_codecs(); let mut v = Value::array(0); + v.push(vp8); v.push(h264); v.push(h265); v diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index bd6eab3bf..d34b7a299 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -15,7 +15,12 @@ use std::{ use clipboard::{cliprdr::CliprdrClientContext, empty_clipboard, set_conn_enabled, ContextSend}; use serde_derive::Serialize; -use crate::ipc::{self, Connection, Data}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::ipc::Connection; +#[cfg(not(any(target_os = "ios")))] +use crate::ipc::{self, Data}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::tokio::sync::mpsc::unbounded_channel; #[cfg(windows)] use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::{ @@ -28,7 +33,7 @@ use hbb_common::{ protobuf::Message as _, tokio::{ self, - sync::mpsc::{self, unbounded_channel, UnboundedSender}, + sync::mpsc::{self, UnboundedSender}, task::spawn_blocking, }, }; @@ -52,9 +57,11 @@ pub struct Client { pub in_voice_call: bool, pub incoming_voice_call: bool, #[serde(skip)] + #[cfg(not(any(target_os = "ios")))] tx: UnboundedSender, } +#[cfg(not(any(target_os = "android", target_os = "ios")))] struct IpcTaskRunner { stream: Connection, cm: ConnectionManager, @@ -124,6 +131,7 @@ impl ConnectionManager { restart: bool, recording: bool, from_switch: bool, + #[cfg(not(any(target_os = "ios")))] tx: mpsc::UnboundedSender, ) { let client = Client { @@ -141,9 +149,10 @@ impl ConnectionManager { restart, recording, from_switch, + #[cfg(not(any(target_os = "ios")))] tx, in_voice_call: false, - incoming_voice_call: false + incoming_voice_call: false, }; CLIENTS .write() @@ -183,10 +192,12 @@ impl ConnectionManager { self.ui_handler.remove_connection(id, close); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn show_elevation(&self, show: bool) { self.ui_handler.show_elevation(show); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn voice_call_started(&self, id: i32) { if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { client.incoming_voice_call = false; @@ -195,6 +206,7 @@ impl ConnectionManager { } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn voice_call_incoming(&self, id: i32) { if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { client.incoming_voice_call = true; @@ -203,6 +215,7 @@ impl ConnectionManager { } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn voice_call_closed(&self, id: i32, _reason: &str) { if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { client.incoming_voice_call = false; @@ -213,6 +226,7 @@ impl ConnectionManager { } #[inline] +#[cfg(not(any(target_os = "ios")))] pub fn check_click_time(id: i32) { if let Some(client) = CLIENTS.read().unwrap().get(&id) { allow_err!(client.tx.send(Data::ClickTime(0))); @@ -225,6 +239,7 @@ pub fn get_click_time() -> i64 { } #[inline] +#[cfg(not(any(target_os = "ios")))] pub fn authorize(id: i32) { if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { client.authorized = true; @@ -233,6 +248,7 @@ pub fn authorize(id: i32) { } #[inline] +#[cfg(not(any(target_os = "ios")))] pub fn close(id: i32) { if let Some(client) = CLIENTS.read().unwrap().get(&id) { allow_err!(client.tx.send(Data::Close)); @@ -246,6 +262,7 @@ pub fn remove(id: i32) { // server mode send chat to peer #[inline] +#[cfg(not(any(target_os = "ios")))] pub fn send_chat(id: i32, text: String) { let clients = CLIENTS.read().unwrap(); if let Some(client) = clients.get(&id) { @@ -254,6 +271,7 @@ pub fn send_chat(id: i32, text: String) { } #[inline] +#[cfg(not(any(target_os = "ios")))] pub fn switch_permission(id: i32, name: String, enabled: bool) { if let Some(client) = CLIENTS.read().unwrap().get(&id) { allow_err!(client.tx.send(Data::SwitchPermission { name, enabled })); @@ -276,12 +294,14 @@ pub fn get_clients_length() -> usize { #[inline] #[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "ios")))] pub fn switch_back(id: i32) { if let Some(client) = CLIENTS.read().unwrap().get(&id) { allow_err!(client.tx.send(Data::SwitchSidesBack)); }; } +#[cfg(not(any(target_os = "android", target_os = "ios")))] impl IpcTaskRunner { #[cfg(windows)] async fn enable_cliprdr_file_context(&mut self, conn_id: i32, enabled: bool) { @@ -494,7 +514,7 @@ pub async fn start_ipc(cm: ConnectionManager) { e ); } - allow_err!(crate::win_privacy::start()); + allow_err!(crate::privacy_win_mag::start()); }); match ipc::new_listener("_cm").await { @@ -584,6 +604,7 @@ pub async fn start_listen( cm.remove_connection(current_id, true); } +#[cfg(not(any(target_os = "ios")))] async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &UnboundedSender) { match fs { ipc::FS::ReadDir { @@ -729,6 +750,7 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &Unbo } } +#[cfg(not(any(target_os = "ios")))] async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender) { let path = { if dir.is_empty() { @@ -746,6 +768,7 @@ async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender) { } } +#[cfg(not(any(target_os = "ios")))] async fn handle_result( res: std::result::Result, S>, id: i32, @@ -765,6 +788,7 @@ async fn handle_result( } } +#[cfg(not(any(target_os = "ios")))] async fn remove_file(path: String, id: i32, file_num: i32, tx: &UnboundedSender) { handle_result( spawn_blocking(move || fs::remove_file(&path)).await, @@ -775,6 +799,7 @@ async fn remove_file(path: String, id: i32, file_num: i32, tx: &UnboundedSender< .await; } +#[cfg(not(any(target_os = "ios")))] async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { handle_result( spawn_blocking(move || fs::create_dir(&path)).await, @@ -785,6 +810,7 @@ async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { .await; } +#[cfg(not(any(target_os = "ios")))] async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender) { let path = fs::get_path(&path); handle_result( @@ -803,6 +829,7 @@ async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender .await; } +#[cfg(not(any(target_os = "ios")))] fn send_raw(msg: Message, tx: &UnboundedSender) { match msg.write_to_bytes() { Ok(bytes) => { @@ -849,6 +876,8 @@ pub fn elevate_portable(_id: i32) { #[inline] pub fn handle_incoming_voice_call(id: i32, accept: bool) { if let Some(client) = CLIENTS.read().unwrap().get(&id) { + // Not handled in iOS yet. + #[cfg(not(any(target_os = "ios")))] allow_err!(client.tx.send(Data::VoiceCallResponse(accept))); }; } @@ -857,6 +886,8 @@ pub fn handle_incoming_voice_call(id: i32, accept: bool) { #[inline] pub fn close_voice_call(id: i32) { if let Some(client) = CLIENTS.read().unwrap().get(&id) { + // Not handled in iOS yet. + #[cfg(not(any(target_os = "ios")))] allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); }; } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 17b4fafca..10f2982f2 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -9,8 +9,12 @@ use hbb_common::password_security; use hbb_common::{ allow_err, config::{self, Config, LocalConfig, PeerConfig}, - directories_next, log, sleep, - tokio::{self, sync::mpsc, time}, + directories_next, log, tokio, +}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::{ + sleep, + tokio::{sync::mpsc, time}, }; use hbb_common::{ @@ -20,9 +24,11 @@ use hbb_common::{ rendezvous_proto::*, }; +use crate::common::SOFTWARE_UPDATE_URL; #[cfg(feature = "flutter")] use crate::hbbs_http::account; -use crate::{common::SOFTWARE_UPDATE_URL, ipc}; +#[cfg(not(any(target_os = "ios")))] +use crate::ipc; type Message = RendezvousMessage; @@ -339,8 +345,8 @@ pub fn get_socks() -> Vec { } #[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn set_socks(proxy: String, username: String, password: String) { - #[cfg(not(any(target_os = "android", target_os = "ios")))] ipc::set_socks(config::Socks5Server { proxy, username, @@ -349,6 +355,9 @@ pub fn set_socks(proxy: String, username: String, password: String) { .ok(); } +#[cfg(any(target_os = "android", target_os = "ios"))] +pub fn set_socks(_: String, _: String, _: String) {} + #[cfg(not(any(target_os = "android", target_os = "ios")))] #[inline] pub fn is_installed() -> bool { @@ -505,10 +514,10 @@ pub fn get_error() -> String { #[cfg(target_os = "linux")] { let dtype = crate::platform::linux::get_display_server(); - if "wayland" == dtype { + if crate::platform::linux::DISPLAY_SERVER_WAYLAND == dtype { return crate::server::wayland::common_get_error(); } - if dtype != "x11" { + if dtype != crate::platform::linux::DISPLAY_SERVER_X11 { return format!( "{} {}, {}", crate::client::translate("Unsupported display server".to_owned()), @@ -561,6 +570,7 @@ pub fn create_shortcut(_id: String) { #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn discover() { + #[cfg(not(any(target_os = "ios")))] std::thread::spawn(move || { allow_err!(crate::lan::discover()); }); @@ -745,6 +755,13 @@ pub fn has_hwcodec() -> bool { return true; } +#[cfg(feature = "flutter")] +#[inline] +pub fn supported_hwdecodings() -> (bool, bool) { + let decoding = scrap::codec::Decoder::supported_decodings(None); + (decoding.ability_h264 > 0, decoding.ability_h265 > 0) +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] #[inline] pub fn is_root() -> bool { @@ -791,14 +808,15 @@ pub fn check_zombie(children: Children) { } } +// Make sure `SENDER` is inited here. +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn start_option_status_sync() { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - let _sender = SENDER.lock().unwrap(); - } + let _sender = SENDER.lock().unwrap(); } // not call directly +#[cfg(not(any(target_os = "android", target_os = "ios")))] fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { let (tx, rx) = mpsc::unbounded_channel::(); std::thread::spawn(move || check_connect_status_(reconnect, rx)); @@ -832,8 +850,20 @@ pub fn get_user_default_option(key: String) -> String { UserDefaultConfig::load().get(&key) } +pub fn get_fingerprint() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + if Config::get_key_confirmed() { + return crate::common::pk_to_fingerprint(Config::get_key_pair().1); + } else { + return "".to_owned(); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_fingerprint(); +} + // notice: avoiding create ipc connection repeatedly, // because windows named pipe has serious memory leak issue. +#[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main(flavor = "current_thread")] async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { let mut key_confirmed = false; @@ -905,7 +935,8 @@ pub fn option_synced() -> bool { OPTION_SYNCED.lock().unwrap().clone() } -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[cfg(any(target_os = "android", feature = "flutter"))] +#[cfg(not(any(target_os = "ios")))] #[tokio::main(flavor = "current_thread")] pub(crate) async fn send_to_cm(data: &ipc::Data) { if let Ok(mut c) = ipc::connect(1000, "_cm").await { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2fd095347..e71e13919 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,23 +1,27 @@ #[cfg(not(any(target_os = "android", target_os = "ios")))] -use std::collections::HashMap; -use std::ops::{Deref, DerefMut}; -use std::str::FromStr; -use std::sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, Mutex, RwLock, +use std::{collections::HashMap, sync::atomic::AtomicBool}; +use std::{ + ops::{Deref, DerefMut}, + str::FromStr, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, RwLock, + }, + time::{Duration, SystemTime}, }; -use std::time::{Duration, SystemTime}; use async_trait::async_trait; use bytes::Bytes; -use rdev::{Event, EventType::*}; +use rdev::{Event, EventType::*, KeyCode}; use uuid::Uuid; use hbb_common::config::{Config, LocalConfig, PeerConfig}; +#[cfg(not(feature = "flutter"))] +use hbb_common::fs; use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; use hbb_common::{allow_err, message_proto::*}; -use hbb_common::{fs, get_version_number, log, Stream}; +use hbb_common::{get_version_number, log, Stream}; use crate::client::io_loop::Remote; use crate::client::{ @@ -25,10 +29,12 @@ use crate::client::{ input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, }; -use crate::common::{self, GrabState}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::GrabState; use crate::keyboard; use crate::{client::Data, client::Interface}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub static IS_IN: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] @@ -53,6 +59,7 @@ pub struct SessionPermissionConfig { pub server_clipboard_enabled: Arc>, } +#[cfg(not(any(target_os = "android", target_os = "ios")))] impl SessionPermissionConfig { pub fn is_text_clipboard_required(&self) -> bool { *self.server_clipboard_enabled.read().unwrap() @@ -62,6 +69,7 @@ impl SessionPermissionConfig { } impl Session { + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn get_permission_config(&self) -> SessionPermissionConfig { SessionPermissionConfig { lc: self.lc.clone(), @@ -87,6 +95,7 @@ impl Session { .eq(&ConnType::PORT_FORWARD) } + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_rdp(&self) -> bool { self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) } @@ -155,10 +164,12 @@ impl Session { self.lc.read().unwrap().get_toggle_option(&name) } + #[cfg(not(feature = "flutter"))] pub fn is_privacy_mode_supported(&self) -> bool { self.lc.read().unwrap().is_privacy_mode_supported() } + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_text_clipboard_required(&self) -> bool { *self.server_clipboard_enabled.read().unwrap() && *self.server_keyboard_enabled.read().unwrap() @@ -198,6 +209,7 @@ impl Session { self.lc.read().unwrap().remember } + #[cfg(not(feature = "flutter"))] pub fn set_write_override( &mut self, job_id: i32, @@ -216,24 +228,16 @@ impl Session { true } - pub fn supported_hwcodec(&self) -> (bool, bool) { - #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] - { - let decoder = scrap::codec::Decoder::video_codec_state(&self.id); - let mut h264 = decoder.score_h264 > 0; - let mut h265 = decoder.score_h265 > 0; - let (encoding_264, encoding_265) = self - .lc - .read() - .unwrap() - .supported_encoding - .unwrap_or_default(); - h264 = h264 && encoding_264; - h265 = h265 && encoding_265; - return (h264, h265); - } - #[allow(unreachable_code)] - (false, false) + pub fn alternative_codecs(&self) -> (bool, bool, bool) { + let decoder = scrap::codec::Decoder::supported_decodings(None); + let mut vp8 = decoder.ability_vp8 > 0; + let mut h264 = decoder.ability_h264 > 0; + let mut h265 = decoder.ability_h265 > 0; + let enc = &self.lc.read().unwrap().supported_encoding; + vp8 = vp8 && enc.vp8; + h264 = h264 && enc.h264; + h265 = h265 && enc.h265; + (vp8, h264, h265) } pub fn change_prefer_codec(&self) { @@ -248,6 +252,16 @@ impl Session { self.send(Data::Message(msg)); } + #[cfg(all(feature = "flutter", feature = "plugin_framework"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn send_plugin_request(&self, request: PluginRequest) { + let mut misc = Misc::new(); + misc.set_plugin_request(request); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + pub fn get_audit_server(&self, typ: String) -> String { if self.lc.read().unwrap().conn_id <= 0 || LocalConfig::get_option("access_token").is_empty() @@ -270,13 +284,13 @@ impl Session { }); } + #[cfg(not(feature = "flutter"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn is_xfce(&self) -> bool { - crate::platform::is_xfce() - } - - pub fn get_supported_keyboard_modes(&self) -> Vec { - let version = self.get_peer_version(); - common::get_supported_keyboard_modes(version) + #[cfg(not(any(target_os = "ios")))] + return crate::platform::is_xfce(); + #[cfg(any(target_os = "ios"))] + false } pub fn remove_port_forward(&self, port: i32) { @@ -307,6 +321,7 @@ impl Session { self.send(Data::AddPortForward(pf)); } + #[cfg(not(feature = "flutter"))] pub fn get_id(&self) -> String { self.id.clone() } @@ -366,6 +381,7 @@ impl Session { input_os_password(pass, activate, self.clone()); } + #[cfg(not(feature = "flutter"))] pub fn get_chatbox(&self) -> String { #[cfg(feature = "inline")] return crate::ui::inline::get_chatbox(); @@ -421,7 +437,7 @@ impl Session { rdev::win_scancode_from_key(key).unwrap_or_default() } "macos" => { - let key = rdev::macos_key_from_code(code); + let key = rdev::macos_key_from_code(code as _); let key = match key { rdev::Key::ControlLeft => rdev::Key::MetaLeft, rdev::Key::MetaLeft => rdev::Key::ControlLeft, @@ -429,7 +445,7 @@ impl Session { rdev::Key::MetaRight => rdev::Key::ControlLeft, _ => key, }; - rdev::macos_keycode_from_key(key).unwrap_or_default() + rdev::macos_keycode_from_key(key).unwrap_or_default() as _ } _ => { let key = rdev::linux_key_from_code(code); @@ -480,6 +496,7 @@ impl Session { self.send(Data::Message(msg_out)); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn enter(&self) { #[cfg(target_os = "windows")] { @@ -494,6 +511,7 @@ impl Session { keyboard::client::change_grab_status(GrabState::Run); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn leave(&self) { #[cfg(target_os = "windows")] { @@ -534,25 +552,37 @@ impl Session { self.send(Data::Message(msg_out)); } + #[cfg(any(target_os = "ios"))] pub fn handle_flutter_key_event( &self, _name: &str, - keycode: i32, - scancode: i32, + platform_code: i32, + position_code: i32, lock_modes: i32, down_or_up: bool, ) { - if scancode < 0 || keycode < 0 { + } + + #[cfg(not(any(target_os = "ios")))] + pub fn handle_flutter_key_event( + &self, + _name: &str, + platform_code: i32, + position_code: i32, + lock_modes: i32, + down_or_up: bool, + ) { + if position_code < 0 || platform_code < 0 { return; } - let keycode: u32 = keycode as u32; - let scancode: u32 = scancode as u32; + let platform_code: u32 = platform_code as _; + let position_code: KeyCode = position_code as _; #[cfg(not(target_os = "windows"))] - let key = rdev::key_from_code(keycode) as rdev::Key; + let key = rdev::key_from_code(position_code) as rdev::Key; // Windows requires special handling #[cfg(target_os = "windows")] - let key = rdev::get_win_key(keycode, scancode); + let key = rdev::get_win_key(platform_code, position_code); let event_type = if down_or_up { KeyPress(key) @@ -562,9 +592,9 @@ impl Session { let event = Event { time: SystemTime::now(), unicode: None, - code: keycode as _, - scan_code: scancode as _, - event_type: event_type, + platform_code, + position_code: position_code as _, + event_type, }; keyboard::client::process_event(&event, Some(lock_modes)); } @@ -661,6 +691,7 @@ impl Session { })); } + #[cfg(not(feature = "flutter"))] pub fn get_icon_path(&self, file_type: i32, ext: String) -> String { let mut path = Config::icon_path(); if file_type == FileType::DirLink as i32 { @@ -711,8 +742,14 @@ impl Session { fs::get_string(&path) } - pub fn login(&self, password: String, remember: bool) { - self.send(Data::Login((password, remember))); + pub fn login( + &self, + os_username: String, + os_password: String, + password: String, + remember: bool, + ) { + self.send(Data::Login((os_username, os_password, password, remember))); } pub fn new_rdp(&self) { @@ -757,6 +794,10 @@ impl Session { self.send(Data::ElevateWithLogon(username, password)); } + #[cfg(any(target_os = "ios"))] + pub fn switch_sides(&self) {} + + #[cfg(not(any(target_os = "ios")))] #[tokio::main(flavor = "current_thread")] pub async fn switch_sides(&self) { match crate::ipc::connect(1000, "").await { @@ -858,6 +899,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn close_success(&self); fn update_quality_status(&self, qs: QualityStatus); fn set_connection_type(&self, is_secured: bool, direct: bool); + fn set_fingerprint(&self, fingerprint: String); fn job_error(&self, id: i32, err: String, file_num: i32); fn job_done(&self, id: i32, file_num: i32); fn clear_all_jobs(&self); @@ -1005,8 +1047,23 @@ impl Interface for Session { handle_hash(self.lc.clone(), pass, hash, self, peer).await; } - async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { - handle_login_from_ui(self.lc.clone(), password, remember, peer).await; + async fn handle_login_from_ui( + &mut self, + os_username: String, + os_password: String, + password: String, + remember: bool, + peer: &mut Stream, + ) { + handle_login_from_ui( + self.lc.clone(), + os_username, + os_password, + password, + remember, + peer, + ) + .await; } async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { @@ -1053,6 +1110,9 @@ impl Session { #[tokio::main(flavor = "current_thread")] pub async fn io_loop(handler: Session) { + #[cfg(any(target_os = "android", target_os = "ios"))] + let (sender, receiver) = mpsc::unbounded_channel::(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] let (sender, mut receiver) = mpsc::unbounded_channel::(); *handler.sender.write().unwrap() = Some(sender.clone()); let token = LocalConfig::get_option("access_token"); @@ -1146,18 +1206,21 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: &mut Vec| { - frame_count_cl.fetch_add(1, Ordering::Relaxed); - ui_handler.on_rgba(data); - }); + let (video_sender, audio_sender, video_queue, decode_fps) = + start_video_audio_threads(move |data: &mut Vec| { + frame_count_cl.fetch_add(1, Ordering::Relaxed); + ui_handler.on_rgba(data); + }); let mut remote = Remote::new( handler, + video_queue, video_sender, audio_sender, receiver, sender, frame_count, + decode_fps, ); remote.io_loop(&key, &token).await; remote.sync_jobs_status_to_local().await; diff --git a/src/virtual_display_manager.rs b/src/virtual_display_manager.rs new file mode 100644 index 000000000..82328915e --- /dev/null +++ b/src/virtual_display_manager.rs @@ -0,0 +1,106 @@ +use hbb_common::{allow_err, bail, lazy_static, log, ResultType}; +use std::{ + collections::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 = 3; +const VIRTUAL_DISPLAY_MAX_COUNT: u32 = 10; + +lazy_static::lazy_static! { + static ref VIRTUAL_DISPLAY_MANAGER: Arc> = + Arc::new(Mutex::new(VirtualDisplayManager::default())); +} + +#[derive(Default)] +struct VirtualDisplayManager { + headless_index: Option, + peer_required_indices: HashSet, +} + +impl VirtualDisplayManager { + fn prepare_driver() -> 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; + allow_err!(virtual_display::install_update_driver( + &mut _reboot_required + )); + 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 plug_in_headless() -> ResultType<()> { + let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + VirtualDisplayManager::prepare_driver()?; + let modes = [virtual_display::MonitorMode { + width: 1920, + height: 1080, + sync: 60, + }]; + VirtualDisplayManager::plug_in_monitor(VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS, &modes)?; + manager.headless_index = Some(VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS); + Ok(()) +} + +pub fn plug_out_headless() { + let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + if let Some(index) = manager.headless_index.take() { + if let Err(e) = virtual_display::plug_out_monitor(index) { + log::error!("Plug out monitor failed {}", e); + } + } +} + +pub fn plug_in_peer_required( + modes: Vec>, +) -> ResultType> { + let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + VirtualDisplayManager::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_required_indices.contains(&idx) { + match VirtualDisplayManager::plug_in_monitor(idx, m) { + Ok(_) => { + manager.peer_required_indices.insert(idx); + indices.push(idx); + } + Err(e) => { + log::error!("Plug in monitor failed {}", e); + } + } + } + } + } + + Ok(indices) +} + +pub fn plug_out_peer_required(modes: &[u32]) -> ResultType<()> { + let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + for idx in modes.iter() { + if manager.peer_required_indices.contains(idx) { + allow_err!(virtual_display::plug_out_monitor(*idx)); + manager.peer_required_indices.remove(idx); + } + } + Ok(()) +} diff --git a/vdi/host/.cargo/config.toml b/vdi/host/.cargo/config.toml new file mode 100644 index 000000000..70f9eaeb2 --- /dev/null +++ b/vdi/host/.cargo/config.toml @@ -0,0 +1,2 @@ +[registries.crates-io] +protocol = "sparse" diff --git a/vdi/host/.devcontainer/Dockerfile b/vdi/host/.devcontainer/Dockerfile deleted file mode 100644 index f02042771..000000000 --- a/vdi/host/.devcontainer/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM rockylinux:9.1 -ENV HOME=/home/vscode -ENV WORKDIR=$HOME/rustdesk/vdi/host - -# https://ciq.co/blog/top-10-things-to-do-after-rocky-linux-9-install/ also gpu driver install -WORKDIR $HOME -RUN dnf -y install epel-release -RUN dnf config-manager --set-enabled crb -RUN dnf -y install cargo libvpx-devel opus-devel usbredir-devel git cmake gcc-c++ pkg-config nasm yasm ninja-build automake libtool libva-devel libvdpau-devel llvm-devel -WORKDIR / - -RUN git clone https://chromium.googlesource.com/libyuv/libyuv -WORKDIR /libyuv -RUN cmake . -DCMAKE_INSTALL_PREFIX=/user -RUN make -j4 && make install -WORKDIR / \ No newline at end of file diff --git a/vdi/host/.devcontainer/devcontainer.json b/vdi/host/.devcontainer/devcontainer.json deleted file mode 100644 index f0016b5b1..000000000 --- a/vdi/host/.devcontainer/devcontainer.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "rustdesk", - "build": { - "dockerfile": "./Dockerfile", - "context": "." - }, - "workspaceMount": "source=${localWorkspaceFolder}/../..,target=/home/vscode/rustdesk,type=bind,consistency=cache", - "workspaceFolder": "/home/vscode/rustdesk/vdi/host", - "customizations": { - "vscode": { - "extensions": [ - "vadimcn.vscode-lldb", - "mutantdino.resourcemonitor", - "rust-lang.rust-analyzer", - "tamasfe.even-better-toml", - "serayuzgur.crates", - "mhutchie.git-graph", - "formulahendry.terminal", - "eamodio.gitlens" - ], - "settings": { - "files.watcherExclude": { - "**/target/**": true - } - } - } - } -} \ No newline at end of file diff --git a/vdi/host/Cargo.lock b/vdi/host/Cargo.lock index 7b7cf26bd..0b2e8ca2b 100644 --- a/vdi/host/Cargo.lock +++ b/vdi/host/Cargo.lock @@ -157,17 +157,6 @@ dependencies = [ "syn", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -189,18 +178,36 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" + [[package]] name = "bumpalo" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + [[package]] name = "byteorder" version = "1.4.3" @@ -246,6 +253,43 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "4.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098" +dependencies = [ + "bitflags 2.0.2", + "clap_derive", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -256,6 +300,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "concurrent-queue" version = "2.1.0" @@ -282,6 +332,15 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.6" @@ -316,6 +375,16 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.14" @@ -325,6 +394,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "cxx" version = "1.0.91" @@ -475,23 +550,60 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" dependencies = [ - "atty", "humantime", + "is-terminal", "log", "regex", "termcolor", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "exr" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -513,6 +625,47 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flexi_logger" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eae57842a8221ef13f1f207632d786a175dd13bd8fbdc8be9d852f7c9cf1046" +dependencies = [ + "chrono", + "crossbeam-channel", + "crossbeam-queue", + "glob", + "is-terminal", + "lazy_static", + "log", + "nu-ansi-term", + "regex", + "thiserror", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + [[package]] name = "futures" version = "0.3.26" @@ -624,8 +777,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", ] [[package]] @@ -634,6 +799,21 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -656,6 +836,7 @@ dependencies = [ "dirs-next", "env_logger", "filetime", + "flexi_logger", "futures", "futures-util", "lazy_static", @@ -681,13 +862,10 @@ dependencies = [ ] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "heck" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" @@ -698,6 +876,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -734,6 +918,25 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "image" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -753,6 +956,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.45.0", +] + [[package]] name = "itoa" version = "1.0.5" @@ -768,6 +994,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.61" @@ -783,6 +1018,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.139" @@ -822,6 +1063,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "lock_api" version = "0.4.9" @@ -905,13 +1152,22 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "nix" version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cc", "cfg-if", "libc", @@ -924,7 +1180,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", "memoffset 0.6.5", @@ -941,13 +1197,23 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.7" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -958,6 +1224,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -1002,6 +1279,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_str_bytes" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" + [[package]] name = "osascript" version = "0.3.0" @@ -1013,6 +1296,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.0.0" @@ -1080,6 +1369,18 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +[[package]] +name = "png" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "2.5.2" @@ -1110,6 +1411,30 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.51" @@ -1174,7 +1499,7 @@ dependencies = [ [[package]] name = "qemu-display" version = "0.1.0" -source = "git+https://gitlab.com/marcandre.lureau/qemu-display#544a4075615702abf414cd2d63bbb6a9ca10d0ea" +source = "git+https://github.com/rustdesk/qemu-display#e8a0925c2e804aa1eb07ee3027deaf8dd1c71b1d" dependencies = [ "async-broadcast 0.3.4", "async-lock", @@ -1201,8 +1526,12 @@ dependencies = [ name = "qemu-rustdesk" version = "0.1.0" dependencies = [ + "async-trait", + "clap", "hbb_common", + "image", "qemu-display", + "zbus", ] [[package]] @@ -1256,9 +1585,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.10.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -1272,7 +1601,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1328,6 +1657,20 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "rustix" +version = "0.36.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + [[package]] name = "ryu" version = "1.0.12" @@ -1343,6 +1686,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.1.0" @@ -1448,6 +1797,12 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + [[package]] name = "slab" version = "0.4.8" @@ -1496,12 +1851,27 @@ dependencies = [ "serde", ] +[[package]] +name = "spin" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d6e0250b93c8427a177b849d144a96d5acc57006149479403d7861ab721e34" +dependencies = [ + "lock_api", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.109" @@ -1515,9 +1885,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.24.7" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb4ebf3d49308b99e6e9dc95e989e2fdbdc210e4f67c39db0bb89ba927001c" +checksum = "f69e0d827cce279e61c2f3399eb789271a8f136d8245edef70f06e3c9601a670" dependencies = [ "cfg-if", "core-foundation-sys", @@ -1571,6 +1941,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.1.45" @@ -1864,6 +2245,12 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "wepoll-ffi" version = "0.1.2" @@ -2119,6 +2506,15 @@ dependencies = [ "libc", ] +[[package]] +name = "zune-inflate" +version = "0.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01728b79fb9b7e28a8c11f715e1cd8dc2cda7416a007d66cac55cebb3a8ac6b" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zvariant" version = "3.11.0" diff --git a/vdi/host/Cargo.toml b/vdi/host/Cargo.toml index 6a67813a2..42b6fc83a 100644 --- a/vdi/host/Cargo.toml +++ b/vdi/host/Cargo.toml @@ -5,5 +5,9 @@ authors = ["rustdesk "] edition = "2021" [dependencies] -qemu-display = { git = "https://gitlab.com/marcandre.lureau/qemu-display" } +qemu-display = { git = "https://github.com/rustdesk/qemu-display" } hbb_common = { path = "../../libs/hbb_common" } +clap = { version = "4.1", features = ["derive"] } +zbus = { version = "3.0" } +image = "0.24" +async-trait = "0.1" diff --git a/vdi/host/README.md b/vdi/host/README.md index 3b29a10e3..0283266bf 100644 --- a/vdi/host/README.md +++ b/vdi/host/README.md @@ -1 +1,5 @@ # RustDesk protocol on QEMU D-Bus display + +``` +sudo apt install libusbredirparser-dev libusbredirhost-dev libusb-1.0-0-dev +``` diff --git a/vdi/host/src/connection.rs b/vdi/host/src/connection.rs new file mode 100644 index 000000000..9f856fa2e --- /dev/null +++ b/vdi/host/src/connection.rs @@ -0,0 +1,11 @@ +use hbb_common::{message_proto::*, tokio, ResultType}; +pub use tokio::sync::{mpsc, Mutex}; +pub struct Connection { + pub tx: mpsc::UnboundedSender, +} + +impl Connection { + pub async fn on_message(&mut self, message: Message) -> ResultType { + Ok(true) + } +} diff --git a/vdi/host/src/console.rs b/vdi/host/src/console.rs new file mode 100644 index 000000000..a342f1a9a --- /dev/null +++ b/vdi/host/src/console.rs @@ -0,0 +1,119 @@ +use hbb_common::{tokio, ResultType}; +use image::GenericImage; +use qemu_display::{Console, ConsoleListenerHandler, MouseButton}; +use std::{collections::HashSet, sync::Arc}; +pub use tokio::sync::{mpsc, Mutex}; + +#[derive(Debug)] +pub enum Event { + ConsoleUpdate((i32, i32, i32, i32)), + Disconnected, +} + +const PIXMAN_X8R8G8B8: u32 = 0x20020888; +pub type BgraImage = image::ImageBuffer, Vec>; +#[derive(Debug)] +pub struct ConsoleListener { + pub image: Arc>, + pub tx: mpsc::UnboundedSender, +} + +#[async_trait::async_trait] +impl ConsoleListenerHandler for ConsoleListener { + async fn scanout(&mut self, s: qemu_display::Scanout) { + *self.image.lock().await = image_from_vec(s.format, s.width, s.height, s.stride, s.data); + } + + async fn update(&mut self, u: qemu_display::Update) { + let update = image_from_vec(u.format, u.w as _, u.h as _, u.stride, u.data); + let mut image = self.image.lock().await; + if (u.x, u.y) == (0, 0) && update.dimensions() == image.dimensions() { + *image = update; + } else { + image.copy_from(&update, u.x as _, u.y as _).unwrap(); + } + self.tx + .send(Event::ConsoleUpdate((u.x, u.y, u.w, u.h))) + .ok(); + } + + async fn scanout_dmabuf(&mut self, _scanout: qemu_display::ScanoutDMABUF) { + unimplemented!() + } + + async fn update_dmabuf(&mut self, _update: qemu_display::UpdateDMABUF) { + unimplemented!() + } + + async fn mouse_set(&mut self, set: qemu_display::MouseSet) { + dbg!(set); + } + + async fn cursor_define(&mut self, cursor: qemu_display::Cursor) { + dbg!(cursor); + } + + fn disconnected(&mut self) { + self.tx.send(Event::Disconnected).ok(); + } +} + +pub async fn key_event(console: &mut Console, qnum: u32, down: bool) -> ResultType<()> { + if down { + console.keyboard.press(qnum).await?; + } else { + console.keyboard.release(qnum).await?; + } + Ok(()) +} + +fn image_from_vec(format: u32, width: u32, height: u32, stride: u32, data: Vec) -> BgraImage { + if format != PIXMAN_X8R8G8B8 { + todo!("unhandled pixman format: {}", format) + } + if cfg!(target_endian = "big") { + todo!("pixman/image in big endian") + } + let layout = image::flat::SampleLayout { + channels: 4, + channel_stride: 1, + width, + width_stride: 4, + height, + height_stride: stride as _, + }; + let samples = image::flat::FlatSamples { + samples: data, + layout, + color_hint: None, + }; + samples + .try_into_buffer::>() + .or_else::<&str, _>(|(_err, samples)| { + let view = samples.as_view::>().unwrap(); + let mut img = BgraImage::new(width, height); + img.copy_from(&view, 0, 0).unwrap(); + Ok(img) + }) + .unwrap() +} + +fn button_mask_to_set(mask: u8) -> HashSet { + let mut set = HashSet::new(); + if mask & 0b0000_0001 != 0 { + set.insert(MouseButton::Left); + } + if mask & 0b0000_0010 != 0 { + set.insert(MouseButton::Middle); + } + if mask & 0b0000_0100 != 0 { + set.insert(MouseButton::Right); + } + if mask & 0b0000_1000 != 0 { + set.insert(MouseButton::WheelUp); + } + if mask & 0b0001_0000 != 0 { + set.insert(MouseButton::WheelDown); + } + set +} diff --git a/vdi/host/src/lib.rs b/vdi/host/src/lib.rs new file mode 100644 index 000000000..e9f8d7ed3 --- /dev/null +++ b/vdi/host/src/lib.rs @@ -0,0 +1,3 @@ +pub mod server; +mod console; +mod connection; diff --git a/vdi/host/src/main.rs b/vdi/host/src/main.rs index f79c691f0..ea32a028a 100644 --- a/vdi/host/src/main.rs +++ b/vdi/host/src/main.rs @@ -1,2 +1,6 @@ fn main() { + hbb_common::init_log(false, ""); + if let Err(err) = qemu_rustdesk::server::run() { + hbb_common::log::error!("{err}"); + } } diff --git a/vdi/host/src/server.rs b/vdi/host/src/server.rs new file mode 100644 index 000000000..b43bd364f --- /dev/null +++ b/vdi/host/src/server.rs @@ -0,0 +1,172 @@ +use clap::Parser; +use hbb_common::{ + allow_err, + anyhow::{bail, Context}, + log, + message_proto::*, + protobuf::Message as _, + tokio, + tokio::net::TcpListener, + ResultType, Stream, +}; +use qemu_display::{Console, VMProxy}; +use std::{borrow::Borrow, sync::Arc}; + +use crate::connection::*; +use crate::console::*; + +#[derive(Parser, Debug)] +pub struct SocketAddrArgs { + /// IP address + #[clap(short, long, default_value = "0.0.0.0")] + address: std::net::IpAddr, + /// IP port number + #[clap(short, long, default_value = "21116")] + port: u16, +} + +impl From for std::net::SocketAddr { + fn from(args: SocketAddrArgs) -> Self { + (args.address, args.port).into() + } +} + +#[derive(Parser, Debug)] +struct Cli { + #[clap(flatten)] + address: SocketAddrArgs, + #[clap(short, long)] + dbus_address: Option, +} + +#[derive(Debug)] +struct Server { + vm_name: String, + rx_console: mpsc::UnboundedReceiver, + tx_console: mpsc::UnboundedSender, + rx_conn: mpsc::UnboundedReceiver, + tx_conn: mpsc::UnboundedSender, + image: Arc>, + console: Arc>, +} + +impl Server { + async fn new(vm_name: String, console: Console) -> ResultType { + let width = console.width().await?; + let height = console.height().await?; + let image = BgraImage::new(width as _, height as _); + let (tx_console, rx_console) = mpsc::unbounded_channel(); + let (tx_conn, rx_conn) = mpsc::unbounded_channel(); + Ok(Self { + vm_name, + rx_console, + tx_console, + rx_conn, + tx_conn, + image: Arc::new(Mutex::new(image)), + console: Arc::new(Mutex::new(console)), + }) + } + + async fn stop_console(&self) -> ResultType<()> { + self.console.lock().await.unregister_listener(); + Ok(()) + } + + async fn run_console(&self) -> ResultType<()> { + self.console + .lock() + .await + .register_listener(ConsoleListener { + image: self.image.clone(), + tx: self.tx_console.clone(), + }) + .await?; + Ok(()) + } + + async fn dimensions(&self) -> (u16, u16) { + let image = self.image.lock().await; + (image.width() as u16, image.height() as u16) + } + + async fn handle_connection(&mut self, stream: Stream) -> ResultType<()> { + let mut stream = stream; + self.run_console().await?; + let mut conn = Connection { + tx: self.tx_conn.clone(), + }; + + loop { + tokio::select! { + Some(evt) = self.rx_console.recv() => { + match evt { + _ => {} + } + } + Some(msg) = self.rx_conn.recv() => { + allow_err!(stream.send(&msg).await); + } + res = stream.next() => { + if let Some(res) = res { + match res { + Err(err) => { + bail!(err); + } + Ok(bytes) => { + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + match conn.on_message(msg_in).await { + Ok(false) => { + break; + } + Err(err) => { + log::error!("{err}"); + } + _ => {} + } + } + } + } + } else { + bail!("Reset by the peer"); + } + } + } + } + + self.stop_console().await?; + Ok(()) + } +} + +#[tokio::main] +pub async fn run() -> ResultType<()> { + let args = Cli::parse(); + + let listener = TcpListener::bind::(args.address.into()) + .await + .unwrap(); + let dbus = if let Some(addr) = args.dbus_address { + zbus::ConnectionBuilder::address(addr.borrow())? + .build() + .await + } else { + zbus::Connection::session().await + } + .context("Failed to connect to DBus")?; + + let vm_name = VMProxy::new(&dbus).await?.name().await?; + let console = Console::new(&dbus.into(), 0) + .await + .context("Failed to get the console")?; + let mut server = Server::new(format!("qemu-rustdesk ({})", vm_name), console).await?; + loop { + let (stream, addr) = listener.accept().await?; + stream.set_nodelay(true).ok(); + let laddr = stream.local_addr()?; + let stream = Stream::from(stream, laddr); + if let Err(err) = server.handle_connection(stream).await { + log::error!("Connection from {addr} closed: {err}"); + } + } +}