Compare commits

..

3 Commits

Author SHA1 Message Date
21pages
8deee9b281 fix: don't clear audit_guid during reconnect, clear it after connection established
Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-03-15 23:00:46 +08:00
copilot-swe-agent[bot]
439e3abc79 Fix: show ask-for-note dialog when user clicks OK on reconnecting screen (#14527)
Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
2026-03-12 15:18:50 +00:00
copilot-swe-agent[bot]
b8c0787273 Initial plan 2026-03-12 15:12:58 +00:00
190 changed files with 7015 additions and 20445 deletions

View File

@@ -25,7 +25,7 @@ jobs:
}
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
@@ -49,25 +49,25 @@ jobs:
wget
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: bridge-${{ matrix.job.os }}
- name: Cache Bridge
id: cache-bridge
uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3
uses: actions/cache@v3
with:
path: /tmp/flutter_rust_bridge
key: vcpkg-${{ matrix.job.arch }}
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -86,7 +86,7 @@ jobs:
cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h
- name: Upload Artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: bridge-artifact
path: |

View File

@@ -29,13 +29,13 @@ jobs:
# name: Ensure 'cargo fmt' has been run
# runs-on: ubuntu-20.04
# steps:
# - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1
# - uses: actions-rs/toolchain@v1
# with:
# toolchain: stable
# default: true
# profile: minimal
# components: rustfmt
# - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
# - uses: actions/checkout@v3
# - run: cargo fmt -- --check
# min_version:
@@ -43,24 +43,24 @@ jobs:
# runs-on: ubuntu-20.04
# steps:
# - name: Checkout source code
# uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
# uses: actions/checkout@v3
# with:
# submodules: recursive
# - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
# uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1
# uses: actions-rs/toolchain@v1
# with:
# toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }}
# default: true
# profile: minimal # minimal component installation (ie, no documentation)
# components: clippy
# - name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
# uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
# uses: actions-rs/cargo@v1
# with:
# command: clippy
# args: --locked --all-targets --all-features -- --allow clippy::unknown_clippy_lints
# - name: Run tests
# uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
# uses: actions-rs/cargo@v1
# with:
# command: test
# args: --locked
@@ -86,9 +86,9 @@ jobs:
steps:
- name: Free Disk Space (Ubuntu)
if: runner.os == 'Linux'
# jlumbroso/free-disk-space@v1.3.1 is used in .github\workflows\flutter-build.yml
# jlumbroso/free-disk-space@main is used in .github\workflows\flutter-build.yml
# But pinning to a specific version to avoid unexpected issues is preferred.
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
uses: jlumbroso/free-disk-space@v1.3.1
with:
tool-cache: false
android: true
@@ -99,14 +99,14 @@ jobs:
swap-storage: false
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
@@ -145,7 +145,7 @@ jobs:
esac
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -156,7 +156,7 @@ jobs:
shell: bash
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
targets: ${{ matrix.job.target }}
@@ -172,10 +172,10 @@ jobs:
cargo -V
rustc -V
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
- name: Build
uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.job.use-cross }}
command: build
@@ -243,7 +243,7 @@ jobs:
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
- name: Run tests
uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.job.use-cross }}
command: test

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clear cache
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
uses: actions/github-script@v7
with:
script: |
console.log("About to clear")
@@ -30,7 +30,7 @@ jobs:
console.log("Clear completed")
- name: Purge cache # Above seems not clear thouroughly, so add this to double clear
uses: MyAlbum/purge-cache@881eb5957687193fa612bf74c0042adc78ea5e54 # v2
uses: MyAlbum/purge-cache@v2
with:
accessed: true # Purge caches by their last accessed time (default)
created: false # Purge caches by their created time (default)

View File

@@ -31,7 +31,7 @@ jobs:
shell: bash
- name: Publish RustDesk version file
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: "fdroid-version"

View File

@@ -39,7 +39,7 @@ env:
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
VERSION: "1.4.7"
VERSION: "1.4.6"
NDK_VERSION: "r28c"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -81,30 +81,30 @@ jobs:
# - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@1a3da29f56261a1e1f937ec88f0856a9b8321d7e # v1
uses: KyleMayes/install-llvm-action@v1
with:
version: ${{ env.LLVM_VERSION }}
- name: Install flutter
uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 # v2.12.0; https://github.com/subosito/flutter-action/issues/277
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -126,18 +126,18 @@ jobs:
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.SCITER_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -220,7 +220,7 @@ jobs:
fi
- name: Download RustDeskTempTopMostWindow artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
if: ${{ inputs.upload-artifact }}
with:
name: topmostwindow-artifacts
@@ -228,7 +228,7 @@ jobs:
- name: Upload unsigned
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
path: rustdesk
@@ -253,7 +253,7 @@ jobs:
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.exe
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
uses: microsoft/setup-msbuild@v2
- name: Build msi
if: env.UPLOAD_ARTIFACT == 'true'
@@ -272,7 +272,7 @@ jobs:
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput
- name: Publish Release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
if: env.UPLOAD_ARTIFACT == 'true'
with:
prerelease: true
@@ -302,35 +302,35 @@ jobs:
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install LLVM and Clang
uses: rustdesk-org/install-llvm-action-32bit@6aa7d9ad3df84dff01cd4596dd0fc880a7f47fce # no release tag; commit 2026-05-26
uses: rustdesk-org/install-llvm-action-32bit@master
with:
version: ${{ env.LLVM_VERSION }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: nightly-2023-10-13-${{ matrix.job.target }} # must use nightly here, because of abi_thiscall feature required
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}-sciter
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -363,8 +363,7 @@ jobs:
python3 res/inline-sciter.py
# Patch sciter x86
sed -i 's/branch = "dyn"/branch = "dyn_x86"/g' ./Cargo.toml
cargo update -p sciter-rs --precise 674e07d3066ca9a92ced3816203ab6b652629d1e
cargo build --locked --features inline,vram,hwcodec --release --bins
cargo build --features inline,vram,hwcodec --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
@@ -395,7 +394,7 @@ jobs:
- name: Upload unsigned
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
path: Release
@@ -425,7 +424,7 @@ jobs:
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/
- name: Publish Release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
if: env.UPLOAD_ARTIFACT == 'true'
with:
prerelease: true
@@ -450,7 +449,7 @@ jobs:
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
@@ -460,12 +459,12 @@ jobs:
run: |
brew install nasm yasm
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -476,7 +475,7 @@ jobs:
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
@@ -500,19 +499,19 @@ jobs:
shell: bash
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: rustdesk-lib-cache-ios
key: ${{ matrix.job.target }}
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
@@ -520,10 +519,10 @@ jobs:
- name: Build rustdesk lib
run: |
rustup target add ${{ matrix.job.target }}
cargo build --locked --features flutter,hwcodec --release --target aarch64-apple-ios --lib
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
- name: Upload liblibrustdesk.a Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: liblibrustdesk.a
path: target/aarch64-apple-ios/release/liblibrustdesk.a
@@ -538,14 +537,14 @@ jobs:
# - name: Upload Artifacts
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
# uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
# 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@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
# uses: softprops/action-gh-release@v1
# with:
# prerelease: true
# tag_name: ${{ env.TAG_NAME }}
@@ -578,20 +577,20 @@ jobs:
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 # v1
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
@@ -605,7 +604,7 @@ jobs:
- name: Import notarize key
if: env.MACOS_P12_BASE64 != null
uses: timheuer/base64-to-file@adaa40c0c581f276132199d4cf60afa07ce60eac # v1.2
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
@@ -644,7 +643,7 @@ jobs:
nasm --version
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -663,24 +662,24 @@ jobs:
grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.MAC_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
@@ -732,7 +731,7 @@ jobs:
- name: Upload unsigned macOS app
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: rustdesk-unsigned-macos-${{ matrix.job.arch }}
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg # can not upload the directory directly or tar.gz, which destroy the link structure, causing the codesign failed
@@ -764,7 +763,7 @@ jobs:
- name: Publish DMG package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -780,25 +779,25 @@ jobs:
if: ${{ inputs.upload-artifact }}
steps:
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-unsigned-macos-x86_64
path: ./
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-unsigned-macos-aarch64
path: ./
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-unsigned-windows-x86_64
path: ./windows-x86_64/
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-unsigned-windows-x86
path: ./windows-x86/
@@ -808,7 +807,7 @@ jobs:
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-x86
- name: Publish unsigned app
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -845,7 +844,7 @@ jobs:
}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
@@ -856,7 +855,7 @@ jobs:
swap-storage: false
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
@@ -898,12 +897,12 @@ jobs:
wget
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
@@ -913,14 +912,14 @@ jobs:
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: ${{ env.NDK_VERSION }}
add-to-path: true
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -955,18 +954,18 @@ jobs:
shell: bash
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
key: ${{ matrix.job.target }}
@@ -1002,7 +1001,7 @@ jobs:
esac
- name: Upload Rustdesk library to Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: librustdesk.so.${{ matrix.job.target }}
path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so
@@ -1067,7 +1066,7 @@ jobs:
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
@@ -1083,14 +1082,14 @@ jobs:
- name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1099,7 +1098,7 @@ jobs:
- name: Publish unsigned apk package
if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1117,7 +1116,7 @@ jobs:
suffix: ""
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
@@ -1128,7 +1127,7 @@ jobs:
swap-storage: false
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
@@ -1170,12 +1169,12 @@ jobs:
wget
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
@@ -1186,32 +1185,32 @@ jobs:
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Download Rustdesk library from Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: librustdesk.so.aarch64-linux-android
path: ./flutter/android/app/src/main/jniLibs/arm64-v8a
- name: Download Rustdesk library from Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: librustdesk.so.armv7-linux-androideabi
path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a
- name: Download Rustdesk library from Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: librustdesk.so.x86_64-linux-android
path: ./flutter/android/app/src/main/jniLibs/x86_64
- name: Download Rustdesk library from Artifacts
if: ${{ env.reltype == 'debug' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: librustdesk.so.i686-linux-android
path: ./flutter/android/app/src/main/jniLibs/x86
@@ -1251,7 +1250,7 @@ jobs:
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
@@ -1267,14 +1266,14 @@ jobs:
- name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1283,7 +1282,7 @@ jobs:
- name: Publish unsigned apk package
if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1317,7 +1316,7 @@ jobs:
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
@@ -1335,13 +1334,13 @@ jobs:
fi
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set Swap Space
if: ${{ matrix.job.arch == 'x86_64' }}
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 12
@@ -1351,7 +1350,7 @@ jobs:
free -m
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
with:
toolchain: ${{ env.RUST_VERSION }}
@@ -1370,14 +1369,14 @@ jobs:
- name: Restore bridge files
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Setup vcpkg with Github Actions binary cache
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -1405,12 +1404,12 @@ jobs:
- name: Restore bridge files
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
- uses: rustdesk-org/run-on-arch-action@amd64-support
name: Build rustdesk
id: vcpkg
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
@@ -1492,7 +1491,7 @@ jobs:
export JOBS=""
fi
echo $JOBS
cargo build --locked --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release
cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release
rm -rf target/release/deps target/release/build
rm -rf ~/.cargo
@@ -1584,7 +1583,7 @@ jobs:
- name: Publish debian/rpm package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1593,7 +1592,7 @@ jobs:
rustdesk-*.rpm
- name: Upload deb
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
if: env.UPLOAD_ARTIFACT == 'true'
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
@@ -1612,7 +1611,7 @@ jobs:
- name: Build archlinux package
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
uses: rustdesk-org/arch-makepkg-action@04200739ed1d0bf6f2188b6736b26a767c57a7f9 # no release tag; commit 2026-05-26
uses: rustdesk-org/arch-makepkg-action@master
with:
packages:
scripts: |
@@ -1620,7 +1619,7 @@ jobs:
- name: Publish archlinux package
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1658,14 +1657,14 @@ jobs:
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
@@ -1683,7 +1682,7 @@ jobs:
free -m
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.SCITER_RUST_VERSION }}
targets: ${{ matrix.job.target }}
@@ -1694,7 +1693,7 @@ jobs:
RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
- uses: rustdesk-org/run-on-arch-action@amd64-support
name: Build rustdesk sciter binary for ${{ matrix.job.arch }}
id: vcpkg
with:
@@ -1822,7 +1821,7 @@ jobs:
# build rustdesk
python3 ./res/inline-sciter.py
export CARGO_INCREMENTAL=0
cargo build --locked --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1
cargo build --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1
# make debian package
mkdir -p ./Release
mv ./target/release/rustdesk ./Release/rustdesk
@@ -1840,7 +1839,7 @@ jobs:
- name: Publish debian package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1848,7 +1847,7 @@ jobs:
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
- name: Upload deb
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
if: env.UPLOAD_ARTIFACT == 'true'
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
@@ -1867,12 +1866,12 @@ jobs:
- { target: aarch64-unknown-linux-gnu, arch: aarch64 }
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Download Binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
path: .
@@ -1897,7 +1896,7 @@ jobs:
- name: Publish appimage package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1940,12 +1939,12 @@ jobs:
}
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Download Binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb
path: .
@@ -1954,7 +1953,7 @@ jobs:
run: |
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb flatpak/rustdesk.deb
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
- uses: rustdesk-org/run-on-arch-action@amd64-support
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
id: flatpak
with:
@@ -1982,7 +1981,7 @@ jobs:
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk
- name: Publish flatpak package
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -2001,7 +2000,7 @@ jobs:
RELEASE_NAME: web-basic
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
@@ -2011,7 +2010,7 @@ jobs:
sudo apt-get install -y wget npm
- name: Install flutter
uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 # v2.12.0; https://github.com/subosito/flutter-action/issues/277
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -2055,7 +2054,7 @@ jobs:
- name: Publish web
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}

View File

@@ -17,7 +17,7 @@ env:
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VERSION: "1.4.7"
VERSION: "1.4.6"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -79,21 +79,21 @@ jobs:
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
submodules: recursive
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 # v1
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
@@ -107,7 +107,7 @@ jobs:
- name: Import notarize key
if: env.MACOS_P12_BASE64 != null
uses: timheuer/base64-to-file@adaa40c0c581f276132199d4cf60afa07ce60eac # v1.2
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
@@ -129,19 +129,19 @@ jobs:
brew install llvm create-dmg nasm pkg-config
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ matrix.job.flutter }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}
@@ -156,7 +156,7 @@ jobs:
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -165,7 +165,7 @@ jobs:
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
- name: Restore from cache and install vcpkg
uses: lukka/run-vcpkg@8a5116de2b552d6fc8894e9774aacaf2e5db4823 # v7 2026-05-26
uses: lukka/run-vcpkg@v7
if: false
with:
setupOnly: true
@@ -222,7 +222,7 @@ jobs:
done
- name: Publish DMG package
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -247,7 +247,7 @@ jobs:
}
steps:
- name: Checkout source code
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
submodules: recursive
@@ -290,13 +290,13 @@ jobs:
wget
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
components: "rustfmt"
@@ -310,14 +310,14 @@ jobs:
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
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: ${{ env.NDK_VERSION }}
add-to-path: true
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -395,7 +395,7 @@ jobs:
mkdir -p signed-apk; pushd signed-apk
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk
- uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
@@ -410,7 +410,7 @@ jobs:
BUILD_TOOLS_VERSION: "30.0.2"
- name: Publish signed apk package
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}

View File

@@ -39,7 +39,7 @@ jobs:
build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }}
steps:
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
uses: microsoft/setup-msbuild@v2
- name: Download the source code
run: |
@@ -52,7 +52,7 @@ jobs:
msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }}
- name: Archive build artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
if: ${{ inputs.upload-artifact }}
with:
name: topmostwindow-artifacts

View File

@@ -1,85 +0,0 @@
name: wf-cliprdr CI
on:
workflow_dispatch:
pull_request:
paths:
- "libs/clipboard/src/windows/**"
- "tests/test_invariant_wf_cliprdr.c"
- ".github/workflows/wf-cliprdr-ci.yml"
push:
branches:
- master
paths:
- "libs/clipboard/src/windows/**"
- "tests/test_invariant_wf_cliprdr.c"
- ".github/workflows/wf-cliprdr-ci.yml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: wf_cliprdr invariant test
runs-on: windows-2022
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Set up MSVC
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
with:
arch: x64
- name: Setup vcpkg with GitHub Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: C:\vcpkg
doNotCache: false
- name: Install vcpkg dependency
shell: pwsh
run: |
& "$env:VCPKG_ROOT\vcpkg.exe" install check:x64-windows --classic --x-install-root="$env:VCPKG_ROOT\installed"
- name: Build test
shell: pwsh
run: |
$testRoot = Join-Path $env:GITHUB_WORKSPACE 'build\wf-cliprdr'
New-Item -ItemType Directory -Force $testRoot | Out-Null
$testSource = (($env:GITHUB_WORKSPACE -replace '\\', '/') + '/tests/test_invariant_wf_cliprdr.c')
$cmakeLists = @(
'cmake_minimum_required(VERSION 3.20)'
'project(test_invariant_wf_cliprdr C)'
''
'set(CMAKE_C_STANDARD 11)'
'set(CMAKE_C_STANDARD_REQUIRED ON)'
'set(CMAKE_C_EXTENSIONS OFF)'
''
'find_package(check CONFIG REQUIRED)'
''
'add_executable(test_invariant_wf_cliprdr'
' "TEST_SOURCE"'
')'
''
'target_link_libraries(test_invariant_wf_cliprdr PRIVATE'
' $<$<TARGET_EXISTS:Check::check>:Check::check>'
' $<$<NOT:$<TARGET_EXISTS:Check::check>>:Check::checkShared>'
')'
) -join [Environment]::NewLine
$cmakeLists.Replace('TEST_SOURCE', $testSource) | Set-Content -NoNewline (Join-Path $testRoot 'CMakeLists.txt')
cmake -S $testRoot -B (Join-Path $testRoot 'out') -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build (Join-Path $testRoot 'out') --config Release
- name: Run test
shell: pwsh
run: .\build\wf-cliprdr\out\Release\test_invariant_wf_cliprdr.exe

15
.github/workflows/winget.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Publish to WinGet
on:
release:
types: [released]
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: vedantmgoyal9/winget-releaser@main
with:
identifier: RustDesk.RustDesk
version: "1.4.6"
release-tag: "1.4.6"
token: ${{ secrets.WINGET_TOKEN }}

View File

@@ -1,86 +0,0 @@
# RustDesk Guide
## Project Layout
### Directory Structure
* `src/` Rust app
* `src/server/` audio / clipboard / input / video / network
* `src/platform/` platform-specific code
* `src/ui/` legacy Sciter UI (deprecated)
* `flutter/` current UI
* `libs/hbb_common/` config / proto / shared utils
* `libs/scrap/` screen capture
* `libs/enigo/` input control
* `libs/clipboard/` clipboard
* `libs/hbb_common/src/config.rs` all options
### Key Components
- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
- **Screen Capture**: Platform-specific screen capture in `libs/scrap/`
- **Input Handling**: Cross-platform input simulation in `libs/enigo/`
- **Audio/Video Services**: Real-time audio/video streaming in `src/server/`
- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/`
### UI Architecture
- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/`
- **Modern UI**: Flutter-based - files in `flutter/`
- Desktop: `flutter/lib/desktop/`
- Mobile: `flutter/lib/mobile/`
- Shared: `flutter/lib/common/` and `flutter/lib/models/`
## Rust Rules
* Avoid `unwrap()` / `expect()` in production code.
* Exceptions:
* tests;
* lock acquisition where failure means poisoning, not normal control flow.
* Otherwise prefer `Result` + `?` or explicit handling.
* Do not ignore errors silently.
* Avoid unnecessary `.clone()`.
* Prefer borrowing when practical.
* Do not add dependencies unless needed.
* Keep code simple and idiomatic.
## Tokio Rules
* Assume a Tokio runtime already exists.
* Never create nested runtimes.
* Never call `Runtime::block_on()` inside Tokio / async code.
* Do not hide runtime creation inside helpers or libraries.
* Do not hold locks across `.await`.
* Prefer `.await`, `tokio::spawn`, channels.
* Use `spawn_blocking` or dedicated threads for blocking work.
* Do not use `std::thread::sleep()` in async code.
## Editing Hygiene
* Change only what is required.
* Prefer the smallest valid diff.
* Do not refactor unrelated code.
* Do not make formatting-only changes.
* Keep naming/style consistent with nearby code.
## Localization (`src/lang/*.rs`)
Each file is a `HashMap<key, translation>`. Layout:
* `template.rs` is the master list of every key. **Never edit it** as part of translation work.
* `en.rs` holds only the keys whose English display text differs from the key itself.
* Every other file (`de.rs`, `fr.rs`, …) carries the full key set; an untranslated entry has an empty value: `("key", "")`.
### Finding the English source for a key
When filling an empty entry, determine the source English text with this rule:
* If `key` exists in `en.rs` **with a non-empty value**, that value is the source text (look it up in `en.rs`).
* Otherwise the **key string itself is the source text** (the key is already plain English).
Then translate that source into the file's target language (infer the language from the file's existing non-empty entries / filename).
### Translation hygiene
* Only fill empty values. Never change keys, and never touch existing non-empty translations.
* Preserve placeholders (`{}`) and escape sequences (`\n`, `\"`) exactly as in the source.
* Do not translate brand or technical tokens: `RustDesk`, `Socks5`, `TLS`, `UAC`, `Wayland`, `X11`, `TCP`, `UDP`, `2FA`, `RDP`, `D3D`, etc.
* Copy URL values (e.g. `doc_*` keys) verbatim from `en.rs`.

View File

