Compare commits

..

1 Commits

Author SHA1 Message Date
rustdesk
0a0625126e fix https://github.com/rustdesk/rustdesk/issues/15326 2026-06-18 12:09:51 +08:00
26 changed files with 242 additions and 851 deletions

View File

@@ -2,8 +2,6 @@
rustflags = ["-Ctarget-feature=+crt-static"]
[target.i686-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
[target.aarch64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.'cfg(target_os="macos")']
rustflags = [
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",

View File

@@ -1,39 +0,0 @@
#!/usr/bin/env bash
# Applies the Flutter 3.44-only source/pubspec changes on the fly, in CI only.
#
# Windows arm64 needs Flutter >= 3.44 (the first stable release shipping an arm64 Dart SDK +
# engine), which renamed DialogTheme/TabBarTheme -> *Data and needs newer extended_text/
# google_fonts. Every other platform is still on Flutter 3.24.5, where the old names/versions
# are required, so these changes are kept OUT of the committed sources and applied here instead.
#
# Used by BOTH the Windows arm64 build (flutter-build.yml) and its dedicated bridge artifact
# (bridge.yml) so they share an identical 3.44 source state -- the generated *.freezed.dart must
# compile against the same Flutter/freezed version the arm64 build resolves.
#
# Remove this script (and commit the changes) once upstream bumps Flutter across the board.
#
# Run from the repository root. sed is used (not a git-apply patch) because the checked-out
# sources are CRLF on the windows-11-arm runner; the substitutions below are anchor-free and
# therefore CRLF-safe.
set -euo pipefail
# ThemeData API renames (Flutter 3.27+):
sed -i 's/dialogTheme: DialogTheme(/dialogTheme: DialogThemeData(/g' flutter/lib/common.dart
sed -i 's/tabBarTheme: const TabBarTheme(/tabBarTheme: const TabBarThemeData(/g' flutter/lib/common.dart
sed -i '/static ThemeData lightTheme = ThemeData(/,/static ThemeData darkTheme = ThemeData(/s/dialogTheme: DialogThemeData(/dialogTheme: DialogThemeData(\
backgroundColor: Colors.white,/' flutter/lib/common.dart
sed -i '/static ThemeData darkTheme = ThemeData(/,/scrollbarTheme: scrollbarThemeDark,/s/dialogTheme: DialogThemeData(/dialogTheme: DialogThemeData(\
backgroundColor: Color(0xFF18191E),/' flutter/lib/common.dart
# Dependency bumps required by the newer Dart/Flutter:
sed -i 's/extended_text: 14.0.0/extended_text: 15.0.2/' flutter/pubspec.yaml
sed -i 's/google_fonts: \^6.2.1/google_fonts: ^8.1.0/' flutter/pubspec.yaml
# Fail loudly if any expected string drifted, so we never silently build unpatched:
grep -qF 'dialogTheme: DialogThemeData(' flutter/lib/common.dart
grep -qF 'tabBarTheme: const TabBarThemeData(' flutter/lib/common.dart
grep -qF 'backgroundColor: Colors.white,' flutter/lib/common.dart
grep -qF 'backgroundColor: Color(0xFF18191E),' flutter/lib/common.dart
grep -qF 'extended_text: 15.0.2' flutter/pubspec.yaml
grep -qF 'google_fonts: ^8.1.0' flutter/pubspec.yaml
git --no-pager diff -- flutter/lib/common.dart flutter/pubspec.yaml

View File

@@ -7,6 +7,7 @@ on:
env:
CARGO_EXPAND_VERSION: "1.0.95"
FLUTTER_VERSION: "3.22.3"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
@@ -17,21 +18,10 @@ jobs:
fail-fast: false
matrix:
job:
# Default bridge for every platform still on Flutter 3.24.5 (generated with 3.22.3).
- {
target: x86_64-unknown-linux-gnu,
os: ubuntu-22.04,
extra-build-args: "",
flutter-version: "3.22.3",
artifact-name: "bridge-artifact",
}
# Dedicated bridge for the Windows arm64 build (Flutter 3.44); runs in parallel.
- {
target: x86_64-unknown-linux-gnu,
os: ubuntu-22.04,
extra-build-args: "",
flutter-version: "3.44.0",
artifact-name: "bridge-artifact-flutter-3.44",
}
steps:
- name: Checkout source code
@@ -74,13 +64,13 @@ jobs:
uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3
with:
path: /tmp/flutter_rust_bridge
key: bridge-${{ matrix.job.flutter-version }}
key: vcpkg-${{ matrix.job.arch }}
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ matrix.job.flutter-version }}
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install flutter rust bridge deps
@@ -88,15 +78,7 @@ jobs:
run: |
cargo install cargo-expand --version ${{ env.CARGO_EXPAND_VERSION }} --locked
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
if [[ "${{ matrix.job.flutter-version }}" == "3.22.3" ]]; then
# Default Flutter 3.22.3: extended_text 14 needs a newer Dart, so downgrade for resolution.
sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' flutter/pubspec.yaml
else
# Flutter 3.44 bridge for Windows arm64: match that build's source/pubspec state so the
# generated *.freezed.dart compiles against the same Flutter/freezed it resolves.
bash .github/patches/apply_flutter_3.44_source_patches.sh
fi
pushd flutter && flutter pub get && popd
pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd
- name: Run flutter rust bridge
run: |
@@ -106,7 +88,7 @@ jobs:
- name: Upload Artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.job.artifact-name }}
name: bridge-artifact
path: |
./src/bridge_generated.rs
./src/bridge_generated.io.rs

View File

@@ -81,7 +81,6 @@ jobs:
# - { target: x86_64-apple-darwin , os: macos-10.15 }
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
# - { target: x86_64-pc-windows-msvc , os: windows-2022 }
# - { target: aarch64-pc-windows-msvc , os: windows-11-arm }
- { target: x86_64-unknown-linux-gnu , os: ubuntu-24.04 }
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
steps:

View File

@@ -27,11 +27,6 @@ env:
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.24.5"
ANDROID_FLUTTER_VERSION: "3.24.5"
# Windows arm64 only: the first stable Flutter to ship a native arm64 Windows Dart SDK +
# engine is 3.44. Every other platform stays on FLUTTER_VERSION (3.24.5) until Windows 7
# support is restored after the upstream-wide Flutter bump. The arm64 job patches the few
# 3.44-only source/pubspec changes on the fly (see "Patch RustDesk sources for Flutter 3.44").
FLUTTER_WINDOWS_ARM_VERSION: "3.44.0"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}"
@@ -58,24 +53,14 @@ jobs:
build-RustDeskTempTopMostWindow:
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
strategy:
fail-fast: false
matrix:
job:
- {
target: windows-2022,
platform: x64,
}
- {
target: windows-11-arm,
platform: ARM64,
}
with:
upload-artifact: ${{ inputs.upload-artifact }}
target: ${{ matrix.job.target }}
target: windows-2022
configuration: Release
platform: ${{ matrix.job.platform }}
platform: x64
target_version: Windows10
strategy:
fail-fast: false
build-for-windows-flutter:
name: ${{ matrix.job.target }}
@@ -91,20 +76,9 @@ jobs:
target: x86_64-pc-windows-msvc,
os: windows-2022,
arch: x86_64,
flutter-arch: x64,
vcpkg-triplet: x64-windows-static,
build-args: "--vram",
}
- {
target: aarch64-pc-windows-msvc,
os: windows-11-arm,
arch: aarch64,
flutter-arch: arm64,
vcpkg-triplet: arm64-windows-static,
# vram is x86/x64-only (NVENC needs CUDA, Intel MediaSDK needs __rdtsc);
# no NV/Intel/AMD hardware exists on Windows-on-ARM, so vram stays disabled here.
build-args: "",
}
# - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
@@ -121,91 +95,36 @@ jobs:
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
# arm64 is on Flutter 3.44, so it needs the bridge generated with the same Flutter
# (its *.freezed.dart must match the freezed the arm64 build resolves). x64 and every
# other platform keep the default 3.22.3-generated bridge.
name: ${{ matrix.job.arch == 'aarch64' && 'bridge-artifact-flutter-3.44' || 'bridge-artifact' }}
name: bridge-artifact
path: ./
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@ebc0426251bc40c7cd31162802432c68818ab8f0 # v2.0.9
uses: KyleMayes/install-llvm-action@1a3da29f56261a1e1f937ec88f0856a9b8321d7e # v1
with:
version: ${{ env.LLVM_VERSION }}
- name: Install flutter
id: flutter
# arm64 builds with FLUTTER_WINDOWS_ARM_VERSION (>=3.44); x64 stays on FLUTTER_VERSION.
# subosito only ships an x64 Windows SDK (Flutter's release manifest lists x64 only),
# so it installs x64 on both arches. The arm64 runner re-bootstraps Dart to arm64 in
# the next step.
uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 # v2.12.0; https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ matrix.job.arch == 'aarch64' && env.FLUTTER_WINDOWS_ARM_VERSION || env.FLUTTER_VERSION }}
architecture: x64
- name: Force arm64 Dart SDK + engine
# The x64 SDK subosito installs bundles an x64 Dart with a matching engine-dart-sdk.stamp,
# so update_dart_sdk.ps1 short-circuits (stamp matches -> return) and keeps x64 Dart;
# `flutter build windows` then targets the Dart VM's arch = x64, even on this arm64 host.
# On this native-arm64 runner (PROCESSOR_ARCHITECTURE=ARM64), deleting the stamp and
# re-running update_dart_sdk.ps1 pulls the arm64 Dart (available since Flutter 3.44.0).
# https://github.com/flutter/flutter/issues/186730#issuecomment-4573214964
if: ${{ matrix.job.arch == 'aarch64' }}
run: |
$flutterRoot = "${{ steps.flutter.outputs['CACHE-PATH'] }}"
Write-Host "PROCESSOR_ARCHITECTURE=$env:PROCESSOR_ARCHITECTURE"
Write-Host "Flutter root: $flutterRoot"
Remove-Item -Force "$flutterRoot\bin\cache\engine-dart-sdk.stamp" -ErrorAction SilentlyContinue
& "$flutterRoot\bin\internal\update_dart_sdk.ps1"
# Confirm the Dart we ended up with is arm64 ("on windows_arm64"); fail loudly if not.
$dartVer = & "$flutterRoot\bin\dart.bat" --version 2>&1 | Out-String
Write-Host $dartVer
if ($dartVer -notmatch "windows_arm64") {
Write-Error "Expected an arm64 Dart SDK but got: $dartVer"
exit 1
}
& "$flutterRoot\bin\flutter.bat" precache --windows
# Fail fast if precache pulled the wrong-arch Windows engine: an arm64 Dart should
# fetch windows-arm64 engine artifacts. Bailing here saves the ~25min Rust build.
$engineDir = "$flutterRoot\bin\cache\artifacts\engine"
Write-Host "Engine artifacts present:"
Get-ChildItem $engineDir -Directory | Select-Object -ExpandProperty Name | Write-Host
if (-not (Test-Path "$engineDir\windows-arm64-release")) {
Write-Error "Expected windows-arm64-release engine artifacts but they are missing (wrong-arch SDK)."
exit 1
}
flutter-version: ${{ env.FLUTTER_VERSION }}
# https://github.com/flutter/flutter/issues/155685
# x64 only: arm64 uses the stock native arm64 Windows engine, and the rustdesk/engine
# windows-x64-release.zip is built for the 3.24-era x64 engine (matches FLUTTER_VERSION).
- name: Replace engine with rustdesk custom flutter engine
if: ${{ matrix.job.arch == 'x86_64' }}
run: |
flutter doctor -v
flutter precache --windows
Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip
Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release
mv -Force windows-x64-release/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
mv -Force windows-x64-release/*  C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
- name: Patch flutter
# x64 stays on Flutter 3.24.5, which needs the dropdown filter patch.
# arm64 is on Flutter 3.44 (patched separately below) and does not use this patch.
if: ${{ matrix.job.arch == 'x86_64' }}
shell: bash
run: |
cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Patch RustDesk sources for Flutter 3.44 (arm64)
# arm64 is the only target on Flutter 3.44; apply its source/pubspec deltas on the fly
# (shared with the 3.44 bridge job) so the committed sources stay on Flutter 3.24.5.
# `flutter build` then runs `pub get`, regenerating pubspec.lock for the bumped deps.
if: ${{ matrix.job.arch == 'aarch64' }}
shell: bash
run: bash .github/patches/apply_flutter_3.44_source_patches.sh
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
@@ -244,19 +163,11 @@ jobs:
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash
- name: Set SODIUM_LIB_DIR (arm64)
# libsodium-sys ships no arm64 Windows prebuilt lib; point it at the vcpkg-built one
# (only for arm64 — leaving it unset lets x64 use the crate's bundled lib).
if: ${{ matrix.job.arch == 'aarch64' }}
shell: bash
run: echo "SODIUM_LIB_DIR=$VCPKG_ROOT/installed/${{ matrix.job.vcpkg-triplet }}/lib" >> "$GITHUB_ENV"
- name: Build rustdesk
run: |
# Windows: build RustDesk
# --hwcodec is shared by all Windows targets; per-target extras (e.g. --vram) come from the matrix
python3 .\build.py --portable --flutter --skip-portable-pack --hwcodec ${{ matrix.job.build-args }}
mv ./flutter/build/windows/${{ matrix.job.flutter-arch }}/runner/Release ./rustdesk
python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack
mv ./flutter/build/windows/x64/runner/Release ./rustdesk
# Download usbmmidd_v2.zip and extract it to ./rustdesk
Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip
@@ -312,7 +223,7 @@ jobs:
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
if: ${{ inputs.upload-artifact }}
with:
name: ${{ matrix.job.arch == 'aarch64' && 'topmostwindow-artifacts-ARM64' || 'topmostwindow-artifacts-x64' }}
name: topmostwindow-artifacts
path: "./rustdesk"
- name: Upload unsigned
@@ -345,18 +256,13 @@ jobs:
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
- name: Build msi
# Builds the MSI for the matrix arch. res/msi (WiX v4 + native CustomActions) carries
# both x64 and ARM64 platform configs; WcaUtil/DUtil ship arm64 libs. msbuild platform
# is x64 / ARM64; the produced Package.msi is globbed since its bin/<platform>/ dir varies.
if: env.UPLOAD_ARTIFACT == 'true'
run: |
pushd ./res/msi
python preprocess.py --arp -d ../../rustdesk
nuget restore msi.sln
$msiPlatform = if ('${{ matrix.job.arch }}' -eq 'aarch64') { 'ARM64' } else { 'x64' }
msbuild msi.sln -p:Configuration=Release -p:Platform=$msiPlatform /p:TargetVersion=Windows10
$msi = Get-ChildItem ./Package/bin/*/Release/en-us/Package.msi | Select-Object -First 1
mv $msi.FullName ../../SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.msi
msbuild msi.sln -p:Configuration=Release -p:Platform=x64 /p:TargetVersion=Windows10
mv ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.msi
sha256sum ../../SignOutput/rustdesk-*.msi
- name: Sign rustdesk self-extracted file