@@ -1 +1,91 @@
AGENTS.md
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
### Build Commands
- `cargo run` - Build and run the desktop application (requires libsciter library)
- `python3 build.py --flutter` - Build Flutter version (desktop)
- `python3 build.py --flutter --release` - Build Flutter version in release mode
- `python3 build.py --hwcodec` - Build with hardware codec support
- `python3 build.py --vram` - Build with VRAM feature (Windows only)
- `cargo build --release` - Build Rust binary in release mode
- `cargo build --features hwcodec` - Build with specific features
### Flutter Mobile Commands
- `cd flutter && flutter build android` - Build Android APK
- `cd flutter && flutter build ios` - Build iOS app
- `cd flutter && flutter run` - Run Flutter app in development mode
- `cd flutter && flutter test` - Run Flutter tests
### Testing
- `cargo test` - Run Rust tests
- `cd flutter && flutter test` - Run Flutter tests
### Platform-Specific Build Scripts
- `flutter/build_android.sh` - Android build script
- `flutter/build_ios.sh` - iOS build script
- `flutter/build_fdroid.sh` - F-Droid build script
## Project Architecture
### Directory Structure
- **`src/`** - Main Rust application code
- `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead)
- `src/server/` - Audio/clipboard/input/video services and network connections
- `src/client.rs` - Peer connection handling
- `src/platform/` - Platform-specific code
- **`flutter/`** - Flutter UI code for desktop and mobile
- **`libs/`** - Core libraries
- `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities
- `libs/scrap/` - Screen capture functionality
- `libs/enigo/` - Platform-specific keyboard/mouse control
- `libs/clipboard/` - Cross-platform clipboard implementation
### Key Components
- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server
- **Screen Capture**: Platform-specific screen capture in `libs/scrap/`
- **Input Handling**: Cross-platform input simulation in `libs/enigo/`
- **Audio/Video Services**: Real-time audio/video streaming in `src/server/`
- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/`
### UI Architecture
- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/`
- **Modern UI**: Flutter-based - files in `flutter/`
- Desktop: `flutter/lib/desktop/`
- Mobile: `flutter/lib/mobile/`
- Shared: `flutter/lib/common/` and `flutter/lib/models/`
## Important Build Notes
### Dependencies
- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom`
- Set `VCPKG_ROOT` environment variable
- Download appropriate Sciter library for legacy UI support
### Ignore Patterns
When working with files, ignore these directories:
- `target/` - Rust build artifacts
- `flutter/build/` - Flutter build output
- `flutter/.dart_tool/` - Flutter tooling files
### Cross-Platform Considerations
- Windows builds require additional DLLs and virtual display drivers
- macOS builds need proper signing and notarization for distribution
- Linux builds support multiple package formats (deb, rpm, AppImage)
- Mobile builds require platform-specific toolchains (Android SDK, Xcode)
### Feature Flags
- `hwcodec` - Hardware video encoding/decoding
- `vram` - VRAM optimization (Windows only)
- `flutter` - Enable Flutter UI
- `unix-file-copy-paste` - Unix file clipboard support
- `screencapturekit` - macOS ScreenCaptureKit (macOS only)
### Config
All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types:
- Settings
- Local
- Display
- Built-in

362
Cargo.lock generated
View File

@@ -33,12 +33,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aead"
version = "0.5.2"
@@ -299,8 +293,8 @@ dependencies = [
"image 0.25.1",
"log",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
"objc2-app-kit",
"objc2-foundation",
"parking_lot",
"percent-encoding",
"serde 1.0.228",
@@ -643,7 +637,7 @@ dependencies = [
"cc",
"cfg-if 1.0.0",
"libc",
"miniz_oxide 0.7.4",
"miniz_oxide",
"object",
"rustc-demangle",
]
@@ -866,15 +860,6 @@ dependencies = [
"objc2 0.5.2",
]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2 0.6.4",
]
[[package]]
name = "blocking"
version = "1.6.1"
@@ -1197,7 +1182,7 @@ dependencies = [
"js-sys",
"num-traits 0.2.19",
"wasm-bindgen",
"windows-link 0.1.1",
"windows-link",
]
[[package]]
@@ -1305,8 +1290,8 @@ dependencies = [
"lazy_static",
"libc",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
"objc2-app-kit",
"objc2-foundation",
"once_cell",
"parking_lot",
"percent-encoding",
@@ -2231,15 +2216,6 @@ dependencies = [
"dirs-sys 0.4.1",
]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys 0.5.0",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
@@ -2257,7 +2233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users 0.4.5",
"redox_users",
"winapi 0.3.9",
]
@@ -2269,22 +2245,10 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users 0.4.5",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.61.2",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
@@ -2292,7 +2256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users 0.4.5",
"redox_users",
"winapi 0.3.9",
]
@@ -2302,16 +2266,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags 2.9.1",
"objc2 0.6.4",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -2761,7 +2715,7 @@ dependencies = [
"flume",
"half",
"lebe",
"miniz_oxide 0.7.4",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
@@ -2847,12 +2801,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
version = "1.1.9"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
"crc32fast",
"miniz_oxide 0.8.9",
"miniz_oxide",
]
[[package]]
@@ -4087,7 +4041,7 @@ dependencies = [
"gif",
"jpeg-decoder",
"num-traits 0.2.19",
"png 0.17.13",
"png",
"qoi",
"tiff",
]
@@ -4101,7 +4055,7 @@ dependencies = [
"bytemuck",
"byteorder",
"num-traits 0.2.19",
"png 0.17.13",
"png",
"tiff",
]
@@ -4812,16 +4766,6 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "mio"
version = "0.8.11"
@@ -4872,23 +4816,21 @@ dependencies = [
[[package]]
name = "muda"
version = "0.17.1"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a"
checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145"
dependencies = [
"cocoa 0.25.0",
"crossbeam-channel",
"dpi",
"gtk",
"keyboard-types",
"libxdo",
"objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"objc",
"once_cell",
"png 0.17.13",
"thiserror 2.0.17",
"windows-sys 0.60.2",
"png",
"thiserror 1.0.61",
"windows-sys 0.52.0",
]
[[package]]
@@ -5432,16 +5374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
dependencies = [
"objc-sys 0.3.5",
"objc2-encode 4.1.0",
]
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode 4.1.0",
"objc2-encode 4.0.3",
]
[[package]]
@@ -5456,22 +5389,10 @@ dependencies = [
"objc2 0.5.2",
"objc2-core-data",
"objc2-core-image",
"objc2-foundation 0.2.2",
"objc2-foundation",
"objc2-quartz-core",
]
[[package]]
name = "objc2-app-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.9.1",
"objc2 0.6.4",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-cloud-kit"
version = "0.2.2"
@@ -5482,7 +5403,7 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation 0.2.2",
"objc2-foundation",
]
[[package]]
@@ -5493,7 +5414,7 @@ checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-foundation",
]
[[package]]
@@ -5505,28 +5426,7 @@ dependencies = [
"bitflags 2.9.1",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.9.1",
"dispatch2",
"objc2 0.6.4",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags 2.9.1",
"objc2-core-foundation",
"objc2-foundation",
]
[[package]]
@@ -5537,7 +5437,7 @@ checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-foundation",
"objc2-metal",
]
@@ -5550,7 +5450,7 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-contacts",
"objc2-foundation 0.2.2",
"objc2-foundation",
]
[[package]]
@@ -5564,9 +5464,9 @@ dependencies = [
[[package]]
name = "objc2-encode"
version = "4.1.0"
version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8"
[[package]]
name = "objc2-foundation"
@@ -5581,18 +5481,6 @@ dependencies = [
"objc2 0.5.2",
]
[[package]]
name = "objc2-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.9.1",
"block2 0.6.2",
"objc2 0.6.4",
"objc2-core-foundation",
]
[[package]]
name = "objc2-link-presentation"
version = "0.2.2"
@@ -5601,8 +5489,8 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]]
@@ -5614,7 +5502,7 @@ dependencies = [
"bitflags 2.9.1",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-foundation",
]
[[package]]
@@ -5626,7 +5514,7 @@ dependencies = [
"bitflags 2.9.1",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-foundation",
"objc2-metal",
]
@@ -5637,7 +5525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
dependencies = [
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-foundation",
]
[[package]]
@@ -5653,7 +5541,7 @@ dependencies = [
"objc2-core-data",
"objc2-core-image",
"objc2-core-location",
"objc2-foundation 0.2.2",
"objc2-foundation",
"objc2-link-presentation",
"objc2-quartz-core",
"objc2-symbols",
@@ -5669,7 +5557,7 @@ checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-foundation",
]
[[package]]
@@ -5682,7 +5570,7 @@ dependencies = [
"block2 0.5.1",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation 0.2.2",
"objc2-foundation",
]
[[package]]
@@ -5996,8 +5884,8 @@ dependencies = [
[[package]]
name = "parity-tokio-ipc"
version = "0.7.3-6"
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#d0ae39bffe5d5a3e8d82a1b6bcb1ca5a9b2f1c01"
version = "0.7.3-5"
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291"
dependencies = [
"futures",
"libc",
@@ -6290,20 +6178,7 @@ dependencies = [
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.7.4",
]
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags 2.9.1",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.8.9",
"miniz_oxide",
]
[[package]]
@@ -6988,17 +6863,6 @@ dependencies = [
"thiserror 1.0.61",
]
[[package]]
name = "redox_users"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.15",
"libredox",
"thiserror 2.0.17",
]
[[package]]
name = "regex"
version = "1.11.1"
@@ -7270,7 +7134,7 @@ dependencies = [
[[package]]
name = "rustdesk"
version = "1.4.7"
version = "1.4.6"
dependencies = [
"android-wakelock",
"android_logger",
@@ -7385,7 +7249,7 @@ dependencies = [
[[package]]
name = "rustdesk-portable-packer"
version = "1.4.7"
version = "1.4.6"
dependencies = [
"brotli",
"dirs 5.0.1",
@@ -8117,8 +7981,8 @@ dependencies = [
"log",
"memmap2",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle 0.6.2",
"redox_syscall 0.5.2",
@@ -8448,7 +8312,7 @@ dependencies = [
"objc",
"once_cell",
"parking_lot",
"png 0.17.13",
"png",
"raw-window-handle 0.6.2",
"scopeguard",
"tao-macros",
@@ -8702,7 +8566,7 @@ dependencies = [
"bytemuck",
"cfg-if 1.0.0",
"log",
"png 0.17.13",
"png",
"tiny-skia-path",
]
@@ -9075,22 +8939,21 @@ dependencies = [
[[package]]
name = "tray-icon"
version = "0.21.3"
source = "git+https://github.com/tauri-apps/tray-icon#0a5835b0e6828e37a1f781de9c2d671ae7a939e6"
version = "0.14.3"
source = "git+https://github.com/tauri-apps/tray-icon#d4078696edba67b0ab42cef67e6a421a0332c96f"
dependencies = [
"core-graphics 0.23.2",
"crossbeam-channel",
"dirs 6.0.0",
"dirs 5.0.1",
"libappindicator",
"muda",
"objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"once_cell",
"png 0.18.1",
"thiserror 2.0.17",
"windows-sys 0.60.2",
"png",
"thiserror 1.0.61",
"windows-sys 0.52.0",
]
[[package]]
@@ -10195,7 +10058,7 @@ dependencies = [
"windows-collections",
"windows-core 0.61.0",
"windows-future",
"windows-link 0.1.1",
"windows-link",
"windows-numerics",
]
@@ -10244,7 +10107,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link 0.1.1",
"windows-link",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
@@ -10256,7 +10119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
dependencies = [
"windows-core 0.61.0",
"windows-link 0.1.1",
"windows-link",
]
[[package]]
@@ -10309,12 +10172,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.2.0"
@@ -10322,7 +10179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.0",
"windows-link 0.1.1",
"windows-link",
]
[[package]]
@@ -10340,7 +10197,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link 0.1.1",
"windows-link",
]
[[package]]
@@ -10360,7 +10217,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link 0.1.1",
"windows-link",
]
[[package]]
@@ -10369,7 +10226,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link 0.1.1",
"windows-link",
]
[[package]]
@@ -10399,24 +10256,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -10456,30 +10295,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link 0.2.1",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows-version"
version = "0.1.1"
@@ -10516,12 +10338,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.32.0"
@@ -10552,12 +10368,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.32.0"
@@ -10588,24 +10398,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.32.0"
@@ -10636,12 +10434,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.32.0"
@@ -10672,12 +10464,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -10696,12 +10482,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.32.0"
@@ -10732,12 +10512,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winit"
version = "0.30.9"
@@ -10762,8 +10536,8 @@ dependencies = [
"memmap2",
"ndk 0.9.0",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-ui-kit",
"orbclient",
"percent-encoding",

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.4.7"
version = "1.4.6"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"
@@ -160,7 +160,7 @@ piet-coregraphics = "0.6"
foreign-types = "0.3"
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
tray-icon = { git = "https://github.com/tauri-apps/tray-icon", version = "0.21.3" }
tray-icon = { git = "https://github.com/tauri-apps/tray-icon" }
tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" }
image = "0.24"
@@ -245,6 +245,3 @@ panic = 'abort'
strip = true
#opt-level = 'z' # only have smaller size after strip
rpath = true
[profile.dev]
debug = 1

View File

@@ -1 +0,0 @@
AGENTS.md

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.4.7
version: 1.4.6
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.4.7
version: 1.4.6
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -172,7 +172,7 @@ def generate_build_script_for_docker():
# flutter_rust_bridge
dart pub global activate ffigen --version 5.0.1
pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd
pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . --locked && popd
pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd
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
# install vcpkg
@@ -299,7 +299,7 @@ Version: %s
Architecture: %s
Maintainer: rustdesk <info@rustdesk.com>
Homepage: https://rustdesk.com
Depends: libgtk-3-0t64 | libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2t64 | libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
Depends: libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
Recommends: libayatana-appindicator3-1
Description: A remote control software.
@@ -317,7 +317,7 @@ def ffi_bindgen_function_refactor():
def build_flutter_deb(version, features):
if not skip_cargo:
system2(f'cargo build --locked --features {features} --lib --release')
system2(f'cargo build --features {features} --lib --release')
ffi_bindgen_function_refactor()
os.chdir('flutter')
system2('flutter build linux --release')
@@ -405,7 +405,7 @@ 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
system2(
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --locked --features {features} --release')
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --release')
# copy dylib
system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
@@ -422,7 +422,7 @@ def build_flutter_dmg(version, features):
def build_flutter_arch_manjaro(version, features):
if not skip_cargo:
system2(f'cargo build --locked --features {features} --lib --release')
system2(f'cargo build --features {features} --lib --release')
ffi_bindgen_function_refactor()
os.chdir('flutter')
system2('flutter build linux --release')
@@ -433,7 +433,7 @@ def build_flutter_arch_manjaro(version, features):
def build_flutter_windows(version, features, skip_portable_pack):
if not skip_cargo:
system2(f'cargo build --locked --features {features} --lib --release')
system2(f'cargo build --features {features} --lib --release')
if not os.path.exists("target/release/librustdesk.dll"):
print("cargo build failed, please check rust source code.")
exit(-1)
@@ -489,13 +489,13 @@ def main():
if windows:
# build virtual display dynamic library
os.chdir('libs/virtual_display/dylib')
system2('cargo build --locked --release')
system2('cargo build --release')
os.chdir('../../..')
if flutter:
build_flutter_windows(version, features, args.skip_portable_pack)
return
system2('cargo build --locked --release --features ' + features)
system2('cargo build --release --features ' + features)
# system2('upx.exe target/release/rustdesk.exe')
system2('mv target/release/rustdesk.exe target/release/RustDesk.exe')
pa = os.environ.get('P')
@@ -512,14 +512,14 @@ def main():
system2('pip3 install -r requirements.txt')
system2(
f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe')
system2(f'mv ../../{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)
if flutter:
build_flutter_arch_manjaro(version, features)
else:
system2('cargo build --locked --release --features ' + features)
system2('cargo build --release --features ' + features)
system2('git checkout src/ui/common.tis')
system2('strip target/release/rustdesk')
system2('ln -s res/pacman_install && ln -s res/PKGBUILD')
@@ -528,7 +528,7 @@ def main():
version, version))
# pacman -U ./rustdesk.pkg.tar.zst
elif os.path.isfile('/usr/bin/yum'):
system2('cargo build --locked --release --features ' + features)
system2('cargo build --release --features ' + features)
system2('strip target/release/rustdesk')
system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version)
@@ -538,7 +538,7 @@ def main():
version, version))
# yum localinstall rustdesk.rpm
elif os.path.isfile('/usr/bin/zypper'):
system2('cargo build --locked --release --features ' + features)
system2('cargo build --release --features ' + features)
system2('strip target/release/rustdesk')
system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version)
@@ -557,7 +557,7 @@ def main():
# 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb')
build_flutter_deb(version, features)
else:
system2('cargo --locked bundle --release --features ' + features)
system2('cargo bundle --release --features ' + features)
if osx:
system2(
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk')

View File

@@ -1,143 +0,0 @@
# Code de conduite des contributeurs
## Notre engagement
En tant que membres, contributeurs et responsables, nous nous engageons à faire
de la participation à notre communauté une expérience exempte de harcèlement pour
tous, indépendamment de l'âge, de la taille corporelle, du handicap visible ou
invisible, de l'origine ethnique, des caractéristiques sexuelles, de l'identité
et de l'expression de genre, du niveau d'expérience, de l'éducation, du statut
socio-économique, de la nationalité, de l'apparence personnelle, de la race, de
la religion ou de l'identité et de l'orientation sexuelle.
Nous nous engageons à agir et à interagir de manière à contribuer à une
communauté ouverte, accueillante, diversifiée, inclusive et saine.
## Nos standards
Exemples de comportements qui contribuent à un environnement positif pour notre
communauté :
* Faire preuve d'empathie et de bienveillance envers les autres
* Respecter les opinions, les points de vue et les expériences différents
* Donner et accepter gracieusement les retours constructifs
* Assumer ses responsabilités, s'excuser auprès des personnes affectées par nos
erreurs et apprendre de l'expérience
* Se concentrer sur ce qui est le mieux non seulement pour nous en tant
qu'individus, mais pour l'ensemble de la communauté
Exemples de comportements inacceptables :
* L'utilisation de langage ou d'images à caractère sexuel, et les attentions ou
avances sexuelles de quelque nature que ce soit
* Le trolling, les commentaires insultants ou désobligeants, et les attaques
personnelles ou politiques
* Le harcèlement public ou privé
* La publication d'informations privées d'autrui, telles qu'une adresse physique
ou électronique, sans autorisation explicite
* Tout autre comportement qui pourrait raisonnablement être considéré comme
inapproprié dans un cadre professionnel
## Responsabilités en matière d'application
Les responsables de la communauté sont chargés de clarifier et d'appliquer nos
standards de comportement acceptable et prendront des mesures correctives
appropriées et équitables en réponse à tout comportement qu'ils jugent
inapproprié, menaçant, offensant ou nuisible.
Les responsables de la communauté ont le droit et la responsabilité de
supprimer, modifier ou rejeter les commentaires, commits, code, modifications
du wiki, issues et autres contributions qui ne sont pas conformes à ce Code de
conduite, et communiqueront les raisons de leurs décisions de modération le cas
échéant.
## Portée
Ce Code de conduite s'applique dans tous les espaces communautaires, et
s'applique également lorsqu'une personne représente officiellement la communauté
dans les espaces publics. Les exemples de représentation de notre communauté
incluent l'utilisation d'une adresse e-mail officielle, la publication via un
compte de réseau social officiel, ou le fait d'agir en tant que représentant
désigné lors d'un événement en ligne ou hors ligne.
## Application
Les cas de comportements abusifs, harcelants ou autrement inacceptables peuvent
être signalés aux responsables de la communauté chargés de l'application à
[info@rustdesk.com](mailto:info@rustdesk.com).
Toutes les plaintes seront examinées et feront l'objet d'une enquête rapide et
équitable.
Tous les responsables de la communauté sont tenus de respecter la vie privée et
la sécurité de la personne ayant signalé un incident.
## Directives d'application
Les responsables de la communauté suivront ces Directives d'impact communautaire
pour déterminer les conséquences de toute action qu'ils jugent en violation de ce
Code de conduite :
### 1. Correction
**Impact communautaire** : Utilisation d'un langage inapproprié ou autre
comportement jugé non professionnel ou indésirable dans la communauté.
**Conséquence** : Un avertissement écrit et privé de la part des responsables de
la communauté, expliquant la nature de la violation et pourquoi le comportement
était inapproprié. Des excuses publiques peuvent être demandées.
### 2. Avertissement
**Impact communautaire** : Une violation par un incident isolé ou une série
d'actions.
**Conséquence** : Un avertissement avec des conséquences en cas de comportement
répété. Aucune interaction avec les personnes impliquées, y compris les
interactions non sollicitées avec les personnes chargées d'appliquer le Code de
conduite, pendant une période déterminée. Cela inclut d'éviter les interactions
dans les espaces communautaires ainsi que dans les canaux externes comme les
réseaux sociaux. Le non-respect de ces conditions peut entraîner une exclusion
temporaire ou permanente.
### 3. Exclusion temporaire
**Impact communautaire** : Une violation grave des standards communautaires, y
compris un comportement inapproprié persistant.
**Conséquence** : Une exclusion temporaire de toute interaction ou communication
publique avec la communauté pendant une période déterminée. Aucune interaction
publique ou privée avec les personnes impliquées, y compris les interactions non
sollicitées avec les personnes chargées d'appliquer le Code de conduite, n'est
autorisée pendant cette période. Le non-respect de ces conditions peut entraîner
une exclusion permanente.
### 4. Exclusion permanente
**Impact communautaire** : Démontrer un schéma de violation des standards
communautaires, y compris un comportement inapproprié persistant, le harcèlement
d'une personne, ou une agression envers des catégories de personnes ou leur
dénigrement.
**Conséquence** : Une exclusion permanente de toute interaction publique au sein
de la communauté.
## Attribution
Ce Code de conduite est adapté du [Contributor Covenant][homepage], version 2.0,
disponible à l'adresse
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Les Directives d'impact communautaire ont été inspirées par
[l'échelle d'application du code de conduite de Mozilla][Mozilla CoC].
Pour des réponses aux questions fréquentes sur ce code de conduite, consultez la
FAQ à l'adresse [https://www.contributor-covenant.org/faq][FAQ]. Des traductions
sont disponibles à l'adresse
[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

View File

@@ -1,55 +0,0 @@
# Contribuer à RustDesk
RustDesk accueille les contributions de tous. Voici les directives si vous
envisagez de nous aider :
## Contributions
Les contributions à RustDesk ou à ses dépendances doivent être soumises sous
forme de pull requests GitHub. Chaque pull request sera examinée par un
contributeur principal (une personne ayant la permission d'intégrer des
correctifs) et sera soit intégrée dans la branche principale, soit accompagnée
de retours sur les modifications requises. Toutes les contributions doivent
suivre ce format, même celles des contributeurs principaux.
Si vous souhaitez travailler sur une issue, veuillez d'abord la revendiquer en
commentant sur l'issue GitHub indiquant que vous souhaitez la traiter. Cela
permet d'éviter les efforts en double de la part des contributeurs sur la même
issue.
## Liste de vérification pour les pull requests
- Partez de la branche master et, si nécessaire, effectuez un rebase sur la
branche master actuelle avant de soumettre votre pull request. Si elle ne
fusionne pas proprement avec master, il vous sera peut-être demandé de
rebaser vos modifications.
- Les commits doivent être aussi petits que possible, tout en s'assurant que
chaque commit est correct de manière indépendante (c.-à-d. que chaque commit
doit compiler et passer les tests).
- Les commits doivent être accompagnés d'une signature Developer Certificate of
Origin (http://developercertificate.org), indiquant que vous (et votre
employeur le cas échéant) acceptez d'être liés par les termes de la
[licence du projet](../LICENCE). Dans git, il s'agit de l'option `-s` de
`git commit`.
- Si votre correctif n'est pas examiné ou si vous avez besoin qu'une personne
spécifique l'examine, vous pouvez @-mentionner un relecteur pour demander une
revue dans la pull request ou un commentaire, ou vous pouvez demander une
revue par [e-mail](mailto:info@rustdesk.com).
- Ajoutez des tests relatifs au bug corrigé ou à la nouvelle fonctionnalité.
Pour des instructions git spécifiques, consultez le
[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## Conduite
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
## Communication
Les contributeurs de RustDesk se retrouvent fréquemment sur
[Discord](https://discord.gg/nDceKgxnkV).

View File

@@ -34,9 +34,9 @@ Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface
- Installez [vcpkg](https://github.com/microsoft/vcpkg), et définissez correctement la variable d'environnement `VCPKG_ROOT`.
- Windows : vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/macOS : vcpkg install libvpx libyuv opus aom
- Linux/Osx : vcpkg install libvpx libyuv opus aom
- Exécutez `cargo run`
- Exécuter `cargo run`
## Comment compiler/build sous Linux
@@ -93,7 +93,7 @@ cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
mv libsciter-gtk.so target/debug
cargo run
Exécution du cargo
```
## Comment construire avec Docker

View File