View File

@@ -45,15 +45,16 @@ jobs:
run: |
git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow
# Build. commit 53b548a5398624f7149a382000397993542ad796 is tag v0.3
- name: Build the project
run: |
cd RustDeskTempTopMostWindow && git checkout ecd8d6a139eee76845ea66423fb739af450fda90
cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796
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
if: ${{ inputs.upload-artifact }}
with:
name: topmostwindow-artifacts-${{ inputs.platform }}
name: topmostwindow-artifacts
path: |
./${{ env.build_output_dir }}/WindowInjection.dll

14
Cargo.lock generated
View File

@@ -2329,7 +2329,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.7.4",
"libloading 0.8.4",
]
[[package]]
@@ -2694,7 +2694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -4494,7 +4494,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
dependencies = [
"cfg-if 1.0.0",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -4695,7 +4695,7 @@ dependencies = [
[[package]]
name = "magnum-opus"
version = "0.4.0"
source = "git+https://github.com/rustdesk-org/magnum-opus#588c6e1f9ed50c3a01fa64f3bd3e7cdb0378a114"
source = "git+https://github.com/rustdesk-org/magnum-opus#5cd2bf989c148662fa3a2d9d539a71d71fd1d256"
dependencies = [
"bindgen 0.59.2",
"pkg-config",
@@ -6673,7 +6673,7 @@ dependencies = [
"once_cell",
"socket2 0.5.10",
"tracing",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -7457,7 +7457,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -7514,7 +7514,7 @@ dependencies = [
"security-framework 3.5.1",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]

View File

@@ -17,8 +17,7 @@ osx = platform.platform().startswith(
hbb_name = 'rustdesk' + ('.exe' if windows else '')
exe_path = 'target/release/' + hbb_name
if windows:
win_arch = 'arm64' if platform.machine().lower() in ('arm64', 'aarch64') else 'x64'
flutter_build_dir = f'build/windows/{win_arch}/runner/Release/'
flutter_build_dir = 'build/windows/x64/runner/Release/'
elif osx:
flutter_build_dir = 'build/macos/Build/Products/Release/'
else:
@@ -411,12 +410,7 @@ def build_flutter_dmg(version, features):
system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
os.chdir('flutter')
# cargo builds a single-arch dylib for the host; restrict Xcode to the same arch
# so the universal-by-default ARCHS_STANDARD doesn't try to link a missing slice.
# FLUTTER_XCODE_* env vars are forwarded to xcodebuild as build settings.
mac_arch = 'arm64' if platform.machine().lower() in ('arm64', 'aarch64') else 'x86_64'
system2(
f'FLUTTER_XCODE_ARCHS={mac_arch} FLUTTER_XCODE_ONLY_ACTIVE_ARCH=YES flutter build macos --release')
system2('flutter build macos --release')
system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/')
'''
system2(
@@ -512,7 +506,6 @@ def main():
'target\\release\\rustdesk.exe')
else:
print('Not signed')
os.makedirs(res_dir, exist_ok=True)
system2(
f'cp -rf target/release/RustDesk.exe {res_dir}')
os.chdir('libs/portable')

View File

@@ -598,22 +598,6 @@ class MyTheme {
}
}
/// Applies [fallbacks] as fontFamilyFallback to every text style in both
/// themes. Called once at startup on ARM64 Linux after a CJK font has been
/// loaded via FontLoader (see flutter/flutter#139293).
static void applyFontFallback(List<String> fallbacks) {
lightTheme = lightTheme.copyWith(
textTheme: lightTheme.textTheme.apply(fontFamilyFallback: fallbacks),
primaryTextTheme:
lightTheme.primaryTextTheme.apply(fontFamilyFallback: fallbacks),
);
darkTheme = darkTheme.copyWith(
textTheme: darkTheme.textTheme.apply(fontFamilyFallback: fallbacks),
primaryTextTheme:
darkTheme.primaryTextTheme.apply(fontFamilyFallback: fallbacks),
);
}
static ThemeMode currentThemeMode() {
final preference = getThemeModePreference();
if (preference == ThemeMode.system) {

View File

@@ -72,24 +72,10 @@ Widget waylandKeyboardScopeChip(BuildContext context, String text) {
);
}
bool _isWindowsMode1PrivacyImpl(String privacyModeImpl) {
return privacyModeImpl == kPrivacyModeImplMag ||
privacyModeImpl == kPrivacyModeImplExcludeFromCapture;
}
// macOS privacy mode blacks out all online displays. Windows Mode 1 also
// covers every local monitor with privacy overlay windows, so remote display
// switching does not weaken local privacy protection.
//
// Keep this separate from the capture backend capability. The legacy Windows
// magnifier capturer is not reliable for multi-monitor capture; WebRTC's
// screen_capturer_win_magnifier also disables it when SM_CMONITORS != 1:
// https://webrtc.googlesource.com/src/+/1845922d5a1bf9c27deeffb4a8c8daea124434c1/modules/desktop_capture/win/screen_capturer_win_magnifier.cc
bool allowDisplaySwitchInPrivacyMode(PeerInfo pi, String privacyModeImpl) {
return pi.platform == kPeerPlatformMacOS ||
(pi.platform == kPeerPlatformWindows &&
_isWindowsMode1PrivacyImpl(privacyModeImpl) &&
versionCmp(pi.version, '1.4.8') >= 0);
// 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 {
@@ -978,8 +964,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
final privacyModeState = PrivacyModeState.find(id);
if (pi.isSupportMultiDisplay &&
(privacyModeState.isEmpty ||
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value)) &&
(privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) &&
pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
final value =
@@ -1063,20 +1048,7 @@ List<TToggleMenu> toolbarPrivacyMode(
return []; // No permission and not active, hide options.
}
bool checkDisplayAllowedForPrivacyMode(String targetImplKey, bool turnOn) {
if (!turnOn ||
allowDisplaySwitchInPrivacyMode(pi, targetImplKey) ||
(ffiModel.pi.currentDisplay == 0 &&
!bind.sessionIsMultiUiSession(sessionId: sessionId))) {
return true;
}
msgBox(sessionId, 'custom-nook-nocancel-hasclose', 'info',
'Please switch to Display 1 first', '', ffi.dialogManager);
return false;
}
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc,
String targetImplKey) {
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
final enabled = !ffiModel.viewOnly &&
(hasPrivacyModePermission || privacyModeState.isNotEmpty);
return TToggleMenu(
@@ -1084,7 +1056,16 @@ List<TToggleMenu> toolbarPrivacyMode(
onChanged: enabled
? (value) {
if (value == null) return;
if (!checkDisplayAllowedForPrivacyMode(targetImplKey, value)) {
if (!allowDisplaySwitchInPrivacyMode(pi) &&
ffiModel.pi.currentDisplay != 0 &&
ffiModel.pi.currentDisplay != kAllDisplayValue) {
msgBox(
sessionId,
'custom-nook-nocancel-hasclose',
'info',
'Please switch to Display 1 first',
'',
ffi.dialogManager);
return;
}
final option = 'privacy-mode';
@@ -1102,7 +1083,7 @@ List<TToggleMenu> toolbarPrivacyMode(
getDefaultMenu((sid, opt) async {
bind.sessionToggleOption(sessionId: sid, value: opt);
togglePrivacyModeTime = DateTime.now();
}, kPrivacyModeImplMag)
})
];
}
if (privacyModeImpls.isEmpty) {
@@ -1116,7 +1097,7 @@ List<TToggleMenu> toolbarPrivacyMode(
bind.sessionTogglePrivacyMode(
sessionId: sid, implKey: implKey, on: privacyModeState.isEmpty);
togglePrivacyModeTime = DateTime.now();
}, implKey)
})
];
} else {
final visibleImpls = hasPrivacyModePermission
@@ -1137,9 +1118,6 @@ List<TToggleMenu> toolbarPrivacyMode(
? (value) {
if (value == null) return;
if (value && !hasPrivacyModePermission) return;
if (!checkDisplayAllowedForPrivacyMode(implKey, value)) {
return;
}
togglePrivacyModeTime = DateTime.now();
bind.sessionTogglePrivacyMode(
sessionId: sessionId, implKey: implKey, on: value);

View File

@@ -29,10 +29,6 @@ const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPlatformAdditionsSupportedPrivacyModeImpl =
"supported_privacy_mode_impl";
const String kPrivacyModeImplMag = 'privacy_mode_impl_mag';
const String kPrivacyModeImplExcludeFromCapture =
'privacy_mode_impl_exclude_from_capture';
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";

View File

@@ -810,9 +810,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
}
toolbarItems.add(Obx(() {
final privacyModeState = PrivacyModeState.find(widget.id);
if ((privacyModeState.isEmpty ||
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value)) &&
if ((PrivacyModeState.find(widget.id).isEmpty ||
allowDisplaySwitchInPrivacyMode(pi)) &&
pi.displaysCount.value > 1) {
return _MonitorMenu(
id: widget.id,

View File

@@ -29,8 +29,6 @@ import 'mobile/pages/home_page.dart';
import 'mobile/pages/server_page.dart';
import 'mobile/widgets/deploy_dialog.dart';
import 'models/platform_model.dart';
import 'native/font_manager.dart'
if (dart.library.html) 'web/font_manager.dart';
import 'package:flutter_hbb/plugin/handlers.dart'
if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart';
@@ -39,15 +37,10 @@ import 'package:flutter_hbb/plugin/handlers.dart'
int? kWindowId;
WindowType? kWindowType;
late List<String> kBootArgs;
bool _cjkFontLoaded = false;
Future<void> main(List<String> args) async {
earlyAssert();
WidgetsFlutterBinding.ensureInitialized();
_cjkFontLoaded = await loadSystemCJKFonts();
if (_cjkFontLoaded) {
MyTheme.applyFontFallback([kLinuxCjkFontFamily]);
}
debugPrint("launch args: $args");
kBootArgs = List.from(args);
@@ -390,7 +383,6 @@ void _runApp(
builder: (context, child) {
child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child);
if (_cjkFontLoaded) child = _mergeCjkFallback(context, child);
return child;
},
),
@@ -541,7 +533,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
: (context, child) {
child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child);
if (_cjkFontLoaded) child = _mergeCjkFallback(context, child);
if ((isDesktop && desktopType == DesktopType.main) ||
isWebDesktop) {
child = keyListenerBuilder(context, child);
@@ -595,19 +586,6 @@ _registerEventHandler() {
}
}
/// Merges the theme's fontFamilyFallback into [DefaultTextStyle] so that
/// bare [Text] widgets (and those with inherit:true styles) also pick up the
/// CJK fallback font loaded on ARM64 Linux.
Widget _mergeCjkFallback(BuildContext context, Widget? child) {
final result = child ?? Container();
final fallback = Theme.of(context).textTheme.bodyMedium?.fontFamilyFallback;
if (fallback == null || fallback.isEmpty) return result;
return DefaultTextStyle.merge(
style: TextStyle(fontFamilyFallback: fallback),
child: result,
);
}
Widget keyListenerBuilder(BuildContext context, Widget? child) {
return RawKeyboardListener(
focusNode: FocusNode(),

View File

@@ -1220,11 +1220,7 @@ void showOptions(
if (image != null) {
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
}
final privacyModeState = PrivacyModeState.find(id);
if (pi.displays.length > 1 &&
pi.currentDisplay != kAllDisplayValue &&
(privacyModeState.isEmpty ||
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value))) {
if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
final cur = pi.currentDisplay;
final children = <Widget>[];
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
@@ -1278,6 +1274,8 @@ void showOptions(
await toolbarDisplayToggle(context, id, gFFI);
List<TToggleMenu> privacyModeList = [];
// privacy mode
final privacyModeState = PrivacyModeState.find(id);
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
privacyModeState.isNotEmpty) {
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);

View File

@@ -1,109 +0,0 @@
import 'dart:ffi' show Abi;
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// Font family name registered with [FontLoader] when a system CJK font is
/// successfully loaded on ARM64 Linux.
const kLinuxCjkFontFamily = 'SystemCJK';
const _kFontSearchPaths = [
// Debian / Ubuntu (noto-fonts / fonts-noto-cjk)
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf',
// Fedora / RHEL / Rocky (google-noto-sans-cjk-fonts)
'/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc',
// Arch Linux (noto-fonts-cjk)
'/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf',
// Generic fallback paths
'/usr/share/fonts/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/noto/NotoSansCJKsc-Regular.otf',
// WenQuanYi — commonly pre-installed on CJK-locale systems
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
'/usr/share/fonts/wqy-microhei/wqy-microhei.ttc',
'/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc',
];
/// Loads a system CJK font on ARM64 Linux into Flutter's font registry via
/// [FontLoader], working around the missing fontconfig support in the
/// flutter-elinux engine (https://github.com/flutter/flutter/issues/139293).
///
/// Returns true if a CJK font was successfully loaded; false otherwise.
/// On all other platforms this is a no-op and returns false immediately.
Future<bool> loadSystemCJKFonts() async {
if (Abi.current() != Abi.linuxArm64) return false;
final path = await _findCjkFontPath();
if (path == null) {
debugPrint('ARM64 Linux: no CJK font found; CJK text may not render');
return false;
}
try {
final loader = FontLoader(kLinuxCjkFontFamily);
final bytes = await File(path).readAsBytes();
loader.addFont(Future.value(ByteData.view(bytes.buffer, bytes.offsetInBytes, bytes.lengthInBytes)));
await loader.load();
debugPrint('ARM64 Linux: loaded CJK font from $path');
return true;
} catch (e) {
debugPrint('ARM64 Linux: failed to load CJK font: $e');
return false;
}
}
Future<String?> _findCjkFontPath() async {
// Query fc-list for each CJK script separately. Fonts present in all three
// sets (zh ∩ ja ∩ ko) are true pan-CJK fonts; prefer them so we don't
// accidentally pick a Chinese-only font that lacks Japanese kana or Korean
// hangul glyphs. fc-list is a fontconfig CLI tool available on most Linux
// systems independent of whether the Flutter engine was built with fontconfig.
final byLang = <String, Set<String>>{};
for (final lang in const ['zh', 'ja', 'ko']) {
final paths = <String>{};
try {
final r =
await Process.run('fc-list', [':lang=$lang', '--format=%{file}\n']);
if (r.exitCode == 0) {
for (final line in r.stdout.toString().split('\n')) {
final p = line.trim();
if (p.isNotEmpty && File(p).existsSync()) paths.add(p);
}
}
} catch (e) {
debugPrint('ARM64 Linux: fc-list failed for lang=$lang: $e');
}
byLang[lang] = paths;
}
final panCjk = byLang['zh']!
.intersection(byLang['ja']!)
.intersection(byLang['ko']!);
final anyCjk =
byLang.values.fold(<String>{}, (acc, s) => acc..addAll(s));
// Among candidates, prefer well-known pan-CJK font families.
String? pick(Iterable<String> pool) {
const preferred = ['notosanscjk', 'sourcehansans', 'sourcehanserif'];
for (final name in preferred) {
for (final p in pool) {
if (p.toLowerCase().contains(name)) return p;
}
}
return pool.isNotEmpty ? pool.first : null;
}
final found = pick(panCjk) ?? pick(anyCjk);
if (found != null) return found;
for (final p in _kFontSearchPaths) {
if (File(p).existsSync()) return p;
}
return null;
}

View File

@@ -1,8 +0,0 @@
/// Web stub for `native/font_manager.dart`.
///
/// The native implementation depends on `dart:io` (Process/File/Platform) to
/// load a system CJK font on ARM64 Linux, which cannot compile for the web
/// target. The web build has no such fontconfig limitation, so this is a no-op.
const kLinuxCjkFontFamily = 'SystemCJK';
Future<bool> loadSystemCJKFonts() async => false;

View File

@@ -7,7 +7,8 @@ use fuser::MountOption;
use hbb_common::{config::APP_NAME, log};
use parking_lot::Mutex;
use std::{
path::PathBuf,
io,
path::{Path, PathBuf},
sync::{mpsc::Sender, Arc},
time::Duration,
};
@@ -53,8 +54,16 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
} else {
FUSE_CONTEXT_SERVER.lock()
};
if fuse_context_lock.is_some() {
return Ok(());
if let Some(ctx) = fuse_context_lock.as_ref() {
if is_mount_point_healthy(&ctx.mount_point) {
return Ok(());
}
log::warn!(
"clipboard FUSE mount {} is disconnected, remounting",
ctx.mount_point.display()
);
let stale_context = fuse_context_lock.take();
drop(stale_context);
}
let mount_point = if is_client {
FUSE_MOUNT_POINT_CLIENT.clone()
@@ -159,35 +168,100 @@ struct FuseContext {
}
// this function must be called after the main IPC is up
fn prepare_fuse_mount_point(mount_point: &PathBuf) {
fn prepare_fuse_mount_point(mount_point: &Path) {
use std::{
fs::{self, Permissions},
os::unix::prelude::PermissionsExt,
};
fs::create_dir(mount_point).ok();
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok();
if let Some(parent) = mount_point.parent() {
if let Err(e) = fs::create_dir_all(parent) {
log::warn!("failed to create FUSE mount parent {:?}: {:?}", parent, e);
}
}
if let Err(e) = std::process::Command::new("umount")
.arg(mount_point)
.status()
{
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
unmount_fuse_mount_point(mount_point);
if let Err(e) = fs::create_dir_all(mount_point) {
log::warn!(
"failed to create FUSE mount point {:?}: {:?}",
mount_point,
e
);
}
if let Err(e) = fs::set_permissions(mount_point, Permissions::from_mode(0o777)) {
log::warn!(
"failed to set FUSE mount point permissions {:?}: {:?}",
mount_point,
e
);
}
}
fn uninit_fuse_context_(is_client: bool) {
if is_client {
let _ = FUSE_CONTEXT_CLIENT.lock().take();
} else {
let _ = FUSE_CONTEXT_SERVER.lock().take();
fn is_mount_point_healthy(mount_point: &Path) -> bool {
is_mount_point_healthy_result(std::fs::metadata(mount_point))
}
fn is_mount_point_healthy_result<T>(result: io::Result<T>) -> bool {
match result {
Ok(_) => true,
Err(e) => {
e.raw_os_error() != Some(libc::ENOTCONN) && e.kind() != io::ErrorKind::NotFound
}
}
}
fn unmount_fuse_mount_point(mount_point: &Path) {
if run_unmount_command("umount", &["-l"], mount_point) {
return;
}
if run_unmount_command("fusermount3", &["-uz"], mount_point) {
return;
}
run_unmount_command("fusermount", &["-uz"], mount_point);
}
fn run_unmount_command(program: &str, args: &[&str], mount_point: &Path) -> bool {
match std::process::Command::new(program)
.args(args)
.arg(mount_point)
.status()
{
Ok(status) if status.success() => {}
Ok(status) => {
log::debug!(
"{} {:?} exited with status {:?}",
program,
mount_point,
status.code()
);
return false;
}
Err(e) => {
log::debug!("failed to run {} for {:?}: {:?}", program, mount_point, e);
return false;
}
}
true
}
fn uninit_fuse_context_(is_client: bool) {
let mut fuse_context_lock = if is_client {
FUSE_CONTEXT_CLIENT.lock()
} else {
FUSE_CONTEXT_SERVER.lock()
};
let ctx = fuse_context_lock.take();
drop(ctx);
}
impl Drop for FuseContext {
fn drop(&mut self) {
self.session.lock().take().map(|s| s.join());
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display());
unmount_fuse_mount_point(&self.mount_point);
if let Some(session) = self.session.lock().take() {
session.join();
}
}
}
@@ -223,3 +297,30 @@ impl FuseContext {
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs, io};
#[test]
fn reports_disconnected_fuse_mount_as_unhealthy() {
let err = io::Error::from_raw_os_error(libc::ENOTCONN);
assert!(!is_mount_point_healthy_result::<()>(Err(err)));
}
#[test]
fn reports_existing_directory_mount_point_as_healthy() {
let mount_point = std::env::temp_dir().join(format!(
"rustdesk-fuse-mount-health-test-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&mount_point);
fs::create_dir(&mount_point).unwrap();
assert!(is_mount_point_healthy(&mount_point));
let _ = fs::remove_dir_all(&mount_point);
}
}

View File

@@ -47,7 +47,7 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf {
format!("{}-{}", target_arch, target_os)
}
} else if target_os == "windows" {
format!("{}-windows-static", target_arch)
"x64-windows-static".to_owned()
} else {
format!("{}-{}", target_arch, target_os)
};

View File

@@ -52,33 +52,6 @@ lazy_static::lazy_static! {
static ref MAG_BUFFER: Mutex<(bool, Vec<u8>)> = Default::default();
}
fn find_windows(cls: &str, name: &str) -> Result<Vec<HWND>> {
let name_c = CString::new(name)?;
let cls_c = if cls.is_empty() {
None
} else {
Some(CString::new(cls)?)
};
let mut hwnds = Vec::new();
unsafe {
let mut after = NULL as _;
loop {
let hwnd = FindWindowExA(
NULL as _,
after,
cls_c.as_ref().map_or(NULL as _, |c| c.as_ptr()),
name_c.as_ptr(),
);
if hwnd.is_null() {
break;
}
hwnds.push(hwnd);
after = hwnd;
}
}
Ok(hwnds)
}
pub type REFWICPixelFormatGUID = *const GUID;
pub type WICPixelFormatGUID = GUID;
@@ -274,8 +247,6 @@ pub struct CapturerMag {
rect: RECT,
width: usize,
height: usize,
excluded_window_target: Option<(String, String)>,
excluded_windows: Vec<HWND>,
}
impl Drop for CapturerMag {
@@ -290,10 +261,6 @@ impl CapturerMag {
MagInterface::new().is_ok()
}
// This captures through the legacy Windows Magnification API. Do not infer
// multi-monitor capture support from privacy overlay coverage: WebRTC also
// disables its magnifier capturer when SM_CMONITORS != 1.
// https://webrtc.googlesource.com/src/+/1845922d5a1bf9c27deeffb4a8c8daea124434c1/modules/desktop_capture/win/screen_capturer_win_magnifier.cc
pub(crate) fn new(origin: (i32, i32), width: usize, height: usize) -> Result<Self> {
unsafe {
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
@@ -338,8 +305,6 @@ impl CapturerMag {
},
width,
height,
excluded_window_target: None,
excluded_windows: Vec::new(),
};
unsafe {
@@ -471,41 +436,19 @@ impl CapturerMag {
}
pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
let mut hwnds = find_windows(cls, name)?;
hwnds.sort_unstable_by_key(|hwnd| *hwnd as usize);
self.excluded_window_target = Some((cls.to_owned(), name.to_owned()));
if hwnds.is_empty() {
self.excluded_windows.clear();
return Ok(false);
}
self.exclude_windows(&mut hwnds)?;
self.excluded_windows = hwnds;
Ok(true)
}
fn refresh_excluded_windows(&mut self) -> Result<()> {
let Some((cls, name)) = self.excluded_window_target.as_ref() else {
return Ok(());
};
let mut hwnds = find_windows(cls, name)?;
hwnds.sort_unstable_by_key(|hwnd| *hwnd as usize);
// This runs from frame() because refreshed privacy overlays get new
// HWNDs. It is only used on the legacy magnifier backend while privacy
// mode is active; if it shows up as hot-path cost, throttle this check.
// Keep the previous filter list while privacy windows are being recreated.
if hwnds.is_empty() || hwnds == self.excluded_windows {
return Ok(());
}
self.exclude_windows(&mut hwnds)?;
self.excluded_windows = hwnds;
Ok(())
}
fn exclude_windows(&mut self, hwnds: &mut [HWND]) -> Result<bool> {
let count = hwnds.len() as _;
let name_c = CString::new(name)?;
unsafe {
let mut hwnd = if cls.len() == 0 {
FindWindowExA(NULL as _, NULL as _, NULL as _, name_c.as_ptr())
} else {
let cls_c = CString::new(cls).unwrap();
FindWindowExA(NULL as _, NULL as _, cls_c.as_ptr(), name_c.as_ptr())
};
if hwnd.is_null() {
return Ok(false);
}
if let Some(set_window_filter_list_func) =
self.mag_interface.set_window_filter_list_func
{
@@ -513,15 +456,16 @@ impl CapturerMag {
== set_window_filter_list_func(
self.magnifier_window,
MW_FILTERMODE_EXCLUDE,
count,
hwnds.as_mut_ptr(),
1,
&mut hwnd,
)
{
return Err(Error::new(
ErrorKind::Other,
format!(
"Failed MagSetWindowFilterList for {} windows, error {}",
count,
"Failed MagSetWindowFilterList for cls {} name {}, error {}",
cls,
name,
Error::last_os_error()
),
));
@@ -552,7 +496,6 @@ impl CapturerMag {
}
pub(crate) fn frame(&mut self, data: &mut Vec<u8>) -> Result<()> {
self.refresh_excluded_windows()?;
Self::clear_data();
unsafe {

View File

@@ -7,10 +7,6 @@
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<Keyword>Win32Proj</Keyword>
@@ -26,12 +22,6 @@
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
@@ -40,9 +30,6 @@
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
@@ -66,28 +53,6 @@
<ModuleDefinitionFile>CustomActions.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;EXAMPLECADLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<AdditionalDependencies>msi.lib;version.lib;%(AdditionalDependencies)</AdditionalDependencies>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<ModuleDefinitionFile>CustomActions.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="Common.h" />
<ClInclude Include="framework.h" />
@@ -100,7 +65,6 @@
<ClCompile Include="FirewallRules.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="ReadConfig.cpp" />
<ClCompile Include="RemotePrinter.cpp" />

View File

@@ -3,7 +3,7 @@
<IncludeSearchPaths>
</IncludeSearchPaths>
<Configurations>Release</Configurations>
<Platforms>x64;ARM64</Platforms>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<Content Include="Includes.wxi" />

View File

@@ -10,17 +10,12 @@ EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Release|x64 = Release|x64
Release|ARM64 = Release|ARM64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|x64.ActiveCfg = Release|x64
{F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|x64.Build.0 = Release|x64
{F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|ARM64.ActiveCfg = Release|ARM64
{F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|ARM64.Build.0 = Release|ARM64
{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|x64.ActiveCfg = Release|x64
{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|x64.Build.0 = Release|x64
{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|ARM64.ActiveCfg = Release|ARM64
{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|ARM64.Build.0 = Release|ARM64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -130,18 +130,14 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
--cc=cl \
--enable-gpl \
--enable-d3d11va \
--enable-hwaccel=h264_d3d11va \
--enable-hwaccel=hevc_d3d11va \
--enable-hwaccel=h264_d3d11va2 \
--enable-hwaccel=hevc_d3d11va2 \
")
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86" OR VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
string(APPEND OPTIONS "\
--enable-cuda \
--enable-ffnvcodec \
--enable-hwaccel=h264_nvdec \
--enable-hwaccel=hevc_nvdec \
--enable-hwaccel=h264_d3d11va \
--enable-hwaccel=hevc_d3d11va \
--enable-hwaccel=h264_d3d11va2 \
--enable-hwaccel=hevc_d3d11va2 \
--enable-amf \
--enable-encoder=h264_amf \
--enable-encoder=hevc_amf \
@@ -151,7 +147,6 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
--enable-encoder=h264_qsv \
--enable-encoder=hevc_qsv \
")
endif()
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86")
set(LIB_MACHINE_ARG /machine:x86)
@@ -159,9 +154,6 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
set(LIB_MACHINE_ARG /machine:x64)
string(APPEND OPTIONS " --arch=x86_64")
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64")
set(LIB_MACHINE_ARG /machine:arm64)
string(APPEND OPTIONS " --arch=aarch64 --enable-cross-compile")
else()
message(FATAL_ERROR "Unsupported target architecture")
endif()

View File

@@ -4,14 +4,13 @@ use hbb_common::{allow_err, bail, log, ResultType};
use std::{
ffi::CString,
io::Error,
mem::size_of,
time::{Duration, Instant},
};
use winapi::{
shared::{
minwindef::{BOOL, FALSE, LPARAM, TRUE},
minwindef::FALSE,
ntdef::{HANDLE, NULL},
windef::{HDC, HMONITOR, HWND, RECT},
windef::HWND,
},
um::{
handleapi::CloseHandle,
@@ -32,13 +31,7 @@ pub(super) const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_mag";
pub const ORIGIN_PROCESS_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe";
pub const WIN_TOPMOST_INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe";
pub const INJECTED_PROCESS_EXE: &'static str = WIN_TOPMOST_INJECTED_PROCESS_EXE;
pub(super) const PRIVACY_WINDOW_CLASS: &'static str = "RustDeskPrivacyWindowClass";
pub(super) const PRIVACY_WINDOW_NAME: &'static str = "RustDeskPrivacyWindow";
const PRIVACY_WINDOW_WAIT_MILLIS: u128 = 1_000;
const PRIVACY_WINDOW_WAIT_EXTRA_MONITOR_MILLIS: u128 = 500;
const PRIVACY_WINDOW_POLL_INTERVAL_MILLIS: u64 = 100;
const WM_RUSTDESK_SHOW_WINDOWS: u32 = WM_APP + 3;
const WM_RUSTDESK_HIDE_WINDOWS: u32 = WM_APP + 4;
struct WindowHandlers {
hthread: u64,
@@ -109,17 +102,22 @@ impl PrivacyMode for PrivacyModeImpl {
);
}
let should_start_broker = self.handlers.is_default();
if should_start_broker {
log::info!("turn_on_privacy, broker not running, try start");
if self.handlers.is_default() {
log::info!("turn_on_privacy, dll not found when started, try start");
self.start()?;
std::thread::sleep(std::time::Duration::from_millis(1_000));
}
if let Err(e) = self.show_privacy_windows(conn_id, true) {
self.stop();
return Err(e);
let hwnd = wait_find_privacy_hwnd(0)?;
if hwnd.is_null() {
bail!("No privacy window created");
}
super::win_input::hook()?;
unsafe {
ShowWindow(hwnd as _, SW_SHOW);
}
self.conn_id = conn_id;
self.hwnd = hwnd as _;
Ok(true)
}
@@ -130,33 +128,27 @@ impl PrivacyMode for PrivacyModeImpl {
) -> ResultType<()> {
self.check_off_conn_id(conn_id)?;
super::win_input::unhook()?;
let hwnds = find_privacy_hwnds()?;
let hide_result = set_privacy_windows_visible(&hwnds, false);
if hide_result.is_err() {
self.stop();
unsafe {
let hwnd = wait_find_privacy_hwnd(0)?;
if !hwnd.is_null() {
ShowWindow(hwnd, SW_HIDE);
}
}
// Continue local state cleanup even after stop(); the broker has
// been torn down, so keeping conn_id/hwnd would leave stale state.
if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID {
// Only publish the off state after the hide message was posted.
// Otherwise the peer may receive a success-like state and then a
// failed turn-off response for the same request.
if hide_result.is_ok() {
if let Some(state) = state {
allow_err!(super::set_privacy_mode_state(
conn_id,
state,
PRIVACY_MODE_IMPL.to_string(),
1_000
));
}
if let Some(state) = state {
allow_err!(super::set_privacy_mode_state(
conn_id,
state,
PRIVACY_MODE_IMPL.to_string(),
1_000
));
}
self.conn_id = INVALID_PRIVACY_MODE_CONN_ID.to_owned();
self.hwnd = 0;
}
hide_result.map(|_| ())
Ok(())
}
#[inline]
@@ -214,7 +206,8 @@ impl PrivacyModeImpl {
);
}
if wait_find_privacy_hwnds(PRIVACY_WINDOW_WAIT_MILLIS).is_ok() {
let hwnd = wait_find_privacy_hwnd(1_000)?;
if !hwnd.is_null() {
log::info!("Privacy window is ready");
return Ok(());
}
@@ -283,19 +276,14 @@ impl PrivacyModeImpl {
);
};
if let Err(e) = inject_dll(
inject_dll(
proc_info.hProcess,
proc_info.hThread,
dll_file.to_string_lossy().as_ref(),
) {
TerminateProcess(proc_info.hProcess, 0);
CloseHandle(proc_info.hThread);
CloseHandle(proc_info.hProcess);
return Err(e);
}
)?;
if 0xffffffff == ResumeThread(proc_info.hThread) {
TerminateProcess(proc_info.hProcess, 0);
// CloseHandle
CloseHandle(proc_info.hThread);
CloseHandle(proc_info.hProcess);
@@ -308,9 +296,9 @@ impl PrivacyModeImpl {
self.handlers.hthread = proc_info.hThread as _;
self.handlers.hprocess = proc_info.hProcess as _;
if let Err(e) = wait_find_privacy_hwnds(PRIVACY_WINDOW_WAIT_MILLIS) {
self.handlers.reset();
return Err(e);
let hwnd = wait_find_privacy_hwnd(1_000)?;
if hwnd.is_null() {
bail!("Failed to get hwnd after started");
}
}
@@ -321,49 +309,6 @@ impl PrivacyModeImpl {
pub fn stop(&mut self) {
self.handlers.reset();
}
fn show_privacy_windows(&mut self, conn_id: i32, hook_input: bool) -> ResultType<()> {
let hwnds = wait_find_privacy_hwnds(PRIVACY_WINDOW_WAIT_MILLIS)?;
if hwnds.is_empty() {
bail!("No privacy window created");
}
if hook_input {
super::win_input::hook()?;
}
match set_privacy_windows_visible(&hwnds, true) {
Ok(_) => {
let visible_hwnds =
match wait_find_visible_privacy_hwnds(PRIVACY_WINDOW_WAIT_MILLIS) {
Ok(hwnds) => hwnds,
Err(e) => {
allow_err!(set_privacy_windows_visible(&hwnds, false));
if hook_input {
allow_err!(super::win_input::unhook());
}
return Err(e);
}
};
let Some(hwnd) = visible_hwnds.first() else {
allow_err!(set_privacy_windows_visible(&hwnds, false));
if hook_input {
allow_err!(super::win_input::unhook());
}
bail!("No visible privacy window created");
};
self.conn_id = conn_id;
self.hwnd = *hwnd as _;
Ok(())
}
Err(e) => {
allow_err!(set_privacy_windows_visible(&hwnds, false));
if hook_input {
allow_err!(super::win_input::unhook());
}
Err(e)
}
}
}
}
impl Drop for PrivacyModeImpl {
@@ -418,217 +363,21 @@ unsafe fn inject_dll<'a>(hproc: HANDLE, hthread: HANDLE, dll_file: &'a str) -> R
Ok(())
}
fn wait_find_privacy_hwnds(msecs: u128) -> ResultType<Vec<HWND>> {
wait_find_privacy_hwnds_impl(msecs, false)
}
fn wait_find_visible_privacy_hwnds(msecs: u128) -> ResultType<Vec<HWND>> {
wait_find_privacy_hwnds_impl(msecs, true)
}
fn privacy_window_wait_millis(base_millis: u128, monitor_count: usize) -> u128 {
if base_millis == 0 {
return 0;
}
// Privacy Mode 1 creates one overlay per monitor. Keep the single-monitor
// wait as the base and add time for each extra overlay before coverage
// verification times out.
base_millis
+ (monitor_count.saturating_sub(1) as u128) * PRIVACY_WINDOW_WAIT_EXTRA_MONITOR_MILLIS
}
fn wait_find_privacy_hwnds_impl(msecs: u128, require_visible: bool) -> ResultType<Vec<HWND>> {
// This verifies initial turn-on coverage. If displays change during this
// short poll window, the DLL refreshes overlays asynchronously, while this
// check may still time out against the geometry sampled here.
let monitor_rects = get_monitor_rects()?;
if monitor_rects.is_empty() {
bail!("No privacy monitor found");
}
let msecs = privacy_window_wait_millis(msecs, monitor_rects.len());
pub(super) fn wait_find_privacy_hwnd(msecs: u128) -> ResultType<HWND> {
let tm_begin = Instant::now();
let wndname = CString::new(PRIVACY_WINDOW_NAME)?;
loop {
let hwnds = find_privacy_hwnds()?;
let visible_hwnds = if require_visible {
filter_visible_hwnds(&hwnds)
} else {
Vec::new()
};
let covered_hwnds = if require_visible {
visible_hwnds.as_slice()
} else {
hwnds.as_slice()
};
let covered = count_covered_monitors(covered_hwnds, &monitor_rects);
if covered == monitor_rects.len() {
return Ok(if require_visible {
visible_hwnds
} else {
hwnds
});
unsafe {
let hwnd = FindWindowA(NULL as _, wndname.as_ptr() as _);
if !hwnd.is_null() {
return Ok(hwnd);
}
}
if msecs == 0 || tm_begin.elapsed().as_millis() > msecs {
let visible = if require_visible { "visible " } else { "" };
bail!(
"Expected {}privacy windows to cover {} monitors, covered {}, found {}",
visible,
monitor_rects.len(),
covered,
hwnds.len(),
);
return Ok(NULL as _);
}
std::thread::sleep(Duration::from_millis(PRIVACY_WINDOW_POLL_INTERVAL_MILLIS));
std::thread::sleep(Duration::from_millis(100));
}
}
fn find_privacy_hwnds() -> ResultType<Vec<HWND>> {
let class_name = CString::new(PRIVACY_WINDOW_CLASS)?;
let wndname = CString::new(PRIVACY_WINDOW_NAME)?;
let mut hwnds = Vec::new();
unsafe {
let mut after = NULL as _;
loop {
let hwnd = FindWindowExA(
NULL as _,
after,
class_name.as_ptr() as _,
wndname.as_ptr() as _,
);
if hwnd.is_null() {
break;
}
hwnds.push(hwnd);
after = hwnd;
}
}
Ok(hwnds)
}
fn filter_visible_hwnds(hwnds: &[HWND]) -> Vec<HWND> {
hwnds
.iter()
.copied()
.filter(|hwnd| unsafe { FALSE != IsWindowVisible(*hwnd) })
.collect()
}
fn set_privacy_windows_visible(hwnds: &[HWND], show: bool) -> ResultType<usize> {
if hwnds.is_empty() {
return Ok(0);
};
let message = if show {
WM_RUSTDESK_SHOW_WINDOWS
} else {
WM_RUSTDESK_HIDE_WINDOWS
};
let mut posted = 0;
let mut first_error = None;
for &hwnd in hwnds {
unsafe {
if FALSE == PostMessageA(hwnd, message, 0, 0) {
if first_error.is_none() {
first_error = Some(Error::last_os_error());
}
} else {
posted += 1;
}
}
}
if let Some(error) = first_error {
bail!(
"Failed to post privacy window visibility message to all privacy windows, posted {}/{}, first error {}",
posted,
hwnds.len(),
error,
);
}
Ok(posted)
}
fn get_monitor_rects() -> ResultType<Vec<RECT>> {
let mut rects = Vec::new();
unsafe {
if FALSE
== EnumDisplayMonitors(
NULL as _,
NULL as _,
Some(enum_monitor_rect_proc),
&mut rects as *mut Vec<RECT> as LPARAM,
)
{
bail!(
"Failed EnumDisplayMonitors, error {}",
Error::last_os_error()
);
}
}
Ok(rects)
}
unsafe extern "system" fn enum_monitor_rect_proc(
hmon: HMONITOR,
_hdc: HDC,
_rect: *mut RECT,
lparam: LPARAM,
) -> BOOL {
let rects = &mut *(lparam as *mut Vec<RECT>);
let mut monitor_info = MONITORINFO {
cbSize: size_of::<MONITORINFO>() as _,
rcMonitor: RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
rcWork: RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
},
dwFlags: 0,
};
if FALSE == GetMonitorInfoA(hmon, &mut monitor_info) {
return FALSE;
}
rects.push(monitor_info.rcMonitor);
TRUE
}
fn count_covered_monitors(hwnds: &[HWND], monitor_rects: &[RECT]) -> usize {
let mut covered = 0;
for monitor_rect in monitor_rects {
for hwnd in hwnds {
let mut window_rect = RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
unsafe {
if FALSE == GetWindowRect(*hwnd, &mut window_rect) {
log::warn!(
"Failed GetWindowRect for privacy window, error {}",
Error::last_os_error()
);
continue;
}
}
if rect_covers(&window_rect, monitor_rect) {
covered += 1;
break;
}
}
}
covered
}
fn rect_covers(window_rect: &RECT, monitor_rect: &RECT) -> bool {
window_rect.left <= monitor_rect.left
&& window_rect.top <= monitor_rect.top
&& window_rect.right >= monitor_rect.right
&& window_rect.bottom >= monitor_rect.bottom
}

View File

@@ -272,10 +272,6 @@ fn create_capturer(
if privacy_mode_id > 0 {
#[cfg(windows)]
{
// Windows Mode 1 can cover every local monitor with overlay windows,
// but the legacy magnifier capture backend is still single-monitor
// constrained. Keep display-switch gating aligned with that backend
// limit, not just the overlay coverage.
if let Some(c1) = crate::privacy_mode::win_mag::create_capturer(
privacy_mode_id,
display.origin(),

View File

@@ -20,11 +20,6 @@
"name": "libjpeg-turbo",
"host": false
},
{
"name": "libsodium",
"host": false,
"platform": "windows & arm64"
},
{
"name": "oboe",
"platform": "android"
@@ -69,15 +64,15 @@
"features": [
{
"name": "amf",
"platform": "(((windows & !arm) | linux) & static)"
"platform": "((windows | linux) & static)"
},
{
"name": "nvcodec",
"platform": "(((windows & !arm) | linux) & static)"
"platform": "((windows | linux) & static)"
},
{
"name": "qsv",
"platform": "(windows & !arm & static)"
"platform": "(windows & static)"
}
],
"platform": "((windows | (linux & !arm32) | osx) & static)"