@@ -1,10 +1,10 @@
<p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#빌드를_위한_원시_단계">빌드</a> •
<a href="#Docker로_빌드하는_방법">Docker</a> •
<a href="#파일_구조">구조</a> •
<a href="#스크린샷">스샷</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>] | [<a href="README-NO.md">Norsk</a>] | [<a href="README-RO.md">Română</a>]<br>
<a href="#빌드를 위한 원시 단계">빌드</a> •
<a href="#Docker로 빌드하는 방법">Docker</a> •
<a href="#파일 구조">구조</a> •
<a href="#스크린샷">스샷</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>] | [<a href="README-NO.md">Norsk</a>]<br>
<b>이 README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> 및 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk 문서</a>를 귀하의 모국어로 번역하는 데 도움이 필요합니다</b>
</p>
@@ -46,9 +46,9 @@ Sciter 동적 라이브러리를 직접 다운로드하세요.
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## 빌드를_위한_원시_단계
## 빌드를 위한 원시 단계
- Rust 개발 환경과 C++ 빌드 환경 준비
- Rust 개발 환경과 C++ 빌드 환경 준비합니다
- [vcpkg](https://github.com/microsoft/vcpkg)를 설치하고 `VCPKG_ROOT` 환경 변수를 올바르게 설정합니다
@@ -125,7 +125,7 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run
```
## Docker로_빌드하는_방법
## Docker로 빌드하는 방법
먼저 리포지토리를 복제하고 Docker 컨테이너를 빌드합니다:
@@ -156,7 +156,7 @@ target/release/rustdesk
RustDesk 리포지토리의 루트에서 이러한 명령을 실행하고 있는지 확인하세요. 그렇지 않으면 응용 프로그램이 필요한 리소스를 찾지 못할 수 있습니다. 또한 `install` 또는 `run` 과 같은 다른 cargo 하위 명령은 호스트가 아닌 컨테이너 내부에 프로그램을 설치하거나 실행하므로 현재 이 방법을 통해 지원되지 않는다는 점에 유의하세요.
## 파일_구조
## 파일 구조
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 구성, tcp/udp wrapper, protobuf, 파일 전송을 위한 fs 함수 및 기타 유틸리티 함수
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡쳐

View File

@@ -1,82 +1,55 @@
<p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Seu desktop remoto"><br>
<a href="#compilar">Compilar</a> •
<a href="#como-compilar-com-o-docker">Docker</a> •
<a href="#servidores-públicos-grátis">Servidores</a> •
<a href="#compilação-crua">Compilar</a> •
<a href="#como-compilar-com-docker">Docker</a> •
<a href="#estrutura-de-arquivos">Estrutura</a> •
<a href="#capturas-de-tela">Capturas de Tela</a><br>
[<a href="../README.md">Inglês</a>] | [<a href="docs/README-UA.md">Ucraniano</a>] | [<a href="docs/README-CS.md">Tcheco</a>] | [<a href="docs/README-ZH.md">Chinês</a>] | [<a href="docs/README-HU.md">Húngaro</a>] | [<a href="docs/README-ES.md">Espanhol</a>] | [<a href="docs/README-FA.md">Persa</a>] | [<a href="docs/README-FR.md">Frans</a>] | [<a href="docs/README-DE.md">Alemão</a>] | [<a href="docs/README-PL.md">Polonês</a>] | [<a href="docs/README-ID.md">Indonésio</a>] | [<a href="docs/README-FI.md">Finlandês</a>] | [<a href="docs/README-ML.md">Malaiala</a>] | [<a href="docs/README-JP.md">Japonês</a>] | [<a href="docs/README-NL.md">Holandês</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Russo</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">Coreano</a>] | [<a href="docs/README-AR.md">Árabe</a>] | [<a href="docs/README-VN.md">Vietnamita</a>] | [<a href="docs/README-DA.md">Dinamarquês</a>] | [<a href="docs/README-GR.md">Grego</a>] | [<a href="docs/README-TR.md">Turco</a>] | [<a href="docs/README-NO.md">Norueguês</a>] | [<a href="docs/README-RO.md">Romeno</a>]<br>
<b>Precisamos da sua ajuda para traduzir este README, a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">Interface do RustDesk</a> e a <a href="https://github.com/rustdesk/doc.rustdesk.com">Documentação do RustDesk</a> para o seu idioma nativo</b>
<a href="#screenshots">Screenshots</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
<b>Precisamos de sua ajuda para traduzir este README e a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI do RustDesk</a> para sua língua nativa</b>
</p>
> [!Caution]
> **Aviso de Isenção de Responsabilidade por Uso Indevido:** <br>
> Os desenvolvedores do RustDesk não toleram ou apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, viola estritamente nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido do aplicativo.
Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Advanced%20Features-blue)](https://rustdesk.com/pricing.html)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Recursos%20Avan%C3%A7ados-blue)](https://rustdesk.com/pricing.html)
Mais uma solução de desktop remoto, escrita em Rust. Funciona imediatamente, sem necessidade de configuração. Você tem controle total dos seus dados, sem preocupações com segurança. Você pode usar nosso servidor de conexão/retransmissão (rendezvous/relay), [configurar o seu próprio](https://rustdesk.com/server) ou [escrever seu próprio servidor de conexão/retransmissão](https://github.com/rustdesk/rustdesk-server-demo).
Mais um software de desktop remoto, escrito em Rust. Funciona por padrão, sem necessidade de configuração. Você tem completo controle de seus dados, sem se preocupar com segurança. Você pode usar nossos servidores de rendezvous/relay, [configurar seu próprio](https://rustdesk.com/server), ou [escrever seu próprio servidor de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk acolhe contribuições de todos. Leia [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ver como começar.
O RustDesk acolhe a contribuição de todos. Veja [CONTRIBUTING.md](docs/CONTRIBUTING.md) para ajuda em como começar.
[**Perguntas Frequentes (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**DOWNLOAD DOS BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases)
[**VERSÕES NIGHTLY (EM DESENVOLVIMENTO)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="Baixe no F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
[<img src="https://flathub.org/api/badge?svg&locale=en"
alt="Baixe no Flathub"
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
[**DOWNLOAD DE BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases)
## Dependências
As versões de desktop usam Flutter ou Sciter (descontinuado) para a interface gráfica (GUI). Este tutorial é apenas para o Sciter, por ser mais fácil e amigável para começar. Verifique nosso [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) para instruções de compilação da versão em Flutter.
Por favor, faça o download da biblioteca dinâmica do Sciter por conta própria.
Versões de desktop utilizam [sciter](https://sciter.com/) para a GUI, por favor baixe a biblioteca dinâmica sciter por conta própria.
[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) |
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## Passos básicos para compilar
## Compilação crua
- Prepare seu ambiente de desenvolvimento Rust e o ambiente de compilação C++
- Prepare seu ambiente de desenvolvimento Rust e ambiente de compilação C++
- Instale o [vcpkg](https://github.com/microsoft/vcpkg) e configure a variável de ambiente `VCPKG_ROOT` corretamente
- Instale [vcpkg](https://github.com/microsoft/vcpkg), e configure a variável de ambiente `VCPKG_ROOT` corretamente
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static`
- Linux/macOS: `vcpkg install libvpx libyuv opus aom`
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
- Execute `cargo run`
## [Compilar](https://rustdesk.com/docs/en/dev/build/)
## Como Compilar no Linux
## Como compilar no Linux
### Ubuntu 18 (Debian 10)
```sh
sudo apt install -y zip 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 make libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
```
### 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 pam-devel
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
```
### Fedora 28 (CentOS 8)
```sh
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
```
### Arch (Manjaro)
@@ -85,7 +58,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
```
### Instalar o vcpkg
### Instale vcpkg
```sh
git clone https://github.com/microsoft/vcpkg
@@ -97,7 +70,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
### Corrigir o libvpx (Para Fedora)
### Conserte libvpx (Para o Fedora)
```sh
cd vcpkg/buildtrees/libvpx/src
@@ -110,12 +83,12 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd
```
### Compilar
### Compile
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
git clone --recurse-submodules https://github.com/rustdesk/rustdesk
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
@@ -123,57 +96,57 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run
```
## Como compilar com o Docker
## Como compilar com Docker
Comece clonando o repositório e construindo o contêiner Docker:
Comece clonando o repositório e montando o container docker:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
git submodule update --init --recursive
docker build -t "rustdesk-builder" .
```
Depois, cada vez que precisar compilar o aplicativo, execute o seguinte comando:
Então, sempre que precisar compilar a aplicação, execute este comando:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
Note que a primeira compilação pode demorar mais a que as dependências sejam armazenadas em cache; as compilações subsequentes serão mais rápidas. Além disso, se você precisar especificar argumentos diferentes para o comando de compilação, pode fazê-lo ao final do comando na posição `<ARGUMENTOS-OPCIONAIS>`. Por exemplo, se você quiser compilar uma versão de lançamento (release) otimizada, executaria o comando acima seguido de `--release`. O executável resultante estará disponível na pasta `target` do seu sistema e pode ser executado com:
Note que a primeira compilação pode demorar mais antes que as dependências sejam armazenadas em cache, as compilações subsequentes serão mais rápidas. Adicionalmente, se você precisar especificar argumentos diferentes para o comando de compilação, você pode fazê-lo ao final do comando na posição do `<OPTIONAL-ARGS>`. Por exemplo, se você gostaria de compilar uma versão de release otimizada, você executaria o comando acima seguido de `--release`. O executável gerado estará disponível no diretório alvo no seu sistema, e pode ser executado com:
```sh
target/debug/rustdesk
```
Ou, se estiver executando o executável de lançamento:
Ou, se estiver rodando um executável de release:
```sh
target/release/rustdesk
```
Certifique-se de executar esses comandos a partir da raiz do repositório do RustDesk, do contrário o aplicativo pode não encontrar os recursos necessários. Note também que outros subcomandos do cargo, como `install` ou `run`, não são suportados atualmente por este método, pois instalariam ou executariam o programa dentro do contêiner em vez de no sistema hospedeiro.
Por favor verifique que está executando estes comandos da raiz do repositório do RustDesk, senão a aplicação pode não encontrar os recursos necessários. Note também que outros subcomandos do cargo como `install` ou `run` não são suportados atualmente via este método, já que eles iriam instalar ou rodar o programa dentro do container ao invés do host.
## Estrutura de Arquivos
## Estrutura de arquivos
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec de vídeo, configuração, encapsulador (wrapper) tcp/udp, protobuf, funções de sistema de arquivos para transferência de arquivos e algumas outras funções utilitárias.
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de tela.
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controle de teclado/mouse específico de cada plataforma.
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementação de copiar e colar arquivos para Windows, Linux e macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: interface Sciter antiga (descontinuada).
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: serviços de áudio/área de transferência/entrada/vídeo e conexões de rede.
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: inicia uma conexão direta (peer connection).
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica-se com o [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguarda por conexão remota direta (perfuração de túnel TCP / hole punching) ou retransmitida.
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma.
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: código Flutter para desktop e dispositivos móveis.
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript para o cliente web do Flutter.
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec de vídeo, configurações, wrapper de tcp/udp, protobuf, funções de sistema de arquivos para transferência de arquivos, e outras funções utilitárias
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de tela
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controle de teclado/mouse específico a cada plataforma
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: serviços de áudio/área de transferência/entrada/vídeo, e conexões de rede
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: iniciar uma conexão "peer to peer"
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicação com [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguardar pela conexão remota direta (TCP hole punching) ou conexão indireta (relayed)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico a cada plataforma
## Capturas de Tela
> [!Cuidadob]
> **Aviso de uso indevido:** <br>
> Os desenvolvedores do RustDesk não aprovam nem apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, é estritamente contra nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido da aplicação.
![Gerenciador de Conexões](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
## Screenshots
![Conectado a um PC Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
![Transferência de Arquivos](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png)
![Tunelamento TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)

View File

@@ -1,16 +0,0 @@
# Politique de sécurité
## Signaler une vulnérabilité
Nous accordons une très grande importance à la sécurité du projet. Nous
encourageons tous les utilisateurs à nous signaler toute vulnérabilité qu'ils
découvrent.
Si vous trouvez une vulnérabilité de sécurité dans le projet RustDesk, veuillez
la signaler de manière responsable en envoyant un e-mail à info@rustdesk.com.
À ce stade, nous n'avons pas de programme de bug bounty. Nous sommes une petite
équipe qui s'attaque à un grand défi. Nous vous encourageons vivement à signaler
toute vulnérabilité de manière responsable afin que nous puissions continuer à
développer une application sécurisée pour l'ensemble de la communauté.

View File

@@ -33,4 +33,4 @@ if [ -z $release ]; then
fi
set -f
#shellcheck disable=2086
VCPKG_ROOT=/vcpkg cargo build --locked $argv
VCPKG_ROOT=/vcpkg cargo build $argv

View File

@@ -18,7 +18,7 @@
<li> Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs. </li>
<li> Own your data, easily set up self-hosting solution on your infrastructure. </li>
<li> P2P connection with end-to-end encryption based on NaCl. </li>
<li> No administrative privileges or installation needed for Windows, elevate privilege locally or from remote on demand. </li>
<li> No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand. </li>
<li> We like to keep things simple and will strive to make simpler where possible. </li>
</ul>
<p>
@@ -56,4 +56,4 @@
<control>pointing</control>
</supports>
<content_rating type="oars-1.1"/>
</component>
</component>

View File

@@ -62,13 +62,7 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart:
return false
}
}
val recorder = try {
builder.build()
} catch (e: Exception) {
Log.e(logTag, "createAudioRecorder failed", e)
return false
}
audioRecorder = recorder
audioRecorder = builder.build()
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
return true
}

View File

@@ -311,10 +311,7 @@ class FloatingWindowService : Service(), View.OnTouchListener {
popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard"))
}
val idStopService = 2
val hideStopService = FFI.getBuildinOption("hide-stop-service") == "Y"
if (!hideStopService) {
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
}
popupMenu.menu.add(0, idStopService, 0, translate("Stop service"))
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
idShowRustDesk -> {
@@ -392,3 +389,4 @@ class FloatingWindowService : Service(), View.OnTouchListener {
return false
}
}

View File

@@ -24,7 +24,6 @@ object FFI {
external fun setFrameRawEnable(name: String, value: Boolean)
external fun setCodecInfo(info: String)
external fun getLocalOption(key: String): String
external fun getBuildinOption(key: String): String
external fun onClipboardUpdate(clips: ByteBuffer)
external fun isServiceClipboardEnabled(): Boolean
}

View File

@@ -1 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><rect x="4" y="4" width="19" height="19" fill="#f25022"/><rect x="25" y="4" width="19" height="19" fill="#7fba00"/><rect x="4" y="25" width="19" height="19" fill="#00a4ef"/><rect x="25" y="25" width="19" height="19" fill="#ffb900"/></svg>

Before

Width:  |  Height:  |  Size: 321 B

View File

@@ -7,7 +7,7 @@
# 2024, Vasyl Gello <vasek.gello@gmail.com>
#
# The script is invoked by F-Droid builder system step-by-step.
# The script is invoked by F-Droid builder system ste-by-step.
#
# It accepts the following arguments:
#
@@ -16,6 +16,7 @@
# - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64
# - The build step to execute:
#
# + sudo-deps: as root, install needed Debian packages into builder VM
# + prebuild: patch sources and do other stuff before the build
# + build: perform actual build of APK file
#
@@ -183,9 +184,13 @@ prebuild)
fi
# Map NDK version to revision
NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json |
jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" |
sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')"
NDK_VERSION="$(wget \
-qO- \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
'https://api.github.com/repos/android/ndk/releases' |
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
if [ -z "${NDK_VERSION}" ]; then
echo "ERROR: Can not map Android NDK codename to revision!" >&2
@@ -311,18 +316,6 @@ prebuild)
# `FLUTTER_BRIDGE_VERSION` an restore the pubspec later
if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then
# Find first libclang.so and set BRIDGE_LLVM_PATH
BRIDGE_LLVM_PATH="$(find /usr/lib/ -name libclang.so | head -n1)"
if [ -z "${BRIDGE_LLVM_PATH}" ]; then
echo 'ERROR: Can not find libclang.so for bridge generator!' >&2
exit 1
fi
BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")"
BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")"
# Install Flutter bridge version
prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter"
@@ -351,8 +344,7 @@ prebuild)
flutter_rust_bridge_codegen \
--rust-input ./src/flutter_ffi.rs \
--dart-output ./flutter/lib/generated_bridge.dart \
--llvm-path "${BRIDGE_LLVM_PATH}"
--dart-output ./flutter/lib/generated_bridge.dart
# Add bridge files to save-list
@@ -363,15 +355,13 @@ prebuild)
git checkout '*'
git clean -dffx
git reset
unset BRIDGE_LLVM_PATH
fi
# Install Flutter version for RustDesk library build
prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter"
# gms is not in these files now, but we still keep the following line for future reference(maybe).
# gms is not in thoes files now, but we still keep the following line for future reference(maybe).
sed \
-i \
@@ -424,9 +414,13 @@ build)
.github/workflows/flutter-build.yml)"
# Map NDK version to revision
NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json |
jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" |
sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')"
NDK_VERSION="$(wget \
-qO- \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
'https://api.github.com/repos/android/ndk/releases' |
jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")"
if [ -z "${NDK_VERSION}" ]; then
echo "ERROR: Can not map Android NDK codename to revision!" >&2
@@ -460,7 +454,6 @@ build)
--target "${RUST_TARGET}" \
--bindgen \
build \
--locked \
--release \
--features "${RUSTDESK_FEATURES}"

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo build --locked --features flutter,hwcodec --release --target aarch64-apple-ios --lib
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo build --locked --features flutter --release --target x86_64-apple-ios --lib
cargo build --features flutter --release --target x86_64-apple-ios --lib

View File

@@ -716,17 +716,6 @@ closeConnection({String? id}) {
stateGlobal.isInMainPage = true;
} else {
final controller = Get.find<DesktopTabController>();
if (controller.tabType == DesktopTabType.terminal &&
controller.onCloseWindow != null) {
// Terminal windows are scoped to one peer. The optional id passed to
// closeConnection() is that peer id, not a terminal tab key
// (${peerId}_${terminalId}). Closing from terminal dialogs should close
// the peer's whole terminal window, including all terminal tabs.
unawaited(controller.onCloseWindow!().catchError((e, _) {
debugPrint('[closeConnection] Failed to close terminal window: $e');
}));
return;
}
controller.closeBy(id);
}
}
@@ -2376,19 +2365,6 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
id = uri.path.substring("/new/".length);
} else if (uri.authority == "config") {
if (isAndroid || isIOS) {
final allowDeepLinkServerSettings =
bind.mainGetBuildinOption(key: kOptionAllowDeepLinkServerSettings) ==
'Y';
if (!allowDeepLinkServerSettings) {
debugPrint(
"Ignore rustdesk://config because $kOptionAllowDeepLinkServerSettings is not enabled.");
// Keep the user-facing error generic; detailed rejection reason is in debug logs.
// Delay toast to avoid missing overlay during cold-start deeplink handling.
Timer(Duration(seconds: 1), () {
showToast(translate('Failed'));
});
return null;
}
final config = uri.path.substring("/".length);
// add a timer to make showToast work
Timer(Duration(seconds: 1), () {
@@ -2398,24 +2374,11 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
return null;
} else if (uri.authority == "password") {
if (isAndroid || isIOS) {
final allowDeepLinkPassword =
bind.mainGetBuildinOption(key: kOptionAllowDeepLinkPassword) == 'Y';
if (!allowDeepLinkPassword) {
debugPrint(
"Ignore rustdesk://password because $kOptionAllowDeepLinkPassword is not enabled.");
// Keep the user-facing error generic; detailed rejection reason is in debug logs.
// Delay toast to avoid missing overlay during cold-start deeplink handling.
Timer(Duration(seconds: 1), () {
showToast(translate('Failed'));
});
return null;
}
final password = uri.path.substring("/".length);
if (password.isNotEmpty) {
Timer(Duration(seconds: 1), () async {
final ok =
await bind.mainSetPermanentPasswordWithResult(password: password);
showToast(translate(ok ? 'Successful' : 'Failed'));
await bind.mainSetPermanentPassword(password: password);
showToast(translate('Successful'));
});
}
}
@@ -4190,7 +4153,8 @@ Widget? buildAvatarWidget({
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => fallback ?? SizedBox.shrink(),
errorBuilder: (_, __, ___) =>
fallback ?? SizedBox.shrink(),
),
);
}

View File

@@ -54,9 +54,9 @@ class _AddressBookState extends State<AddressBook> {
const LinearProgressIndicator(),
buildErrorBanner(context,
loading: gFFI.abModel.currentAbLoading,
err: gFFI.abModel.abPullError,
err: gFFI.abModel.currentAbPullError,
retry: null,
close: gFFI.abModel.clearPullErrors),
close: () => gFFI.abModel.currentAbPullError.value = ''),
buildErrorBanner(context,
loading: gFFI.abModel.currentAbLoading,
err: gFFI.abModel.currentAbPushError,

View File

@@ -20,8 +20,7 @@ const kOpSvgList = [
'okta',
'facebook',
'azure',
'auth0',
'microsoft'
'auth0'
];
class _IconOP extends StatelessWidget {
@@ -225,59 +224,21 @@ class _WidgetOPState extends State<WidgetOP> {
return Offstage(
offstage:
_failedMsg.isEmpty && widget.curOP.value != widget.config.op,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (_stateMsg.isNotEmpty && _failedMsg.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SelectableText(
translate(_stateMsg),
style: DefaultTextStyle.of(context)
.style
.copyWith(fontSize: 12),
),
),
if (_failedMsg.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Builder(builder: (context) {
final errorColor =
Theme.of(context).colorScheme.error;
final bgColor = Theme.of(context)
.colorScheme
.errorContainer
.withOpacity(0.3);
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 6.0),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(4.0),
child: RichText(
text: TextSpan(
text: '$_stateMsg ',
style:
DefaultTextStyle.of(context).style.copyWith(fontSize: 12),
children: <TextSpan>[
TextSpan(
text: _failedMsg,
style: DefaultTextStyle.of(context).style.copyWith(
fontSize: 14,
color: Colors.red,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline,
color: errorColor, size: 16),
const SizedBox(width: 6),
Flexible(
child: SelectableText(
translate(_failedMsg),
style: DefaultTextStyle.of(context)
.style
.copyWith(
fontSize: 13,
color: errorColor,
),
),
),
],
),
);
}),
),
],
],
),
),
);
}),

View File

@@ -31,7 +31,7 @@ class RawKeyFocusScope extends StatelessWidget {
// https://github.com/flutter/flutter/issues/154053
final useRawKeyEvents = isLinux && !isWeb;
// FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events,
// while `Alt` and `Control` are separated key events for en-US input method.
// while `Alt` and `Control` are seperated key events for en-US input method.
return FocusScope(
autofocus: true,
child: Focus(
@@ -532,9 +532,7 @@ class _RawTouchGestureDetectorRegionState
// Official
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
() => TapGestureRecognizer(), (instance) {
instance
..onTapDown = onTapDown
..onTapUp = onTapUp
@@ -542,18 +540,14 @@ class _RawTouchGestureDetectorRegionState
}),
DoubleTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
() => DoubleTapGestureRecognizer(), (instance) {
instance
..onDoubleTapDown = onDoubleTapDown
..onDoubleTap = onDoubleTap;
}),
LongPressGestureRecognizer:
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
() => LongPressGestureRecognizer(), (instance) {
instance
..onLongPressDown = onLongPressDown
..onLongPressUp = onLongPressUp
@@ -563,9 +557,7 @@ class _RawTouchGestureDetectorRegionState
// Customized
HoldTapMoveGestureRecognizer:
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
() => HoldTapMoveGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
),
() => HoldTapMoveGestureRecognizer(),
(instance) => instance
..onHoldDragStart = onHoldDragStart
..onHoldDragUpdate = onHoldDragUpdate
@@ -573,18 +565,14 @@ class _RawTouchGestureDetectorRegionState
..onHoldDragEnd = onHoldDragEnd),
DoubleFinerTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
() => DoubleFinerTapGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
() => DoubleFinerTapGestureRecognizer(), (instance) {
instance
..onDoubleFinerTap = onDoubleFinerTap
..onDoubleFinerTapDown = onDoubleFinerTapDown;
}),
CustomTouchGestureRecognizer:
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
() => CustomTouchGestureRecognizer(
supportedDevices: kTouchBasedDeviceKinds,
), (instance) {
() => CustomTouchGestureRecognizer(), (instance) {
instance.onOneFingerPanStart =
(DragStartDetails d) => onOneFingerPanStart(context, d);
instance

View File

@@ -13,70 +13,8 @@ import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
bool isEditOsPassword = false;
const String kPeerOptionAllowWaylandKeyboard = 'allow-wayland-keyboard';
const String kWaylandKeyboardIssueUrl =
'https://github.com/rustdesk/rustdesk/issues/14586';
final Set<String> _waylandKeyboardPromptSuppressedConnectionIds = <String>{};
Future<bool> openWaylandKeyboardIssueUrl() {
return launchUrl(
Uri.parse(kWaylandKeyboardIssueUrl),
mode: LaunchMode.externalApplication,
);
}
bool isWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
return _waylandKeyboardPromptSuppressedConnectionIds.contains(connectionId);
}
void setWaylandKeyboardPromptSuppressedForConnection(
String connectionId, bool suppressed) {
if (suppressed) {
_waylandKeyboardPromptSuppressedConnectionIds.add(connectionId);
} else {
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
}
}
void clearWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
}
bool shouldShowWaylandKeyboardPrompt({
required String connectionId,
required bool isWaylandPeer,
required bool allowWaylandKeyboardRemembered,
}) {
return isWaylandPeer &&
!allowWaylandKeyboardRemembered &&
!isWaylandKeyboardPromptSuppressedForConnection(connectionId);
}
Widget waylandKeyboardScopeChip(BuildContext context, String text) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999),
border: Border.all(color: colorScheme.primary.withOpacity(0.35)),
),
child: Text(
text,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
),
);
}
// macOS privacy mode blacks out all online displays, so switching the remote
// display does not weaken the local privacy protection.
bool allowDisplaySwitchInPrivacyMode(PeerInfo pi) {
return pi.platform == kPeerPlatformMacOS;
}
class TTextMenu {
final Widget child;
@@ -149,179 +87,12 @@ handleOsPasswordAction(
}
}
void showWaylandKeyboardInputWarningDialog(
{required String id,
required String connectionId,
required FFI ffi,
required Future<void> Function() onEnable}) {
bool remember = false;
bool consentInProgress = false;
bool dialogClosed = false;
final dialogFuture = ffi.dialogManager.show((setState, close, context) {
void safeSetState(VoidCallback fn) {
if (dialogClosed) {
return;
}
try {
setState(fn);
} catch (e) {
debugPrint('Ignore setState after dialog disposal: $e');
}
}
void closeDialog() {
if (dialogClosed) {
return;
}
dialogClosed = true;
close();
}
Future<void> enableAndContinue() async {
if (consentInProgress || dialogClosed) {
return;
}
consentInProgress = true;
safeSetState(() {});
try {
await onEnable();
} catch (e, st) {
debugPrint('Failed to enable Wayland keyboard input consent: $e');
debugPrintStack(stackTrace: st);
consentInProgress = false;
safeSetState(() {});
return;
}
ffi.inputModel.keyboardInputAllowed = true;
var rememberPersisted = true;
if (remember) {
try {
await bind.mainSetPeerOption(
id: id,
key: kPeerOptionAllowWaylandKeyboard,
value: bool2option(kPeerOptionAllowWaylandKeyboard, true));
} catch (e) {
rememberPersisted = false;
debugPrint('Failed to persist Wayland keyboard input consent: $e');
}
}
// Always suppress prompt for current connection after explicit consent.
setWaylandKeyboardPromptSuppressedForConnection(connectionId, true);
closeDialog();
if (remember && !rememberPersisted) {
// It's a rare edge case that persisting the user's choice fails.
// Failed to persist the user's choice, but still allow keyboard input for current session.
showToast(translate('Failed'));
}
}
void cancel() {
if (consentInProgress) {
return;
}
closeDialog();
}
return CustomAlertDialog(
title: null,
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
msgboxContent(
'',
'wayland-keyboard-input-disabled-tip',
'wayland-keyboard-input-consent-tip',
),
SizedBox(height: isMobile ? 2 : 6),
if (isMobile) ...[
Text(
translate('wayland-keyboard-input-applies-to-tip'),
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
).marginOnly(bottom: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
waylandKeyboardScopeChip(
context, translate('Send clipboard keystrokes')),
waylandKeyboardScopeChip(
context, translate('wayland-soft-keyboard-input-label')),
],
).marginOnly(bottom: 10),
],
TextButton(
onPressed: consentInProgress
? null
: () async {
try {
final opened = await openWaylandKeyboardIssueUrl();
if (!opened) {
// Opening this optional help link almost never fails in
// normal desktop environments. Keep the result handled
// for review hygiene, but avoid a low-value user toast.
debugPrint('Failed to open Wayland keyboard issue URL');
}
} catch (e) {
debugPrint(
'Failed to open Wayland keyboard issue URL: $e');
}
},
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
translate('Why this happens'),
style: const TextStyle(decoration: TextDecoration.underline),
),
).marginOnly(bottom: 6),
CheckboxListTile(
value: remember,
dense: true,
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
title: Text(translate('remember-wayland-keyboard-choice-tip')),
onChanged: consentInProgress
? null
: (v) {
safeSetState(() => remember = v == true);
},
),
],
),
actions: [
dialogButton(
'Cancel',
onPressed: consentInProgress ? null : cancel,
isOutline: true,
),
dialogButton(
'OK',
onPressed:
consentInProgress ? null : () => unawaited(enableAndContinue()),
),
],
onCancel: consentInProgress ? null : cancel,
onSubmit: consentInProgress ? null : () => unawaited(enableAndContinue()),
);
}, clickMaskDismiss: false, backDismiss: false);
unawaited(dialogFuture.whenComplete(() => dialogClosed = true));
}
List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId;
final isDefaultConn = ffi.connType == ConnType.defaultConn;
final isWaylandPeer = pi.platform == kPeerPlatformLinux && pi.isWayland;
List<TTextMenu> v = [];
// elevation
@@ -371,60 +142,11 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
v.add(TTextMenu(
child: Text(translate('Send clipboard keystrokes')),
onPressed: () async {
Future<void> sendClipboardKeystrokes() async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
bind.sessionInputString(
sessionId: sessionId, value: data.text ?? "");
}
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
bind.sessionInputString(
sessionId: sessionId, value: data.text ?? "");
}
final allowWaylandKeyboard =
mainGetPeerBoolOptionSync(id, kPeerOptionAllowWaylandKeyboard);
if (shouldShowWaylandKeyboardPrompt(
connectionId: sessionId.toString(),
isWaylandPeer: isWaylandPeer,
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
)) {
ffi.inputModel.keyboardInputAllowed = false;
showWaylandKeyboardInputWarningDialog(
id: id,
connectionId: sessionId.toString(),
ffi: ffi,
onEnable: sendClipboardKeystrokes,
);
return;
}
await sendClipboardKeystrokes();
}));
}
if (isDefaultConn &&
isWaylandPeer &&
(mainGetPeerBoolOptionSync(id, kPeerOptionAllowWaylandKeyboard) ||
isWaylandKeyboardPromptSuppressedForConnection(
sessionId.toString()))) {
v.add(TTextMenu(
child: Text(translate('wayland-keyboard-input-reset-choice-tip')),
onPressed: () async {
var persistedCleared = false;
try {
await bind.mainSetPeerOption(
id: id,
key: kPeerOptionAllowWaylandKeyboard,
value: bool2option(kPeerOptionAllowWaylandKeyboard, false));
persistedCleared = true;
} catch (e) {
debugPrint(
'Failed to clear persisted Wayland keyboard permission: $e');
} finally {
clearWaylandKeyboardPromptSuppressedForConnection(
sessionId.toString());
ffi.inputModel.keyboardInputAllowed = false;
if (isMobile) {
await ffi.invokeMethod("enable_soft_keyboard", false);
}
}
showToast(translate(persistedCleared ? 'Successful' : 'Failed'));
}));
}
// reset canvas
@@ -553,6 +275,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
isDesktop &&
ffiModel.keyboard &&
pi.platform != kPeerPlatformAndroid &&
pi.platform != kPeerPlatformMacOS &&
versionCmp(pi.version, '1.2.0') >= 0 &&
bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) {
v.add(TTextMenu(
@@ -962,9 +685,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Lock after session end'))));
}
final privacyModeState = PrivacyModeState.find(id);
if (pi.isSupportMultiDisplay &&
(privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) &&
PrivacyModeState.find(id).isEmpty &&
pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
final value =
@@ -1038,26 +760,15 @@ List<TToggleMenu> toolbarPrivacyMode(
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final sessionId = ffi.sessionId;
final hasPrivacyModePermission =
ffiModel.permissions['privacy_mode'] != false;
// Backend revocation already attempts to turn privacy mode off.
// Still keep this menu when privacy mode is active, so users can turn it off
// if there is a sync delay, version mismatch, or off attempt failure.
if (!hasPrivacyModePermission && privacyModeState.isEmpty) {
return []; // No permission and not active, hide options.
}
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
final enabled = !ffiModel.viewOnly &&
(hasPrivacyModePermission || privacyModeState.isNotEmpty);
final enabled = !ffi.ffiModel.viewOnly;
return TToggleMenu(
value: privacyModeState.isNotEmpty,
onChanged: enabled
? (value) {
if (value == null) return;
if (!allowDisplaySwitchInPrivacyMode(pi) &&
ffiModel.pi.currentDisplay != 0 &&
if (ffiModel.pi.currentDisplay != 0 &&
ffiModel.pi.currentDisplay != kAllDisplayValue) {
msgBox(
sessionId,
@@ -1100,29 +811,18 @@ List<TToggleMenu> toolbarPrivacyMode(
})
];
} else {
final visibleImpls = hasPrivacyModePermission
? privacyModeImpls
: privacyModeImpls.where((e) {
final implKey = (e as List<dynamic>)[0] as String;
return privacyModeState.value == implKey;
}).toList();
return visibleImpls.map((e) {
return privacyModeImpls.map((e) {
final implKey = (e as List<dynamic>)[0] as String;
final implName = (e)[1] as String;
final enabled = !ffiModel.viewOnly &&
(hasPrivacyModePermission || privacyModeState.value == implKey);
return TToggleMenu(
child: Text(translate(implName)),
value: privacyModeState.value == implKey,
onChanged: enabled
? (value) {
if (value == null) return;
if (value && !hasPrivacyModePermission) return;
togglePrivacyModeTime = DateTime.now();
bind.sessionTogglePrivacyMode(
sessionId: sessionId, implKey: implKey, on: value);
}
: null);
onChanged: (value) {
if (value == null) return;
togglePrivacyModeTime = DateTime.now();
bind.sessionTogglePrivacyMode(
sessionId: sessionId, implKey: implKey, on: value);
});
}).toList();
}
}

View File

@@ -114,9 +114,6 @@ const String kOptionTerminalPersistent = "terminal-persistent";
const String kOptionEnableTunnel = "enable-tunnel";
const String kOptionEnableRemoteRestart = "enable-remote-restart";
const String kOptionEnableBlockInput = "enable-block-input";
const String kOptionEnablePrivacyMode = "enable-privacy-mode";
const String kOptionEnablePermChangeInAcceptWindow =
"enable-perm-change-in-accept-window";
const String kOptionAllowRemoteConfigModification =
"allow-remote-config-modification";
const String kOptionVerificationMethod = "verification-method";
@@ -142,10 +139,6 @@ const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
const String kOptionCodecPreference = "codec-preference";
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
const String kOptionRemoteMenubarEdge = "remote-menubar-edge";
const String kOptionRemoteMenubarFraction = "remote-menubar-frac";
const String kOptionAllowMultiEdgeToolbarDock =
"allow-multi-edge-toolbar-dock";
const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
const String kOptionRemoteMenubarState = "remoteMenubarState";
const String kOptionPeerSorting = "peer-sorting";
@@ -182,7 +175,6 @@ const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust";
const String kOptionHideServerSetting = "hide-server-settings";
const String kOptionHideProxySetting = "hide-proxy-settings";
const String kOptionHideWebSocketSetting = "hide-websocket-settings";
const String kOptionHideStopService = "hide-stop-service";
const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings";
const String kOptionHideSecuritySetting = "hide-security-settings";
const String kOptionHideNetworkSetting = "hide-network-settings";
@@ -194,9 +186,6 @@ const String kOptionDisableChangeId = "disable-change-id";
const String kOptionDisableUnlockPin = "disable-unlock-pin";
const kHideUsernameOnCard = "hide-username-on-card";
const String kOptionHideHelpCards = "hide-help-cards";
const String kOptionAllowDeepLinkPassword = "allow-deep-link-password";
const String kOptionAllowDeepLinkServerSettings =
"allow-deep-link-server-settings";
const String kOptionToggleViewOnly = "view-only";
const String kOptionToggleShowMyCursor = "show-my-cursor";

View File

@@ -908,17 +908,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
}
void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
final p0 = TextEditingController(text: "");
final p1 = TextEditingController(text: "");
final pw = await bind.mainGetPermanentPassword();
final p0 = TextEditingController(text: pw);
final p1 = TextEditingController(text: pw);
var errMsg0 = "";
var errMsg1 = "";
final localPasswordSet =
(await bind.mainGetCommon(key: "local-permanent-password-set")) == "true";
final permanentPasswordSet =
(await bind.mainGetCommon(key: "permanent-password-set")) == "true";
final presetPassword = permanentPasswordSet && !localPasswordSet;
var canSubmit = false;
final RxString rxPass = "".obs;
final RxString rxPass = pw.trim().obs;
final rules = [
DigitValidationRule(),
UppercaseValidationRule(),
@@ -927,21 +922,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
MinCharactersValidationRule(8),
];
final maxLength = bind.mainMaxEncryptLen();
final statusTip = localPasswordSet
? translate('password-hidden-tip')
: (presetPassword ? translate('preset-password-in-use-tip') : '');
final showStatusTipOnMobile =
statusTip.isNotEmpty && !isDesktop && !isWebDesktop;
gFFI.dialogManager.show((setState, close, context) {
updateCanSubmit() {
canSubmit = p0.text.trim().isNotEmpty || p1.text.trim().isNotEmpty;
}
submit() async {
if (!canSubmit) {
return;
}
submit() {
setState(() {
errMsg0 = "";
errMsg1 = "";
@@ -964,13 +947,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
});
return;
}
final ok = await bind.mainSetPermanentPasswordWithResult(password: pass);
if (!ok) {
setState(() {
errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
});
return;
}
bind.mainSetPermanentPassword(password: pass);
if (pass.isNotEmpty) {
notEmptyCallback?.call();
}
@@ -978,20 +955,14 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
}
return CustomAlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.key, color: MyTheme.accent),
Text(translate("Set Password")).paddingOnly(left: 10),
],
),
title: Text(translate("Set Password")),
content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 500),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: showStatusTipOnMobile ? 0.0 : 6.0,
const SizedBox(
height: 8.0,
),
Row(
children: [
@@ -1007,7 +978,6 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
rxPass.value = value.trim();
setState(() {
errMsg0 = '';
updateCanSubmit();
});
},
maxLength: maxLength,
@@ -1019,9 +989,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
children: [
Expanded(child: PasswordStrengthIndicator(password: rxPass)),
],
).marginOnly(top: 2, bottom: showStatusTipOnMobile ? 2 : 8),
SizedBox(
height: showStatusTipOnMobile ? 0.0 : 8.0,
).marginSymmetric(vertical: 8),
const SizedBox(
height: 8.0,
),
Row(
children: [
@@ -1035,7 +1005,6 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
onChanged: (value) {
setState(() {
errMsg1 = '';
updateCanSubmit();
});
},
maxLength: maxLength,
@@ -1043,23 +1012,11 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
),
],
),
if (statusTip.isNotEmpty)
Row(
children: [
Icon(Icons.info, color: Colors.amber, size: 18)
.marginOnly(right: 6),
Expanded(
child: Text(
statusTip,
style: const TextStyle(fontSize: 13, height: 1.1),
))
],
).marginOnly(top: 6, bottom: 2),
SizedBox(
height: showStatusTipOnMobile ? 0.0 : 8.0,
const SizedBox(
height: 8.0,
),
Obx(() => Wrap(
runSpacing: showStatusTipOnMobile ? 2.0 : 8.0,
runSpacing: 8,
spacing: 4,
children: rules.map((e) {
var checked = e.validate(rxPass.value.trim());
@@ -1079,67 +1036,11 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async {
],
),
),
actions: (() {
final cancelButton = dialogButton(
"Cancel",
icon: Icon(Icons.close_rounded),
onPressed: close,
isOutline: true,
);
final removeButton = dialogButton(
"Remove",
icon: Icon(Icons.delete_outline_rounded),
onPressed: () async {
setState(() {
errMsg0 = "";
errMsg1 = "";
});
final ok =
await bind.mainSetPermanentPasswordWithResult(password: "");
if (!ok) {
setState(() {
errMsg0 = '${translate('Prompt')}: ${translate("Failed")}';
});
return;
}
close();
},
buttonStyle: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(Colors.red)),
);
final okButton = dialogButton(
"OK",
icon: Icon(Icons.done_rounded),
onPressed: canSubmit ? submit : null,
);
if (!isDesktop && !isWebDesktop && localPasswordSet) {
return [
Align(
alignment: Alignment.centerRight,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
cancelButton,
const SizedBox(width: 4),
removeButton,
const SizedBox(width: 4),
okButton,
],
),
),
),
];
}
return [
cancelButton,
if (localPasswordSet) removeButton,
okButton,
];
})(),
onSubmit: canSubmit ? submit : null,
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
});

View File

@@ -458,27 +458,18 @@ class _GeneralState extends State<_General> {
return const Offstage();
}
final hideStopService =
bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
return Obx(() {
if (hideStopService && !serviceStop.value) {
return const Offstage();
}
return _Card(title: 'Service', children: [
_Button(serviceStop.value ? 'Start' : 'Stop', () {
() async {
serviceBtnEnabled.value = false;
await start_service(serviceStop.value);
// enable the button after 1 second
Future.delayed(const Duration(seconds: 1), () {
serviceBtnEnabled.value = true;
});
}();
}, enabled: serviceBtnEnabled.value)
]);
});
return _Card(title: 'Service', children: [
Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
() async {
serviceBtnEnabled.value = false;
await start_service(serviceStop.value);
// enable the button after 1 second
Future.delayed(const Duration(seconds: 1), () {
serviceBtnEnabled.value = true;
});
}();
}, enabled: serviceBtnEnabled.value))
]);
}
Widget other() {
@@ -488,16 +479,6 @@ class _GeneralState extends State<_General> {
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs,
isServer: false),
if (!bind.isIncomingOnly())
_OptionCheckBox(
context,
'allow-remote-toolbar-docking-any-edge',
kOptionAllowMultiEdgeToolbarDock,
isServer: false,
update: (_) {
reloadAllWindows();
},
),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
if (!isWeb) wallpaper(),
if (!isWeb && !bind.isIncomingOnly()) ...[
@@ -1072,10 +1053,6 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
_OptionCheckBox(context, 'Enable blocking user input',
kOptionEnableBlockInput,
enabled: enabled, fakeValue: fakeValue),
if (bind.mainSupportedPrivacyModeImpls() != '[]')
_OptionCheckBox(
context, 'Enable privacy mode', kOptionEnablePrivacyMode,
enabled: enabled, fakeValue: fakeValue),
_OptionCheckBox(context, 'Enable remote configuration modification',
kOptionAllowRemoteConfigModification,
enabled: enabled, fakeValue: fakeValue),
@@ -1123,9 +1100,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
if (value ==
passwordValues[passwordKeys
.indexOf(kUsePermanentPassword)] &&
(await bind.mainGetCommon(
key: "permanent-password-set")) !=
"true") {
(await bind.mainGetPermanentPassword())
.isEmpty) {
if (isChangePermanentPasswordDisabled()) {
await callback();
return;

View File

@@ -101,9 +101,6 @@ class _RemotePageState extends State<RemotePage>
Function(bool)? _onEnterOrLeaveImage4Toolbar;
late FFI _ffi;
Worker? _waylandKeyboardModeWorker;
bool _waylandKeyboardModeNormalized = false;
bool _waylandKeyboardModeNormalizing = false;
SessionID get sessionId => _ffi.sessionId;
@@ -181,48 +178,6 @@ class _RemotePageState extends State<RemotePage>
// Register callback to cancel debounce timer when relative mouse mode is disabled
_ffi.inputModel.onRelativeMouseModeDisabled =
_cancelPointerLockCenterDebounceTimer;
_waylandKeyboardModeWorker = ever(_ffi.ffiModel.pi.isSet, (bool isSet) {
if (isSet) {
unawaited(_normalizeWaylandKeyboardModeIfNeeded());
}
});
if (_ffi.ffiModel.pi.isSet.value) {
unawaited(_normalizeWaylandKeyboardModeIfNeeded());
}
}
Future<void> _normalizeWaylandKeyboardModeIfNeeded() async {
if (!mounted ||
_waylandKeyboardModeNormalized ||
_waylandKeyboardModeNormalizing) {
return;
}
_waylandKeyboardModeNormalizing = true;
try {
final pi = _ffi.ffiModel.pi;
if (pi.platform != kPeerPlatformLinux || !pi.isWayland) return;
final mapSupported = bind.sessionIsKeyboardModeSupported(
sessionId: sessionId, mode: kKeyMapMode);
if (!mapSupported) return;
final current = await bind.sessionGetKeyboardMode(sessionId: sessionId);
if (!mounted) return;
if (current == kKeyMapMode) {
_waylandKeyboardModeNormalized = true;
return;
}
await bind.sessionSetKeyboardMode(
sessionId: sessionId, value: kKeyMapMode);
if (!mounted) return;
await _ffi.inputModel.updateKeyboardMode();
if (!mounted) return;
_waylandKeyboardModeNormalized = true;
} catch (e, st) {
debugPrint('Failed to normalize Wayland keyboard mode: $e');
debugPrintStack(stackTrace: st);
} finally {
_waylandKeyboardModeNormalizing = false;
}
}
/// Cancel the pointer lock center debounce timer
@@ -363,7 +318,6 @@ class _RemotePageState extends State<RemotePage>
_pointerLockCenterDebounceTimer?.cancel();
_pointerLockCenterDebounceTimer = null;
_waylandKeyboardModeWorker?.dispose();
// Clear callback reference to prevent memory leaks and stale references
_ffi.inputModel.onRelativeMouseModeDisabled = null;
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
@@ -377,9 +331,6 @@ class _RemotePageState extends State<RemotePage>
_ffi.imageModel.disposeImage();
_ffi.cursorModel.disposeImages();
_rawKeyFocusNode.dispose();
if (closeSession) {
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
}
await _ffi.close(closeSession: closeSession);
_timer?.cancel();
_ffi.dialogManager.dismissAll();

View File

@@ -610,24 +610,19 @@ class _PrivilegeBoard extends StatefulWidget {
class _PrivilegeBoardState extends State<_PrivilegeBoard> {
late final client = widget.client;
Widget buildPermissionIcon(bool enabled, IconData iconData,
Function(bool)? onTap, String tooltipText,
{required bool canModify}) {
Function(bool)? onTap, String tooltipText) {
return Tooltip(
message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
waitDuration: Duration.zero,
child: Container(
decoration: BoxDecoration(
color: enabled
? (canModify ? MyTheme.accent : MyTheme.accent.withOpacity(0.6))
: Colors.grey[700],
color: enabled ? MyTheme.accent : Colors.grey[700],
borderRadius: BorderRadius.circular(10.0),
),
padding: EdgeInsets.all(8.0),
child: InkWell(
onTap: canModify
? () =>
checkClickTime(widget.client.id, () => onTap?.call(!enabled))
: null,
onTap: () =>
checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
@@ -648,9 +643,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
Widget build(BuildContext context) {
final crossAxisCount = 4;
final spacing = 10.0;
final canModifyPermission =
bind.mainGetBuildinOption(key: kOptionEnablePermChangeInAcceptWindow) !=
'N';
return Container(
width: double.infinity,
height: 160.0,
@@ -697,7 +689,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable audio'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.recording,
@@ -712,7 +703,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable recording session'),
canModify: canModifyPermission,
),
]
: [
@@ -729,7 +719,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable keyboard/mouse'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.clipboard,
@@ -744,7 +733,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable clipboard'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.audio,
@@ -759,7 +747,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable audio'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.file,
@@ -774,7 +761,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable file copy and paste'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.restart,
@@ -789,7 +775,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable remote restart'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.recording,
@@ -804,7 +789,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable recording session'),
canModify: canModifyPermission,
),
// only windows support block input
if (isWindows)
@@ -821,23 +805,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable blocking user input'),
canModify: canModifyPermission,
),
if (bind.mainSupportedPrivacyModeImpls() != '[]')
buildPermissionIcon(
client.privacyMode,
Icons.visibility_off,
(enabled) {
bind.cmSwitchPermission(
connId: client.id,
name: "privacy_mode",
enabled: enabled);
setState(() {
client.privacyMode = enabled;
});
},
translate('Enable privacy mode'),
canModify: canModifyPermission,
)
],
),

View File

@@ -27,7 +27,6 @@ class TerminalPage extends StatefulWidget {
final bool? isSharedPassword;
final String? connToken;
final int terminalId;
/// Tab key for focus management, passed from parent to avoid duplicate construction
final String tabKey;
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
@@ -44,9 +43,6 @@ class TerminalPage extends StatefulWidget {
class _TerminalPageState extends State<TerminalPage>
with AutomaticKeepAliveClientMixin {
static const EdgeInsets _defaultTerminalPadding =
EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
late FFI _ffi;
late TerminalModel _terminalModel;
double? _cellHeight;
@@ -159,27 +155,13 @@ class _TerminalPageState extends State<TerminalPage>
// extra space left after dividing the available height by the height of a single
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
EdgeInsets _calculatePadding(double heightPx) {
final cellHeight = _cellHeight;
if (!heightPx.isFinite ||
heightPx <= 0 ||
cellHeight == null ||
!cellHeight.isFinite ||
cellHeight <= 0) {
return _defaultTerminalPadding;
}
final rows = (heightPx / cellHeight).floor();
if (rows <= 0) {
return _defaultTerminalPadding;
}
final extraSpace = heightPx - rows * cellHeight;
if (!extraSpace.isFinite || extraSpace < 0) {
return _defaultTerminalPadding;
if (_cellHeight == null) {
return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
}
final rows = (heightPx / _cellHeight!).floor();
final extraSpace = heightPx - rows * _cellHeight!;
final topBottom = extraSpace / 2.0;
return EdgeInsets.symmetric(
horizontal: _defaultTerminalPadding.horizontal / 2,
vertical: topBottom,
);
return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom);
}
@override

View File

@@ -46,7 +46,6 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
.setTitle(getWindowNameWithId(id));
};
tabController.onRemoved = (_, id) => onRemoveId(id);
tabController.onCloseWindow = _closeWindowFromConnection;
final terminalId = params['terminalId'] ?? _nextTerminalId++;
tabController.add(_createTerminalTab(
peerId: params['id'],
@@ -145,8 +144,6 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
_windowClosing = true;
final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList();
// Remove all UI tabs immediately (same instant behavior as the old tabController.clear())
// Keep the cleanup target lookup below synchronous before its first await:
// it relies on the current frame still retaining each TerminalPage's FFI/model.
tabController.clear();
// Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout).
// Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls.
@@ -371,34 +368,8 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
final persistentSessions =
args['persistent_sessions'] as List<dynamic>? ?? [];
final sortedSessions = persistentSessions.whereType<int>().toList()..sort();
var peerId = args['peer_id'] as String? ?? '';
if (peerId.isEmpty) {
if (tabController.state.value.tabs.isEmpty ||
tabController.state.value.selected >=
tabController.state.value.tabs.length) {
debugPrint('[TerminalTabPage] Skip restore: no selected tab');
return;
}
final currentTab = tabController.state.value.selectedTabInfo;
final parsed = _parseTabKey(currentTab.key);
if (parsed == null) return;
peerId = parsed.$1;
}
final existingTerminalIds = tabController.state.value.tabs
.map((tab) => _parseTabKey(tab.key))
.where((parsed) => parsed != null && parsed.$1 == peerId)
.map((parsed) => parsed!.$2)
.toSet();
if (existingTerminalIds.isEmpty) {
debugPrint(
'[TerminalTabPage] Skip restore: no seed tab for peer $peerId');
return;
}
for (final terminalId in sortedSessions) {
if (!existingTerminalIds.add(terminalId)) {
continue;
}
_addNewTerminal(peerId, terminalId: terminalId);
_addNewTerminalForCurrentPeer(terminalId: terminalId);
// A delay is required to ensure the UI has sufficient time to update
// before adding the next terminal. Without this delay, `_TerminalPageState::dispose()`
// may be called prematurely while the tab widget is still in the tab controller.
@@ -575,11 +546,6 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
}
}
Future<void> _closeWindowFromConnection() async {
await _closeAllTabs();
await WindowController.fromWindowId(windowId()).close();
}
int windowId() {
return widget.params["windowId"];
}

File diff suppressed because it is too large Load Diff

View File

@@ -99,7 +99,6 @@ class DesktopTabController {
/// index, key
Function(int, String)? onRemoved;
Function(String)? onSelected;
Future<void> Function()? onCloseWindow;
DesktopTabController(
{required this.tabType, this.onRemoved, this.onSelected});
@@ -593,13 +592,13 @@ class _DesktopTabState extends State<DesktopTab>
}
Widget _buildBar() {
final isIncomingHomePage = bind.isIncomingOnly() && isInHomePage();
return Row(
children: [
Expanded(
child: GestureDetector(
// custom double tap handler
onTap: !isIncomingHomePage && showMaximize
onTap: !(bind.isIncomingOnly() && isInHomePage()) &&
showMaximize
? () {
final current = DateTime.now().millisecondsSinceEpoch;
final elapsed = current - _lastClickTime;
@@ -610,7 +609,7 @@ class _DesktopTabState extends State<DesktopTab>
.then((value) => stateGlobal.setMaximized(value));
}
}
: (isIncomingHomePage ? () {} : null), // Keep tap recognizer for Windows touch.
: null,
onPanStart: (_) => startDragging(isMainWindow),
onPanCancel: () {
// We want to disable dragging of the tab area in the tab bar.

View File

@@ -27,7 +27,6 @@ import 'common.dart';
import 'consts.dart';
import 'mobile/pages/home_page.dart';
import 'mobile/pages/server_page.dart';
import 'mobile/widgets/deploy_dialog.dart';
import 'models/platform_model.dart';
import 'package:flutter_hbb/plugin/handlers.dart'
@@ -576,14 +575,6 @@ _registerEventHandler() {
NativeUiHandler.instance.onEvent(evt);
});
}
if (isAndroid) {
platformFFI.registerEventHandler(
'android_needs_deploy', 'android_needs_deploy', (_) async {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDeployPromptDialog();
});
});
}
}
Widget keyListenerBuilder(BuildContext context, Widget? child) {

View File

@@ -75,9 +75,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
final FocusNode _physicalFocusNode = FocusNode();
var _showEdit = false; // use soft keyboard
Worker? _waylandKeyboardGateWorker;
bool _waylandKeyboardGateInitialized = false;
InputModel get inputModel => gFFI.inputModel;
SessionID get sessionId => gFFI.sessionId;
@@ -124,33 +121,11 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
isKeyboardVisible: keyboardVisibilityController.isVisible);
});
WidgetsBinding.instance.addObserver(this);
inputModel.keyboardInputAllowed = true;
// Wayland sessions may use clipboard-based text input on the controlled side.
// Require explicit user confirmation before allowing soft-keyboard and
// clipboard-assisted text input. Physical keyboard events are not gated here.
_waylandKeyboardGateWorker = ever(gFFI.ffiModel.pi.isSet, (bool isSet) {
if (isSet) {
_initWaylandKeyboardGateIfNeeded();
}
});
if (gFFI.ffiModel.pi.isSet.value) {
_initWaylandKeyboardGateIfNeeded();
}
}
@override
Future<void> dispose() async {
WidgetsBinding.instance.removeObserver(this);
// Close the session up-front. `gFFI.close()` below only calls `sessionClose`
// after several awaits (canvas save, image update, the `enable_soft_keyboard`
// platform call), so if the app is backgrounded while this page is disposing,
// dispose can be suspended before reaching it and the connection is never torn
// down. The reconnect then re-attaches to the leaked session and is stuck on
// "Connecting...". Dispatching it here makes teardown happen synchronously on
// pop; the `sessionClose` in `gFFI.close()` becomes a no-op once removed.
unawaited(bind.sessionClose(sessionId: sessionId));
// https://github.com/flutter/flutter/issues/64935
super.dispose();
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
@@ -160,9 +135,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
await gFFI.invokeMethod("enable_soft_keyboard", true);
_mobileFocusNode.dispose();
_physicalFocusNode.dispose();
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
_waylandKeyboardGateWorker?.dispose();
inputModel.keyboardInputAllowed = true;
await gFFI.close();
_timer?.cancel();
_iosKeyboardWorkaroundTimer?.cancel();
@@ -191,40 +163,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
gFFI.invokeMethod("try_sync_clipboard");
}
bool _shouldGateKeyboardForWayland() {
if (!(isAndroid || isIOS)) return false;
final pi = gFFI.ffiModel.pi;
return pi.platform == kPeerPlatformLinux && pi.isWayland;
}
void _initWaylandKeyboardGateIfNeeded() {
if (!mounted) return;
if (_waylandKeyboardGateInitialized) return;
if (!_shouldGateKeyboardForWayland()) return;
_waylandKeyboardGateInitialized = true;
final allowWaylandKeyboard =
mainGetPeerBoolOptionSync(widget.id, kPeerOptionAllowWaylandKeyboard);
if (!shouldShowWaylandKeyboardPrompt(
connectionId: sessionId.toString(),
isWaylandPeer: _shouldGateKeyboardForWayland(),
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
)) {
inputModel.keyboardInputAllowed = true;
return;
}
inputModel.keyboardInputAllowed = false;
// Ensure soft keyboard is not active before user confirms.
_showEdit = false;
gFFI.invokeMethod("enable_soft_keyboard", false);
_mobileFocusNode.unfocus();
_physicalFocusNode.requestFocus();
setState(() {});
}
// to-do: It should be better to use transparent color instead of the bgColor.
// But for now, the transparent color will cause the canvas to be white.
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
@@ -356,7 +294,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
content == '【】')) {
// can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input
bind.sessionInputString(sessionId: sessionId, value: content);
_openKeyboardUnlocked();
openKeyboard();
return;
}
bind.sessionInputString(sessionId: sessionId, value: content);
@@ -368,9 +306,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
// handle mobile virtual keyboard
void handleSoftKeyboardInput(String newValue) {
if (!inputModel.keyboardInputAllowed) {
return;
}
if (isIOS) {
_handleIOSSoftKeyboardInput(newValue);
} else {
@@ -379,9 +314,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
}
void inputChar(String char) {
if (!inputModel.keyboardInputAllowed) {
return;
}
if (char == '\n') {
char = 'VK_RETURN';
} else if (char == ' ') {
@@ -391,29 +323,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
}
void openKeyboard() {
final allowWaylandKeyboard =
mainGetPeerBoolOptionSync(widget.id, kPeerOptionAllowWaylandKeyboard);
if (shouldShowWaylandKeyboardPrompt(
connectionId: sessionId.toString(),
isWaylandPeer: _shouldGateKeyboardForWayland(),
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
)) {
inputModel.keyboardInputAllowed = false;
showWaylandKeyboardInputWarningDialog(
id: widget.id,
connectionId: sessionId.toString(),
ffi: gFFI,
onEnable: () async {
_openKeyboardUnlocked();
},
);
return;
}
_openKeyboardUnlocked();
}
void _openKeyboardUnlocked() {
inputModel.keyboardInputAllowed = true;
gFFI.invokeMethod("enable_soft_keyboard", true);
// destroy first, so that our _value trick can work
_value = initText;
@@ -517,10 +426,12 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
}
return Container(
color: MyTheme.canvasColor,
child: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
),
child: inputModel.isPhysicalMouse.value
? getBodyForMobile()
: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
),
);
}),
),
@@ -1274,8 +1185,7 @@ void showOptions(
List<TToggleMenu> privacyModeList = [];
// privacy mode
final privacyModeState = PrivacyModeState.find(id);
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
privacyModeState.isNotEmpty) {
if (gFFI.ffiModel.keyboard && gFFI.ffiModel.pi.features.privacyMode) {
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
if (privacyModeList.length == 1) {
displayToggles.add(privacyModeList[0]);

View File

@@ -150,8 +150,7 @@ class _DropDownAction extends StatelessWidget {
}
if (value == kUsePermanentPassword &&
(await bind.mainGetCommon(key: "permanent-password-set")) !=
"true") {
(await bind.mainGetPermanentPassword()).isEmpty) {
if (isChangePermanentPasswordDisabled()) {
callback();
return;
@@ -583,20 +582,10 @@ class _PermissionCheckerState extends State<PermissionChecker> {
Widget build(BuildContext context) {
final serverModel = Provider.of<ServerModel>(context);
final hasAudioPermission = androidVersion >= 30;
final hideStopService = isAndroid &&
bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
final allowPermChangeInAcceptWindow = option2bool(
kOptionEnablePermChangeInAcceptWindow,
bind.mainGetBuildinOption(
key: kOptionEnablePermChangeInAcceptWindow,
));
final permissionChangeLocked = isAndroid &&
serverModel.clients.any((c) => !c.disconnected) &&
!allowPermChangeInAcceptWindow;
return PaddingCard(
title: translate("Permissions"),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
serverModel.mediaOk && !hideStopService
serverModel.mediaOk
? ElevatedButton.icon(
style: ButtonStyle(
backgroundColor:
@@ -606,30 +595,21 @@ class _PermissionCheckerState extends State<PermissionChecker> {
label: Text(translate("Stop service")))
.marginOnly(bottom: 8)
: SizedBox.shrink(),
if (!hideStopService || !serverModel.mediaOk)
PermissionRow(
translate("Screen Capture"),
serverModel.mediaOk,
!serverModel.mediaOk &&
gFFI.userModel.userName.value.isEmpty &&
bind.mainGetLocalOption(key: "show-scam-warning") != "N"
? () => showScamWarning(context, serverModel)
: serverModel.toggleService),
PermissionRow(
translate("Input Control"),
serverModel.inputOk,
serverModel.toggleInput,
),
PermissionRow(
translate("Transfer file"),
serverModel.fileOk,
serverModel.toggleFile,
enabled: !permissionChangeLocked,
),
translate("Screen Capture"),
serverModel.mediaOk,
!serverModel.mediaOk &&
gFFI.userModel.userName.value.isEmpty &&
bind.mainGetLocalOption(key: "show-scam-warning") != "N"
? () => showScamWarning(context, serverModel)
: serverModel.toggleService),
PermissionRow(translate("Input Control"), serverModel.inputOk,
serverModel.toggleInput),
PermissionRow(translate("Transfer file"), serverModel.fileOk,
serverModel.toggleFile),
hasAudioPermission
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
serverModel.toggleAudio,
enabled: !permissionChangeLocked)
serverModel.toggleAudio)
: Row(children: [
Icon(Icons.info_outline).marginOnly(right: 15),
Expanded(
@@ -638,25 +618,19 @@ class _PermissionCheckerState extends State<PermissionChecker> {
style: const TextStyle(color: MyTheme.darkGray),
))
]),
PermissionRow(
translate("Enable clipboard"),
serverModel.clipboardOk,
serverModel.toggleClipboard,
enabled: !permissionChangeLocked,
),
PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
serverModel.toggleClipboard),
]));
}
}
class PermissionRow extends StatelessWidget {
const PermissionRow(this.name, this.isOk, this.onPressed,
{Key? key, this.enabled = true})
const PermissionRow(this.name, this.isOk, this.onPressed, {Key? key})
: super(key: key);
final String name;
final bool isOk;
final VoidCallback onPressed;
final bool enabled;
@override
Widget build(BuildContext context) {
@@ -665,11 +639,9 @@ class PermissionRow extends StatelessWidget {
contentPadding: EdgeInsets.all(0),
title: Text(name),
value: isOk,
onChanged: enabled
? (bool value) {
onPressed();
}
: null);
onChanged: (bool value) {
onPressed();
});
}
}

View File

@@ -17,7 +17,6 @@ import '../../common/widgets/login.dart';
import '../../consts.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../widgets/deploy_dialog.dart';
import '../widgets/dialog.dart';
import 'home_page.dart';
import 'scan_page.dart';
@@ -729,13 +728,6 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
onPressed: (context) {
changeSocks5Proxy();
}),
if (isAndroid && !bind.isOutgoingOnly())
SettingsTile(
title: Text(translate('Deploy')),
leading: Icon(Icons.cloud_upload),
onPressed: (context) {
showDeployDialog();
}),
if (!disabledSettings && !_hideNetwork && !_hideWebSocket)
SettingsTile.switchTile(
title: Text(translate('Use WebSocket')),

View File

@@ -259,11 +259,13 @@ class _ViewCameraPageState extends State<ViewCameraPage>
}
return Container(
color: MyTheme.canvasColor,
child: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
isCamera: true,
),
child: inputModel.isPhysicalMouse.value
? getBodyForMobile()
: RawTouchGestureDetectorRegion(
child: getBodyForMobile(),
ffi: gFFI,
isCamera: true,
),
);
}),
),

View File

@@ -1,114 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
const _deployDialogTag = 'android-deploy-device';
void showDeployPromptDialog() {
gFFI.dialogManager.dismissByTag(_deployDialogTag);
gFFI.dialogManager.show<bool>((setState, close, context) {
submit() => close(true);
return CustomAlertDialog(
title: Text(translate("Deploy")),
content: Text(translate("server_requires_deployment_tip")),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
}, tag: _deployDialogTag).then((deploy) {
if (deploy == true) {
showDeployDialog();
}
});
}
void showDeployDialog() {
gFFI.dialogManager.dismissByTag(_deployDialogTag);
final tokenController = TextEditingController();
final idController = TextEditingController();
var errorText = "";
var isInProgress = false;
gFFI.dialogManager.show((setState, close, context) {
submit() async {
if (isInProgress) return;
final token = tokenController.text.trim();
if (token.isEmpty) {
setState(() {
errorText = translate("token is required!");
});
return;
}
setState(() {
errorText = "";
isInProgress = true;
});
String res;
try {
res = await bind.mainDeployDevice(
token: token, id: idController.text.trim());
} catch (e) {
setState(() {
errorText = translate(e.toString());
isInProgress = false;
});
return;
}
if (res.isEmpty) {
close();
await gFFI.serverModel.fetchID();
showToast(translate("Successful"));
} else {
setState(() {
errorText = translate(res.toString());
isInProgress = false;
});
}
}
return CustomAlertDialog(
title: Text(translate("Deploy")),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: tokenController,
decoration: InputDecoration(labelText: translate("API Token")),
obscureText: true,
enableSuggestions: false,
autocorrect: false,
autofocus: true,
).workaroundFreezeLinuxMint(),
TextField(
controller: idController,
decoration:
InputDecoration(labelText: translate("Custom ID (optional)")),
).workaroundFreezeLinuxMint(),
if (errorText.isNotEmpty)
Align(
alignment: Alignment.centerLeft,
child: SelectableText(
errorText,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
).paddingOnly(top: 8),
),
if (isInProgress) const LinearProgressIndicator().paddingOnly(top: 8),
],
),
actions: [
dialogButton("Cancel",
onPressed: isInProgress ? null : close, isOutline: true),
dialogButton("OK", onPressed: isInProgress ? null : submit),
],
onSubmit: submit,
onCancel: isInProgress ? null : close,
);
}, tag: _deployDialogTag);
}

View File

@@ -12,6 +12,100 @@ void _showSuccess() {
showToast(translate("Successful"));
}
void _showError() {
showToast(translate("Error"));
}
void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async {
final pw = await bind.mainGetPermanentPassword();
final p0 = TextEditingController(text: pw);
final p1 = TextEditingController(text: pw);
var validateLength = false;
var validateSame = false;
dialogManager.show((setState, close, context) {
submit() async {
close();
dialogManager.showLoading(translate("Waiting"));
if (await gFFI.serverModel.setPermanentPassword(p0.text)) {
dialogManager.dismissAll();
_showSuccess();
} else {
dialogManager.dismissAll();
_showError();
}
}
return CustomAlertDialog(
title: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.password_rounded, color: MyTheme.accent),
Text(translate('Set your own password')).paddingOnly(left: 10),
],
),
content: Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(mainAxisSize: MainAxisSize.min, children: [
TextFormField(
autofocus: true,
obscureText: true,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
labelText: translate('Password'),
),
controller: p0,
validator: (v) {
if (v == null) return null;
final val = v.trim().length > 5;
if (validateLength != val) {
// use delay to make setState success
Future.delayed(Duration(microseconds: 1),
() => setState(() => validateLength = val));
}
return val
? null
: translate('Too short, at least 6 characters.');
},
).workaroundFreezeLinuxMint(),
TextFormField(
obscureText: true,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
labelText: translate('Confirmation'),
),
controller: p1,
validator: (v) {
if (v == null) return null;
final val = p0.text == v;
if (validateSame != val) {
Future.delayed(Duration(microseconds: 1),
() => setState(() => validateSame = val));
}
return val
? null
: translate('The confirmation is not identical.');
},
).workaroundFreezeLinuxMint(),
])),
onCancel: close,
onSubmit: (validateLength && validateSame) ? submit : null,
actions: [
dialogButton(
'Cancel',
icon: Icon(Icons.close_rounded),
onPressed: close,
isOutline: true,
),
dialogButton(
'OK',
icon: Icon(Icons.done_rounded),
onPressed: (validateLength && validateSame) ? submit : null,
),
],
);
});
}
void setTemporaryPasswordLengthDialog(
OverlayDialogManager dialogManager) async {
List<String> lengths = ['6', '8', '10'];

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
@@ -52,9 +53,7 @@ class AbModel {
RxBool get currentAbLoading => current.abLoading;
bool get currentAbEmpty => current.peers.isEmpty && current.tags.isEmpty;
final _listPullError = ''.obs;
RxString get abPullError =>
_listPullError.value.isNotEmpty ? _listPullError : current.pullError;
RxString get currentAbPullError => current.pullError;
RxString get currentAbPushError => current.pushError;
String? _personalAbGuid;
RxBool legacyMode = false.obs;
@@ -69,7 +68,6 @@ class AbModel {
var _syncFromRecentLock = false;
var _timerCounter = 0;
var _cacheLoadOnceFlag = false;
var _pulledOnce = false;
var listInitialized = false;
var _maxPeerOneAb = 0;
@@ -99,17 +97,10 @@ class AbModel {
print("reset ab model");
addressbooks.clear();
_currentName.value = '';
_listPullError.value = '';
_pulledOnce = false;
await bind.mainClearAb();
listInitialized = false;
}
void clearPullErrors() {
_listPullError.value = '';
current.pullError.value = '';
}
// #region ab
/// Pulls the address book data from the server.
///
@@ -119,41 +110,31 @@ class AbModel {
var _pulling = false;
Future<void> pullAb(
{required ForcePullAb? force, required bool quiet}) async {
if (bind.isDisableAb()) return;
if (!gFFI.userModel.isLogin) return;
if (gFFI.userModel.networkError.isNotEmpty) return;
if (_pulling) return;
if (force == null && _pulledOnce) {
return;
}
_pulling = true;
if (!quiet) {
_listPullError.value = '';
current.pullError.value = '';
}
try {
await _pullAb(force: force, quiet: quiet);
_refreshTab();
} catch (_) {}
_pulling = false;
_pulledOnce = true;
}
Future<void> _pullAb(
{required ForcePullAb? force, required bool quiet}) async {
if (bind.isDisableAb()) return;
if (!gFFI.userModel.isLogin) return;
if (gFFI.userModel.networkError.isNotEmpty) return;
if (force == null && listInitialized && current.initialized) return;
debugPrint("pullAb, force: $force, quiet: $quiet");
if (!listInitialized || force == ForcePullAb.listAndCurrent) {
try {
// Read personal guid every time to avoid upgrading the server without closing the main window
_personalAbGuid = null;
// `true`: continue init. `false`: stop, error already recorded.
if (!await _getPersonalAbGuid(quiet: quiet)) {
return;
}
await _getPersonalAbGuid();
// Determine legacy mode based on whether _personalAbGuid is null
legacyMode.value = _personalAbGuid == null;
if (!legacyMode.value && _maxPeerOneAb == 0) {
await _getAbSettings(quiet: quiet);
await _getAbSettings();
}
if (_personalAbGuid != null) {
debugPrint("pull ab list");
@@ -161,7 +142,7 @@ class AbModel {
abProfiles.add(AbProfile(_personalAbGuid!, _personalAddressBookName,
gFFI.userModel.userName.value, null, ShareRule.read.value, null));
// get all address book name
await _getSharedAbProfiles(abProfiles, quiet: quiet);
await _getSharedAbProfiles(abProfiles);
addressbooks.removeWhere((key, value) =>
abProfiles.firstWhereOrNull((e) => e.name == key) == null);
for (int i = 0; i < abProfiles.length; i++) {
@@ -201,7 +182,6 @@ class AbModel {
}
} catch (e) {
debugPrint("pull ab list error: $e");
_setListPullError(e, quiet: quiet);
}
} else if (listInitialized &&
(!current.initialized || force == ForcePullAb.current)) {
@@ -217,26 +197,14 @@ class AbModel {
}
}
void _setListPullError(Object err, {required bool quiet, int? statusCode}) {
if (!quiet) {
_listPullError.value =
'${translate('pull_ab_failed_tip')}: ${translate(err.toString())}';
}
if (statusCode == 401) {
gFFI.userModel.reset(resetOther: true);
}
}
Future<bool> _getAbSettings({required bool quiet}) async {
int? statusCode;
Future<bool> _getAbSettings() async {
try {
final api = "${await bind.mainGetApiServer()}/api/ab/settings";
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
_setEmptyBody(headers);
final resp = await http.post(Uri.parse(api), headers: headers);
statusCode = resp.statusCode;
if (statusCode == 404) {
if (resp.statusCode == 404) {
debugPrint("HTTP 404, api server doesn't support shared address book");
return false;
}
@@ -245,57 +213,46 @@ class AbModel {
if (json.containsKey('error')) {
throw json['error'];
}
if (statusCode != 200) {
throw 'HTTP $statusCode';
if (resp.statusCode != 200) {
throw 'HTTP ${resp.statusCode}';
}
_maxPeerOneAb = json['max_peer_one_ab'] ?? 0;
return true;
} catch (err) {
debugPrint('get ab settings err: ${err.toString()}');
_setListPullError(err, quiet: quiet, statusCode: statusCode);
}
return false;
}
/// Loads `/api/ab/personal`.
/// Returns `true` to continue init, `false` to stop after a real error.
Future<bool> _getPersonalAbGuid({required bool quiet}) async {
int? statusCode;
Future<bool> _getPersonalAbGuid() async {
try {
final api = "${await bind.mainGetApiServer()}/api/ab/personal";
var headers = getHttpHeaders();
headers['Content-Type'] = "application/json";
_setEmptyBody(headers);
final resp = await http.post(Uri.parse(api), headers: headers);
statusCode = resp.statusCode;
if (statusCode == 404) {
if (resp.statusCode == 404) {
debugPrint("HTTP 404, current api server is legacy mode");
// Old server: keep `_personalAbGuid` null and continue in legacy mode.
return true;
return false;
}
Map<String, dynamic> json =
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
if (statusCode != 200) {
throw 'HTTP $statusCode';
if (resp.statusCode != 200) {
throw 'HTTP ${resp.statusCode}';
}
_personalAbGuid = json['guid'];
// New server: guid is available, continue in non-legacy mode.
return true;
} catch (err) {
debugPrint('get personal ab err: ${err.toString()}');
_setListPullError(err, quiet: quiet, statusCode: statusCode);
}
// Real error: stop the current pull.
return false;
}
Future<bool> _getSharedAbProfiles(List<AbProfile> profiles,
{required bool quiet}) async {
Future<bool> _getSharedAbProfiles(List<AbProfile> profiles) async {
final api = "${await bind.mainGetApiServer()}/api/ab/shared/profiles";
int? statusCode;
try {
var uri0 = Uri.parse(api);
final pageSize = 100;
@@ -316,19 +273,13 @@ class AbModel {
headers['Content-Type'] = "application/json";
_setEmptyBody(headers);
final resp = await http.post(uri, headers: headers);
statusCode = resp.statusCode;
if (statusCode == 404) {
debugPrint(
"HTTP 404, api server doesn't support shared address book");
return false;
}
Map<String, dynamic> json =
_jsonDecodeRespMap(decode_http_response(resp), resp.statusCode);
if (json.containsKey('error')) {
throw json['error'];
}
if (statusCode != 200) {
throw 'HTTP $statusCode';
if (resp.statusCode != 200) {
throw 'HTTP ${resp.statusCode}';
}
if (json.containsKey('total')) {
if (total == 0) total = json['total'];
@@ -351,7 +302,6 @@ class AbModel {
return true;
} catch (err) {
debugPrint('_getSharedAbProfiles err: ${err.toString()}');
_setListPullError(err, quiet: quiet, statusCode: statusCode);
}
return false;
}

View File

@@ -391,30 +391,14 @@ class FileController {
await Future.delayed(Duration(milliseconds: 100));
final savedDir = (await bind.sessionGetPeerOption(
final dir = (await bind.sessionGetPeerOption(
sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir"));
Future<bool> tryOpenReadyDirs() async {
final dirs = <String>{
if (directory.value.path.isNotEmpty) directory.value.path,
if (savedDir.isNotEmpty) savedDir,
options.value.home,
};
for (final dir in dirs) {
if (await _openDirectoryPath(dir, isBack: true)) {
return true;
}
}
return false;
}
var opened = await tryOpenReadyDirs();
openDirectory(dir.isEmpty ? options.value.home : dir);
await Future.delayed(Duration(seconds: 1));
if (!opened) {
// The peer may become ready during the reconnect delay, so retry the
// same candidates instead of only retrying the default home directory.
await tryOpenReadyDirs();
if (directory.value.path.isEmpty) {
openDirectory(options.value.home);
}
}
@@ -445,23 +429,19 @@ class FileController {
});
}
Future<bool> refresh() async {
// "." can be both a refresh command and a real remote directory path.
// Refresh must bypass openDirectory's command dispatch to avoid recursion.
return await _openDirectoryPath(directory.value.path, isBack: true);
Future<void> refresh() async {
await openDirectory(directory.value.path);
}
Future<bool> openDirectory(String path, {bool isBack = false}) async {
if (!isBack && path == ".") {
return await refresh();
Future<void> openDirectory(String path, {bool isBack = false}) async {
if (path == ".") {
refresh();
return;
}
if (!isBack && path == "..") {
return await _goToParentDirectory(isBack: isBack);
if (path == "..") {
goToParentDirectory();
return;
}
return await _openDirectoryPath(path, isBack: isBack);
}
Future<bool> _openDirectoryPath(String path, {bool isBack = false}) async {
if (!isBack) {
pushHistory();
}
@@ -478,10 +458,8 @@ class FileController {
final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
fd.format(isWindows, sort: sortBy.value);
directory.value = fd;
return true;
} catch (e) {
debugPrint("Failed to openDirectory $path: $e");
return false;
}
}
@@ -509,22 +487,19 @@ class FileController {
goBack();
return;
}
unawaited(_openDirectoryPath(path, isBack: true).then<void>((_) {}));
openDirectory(path, isBack: true);
}
void goToParentDirectory() {
unawaited(_goToParentDirectory().then<void>((_) {}));
}
Future<bool> _goToParentDirectory({bool isBack = false}) async {
final isWindows = options.value.isWindows;
final dirPath = directory.value.path;
var parent = PathUtil.dirname(dirPath, isWindows);
// specially for C:\, D:\, goto '/'
if (parent == dirPath && isWindows) {
return await _openDirectoryPath('/', isBack: isBack);
openDirectory('/');
return;
}
return await _openDirectoryPath(parent, isBack: isBack);
openDirectory(parent);
}
// TODO deprecated this

View File

@@ -343,7 +343,6 @@ class GroupModel {
}
reset() async {
initialized = false;
groupLoadError.value = '';
deviceGroups.clear();
users.clear();

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
@@ -16,13 +15,12 @@ import 'package:get/get.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../models/state_model.dart';
import 'input_modifier_utils.dart';
import 'relative_mouse_model.dart';
import '../common.dart';
import '../consts.dart';
/// Mouse button enum.
enum MouseButtons { left, right, wheel, back, forward }
enum MouseButtons { left, right, wheel, back }
const _kMouseEventDown = 'mousedown';
const _kMouseEventUp = 'mouseup';
@@ -159,8 +157,6 @@ extension ToString on MouseButtons {
return 'wheel';
case MouseButtons.back:
return 'back';
case MouseButtons.forward:
return 'forward';
}
}
}
@@ -331,80 +327,6 @@ class ToReleaseKeys {
}
class InputModel {
// Side mouse button support for Linux.
// Flutter's Linux embedder drops X11 button 8/9 events, so we capture them
// natively via GDK and forward through the platform channel.
static InputModel? _activeSideButtonModel;
// Tracks per-button which model received a side button down event, so the
// matching up event is routed there even if the pointer has left the view
// or a different button was pressed in between.
static final Map<MouseButtons, InputModel> _sideButtonDownModels = {};
static bool _sideButtonChannelInitialized = false;
/// Each Flutter engine (main window + sub-windows from desktop_multi_window)
/// runs its own Dart isolate with its own statics. Called from initEnv()
/// which runs per-engine, so each isolate registers its own handler tied
/// to its own set of InputModels.
static void initSideButtonChannel() {
if (!isLinux) return;
if (_sideButtonChannelInitialized) return;
_sideButtonChannelInitialized = true;
const channel = MethodChannel('org.rustdesk.rustdesk/side_buttons');
channel.setMethodCallHandler((call) async {
if (call.method == 'onSideMouseButton') {
final args = call.arguments as Map<dynamic, dynamic>;
final button = args['button'] as String;
final type = args['type'] as String;
final mb = button == 'back' ? MouseButtons.back : MouseButtons.forward;
if (type == 'down') {
final model = _activeSideButtonModel;
if (model != null &&
!(model.isViewOnly && !model.showMyCursor) &&
model.keyboardPerm &&
!model.isViewCamera) {
_sideButtonDownModels[mb] = model;
// Fire-and-forget to avoid blocking the platform channel handler.
unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) {
debugPrint('[InputModel] failed to send side button $type for $mb: $e');
}));
}
} else {
// Only route 'up' when we recorded the matching 'down';
// dropping avoids sending unpaired 'up' to an unrelated session.
// Use _sendMouseUnchecked to bypass permission checks so the
// release always goes through even if permissions changed.
final model = _sideButtonDownModels.remove(mb);
if (model != null) {
unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) {
debugPrint('[InputModel] failed to send side button $type for $mb: $e');
}));
}
}
}
return null;
});
}
/// Clear any static references to this model (prevents stale routing).
/// Releases any held side buttons on the peer so closing a session
/// mid-press does not leave a stuck button.
void disposeSideButtonTracking() {
if (_activeSideButtonModel == this) _activeSideButtonModel = null;
final held = _sideButtonDownModels.entries
.where((e) => e.value == this)
.map((e) => e.key)
.toList();
for (final mb in held) {
_sideButtonDownModels.remove(mb);
// Best-effort release; session may already be tearing down.
unawaited(_sendMouseUnchecked('up', mb).catchError((Object e) {
debugPrint('[InputModel] failed to release side button $mb: $e');
}));
}
}
final WeakReference<FFI> parent;
String keyboardMode = '';
@@ -474,10 +396,6 @@ class InputModel {
late final SessionID sessionId;
// Local gate for clipboard-assisted input flows on mobile Wayland dialogs.
// It should not block physical keyboard events.
bool keyboardInputAllowed = true;
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
@@ -494,7 +412,6 @@ class InputModel {
bool get isRelativeMouseModeSupported => _relativeMouse.isSupported;
InputModel(this.parent) {
initSideButtonChannel();
sessionId = parent.target!.sessionId;
_relativeMouse = RelativeMouseModel(
sessionId: sessionId,
@@ -703,38 +620,6 @@ class InputModel {
}
}
// Safe: this only re-dispatches synthesized Shift key-up events.
// The key-up path clears the tracked Shift state so this does not loop.
void _releaseTrackedShiftKeyEventIfNeeded() {
final leftShift = toReleaseKeys.lastLShiftKeyEvent;
final rightShift = toReleaseKeys.lastRShiftKeyEvent;
if (leftShift != null) {
handleKeyEvent(leftShift);
}
if (rightShift != null) {
handleKeyEvent(rightShift);
}
}
// Safe: this only re-dispatches synthesized Shift key-up events.
// The raw key-up path clears the tracked Shift state so this does not loop.
void _releaseTrackedRawShiftKeyEventIfNeeded() {
final leftShift = toReleaseRawKeys.lastLShiftKeyEvent;
final rightShift = toReleaseRawKeys.lastRShiftKeyEvent;
if (leftShift != null) {
handleRawKeyEvent(RawKeyUpEvent(
data: leftShift.data,
character: leftShift.character,
));
}
if (rightShift != null) {
handleRawKeyEvent(RawKeyUpEvent(
data: rightShift.data,
character: rightShift.character,
));
}
}
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
if (isViewOnly) return KeyEventResult.handled;
if (isViewCamera) return KeyEventResult.handled;
@@ -789,27 +674,6 @@ class InputModel {
toReleaseRawKeys.updateKeyUp(key, e);
}
// On some mobile soft-keyboard paths, Flutter may leave cached Shift state
// set even though the current raw key event is not shifted anymore.
if (e is RawKeyDownEvent &&
shouldReleaseStaleMobileShift(
isMobile: isMobile,
cachedShiftPressed: shift,
actualShiftPressed: e.isShiftPressed,
logicalKey: e.logicalKey,
hasTrackedShiftKeyDown: toReleaseRawKeys.lastLShiftKeyEvent != null ||
toReleaseRawKeys.lastRShiftKeyEvent != null,
)) {
if (kDebugMode) {
debugPrint(
'input: releasing stale mobile Shift before replaying tracked raw '
'key-up (logicalKey=${e.logicalKey.keyLabel}, '
'actualShiftPressed=${e.isShiftPressed}, cachedShiftPressed=$shift)',
);
}
_releaseTrackedRawShiftKeyEventIfNeeded();
}
// * Currently mobile does not enable map mode
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
mapKeyboardModeRaw(e, iosCapsLock);
@@ -853,8 +717,6 @@ class InputModel {
iosCapsLock = _getIosCapsFromCharacter(e);
}
// Update cached modifier state before sending the event. The stale mobile
// Shift release check below relies on this cached state.
if (e is KeyUpEvent) {
handleKeyUpEventModifiers(e);
} else if (e is KeyDownEvent) {
@@ -892,21 +754,6 @@ class InputModel {
}
}
}
// On some mobile soft-keyboard paths, Flutter may leave cached Shift state
// set even though the current key event is not shifted anymore.
if (e is KeyDownEvent &&
shouldReleaseStaleMobileShift(
isMobile: isMobile,
cachedShiftPressed: shift,
actualShiftPressed: HardwareKeyboard.instance.isShiftPressed,
logicalKey: e.logicalKey,
hasTrackedShiftKeyDown: toReleaseKeys.lastLShiftKeyEvent != null ||
toReleaseKeys.lastRShiftKeyEvent != null,
)) {
_releaseTrackedShiftKeyEventIfNeeded();
}
final isDesktopAndMapMode =
isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode);
if (isMobileAndMapMode || isDesktopAndMapMode) {
@@ -1119,20 +966,13 @@ class InputModel {
return evt;
}
/// Send mouse event unconditionally (no permission checks).
/// Used for side button releases that must go through even if permissions
/// changed after the matching down was sent.
Future<void> _sendMouseUnchecked(String type, MouseButtons button) async {
await bind.sessionSendMouse(
sessionId: sessionId,
msg: json.encode(modify({'type': type, 'buttons': button.value})));
}
/// Send mouse press event.
Future<void> sendMouse(String type, MouseButtons button) async {
if (!keyboardPerm) return;
if (isViewCamera) return;
await _sendMouseUnchecked(type, button);
await bind.sessionSendMouse(
sessionId: sessionId,
msg: json.encode(modify({'type': type, 'buttons': button.value})));
}
void enterOrLeave(bool enter) {
@@ -1142,13 +982,6 @@ class InputModel {
_pointerInsideImage = enter;
_lastWheelTsUs = 0;
// Track active model for side button events (Linux).
if (enter) {
_activeSideButtonModel = this;
} else if (_activeSideButtonModel == this) {
_activeSideButtonModel = null;
}
// Fix status
if (!enter) {
resetModifiers();
@@ -1499,16 +1332,6 @@ class InputModel {
return false;
}
/// iOS may emit a synthesized touch event after a real mouse click.
/// This helper ignores touch-down events that arrive shortly after a mouse down,
/// even when the position is far (e.g., near the top edge).
bool _shouldIgnoreTouchAfterMouse(int nowMs) {
if (!isIOS) return false;
const int kTouchAfterMouseWindowMs = 700;
final dt = nowMs - _lastMouseDownTimeMs;
return dt >= 0 && dt < kTouchAfterMouseWindowMs;
}
void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage ${e.kind}");
_stopFling = true;
@@ -1521,9 +1344,6 @@ class InputModel {
// Track mouse down events for duplicate detection on iOS.
final nowMs = DateTime.now().millisecondsSinceEpoch;
if (e.kind == ui.PointerDeviceKind.mouse) {
if (!isPhysicalMouse.value) {
isPhysicalMouse.value = true;
}
_lastMouseDownTimeMs = nowMs;
_lastMouseDownPos = e.position;
}
@@ -1533,10 +1353,6 @@ class InputModel {
}
if (e.kind != ui.PointerDeviceKind.mouse) {
// Ignore duplicate touch events that follow a recent mouse click (iOS Magic Mouse issue).
if (isPhysicalMouse.value && _shouldIgnoreTouchAfterMouse(nowMs)) {
return;
}
if (isPhysicalMouse.value) {
isPhysicalMouse.value = false;
}

View File

@@ -1,38 +0,0 @@
import 'package:flutter/services.dart';
/// Returns true when a stale mobile one-shot Shift state should be released
/// by replaying a tracked Shift key-down as a synthesized key-up.
///
/// This is only valid on mobile when Flutter's cached Shift state is still on
/// (`cachedShiftPressed == true`) but the current hardware/raw event reports
/// Shift as off (`actualShiftPressed == false`).
///
/// A tracked Shift key-down is required so the caller can safely synthesize the
/// matching key-up. Both `shiftLeft` and `shiftRight` are excluded because the
/// Shift key event itself must be processed first; otherwise we could release
/// the tracked key while still handling the original Shift press/release.
/// Callers should evaluate this only after their cached modifier state has been
/// updated for the current event.
///
/// When this returns true, the caller logs a line like:
/// `input: releasing stale mobile Shift before replaying tracked raw key-up`
/// immediately before calling `_releaseTrackedRawShiftKeyEventIfNeeded()`.
bool shouldReleaseStaleMobileShift({
required bool isMobile,
required bool cachedShiftPressed,
required bool actualShiftPressed,
required LogicalKeyboardKey logicalKey,
required bool hasTrackedShiftKeyDown,
}) {
if (!isMobile || !cachedShiftPressed || actualShiftPressed) {
return false;
}
if (!hasTrackedShiftKeyDown) {
return false;
}
if (logicalKey == LogicalKeyboardKey.shiftLeft ||
logicalKey == LogicalKeyboardKey.shiftRight) {
return false;
}
return true;
}

View File

@@ -3932,7 +3932,6 @@ class FFI {
inputModel.resetModifiers();
// Dispose relative mouse mode resources to ensure cursor is restored
inputModel.disposeRelativeMouseMode();
inputModel.disposeSideButtonTracking();
if (closeSession) {
await bind.sessionClose(sessionId: sessionId);
}

View File

@@ -298,7 +298,7 @@ class ServerModel with ChangeNotifier {
}
toggleAudio() async {
if (clients.any((c) => !c.disconnected)) {
if (clients.isNotEmpty) {
await showClientsMayNotBeChangedAlert(parent.target);
}
if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
@@ -316,7 +316,7 @@ class ServerModel with ChangeNotifier {
}
toggleFile() async {
if (clients.any((c) => !c.disconnected)) {
if (clients.isNotEmpty) {
await showClientsMayNotBeChangedAlert(parent.target);
}
if (!_fileOk &&
@@ -345,7 +345,7 @@ class ServerModel with ChangeNotifier {
}
toggleInput() async {
if (clients.any((c) => !c.disconnected)) {
if (clients.isNotEmpty) {
await showClientsMayNotBeChangedAlert(parent.target);
}
if (_inputOk) {
@@ -471,6 +471,17 @@ class ServerModel with ChangeNotifier {
WakelockManager.disable(_wakelockKey);
}
Future<bool> setPermanentPassword(String newPW) async {
await bind.mainSetPermanentPassword(password: newPW);
await Future.delayed(Duration(milliseconds: 500));
final pw = await bind.mainGetPermanentPassword();
if (newPW == pw) {
return true;
} else {
return false;
}
}
fetchID() async {
final id = await bind.mainGetMyId();
if (id != _serverId.id) {
@@ -549,19 +560,10 @@ class ServerModel with ChangeNotifier {
if (index < 0) {
_clients.add(client);
} else {
if (_clients[index].authorized) {
_clients[index].privacyMode = client.privacyMode;
notifyListeners();
return;
}
_clients[index].authorized = true;
_clients[index].privacyMode = client.privacyMode;
}
} else {
final index = _clients.indexWhere((c) => c.id == client.id);
if (index >= 0) {
_clients[index].privacyMode = client.privacyMode;
notifyListeners();
if (_clients.any((c) => c.id == client.id)) {
return;
}
_clients.add(client);
@@ -827,7 +829,6 @@ class Client {
bool restart = false;
bool recording = false;
bool blockInput = false;
bool privacyMode = false;
bool disconnected = false;
bool fromSwitch = false;
bool inVoiceCall = false;
@@ -856,7 +857,6 @@ class Client {
restart = json['restart'];
recording = json['recording'];
blockInput = json['block_input'];
privacyMode = json['privacy_mode'] ?? privacyMode;
disconnected = json['disconnected'];
fromSwitch = json['from_switch'];
inVoiceCall = json['in_voice_call'];
@@ -881,7 +881,6 @@ class Client {
data['restart'] = restart;
data['recording'] = recording;
data['block_input'] = blockInput;
data['privacy_mode'] = privacyMode;
data['disconnected'] = disconnected;
data['from_switch'] = fromSwitch;
data['in_voice_call'] = inVoiceCall;

View File

@@ -27,30 +27,25 @@ class TerminalModel with ChangeNotifier {
// Buffer for output data received before terminal view has valid dimensions.
// This prevents NaN errors when writing to terminal before layout is complete.
final _pendingOutputChunks = <String>[];
final _pendingOutputSuppressFlags = <bool>[];
int _pendingOutputSize = 0;
static const int _kMaxOutputBufferChars = 8 * 1024;
// View ready state: true when terminal has valid dimensions, safe to write
bool _terminalViewReady = false;
bool _markViewReadyScheduled = false;
bool _suppressTerminalOutput = false;
bool _suppressNextTerminalDataOutput = false;
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
void Function(int w, int h, int pw, int ph)? onResizeExternal;
Future<void> _handleInput(String data) async {
// Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a
// real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'.
// - Peer Windows: '\r' works, '\n' is just a newline.
// - Peer Linux: canonical-mode shells accept both, but raw-mode apps
// (readline, prompt_toolkit, vim, TUI frameworks) expect '\r'.
// - Peer macOS: same as Linux, raw-mode apps expect '\r'
// (https://github.com/rustdesk/rustdesk/issues/14907).
// So on mobile / web-mobile, always normalize a lone '\n' to '\r'.
// We deliberately do not touch multi-character payloads (e.g. pasted text)
// so embedded newlines in pasted content are preserved.
// If we press the `Enter` button on Android,
// `data` can be '\r' or '\n' when using different keyboards.
// Android -> Windows. '\r' works, but '\n' does not. '\n' is just a newline.
// Android -> Linux. Both '\r' and '\n' work as expected (execute a command).
// So when we receive '\n', we may need to convert it to '\r' to ensure compatibility.
// Desktop -> Desktop works fine.
// Check if we are on mobile or web(mobile), and convert '\n' to '\r'.
final isMobileOrWebMobile = (isMobile || (isWeb && !isWebDesktop));
if (isMobileOrWebMobile && data == '\n') {
if (isMobileOrWebMobile && isPeerWindows && data == '\n') {
data = '\r';
}
if (_terminalOpened) {
@@ -75,10 +70,7 @@ class TerminalModel with ChangeNotifier {
terminalController = TerminalController();
// Setup terminal callbacks
terminal.onOutput = (data) {
if (_suppressTerminalOutput) return;
_handleInput(data);
};
terminal.onOutput = _handleInput;
terminal.onResize = (w, h, pw, ph) async {
// Validate all dimensions before using them
@@ -92,7 +84,7 @@ class TerminalModel with ChangeNotifier {
// Mark terminal view as ready and flush any buffered output on first valid resize.
// Must be after onResizeExternal so the view layer has valid dimensions before flushing.
if (!_terminalViewReady) {
_scheduleMarkViewReady();
_markViewReady();
}
if (_terminalOpened) {
@@ -118,16 +110,14 @@ class TerminalModel with ChangeNotifier {
void onReady() {
parent.dialogManager.dismissAll();
// Fire and forget - don't block onReady. If the transport reconnects while
// this model is still open, re-send OpenTerminal so the remote service marks
// the persistent session active again and resumes output streaming.
openTerminal(force: _terminalOpened).catchError((e) {
// Fire and forget - don't block onReady
openTerminal().catchError((e) {
debugPrint('[TerminalModel] Error opening terminal: $e');
});
}
Future<void> openTerminal({bool force = false}) async {
if (_terminalOpened && !force) return;
Future<void> openTerminal() async {
if (_terminalOpened) return;
// Request the remote side to open a terminal with default shell
// The remote side will decide which shell to use based on its OS
@@ -285,12 +275,9 @@ class TerminalModel with ChangeNotifier {
if (success) {
_terminalOpened = true;
// On reconnect, the server may replay recent output. That replay can include
// terminal queries like DSR/DA; xterm answers them through onOutput as
// "^[[1;1R^[[2;2R^[[>0;0;0c", which must not be sent back to the peer.
final replayTerminalOutput = evt['replay_terminal_output'];
_suppressNextTerminalDataOutput = replayTerminalOutput == true ||
message == 'Reconnected to existing terminal with pending output';
// On reconnect ("Reconnected to existing terminal"), server may replay recent output.
// If this TerminalView instance is reused (not rebuilt), duplicate lines can appear.
// We intentionally accept this tradeoff for now to keep logic simple.
// Fallback: if terminal view is not yet ready but already has valid
// dimensions (e.g. layout completed before open response arrived),
@@ -298,7 +285,7 @@ class TerminalModel with ChangeNotifier {
if (!_terminalViewReady &&
terminal.viewWidth > 0 &&
terminal.viewHeight > 0) {
_scheduleMarkViewReady();
_markViewReady();
}
// Process any buffered input
@@ -310,16 +297,12 @@ class TerminalModel with ChangeNotifier {
});
final persistentSessions =
(evt['persistent_sessions'] as List<dynamic>? ?? [])
.whereType<int>()
.where((id) => !parent.terminalModels.containsKey(id))
.toList();
evt['persistent_sessions'] as List<dynamic>? ?? [];
if (kWindowId != null && persistentSessions.isNotEmpty) {
DesktopMultiWindow.invokeMethod(
kWindowId!,
kWindowEventRestoreTerminalSessions,
jsonEncode({
'peer_id': id,
'persistent_sessions': persistentSessions,
}));
}
@@ -349,8 +332,6 @@ class TerminalModel with ChangeNotifier {
final data = evt['data'];
if (data != null) {
final suppressTerminalOutput = _suppressNextTerminalDataOutput;
_suppressNextTerminalDataOutput = false;
try {
String text = '';
if (data is String) {
@@ -370,7 +351,7 @@ class TerminalModel with ChangeNotifier {
return;
}
_writeToTerminal(text, suppressTerminalOutput: suppressTerminalOutput);
_writeToTerminal(text);
} catch (e) {
debugPrint('[TerminalModel] Failed to process terminal data: $e');
}
@@ -380,10 +361,7 @@ class TerminalModel with ChangeNotifier {
/// Write text to terminal, buffering if the view is not yet ready.
/// All terminal output should go through this method to avoid NaN errors
/// from writing before the terminal view has valid layout dimensions.
void _writeToTerminal(
String text, {
bool suppressTerminalOutput = false,
}) {
void _writeToTerminal(String text) {
if (!_terminalViewReady) {
// If a single chunk exceeds the cap, keep only its tail.
// Note: truncation may split a multi-byte ANSI escape sequence,
@@ -395,73 +373,34 @@ class TerminalModel with ChangeNotifier {
_pendingOutputChunks
..clear()
..add(truncated);
_pendingOutputSuppressFlags
..clear()
..add(suppressTerminalOutput);
_pendingOutputSize = truncated.length;
} else {
_pendingOutputChunks.add(text);
_pendingOutputSuppressFlags.add(suppressTerminalOutput);
_pendingOutputSize += text.length;
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
while (_pendingOutputSize > _kMaxOutputBufferChars &&
_pendingOutputChunks.length > 1) {
final removed = _pendingOutputChunks.removeAt(0);
_pendingOutputSuppressFlags.removeAt(0);
_pendingOutputSize -= removed.length;
}
}
return;
}
_writeTerminalChunk(text, suppressTerminalOutput: suppressTerminalOutput);
terminal.write(text);
}
void _flushOutputBuffer() {
if (_pendingOutputChunks.isEmpty) return;
debugPrint(
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
for (var i = 0; i < _pendingOutputChunks.length; i++) {
_writeTerminalChunk(
_pendingOutputChunks[i],
suppressTerminalOutput: _pendingOutputSuppressFlags[i],
);
for (final chunk in _pendingOutputChunks) {
terminal.write(chunk);
}
_pendingOutputChunks.clear();
_pendingOutputSuppressFlags.clear();
_pendingOutputSize = 0;
}
void _writeTerminalChunk(
String text, {
required bool suppressTerminalOutput,
}) {
if (!suppressTerminalOutput) {
terminal.write(text);
return;
}
final previous = _suppressTerminalOutput;
_suppressTerminalOutput = true;
try {
terminal.write(text);
} finally {
_suppressTerminalOutput = previous;
}
}
/// Mark terminal view as ready and flush buffered output.
void _scheduleMarkViewReady() {
if (_disposed || _terminalViewReady || _markViewReadyScheduled) return;
_markViewReadyScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_markViewReadyScheduled = false;
if (_disposed || _terminalViewReady) return;
if (terminal.viewWidth > 0 && terminal.viewHeight > 0) {
_markViewReady();
}
});
WidgetsBinding.instance.ensureVisualUpdate();
}
void _markViewReady() {
if (_terminalViewReady) return;
_terminalViewReady = true;
@@ -487,10 +426,7 @@ class TerminalModel with ChangeNotifier {
// Clear buffers to free memory
_inputBuffer.clear();
_pendingOutputChunks.clear();
_pendingOutputSuppressFlags.clear();
_pendingOutputSize = 0;
_markViewReadyScheduled = false;
_suppressNextTerminalDataOutput = false;
// Terminal cleanup is handled server-side when service closes
super.dispose();
}

View File

@@ -1159,6 +1159,10 @@ class RustdeskImpl {
return Future.value('');
}
Future<String> mainGetPermanentPassword({dynamic hint}) {
return Future.value('');
}
Future<String> mainGetFingerprint({dynamic hint}) {
return Future.value('');
}
@@ -1342,9 +1346,9 @@ class RustdeskImpl {
throw UnimplementedError("mainUpdateTemporaryPassword");
}
Future<bool> mainSetPermanentPasswordWithResult(
Future<void> mainSetPermanentPassword(
{required String password, dynamic hint}) {
throw UnimplementedError("mainSetPermanentPasswordWithResult");
throw UnimplementedError("mainSetPermanentPassword");
}
Future<bool> mainCheckSuperUserPermission({dynamic hint}) {
@@ -1538,10 +1542,7 @@ class RustdeskImpl {
Future<void> mainAccountAuth(
{required String op, required bool rememberMe, dynamic hint}) {
// Safari only allows auth popups while handling the original user gesture.
// Use Future.sync so the JS call runs synchronously (pre-opening the OIDC
// window) while any interop error still surfaces as a Future error.
return Future.sync(() => js.context.callMethod('setByName', [
return Future(() => js.context.callMethod('setByName', [
'account_auth',
jsonEncode({'op': op, 'remember': rememberMe})
]));
@@ -1729,7 +1730,7 @@ class RustdeskImpl {
}
String mainSupportedPrivacyModeImpls({dynamic hint}) {
return '[]';
throw UnimplementedError("mainSupportedPrivacyModeImpls");
}
String mainSupportedInputSource({dynamic hint}) {
@@ -2034,14 +2035,7 @@ class RustdeskImpl {
}
String mainResolveAvatarUrl({required String avatar, dynamic hint}) {
return js.context.callMethod(
'getByName', ['resolve_avatar_url', avatar])?.toString() ??
avatar;
}
Future<String> mainDeployDevice(
{required String token, required String id, dynamic hint}) {
throw UnimplementedError("mainDeployDevice");
return js.context.callMethod('getByName', ['resolve_avatar_url', avatar])?.toString() ?? avatar;
}
void dispose() {}

View File

@@ -29,80 +29,6 @@ void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view);
extern bool gIsConnectionManager;
// --- Side mouse button support (back/forward) ---
// Flutter's Linux embedder doesn't deliver X11 button 8/9 events to Dart.
// We intercept them via GDK and forward through a dedicated platform channel.
static const char* kSideButtonChannelName = "org.rustdesk.rustdesk/side_buttons";
static gboolean on_side_button_event(GtkWidget* widget, GdkEventButton* event, gpointer user_data) {
if (event->button != 8 && event->button != 9) {
return FALSE;
}
// Ignore GDK_2BUTTON_PRESS / GDK_3BUTTON_PRESS (double/triple-click synthetic
// events) - only handle real press and release.
if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE) {
return FALSE;
}
FlMethodChannel* channel = FL_METHOD_CHANNEL(user_data);
if (channel == NULL) return FALSE;
g_autoptr(FlValue) args = fl_value_new_map();
fl_value_set_string_take(args, "button",
fl_value_new_string(event->button == 8 ? "back" : "forward"));
fl_value_set_string_take(args, "type",
fl_value_new_string(event->type == GDK_BUTTON_PRESS ? "down" : "up"));
fl_method_channel_invoke_method(channel, "onSideMouseButton", args,
NULL, NULL, NULL);
return TRUE;
}
static FlMethodChannel* side_buttons_create_channel(FlEngine* engine) {
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
return fl_method_channel_new(
fl_engine_get_binary_messenger(engine),
kSideButtonChannelName,
FL_METHOD_CODEC(codec));
}
static void side_buttons_channel_destroy(gpointer data) {
g_object_unref(data);
}
static void side_buttons_init_for_window(GtkWindow* window, FlMethodChannel* channel) {
// Guard against double-initialization (would leave dangling signal user_data).
if (g_object_get_data(G_OBJECT(window), "side-buttons-channel") != NULL) return;
gtk_widget_add_events(GTK_WIDGET(window),
GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
// Store channel on the window so it stays alive and is freed with the window.
g_object_set_data_full(G_OBJECT(window), "side-buttons-channel",
g_object_ref(channel), side_buttons_channel_destroy);
g_signal_connect(window, "button-press-event",
G_CALLBACK(on_side_button_event), channel);
g_signal_connect(window, "button-release-event",
G_CALLBACK(on_side_button_event), channel);
}
static void on_subwindow_created(FlPluginRegistry* registry) {
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
wayland_shortcuts_inhibit_init_for_subwindow(registry);
#endif
// Set up side button forwarding for sub-windows.
if (registry == NULL || !FL_IS_VIEW(registry)) return;
FlView* view = FL_VIEW(registry);
GtkWidget* toplevel = gtk_widget_get_toplevel(GTK_WIDGET(view));
if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) {
FlMethodChannel* channel = side_buttons_create_channel(fl_view_get_engine(view));
if (channel == NULL) return;
side_buttons_init_for_window(GTK_WINDOW(toplevel), channel);
g_object_unref(channel); // window now owns a ref via g_object_set_data_full
}
}
GtkWidget *find_gl_area(GtkWidget *widget);
// Implements GApplication::activate.
@@ -170,12 +96,12 @@ static void my_application_activate(GApplication* application) {
gtk_widget_show(GTK_WIDGET(window));
gtk_widget_show(GTK_WIDGET(view));
// Register callback for sub-windows created by desktop_multi_window plugin.
// Handles both Wayland shortcuts inhibition (guarded inside) and side button
// forwarding. Safe to call on X11-only builds - the plugin just stores the
// callback pointer regardless of windowing system.
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
// Register callback for sub-windows created by desktop_multi_window plugin
// Only sub-windows (remote windows) need keyboard shortcuts inhibition
desktop_multi_window_plugin_set_window_created_callback(
(WindowCreatedCallback)on_subwindow_created);
(WindowCreatedCallback)wayland_shortcuts_inhibit_init_for_subwindow);
#endif
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
@@ -190,11 +116,6 @@ static void my_application_activate(GApplication* application) {
self,
nullptr);
// Forward side mouse button events (back/forward) to Dart on the main window.
FlMethodChannel* side_channel = side_buttons_create_channel(fl_view_get_engine(view));
side_buttons_init_for_window(window, side_channel);
g_object_unref(side_channel);
gtk_widget_grab_focus(GTK_WIDGET(view));
}

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo ndk --platform 21 --target armv7-linux-androideabi build --locked --release --features flutter,hwcodec
cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter,hwcodec

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo ndk --platform 21 --target aarch64-linux-android build --locked --release --features flutter,hwcodec
cargo ndk --platform 21 --target aarch64-linux-android build --release --features flutter,hwcodec

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo ndk --platform 21 --target x86_64-linux-android build --locked --release --features flutter
cargo ndk --platform 21 --target x86_64-linux-android build --release --features flutter

View File

@@ -7,4 +7,4 @@
export CFLAGS="-DBROKEN_CLANG_ATOMICS"
export CXXFLAGS="-DBROKEN_CLANG_ATOMICS"
cargo ndk --platform 21 --target i686-linux-android build --locked --release --features flutter
cargo ndk --platform 21 --target i686-linux-android build --release --features flutter

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
version: 1.4.7+65
version: 1.4.6+64
environment:
sdk: '^3.1.0'
@@ -113,8 +113,8 @@ dependencies:
dev_dependencies:
icons_launcher: ^2.0.4
flutter_test:
sdk: flutter
#flutter_test:
#sdk: flutter
build_runner: ^2.4.6
freezed: ^2.4.2
flutter_lints: ^2.0.2

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env bash
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid --locked
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid
flutter pub get
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
# call `flutter clean` if cargo build fails
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
cargo build --locked --features flutter
cargo build --features flutter
flutter run $@

View File

@@ -1,125 +0,0 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_hbb/models/input_modifier_utils.dart';
void main() {
group('shouldReleaseStaleMobileShift', () {
test('does not release when cached shift is already false', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: false,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.keyD,
hasTrackedShiftKeyDown: true,
),
isFalse,
);
});
test('releases one-shot mobile shift after a text key', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.keyD,
hasTrackedShiftKeyDown: true,
),
isTrue,
);
});
test('does not release manually toggled shift without tracked key down',
() {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.keyD,
hasTrackedShiftKeyDown: false,
),
isFalse,
);
});
test('does not release when shift is still physically pressed', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: true,
logicalKey: LogicalKeyboardKey.keyD,
hasTrackedShiftKeyDown: true,
),
isFalse,
);
});
test('does not release on non-mobile platforms', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: false,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.keyD,
hasTrackedShiftKeyDown: true,
),
isFalse,
);
});
test('releases on enter key', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.enter,
hasTrackedShiftKeyDown: true,
),
isTrue,
);
});
test('releases on arrow key', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.arrowLeft,
hasTrackedShiftKeyDown: true,
),
isTrue,
);
});
test('does not release on modifier events', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.shiftLeft,
hasTrackedShiftKeyDown: true,
),
isFalse,
);
});
test('does not release on shiftRight modifier events', () {
expect(
shouldReleaseStaleMobileShift(
isMobile: true,
cachedShiftPressed: true,
actualShiftPressed: false,
logicalKey: LogicalKeyboardKey.shiftRight,
hasTrackedShiftKeyDown: true,
),
isFalse,
);
});
});
}

View File

@@ -12,7 +12,7 @@
//!
//! For now, we transfer all file names with windows separators, UTF-16 encoded.
//! *Need a way to transfer file names with '\' safely*.
//! Maybe we can use URL encoded file names and '/' separators as a new standard, while keep the support to old schemes.
//! Maybe we can use URL encoded file names and '/' seperators as a new standard, while keep the support to old schemes.
//!
//! # Note
//! - all files on FS should be read only, and mark the owner to be the current user

View File

@@ -39,28 +39,6 @@
#define CLIPRDR_SVC_CHANNEL_NAME "cliprdr"
/* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */
#define WF_CLIPRDR_MAX_STREAMS 16384
/* Validates the remote descriptor array size after cItems has been read safely. */
static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
{
SIZE_T header_size = offsetof(FILEGROUPDESCRIPTORW, fgd);
SIZE_T descriptors_size;
if (count == 0 || count > WF_CLIPRDR_MAX_STREAMS)
return FALSE;
if (size < header_size)
return FALSE;
if ((SIZE_T)count > (((SIZE_T)-1) - header_size) / sizeof(FILEDESCRIPTORW))
return FALSE;
descriptors_size = header_size + (SIZE_T)count * sizeof(FILEDESCRIPTORW);
return size >= descriptors_size;
}
/**
* Clipboard Formats
*/
@@ -246,7 +224,6 @@ struct wf_clipboard
HWND hwnd;
HANDLE hmem;
SIZE_T hmem_data_len;
HANDLE thread;
HANDLE formatDataRespEvent;
BOOL formatDataRespReceived;
@@ -647,55 +624,10 @@ void CliprdrStream_Delete(CliprdrStream *instance)
if (instance)
{
free(instance->iStream.lpVtbl);
instance->iStream.lpVtbl = NULL;
free(instance);
}
}
static void wf_cliprdr_release_streams(IStream **streams, ULONG count)
{
ULONG i;
if (!streams)
return;
for (i = 0; i < count; i++)
{
if (streams[i])
CliprdrStream_Release(streams[i]);
}
free(streams);
}
static void wf_cliprdr_reset_streams(CliprdrDataObject *instance)
{
if (!instance)
return;
wf_cliprdr_release_streams(instance->m_pStream, instance->m_nStreams);
instance->m_pStream = NULL;
instance->m_nStreams = 0;
}
/* Only call after clipboard->hmem has been locked by GlobalLock. */
static HRESULT wf_cliprdr_fail_locked_file_descriptor_data(wfClipboard *clipboard,
STGMEDIUM *medium,
CliprdrDataObject *instance,
IStream **streams,
ULONG stream_count,
HRESULT error)
{
GlobalUnlock(clipboard->hmem);
GlobalFree(clipboard->hmem);
clipboard->hmem = NULL;
clipboard->hmem_data_len = 0;
medium->hGlobal = NULL;
wf_cliprdr_release_streams(streams, stream_count);
wf_cliprdr_reset_streams(instance);
return error;
}
/**
* IDataObject
*/
@@ -814,9 +746,6 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetData(IDataObject *This, FO
{
// FILEGROUPDESCRIPTOR *dsc;
FILEGROUPDESCRIPTORW *dsc;
IStream **streams = NULL;
UINT stream_count = 0;
SIZE_T hmem_size;
// DWORD remote_format_id = get_remote_format_id(clipboard, instance->m_pFormatEtc[idx].cfFormat);
// FIXME: origin code may be failed here???
if (cliprdr_send_data_request(instance->m_connID, clipboard, instance->m_pFormatEtc[idx].cfFormat) != 0)
@@ -834,48 +763,40 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetData(IDataObject *This, FO
* is the number of FILEDESCRIPTOR's */
// dsc = (FILEGROUPDESCRIPTOR *)GlobalLock(clipboard->hmem);
dsc = (FILEGROUPDESCRIPTORW *)GlobalLock(clipboard->hmem);
if (!dsc)
instance->m_nStreams = dsc->cItems;
GlobalUnlock(clipboard->hmem);
if (instance->m_nStreams > 0)
{
pMedium->hGlobal = NULL;
GlobalFree(clipboard->hmem);
clipboard->hmem = NULL;
clipboard->hmem_data_len = 0;
wf_cliprdr_reset_streams(instance);
return E_UNEXPECTED;
}
hmem_size = clipboard->hmem_data_len;
/* cItems is remote-controlled; verify the fixed header exists before reading it. */
if (hmem_size < offsetof(FILEGROUPDESCRIPTORW, fgd))
return wf_cliprdr_fail_locked_file_descriptor_data(
clipboard, pMedium, instance, NULL, 0, E_UNEXPECTED);
stream_count = dsc->cItems;
if (!wf_cliprdr_file_group_descriptor_size_valid(hmem_size, stream_count))
return wf_cliprdr_fail_locked_file_descriptor_data(
clipboard, pMedium, instance, NULL, 0, E_UNEXPECTED);
streams = (IStream **)calloc(stream_count, sizeof(IStream *));
if (!streams)
return wf_cliprdr_fail_locked_file_descriptor_data(
clipboard, pMedium, instance, NULL, 0, E_OUTOFMEMORY);
for (i = 0; i < stream_count; i++)
{
streams[i] =
(IStream *)CliprdrStream_New(instance->m_connID, i, clipboard, &dsc->fgd[i]);
if (!streams[i])
if (!instance->m_pStream)
{
return wf_cliprdr_fail_locked_file_descriptor_data(
clipboard, pMedium, instance, streams, i, E_OUTOFMEMORY);
instance->m_pStream = (LPSTREAM *)calloc(instance->m_nStreams, sizeof(LPSTREAM));
if (instance->m_pStream)
{
for (i = 0; i < instance->m_nStreams; i++)
{
instance->m_pStream[i] =
(IStream *)CliprdrStream_New(instance->m_connID, i, clipboard, &dsc->fgd[i]);
if (!instance->m_pStream[i])
return E_OUTOFMEMORY;
}
}
}
}
GlobalUnlock(clipboard->hmem);
wf_cliprdr_reset_streams(instance);
instance->m_pStream = streams;
instance->m_nStreams = stream_count;
return S_OK;
if (!instance->m_pStream)
{
if (clipboard->hmem)
{
GlobalFree(clipboard->hmem);
clipboard->hmem = NULL;
}
pMedium->hGlobal = NULL;
return E_OUTOFMEMORY;
}
}
else if (instance->m_pFormatEtc[idx].cfFormat == RegisterClipboardFormat(CFSTR_FILECONTENTS))
{
@@ -2239,16 +2160,16 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi
return FALSE;
/* add to name array */
clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc(MAX_PATH * 2);
if (!clipboard->file_names[clipboard->nFiles])
return FALSE;
// `MAX_PATH` is long enough for the file name.
// So we just return FALSE if the file name is too long, which is not a normal case.
if ((wcslen(full_file_name) + 1) > MAX_PATH)
return FALSE;
clipboard->file_names[clipboard->nFiles] = (LPWSTR)calloc(MAX_PATH, sizeof(WCHAR));
if (!clipboard->file_names[clipboard->nFiles])
return FALSE;
wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1);
/* add to descriptor array */
clipboard->fileDescriptor[clipboard->nFiles] =
@@ -2856,7 +2777,6 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
break;
}
clipboard->hmem = NULL;
clipboard->hmem_data_len = 0;
if (formatDataResponse->msgFlags != CB_RESPONSE_OK)
{
@@ -2890,7 +2810,6 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
break;
}
clipboard->hmem_data_len = formatDataResponse->dataLen;
clipboard->hmem = hMem;
rc = CHANNEL_RC_OK;
} while (0);

View File

@@ -8,7 +8,6 @@
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
use hbb_common::libc::c_int;
use hbb_common::x11::xlib::{Display, XCloseDisplay, XGetPointerMapping, XOpenDisplay};
use libxdo_sys::{self, xdo_t, CURRENTWINDOW};
use std::{borrow::Cow, ffi::CString};
@@ -33,51 +32,6 @@ fn mousebutton(button: MouseButton) -> c_int {
}
}
/// Minimum number of buttons the X11 core pointer must support.
/// Buttons 8 (Back) and 9 (Forward) are needed for mouse side buttons.
const MIN_POINTER_BUTTONS: usize = 9;
/// Check that the X11 core pointer's button map includes at least 9 buttons
/// so that `XTestFakeButtonEvent` can simulate Back (8) and Forward (9).
///
/// RustDesk's uinput "Mouse passthrough" device normally provides enough
/// buttons, but we log a warning if the map is too small so the issue is
/// diagnosable. `XSetPointerMapping` cannot extend the button count (its
/// length must match `XGetPointerMapping`), so we only diagnose here.
fn check_x11_button_map() {
// Skip on non-X11 sessions to avoid noisy "XOpenDisplay failed" warnings
// on pure Wayland or headless environments without $DISPLAY.
if std::env::var_os("DISPLAY").is_none() {
return;
}
let display: *mut Display = unsafe { XOpenDisplay(std::ptr::null()) };
if display.is_null() {
log::warn!("XOpenDisplay failed, cannot check button map");
return;
}
let mut current_map = [0u8; 32];
let nbuttons =
unsafe { XGetPointerMapping(display, current_map.as_mut_ptr(), current_map.len() as i32) };
unsafe { XCloseDisplay(display) };
if nbuttons < 0 {
log::warn!("XGetPointerMapping failed (returned {nbuttons})");
return;
}
let nbuttons = nbuttons as usize;
if nbuttons >= MIN_POINTER_BUTTONS {
log::info!("X11 pointer has {nbuttons} buttons, side buttons supported");
} else {
log::warn!(
"X11 pointer has only {nbuttons} buttons (need {MIN_POINTER_BUTTONS}); \
back/forward side buttons may not work until a device with more buttons is added"
);
}
}
/// The main struct for handling the event emitting
pub(super) struct EnigoXdo {
xdo: *mut xdo_t,
@@ -98,7 +52,6 @@ impl Default for EnigoXdo {
log::warn!("Failed to create xdo context, xdo functions will be disabled");
} else {
log::info!("xdo context created successfully");
check_x11_button_map();
}
Self {
xdo,

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk-portable-packer"
version = "1.4.7"
version = "1.4.6"
edition = "2021"
description = "RustDesk Remote Desktop"

View File

@@ -67,9 +67,9 @@ def write_app_metadata(output_folder: str):
def build_portable(output_folder: str, target: str):
os.chdir(output_folder)
if target:
os.system("cargo build --locked --release --target " + target)
os.system("cargo build --release --target " + target)
else:
os.system("cargo build --locked --release")
os.system("cargo build --release")
# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py
# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe

View File

@@ -151,7 +151,7 @@ fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option<Medi
log::error!("Failed to start decoder: {:?}", e);
return None;
};
log::debug!("Init decoder succeeded!: {:?}", name);
log::debug!("Init decoder successed!: {:?}", name);
return Some(MediaCodecDecoder {
decoder: codec,
name: name.to_owned(),

View File

@@ -24,7 +24,7 @@ impl<'a> PixelProvider<'a> {
}
pub trait Recorder {
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider<'_>, Box<dyn Error>>;
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider, Box<dyn Error>>;
}
pub trait BoxCloneCapturable {

View File

@@ -276,21 +276,12 @@ impl PipeWireRecorder {
// see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982
src.set_property("always-copy", &true)?;
// COSMIC/Wayland fix: insert videoconvert between pipewiresrc and appsink.
// xdg-desktop-portal-cosmic's modifier negotiation fails when the downstream
// format set is too narrow (appsink only accepts BGRx/RGBx), producing
// "no more output formats" / not-negotiated (-4). videoconvert accepts any
// system-memory video/x-raw format, widening negotiation so the portal can
// settle on a format it can deliver via its SHM path.
let convert = gst::ElementFactory::make("videoconvert", None)?;
let sink = gst::ElementFactory::make("appsink", None)?;
sink.set_property("drop", &true)?;
sink.set_property("max-buffers", &1u32)?;
pipeline.add_many(&[&src, &convert, &sink])?;
src.link(&convert)?;
convert.link(&sink)?;
pipeline.add_many(&[&src, &sink])?;
src.link(&sink)?;
let appsink = sink
.dynamic_cast::<AppSink>()
@@ -355,7 +346,7 @@ impl PipeWireRecorder {
}
impl Recorder for PipeWireRecorder {
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider<'_>, Box<dyn Error>> {
fn capture(&mut self, timeout_ms: u64) -> Result<PixelProvider, Box<dyn Error>> {
if let Some(sample) = self
.appsink
.try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms))

View File

@@ -29,4 +29,4 @@ TODO
## X11
## macOS
## OSX

View File

@@ -1,5 +1,5 @@
pkgname=rustdesk
pkgver=1.4.7
pkgver=1.4.6
pkgrel=0
epoch=
pkgdesc=""

82
res/audits.py Executable file → Normal file
View File

@@ -43,7 +43,7 @@ def get_connection_type_name(conn_type):
"""Convert connection type number to readable name"""
type_map = {
0: "Remote Desktop",
1: "File Transfer",
1: "File Transfer",
2: "Port Transfer",
3: "View Camera",
4: "Terminal"
@@ -55,7 +55,7 @@ def get_console_type_name(console_type):
"""Convert console audit type number to readable name"""
type_map = {
0: "Group Management",
1: "User Management",
1: "User Management",
2: "Device Management",
3: "Address Book Management"
}
@@ -67,7 +67,7 @@ def get_console_operation_name(operation_code):
operation_map = {
0: "User Login",
1: "Add Group",
2: "Add User",
2: "Add User",
3: "Add Device",
4: "Delete Groups",
5: "Disconnect Device",
@@ -95,7 +95,7 @@ def get_console_operation_name(operation_code):
def get_alarm_type_name(alarm_type):
"""Convert alarm type number to readable name"""
type_map = {
0: "Access attempt outside the IP whitelist",
0: "Access attempt outside the IP whiltelist",
1: "Over 30 consecutive access attempts",
2: "Multiple access attempts within one minute",
3: "Over 30 consecutive login attempts",
@@ -109,24 +109,24 @@ def enhance_audit_data(data, audit_type):
"""Enhance audit data with readable formats"""
if not data:
return data
enhanced_data = []
for item in data:
enhanced_item = item.copy()
# Convert timestamps - replace original values
if 'created_at' in enhanced_item:
enhanced_item['created_at'] = format_timestamp(enhanced_item['created_at'])
if 'end_time' in enhanced_item:
enhanced_item['end_time'] = format_timestamp(enhanced_item['end_time'])
# Add type-specific enhancements - replace original values
if audit_type == 'conn':
if 'conn_type' in enhanced_item:
enhanced_item['conn_type'] = get_connection_type_name(enhanced_item['conn_type'])
else:
enhanced_item['conn_type'] = "Not Logged In"
elif audit_type == 'console':
if 'typ' in enhanced_item:
# Replace typ field with type and convert to readable name
@@ -136,14 +136,14 @@ def enhance_audit_data(data, audit_type):
# Replace iop field with operation and convert to readable name
enhanced_item['operation'] = get_console_operation_name(enhanced_item['iop'])
del enhanced_item['iop']
elif audit_type == 'alarm' and 'typ' in enhanced_item:
# Replace typ field with type and convert to readable name
enhanced_item['type'] = get_alarm_type_name(enhanced_item['typ'])
del enhanced_item['typ']
enhanced_data.append(enhanced_item)
return enhanced_data
@@ -152,7 +152,7 @@ def check_response(response):
if response.status_code != 200:
print(f"Error: HTTP {response.status_code} - {response.text}")
exit(1)
try:
response_json = response.json()
if "error" in response_json:
@@ -163,28 +163,28 @@ def check_response(response):
return response.text or "Success"
def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None,
def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None,
created_at=None, days_ago=None, non_wildcard_fields=None):
"""Common function for viewing audits"""
headers = {"Authorization": f"Bearer {token}"}
# Set default page size and current page
if page_size is None:
page_size = 10
if current is None:
current = 1
params = {
"pageSize": page_size,
"current": current
}
# Add filter parameters if provided
if filters:
for key, value in filters.items():
if value is not None:
params[key] = value
# Handle time filters
if days_ago is not None:
# Calculate datetime from days ago
@@ -205,10 +205,10 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
# Apply wildcard patterns for string fields (excluding specific fields)
if non_wildcard_fields is None:
non_wildcard_fields = set()
# Always exclude these fields from wildcard treatment
non_wildcard_fields.update(["created_at", "pageSize", "current"])
string_params = {}
for k, v in params.items():
if isinstance(v, str) and k not in non_wildcard_fields:
@@ -221,10 +221,10 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
response = requests.get(f"{url}/api/audits/{endpoint}", headers=headers, params=string_params)
response_json = check_response(response)
# Enhance the data with readable formats
data = enhance_audit_data(response_json.get("data", []), endpoint)
return {
"data": data,
"total": response_json.get("total", 0),
@@ -233,7 +233,7 @@ def view_audits_common(url, token, endpoint, filters=None, page_size=None, curre
}
def view_conn_audits(url, token, remote=None, conn_type=None,
def view_conn_audits(url, token, remote=None, conn_type=None,
page_size=None, current=None, created_at=None, days_ago=None):
"""View connection audits"""
filters = {
@@ -241,7 +241,7 @@ def view_conn_audits(url, token, remote=None, conn_type=None,
"conn_type": conn_type
}
non_wildcard_fields = {"conn_type"}
return view_audits_common(
url, token, "conn", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -254,7 +254,7 @@ def view_file_audits(url, token, remote=None,
"remote": remote
}
non_wildcard_fields = set()
return view_audits_common(
url, token, "file", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -267,7 +267,7 @@ def view_alarm_audits(url, token, device=None,
"device": device
}
non_wildcard_fields = set()
return view_audits_common(
url, token, "alarm", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -280,7 +280,7 @@ def view_console_audits(url, token, operator=None,
"operator": operator
}
non_wildcard_fields = set()
return view_audits_common(
url, token, "console", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
@@ -295,15 +295,15 @@ def main():
)
parser.add_argument("--url", required=True, help="URL of the API")
parser.add_argument("--token", required=True, help="Bearer token for authentication")
# Pagination parameters
parser.add_argument("--page-size", type=int, default=10, help="Number of records per page (default: 10)")
parser.add_argument("--current", type=int, default=1, help="Current page number (default: 1)")
# Time filtering parameters
parser.add_argument("--created-at", help="Filter by creation time in local time (format: 2025-09-16 14:15:57 or 2025-09-16 14:15:57.000)")
parser.add_argument("--days-ago", type=int, help="Filter by days ago (e.g., 7 for last 7 days)")
# Audit filters (simplified)
parser.add_argument("--remote", help="Remote peer ID filter (for conn/file audits)")
parser.add_argument("--device", help="Device ID filter (for alarm audits)")
@@ -319,9 +319,9 @@ def main():
if args.command == "view-conn":
# View connection audits
result = view_conn_audits(
args.url,
args.token,
args.remote,
args.url,
args.token,
args.remote,
args.conn_type,
args.page_size,
args.current,
@@ -329,12 +329,12 @@ def main():
args.days_ago
)
print(json.dumps(result, indent=2))
elif args.command == "view-file":
# View file audits
result = view_file_audits(
args.url,
args.token,
args.url,
args.token,
args.remote,
args.page_size,
args.current,
@@ -342,12 +342,12 @@ def main():
args.days_ago
)
print(json.dumps(result, indent=2))
elif args.command == "view-alarm":
# View alarm audits
result = view_alarm_audits(
args.url,
args.token,
args.url,
args.token,
args.device,
args.page_size,
args.current,
@@ -355,12 +355,12 @@ def main():
args.days_ago
)
print(json.dumps(result, indent=2))
elif args.command == "view-console":
# View console audits
result = view_console_audits(
args.url,
args.token,
args.url,
args.token,
args.operator,
args.page_size,
args.current,

View File

@@ -31,17 +31,17 @@ LExit:
return WcaFinalize(er);
}
// Helper function to safely delete a file using handle-based deletion.
// Directories are refused after opening the handle.
// Helper function to safely delete a file or directory using handle-based deletion.
// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
BOOL SafeDeleteItem(LPCWSTR fullPath)
{
// Open the file/directory with delete and attribute-read access plus FILE_FLAG_OPEN_REPARSE_POINT
// Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT
// to prevent following symlinks.
// Use shared access to allow deletion even when other processes have the file open.
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
HANDLE hFile = CreateFileW(
fullPath,
DELETE | FILE_READ_ATTRIBUTES,
DELETE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
NULL,
OPEN_EXISTING,
@@ -55,21 +55,6 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
return FALSE;
}
BY_HANDLE_FILE_INFORMATION fileInfo;
if (FALSE == GetFileInformationByHandle(hFile, &fileInfo))
{
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to inspect '%ls'. Error: %lu", fullPath, GetLastError());
CloseHandle(hFile);
return FALSE;
}
if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Refusing to delete directory '%ls'.", fullPath);
CloseHandle(hFile);
return FALSE;
}
// Use SetFileInformationByHandle to mark for deletion.
// The file will be deleted when the handle is closed.
FILE_DISPOSITION_INFO dispInfo;
@@ -92,74 +77,98 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
return result;
}
BOOL PathEndsWithSlash(LPCWSTR path)
// Helper function to recursively delete a directory's contents with detailed logging.
void RecursiveDelete(LPCWSTR path)
{
size_t length = 0;
HRESULT hr = StringCchLengthW(path, MAX_PATH, &length);
if (FAILED(hr) || length == 0)
{
return FALSE;
}
WCHAR last = path[length - 1];
return last == L'\\' || last == L'/';
}
void ClearReadOnlyAttribute(LPCWSTR fullPath, DWORD attributes)
{
if (!(attributes & FILE_ATTRIBUTE_READONLY))
// Ensure the path is not empty or null.
if (path == NULL || path[0] == L'\0')
{
return;
}
DWORD writableAttributes = attributes & ~FILE_ATTRIBUTE_READONLY;
if (writableAttributes == 0)
// Extra safety: never operate directly on a root path.
if (PathIsRootW(path))
{
writableAttributes = FILE_ATTRIBUTE_NORMAL;
}
if (SetFileAttributesW(fullPath, writableAttributes))
{
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cleared read-only attribute for '%ls'.", fullPath);
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path);
return;
}
WcaLog(LOGMSG_STANDARD, "Runtime cleanup failed to clear read-only attribute for '%ls'. Error: %lu", fullPath, GetLastError());
}
BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName)
{
WCHAR fullPath[MAX_PATH];
LPCWSTR separator = PathEndsWithSlash(installFolder) ? L"" : L"\\";
HRESULT hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s%s%s", installFolder, separator, fileName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Runtime cleanup path is too long for '%ls'.", fileName);
return FALSE;
// MAX_PATH is enough here since the installer should not be using longer paths.
// No need to handle extended-length paths (\\?\) in this context.
WCHAR searchPath[MAX_PATH];
HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path);
if (FAILED(hr)) {
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path);
return;
}
DWORD attributes = GetFileAttributesW(fullPath);
if (attributes == INVALID_FILE_ATTRIBUTES)
WIN32_FIND_DATAW findData;
HANDLE hFind = FindFirstFileW(searchPath, &findData);
if (hFind == INVALID_HANDLE_VALUE)
{
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND)
// This can happen if the directory is empty or doesn't exist, which is not an error in our case.
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError());
return;
}
do
{
// Skip '.' and '..' directories.
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
{
return TRUE;
continue;
}
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cannot stat '%ls'. Error: %lu", fullPath, error);
return FALSE;
}
// MAX_PATH is enough here since the installer should not be using longer paths.
// No need to handle extended-length paths (\\?\) in this context.
WCHAR fullPath[MAX_PATH];
hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName);
if (FAILED(hr)) {
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path);
continue;
}
if (attributes & FILE_ATTRIBUTE_DIRECTORY)
// Before acting, ensure the read-only attribute is not set.
if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
{
if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY))
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError());
}
}
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
// Check for reparse points (symlinks/junctions) to prevent directory traversal attacks.
// Do not follow reparse points, only remove the link itself.
if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath);
SafeDeleteItem(fullPath);
}
else
{
// Recursively delete directory contents first
RecursiveDelete(fullPath);
// Then delete the directory itself
SafeDeleteItem(fullPath);
}
}
else
{
// Delete file using safe handle-based deletion
SafeDeleteItem(fullPath);
}
} while (FindNextFileW(hFind, &findData) != 0);
DWORD lastError = GetLastError();
if (lastError != ERROR_NO_MORE_FILES)
{
WcaLog(LOGMSG_STANDARD, "Runtime cleanup skipped directory '%ls'.", fullPath);
return FALSE;
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
}
ClearReadOnlyAttribute(fullPath, attributes);
WcaLog(LOGMSG_STANDARD, "Runtime cleanup deleting '%ls'.", fullPath);
return SafeDeleteItem(fullPath);
FindClose(hFind);
}
// See `Package.wxs` for the sequence of this custom action.
@@ -169,13 +178,13 @@ BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName)
// 2. RemoveExistingProducts
// ├─ TerminateProcesses
// ├─ TryStopDeleteService
// ├─ RemoveRuntimeGeneratedFiles - <-- Here
// ├─ RemoveInstallFolder - <-- Here
// └─ RemoveFiles
// 3. InstallValidate
// 4. InstallFiles
// 5. InstallExecute
// 6. InstallFinalize
UINT __stdcall RemoveRuntimeGeneratedFiles(
UINT __stdcall RemoveInstallFolder(
__in MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
@@ -185,7 +194,7 @@ UINT __stdcall RemoveRuntimeGeneratedFiles(
LPWSTR pwz = NULL;
LPWSTR pwzData = NULL;
hr = WcaInitialize(hInstall, "RemoveRuntimeGeneratedFiles");
hr = WcaInitialize(hInstall, "RemoveInstallFolder");
ExitOnFailure(hr, "Failed to initialize");
hr = WcaGetProperty(L"CustomActionData", &pwzData);
@@ -193,20 +202,24 @@ UINT __stdcall RemoveRuntimeGeneratedFiles(
pwz = pwzData;
hr = WcaReadStringFromCaData(&pwz, &installFolder);
ExitOnFailure(hr, "failed to read install folder from custom action data: %ls", pwz);
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
if (installFolder == NULL || installFolder[0] == L'\0') {
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping runtime cleanup.");
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
goto LExit;
}
if (PathIsRootW(installFolder)) {
WcaLog(LOGMSG_STANDARD, "Refusing runtime cleanup in root folder '%ls'.", installFolder);
WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
goto LExit;
}
WcaLog(LOGMSG_STANDARD, "Removing runtime-generated files from install folder: %ls", installFolder);
DeleteRuntimeGeneratedFile(installFolder, L"RuntimeBroker_rustdesk.exe");
WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder);
RecursiveDelete(installFolder);
// The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories.
// We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer.
LExit:
ReleaseStr(pwzData);
@@ -603,10 +616,10 @@ UINT __stdcall TryStopDeleteService(__in MSIHANDLE hInstall)
}
if (IsServiceRunningW(svcName)) {
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not stopped after 1000 ms.", svcName);
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is not stoped after 1000 ms.", svcName);
}
else {
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stopped.", svcName);
WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is stoped.", svcName);
}
if (MyDeleteServiceW(svcName)) {
@@ -632,7 +645,7 @@ UINT __stdcall TryStopDeleteService(__in MSIHANDLE hInstall)
}
// It's really strange that we need sleep here.
// But the upgrading may be stuck at "copying new files" because the file is in using.
// But the upgrading may be stucked at "copying new files" because the file is in using.
// Steps to reproduce: Install -> stop service in tray --> start service -> upgrade
// Sleep(300);
@@ -745,7 +758,7 @@ UINT __stdcall AddRegSoftwareSASGeneration(__in MSIHANDLE hInstall)
}
// Why RegSetValueExW always return 998?
//
//
result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, subKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);
if (result != ERROR_SUCCESS) {
WcaLog(LOGMSG_STANDARD, "Failed to create or open registry key: %d", result);
@@ -861,7 +874,7 @@ void TryCreateStartServiceByShell(LPWSTR svcName, LPWSTR svcBinary, LPWSTR szSvc
i = 0;
j = 0;
// svcBinary is a string with double quotes, we need to escape it for shell arguments.
// It is original used for `CreateServiceW`.
// It is orignal used for `CreateServiceW`.
// eg. "C:\Program Files\MyApp\MyApp.exe" --service -> \"C:\Program Files\MyApp\MyApp.exe\" --service
while (true) {
if (svcBinary[j] == L'"') {

View File

@@ -2,7 +2,7 @@ LIBRARY "CustomActions"
EXPORTS
CustomActionHello
RemoveRuntimeGeneratedFiles
RemoveInstallFolder
TerminateProcesses
AddFirewallRules
SetPropertyIsServiceRunning

View File

@@ -16,15 +16,8 @@
<!-- If a command line value was stored, restore it after the registry search has been performed -->
<SetProperty Action="RestoreSavedInstallFolderValue" Id="INSTALLFOLDER" Value="[SavedInstallFolderCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedInstallFolderCmdLineValue" />
<!-- Normalize INSTALLFOLDER from the command line or registry before assigning INSTALLFOLDER_INNER. -->
<!-- Case 1: already ends with \$(var.Product)\, keep it unchanged. -->
<SetProperty Action="SetInstallFolderInnerFromProductDir" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot;" />
<!-- Case 2: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
<SetProperty Action="SetInstallFolderInnerFromProductDirNoSlash" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;" />
<!-- Case 3: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
<SetProperty Action="SetInstallFolderInnerAppendProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- Case 4: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
<SetProperty Action="SetInstallFolderInnerAppendSlashProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND NOT INSTALLFOLDER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- If a command line value or registry value was set, update the main properties with the value -->
<SetProperty Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER" />
<!-- INSTALLFOLDER_INNER is defined for compatibility with previous versions of the installer. -->
<!-- Because we need to use INSTALLFOLDER as the command line argument. -->

View File

@@ -12,7 +12,7 @@
</Component>
</DirectoryRef>
<CustomAction Id="RemoveRuntimeGeneratedFiles.SetParam" Return="check" Property="RemoveRuntimeGeneratedFiles" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="RemoveInstallFolder.SetParam" Return="check" Property="RemoveInstallFolder" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="AddFirewallRules.SetParam" Return="check" Property="AddFirewallRules" Value="1[INSTALLFOLDER_INNER]$(var.Product).exe" />
<CustomAction Id="RemoveFirewallRules.SetParam" Return="check" Property="RemoveFirewallRules" Value="0[INSTALLFOLDER_INNER]$(var.Product).exe" />
<CustomAction Id="CreateStartService.SetParam" Return="check" Property="CreateStartService" Value="$(var.Product);&quot;[INSTALLFOLDER_INNER]$(var.Product).exe&quot; --service" />
@@ -77,21 +77,21 @@
<Custom Action="AddRegSoftwareSASGeneration" Before="InstallFinalize" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)"/>
<Custom Action="RemoveRuntimeGeneratedFiles" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot; OR UPGRADINGPRODUCTCODE)"/>
<Custom Action="RemoveRuntimeGeneratedFiles.SetParam" Before="RemoveRuntimeGeneratedFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot; OR UPGRADINGPRODUCTCODE)"/>
<Custom Action="TryStopDeleteService" Before="RemoveRuntimeGeneratedFiles.SetParam" />
<Custom Action="RemoveInstallFolder" Before="RemoveFiles"/>
<Custom Action="RemoveInstallFolder.SetParam" Before="RemoveInstallFolder"/>
<Custom Action="TryStopDeleteService" Before="RemoveInstallFolder.SetParam" />
<Custom Action="TryStopDeleteService.SetParam" Before="TryStopDeleteService" />
<Custom Action="RemoveFirewallRules" Before="RemoveFiles"/>
<Custom Action="RemoveFirewallRules.SetParam" Before="RemoveFirewallRules"/>
<Custom Action="UninstallPrinter" Before="RemoveRuntimeGeneratedFiles" Condition="VersionNT &gt;= 603" />
<Custom Action="UninstallPrinter" Before="RemoveInstallFolder" Condition="VersionNT &gt;= 603" />
<Custom Action="TerminateProcesses" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateProcesses" Before="RemoveInstallFolder"/>
<Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/>
<Custom Action="TerminateBrokers" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateBrokers" Before="RemoveInstallFolder"/>
<Custom Action="TerminateBrokers.SetParam" Before="TerminateBrokers"/>
<Custom Action="RemoveAmyuniIdd" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="RemoveAmyuniIdd" Before="RemoveInstallFolder"/>
<Custom Action="RemoveAmyuniIdd.SetParam" Before="RemoveAmyuniIdd"/>
</InstallExecuteSequence>

View File

@@ -5,7 +5,7 @@
<Binary Id="Custom_Actions_Dll" SourceFile="$(var.CustomActions.TargetDir)$(var.CustomActions.TargetName).dll" />
<CustomAction Id="CustomActionHello" DllEntry="CustomActionHello" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveRuntimeGeneratedFiles" DllEntry="RemoveRuntimeGeneratedFiles" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveInstallFolder" DllEntry="RemoveInstallFolder" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TerminateProcesses" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TerminateBrokers" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="AddFirewallRules" DllEntry="AddFirewallRules" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>

View File

@@ -23,13 +23,12 @@ Patch dialog sequence:
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<?include ../Includes.wxi?>
<?foreach WIXUIARCH in X86;X64;A64 ?>
<Fragment>
<UI Id="UI_MyInstallDialog_$(WIXUIARCH)">
<Publish Dialog="LicenseAgreementDlg" Control="Print" Event="DoAction" Value="WixUIPrintEula_$(WIXUIARCH)" />
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="5" Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="2" Condition="NOT WIXUI_DONTVALIDATEPATH" />
</UI>
<UIRef Id="UI_MyInstallDialog" />
@@ -65,16 +64,9 @@ Patch dialog sequence:
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="MyInstallDirDlg" Condition="LicenseAccepted = &quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg" />
<!-- Normalize INSTALLFOLDER_INNER before SetTargetPath and WixUIValidatePath run. -->
<!-- UI case 1: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\" Order="1" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;" />
<!-- UI case 2: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]$(var.Product)\" Order="2" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- UI case 3: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\$(var.Product)\" Order="3" Condition="INSTALLFOLDER_INNER AND NOT INSTALLFOLDER_INNER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="4" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="6" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="7" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MyInstallDirDlg" Order="1" Condition="NOT Installed" />

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
echo $MACOS_CODESIGN_IDENTITY
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid --locked
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid
cd flutter; flutter pub get; cd -
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
./build.py --flutter

View File

@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.7
Version: 1.4.6
Release: 0
Summary: RPM package
License: GPL-3.0

View File

@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.7
Version: 1.4.6
Release: 0
Summary: RPM package
License: GPL-3.0

View File

@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.7
Version: 1.4.6
Release: 0
Summary: RPM package
License: GPL-3.0

View File

@@ -25,13 +25,7 @@ impl Session {
pub fn new(id: &str, sender: mpsc::UnboundedSender<Data>) -> Self {
let mut password = "".to_owned();
if PeerConfig::load(id).password.is_empty() {
match rpassword::prompt_password("Enter password: ") {
Ok(p) => password = p,
Err(e) => {
log::error!("Failed to read password: {:?}", e);
password = "".to_owned();
}
}
password = rpassword::prompt_password("Enter password: ").unwrap();
}
let session = Self {
id: id.to_owned(),

View File

@@ -119,13 +119,10 @@ pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access";
pub const LOGIN_MSG_OFFLINE: &str = "Offline";
pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported";
#[cfg(target_os = "linux")]
pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "ubuntu-21-04-required";
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-linux-version";
#[cfg(target_os = "linux")]
pub const SCRAP_XDP_PORTAL_UNAVAILABLE: &str =
"xdp-portal-unavailable";
"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";
@@ -1745,9 +1742,6 @@ pub struct LoginConfigHandler {
pub direct: Option<bool>,
pub received: bool,
switch_uuid: Option<String>,
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
switch_back_allowed: bool,
pub save_ab_password_to_recent: bool, // true: connected with ab password
pub other_server: Option<(String, String, String)>,
pub custom_fps: Arc<Mutex<Option<usize>>>,
@@ -1864,11 +1858,6 @@ impl LoginConfigHandler {
self.direct = None;
self.received = false;
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
self.switch_back_allowed = false;
}
self.switch_uuid = switch_uuid;
self.adapter_luid = adapter_luid;
self.selected_windows_session_id = None;
@@ -1882,23 +1871,6 @@ impl LoginConfigHandler {
self.is_terminal_admin = is_terminal_admin;
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn allow_switch_back_once(&mut self) {
self.switch_back_allowed = true;
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn consume_switch_back_permission(&mut self) -> bool {
if self.switch_back_allowed {
self.switch_back_allowed = false;
true
} else {
false
}
}
/// Check if the client should auto login.
/// Return password if the client should auto login, otherwise return empty string.
pub fn should_auto_login(&self) -> String {
@@ -3402,36 +3374,6 @@ pub fn handle_login_error(
}
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
async fn consume_local_switch_sides_uuid(id: &str, uuid: &Uuid) -> bool {
let Ok(mut conn) = crate::ipc::connect(1000, "").await else {
return false;
};
let uuid = uuid.to_string();
if conn
.send(&crate::ipc::Data::SwitchSidesUuid(
uuid.clone(),
id.to_owned(),
None,
))
.await
.is_err()
{
return false;
}
match conn.next_timeout(1000).await {
Ok(Some(crate::ipc::Data::SwitchSidesUuid(
returned_uuid,
returned_id,
Some(true),
))) => {
returned_uuid == uuid && returned_id == id
}
_ => false,
}
}
/// Handle hash message sent by peer.
/// Hash will be used for login.
///
@@ -3452,22 +3394,12 @@ pub async fn handle_hash(
// Take care of password application order
// switch_uuid
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let uuid = lc.write().unwrap().switch_uuid.take();
if let Some(uuid) = uuid {
if let Ok(uuid) = uuid::Uuid::from_str(&uuid) {
let id = lc.read().unwrap().id.clone();
if !consume_local_switch_sides_uuid(&id, &uuid).await {
log::warn!("Ignored untrusted switch_uuid");
} else {
lc.write().unwrap().allow_switch_back_once();
send_switch_login_request(lc.clone(), peer, uuid).await;
lc.write().unwrap().password_source = Default::default();
return;
}
}
let uuid = lc.write().unwrap().switch_uuid.take();
if let Some(uuid) = uuid {
if let Ok(uuid) = uuid::Uuid::from_str(&uuid) {
send_switch_login_request(lc.clone(), peer, uuid).await;
lc.write().unwrap().password_source = Default::default();
return;
}
}
// last password
@@ -3935,7 +3867,6 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b
&& !text.to_lowercase().contains("resolve")
&& !text.to_lowercase().contains("mismatch")
&& !text.to_lowercase().contains("manually")
&& !text.to_lowercase().contains("restricted")
&& !text.to_lowercase().contains("not allowed")))
}

View File

@@ -586,6 +586,7 @@ impl<T: InvokeUiSession> Remote<T> {
file_num,
include_hidden,
is_remote,
Vec::new(),
od,
));
allow_err!(
@@ -658,6 +659,7 @@ impl<T: InvokeUiSession> Remote<T> {
file_num,
include_hidden,
is_remote,
Vec::new(),
od,
);
job.is_last_job = true;
@@ -843,7 +845,19 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
Data::CancelJob(id) => {
self.cancel_transfer_job(id, peer).await;
let mut msg_out = Message::new();
let mut file_action = FileAction::new();
file_action.set_cancel(FileTransferCancel {
id: id,
..Default::default()
});
msg_out.set_file_action(file_action);
allow_err!(peer.send(&msg_out).await);
if let Some(job) = fs::remove_job(id, &mut self.write_jobs) {
job.remove_download_file();
}
let _ = fs::remove_job(id, &mut self.read_jobs);
self.remove_jobs.remove(&id);
}
Data::RemoveDir((id, path)) => {
let mut msg_out = Message::new();
@@ -1039,22 +1053,6 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
async fn cancel_transfer_job(&mut self, id: i32, peer: &mut Stream) {
let mut msg_out = Message::new();
let mut file_action = FileAction::new();
file_action.set_cancel(FileTransferCancel {
id,
..Default::default()
});
msg_out.set_file_action(file_action);
allow_err!(peer.send(&msg_out).await);
if let Some(job) = fs::remove_job(id, &mut self.write_jobs) {
job.remove_download_file();
}
let _ = fs::remove_job(id, &mut self.read_jobs);
self.remove_jobs.remove(&id);
}
pub async fn sync_jobs_status_to_local(&mut self) -> bool {
if !self.is_connected {
return false;
@@ -1448,23 +1446,6 @@ impl<T: InvokeUiSession> Remote<T> {
if !self.handler.lc.read().unwrap().disable_clipboard.v {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
#[cfg(target_os = "ios")]
{
if let Some(cb) = _mcb
.clipboards
.iter()
.find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text))
{
let content = if cb.compress {
hbb_common::compress::decompress(&cb.content)
} else {
cb.content.to_vec()
};
if let Ok(content) = String::from_utf8(content) {
self.handler.clipboard(content);
}
}
}
#[cfg(target_os = "android")]
crate::clipboard::handle_msg_multi_clipboards(_mcb);
}
@@ -1489,43 +1470,14 @@ impl<T: InvokeUiSession> Remote<T> {
fs::transform_windows_path(&mut entries);
}
}
// We cannot call cancel_transfer_job/handle_job_status while holding
// a mutable borrow from fs::get_job(&mut self.write_jobs), so defer
// the error handling until after the borrow scope ends.
let mut set_files_err = None;
self.handler
.update_folder_files(fd.id, &entries, fd.path, false, false);
if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) {
log::info!("job set_files: {:?}", entries);
if let Err(err) = job.set_files(entries) {
set_files_err = Some(err.to_string());
} else {
job.set_finished_size_on_resume();
self.handler.update_folder_files(
fd.id,
job.files(),
fd.path,
false,
false,
);
}
job.set_files(entries);
job.set_finished_size_on_resume();
} else if let Some(job) = self.remove_jobs.get_mut(&fd.id) {
// Intentionally keep raw entries here:
// - remote remove flow executes deletions on peer side;
// - local remove flow is populated from local get_recursive_files().
job.files = entries;
self.handler
.update_folder_files(fd.id, &job.files, fd.path, false, false);
} else {
self.handler
.update_folder_files(fd.id, &entries, fd.path, false, false);
}
if let Some(err) = set_files_err {
log::warn!(
"Rejected unsafe file list from remote peer for job {}: {}",
fd.id,
err
);
self.cancel_transfer_job(fd.id, peer).await;
self.handle_job_status(fd.id, -1, Some(err));
}
}
Some(file_response::Union::Digest(digest)) => {
@@ -1797,9 +1749,6 @@ impl<T: InvokeUiSession> Remote<T> {
Ok(Permission::BlockInput) => {
self.handler.set_permission("block_input", p.enabled);
}
Ok(Permission::PrivacyMode) => {
self.handler.set_permission("privacy_mode", p.enabled);
}
_ => {}
}
}
@@ -1923,23 +1872,9 @@ impl<T: InvokeUiSession> Remote<T> {
);
}
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Some(misc::Union::SwitchBack(_)) => {
let allow_switch_back = self
.handler
.lc
.write()
.unwrap()
.consume_switch_back_permission();
if allow_switch_back {
self.handler.switch_back(&self.handler.get_id());
} else {
log::warn!(
"Ignored unsolicited SwitchBack from {}",
self.handler.get_id()
);
}
#[cfg(feature = "flutter")]
self.handler.switch_back(&self.handler.get_id());
}
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]

View File

@@ -1,7 +1,5 @@
#[cfg(not(target_os = "android"))]
use arboard::{ClipboardData, ClipboardFormat};
#[cfg(target_os = "linux")]
use arboard::{LinuxClipboardKind, SetExtLinux};
use hbb_common::{bail, log, message_proto::*, ResultType};
use std::{
sync::{Arc, Mutex},
@@ -56,27 +54,6 @@ pub fn check_clipboard(
side: ClipboardSide,
force: bool,
) -> Option<Message> {
let (msg, clipboards) = read_clipboard_message(ctx, side, force)?;
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
Some(msg)
}
#[cfg(target_os = "linux")]
pub fn peek_clipboard(
ctx: &mut Option<ClipboardContext>,
side: ClipboardSide,
force: bool,
) -> Option<Message> {
let (msg, _) = read_clipboard_message(ctx, side, force)?;
Some(msg)
}
#[cfg(not(target_os = "android"))]
fn read_clipboard_message(
ctx: &mut Option<ClipboardContext>,
side: ClipboardSide,
force: bool,
) -> Option<(Message, MultiClipboards)> {
if ctx.is_none() {
*ctx = ClipboardContext::new().ok();
}
@@ -87,7 +64,8 @@ fn read_clipboard_message(
let mut msg = Message::new();
let clipboards = proto::create_multi_clipboards(content);
msg.set_multi_clipboards(clipboards.clone());
return Some((msg, clipboards));
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
return Some(msg);
}
}
Err(e) => {
@@ -241,7 +219,10 @@ fn do_update_clipboard_(mut to_update_data: Vec<ClipboardData>, side: ClipboardS
}
}
if let Some(ctx) = ctx.as_mut() {
to_update_data = append_owner_marker(to_update_data, side);
to_update_data.push(ClipboardData::Special((
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
side.get_owner_data(),
)));
if let Err(e) = ctx.set(&to_update_data) {
log::debug!("Failed to set clipboard: {}", e);
} else {
@@ -250,29 +231,6 @@ fn do_update_clipboard_(mut to_update_data: Vec<ClipboardData>, side: ClipboardS
}
}
#[cfg(not(target_os = "android"))]
fn append_owner_marker(mut data: Vec<ClipboardData>, side: ClipboardSide) -> Vec<ClipboardData> {
data.push(ClipboardData::Special((
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
side.get_owner_data(),
)));
data
}
#[cfg(target_os = "linux")]
pub fn set_text_clipboard_with_owner_sync(text: &str, side: ClipboardSide) -> ResultType<()> {
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
if ctx.is_none() {
*ctx = Some(ClipboardContext::new()?);
}
let clipboard_ctx = match ctx.as_mut() {
Some(ctx) => ctx,
None => bail!("Failed to create clipboard context"),
};
let data = append_owner_marker(vec![ClipboardData::Text(text.to_owned())], side);
clipboard_ctx.set_with_owner_marker_for_linux(&data)
}
#[cfg(not(target_os = "android"))]
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
std::thread::spawn(move || {
@@ -424,24 +382,6 @@ impl ClipboardContext {
Ok(())
}
#[cfg(target_os = "linux")]
fn set_with_owner_marker_for_linux(&mut self, data: &[ClipboardData]) -> ResultType<()> {
let _lock = ARBOARD_MTX.lock().unwrap();
self.inner
.set()
.clipboard(LinuxClipboardKind::Clipboard)
.formats(data)?;
if let Err(e) = self
.inner
.set()
.clipboard(LinuxClipboardKind::Primary)
.formats(data)
{
log::warn!("Failed to set PRIMARY clipboard with owner marker: {}", e);
}
Ok(())
}
#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))]
fn get_file_urls_set_by_rustdesk(
data: Vec<ClipboardData>,

View File

@@ -39,7 +39,7 @@ use hbb_common::{
use crate::{
hbbs_http::{create_http_client_async, get_url_for_tls},
ui_interface::{get_api_server as ui_get_api_server, get_option, is_installed, set_option},
ui_interface::{get_option, is_installed, set_option},
};
#[derive(Debug, Eq, PartialEq)]
@@ -1086,7 +1086,6 @@ fn get_api_server_(api: String, custom: String) -> String {
#[inline]
pub fn is_public(url: &str) -> bool {
let url = url.to_ascii_lowercase();
url.contains("rustdesk.com/") || url.ends_with("rustdesk.com")
}
@@ -1124,286 +1123,22 @@ pub fn get_audit_server(api: String, custom: String, typ: String) -> String {
format!("{}/api/audit/{}", url, typ)
}
/// Check if we should use raw TCP proxy for API calls.
/// Returns true if USE_RAW_TCP_FOR_API builtin option is "Y", WebSocket is off,
/// and the target URL belongs to the configured non-public API host.
#[inline]
fn should_use_raw_tcp_for_api(url: &str) -> bool {
get_builtin_option(keys::OPTION_USE_RAW_TCP_FOR_API) == "Y"
&& !use_ws()
&& is_tcp_proxy_api_target(url)
}
/// Check if we can attempt raw TCP proxy fallback for this target URL.
#[inline]
fn can_fallback_to_raw_tcp(url: &str) -> bool {
!use_ws() && is_tcp_proxy_api_target(url)
}
#[inline]
fn should_use_tcp_proxy_for_api_url(url: &str, api_url: &str) -> bool {
if api_url.is_empty() || is_public(api_url) {
return false;
}
let target_host = url::Url::parse(url)
.ok()
.and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
let api_host = url::Url::parse(api_url)
.ok()
.and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase()));
matches!((target_host, api_host), (Some(target), Some(api)) if target == api)
}
#[inline]
fn is_tcp_proxy_api_target(url: &str) -> bool {
should_use_tcp_proxy_for_api_url(url, &ui_get_api_server())
}
fn tcp_proxy_log_target(url: &str) -> String {
url::Url::parse(url)
.ok()
.map(|parsed| {
let mut redacted = format!("{}://", parsed.scheme());
let Some(host) = parsed.host() else {
return "<invalid-url>".to_owned();
};
redacted.push_str(&host.to_string());
if let Some(port) = parsed.port() {
redacted.push(':');
redacted.push_str(&port.to_string());
}
redacted.push_str(parsed.path());
redacted
})
.unwrap_or_else(|| "<invalid-url>".to_owned())
}
#[inline]
fn get_tcp_proxy_addr() -> String {
check_port(Config::get_rendezvous_server(), RENDEZVOUS_PORT)
}
/// Send an HTTP request via the rendezvous server's TCP proxy using protobuf.
/// Connects with `connect_tcp` + `secure_tcp`, sends `HttpProxyRequest`,
/// receives `HttpProxyResponse`.
///
/// The entire operation (connect + handshake + send + receive) is wrapped in
/// an overall timeout of `CONNECT_TIMEOUT + READ_TIMEOUT` so that a stall at
/// any stage cannot block the caller indefinitely.
async fn tcp_proxy_request(
method: &str,
url: &str,
body: &[u8],
headers: Vec<HeaderEntry>,
) -> ResultType<HttpProxyResponse> {
let tcp_addr = get_tcp_proxy_addr();
if tcp_addr.is_empty() {
bail!("No rendezvous server configured for TCP proxy");
}
let parsed = url::Url::parse(url)?;
let path = if let Some(query) = parsed.query() {
format!("{}?{}", parsed.path(), query)
} else {
parsed.path().to_string()
};
log::debug!(
"Sending {} {} via TCP proxy to {}",
method,
parsed.path(),
tcp_addr
);
let overall_timeout = CONNECT_TIMEOUT + READ_TIMEOUT;
timeout(overall_timeout, async {
let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?;
let key = crate::get_key(true).await;
secure_tcp_silent(&mut conn, &key).await?;
let mut req = HttpProxyRequest::new();
req.method = method.to_uppercase();
req.path = path;
req.headers = headers.into();
req.body = Bytes::from(body.to_vec());
let mut msg_out = RendezvousMessage::new();
msg_out.set_http_proxy_request(req);
conn.send(&msg_out).await?;
match conn.next().await {
Some(Ok(bytes)) => {
let msg_in = RendezvousMessage::parse_from_bytes(&bytes)?;
match msg_in.union {
Some(rendezvous_message::Union::HttpProxyResponse(resp)) => Ok(resp),
_ => bail!("Unexpected response from TCP proxy"),
}
}
Some(Err(e)) => bail!("TCP proxy read error: {}", e),
None => bail!("TCP proxy connection closed without response"),
}
})
.await?
}
/// Build HeaderEntry list from "Key: Value" style header string (used by post_request).
/// If the caller supplies a Content-Type header it overrides the default `application/json`.
fn parse_simple_header(header: &str) -> Vec<HeaderEntry> {
let mut entries = Vec::new();
let mut has_content_type = false;
if !header.is_empty() {
let tmp: Vec<&str> = header.splitn(2, ": ").collect();
if tmp.len() == 2 {
if tmp[0].eq_ignore_ascii_case("Content-Type") {
has_content_type = true;
}
entries.push(HeaderEntry {
name: tmp[0].into(),
value: tmp[1].into(),
..Default::default()
});
}
}
if !has_content_type {
entries.insert(
0,
HeaderEntry {
name: "Content-Type".into(),
value: "application/json".into(),
..Default::default()
},
);
}
entries
}
/// POST request via TCP proxy.
async fn post_request_via_tcp_proxy(url: &str, body: &str, header: &str) -> ResultType<String> {
let headers = parse_simple_header(header);
let resp = tcp_proxy_request("POST", url, body.as_bytes(), headers).await?;
if !resp.error.is_empty() {
bail!("TCP proxy error: {}", resp.error);
}
Ok(String::from_utf8_lossy(&resp.body).to_string())
}
fn http_proxy_response_to_json(resp: HttpProxyResponse) -> ResultType<String> {
if !resp.error.is_empty() {
bail!("TCP proxy error: {}", resp.error);
}
let mut response_headers = Map::new();
for entry in resp.headers.iter() {
response_headers.insert(entry.name.to_lowercase(), json!(entry.value));
}
let mut result = Map::new();
result.insert("status_code".to_string(), json!(resp.status));
result.insert("headers".to_string(), Value::Object(response_headers));
result.insert(
"body".to_string(),
json!(String::from_utf8_lossy(&resp.body)),
);
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
}
fn parse_json_header_entries(header: &str) -> ResultType<Vec<HeaderEntry>> {
let v: Value = serde_json::from_str(header)?;
if let Value::Object(obj) = v {
Ok(obj
.iter()
.map(|(key, value)| HeaderEntry {
name: key.clone(),
value: value.as_str().unwrap_or_default().into(),
..Default::default()
})
.collect())
} else {
Err(anyhow!("HTTP header information parsing failed!"))
}
}
/// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback.
async fn post_request_http(url: &str, body: &str, header: &str) -> ResultType<(u16, String)> {
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
let proxy_conf = Config::get_socks();
let tls_url = get_url_for_tls(url, &proxy_conf);
let tls_url = get_url_for_tls(&url, &proxy_conf);
let tls_type = get_cached_tls_type(tls_url);
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
let response = post_request_(
url,
&url,
tls_url,
body.to_owned(),
body.clone(),
header,
tls_type,
danger_accept_invalid_cert,
danger_accept_invalid_cert,
)
.await?;
let status = response.status().as_u16();
let text = response.text().await?;
Ok((status, text))
}
/// Try `http_fn` first; on connection failure or 5xx, fall back to `tcp_fn`
/// if the URL is eligible. 4xx responses are returned as-is.
async fn with_tcp_proxy_fallback<HttpFut, TcpFut>(
url: &str,
method: &str,
http_fn: HttpFut,
tcp_fn: TcpFut,
) -> ResultType<String>
where
HttpFut: Future<Output = ResultType<(u16, String)>>,
TcpFut: Future<Output = ResultType<String>>,
{
if should_use_raw_tcp_for_api(url) {
return tcp_fn.await;
}
let http_result = http_fn.await;
let should_fallback = match &http_result {
Err(_) => true,
Ok((status, _)) => *status >= 500,
};
if should_fallback && can_fallback_to_raw_tcp(url) {
log::warn!(
"HTTP {} to {} failed or 5xx (result: {:?}), trying TCP proxy fallback",
method,
tcp_proxy_log_target(url),
http_result
.as_ref()
.map(|(s, _)| *s)
.map_err(|e| e.to_string()),
);
match tcp_fn.await {
Ok(resp) => return Ok(resp),
Err(tcp_err) => {
log::warn!("TCP proxy fallback also failed: {:?}", tcp_err);
}
}
}
http_result.map(|(_status, text)| text)
}
/// POST request with raw TCP proxy support.
/// - If `USE_RAW_TCP_FOR_API` is "Y" and WS is off, goes directly through TCP proxy.
/// - Otherwise tries HTTP first; on connection failure or 5xx status,
/// falls back to TCP proxy if WS is off.
/// - 4xx responses are returned as-is (server is reachable, business logic error).
/// - If fallback also fails, returns the original HTTP result (text or error).
pub async fn post_request(url: String, body: String, header: &str) -> ResultType<String> {
with_tcp_proxy_fallback(
&url,
"POST",
post_request_http(&url, &body, header),
post_request_via_tcp_proxy(&url, &body, header),
)
.await
Ok(response.text().await?)
}
#[async_recursion]
@@ -1511,16 +1246,21 @@ async fn get_http_response_async(
tls_type.unwrap_or(TlsType::Rustls),
danger_accept_invalid_cert.unwrap_or(false),
);
let normalized_method = method.to_ascii_lowercase();
let mut http_client = match normalized_method.as_str() {
let mut http_client = match method {
"get" => http_client.get(url),
"post" => http_client.post(url),
"put" => http_client.put(url),
"delete" => http_client.delete(url),
_ => return Err(anyhow!("The HTTP request method is not supported!")),
};
for entry in parse_json_header_entries(header)? {
http_client = http_client.header(entry.name, entry.value);
let v = serde_json::from_str(header)?;
if let Value::Object(obj) = v {
for (key, value) in obj.iter() {
http_client = http_client.header(key, value.as_str().unwrap_or_default());
}
} else {
return Err(anyhow!("HTTP header information parsing failed!"));
}
if tls_type.is_some() && danger_accept_invalid_cert.is_some() {
@@ -1600,51 +1340,6 @@ async fn get_http_response_async(
}
}
/// Returns (status_code, json_string) so the caller can inspect the status
/// without re-parsing the serialized JSON.
async fn http_request_http(
url: &str,
method: &str,
body: Option<String>,
header: &str,
) -> ResultType<(u16, String)> {
let proxy_conf = Config::get_socks();
let tls_url = get_url_for_tls(url, &proxy_conf);
let tls_type = get_cached_tls_type(tls_url);
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
let response = get_http_response_async(
url,
tls_url,
method,
body,
header,
tls_type,
danger_accept_invalid_cert,
danger_accept_invalid_cert,
)
.await?;
// Serialize response headers
let mut response_headers = Map::new();
for (key, value) in response.headers() {
response_headers.insert(key.to_string(), json!(value.to_str().unwrap_or("")));
}
let status_code = response.status().as_u16();
let response_body = response.text().await?;
// Construct the JSON object
let mut result = Map::new();
result.insert("status_code".to_string(), json!(status_code));
result.insert("headers".to_string(), Value::Object(response_headers));
result.insert("body".to_string(), json!(response_body));
// Convert map to JSON string
let json_str = serde_json::to_string(&result)
.map_err(|e| anyhow!("Failed to serialize response: {}", e))?;
Ok((status_code, json_str))
}
/// HTTP request with raw TCP proxy support.
#[tokio::main(flavor = "current_thread")]
pub async fn http_request_sync(
url: String,
@@ -1652,28 +1347,44 @@ pub async fn http_request_sync(
body: Option<String>,
header: String,
) -> ResultType<String> {
with_tcp_proxy_fallback(
let proxy_conf = Config::get_socks();
let tls_url = get_url_for_tls(&url, &proxy_conf);
let tls_type = get_cached_tls_type(tls_url);
let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
let response = get_http_response_async(
&url,
tls_url,
&method,
http_request_http(&url, &method, body.clone(), &header),
http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header),
body.clone(),
&header,
tls_type,
danger_accept_invalid_cert,
danger_accept_invalid_cert,
)
.await
}
.await?;
// Serialize response headers
let mut response_headers = serde_json::map::Map::new();
for (key, value) in response.headers() {
response_headers.insert(
key.to_string(),
serde_json::json!(value.to_str().unwrap_or("")),
);
}
/// General HTTP request via TCP proxy. Header is a JSON string (used by http_request_sync).
/// Returns a JSON string with status_code, headers, body (same format as http_request_sync).
async fn http_request_via_tcp_proxy(
url: &str,
method: &str,
body: Option<&str>,
header: &str,
) -> ResultType<String> {
let headers = parse_json_header_entries(header)?;
let body_bytes = body.unwrap_or("").as_bytes();
let status_code = response.status().as_u16();
let response_body = response.text().await?;
let resp = tcp_proxy_request(method, url, body_bytes, headers).await?;
http_proxy_response_to_json(resp)
// Construct the JSON object
let mut result = serde_json::map::Map::new();
result.insert("status_code".to_string(), serde_json::json!(status_code));
result.insert(
"headers".to_string(),
serde_json::Value::Object(response_headers),
);
result.insert("body".to_string(), serde_json::json!(response_body));
// Convert map to JSON string
serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e))
}
#[inline]
@@ -1936,7 +1647,7 @@ pub fn check_process(arg: &str, mut same_uid: bool) -> bool {
false
}
async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) -> ResultType<()> {
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
// Skip additional encryption when using WebSocket connections (wss://)
// as WebSocket Secure (wss://) already provides transport layer encryption.
// This doesn't affect the end-to-end encryption between clients,
@@ -1969,9 +1680,7 @@ async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) ->
});
timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??;
conn.set_key(key);
if log_on_success {
log::info!("Connection secured");
}
log::info!("Connection secured");
}
_ => {}
}
@@ -1982,14 +1691,6 @@ async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) ->
Ok(())
}
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
secure_tcp_impl(conn, key, true).await
}
async fn secure_tcp_silent(conn: &mut Stream, key: &str) -> ResultType<()> {
secure_tcp_impl(conn, key, false).await
}
#[inline]
fn get_pk(pk: &[u8]) -> Option<[u8; 32]> {
if pk.len() == 32 {
@@ -2767,13 +2468,11 @@ mod tests {
assert!(is_public("https://rustdesk.com/"));
assert!(is_public("https://www.rustdesk.com/"));
assert!(is_public("https://api.rustdesk.com/v1"));
assert!(is_public("https://API.RUSTDESK.COM/v1"));
assert!(is_public("https://rustdesk.com/path"));
// Test URLs ending with "rustdesk.com"
assert!(is_public("rustdesk.com"));
assert!(is_public("https://rustdesk.com"));
assert!(is_public("https://RustDesk.com"));
assert!(is_public("http://www.rustdesk.com"));
assert!(is_public("https://api.rustdesk.com"));
@@ -2786,193 +2485,6 @@ mod tests {
assert!(!is_public("rustdesk.comhello.com"));
}
#[test]
fn test_should_use_tcp_proxy_for_api_url() {
assert!(should_use_tcp_proxy_for_api_url(
"https://admin.example.com/api/login",
"https://admin.example.com"
));
assert!(should_use_tcp_proxy_for_api_url(
"https://admin.example.com:21114/api/login",
"https://admin.example.com"
));
assert!(!should_use_tcp_proxy_for_api_url(
"https://api.telegram.org/bot123/sendMessage",
"https://admin.example.com"
));
assert!(!should_use_tcp_proxy_for_api_url(
"https://admin.rustdesk.com/api/login",
"https://admin.rustdesk.com"
));
assert!(!should_use_tcp_proxy_for_api_url(
"https://admin.example.com/api/login",
"not a url"
));
assert!(!should_use_tcp_proxy_for_api_url(
"not a url",
"https://admin.example.com"
));
}
#[test]
fn test_get_tcp_proxy_addr_normalizes_bare_ipv6_host() {
struct RestoreCustomRendezvousServer(String);
impl Drop for RestoreCustomRendezvousServer {
fn drop(&mut self) {
Config::set_option(
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
self.0.clone(),
);
}
}
let _restore = RestoreCustomRendezvousServer(Config::get_option(
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER,
));
Config::set_option(
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(),
"1:2".to_string(),
);
assert_eq!(get_tcp_proxy_addr(), format!("[1:2]:{RENDEZVOUS_PORT}"));
}
#[tokio::test]
async fn test_http_request_via_tcp_proxy_rejects_invalid_header_json() {
let result = http_request_via_tcp_proxy("not a url", "get", None, "{").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_http_request_via_tcp_proxy_rejects_non_object_header_json() {
let err = http_request_via_tcp_proxy("not a url", "get", None, "[]")
.await
.unwrap_err()
.to_string();
assert!(err.contains("HTTP header information parsing failed!"));
}
#[test]
fn test_parse_json_header_entries_preserves_single_content_type() {
let headers = parse_json_header_entries(
r#"{"Content-Type":"text/plain","Authorization":"Bearer token"}"#,
)
.unwrap();
assert_eq!(
headers
.iter()
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.count(),
1
);
assert_eq!(
headers
.iter()
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.map(|entry| entry.value.as_str()),
Some("text/plain")
);
}
#[test]
fn test_parse_json_header_entries_does_not_add_default_content_type() {
let headers = parse_json_header_entries(r#"{"Authorization":"Bearer token"}"#).unwrap();
assert!(!headers
.iter()
.any(|entry| entry.name.eq_ignore_ascii_case("Content-Type")));
}
#[test]
fn test_parse_simple_header_respects_custom_content_type() {
let headers = parse_simple_header("Content-Type: text/plain");
assert_eq!(
headers
.iter()
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.count(),
1
);
assert_eq!(
headers
.iter()
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.map(|entry| entry.value.as_str()),
Some("text/plain")
);
}
#[test]
fn test_parse_simple_header_preserves_non_content_type_header() {
let headers = parse_simple_header("Authorization: Bearer token");
assert!(headers.iter().any(|entry| {
entry.name.eq_ignore_ascii_case("Authorization")
&& entry.value.as_str() == "Bearer token"
}));
assert_eq!(
headers
.iter()
.filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.count(),
1
);
assert_eq!(
headers
.iter()
.find(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))
.map(|entry| entry.value.as_str()),
Some("application/json")
);
}
#[test]
fn test_tcp_proxy_log_target_redacts_query_only() {
assert_eq!(
tcp_proxy_log_target("https://example.com/api/heartbeat?token=secret"),
"https://example.com/api/heartbeat"
);
}
#[test]
fn test_tcp_proxy_log_target_brackets_ipv6_host_with_port() {
assert_eq!(
tcp_proxy_log_target("https://[2001:db8::1]:21114/api/heartbeat?token=secret"),
"https://[2001:db8::1]:21114/api/heartbeat"
);
}
#[test]
fn test_http_proxy_response_to_json() {
let mut resp = HttpProxyResponse {
status: 200,
body: br#"{"ok":true}"#.to_vec().into(),
..Default::default()
};
resp.headers.push(HeaderEntry {
name: "Content-Type".into(),
value: "application/json".into(),
..Default::default()
});
let json = http_proxy_response_to_json(resp).unwrap();
let value: Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["status_code"], 200);
assert_eq!(value["headers"]["content-type"], "application/json");
assert_eq!(value["body"], r#"{"ok":true}"#);
let err = http_proxy_response_to_json(HttpProxyResponse {
error: "dial failed".into(),
..Default::default()
})
.unwrap_err()
.to_string();
assert!(err.contains("TCP proxy error: dial failed"));
}
#[test]
fn test_mouse_event_constants_and_mask_layout() {
use super::input::*;

View File

@@ -146,13 +146,7 @@ pub fn core_main() -> Option<Vec<String>> {
crate::portable_service::client::set_quick_support(_is_quick_support);
}
let mut log_name = "".to_owned();
// Keep portable-service logs under a stable directory name.
let has_portable_service_shmem_arg = args
.iter()
.any(|arg| arg.starts_with("--portable-service-shmem-name="));
if has_portable_service_shmem_arg {
log_name = "portable-service".to_owned();
} else if args.len() > 0 && args[0].starts_with("--") {
if args.len() > 0 && args[0].starts_with("--") {
let name = args[0].replace("--", "");
if !name.is_empty() {
log_name = name;
@@ -199,20 +193,6 @@ pub fn core_main() -> Option<Vec<String>> {
}
std::thread::spawn(move || crate::start_server(false, no_server));
} else {
#[cfg(any(target_os = "linux", target_os = "macos"))]
// Root CLI management commands must talk to the user `--server` main IPC.
// Example: `sudo rustdesk --option custom-rendezvous-server` should query the
// user's IPC instead of root's `/tmp/<app>-0/ipc`; `connect()` still limits this
// routing to empty-postfix main IPC only.
let _user_main_ipc_scope = if crate::platform::is_installed()
&& is_root()
&& is_user_main_ipc_scope_cli_command(&args)
{
Some(crate::ipc::UserMainIpcScope::new())
} else {
None
};
#[cfg(windows)]
{
use crate::platform;
@@ -233,7 +213,7 @@ pub fn core_main() -> Option<Vec<String>> {
}
Ok(false) => "Update failed!".to_string(),
Ok(true) => match platform::update_me(false) {
Ok(_) => "Updated successfully!".to_string(),
Ok(_) => "Update successfully!".to_string(),
Err(err) => {
log::error!("Failed with error: {err}");
"Update failed!".to_string()
@@ -355,8 +335,8 @@ pub fn core_main() -> Option<Vec<String>> {
log::info!("Starting update process...");
let _text = match platform::update_me() {
Ok(_) => {
println!("{}", translate("Updated successfully!".to_string()));
log::info!("Updated successfully!");
println!("{}", translate("Update successfully!".to_string()));
log::info!("Update successfully!");
}
Err(err) => {
eprintln!("Update failed with error: {}", err);
@@ -432,7 +412,7 @@ pub fn core_main() -> Option<Vec<String>> {
}
return None;
} else if args[0] == "--password" {
if is_cli_setting_change_disabled() {
if config::is_disable_settings() {
println!("Settings are disabled!");
return None;
}
@@ -474,7 +454,7 @@ pub fn core_main() -> Option<Vec<String>> {
println!("{}", crate::ipc::get_id());
return None;
} else if args[0] == "--set-id" {
if is_cli_setting_change_disabled() {
if config::is_disable_settings() {
println!("Settings are disabled!");
return None;
}
@@ -521,7 +501,7 @@ pub fn core_main() -> Option<Vec<String>> {
}
return None;
} else if args[0] == "--option" {
if is_cli_setting_change_disabled() {
if config::is_disable_settings() {
println!("Settings are disabled!");
return None;
}
@@ -641,56 +621,6 @@ pub fn core_main() -> Option<Vec<String>> {
println!("Installation and administrative privileges required!");
}
return None;
} else if args[0] == "--deploy" {
if config::Config::no_register_device() {
println!("Cannot deploy an unregistrable device!");
} else if config::is_outgoing_only() {
println!("Cannot deploy Outgoing-only clients.");
} else if crate::platform::is_installed() && is_root() {
let max = args.len() - 1;
let pos = args.iter().position(|x| x == "--token").unwrap_or(max);
if pos >= max {
println!("--token is required!");
return None;
}
let token = args[pos + 1].to_owned();
let get_value = |c: &str| {
let pos = args.iter().position(|x| x == c).unwrap_or(max);
if pos < max {
Some(args[pos + 1].to_owned())
} else {
None
}
};
let new_id = get_value("--id");
match crate::ui_interface::deploy_device(token, new_id) {
crate::ui_interface::DeployResult::Ok => {
println!("Device deployed.");
}
crate::ui_interface::DeployResult::NotEnabled => {
println!("Server does not require deployment.");
std::process::exit(3);
}
crate::ui_interface::DeployResult::InvalidInput => {
println!("Invalid input.");
std::process::exit(5);
}
crate::ui_interface::DeployResult::IdTaken(id) => {
println!(
"Id `{}` is already used by another machine on the server.",
id
);
std::process::exit(6);
}
crate::ui_interface::DeployResult::Error(err) => {
println!("{}", err);
std::process::exit(1);
}
}
} else {
println!("Installation and administrative privileges required!");
}
return None;
} else if args[0] == "--check-hwcodec-config" {
#[cfg(feature = "hwcodec")]
crate::ipc::hwcodec_process();
@@ -910,65 +840,6 @@ fn is_root() -> bool {
crate::platform::is_root()
}
#[cfg(any(target_os = "linux", target_os = "macos", test))]
fn is_user_main_ipc_scope_cli_command(args: &[String]) -> bool {
matches!(
args.first().map(String::as_str),
Some("--password")
| Some("--set-unlock-pin")
| Some("--get-id")
| Some("--set-id")
| Some("--config")
| Some("--option")
| Some("--assign")
| Some("--deploy")
)
}
#[inline]
fn is_cli_setting_change_disabled() -> bool {
let option = config::keys::OPTION_ALLOW_COMMAND_LINE_SETTINGS_WHEN_SETTINGS_DISABLED;
let allow_command_line_settings =
config::option2bool(option, &crate::get_builtin_option(option));
config::is_disable_settings() && !allow_command_line_settings
}
#[cfg(test)]
mod tests {
use super::*;
fn args(values: &[&str]) -> Vec<String> {
values.iter().map(|value| value.to_string()).collect()
}
#[test]
fn user_main_ipc_scope_cli_command_matches_management_commands_only() {
for command in [
"--password",
"--set-unlock-pin",
"--get-id",
"--set-id",
"--config",
"--option",
"--assign",
"--deploy",
] {
assert!(is_user_main_ipc_scope_cli_command(&args(&[command])));
}
for command in [
"--service",
"--server",
"--tray",
"--cm",
"--check-hwcodec-config",
"--connect",
] {
assert!(!is_user_main_ipc_scope_cli_command(&args(&[command])));
}
}
}
/// Check if the executable is a Quick Support version.
/// Note: This function must be kept in sync with `libs/portable/src/main.rs`.
#[cfg(windows)]

View File

@@ -1135,10 +1135,6 @@ impl InvokeUiSession for FlutterHandler {
("message", json!(&opened.message)),
("pid", json!(opened.pid)),
("service_id", json!(&opened.service_id)),
(
"replay_terminal_output",
json!(opened.replay_terminal_output),
),
];
if !opened.persistent_sessions.is_empty() {
event_data.push(("persistent_sessions", json!(opened.persistent_sessions)));

View File

@@ -605,30 +605,21 @@ pub fn session_handle_flutter_raw_key_event(
}
}
// SyncReturn<()> is used to make sure enter() and leave() are executed in the sequence this function is called.
//
// If the cursor jumps between remote page of two connections, leave view and enter view will be called.
// session_enter_or_leave() will be called then.
// As Rust is multi-threaded, enter() can be called before leave().
// The Rust-side grab ownership state filters stale transitions.
// As rust is multi-thread, it is possible that enter() is called before leave().
// This will cause the keyboard input to take no effect.
pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Some(session) = sessions::get_session_by_session_id(&_session_id) {
let keyboard_mode = session.get_keyboard_mode();
// Use the full per-window UUID (not lc.session_id which is per-connection)
// so that two windows viewing the same peer get distinct grab owners.
let window_id = _session_id.as_u128();
if _enter {
set_cur_session_id_(_session_id, &keyboard_mode);
crate::keyboard::client::change_grab_status(
crate::common::GrabState::Run,
&keyboard_mode,
window_id,
);
session.enter(keyboard_mode);
} else {
crate::keyboard::client::change_grab_status(
crate::common::GrabState::Wait,
&keyboard_mode,
window_id,
);
session.leave(keyboard_mode);
}
}
SyncReturn(())
@@ -972,27 +963,6 @@ pub fn main_show_option(_key: String) -> SyncReturn<bool> {
}
pub fn main_set_option(key: String, value: String) {
#[cfg(target_os = "android")]
{
let is_permission_option = key.eq(config::keys::OPTION_ENABLE_CLIPBOARD)
|| key.eq(config::keys::OPTION_ENABLE_FILE_TRANSFER)
|| key.eq(config::keys::OPTION_ENABLE_AUDIO);
let allow_perm_change_in_accept_window = config::option2bool(
config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW,
&crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW),
);
if is_permission_option
&& !allow_perm_change_in_accept_window
&& crate::ui_cm_interface::has_active_clients()
{
log::info!(
"blocked main_set_option by policy, key={}, value={}",
key,
value
);
return;
}
}
#[cfg(target_os = "android")]
if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) {
crate::ui_cm_interface::switch_permission_all(
@@ -1040,29 +1010,7 @@ pub fn main_get_options_sync() -> SyncReturn<String> {
}
pub fn main_set_options(json: String) {
let mut map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new());
#[cfg(target_os = "android")]
{
let allow_perm_change_in_accept_window = config::option2bool(
config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW,
&crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW),
);
if !allow_perm_change_in_accept_window && crate::ui_cm_interface::has_active_clients() {
for key in [
config::keys::OPTION_ENABLE_CLIPBOARD,
config::keys::OPTION_ENABLE_FILE_TRANSFER,
config::keys::OPTION_ENABLE_AUDIO,
] {
if let Some(value) = map.remove(key) {
log::info!(
"blocked main_set_options item by policy, key={}, value={}",
key,
value
);
}
}
}
}
let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new());
if !map.is_empty() {
set_options(map)
}
@@ -1153,22 +1101,6 @@ pub fn main_get_api_server() -> String {
get_api_server()
}
pub fn main_deploy_device(token: String, id: String) -> String {
#[cfg(target_os = "android")]
{
let new_id = match id.trim() {
"" => None,
id => Some(id.to_owned()),
};
ui_interface::deploy_device(token, new_id).message()
}
#[cfg(not(target_os = "android"))]
{
let _ = (token, id);
"Deployment is not supported on this platform.".to_owned()
}
}
pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
SyncReturn(resolve_avatar_url(avatar))
}
@@ -1761,8 +1693,8 @@ pub fn main_get_temporary_password() -> String {
ui_interface::temporary_password()
}
pub fn main_set_permanent_password_with_result(password: String) -> bool {
ui_interface::set_permanent_password_with_result(password)
pub fn main_get_permanent_password() -> String {
ui_interface::permanent_password()
}
pub fn main_get_fingerprint() -> String {
@@ -2132,7 +2064,6 @@ pub fn main_start_service() {
#[cfg(target_os = "android")]
{
config::Config::set_option("stop-service".into(), "".into());
crate::rendezvous_mediator::reset_needs_deploy_notification();
crate::rendezvous_mediator::RendezvousMediator::restart();
}
}
@@ -2141,6 +2072,10 @@ pub fn main_update_temporary_password() {
update_temporary_password();
}
pub fn main_set_permanent_password(password: String) {
set_permanent_password(password);
}
pub fn main_check_super_user_permission() -> bool {
check_super_user_permission()
}
@@ -2230,7 +2165,7 @@ pub fn cm_elevate_portable(conn_id: i32) {
}
pub fn cm_switch_back(conn_id: i32) {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "ios")))]
crate::ui_cm_interface::switch_back(conn_id);
}
@@ -2488,13 +2423,16 @@ pub fn is_disable_installation() -> SyncReturn<bool> {
}
pub fn is_preset_password() -> bool {
// On desktop, service owns the authoritative config; query it via IPC and return only a boolean.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
return crate::ipc::is_permanent_password_preset();
// On mobile, we have no service IPC; verify against local storage.
#[cfg(any(target_os = "android", target_os = "ios"))]
return config::Config::is_using_preset_password();
config::HARD_SETTINGS
.read()
.unwrap()
.get("password")
.map_or(false, |p| {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
return p == &crate::ipc::get_permanent_password();
#[cfg(any(target_os = "android", target_os = "ios"))]
return p == &config::Config::get_permanent_password();
})
}
// Don't call this function for desktop version.
@@ -2830,10 +2768,6 @@ pub fn main_get_common(key: String) -> String {
return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string();
#[cfg(not(target_os = "linux"))]
return false.to_string();
} else if key == "permanent-password-set" {
return ui_interface::is_permanent_password_set().to_string();
} else if key == "local-permanent-password-set" {
return ui_interface::is_local_permanent_password_set().to_string();
} else {
if key.starts_with("download-data-") {
let id = key.replace("download-data-", "");
@@ -2943,7 +2877,7 @@ pub fn main_set_common(_key: String, _value: String) {
} else if _key == "update-me" {
if let Some(new_version_file) = get_download_file_from_url(&_value) {
log::debug!(
"New version file is downloaded, update begin, {:?}",
"New version file is downloaed, update begin, {:?}",
new_version_file.to_str()
);
if let Some(f) = new_version_file.to_str() {
@@ -3072,7 +3006,6 @@ pub mod server_side {
pub unsafe extern "system" fn Java_ffi_FFI_startService(_env: JNIEnv, _class: JClass) {
log::debug!("startService from jvm");
config::Config::set_option("stop-service".into(), "".into());
crate::rendezvous_mediator::reset_needs_deploy_notification();
crate::rendezvous_mediator::RendezvousMediator::restart();
}
@@ -3116,22 +3049,6 @@ pub mod server_side {
return env.new_string(res).unwrap_or_default().into_raw();
}
#[no_mangle]
pub unsafe extern "system" fn Java_ffi_FFI_getBuildinOption(
env: JNIEnv,
_class: JClass,
key: JString,
) -> jstring {
let mut env = env;
let res = if let Ok(key) = env.get_string(&key) {
let key: String = key.into();
super::get_builtin_option(&key)
} else {
"".into()
};
return env.new_string(res).unwrap_or_default().into_raw();
}
#[no_mangle]
pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled(
env: JNIEnv,

Some files were not shown because too many files have changed in this diff Show More